karajan-code 4.1.11 → 4.2.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": "4.1.11",
3
+ "version": "4.2.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",
@@ -211,6 +211,7 @@ export function registerPipeline(program, { pkgVersion }) {
211
211
  .option("--check", "Verify the recorded verdict matches the staged diff (exit 0/1, hook-friendly)")
212
212
  .option("--range <range>", "Review a git range (e.g. main..HEAD) instead of the staged diff")
213
213
  .option("--install-gate", "Enable the pre-commit review gate for this project (creates .karajan/review-gate)")
214
+ .option("--no-sonar", "Skip the deterministic Sonar pre-gate that runs before the cross-AI verdict")
214
215
  .action(async (task, flags) => {
215
216
  await withConfig(pkgVersion, "review", flags, async ({ config, logger }) => {
216
217
  // ENV-B1 (KJC-TSK-0637): the gate mode records a verdict tied to
@@ -10,10 +10,11 @@ import { checkVerdict } from "../review/verdict-store.js";
10
10
  import { runOneShotReview } from "../review/one-shot-review.js";
11
11
  import { runSolomonArbitration } from "../review/solomon-arbitration.js";
12
12
  import { ensureGateTrackable } from "../review/gate-gitignore.js";
13
+ import { runSonarPregate, formatSonarFinding } from "../review/sonar-pregate.js";
13
14
 
14
15
  // Raw git always — never a wrapped/compressing runner (KJC-BUG-0115).
15
- async function rawDiff(range) {
16
- const args = range ? ["diff", range] : ["diff", "--cached"];
16
+ async function rawDiff(range, extraArgs = []) {
17
+ const args = range ? ["diff", range, ...extraArgs] : ["diff", "--cached", ...extraArgs];
17
18
  const res = await runCommand("git", args);
18
19
  if (res.exitCode !== 0) {
19
20
  throw new Error(res.stderr?.trim() || `git ${args.join(" ")} failed`);
@@ -29,7 +30,11 @@ function printVerdict(record) {
29
30
  }
30
31
  console.log(`✗ REJECTED by ${record.reviewer} — ${record.issues.length} blocking issue(s):`);
31
32
  for (const issue of record.issues) {
32
- const where = issue.file ? ` [${issue.file}${issue.line ? `:${issue.line}` : ""}]` : "";
33
+ let where = "";
34
+ if (issue.file) {
35
+ const line = issue.line ? `:${issue.line}` : "";
36
+ where = ` [${issue.file}${line}]`;
37
+ }
33
38
  console.log(` - (${issue.severity || "high"})${where} ${issue.description || issue.id}`);
34
39
  if (issue.suggested_fix) console.log(` fix: ${issue.suggested_fix}`);
35
40
  }
@@ -48,7 +53,8 @@ export async function solomonCommand({ config, logger = null, flags = {} }) {
48
53
  if (res.ruling === "approve") {
49
54
  console.log(`⚖ Solomon (${res.solomon}) rules for the brain — verdict recorded, the gate is open.`);
50
55
  } else {
51
- console.log(`⚖ Solomon${res.solomon ? ` (${res.solomon})` : ""} rules for the reviewer — obey and fix:`);
56
+ const who = res.solomon ? ` (${res.solomon})` : "";
57
+ console.log(`⚖ Solomon${who} rules for the reviewer — obey and fix:`);
52
58
  }
53
59
  if (res.reasoning) console.log(` ${res.reasoning}`);
54
60
  process.exitCode = res.ruling === "approve" ? 0 : 1;
@@ -89,7 +95,39 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
89
95
  return res;
90
96
  }
91
97
 
92
- const record = await runOneShotReview({ diff, task: flags.task, config, logger, projectDir });
98
+ // KJC-TSK-0676: deterministic pre-gate — sonar findings on the changed
99
+ // files come back BEFORE any AI opinion. Blocking severities reject
100
+ // without spending reviewer tokens; the rest travel with the task so
101
+ // the cross-AI reviewer weighs them. Unavailable sonar degrades loudly.
102
+ let task = flags.task;
103
+ if (flags.sonar !== false) {
104
+ const files = (await rawDiff(flags.range, ["--name-only"])).split("\n").map((f) => f.trim()).filter(Boolean);
105
+ const pre = await runSonarPregate({ config, stagedFiles: files, logger });
106
+ if (!pre.available) {
107
+ console.log(`⚠ sonar pre-gate skipped: ${pre.reason}`);
108
+ } else {
109
+ const found = [...pre.blocking, ...pre.advisory];
110
+ if (found.length > 0) {
111
+ console.log(`Sonar on the changed files — ${pre.blocking.length} blocking, ${pre.advisory.length} advisory (project total: ${pre.totalProject}):`);
112
+ for (const f of found) console.log(` - ${formatSonarFinding(f)}`);
113
+ }
114
+ if (pre.blocking.length > 0) {
115
+ console.log("✗ REJECTED by sonar (deterministic) — fix the blocking findings; the cross-AI reviewer was not invoked.");
116
+ process.exitCode = 1;
117
+ return {
118
+ verdict: "rejected", reviewer: "sonar",
119
+ issues: pre.blocking.map((i) => ({ severity: i.severity, file: undefined, description: formatSonarFinding(i) })),
120
+ };
121
+ }
122
+ if (pre.advisory.length > 0) {
123
+ task = `${task || "Review the following diff for correctness, security and maintainability."}\n\n`
124
+ + `Deterministic Sonar findings on these files (fold them into your review):\n`
125
+ + pre.advisory.map((f) => `- ${formatSonarFinding(f)}`).join("\n");
126
+ }
127
+ }
128
+ }
129
+
130
+ const record = await runOneShotReview({ diff, task, config, logger, projectDir });
93
131
  printVerdict(record);
94
132
  process.exitCode = record.verdict === "approved" ? 0 : 1;
95
133
  return record;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Sonar pre-gate for `kj review --staged` (KJC-TSK-0676, user request
3
+ * 2026-07-23). With the brain outside the core (v4), skipping sonar is one
4
+ * decision away — proven the day the brain itself shipped 3 sonar issues in
5
+ * new code. Hung off the review gate, it becomes a gate, not discipline:
6
+ * deterministic findings on the STAGED files come back before any AI
7
+ * verdict, and BLOCKER/CRITICAL ones reject without spending reviewer
8
+ * tokens. Unavailable sonar degrades loudly ({available:false, reason}) —
9
+ * a laptop without Docker still commits.
10
+ */
11
+
12
+ import { runSonarScan } from "../sonar/scanner.js";
13
+ import { getOpenIssues } from "../sonar/api.js";
14
+ import { acquireToolLock } from "../utils/tool-governor.js";
15
+
16
+ const BLOCKING_SEVERITIES = new Set(["BLOCKER", "CRITICAL"]);
17
+
18
+ /** Repo-relative path of a sonar issue (component is "<projectKey>:<path>"). */
19
+ export function issueFile(issue) {
20
+ const component = String(issue?.component || "");
21
+ const idx = component.indexOf(":");
22
+ return idx === -1 ? component : component.slice(idx + 1);
23
+ }
24
+
25
+ /** One printable line per finding: (SEVERITY) [file:line] rule — message. */
26
+ export function formatSonarFinding(issue) {
27
+ const line = issue.line ? `:${issue.line}` : "";
28
+ return `(${issue.severity}) [${issueFile(issue)}${line}] ${issue.rule} — ${issue.message}`;
29
+ }
30
+
31
+ /**
32
+ * Scan the project (single-flight via the tool governor) and return the
33
+ * open issues that live on the staged files, split by blocking severity.
34
+ * Every failure path degrades to {available:false, reason} — the pre-gate
35
+ * never breaks the review, it only refuses to stay silent.
36
+ */
37
+ export async function runSonarPregate({ config, stagedFiles = [], logger = null }) {
38
+ if (config?.review_gate?.sonar === false) {
39
+ return { available: false, reason: "disabled in config (review_gate.sonar: false)" };
40
+ }
41
+ let lock = null;
42
+ try {
43
+ lock = await acquireToolLock("sonar-scanner", { timeoutMs: 300_000 });
44
+ const scan = await runSonarScan(config);
45
+ if (!scan.ok) {
46
+ return { available: false, reason: (scan.stderr || scan.stdout || "sonar scan failed").trim() };
47
+ }
48
+ const res = await getOpenIssues(config, scan.projectKey);
49
+ const staged = new Set(stagedFiles);
50
+ const onStaged = (res.issues || []).filter((i) => staged.has(issueFile(i)));
51
+ return {
52
+ available: true,
53
+ blocking: onStaged.filter((i) => BLOCKING_SEVERITIES.has(String(i.severity).toUpperCase())),
54
+ advisory: onStaged.filter((i) => !BLOCKING_SEVERITIES.has(String(i.severity).toUpperCase())),
55
+ totalProject: res.total ?? (res.issues || []).length,
56
+ };
57
+ } catch (err) {
58
+ logger?.warn?.(`[sonar-pregate] ${err.message}`);
59
+ return { available: false, reason: err.message };
60
+ } finally {
61
+ lock?.release?.();
62
+ }
63
+ }