karajan-code 1.31.1 → 1.32.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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="docs/karajan-code-logo-small.png" alt="Karajan Code" width="200">
2
+ <img src="docs/karajan-code-logo.svg" alt="Karajan Code" width="180">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Karajan Code</h1>
@@ -46,6 +46,7 @@ Use Karajan when you want:
46
46
  - **Zero-config operation** — auto-detects test frameworks, starts SonarQube, simplifies pipeline for trivial tasks
47
47
  - **Composable role architecture** — define agent behaviors as plain markdown files that travel with your project
48
48
  - **Local-first** — your code, your keys, your machine, no data leaves unless you say so
49
+ - **Zero API costs** — Karajan uses AI agent CLIs (Claude Code, Codex, Gemini CLI), not APIs. You pay your existing subscription (Claude Pro, ChatGPT Plus), not per-token API fees. No surprise bills.
49
50
 
50
51
  If Claude Code is a smart pair programmer, Karajan is the CI/CD pipeline for AI-assisted development. They work great together — Karajan is designed to be used as an MCP server inside Claude Code.
51
52
 
@@ -64,6 +65,8 @@ That's it. No Docker required (SonarQube uses Docker, but Karajan auto-manages i
64
65
  kj run "Create a utility function that validates Spanish DNI numbers, with tests"
65
66
  ```
66
67
 
68
+ [**▶ Watch the full pipeline demo**](https://karajancode.com#demo) — HU certification, triage, architecture, TDD, SonarQube, code review, Solomon arbitration, security audit.
69
+
67
70
  Karajan will:
68
71
  1. Triage the task complexity and activate the right roles
69
72
  2. Write tests first (TDD)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.31.1",
3
+ "version": "1.32.1",
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",
@@ -1,6 +1,7 @@
1
1
  import { TesterRole } from "../roles/tester-role.js";
2
2
  import { SecurityRole } from "../roles/security-role.js";
3
3
  import { ImpeccableRole } from "../roles/impeccable-role.js";
4
+ import { AuditRole } from "../roles/audit-role.js";
4
5
  import { addCheckpoint, saveSession } from "../session-store.js";
5
6
  import { emitProgress, makeEvent } from "../utils/events.js";
6
7
  import { invokeSolomon } from "./solomon-escalation.js";
@@ -290,5 +291,113 @@ export async function runImpeccableStage({ config, logger, emitter, eventBase, s
290
291
  return { action: "ok", stageResult: { ok: impeccableOutput.ok, verdict, summary: impeccableOutput.summary || "No frontend design issues found" } };
291
292
  }
292
293
 
294
+ export async function runFinalAuditStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, iteration, task, diff }) {
295
+ logger.setContext({ iteration, stage: "audit" });
296
+ emitProgress(
297
+ emitter,
298
+ makeEvent("audit:start", { ...eventBase, stage: "audit" }, {
299
+ message: "Final audit — verifying code quality"
300
+ })
301
+ );
302
+
303
+ const auditStart = Date.now();
304
+ const { output: auditOutput, provider, attempts } = await runRoleWithFallback(
305
+ AuditRole,
306
+ { roleName: "audit", config, logger, emitter, eventBase, task, iteration, diff }
307
+ );
308
+ const totalDuration = Date.now() - auditStart;
309
+
310
+ trackBudget({
311
+ role: "audit",
312
+ provider: provider || coderRole.provider,
313
+ model: config?.roles?.audit?.model || coderRole.model,
314
+ result: auditOutput,
315
+ duration_ms: totalDuration
316
+ });
317
+
318
+ await addCheckpoint(session, {
319
+ stage: "audit",
320
+ iteration,
321
+ ok: auditOutput.ok,
322
+ provider: provider || coderRole.provider,
323
+ model: config?.roles?.audit?.model || coderRole.model || null,
324
+ attempts: attempts.length > 1 ? attempts : undefined
325
+ });
326
+
327
+ if (!auditOutput.ok) {
328
+ // Audit agent failed to run — treat as advisory, don't block pipeline
329
+ logger.warn(`Audit agent error (advisory): ${auditOutput.summary}`);
330
+ emitProgress(
331
+ emitter,
332
+ makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
333
+ status: "warn",
334
+ message: `Audit: agent error (advisory), continuing — ${auditOutput.summary}`
335
+ })
336
+ );
337
+ return { action: "ok", stageResult: { ok: false, summary: auditOutput.summary || "Audit agent error (advisory)", auto_continued: true } };
338
+ }
339
+
340
+ // Parse findings from audit result
341
+ const result = auditOutput.result || {};
342
+ const summary = result.summary || {};
343
+ const overallHealth = summary.overallHealth || "fair";
344
+ const criticalCount = summary.critical || 0;
345
+ const highCount = summary.high || 0;
346
+
347
+ // Collect critical and high findings for feedback
348
+ const actionableFindings = [];
349
+ if (result.dimensions) {
350
+ for (const [dimName, dim] of Object.entries(result.dimensions)) {
351
+ for (const finding of (dim.findings || [])) {
352
+ if (finding.severity === "critical" || finding.severity === "high") {
353
+ actionableFindings.push({
354
+ dimension: dimName,
355
+ ...finding
356
+ });
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ const hasActionableIssues = (overallHealth === "poor" || overallHealth === "critical") && (criticalCount > 0 || highCount > 0);
363
+
364
+ if (hasActionableIssues) {
365
+ // Build feedback string for the coder
366
+ const feedbackLines = actionableFindings.map(f => {
367
+ const loc = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : "";
368
+ return `[${f.severity.toUpperCase()}] ${loc} ${f.description}${f.recommendation ? ` — Fix: ${f.recommendation}` : ""}`;
369
+ });
370
+ const feedback = `Audit found ${criticalCount + highCount} critical/high issue(s) that must be fixed:\n${feedbackLines.join("\n")}`;
371
+
372
+ logger.warn(`Audit: ${criticalCount + highCount} actionable issues found, sending back to coder`);
373
+ emitProgress(
374
+ emitter,
375
+ makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
376
+ status: "fail",
377
+ message: `Audit: ${criticalCount + highCount} issue(s) found, sending back to coder`
378
+ })
379
+ );
380
+
381
+ return { action: "retry", feedback, stageResult: { ok: false, summary: auditOutput.summary || `${criticalCount + highCount} actionable issues` } };
382
+ }
383
+
384
+ // Audit passed (good/fair or no critical/high findings)
385
+ const hasAdvisory = (summary.medium || 0) + (summary.low || 0) > 0;
386
+ const certifiedMsg = hasAdvisory
387
+ ? `Audit: CERTIFIED (with ${(summary.medium || 0) + (summary.low || 0)} advisory warning(s))`
388
+ : "Audit: CERTIFIED";
389
+
390
+ logger.info(certifiedMsg);
391
+ emitProgress(
392
+ emitter,
393
+ makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
394
+ status: "ok",
395
+ message: certifiedMsg
396
+ })
397
+ );
398
+
399
+ return { action: "ok", stageResult: { ok: true, summary: certifiedMsg } };
400
+ }
401
+
293
402
  // Exported for testing
294
403
  export { buildFallbackChain, isAgentFailure, runRoleWithFallback };
@@ -32,10 +32,11 @@ import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
32
32
  import { PipelineContext } from "./orchestrator/pipeline-context.js";
33
33
  import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
34
34
  import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
35
- import { runTesterStage, runSecurityStage, runImpeccableStage } from "./orchestrator/post-loop-stages.js";
35
+ import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./orchestrator/post-loop-stages.js";
36
36
  import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
37
37
  import { detectTestFramework } from "./utils/project-detect.js";
38
38
  import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
39
+ import { detectRtk } from "./utils/rtk-detect.js";
39
40
 
40
41
 
41
42
  // --- Extracted helper functions (pure refactoring, zero behavior change) ---
@@ -70,8 +71,8 @@ async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
70
71
  const projectDir = config.projectDir || process.cwd();
71
72
  const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
72
73
  const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
73
- const coderPrompt = buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled) });
74
- const reviewerPrompt = buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled) });
74
+ const coderPrompt = buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available) });
75
+ const reviewerPrompt = buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available) });
75
76
 
76
77
  const summary = {
77
78
  dry_run: true,
@@ -669,6 +670,22 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
669
670
  }
670
671
  }
671
672
 
673
+ // Final audit — last quality gate before declaring success
674
+ const auditResult = await runFinalAuditStage({
675
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
676
+ iteration: i, task, diff: postLoopDiff
677
+ });
678
+ if (auditResult.stageResult) {
679
+ stageResults.audit = auditResult.stageResult;
680
+ await tryBecariaComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
681
+ }
682
+ if (auditResult.action === "retry") {
683
+ // Audit found actionable issues — loop back to coder
684
+ session.last_reviewer_feedback = auditResult.feedback;
685
+ await saveSession(session);
686
+ return { action: "continue" };
687
+ }
688
+
672
689
  return { action: "proceed" };
673
690
  }
674
691
 
@@ -1071,6 +1088,17 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1071
1088
  ctx.budgetSummary = budgetSummary;
1072
1089
  ctx.trackBudget = trackBudget;
1073
1090
 
1091
+ // --- RTK detection ---
1092
+ const rtkResult = await detectRtk();
1093
+ if (rtkResult.available) {
1094
+ config = { ...config, rtk: { available: true, version: rtkResult.version } };
1095
+ logger.info(`RTK detected (${rtkResult.version}) — instructing agents to prefix Bash commands with rtk`);
1096
+ emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
1097
+ message: "RTK detected — agent commands will use token optimization",
1098
+ detail: { version: rtkResult.version }
1099
+ }));
1100
+ }
1101
+
1074
1102
  ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
1075
1103
  ctx.eventBase.sessionId = ctx.session.id;
1076
1104
 
@@ -1199,7 +1227,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1199
1227
  const checkpointIntervalMs = (ctx.config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
1200
1228
  let lastCheckpointAt = Date.now();
1201
1229
  let checkpointDisabled = false;
1202
- let lastCheckpointSnapshot = null;
1230
+ let lastCheckpointSnapshot = takeCheckpointSnapshot(ctx.session);
1203
1231
 
1204
1232
  let i = 0;
1205
1233
  while (i < ctx.config.max_iterations) {
@@ -1,3 +1,5 @@
1
+ import { RTK_INSTRUCTIONS } from "./rtk-snippet.js";
2
+
1
3
  const SUBAGENT_PREAMBLE = [
2
4
  "IMPORTANT: You are running as a Karajan sub-agent.",
3
5
  "Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
@@ -29,7 +31,7 @@ const SERENA_INSTRUCTIONS = [
29
31
  "Fall back to reading files only when Serena tools are not sufficient."
30
32
  ].join("\n");
31
33
 
32
- export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, deferredContext = null }) {
34
+ export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null }) {
33
35
  const sections = [
34
36
  serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
35
37
  `Task:\n${task}`,
@@ -42,6 +44,10 @@ export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary =
42
44
  sections.push(SERENA_INSTRUCTIONS);
43
45
  }
44
46
 
47
+ if (rtkAvailable) {
48
+ sections.push(RTK_INSTRUCTIONS);
49
+ }
50
+
45
51
  if (coderRules) {
46
52
  sections.push(`Coder rules (MUST follow):\n${coderRules}`);
47
53
  }
@@ -1,3 +1,5 @@
1
+ import { RTK_INSTRUCTIONS } from "./rtk-snippet.js";
2
+
1
3
  const SUBAGENT_PREAMBLE = [
2
4
  "IMPORTANT: You are running as a Karajan sub-agent.",
3
5
  "Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
@@ -20,7 +22,7 @@ const SERENA_INSTRUCTIONS = [
20
22
  "Fall back to reading files only when Serena tools are not sufficient."
21
23
  ].join("\n");
22
24
 
23
- export function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false }) {
25
+ export function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false }) {
24
26
  const truncatedDiff = diff.length > 12000 ? `${diff.slice(0, 12000)}\n\n[TRUNCATED]` : diff;
25
27
 
26
28
  const sections = [
@@ -37,6 +39,10 @@ export function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabl
37
39
  sections.push(SERENA_INSTRUCTIONS);
38
40
  }
39
41
 
42
+ if (rtkAvailable) {
43
+ sections.push(RTK_INSTRUCTIONS);
44
+ }
45
+
40
46
  sections.push(
41
47
  `Task context:\n${task}`,
42
48
  `Review rules:\n${reviewRules}`,
@@ -0,0 +1,15 @@
1
+ /**
2
+ * RTK (Rust Token Killer) prompt instruction snippet.
3
+ * Injected into coder/reviewer prompts when RTK is detected.
4
+ */
5
+ export const RTK_INSTRUCTIONS = [
6
+ "## Token Optimization (RTK detected)",
7
+ "RTK is installed. Prefix ALL Bash tool calls with `rtk` to reduce token usage:",
8
+ "- Use `rtk git status` instead of `git status`",
9
+ "- Use `rtk git diff` instead of `git diff`",
10
+ "- Use `rtk ls` instead of `ls`",
11
+ "- Use `rtk find` instead of `find`",
12
+ "- Use `rtk grep` instead of `grep`",
13
+ "- Use `rtk cat` instead of `cat`",
14
+ "This does NOT apply to non-Bash tools (Read, Write, Edit, Glob, Grep)."
15
+ ].join("\n");
@@ -41,7 +41,8 @@ export class CoderRole extends BaseRole {
41
41
  deferredContext: deferredContext || null,
42
42
  coderRules: this.instructions,
43
43
  methodology: this.config?.development?.methodology || "tdd",
44
- serenaEnabled: Boolean(this.config?.serena?.enabled)
44
+ serenaEnabled: Boolean(this.config?.serena?.enabled),
45
+ rtkAvailable: Boolean(this.config?.rtk?.available)
45
46
  });
46
47
 
47
48
  const coderArgs = { prompt, role: "coder" };
@@ -1,5 +1,6 @@
1
1
  import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
+ import { RTK_INSTRUCTIONS } from "../prompts/rtk-snippet.js";
3
4
 
4
5
  const MAX_DIFF_LENGTH = 12000;
5
6
 
@@ -24,7 +25,7 @@ function truncateDiff(diff) {
24
25
  : diff;
25
26
  }
26
27
 
27
- function buildPrompt({ task, diff, reviewRules, reviewMode, instructions }) {
28
+ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAvailable = false }) {
28
29
  const sections = [];
29
30
 
30
31
  sections.push(SUBAGENT_PREAMBLE);
@@ -41,6 +42,10 @@ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions }) {
41
42
  `Task context:\n${task}`
42
43
  );
43
44
 
45
+ if (rtkAvailable) {
46
+ sections.push(RTK_INSTRUCTIONS);
47
+ }
48
+
44
49
  if (reviewRules) {
45
50
  sections.push(`Review rules:\n${reviewRules}`);
46
51
  }
@@ -78,7 +83,8 @@ export class ReviewerRole extends BaseRole {
78
83
  diff: diff || "",
79
84
  reviewRules: reviewRules || null,
80
85
  reviewMode: this.config?.review_mode || "standard",
81
- instructions: this.instructions
86
+ instructions: this.instructions,
87
+ rtkAvailable: Boolean(this.config?.rtk?.available)
82
88
  });
83
89
 
84
90
  const reviewArgs = { prompt, role: "reviewer" };
@@ -0,0 +1,18 @@
1
+ import { runCommand } from "./process.js";
2
+
3
+ /**
4
+ * Detect whether RTK (Rust Token Killer) is installed and available.
5
+ * @returns {Promise<{ available: boolean, version: string|null }>}
6
+ */
7
+ export async function detectRtk() {
8
+ try {
9
+ const result = await runCommand("rtk", ["--version"]);
10
+ if (result.exitCode === 0) {
11
+ const version = (result.stdout || "").trim() || null;
12
+ return { available: true, version };
13
+ }
14
+ return { available: false, version: null };
15
+ } catch {
16
+ return { available: false, version: null };
17
+ }
18
+ }