karajan-code 1.27.0 → 1.28.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
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
 
package/src/mcp/run-kj.js CHANGED
@@ -76,11 +76,14 @@ export async function runKjCommand({ command, commandArgs = [], options = {}, en
76
76
 
77
77
  const timeout = options.timeoutMs ? Number(options.timeoutMs) : undefined;
78
78
 
79
- const result = await execa("node", args, {
79
+ const execOpts = {
80
80
  env: runEnv,
81
81
  reject: false,
82
82
  timeout
83
- });
83
+ };
84
+ if (options.projectDir) execOpts.cwd = options.projectDir;
85
+
86
+ const result = await execa("node", args, execOpts);
84
87
 
85
88
  const ok = result.exitCode === 0;
86
89
  const payload = {
@@ -25,20 +25,37 @@ import { currentBranch } from "../utils/git.js";
25
25
  import { isPreflightAcked, ackPreflight, getSessionOverrides } from "./preflight.js";
26
26
 
27
27
  /**
28
- * Resolve the user's project directory via MCP roots.
29
- * Falls back to process.cwd() if roots are not available.
28
+ * Resolve the user's project directory.
29
+ * Priority: 1) explicit projectDir param, 2) MCP roots, 3) error with instructions.
30
30
  */
31
- async function resolveProjectDir(server) {
31
+ async function resolveProjectDir(server, explicitProjectDir) {
32
+ // 1. Explicit projectDir from tool parameter — always wins
33
+ if (explicitProjectDir) return explicitProjectDir;
34
+
35
+ // 2. MCP roots (host-provided workspace directory)
32
36
  try {
33
37
  const { roots } = await server.listRoots();
34
38
  if (roots?.length > 0) {
35
39
  const uri = roots[0].uri;
36
- // MCP roots use file:// URIs
37
40
  if (uri.startsWith("file://")) return new URL(uri).pathname;
38
41
  return uri;
39
42
  }
40
43
  } catch { /* client may not support roots */ }
41
- return process.cwd();
44
+
45
+ // 3. Check if process.cwd() looks like a real project (has package.json or .git)
46
+ const cwd = process.cwd();
47
+ try {
48
+ const hasGit = await fs.access(`${cwd}/.git`).then(() => true).catch(() => false);
49
+ const hasPkg = await fs.access(`${cwd}/package.json`).then(() => true).catch(() => false);
50
+ if (hasGit || hasPkg) return cwd;
51
+ } catch { /* ignore */ }
52
+
53
+ // 4. No valid project directory — fail with clear instructions
54
+ throw new Error(
55
+ `Cannot determine project directory. The MCP server is running from "${cwd}" which does not appear to be your project. ` +
56
+ `Fix: pass the "projectDir" parameter with the absolute path to your project (e.g., projectDir: "/home/user/my-project"), ` +
57
+ `or run "kj init" inside your project directory.`
58
+ );
42
59
  }
43
60
 
44
61
  export function asObject(value) {
@@ -260,7 +277,7 @@ export async function handleRunDirect(a, server, extra) {
260
277
  if (config.pipeline?.security?.enabled) requiredProviders.push(resolveRole(config, "security").provider);
261
278
  await assertAgentsAvailable(requiredProviders);
262
279
 
263
- const projectDir = await resolveProjectDir(server);
280
+ const projectDir = await resolveProjectDir(server, a.projectDir);
264
281
  const runLog = createRunLog(projectDir);
265
282
  runLog.logText(`[kj_run] started — task="${a.task.slice(0, 80)}..."`);
266
283
 
@@ -293,7 +310,7 @@ export async function handleResumeDirect(a, server, extra) {
293
310
  const config = await buildConfig(a);
294
311
  const logger = createLogger(config.output.log_level, "mcp");
295
312
 
296
- const projectDir = await resolveProjectDir(server);
313
+ const projectDir = await resolveProjectDir(server, a.projectDir);
297
314
  const runLog = createRunLog(projectDir);
298
315
  runLog.logText(`[kj_resume] started — session="${a.sessionId}"`);
299
316
 
@@ -349,7 +366,7 @@ export async function handlePlanDirect(a, server, extra) {
349
366
  const plannerRole = resolveRole(config, "planner");
350
367
  await assertAgentsAvailable([plannerRole.provider]);
351
368
 
352
- const projectDir = await resolveProjectDir(server);
369
+ const projectDir = await resolveProjectDir(server, a.projectDir);
353
370
  const runLog = createRunLog(projectDir);
354
371
  const silenceTimeoutMs = Number(config?.session?.max_agent_silence_minutes) > 0
355
372
  ? Math.round(Number(config.session.max_agent_silence_minutes) * 60 * 1000)
@@ -416,7 +433,7 @@ export async function handleCodeDirect(a, server, extra) {
416
433
  const coderRole = resolveRole(config, "coder");
417
434
  await assertAgentsAvailable([coderRole.provider]);
418
435
 
419
- const projectDir = await resolveProjectDir(server);
436
+ const projectDir = await resolveProjectDir(server, a.projectDir);
420
437
  const runLog = createRunLog(projectDir);
421
438
  runLog.logText(`[kj_code] started — provider=${coderRole.provider}`);
422
439
  const emitter = buildDirectEmitter(server, runLog, extra);
@@ -465,7 +482,7 @@ export async function handleReviewDirect(a, server, extra) {
465
482
  const reviewerRole = resolveRole(config, "reviewer");
466
483
  await assertAgentsAvailable([reviewerRole.provider, config.reviewer_options?.fallback_reviewer]);
467
484
 
468
- const projectDir = await resolveProjectDir(server);
485
+ const projectDir = await resolveProjectDir(server, a.projectDir);
469
486
  const runLog = createRunLog(projectDir);
470
487
  runLog.logText(`[kj_review] started — provider=${reviewerRole.provider}`);
471
488
  const emitter = buildDirectEmitter(server, runLog, extra);
@@ -512,7 +529,7 @@ export async function handleDiscoverDirect(a, server, extra) {
512
529
  const discoverRole = resolveRole(config, "discover");
513
530
  await assertAgentsAvailable([discoverRole.provider]);
514
531
 
515
- const projectDir = await resolveProjectDir(server);
532
+ const projectDir = await resolveProjectDir(server, a.projectDir);
516
533
  const runLog = createRunLog(projectDir);
517
534
  runLog.logText(`[kj_discover] started — mode=${a.mode || "gaps"}`);
518
535
  const emitter = buildDirectEmitter(server, runLog, extra);
@@ -565,7 +582,7 @@ export async function handleTriageDirect(a, server, extra) {
565
582
  const triageRole = resolveRole(config, "triage");
566
583
  await assertAgentsAvailable([triageRole.provider]);
567
584
 
568
- const projectDir = await resolveProjectDir(server);
585
+ const projectDir = await resolveProjectDir(server, a.projectDir);
569
586
  const runLog = createRunLog(projectDir);
570
587
  runLog.logText(`[kj_triage] started`);
571
588
  const emitter = buildDirectEmitter(server, runLog, extra);
@@ -609,7 +626,7 @@ export async function handleResearcherDirect(a, server, extra) {
609
626
  const researcherRole = resolveRole(config, "researcher");
610
627
  await assertAgentsAvailable([researcherRole.provider]);
611
628
 
612
- const projectDir = await resolveProjectDir(server);
629
+ const projectDir = await resolveProjectDir(server, a.projectDir);
613
630
  const runLog = createRunLog(projectDir);
614
631
  runLog.logText(`[kj_researcher] started`);
615
632
  const emitter = buildDirectEmitter(server, runLog, extra);
@@ -646,6 +663,54 @@ export async function handleResearcherDirect(a, server, extra) {
646
663
  return { ok: true, ...result.result, summary: result.summary };
647
664
  }
648
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
+
649
714
  export async function handleArchitectDirect(a, server, extra) {
650
715
  const config = await buildConfig(a, "architect");
651
716
  const logger = createLogger(config.output.log_level, "mcp");
@@ -653,7 +718,7 @@ export async function handleArchitectDirect(a, server, extra) {
653
718
  const architectRole = resolveRole(config, "architect");
654
719
  await assertAgentsAvailable([architectRole.provider]);
655
720
 
656
- const projectDir = await resolveProjectDir(server);
721
+ const projectDir = await resolveProjectDir(server, a.projectDir);
657
722
  const runLog = createRunLog(projectDir);
658
723
  runLog.logText(`[kj_architect] started`);
659
724
  const emitter = buildDirectEmitter(server, runLog, extra);
@@ -780,7 +845,7 @@ function buildReportArgs(a) {
780
845
 
781
846
  async function handleStatus(a, server) {
782
847
  const maxLines = a.lines || 50;
783
- const projectDir = await resolveProjectDir(server);
848
+ const projectDir = await resolveProjectDir(server, a.projectDir);
784
849
  return readRunLog(projectDir, maxLines);
785
850
  }
786
851
 
@@ -938,6 +1003,10 @@ async function handleArchitect(a, server, extra) {
938
1003
  return handleArchitectDirect(a, server, extra);
939
1004
  }
940
1005
 
1006
+ async function handleAudit(a, server, extra) {
1007
+ return handleAuditDirect(a, server, extra);
1008
+ }
1009
+
941
1010
  /* ── Handler dispatch map ─────────────────────────────────────────── */
942
1011
 
943
1012
  const toolHandlers = {
@@ -958,7 +1027,8 @@ const toolHandlers = {
958
1027
  kj_discover: (a, server, extra) => handleDiscover(a, server, extra),
959
1028
  kj_triage: (a, server, extra) => handleTriage(a, server, extra),
960
1029
  kj_researcher: (a, server, extra) => handleResearcher(a, server, extra),
961
- 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)
962
1032
  };
963
1033
 
964
1034
  export async function handleToolCall(name, args, server, extra) {
package/src/mcp/tools.js CHANGED
@@ -53,6 +53,7 @@ export const tools = [
53
53
  required: ["task"],
54
54
  properties: {
55
55
  task: { type: "string", description: "Task description for the coder (can include a Planning Game card ID like KJC-TSK-0042)" },
56
+ projectDir: { type: "string", description: "Absolute path to the project directory. Required when KJ MCP server runs from a different directory than the target project." },
56
57
  pgTask: { type: "string", description: "Planning Game card ID (e.g., KJC-TSK-0042). If provided, fetches full card details as task context and updates card status on completion." },
57
58
  pgProject: { type: "string", description: "Planning Game project ID (e.g., 'Karajan Code'). Required when pgTask is used." },
58
59
  planner: { type: "string" },
@@ -112,6 +113,7 @@ export const tools = [
112
113
  properties: {
113
114
  sessionId: { type: "string", description: "Session ID to resume" },
114
115
  answer: { type: "string", description: "Answer to the question that caused the pause" },
116
+ projectDir: { type: "string", description: "Absolute path to the project directory" },
115
117
  kjHome: { type: "string" }
116
118
  }
117
119
  }
@@ -185,6 +187,7 @@ export const tools = [
185
187
  task: { type: "string" },
186
188
  coder: { type: "string" },
187
189
  coderModel: { type: "string" },
190
+ projectDir: { type: "string", description: "Absolute path to the project directory" },
188
191
  kjHome: { type: "string" }
189
192
  }
190
193
  }
@@ -200,6 +203,7 @@ export const tools = [
200
203
  reviewer: { type: "string" },
201
204
  reviewerModel: { type: "string" },
202
205
  baseRef: { type: "string" },
206
+ projectDir: { type: "string", description: "Absolute path to the project directory" },
203
207
  kjHome: { type: "string" }
204
208
  }
205
209
  }
@@ -226,6 +230,7 @@ export const tools = [
226
230
  plannerModel: { type: "string" },
227
231
  coder: { type: "string", description: "Legacy alias for planner" },
228
232
  coderModel: { type: "string", description: "Legacy alias for plannerModel" },
233
+ projectDir: { type: "string", description: "Absolute path to the project directory" },
229
234
  kjHome: { type: "string" }
230
235
  }
231
236
  }
@@ -242,6 +247,7 @@ export const tools = [
242
247
  context: { type: "string", description: "Additional context for the analysis (e.g., research output)" },
243
248
  pgTask: { type: "string", description: "Planning Game card ID (e.g., KJC-TSK-0042). If provided, fetches full card details as additional context." },
244
249
  pgProject: { type: "string", description: "Planning Game project ID. Required when pgTask is used." },
250
+ projectDir: { type: "string", description: "Absolute path to the project directory" },
245
251
  kjHome: { type: "string" }
246
252
  }
247
253
  }
@@ -254,6 +260,7 @@ export const tools = [
254
260
  required: ["task"],
255
261
  properties: {
256
262
  task: { type: "string", description: "Task description to classify" },
263
+ projectDir: { type: "string", description: "Absolute path to the project directory" },
257
264
  kjHome: { type: "string" }
258
265
  }
259
266
  }
@@ -266,6 +273,7 @@ export const tools = [
266
273
  required: ["task"],
267
274
  properties: {
268
275
  task: { type: "string", description: "Task description to research" },
276
+ projectDir: { type: "string", description: "Absolute path to the project directory" },
269
277
  kjHome: { type: "string" }
270
278
  }
271
279
  }
@@ -279,6 +287,19 @@ export const tools = [
279
287
  properties: {
280
288
  task: { type: "string", description: "Task description to architect" },
281
289
  context: { type: "string", description: "Additional context (e.g., researcher output)" },
290
+ projectDir: { type: "string", description: "Absolute path to the project directory" },
291
+ kjHome: { type: "string" }
292
+ }
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)" },
282
303
  kjHome: { type: "string" }
283
304
  }
284
305
  }
@@ -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
+ }
@@ -12,3 +12,4 @@ export { SecurityRole } from "./security-role.js";
12
12
  export { SolomonRole } from "./solomon-role.js";
13
13
  export { DiscoverRole } from "./discover-role.js";
14
14
  export { ArchitectRole } from "./architect-role.js";
15
+ export { AuditRole } from "./audit-role.js";
@@ -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