karajan-code 2.8.0 → 2.9.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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/audit/basal-cost.js +92 -9
  4. package/src/audit/deterministic-summary.js +200 -0
  5. package/src/audit/osv-findings.js +162 -0
  6. package/src/audit/semgrep-findings.js +117 -0
  7. package/src/audit/sonar-findings.js +81 -0
  8. package/src/audit/webperf-input.js +90 -0
  9. package/src/canvas/schema.js +1 -1
  10. package/src/checks/binaries.js +2 -2
  11. package/src/checks/ci.js +2 -2
  12. package/src/checks/config-files.js +6 -6
  13. package/src/checks/dir-setup.js +4 -0
  14. package/src/checks/mcp-health.js +6 -0
  15. package/src/checks/ports.js +8 -0
  16. package/src/checks/rtk.js +1 -1
  17. package/src/checks/skills.js +5 -1
  18. package/src/checks/sonar.js +1 -1
  19. package/src/checks/system.js +2 -2
  20. package/src/checks/tokens.js +1 -1
  21. package/src/checks/types.js +0 -12
  22. package/src/cli/register-meta.js +15 -1
  23. package/src/commands/audit.js +239 -28
  24. package/src/commands/doctor.js +1 -1
  25. package/src/config/test-harness.js +0 -1
  26. package/src/orchestrator/ci-integration.js +1 -1
  27. package/src/orchestrator/direct-actions.js +1 -1
  28. package/src/orchestrator/drivers/error-recovery.js +2 -2
  29. package/src/orchestrator/drivers/iteration-loop.js +3 -13
  30. package/src/orchestrator/drivers/pre-loop.js +1 -4
  31. package/src/orchestrator/feedback-queue.js +0 -1
  32. package/src/orchestrator/integrations.js +0 -7
  33. package/src/orchestrator/role-output-compressor.js +0 -1
  34. package/src/orchestrator/session-journal.js +4 -41
  35. package/src/orchestrator/stages/stage-classes.js +0 -2
  36. package/src/plan/plan-store.js +0 -14
  37. package/src/planning-game/client.js +0 -14
  38. package/src/planning-game/pipeline-adapter.js +1 -1
  39. package/src/prompts/audit.js +206 -6
  40. package/src/prompts/rtk-snippet.js +1 -1
  41. package/src/roles/audit-role.js +125 -13
  42. package/src/sonar/api.js +1 -1
  43. package/src/utils/display/event-handlers.js +1 -1
  44. package/src/utils/display/header.js +1 -1
  45. package/src/utils/display/session-summary.js +3 -3
  46. package/src/utils/display/solomon.js +1 -1
  47. package/src/utils/role-assigner.js +1 -1
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Sonar findings collector for `kj audit` (KJC-TSK-0361).
3
+ *
4
+ * The audit pipeline already pays for SonarQube to exist (kj run + the
5
+ * SonarStage drive a full scan and gate the pipeline on its result). When
6
+ * the user runs `kj audit`, we don't want to pay for *another* full scan
7
+ * (~1-3 min) — we want to read whatever Sonar already knows. This module
8
+ * does exactly that: ping the configured Sonar host, and if it's
9
+ * reachable, fetch the current open-issue list + quality-gate status via
10
+ * the existing sonar/api.js helpers. Both results are best-effort: a
11
+ * sonar-down host or a never-scanned project quietly returns
12
+ * `{available: false, reason: "..."}` and the audit continues without
13
+ * the section.
14
+ *
15
+ * Why this matters: the LLM auditor was previously asked to find issues
16
+ * by reading source code, but Sonar already produces them with rule IDs
17
+ * (squid:S1234), severity, and line precision. Feeding those in lets the
18
+ * LLM cross-reference its own findings (high-confidence overlap) and
19
+ * focus its tokens on patterns Sonar can't see (architecture, naming,
20
+ * API design).
21
+ */
22
+
23
+ import { isSonarReachable } from "../sonar/manager.js";
24
+ import { getOpenIssues, getQualityGateStatus } from "../sonar/api.js";
25
+
26
+ /**
27
+ * Collect deterministic Sonar findings for the audit prompt.
28
+ *
29
+ * @param {object} config - resolved Karajan config (must have config.sonarqube.host)
30
+ * @param {object} [logger] - optional logger for warn-level traces
31
+ * @returns {Promise<{available: boolean, reason?: string, issues?: object[], total?: number, qualityGate?: object}>}
32
+ */
33
+ export async function collectSonarFindings(config, logger = null) {
34
+ const sonarConfig = config?.sonarqube || {};
35
+ const host = sonarConfig.host || "http://localhost:9000";
36
+ const healthcheckSeconds = sonarConfig.timeouts?.healthcheckSeconds ?? 5;
37
+
38
+ // Ping first — avoids surfacing a 30-second timeout into audit runtime
39
+ // when Docker is simply down or the user opted out of Sonar.
40
+ const reachable = await isSonarReachable(host, healthcheckSeconds).catch(() => false);
41
+ if (!reachable) {
42
+ if (logger?.warn) logger.warn(`sonar audit input skipped: host ${host} not reachable`);
43
+ return { available: false, reason: `sonar host ${host} not reachable` };
44
+ }
45
+
46
+ let issuesResult;
47
+ try {
48
+ issuesResult = await getOpenIssues(config);
49
+ } catch (err) {
50
+ if (logger?.warn) logger.warn(`sonar audit input: getOpenIssues failed: ${err?.message || err}`);
51
+ return { available: false, reason: `getOpenIssues failed: ${err?.message || err}` };
52
+ }
53
+
54
+ let qualityGate = null;
55
+ try {
56
+ const qg = await getQualityGateStatus(config);
57
+ if (qg?.ok) qualityGate = { status: qg.status, conditions: qg.conditions };
58
+ } catch { /* gate is optional context */ }
59
+
60
+ return {
61
+ available: true,
62
+ issues: issuesResult.issues || [],
63
+ total: issuesResult.total || 0,
64
+ qualityGate,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Group Sonar issues by severity, preserving Sonar's canonical ordering.
70
+ * Used by the prompt builder to render the findings section.
71
+ */
72
+ export function groupIssuesBySeverity(issues) {
73
+ const order = ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"];
74
+ const groups = Object.fromEntries(order.map(s => [s, []]));
75
+ for (const issue of issues || []) {
76
+ const sev = issue.severity || "INFO";
77
+ if (groups[sev]) groups[sev].push(issue);
78
+ else groups.INFO.push(issue);
79
+ }
80
+ return groups;
81
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * WebPerf input collector for `kj audit` (KJC-TSK-0360).
3
+ *
4
+ * Two modes:
5
+ * - "static-hints": no live measurement available, but the project is
6
+ * frontend so the LLM should look for frontend-perf patterns in the
7
+ * source (bundle bloat, render-blocking, image opt, lazy loading).
8
+ * This is the common path because `kj audit` runs against a code
9
+ * tree, not a live preview server.
10
+ * - "cwv-result": a Core Web Vitals measurement was supplied (either
11
+ * via config.webperf.lastResult, or in the future via a `kj webperf`
12
+ * command that drives Chrome DevTools MCP). evaluateCwv from
13
+ * src/webperf/cwv-gate.js produces a structured verdict the prompt
14
+ * renders verbatim.
15
+ *
16
+ * The "live measurement" path is intentionally NOT wired here: the
17
+ * audit's LLM sub-agent is forbidden from MCP tool use (SUBAGENT_PREAMBLE
18
+ * in src/prompts/audit.js), and spawning Chrome / launching a preview
19
+ * server during a read-only audit is out of scope. When a future
20
+ * `kj webperf` command exists, it can write the result into
21
+ * `config.webperf.lastResult` and this collector will surface it.
22
+ */
23
+
24
+ import { evaluateCwv, CWV_THRESHOLDS } from "../webperf/cwv-gate.js";
25
+
26
+ /**
27
+ * Decide what (if anything) to feed the audit prompt about web perf.
28
+ *
29
+ * @param {object|null} stack - detectProjectStack output (null when unknown)
30
+ * @param {object} [config] - resolved Karajan config (config.webperf may carry a stale CWV result)
31
+ * @returns {{available: boolean, mode?: 'static-hints'|'cwv-result', cwv?: object, reason?: string}}
32
+ */
33
+ export function collectWebPerfInput(stack, config = {}) {
34
+ const webperfConfig = config?.webperf || {};
35
+ const lastResult = webperfConfig.lastResult || null;
36
+
37
+ if (lastResult && typeof lastResult === "object" && lastResult.metrics) {
38
+ const verdict = evaluateCwv(lastResult.metrics, lastResult.thresholds || webperfConfig.thresholds);
39
+ return {
40
+ available: true,
41
+ mode: "cwv-result",
42
+ url: lastResult.url || webperfConfig.url || null,
43
+ capturedAt: lastResult.capturedAt || null,
44
+ cwv: verdict,
45
+ thresholds: lastResult.thresholds || webperfConfig.thresholds || CWV_THRESHOLDS,
46
+ };
47
+ }
48
+
49
+ // No live measurement → static hints only when the project actually has
50
+ // a frontend layer to audit. Backend-only projects get nothing.
51
+ const isFrontendish = !stack || stack.isFrontend === true || stack.isFullstack === true;
52
+ if (!isFrontendish) {
53
+ return { available: false, reason: "project is backend-only — no frontend-perf hints to give" };
54
+ }
55
+
56
+ return { available: true, mode: "static-hints" };
57
+ }
58
+
59
+ /**
60
+ * Render a CWV verdict (from collectWebPerfInput's cwv-result mode) as
61
+ * concise prompt-friendly markdown. Kept independent from the prompt
62
+ * builder so it stays unit-testable in isolation.
63
+ */
64
+ export function formatCwvVerdict(input) {
65
+ if (!input || input.mode !== "cwv-result" || !input.cwv) return null;
66
+ const { cwv, url, capturedAt } = input;
67
+ const lines = [];
68
+ if (url) lines.push(`- Target URL: ${url}`);
69
+ if (capturedAt) lines.push(`- Captured: ${capturedAt}`);
70
+ lines.push(`- Verdict: ${cwv.pass ? "PASS" : "FAIL"}`);
71
+ if (Object.keys(cwv.scores || {}).length) {
72
+ lines.push("- Scores:");
73
+ for (const [metric, score] of Object.entries(cwv.scores)) {
74
+ lines.push(` - ${metric.toUpperCase()}: ${score.value} (${score.rating})`);
75
+ }
76
+ }
77
+ if (cwv.blocking?.length) {
78
+ lines.push("- Blocking metrics (above poor threshold):");
79
+ for (const b of cwv.blocking) {
80
+ lines.push(` - ${b.metric.toUpperCase()} ${b.value} > ${b.threshold}`);
81
+ }
82
+ }
83
+ if (cwv.advisory?.length) {
84
+ lines.push("- Advisory metrics (between good and poor):");
85
+ for (const a of cwv.advisory) {
86
+ lines.push(` - ${a.metric.toUpperCase()} ${a.value} > good=${a.threshold}`);
87
+ }
88
+ }
89
+ return lines.join("\n");
90
+ }
@@ -51,7 +51,7 @@ const Operation = v.looseObject({
51
51
  acceptance: v.pipe(v.array(AcceptanceTest), v.minLength(1)),
52
52
  });
53
53
 
54
- export const CanvasSchema = v.looseObject({
54
+ const CanvasSchema = v.looseObject({
55
55
  // Title is the project / feature name. The planner uses it as the
56
56
  // plan name unless the user overrides on prompt.
57
57
  title: NonEmptyString,
@@ -60,7 +60,7 @@ function createCoreBinaryCheck(bin) {
60
60
  /**
61
61
  * Docker check. Manual strategy (platform-dependent install).
62
62
  */
63
- export function createDockerCheck() {
63
+ function createDockerCheck() {
64
64
  return {
65
65
  name: "docker",
66
66
  label: "Docker",
@@ -80,7 +80,7 @@ export function createDockerCheck() {
80
80
  /**
81
81
  * Serena MCP (optional). Prompt strategy: we know how to install it.
82
82
  */
83
- export function createSerenaCheck() {
83
+ function createSerenaCheck() {
84
84
  return {
85
85
  name: "serena",
86
86
  label: "Serena MCP",
package/src/checks/ci.js CHANGED
@@ -37,7 +37,7 @@ function createWorkflowCheck(workflow) {
37
37
  };
38
38
  }
39
39
 
40
- export function createGhCliCheck() {
40
+ function createGhCliCheck() {
41
41
  return {
42
42
  name: "ci:gh",
43
43
  label: "CI: gh CLI",
@@ -55,7 +55,7 @@ export function createGhCliCheck() {
55
55
  };
56
56
  }
57
57
 
58
- export function createGhSecretsCheck() {
58
+ function createGhSecretsCheck() {
59
59
  return {
60
60
  name: "ci:secrets",
61
61
  label: "CI: GitHub secrets",
@@ -20,7 +20,7 @@ import { STRATEGY } from "./types.js";
20
20
  /**
21
21
  * kj.config.yml presence check.
22
22
  */
23
- export function createConfigFileCheck() {
23
+ function createConfigFileCheck() {
24
24
  return {
25
25
  name: "config",
26
26
  label: "Config file",
@@ -42,7 +42,7 @@ export function createConfigFileCheck() {
42
42
  /**
43
43
  * Review rules (.md) informational check.
44
44
  */
45
- export function createReviewRulesCheck() {
45
+ function createReviewRulesCheck() {
46
46
  return {
47
47
  name: "review-rules",
48
48
  label: "Reviewer rules (.md)",
@@ -63,7 +63,7 @@ export function createReviewRulesCheck() {
63
63
  /**
64
64
  * Coder rules (.md) informational check.
65
65
  */
66
- export function createCoderRulesCheck() {
66
+ function createCoderRulesCheck() {
67
67
  return {
68
68
  name: "coder-rules",
69
69
  label: "Coder rules (.md)",
@@ -104,7 +104,7 @@ function findDuplicateTomlKeys(content) {
104
104
  /**
105
105
  * Claude config (~/.claude.json) validity.
106
106
  */
107
- export function createClaudeConfigCheck() {
107
+ function createClaudeConfigCheck() {
108
108
  return {
109
109
  name: "agent-config:claude",
110
110
  label: "Agent config: claude (~/.claude.json)",
@@ -135,7 +135,7 @@ export function createClaudeConfigCheck() {
135
135
  /**
136
136
  * Codex config (~/.codex/config.toml) validity.
137
137
  */
138
- export function createCodexConfigCheck() {
138
+ function createCodexConfigCheck() {
139
139
  return {
140
140
  name: "agent-config:codex",
141
141
  label: "Agent config: codex (~/.codex/config.toml)",
@@ -173,7 +173,7 @@ export function createCodexConfigCheck() {
173
173
  /**
174
174
  * KJ config (~/.karajan/kj.config.yml) YAML validity (separate from existence).
175
175
  */
176
- export function createKjConfigYamlCheck() {
176
+ function createKjConfigYamlCheck() {
177
177
  return {
178
178
  name: "agent-config:karajan",
179
179
  label: "Agent config: karajan (kj.config.yml)",
@@ -15,6 +15,10 @@ const REQUIRED_DIRS = [
15
15
  "logs",
16
16
  ];
17
17
 
18
+ /**
19
+ * @internal Exported for dynamic import from tests/checks/dir-setup.test.js
20
+ * and tests/checks/auto-remediation-e2e.test.js. Knip false positive.
21
+ */
18
22
  export function createKarajanDirsCheck() {
19
23
  return {
20
24
  name: "karajan-dirs",
@@ -86,6 +86,9 @@ async function pingMcpStdio(command, args) {
86
86
  /**
87
87
  * karajan-mcp health — stdio server bundled with this package.
88
88
  * Strategy: auto (we can re-spawn it ourselves).
89
+ *
90
+ * @internal Exported for dynamic import from tests/checks/mcp-health.test.js.
91
+ * Knip false positive.
89
92
  */
90
93
  export function createKarajanMcpCheck() {
91
94
  return {
@@ -121,6 +124,9 @@ export function createKarajanMcpCheck() {
121
124
  /**
122
125
  * Serena MCP — external tool the user installs themselves. Warn only when
123
126
  * enabled in config. Strategy: manual (we don't manage its lifecycle).
127
+ *
128
+ * @internal Exported for dynamic import from tests/checks/mcp-health.test.js.
129
+ * Knip false positive.
124
130
  */
125
131
  export function createSerenaMcpCheck() {
126
132
  return {
@@ -24,6 +24,10 @@ function extractPortFromHost(host, fallback) {
24
24
  return m ? Number(m[1]) : fallback;
25
25
  }
26
26
 
27
+ /**
28
+ * @internal Exported for dynamic import from tests/checks/ports.test.js
29
+ * and tests/checks/auto-remediation-e2e.test.js. Knip false positive.
30
+ */
27
31
  export function createSonarPortCheck() {
28
32
  return {
29
33
  name: "port:sonar",
@@ -133,6 +137,10 @@ export function createSonarPortCheck() {
133
137
  };
134
138
  }
135
139
 
140
+ /**
141
+ * @internal Exported for dynamic import from tests/checks/ports.test.js
142
+ * and tests/checks/auto-remediation-e2e.test.js. Knip false positive.
143
+ */
136
144
  export function createHuBoardPortCheck() {
137
145
  return {
138
146
  name: "port:hu-board",
package/src/checks/rtk.js CHANGED
@@ -8,7 +8,7 @@ import { getInstallCommand } from "../utils/os-detect.js";
8
8
  import { withDocLink } from "../utils/doc-links.js";
9
9
  import { STRATEGY } from "./types.js";
10
10
 
11
- export function createRtkCheck() {
11
+ function createRtkCheck() {
12
12
  return {
13
13
  name: "rtk",
14
14
  label: "RTK (Rust Token Killer)",
@@ -8,10 +8,14 @@ import { isOpenSkillsAvailable } from "../skills/openskills-client.js";
8
8
  import { runCommand } from "../utils/process.js";
9
9
  import { STRATEGY } from "./types.js";
10
10
 
11
- export function skillsEnabled(config) {
11
+ function skillsEnabled(config) {
12
12
  return config?.skills?.enabled !== false;
13
13
  }
14
14
 
15
+ /**
16
+ * @internal Exported for dynamic import from tests/checks/skills.test.js
17
+ * and tests/checks/auto-remediation-e2e.test.js. Knip false positive.
18
+ */
15
19
  export function createOpenSkillsCheck() {
16
20
  return {
17
21
  name: "openskills",
@@ -26,7 +26,7 @@ function sonarHost(config) {
26
26
  * Informative check: Sonar enabled/disabled status.
27
27
  * Produces an OK result with disabled description when Sonar is off.
28
28
  */
29
- export function createSonarStatusCheck() {
29
+ function createSonarStatusCheck() {
30
30
  return {
31
31
  name: "sonarqube",
32
32
  label: "SonarQube",
@@ -14,7 +14,7 @@ function getPackageVersion() {
14
14
  return JSON.parse(readFileSync(pkgPath, "utf8")).version;
15
15
  }
16
16
 
17
- export function createKarajanVersionCheck() {
17
+ function createKarajanVersionCheck() {
18
18
  return {
19
19
  name: "karajan",
20
20
  label: "Karajan Code",
@@ -26,7 +26,7 @@ export function createKarajanVersionCheck() {
26
26
  };
27
27
  }
28
28
 
29
- export function createGitRepoCheck() {
29
+ function createGitRepoCheck() {
30
30
  return {
31
31
  name: "git",
32
32
  label: "Git repository",
@@ -146,4 +146,4 @@ export function getTokenChecks(config) {
146
146
  }
147
147
 
148
148
  // Re-export for tests that need to introspect the mapping.
149
- export { PROVIDER_TO_CLI, normalizeProvider };
149
+ export { normalizeProvider };
@@ -91,15 +91,3 @@ export const STRATEGY = Object.freeze({
91
91
  NONE: "none",
92
92
  });
93
93
 
94
- export const SEVERITY = Object.freeze({
95
- FAIL: "fail",
96
- WARN: "warn",
97
- INFO: "info",
98
- });
99
-
100
- /**
101
- * Whether the final status blocks the pipeline (for preflight).
102
- */
103
- export function isBlocking(status) {
104
- return status === STATUS.FAIL || status === STATUS.TIMEOUT;
105
- }
@@ -90,10 +90,16 @@ export function registerMeta(program, { pkgVersion }) {
90
90
  .description("Analyze codebase health (read-only)")
91
91
  .argument("[task]", "Task description. If absent, defaults to a full-codebase analysis. Use --task-file to point at a .md.")
92
92
  .option("--task-file <path>", "Read the task from a file (e.g. .md)")
93
- .option("--dimensions <list>", "Comma-separated: security,quality,performance,architecture,testing", "all")
93
+ .option("--dimensions <list>", "Comma-separated: security,quality,performance,architecture,testing,accessibility (default: all; accessibility auto-skipped on detected backend-only projects)", "all")
94
94
  .option("--json", "Output raw JSON")
95
95
  .option("--agent-readiness", "Score the repo for AI-agent readability (llms.txt, SKILL.md coverage, page token budgets, robots allowlist, heading hierarchy). LLM-free; uses [path] or cwd as the audit target. See issue #542.")
96
96
  .option("--path <dir>", "Path to audit (used with --agent-readiness; defaults to cwd)")
97
+ .option("--no-sonar", "Skip the SonarQube findings collector (faster, less context). Sonar findings are also skipped automatically when SonarQube is unreachable.")
98
+ .option("--no-osv", "Skip the OSV-Scanner vulnerability collector. Findings are also skipped automatically when osv-scanner is not installed.")
99
+ .option("--no-semgrep", "Skip the Semgrep SAST collector. Findings are also skipped automatically when semgrep is not installed (install via 'pipx install semgrep' or 'brew install semgrep').")
100
+ .option("--report-file <path>", "Write the audit report to disk in addition to stdout. <path> may be a file (extension drives format: .md or .json) or a directory (creates audit-<ISO>.<md|json> inside). $KJ_AUDIT_REPORT_DIR env var is used as default directory if no --report-file is given.")
101
+ .option("--deterministic-only", "Skip the LLM analysis entirely. Print/persist only the deterministic findings (basalCost, sonar, stack, growth-delta, webperf). Zero tokens spent. Compatible with --report-file and --json.")
102
+ .option("-y, --yes", "Auto-confirm the 'Continue with LLM analysis?' prompt. Useful in scripts that want the full audit non-interactively. CI/non-TTY paths already auto-confirm without this flag.")
97
103
  .action(async (task, flags) => {
98
104
  await withConfig(pkgVersion, "audit", flags, async ({ config, logger }) => {
99
105
  let resolvedTask = task;
@@ -101,12 +107,20 @@ export function registerMeta(program, { pkgVersion }) {
101
107
  const { readTaskFile } = await import("../utils/task-file.js");
102
108
  resolvedTask = await readTaskFile(flags.taskFile, { projectDir: config.projectDir });
103
109
  }
110
+ // commander resolves --no-sonar to flags.sonar=false (negation flag).
111
+ // We translate to the explicit `noSonar: true` shape AuditRole expects.
104
112
  await auditCommand({
105
113
  task: resolvedTask || "Analyze the full codebase",
106
114
  config, logger,
107
115
  dimensions: flags.dimensions, json: flags.json,
108
116
  agentReadiness: Boolean(flags.agentReadiness),
109
117
  path: flags.path,
118
+ noSonar: flags.sonar === false,
119
+ noOsv: flags.osv === false,
120
+ noSemgrep: flags.semgrep === false,
121
+ reportFile: flags.reportFile || null,
122
+ deterministicOnly: Boolean(flags.deterministicOnly),
123
+ yes: Boolean(flags.yes),
110
124
  });
111
125
  });
112
126
  });