karajan-code 1.31.1 → 1.32.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 +4 -1
- package/package.json +1 -1
- package/src/orchestrator/post-loop-stages.js +109 -0
- package/src/orchestrator.js +18 -2
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/karajan-code-logo
|
|
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,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 };
|
package/src/orchestrator.js
CHANGED
|
@@ -32,7 +32,7 @@ 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";
|
|
@@ -669,6 +669,22 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
669
669
|
}
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
// Final audit — last quality gate before declaring success
|
|
673
|
+
const auditResult = await runFinalAuditStage({
|
|
674
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
675
|
+
iteration: i, task, diff: postLoopDiff
|
|
676
|
+
});
|
|
677
|
+
if (auditResult.stageResult) {
|
|
678
|
+
stageResults.audit = auditResult.stageResult;
|
|
679
|
+
await tryBecariaComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
|
|
680
|
+
}
|
|
681
|
+
if (auditResult.action === "retry") {
|
|
682
|
+
// Audit found actionable issues — loop back to coder
|
|
683
|
+
session.last_reviewer_feedback = auditResult.feedback;
|
|
684
|
+
await saveSession(session);
|
|
685
|
+
return { action: "continue" };
|
|
686
|
+
}
|
|
687
|
+
|
|
672
688
|
return { action: "proceed" };
|
|
673
689
|
}
|
|
674
690
|
|
|
@@ -1199,7 +1215,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1199
1215
|
const checkpointIntervalMs = (ctx.config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
|
|
1200
1216
|
let lastCheckpointAt = Date.now();
|
|
1201
1217
|
let checkpointDisabled = false;
|
|
1202
|
-
let lastCheckpointSnapshot =
|
|
1218
|
+
let lastCheckpointSnapshot = takeCheckpointSnapshot(ctx.session);
|
|
1203
1219
|
|
|
1204
1220
|
let i = 0;
|
|
1205
1221
|
while (i < ctx.config.max_iterations) {
|