karajan-code 1.27.1 → 1.28.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/package.json +1 -1
- package/src/cli.js +13 -0
- package/src/commands/audit.js +99 -0
- package/src/config.js +1 -1
- package/src/mcp/server-handlers.js +54 -1
- package/src/mcp/tools.js +12 -0
- package/src/prompts/audit.js +210 -0
- package/src/roles/audit-role.js +105 -0
- package/src/roles/index.js +1 -0
- package/src/sonar/scanner.js +4 -0
- package/templates/roles/audit.md +68 -0
- package/templates/skills/kj-audit.md +63 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { discoverCommand } from "./commands/discover.js";
|
|
|
22
22
|
import { triageCommand } from "./commands/triage.js";
|
|
23
23
|
import { researcherCommand } from "./commands/researcher.js";
|
|
24
24
|
import { architectCommand } from "./commands/architect.js";
|
|
25
|
+
import { auditCommand } from "./commands/audit.js";
|
|
25
26
|
|
|
26
27
|
const PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
27
28
|
const PKG_VERSION = JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
|
|
@@ -253,6 +254,18 @@ program
|
|
|
253
254
|
});
|
|
254
255
|
});
|
|
255
256
|
|
|
257
|
+
program
|
|
258
|
+
.command("audit")
|
|
259
|
+
.description("Analyze codebase health (read-only)")
|
|
260
|
+
.argument("[task]")
|
|
261
|
+
.option("--dimensions <list>", "Comma-separated: security,quality,performance,architecture,testing", "all")
|
|
262
|
+
.option("--json", "Output raw JSON")
|
|
263
|
+
.action(async (task, flags) => {
|
|
264
|
+
await withConfig("audit", flags, async ({ config, logger }) => {
|
|
265
|
+
await auditCommand({ task: task || "Analyze the full codebase", config, logger, dimensions: flags.dimensions, json: flags.json });
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
256
269
|
program
|
|
257
270
|
.command("resume")
|
|
258
271
|
.description("Resume a paused session")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createAgent } from "../agents/index.js";
|
|
2
|
+
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
3
|
+
import { resolveRole } from "../config.js";
|
|
4
|
+
import { buildAuditPrompt, parseAuditOutput, AUDIT_DIMENSIONS } from "../prompts/audit.js";
|
|
5
|
+
|
|
6
|
+
function formatFindings(findings) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
for (const f of findings) {
|
|
9
|
+
const loc = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : "";
|
|
10
|
+
const rule = f.rule ? ` [${f.rule}]` : "";
|
|
11
|
+
lines.push(` - [${f.severity.toUpperCase()}] ${loc}${rule}`);
|
|
12
|
+
lines.push(` ${f.description}`);
|
|
13
|
+
if (f.recommendation) lines.push(` Fix: ${f.recommendation}`);
|
|
14
|
+
}
|
|
15
|
+
return lines;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatDimension(name, dim) {
|
|
19
|
+
const lines = [];
|
|
20
|
+
lines.push(`### ${name} — Score: ${dim.score}`);
|
|
21
|
+
if (dim.findings.length === 0) {
|
|
22
|
+
lines.push(" No issues found.");
|
|
23
|
+
} else {
|
|
24
|
+
lines.push(...formatFindings(dim.findings));
|
|
25
|
+
}
|
|
26
|
+
lines.push("");
|
|
27
|
+
return lines;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatRecommendations(recs) {
|
|
31
|
+
const lines = ["## Top Recommendations", ""];
|
|
32
|
+
for (const r of recs) {
|
|
33
|
+
lines.push(`${r.priority}. [${r.dimension}] ${r.action} (impact: ${r.impact}, effort: ${r.effort})`);
|
|
34
|
+
}
|
|
35
|
+
lines.push("");
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DIMENSION_LABELS = {
|
|
40
|
+
security: "Security",
|
|
41
|
+
codeQuality: "Code Quality",
|
|
42
|
+
performance: "Performance",
|
|
43
|
+
architecture: "Architecture",
|
|
44
|
+
testing: "Testing"
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function formatAudit(parsed) {
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push("## Codebase Health Report");
|
|
50
|
+
lines.push(`**Overall Health:** ${parsed.summary.overallHealth}`);
|
|
51
|
+
lines.push(`**Total Findings:** ${parsed.summary.totalFindings} (${parsed.summary.critical} critical, ${parsed.summary.high} high, ${parsed.summary.medium} medium, ${parsed.summary.low} low)`);
|
|
52
|
+
lines.push("");
|
|
53
|
+
|
|
54
|
+
for (const dim of AUDIT_DIMENSIONS) {
|
|
55
|
+
if (parsed.dimensions[dim]) {
|
|
56
|
+
lines.push(...formatDimension(DIMENSION_LABELS[dim] || dim, parsed.dimensions[dim]));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (parsed.topRecommendations?.length) {
|
|
61
|
+
lines.push(...formatRecommendations(parsed.topRecommendations));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (parsed.textSummary) lines.push(`---\n${parsed.textSummary}`);
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function auditCommand({ task, config, logger, dimensions, json }) {
|
|
69
|
+
const auditRole = resolveRole(config, "audit");
|
|
70
|
+
await assertAgentsAvailable([auditRole.provider]);
|
|
71
|
+
logger.info(`Audit (${auditRole.provider}) starting...`);
|
|
72
|
+
|
|
73
|
+
const agent = createAgent(auditRole.provider, config, logger);
|
|
74
|
+
const dimList = dimensions && dimensions !== "all"
|
|
75
|
+
? dimensions.split(",").map(d => d.trim().toLowerCase()).map(d => d === "quality" ? "codeQuality" : d).filter(d => AUDIT_DIMENSIONS.includes(d))
|
|
76
|
+
: null;
|
|
77
|
+
|
|
78
|
+
const prompt = buildAuditPrompt({ task: task || "Analyze the full codebase", dimensions: dimList });
|
|
79
|
+
const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
|
|
80
|
+
const result = await agent.runTask({ prompt, onOutput, role: "audit" });
|
|
81
|
+
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
throw new Error(result.error || result.output || "Audit failed");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parsed = parseAuditOutput(result.output);
|
|
87
|
+
|
|
88
|
+
if (json) {
|
|
89
|
+
console.log(JSON.stringify(parsed || result.output, null, 2));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (parsed?.summary) {
|
|
94
|
+
console.log(formatAudit(parsed));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(result.output);
|
|
97
|
+
}
|
|
98
|
+
logger.info("Audit completed.");
|
|
99
|
+
}
|
package/src/config.js
CHANGED
|
@@ -414,7 +414,7 @@ export function resolveRole(config, role) {
|
|
|
414
414
|
let provider = roleConfig.provider ?? null;
|
|
415
415
|
if (!provider && role === "coder") provider = legacyCoder;
|
|
416
416
|
if (!provider && role === "reviewer") provider = legacyReviewer;
|
|
417
|
-
if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect")) {
|
|
417
|
+
if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect" || role === "audit")) {
|
|
418
418
|
provider = roles.coder?.provider || legacyCoder;
|
|
419
419
|
}
|
|
420
420
|
|
|
@@ -663,6 +663,54 @@ export async function handleResearcherDirect(a, server, extra) {
|
|
|
663
663
|
return { ok: true, ...result.result, summary: result.summary };
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
+
export async function handleAuditDirect(a, server, extra) {
|
|
667
|
+
const config = await buildConfig(a, "audit");
|
|
668
|
+
const logger = createLogger(config.output.log_level, "mcp");
|
|
669
|
+
|
|
670
|
+
const auditRole = resolveRole(config, "audit");
|
|
671
|
+
await assertAgentsAvailable([auditRole.provider]);
|
|
672
|
+
|
|
673
|
+
const projectDir = await resolveProjectDir(server, a.projectDir);
|
|
674
|
+
const runLog = createRunLog(projectDir);
|
|
675
|
+
runLog.logText(`[kj_audit] started — dimensions=${a.dimensions || "all"}`);
|
|
676
|
+
const emitter = buildDirectEmitter(server, runLog, extra);
|
|
677
|
+
const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
|
|
678
|
+
const onOutput = ({ stream, line }) => {
|
|
679
|
+
emitter.emit("progress", { type: "agent:output", stage: "audit", message: line, detail: { stream, agent: auditRole.provider } });
|
|
680
|
+
};
|
|
681
|
+
const stallDetector = createStallDetector({
|
|
682
|
+
onOutput, emitter, eventBase, stage: "audit", provider: auditRole.provider
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const { AuditRole } = await import("../roles/audit-role.js");
|
|
686
|
+
const audit = new AuditRole({ config, logger, emitter });
|
|
687
|
+
await audit.init({ task: a.task || "Analyze the full codebase" });
|
|
688
|
+
|
|
689
|
+
sendTrackerLog(server, "audit", "running", auditRole.provider);
|
|
690
|
+
runLog.logText(`[audit] agent launched, waiting for response...`);
|
|
691
|
+
let result;
|
|
692
|
+
try {
|
|
693
|
+
result = await audit.run({
|
|
694
|
+
task: a.task || "Analyze the full codebase",
|
|
695
|
+
dimensions: a.dimensions || null,
|
|
696
|
+
onOutput: stallDetector.onOutput
|
|
697
|
+
});
|
|
698
|
+
} finally {
|
|
699
|
+
stallDetector.stop();
|
|
700
|
+
const stats = stallDetector.stats();
|
|
701
|
+
runLog.logText(`[audit] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
|
|
702
|
+
runLog.close();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (!result.ok) {
|
|
706
|
+
sendTrackerLog(server, "audit", "failed");
|
|
707
|
+
throw new Error(result.result?.error || result.summary || "Audit failed");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
sendTrackerLog(server, "audit", "done");
|
|
711
|
+
return { ok: true, ...result.result, summary: result.summary };
|
|
712
|
+
}
|
|
713
|
+
|
|
666
714
|
export async function handleArchitectDirect(a, server, extra) {
|
|
667
715
|
const config = await buildConfig(a, "architect");
|
|
668
716
|
const logger = createLogger(config.output.log_level, "mcp");
|
|
@@ -955,6 +1003,10 @@ async function handleArchitect(a, server, extra) {
|
|
|
955
1003
|
return handleArchitectDirect(a, server, extra);
|
|
956
1004
|
}
|
|
957
1005
|
|
|
1006
|
+
async function handleAudit(a, server, extra) {
|
|
1007
|
+
return handleAuditDirect(a, server, extra);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
958
1010
|
/* ── Handler dispatch map ─────────────────────────────────────────── */
|
|
959
1011
|
|
|
960
1012
|
const toolHandlers = {
|
|
@@ -975,7 +1027,8 @@ const toolHandlers = {
|
|
|
975
1027
|
kj_discover: (a, server, extra) => handleDiscover(a, server, extra),
|
|
976
1028
|
kj_triage: (a, server, extra) => handleTriage(a, server, extra),
|
|
977
1029
|
kj_researcher: (a, server, extra) => handleResearcher(a, server, extra),
|
|
978
|
-
kj_architect: (a, server, extra) => handleArchitect(a, server, extra)
|
|
1030
|
+
kj_architect: (a, server, extra) => handleArchitect(a, server, extra),
|
|
1031
|
+
kj_audit: (a, server, extra) => handleAudit(a, server, extra)
|
|
979
1032
|
};
|
|
980
1033
|
|
|
981
1034
|
export async function handleToolCall(name, args, server, extra) {
|
package/src/mcp/tools.js
CHANGED
|
@@ -291,5 +291,17 @@ export const tools = [
|
|
|
291
291
|
kjHome: { type: "string" }
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "kj_audit",
|
|
297
|
+
description: "Analyze codebase health without modifying files. Returns findings across security, code quality, performance, architecture, and testing dimensions.",
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: "object",
|
|
300
|
+
properties: {
|
|
301
|
+
projectDir: { type: "string", description: "Absolute path to the project to audit" },
|
|
302
|
+
dimensions: { type: "string", description: "Comma-separated dimensions to analyze: security,codeQuality,performance,architecture,testing (default: all)" },
|
|
303
|
+
kjHome: { type: "string" }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
294
306
|
}
|
|
295
307
|
];
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const SUBAGENT_PREAMBLE = [
|
|
2
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
3
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
4
|
+
"Do NOT use any MCP tools. Focus only on auditing the codebase for health, quality, and risks.",
|
|
5
|
+
"DO NOT modify any files — this is a read-only analysis. Only use: Read, Grep, Glob, Bash (for analysis commands like wc, find, git log, du, npm ls)."
|
|
6
|
+
].join(" ");
|
|
7
|
+
|
|
8
|
+
export const AUDIT_DIMENSIONS = ["security", "codeQuality", "performance", "architecture", "testing"];
|
|
9
|
+
|
|
10
|
+
const VALID_HEALTH = new Set(["good", "fair", "poor", "critical"]);
|
|
11
|
+
const VALID_SCORES = new Set(["A", "B", "C", "D", "F"]);
|
|
12
|
+
const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
|
|
13
|
+
const VALID_IMPACT = new Set(["high", "medium", "low"]);
|
|
14
|
+
|
|
15
|
+
export function buildAuditPrompt({ task, instructions, dimensions = null, context = null }) {
|
|
16
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
17
|
+
|
|
18
|
+
if (instructions) {
|
|
19
|
+
sections.push(instructions);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
sections.push(
|
|
23
|
+
"You are a codebase auditor for Karajan Code, a multi-agent coding orchestrator.",
|
|
24
|
+
"Analyze the project across multiple dimensions and produce a comprehensive health report.",
|
|
25
|
+
"DO NOT modify any files. This is a READ-ONLY analysis."
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const activeDimensions = dimensions
|
|
29
|
+
? dimensions.filter(d => AUDIT_DIMENSIONS.includes(d))
|
|
30
|
+
: AUDIT_DIMENSIONS;
|
|
31
|
+
|
|
32
|
+
if (activeDimensions.includes("security")) {
|
|
33
|
+
sections.push(
|
|
34
|
+
"## Security Analysis",
|
|
35
|
+
[
|
|
36
|
+
"- Hardcoded secrets, API keys, tokens in source code",
|
|
37
|
+
"- SQL/NoSQL injection vectors",
|
|
38
|
+
"- XSS vulnerabilities (innerHTML, dangerouslySetInnerHTML, eval)",
|
|
39
|
+
"- Command injection (exec, spawn with user input)",
|
|
40
|
+
"- Insecure dependencies (check package.json for known vulnerable packages)",
|
|
41
|
+
"- Missing input validation at system boundaries",
|
|
42
|
+
"- Authentication/authorization gaps"
|
|
43
|
+
].join("\n")
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (activeDimensions.includes("codeQuality")) {
|
|
48
|
+
sections.push(
|
|
49
|
+
"## Code Quality Analysis (SOLID, DRY, KISS, YAGNI)",
|
|
50
|
+
[
|
|
51
|
+
"- Functions/methods longer than 50 lines",
|
|
52
|
+
"- Files longer than 500 lines",
|
|
53
|
+
"- Duplicated code blocks (same logic in multiple places)",
|
|
54
|
+
"- God classes/modules (too many responsibilities)",
|
|
55
|
+
"- Deep nesting (>4 levels)",
|
|
56
|
+
"- Dead code (unused exports, unreachable branches)",
|
|
57
|
+
"- Missing error handling (uncaught promises, empty catches)",
|
|
58
|
+
"- Over-engineering (abstractions for single use)"
|
|
59
|
+
].join("\n")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (activeDimensions.includes("performance")) {
|
|
64
|
+
sections.push(
|
|
65
|
+
"## Performance Analysis",
|
|
66
|
+
[
|
|
67
|
+
"- N+1 query patterns",
|
|
68
|
+
"- Synchronous file I/O in request handlers",
|
|
69
|
+
"- Missing pagination on list endpoints",
|
|
70
|
+
"- Large bundle imports (importing entire libraries for one function)",
|
|
71
|
+
"- Missing lazy loading",
|
|
72
|
+
"- Expensive operations in loops",
|
|
73
|
+
"- Missing caching opportunities"
|
|
74
|
+
].join("\n")
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (activeDimensions.includes("architecture")) {
|
|
79
|
+
sections.push(
|
|
80
|
+
"## Architecture Analysis",
|
|
81
|
+
[
|
|
82
|
+
"- Circular dependencies",
|
|
83
|
+
"- Layer violations (UI importing from data layer directly)",
|
|
84
|
+
"- Coupling between modules (shared mutable state)",
|
|
85
|
+
"- Missing dependency injection",
|
|
86
|
+
"- Inconsistent patterns across the codebase",
|
|
87
|
+
"- Missing or outdated documentation",
|
|
88
|
+
"- Configuration scattered vs centralized"
|
|
89
|
+
].join("\n")
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (activeDimensions.includes("testing")) {
|
|
94
|
+
sections.push(
|
|
95
|
+
"## Testing Analysis",
|
|
96
|
+
[
|
|
97
|
+
"- Test coverage gaps (source files without corresponding tests)",
|
|
98
|
+
"- Test quality (assertions per test, meaningful test names)",
|
|
99
|
+
"- Missing edge case coverage",
|
|
100
|
+
"- Test isolation (shared state between tests)",
|
|
101
|
+
"- Flaky test indicators (timeouts, sleep, retries)"
|
|
102
|
+
].join("\n")
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
sections.push(
|
|
107
|
+
"Return a single valid JSON object and nothing else.",
|
|
108
|
+
'JSON schema: {"ok":true,"result":{"summary":{"overallHealth":"good|fair|poor|critical","totalFindings":number,"critical":number,"high":number,"medium":number,"low":number},"dimensions":{"security":{"score":"A|B|C|D|F","findings":[]},"codeQuality":{"score":"A|B|C|D|F","findings":[]},"performance":{"score":"A|B|C|D|F","findings":[]},"architecture":{"score":"A|B|C|D|F","findings":[]},"testing":{"score":"A|B|C|D|F","findings":[]}},"topRecommendations":[{"priority":number,"dimension":string,"action":string,"impact":"high|medium|low","effort":"high|medium|low"}]},"summary":string}',
|
|
109
|
+
'Each finding: {"severity":"critical|high|medium|low","file":string,"line":number,"rule":string,"description":string,"recommendation":string}',
|
|
110
|
+
`Only include dimensions you were asked to analyze: ${activeDimensions.join(", ")}`
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (context) {
|
|
114
|
+
sections.push(`## Context\n${context}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
sections.push(`## Task\n${task}`);
|
|
118
|
+
|
|
119
|
+
return sections.join("\n\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseFinding(raw) {
|
|
123
|
+
if (!raw || typeof raw !== "object") return null;
|
|
124
|
+
const severity = String(raw.severity || "").toLowerCase();
|
|
125
|
+
if (!VALID_SEVERITIES.has(severity)) return null;
|
|
126
|
+
return {
|
|
127
|
+
severity,
|
|
128
|
+
file: raw.file || "",
|
|
129
|
+
line: typeof raw.line === "number" ? raw.line : 0,
|
|
130
|
+
rule: raw.rule || "",
|
|
131
|
+
description: raw.description || "",
|
|
132
|
+
recommendation: raw.recommendation || ""
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseDimension(raw) {
|
|
137
|
+
if (!raw || typeof raw !== "object") return { score: "F", findings: [] };
|
|
138
|
+
const score = VALID_SCORES.has(raw.score) ? raw.score : "F";
|
|
139
|
+
const findings = (Array.isArray(raw.findings) ? raw.findings : [])
|
|
140
|
+
.map(parseFinding)
|
|
141
|
+
.filter(Boolean);
|
|
142
|
+
return { score, findings };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseRecommendation(raw) {
|
|
146
|
+
if (!raw || typeof raw !== "object") return null;
|
|
147
|
+
return {
|
|
148
|
+
priority: typeof raw.priority === "number" ? raw.priority : 99,
|
|
149
|
+
dimension: raw.dimension || "",
|
|
150
|
+
action: raw.action || "",
|
|
151
|
+
impact: VALID_IMPACT.has(raw.impact) ? raw.impact : "medium",
|
|
152
|
+
effort: VALID_IMPACT.has(raw.effort) ? raw.effort : "medium"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function parseAuditOutput(raw) {
|
|
157
|
+
const text = raw?.trim() || "";
|
|
158
|
+
const jsonMatch = /\{[\s\S]*\}/.exec(text);
|
|
159
|
+
if (!jsonMatch) return null;
|
|
160
|
+
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Handle both wrapped (result.summary) and flat structures
|
|
169
|
+
const resultObj = parsed.result || parsed;
|
|
170
|
+
const summaryObj = resultObj.summary || {};
|
|
171
|
+
|
|
172
|
+
const overallHealth = VALID_HEALTH.has(summaryObj.overallHealth)
|
|
173
|
+
? summaryObj.overallHealth
|
|
174
|
+
: "poor";
|
|
175
|
+
|
|
176
|
+
const dims = resultObj.dimensions || {};
|
|
177
|
+
const dimensions = {};
|
|
178
|
+
for (const d of AUDIT_DIMENSIONS) {
|
|
179
|
+
dimensions[d] = parseDimension(dims[d]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const totalFindings = Object.values(dimensions)
|
|
183
|
+
.reduce((sum, d) => sum + d.findings.length, 0);
|
|
184
|
+
|
|
185
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
186
|
+
for (const d of Object.values(dimensions)) {
|
|
187
|
+
for (const f of d.findings) {
|
|
188
|
+
bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const topRecommendations = (Array.isArray(resultObj.topRecommendations) ? resultObj.topRecommendations : [])
|
|
193
|
+
.map(parseRecommendation)
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.sort((a, b) => a.priority - b.priority);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
summary: {
|
|
199
|
+
overallHealth,
|
|
200
|
+
totalFindings,
|
|
201
|
+
critical: bySeverity.critical,
|
|
202
|
+
high: bySeverity.high,
|
|
203
|
+
medium: bySeverity.medium,
|
|
204
|
+
low: bySeverity.low
|
|
205
|
+
},
|
|
206
|
+
dimensions,
|
|
207
|
+
topRecommendations,
|
|
208
|
+
textSummary: parsed.summary || resultObj.textSummary || ""
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
import { buildAuditPrompt, parseAuditOutput, AUDIT_DIMENSIONS } from "../prompts/audit.js";
|
|
4
|
+
|
|
5
|
+
function resolveProvider(config) {
|
|
6
|
+
return (
|
|
7
|
+
config?.roles?.audit?.provider ||
|
|
8
|
+
config?.roles?.coder?.provider ||
|
|
9
|
+
"claude"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildSummary(parsed) {
|
|
14
|
+
const { summary } = parsed;
|
|
15
|
+
const parts = [];
|
|
16
|
+
if (summary.critical > 0) parts.push(`${summary.critical} critical`);
|
|
17
|
+
if (summary.high > 0) parts.push(`${summary.high} high`);
|
|
18
|
+
if (summary.medium > 0) parts.push(`${summary.medium} medium`);
|
|
19
|
+
if (summary.low > 0) parts.push(`${summary.low} low`);
|
|
20
|
+
|
|
21
|
+
const findingsStr = parts.length > 0 ? parts.join(", ") : "no issues";
|
|
22
|
+
return `Overall health: ${summary.overallHealth}. ${summary.totalFindings} findings (${findingsStr})`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveInput(input, context) {
|
|
26
|
+
if (typeof input === "string") {
|
|
27
|
+
return { task: input, onOutput: null, dimensions: null, context: null };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
task: input?.task || context?.task || "",
|
|
31
|
+
onOutput: input?.onOutput || null,
|
|
32
|
+
dimensions: input?.dimensions || null,
|
|
33
|
+
context: input?.context || null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseDimensions(dimensionsStr) {
|
|
38
|
+
if (!dimensionsStr || dimensionsStr === "all") return null;
|
|
39
|
+
const requested = dimensionsStr.split(",").map(d => d.trim().toLowerCase());
|
|
40
|
+
// Map "quality" shorthand to "codeQuality"
|
|
41
|
+
const mapped = requested.map(d => d === "quality" ? "codeQuality" : d);
|
|
42
|
+
const valid = mapped.filter(d => AUDIT_DIMENSIONS.includes(d));
|
|
43
|
+
return valid.length > 0 ? valid : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class AuditRole extends BaseRole {
|
|
47
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
48
|
+
super({ name: "audit", config, logger, emitter });
|
|
49
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async execute(input) {
|
|
53
|
+
const { task, onOutput, dimensions: rawDimensions, context } = resolveInput(input, this.context);
|
|
54
|
+
const dimensions = typeof rawDimensions === "string"
|
|
55
|
+
? parseDimensions(rawDimensions)
|
|
56
|
+
: rawDimensions;
|
|
57
|
+
const provider = resolveProvider(this.config);
|
|
58
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
59
|
+
|
|
60
|
+
const prompt = buildAuditPrompt({ task, instructions: this.instructions, dimensions, context });
|
|
61
|
+
const runArgs = { prompt, role: "audit" };
|
|
62
|
+
if (onOutput) runArgs.onOutput = onOutput;
|
|
63
|
+
const result = await agent.runTask(runArgs);
|
|
64
|
+
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
result: { error: result.error || result.output || "Audit failed", provider },
|
|
69
|
+
summary: `Audit failed: ${result.error || "unknown error"}`,
|
|
70
|
+
usage: result.usage
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const parsed = parseAuditOutput(result.output);
|
|
76
|
+
if (!parsed) {
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
result: { raw: result.output, provider },
|
|
80
|
+
summary: "Audit complete (unstructured output)",
|
|
81
|
+
usage: result.usage
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
result: {
|
|
88
|
+
summary: parsed.summary,
|
|
89
|
+
dimensions: parsed.dimensions,
|
|
90
|
+
topRecommendations: parsed.topRecommendations,
|
|
91
|
+
provider
|
|
92
|
+
},
|
|
93
|
+
summary: buildSummary(parsed),
|
|
94
|
+
usage: result.usage
|
|
95
|
+
};
|
|
96
|
+
} catch {
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
result: { raw: result.output, provider },
|
|
100
|
+
summary: "Audit complete (unstructured output)",
|
|
101
|
+
usage: result.usage
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/roles/index.js
CHANGED
package/src/sonar/scanner.js
CHANGED
|
@@ -144,6 +144,10 @@ async function resolveSonarToken(config, apiHost) {
|
|
|
144
144
|
].filter(Boolean);
|
|
145
145
|
|
|
146
146
|
for (const password of new Set(candidates)) {
|
|
147
|
+
if (password === "admin") {
|
|
148
|
+
// eslint-disable-next-line no-console
|
|
149
|
+
console.warn("[karajan] WARNING: Using default admin/admin credentials for SonarQube. Set KJ_SONAR_TOKEN for production use.");
|
|
150
|
+
}
|
|
147
151
|
const valid = await validateAdminCredentials(apiHost, adminUser, password);
|
|
148
152
|
if (!valid) continue;
|
|
149
153
|
const token = await generateUserToken(apiHost, adminUser, password);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Audit Role
|
|
2
|
+
|
|
3
|
+
You are the **Codebase Auditor**. Analyze the project for health, quality, and risks. DO NOT modify any files — this is a read-only analysis.
|
|
4
|
+
|
|
5
|
+
## Tools allowed
|
|
6
|
+
ONLY use: Read, Grep, Glob, Bash (for analysis commands only — `wc`, `find`, `git log`, `du`, `npm ls`). DO NOT use Edit or Write.
|
|
7
|
+
|
|
8
|
+
## Analysis dimensions
|
|
9
|
+
|
|
10
|
+
### 1. Security
|
|
11
|
+
- Hardcoded secrets, API keys, tokens in source code
|
|
12
|
+
- SQL/NoSQL injection vectors
|
|
13
|
+
- XSS vulnerabilities (innerHTML, dangerouslySetInnerHTML, eval)
|
|
14
|
+
- Command injection (exec, spawn with user input)
|
|
15
|
+
- Insecure dependencies (check package.json for known vulnerable packages)
|
|
16
|
+
- Missing input validation at system boundaries
|
|
17
|
+
- Authentication/authorization gaps
|
|
18
|
+
|
|
19
|
+
### 2. Code Quality (SOLID, DRY, KISS, YAGNI)
|
|
20
|
+
- Functions/methods longer than 50 lines
|
|
21
|
+
- Files longer than 500 lines
|
|
22
|
+
- Duplicated code blocks (same logic in multiple places)
|
|
23
|
+
- God classes/modules (too many responsibilities)
|
|
24
|
+
- Deep nesting (>4 levels)
|
|
25
|
+
- Dead code (unused exports, unreachable branches)
|
|
26
|
+
- Missing error handling (uncaught promises, empty catches)
|
|
27
|
+
- Over-engineering (abstractions for single use)
|
|
28
|
+
|
|
29
|
+
### 3. Performance
|
|
30
|
+
- N+1 query patterns
|
|
31
|
+
- Synchronous file I/O in request handlers
|
|
32
|
+
- Missing pagination on list endpoints
|
|
33
|
+
- Large bundle imports (importing entire libraries for one function)
|
|
34
|
+
- Missing lazy loading
|
|
35
|
+
- Expensive operations in loops
|
|
36
|
+
- Missing caching opportunities
|
|
37
|
+
|
|
38
|
+
### 4. Architecture
|
|
39
|
+
- Circular dependencies
|
|
40
|
+
- Layer violations (UI importing from data layer directly)
|
|
41
|
+
- Coupling between modules (shared mutable state)
|
|
42
|
+
- Missing dependency injection
|
|
43
|
+
- Inconsistent patterns across the codebase
|
|
44
|
+
- Missing or outdated documentation
|
|
45
|
+
- Configuration scattered vs centralized
|
|
46
|
+
|
|
47
|
+
### 5. Testing
|
|
48
|
+
- Test coverage gaps (source files without corresponding tests)
|
|
49
|
+
- Test quality (assertions per test, meaningful test names)
|
|
50
|
+
- Missing edge case coverage
|
|
51
|
+
- Test isolation (shared state between tests)
|
|
52
|
+
- Flaky test indicators (timeouts, sleep, retries)
|
|
53
|
+
|
|
54
|
+
## Task
|
|
55
|
+
|
|
56
|
+
{{task}}
|
|
57
|
+
|
|
58
|
+
## Context
|
|
59
|
+
|
|
60
|
+
{{context}}
|
|
61
|
+
|
|
62
|
+
## Output format
|
|
63
|
+
|
|
64
|
+
Return a single valid JSON object and nothing else.
|
|
65
|
+
|
|
66
|
+
JSON schema: {"ok":true,"result":{"summary":{"overallHealth":"good|fair|poor|critical","totalFindings":number,"critical":number,"high":number,"medium":number,"low":number},"dimensions":{"security":{"score":"A|B|C|D|F","findings":[]},"codeQuality":{"score":"A|B|C|D|F","findings":[]},"performance":{"score":"A|B|C|D|F","findings":[]},"architecture":{"score":"A|B|C|D|F","findings":[]},"testing":{"score":"A|B|C|D|F","findings":[]}},"topRecommendations":[{"priority":number,"dimension":string,"action":string,"impact":"high|medium|low","effort":"high|medium|low"}]},"summary":string}
|
|
67
|
+
|
|
68
|
+
Each finding: {"severity":"critical|high|medium|low","file":string,"line":number,"rule":string,"description":string,"recommendation":string}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# kj-audit — Codebase Health Audit
|
|
2
|
+
|
|
3
|
+
Analyze the current codebase for security, code quality, performance, architecture, and testing issues. This is a READ-ONLY analysis — do not modify any files.
|
|
4
|
+
|
|
5
|
+
## Your task
|
|
6
|
+
|
|
7
|
+
$ARGUMENTS
|
|
8
|
+
|
|
9
|
+
## Dimensions to analyze
|
|
10
|
+
|
|
11
|
+
### 1. Security
|
|
12
|
+
- Hardcoded secrets, API keys, tokens in source code
|
|
13
|
+
- SQL/NoSQL injection vectors (string concatenation in queries)
|
|
14
|
+
- XSS vulnerabilities (innerHTML, dangerouslySetInnerHTML, eval)
|
|
15
|
+
- Command injection (exec, spawn with user input)
|
|
16
|
+
- Insecure dependencies (check package.json for known vulnerable packages)
|
|
17
|
+
- Missing input validation at system boundaries
|
|
18
|
+
- Authentication/authorization gaps
|
|
19
|
+
|
|
20
|
+
### 2. Code Quality (SOLID, DRY, KISS, YAGNI)
|
|
21
|
+
- Functions/methods longer than 50 lines
|
|
22
|
+
- Files longer than 500 lines
|
|
23
|
+
- Duplicated code blocks (same logic in multiple places)
|
|
24
|
+
- God classes/modules (too many responsibilities)
|
|
25
|
+
- Deep nesting (>4 levels)
|
|
26
|
+
- Dead code (unused exports, unreachable branches)
|
|
27
|
+
- Missing error handling (uncaught promises, empty catches)
|
|
28
|
+
- Over-engineering (abstractions for single use)
|
|
29
|
+
|
|
30
|
+
### 3. Performance
|
|
31
|
+
- N+1 query patterns
|
|
32
|
+
- Synchronous file I/O in request handlers
|
|
33
|
+
- Missing pagination on list endpoints
|
|
34
|
+
- Large bundle imports (importing entire libraries for one function)
|
|
35
|
+
- Missing lazy loading
|
|
36
|
+
- Expensive operations in loops
|
|
37
|
+
- Missing caching opportunities
|
|
38
|
+
|
|
39
|
+
### 4. Architecture
|
|
40
|
+
- Circular dependencies
|
|
41
|
+
- Layer violations (UI importing from data layer directly)
|
|
42
|
+
- Coupling between modules (shared mutable state)
|
|
43
|
+
- Missing dependency injection
|
|
44
|
+
- Inconsistent patterns across the codebase
|
|
45
|
+
- Missing or outdated documentation
|
|
46
|
+
- Configuration scattered vs centralized
|
|
47
|
+
|
|
48
|
+
### 5. Testing
|
|
49
|
+
- Test coverage gaps (source files without corresponding tests)
|
|
50
|
+
- Test quality (assertions per test, meaningful test names)
|
|
51
|
+
- Missing edge case coverage
|
|
52
|
+
- Test isolation (shared state between tests)
|
|
53
|
+
- Flaky test indicators (timeouts, sleep, retries)
|
|
54
|
+
|
|
55
|
+
## Output
|
|
56
|
+
|
|
57
|
+
For each dimension, provide:
|
|
58
|
+
- **Score**: A (excellent) to F (failing)
|
|
59
|
+
- **Findings**: severity, file, line, rule, description, recommendation
|
|
60
|
+
|
|
61
|
+
End with:
|
|
62
|
+
- **Overall Health**: good / fair / poor / critical
|
|
63
|
+
- **Top Recommendations**: prioritized list of actions with impact and effort estimates
|