karajan-code 2.7.4 → 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 (192) hide show
  1. package/README.md +1 -1
  2. package/package.json +24 -4
  3. package/scripts/diet-baseline.mjs +170 -0
  4. package/scripts/regen-arch-stats.sh +71 -0
  5. package/src/agents/availability.js +15 -9
  6. package/src/agents/claude-agent.js +143 -45
  7. package/src/agents/model-registry.js +1 -1
  8. package/src/audit/agent-readiness.js +240 -0
  9. package/src/audit/basal-cost.js +92 -9
  10. package/src/audit/deterministic-summary.js +200 -0
  11. package/src/audit/osv-findings.js +162 -0
  12. package/src/audit/semgrep-findings.js +117 -0
  13. package/src/audit/sonar-findings.js +81 -0
  14. package/src/audit/webperf-input.js +90 -0
  15. package/src/bootstrap.js +26 -1
  16. package/src/brain/decision-tracker.js +3 -1
  17. package/src/brain/decisor.js +1 -1
  18. package/src/canvas/parse-md.js +238 -0
  19. package/src/canvas/schema.js +101 -0
  20. package/src/canvas/to-flat.js +85 -0
  21. package/src/canvas/validate.js +163 -0
  22. package/src/checks/binaries.js +3 -3
  23. package/src/checks/ci.js +2 -2
  24. package/src/checks/config-files.js +6 -6
  25. package/src/checks/dir-setup.js +4 -0
  26. package/src/checks/mcp-health.js +6 -0
  27. package/src/checks/ports.js +91 -29
  28. package/src/checks/rtk.js +1 -1
  29. package/src/checks/skills.js +5 -1
  30. package/src/checks/sonar.js +2 -2
  31. package/src/checks/system.js +3 -3
  32. package/src/checks/tokens.js +1 -1
  33. package/src/checks/types.js +0 -12
  34. package/src/cli/_shared.js +30 -0
  35. package/src/cli/register-meta.js +218 -0
  36. package/src/cli/register-pipeline.js +212 -0
  37. package/src/cli/register-plan.js +152 -0
  38. package/src/cli/register-roles-skills.js +38 -0
  39. package/src/cli/register-sonar.js +28 -0
  40. package/src/cli.js +83 -468
  41. package/src/commands/agents.js +18 -6
  42. package/src/commands/architect.js +29 -2
  43. package/src/commands/audit.js +265 -30
  44. package/src/commands/board.js +160 -30
  45. package/src/commands/clean.js +83 -0
  46. package/src/commands/code.js +7 -2
  47. package/src/commands/discover.js +7 -2
  48. package/src/commands/doctor.js +1 -1
  49. package/src/commands/init.js +2 -2
  50. package/src/commands/plan/_shared.js +40 -0
  51. package/src/commands/plan/add-hu.js +21 -0
  52. package/src/commands/plan/delete.js +9 -0
  53. package/src/commands/plan/generate.js +284 -0
  54. package/src/commands/plan/index.js +14 -0
  55. package/src/commands/plan/list.js +21 -0
  56. package/src/commands/plan/ready.js +46 -0
  57. package/src/commands/plan/remove-hu.js +17 -0
  58. package/src/commands/plan/show.js +81 -0
  59. package/src/commands/plan/validate.js +20 -0
  60. package/src/commands/plan.js +14 -291
  61. package/src/commands/researcher.js +7 -2
  62. package/src/commands/resume.js +24 -1
  63. package/src/commands/review.js +7 -2
  64. package/src/commands/run.js +52 -1
  65. package/src/commands/status.js +1 -1
  66. package/src/commands/sync.js +75 -0
  67. package/src/commands/triage.js +7 -2
  68. package/src/commands/undo.js +1 -1
  69. package/src/config/defaults.js +213 -0
  70. package/src/config/loader.js +137 -0
  71. package/src/config/overrides.js +199 -0
  72. package/src/config/role-resolver.js +159 -0
  73. package/src/config/test-harness.js +78 -0
  74. package/src/config.js +30 -632
  75. package/src/git/automation.js +2 -2
  76. package/src/guards/output-guard.js +0 -2
  77. package/src/hu/acceptance-runner.js +52 -10
  78. package/src/hu/store.js +11 -0
  79. package/src/mcp/handlers/direct-handlers.js +2 -5
  80. package/src/mcp/handlers/management-handlers.js +48 -4
  81. package/src/mcp/handlers/run-handler.js +9 -8
  82. package/src/mcp/preflight.js +32 -13
  83. package/src/mcp/server-handlers.js +4 -3
  84. package/src/mcp/suggest-handler.js +3 -2
  85. package/src/mcp/tools.js +15 -0
  86. package/src/orchestrator/agent-fallback.js +1 -1
  87. package/src/orchestrator/ci-integration.js +11 -11
  88. package/src/orchestrator/config-init.js +25 -7
  89. package/src/orchestrator/direct-actions.js +7 -3
  90. package/src/orchestrator/drivers/error-recovery.js +287 -0
  91. package/src/orchestrator/drivers/init-context.js +222 -0
  92. package/src/orchestrator/drivers/iteration-loop.js +301 -0
  93. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  94. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  95. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  96. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  97. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  98. package/src/orchestrator/drivers/post-loop.js +370 -0
  99. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  100. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  101. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  102. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  103. package/src/orchestrator/drivers/pre-loop.js +431 -0
  104. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  105. package/src/orchestrator/feedback-queue.js +0 -1
  106. package/src/orchestrator/flow-control.js +4 -3
  107. package/src/orchestrator/flow-runner.js +121 -2077
  108. package/src/orchestrator/fs-leak-detector.js +156 -0
  109. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  110. package/src/orchestrator/integrations.js +81 -0
  111. package/src/orchestrator/post-loop-stages.js +35 -8
  112. package/src/orchestrator/preflight-checks.js +20 -7
  113. package/src/orchestrator/repair/repair-detector.js +117 -0
  114. package/src/orchestrator/repair/repair-runner.js +142 -0
  115. package/src/orchestrator/reviewer-fallback.js +1 -1
  116. package/src/orchestrator/role-output-compressor.js +0 -1
  117. package/src/orchestrator/session-journal.js +4 -41
  118. package/src/orchestrator/solomon-escalation.js +3 -2
  119. package/src/orchestrator/solomon-rules.js +7 -3
  120. package/src/orchestrator/stages/architect-stage.js +12 -11
  121. package/src/orchestrator/stages/coder-stage.js +56 -7
  122. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  123. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  124. package/src/orchestrator/stages/planner-stage.js +1 -1
  125. package/src/orchestrator/stages/research-stage.js +1 -1
  126. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  127. package/src/orchestrator/stages/sonar-stage.js +94 -10
  128. package/src/orchestrator/stages/stage-classes.js +84 -0
  129. package/src/orchestrator/stages/stage-executor.js +1 -1
  130. package/src/orchestrator/stages/triage-stage.js +1 -1
  131. package/src/orchestrator/standby.js +3 -2
  132. package/src/orchestrator/verification-gate.js +20 -12
  133. package/src/plan/adr-generator.js +170 -0
  134. package/src/plan/adr-loader.js +60 -0
  135. package/src/plan/plan-executor.js +19 -1
  136. package/src/plan/plan-hu-ops.js +170 -1
  137. package/src/plan/plan-reviewer.js +199 -0
  138. package/src/plan/plan-schema.js +69 -1
  139. package/src/plan/plan-store.js +40 -17
  140. package/src/plan/spec-detector.js +61 -0
  141. package/src/plan/tests-synthesizer.js +116 -0
  142. package/src/planning-game/client.js +3 -28
  143. package/src/planning-game/decomposition.js +1 -1
  144. package/src/planning-game/pipeline-adapter.js +37 -6
  145. package/src/prompts/audit.js +206 -6
  146. package/src/prompts/coder.js +172 -1
  147. package/src/prompts/hu-reviewer.js +0 -1
  148. package/src/prompts/planner.js +81 -4
  149. package/src/prompts/rtk-snippet.js +1 -1
  150. package/src/review/diff-generator.js +35 -22
  151. package/src/review/snapshot-diff.js +1 -1
  152. package/src/roles/audit-role.js +125 -13
  153. package/src/roles/coder-role.js +25 -3
  154. package/src/roles/planner-role.js +42 -1
  155. package/src/roles/repairer-role.js +181 -0
  156. package/src/roles/tester-role.js +70 -4
  157. package/src/session/mutators.js +304 -0
  158. package/src/session/runtime-overrides.js +59 -0
  159. package/src/{session-store.js → session/store.js} +2 -2
  160. package/src/skills/semantic-detector.js +10 -5
  161. package/src/skills/skill-detector.js +1 -1
  162. package/src/sonar/api.js +1 -1
  163. package/src/sonar/cloud-scanner.js +0 -1
  164. package/src/sonar/credentials.js +14 -6
  165. package/src/sonar/discovery.js +123 -0
  166. package/src/sonar/manager.js +18 -0
  167. package/src/sync/detect-drift.js +113 -0
  168. package/src/tracker-bootstrap.js +44 -0
  169. package/src/utils/agent-detect.js +15 -11
  170. package/src/utils/board-prompt-bridge.js +109 -0
  171. package/src/utils/cli-progress.js +161 -0
  172. package/src/utils/cwd-confirm.js +39 -0
  173. package/src/utils/derive-project-name-from-cwd.js +119 -0
  174. package/src/utils/display/event-handlers.js +1 -1
  175. package/src/utils/display/header.js +1 -1
  176. package/src/utils/display/session-summary.js +3 -3
  177. package/src/utils/display/solomon.js +1 -1
  178. package/src/utils/garbage-collector.js +396 -0
  179. package/src/utils/git.js +16 -4
  180. package/src/utils/locale.js +1 -1
  181. package/src/utils/paths.js +21 -1
  182. package/src/utils/process.js +1 -1
  183. package/src/utils/prompt-project-name.js +35 -0
  184. package/src/utils/role-assigner.js +1 -1
  185. package/src/utils/run-context.js +75 -0
  186. package/src/utils/run-log.js +12 -0
  187. package/src/utils/task-file.js +1 -1
  188. package/src/utils/telemetry.js +13 -1
  189. package/src/webperf/devtools-detect.js +1 -1
  190. package/templates/coder-rules.md +13 -0
  191. package/templates/roles/coder.md +34 -0
  192. package/templates/roles/repairer.md +94 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * OSV-Scanner findings collector for `kj audit` (KJC-TSK-0365).
3
+ *
4
+ * Wraps Google's `osv-scanner` CLI and feeds its results into the audit
5
+ * prompt as a deterministic vulnerability section. OSV.dev's database
6
+ * pulls from GitHub Advisory Database, GLSA, PyPI advisories, Go vuln
7
+ * DB and others — broader coverage than `npm audit` alone, with no
8
+ * account, token, or upload (the scanner runs locally and only the
9
+ * advisory data is fetched on demand).
10
+ *
11
+ * This module follows the same pattern as src/audit/sonar-findings.js:
12
+ * - Best-effort: missing binary or scan errors return
13
+ * {available: false, reason} and the audit continues without the
14
+ * section.
15
+ * - Severity-grouped: helps the LLM (and the deterministic summary)
16
+ * prioritise the worst issues without scrolling.
17
+ * - Capped: only the first MAX_OSV_VULNS_IN_PROMPT entries land in
18
+ * the prompt, with overflow markers per severity, so an
19
+ * out-of-control result list cannot blow the token budget.
20
+ */
21
+
22
+ import { execFile } from "node:child_process";
23
+ import { promisify } from "node:util";
24
+
25
+ const execFileAsync = promisify(execFile);
26
+
27
+ const SCAN_TIMEOUT_MS = 60_000;
28
+ const SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "MODERATE"];
29
+
30
+ /**
31
+ * Run osv-scanner on the project and return a structured result.
32
+ *
33
+ * @param {string} projectDir - absolute path to the project root
34
+ * @param {object} [logger]
35
+ * @returns {Promise<{available: boolean, reason?: string, total?: number, vulnerabilities?: object[]}>}
36
+ */
37
+ export async function collectOsvFindings(projectDir, logger = null) {
38
+ let stdout;
39
+ try {
40
+ const { stdout: out } = await execFileAsync(
41
+ "osv-scanner",
42
+ ["--format=json", "--recursive", projectDir || "."],
43
+ { cwd: projectDir, timeout: SCAN_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }
44
+ );
45
+ stdout = out;
46
+ } catch (err) {
47
+ // osv-scanner exits with non-zero when vulnerabilities are found, so
48
+ // err.code 1 is NORMAL — we still want the stdout. Real failures are
49
+ // ENOENT (binary missing) or stdout empty.
50
+ if (err.code === "ENOENT") {
51
+ if (logger?.warn) logger.warn("osv-scanner not found in PATH — install via 'go install github.com/google/osv-scanner@latest'");
52
+ return { available: false, reason: "osv-scanner not installed" };
53
+ }
54
+ if (err.killed) {
55
+ if (logger?.warn) logger.warn(`osv-scanner timed out after ${SCAN_TIMEOUT_MS}ms`);
56
+ return { available: false, reason: `osv-scanner timed out after ${SCAN_TIMEOUT_MS}ms` };
57
+ }
58
+ if (typeof err.stdout === "string" && err.stdout.trim().length > 0) {
59
+ stdout = err.stdout;
60
+ } else {
61
+ if (logger?.warn) logger.warn(`osv-scanner failed: ${err.message}`);
62
+ return { available: false, reason: `osv-scanner failed: ${err.message}` };
63
+ }
64
+ }
65
+
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(stdout);
69
+ } catch (err) {
70
+ if (logger?.warn) logger.warn(`osv-scanner output not parseable: ${err.message}`);
71
+ return { available: false, reason: "osv-scanner output not parseable as JSON" };
72
+ }
73
+
74
+ return { available: true, ...parseOsvOutput(parsed) };
75
+ }
76
+
77
+ /**
78
+ * Flatten the osv-scanner JSON shape into a flat list of vulnerabilities.
79
+ * The scanner emits results[].packages[].vulnerabilities[].
80
+ *
81
+ * @param {object} raw - parsed osv-scanner JSON
82
+ * @returns {{total: number, vulnerabilities: object[]}}
83
+ */
84
+ export function parseOsvOutput(raw) {
85
+ const flat = [];
86
+ const results = Array.isArray(raw?.results) ? raw.results : [];
87
+ for (const result of results) {
88
+ const source = result.source?.path || result.source || "";
89
+ const packages = Array.isArray(result.packages) ? result.packages : [];
90
+ for (const pkg of packages) {
91
+ const name = pkg.package?.name || "";
92
+ const version = pkg.package?.version || "";
93
+ const ecosystem = pkg.package?.ecosystem || "";
94
+ const vulns = Array.isArray(pkg.vulnerabilities) ? pkg.vulnerabilities : [];
95
+ const groups = Array.isArray(pkg.groups) ? pkg.groups : [];
96
+ for (const vuln of vulns) {
97
+ flat.push({
98
+ id: vuln.id || vuln.aliases?.[0] || "UNKNOWN",
99
+ aliases: vuln.aliases || [],
100
+ severity: extractSeverity(vuln, groups),
101
+ summary: vuln.summary || vuln.details?.split("\n")[0] || "",
102
+ package: name,
103
+ version,
104
+ ecosystem,
105
+ source,
106
+ fixed: extractFixedVersion(vuln),
107
+ });
108
+ }
109
+ }
110
+ }
111
+ return { total: flat.length, vulnerabilities: flat };
112
+ }
113
+
114
+ /**
115
+ * Resolve a vulnerability's effective severity. osv-scanner reports
116
+ * CVSS (per-vuln) and a separate `groups[].severity` (per scanner run).
117
+ * Group severity wins when present because it reflects scanner policy.
118
+ */
119
+ function extractSeverity(vuln, groups) {
120
+ // Group-level severity (scanner-resolved, includes ecosystem context)
121
+ for (const group of groups) {
122
+ if (group.ids?.includes(vuln.id) && group.severity) {
123
+ return String(group.severity).toUpperCase();
124
+ }
125
+ }
126
+ // Per-vuln CVSS — pick the highest score
127
+ const severities = Array.isArray(vuln.severity) ? vuln.severity : [];
128
+ for (const sev of severities) {
129
+ if (sev?.severity) return String(sev.severity).toUpperCase();
130
+ }
131
+ // database_specific severity (GHSA-style)
132
+ if (vuln.database_specific?.severity) {
133
+ return String(vuln.database_specific.severity).toUpperCase();
134
+ }
135
+ return "UNKNOWN";
136
+ }
137
+
138
+ function extractFixedVersion(vuln) {
139
+ const ranges = vuln.affected?.[0]?.ranges || [];
140
+ for (const range of ranges) {
141
+ const events = range.events || [];
142
+ for (const event of events) {
143
+ if (event.fixed) return event.fixed;
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * Group vulnerabilities by severity, preserving canonical ordering
151
+ * (CRITICAL → HIGH → MEDIUM/MODERATE → LOW → UNKNOWN).
152
+ */
153
+ export function groupVulnerabilitiesBySeverity(vulnerabilities) {
154
+ const groups = Object.fromEntries(SEVERITY_ORDER.map(s => [s, []]));
155
+ groups.UNKNOWN = [];
156
+ for (const vuln of vulnerabilities || []) {
157
+ const sev = (vuln.severity || "UNKNOWN").toUpperCase();
158
+ if (groups[sev]) groups[sev].push(vuln);
159
+ else groups.UNKNOWN.push(vuln);
160
+ }
161
+ return groups;
162
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Semgrep SAST collector for `kj audit` (KJC-TSK-0366).
3
+ *
4
+ * Wraps `semgrep --config auto --json` and feeds structured static-
5
+ * analysis findings into the audit prompt. Equivalent to the SAST half
6
+ * of `snyk code` but free for OSS, no account, runs locally. Semgrep
7
+ * has 2000+ built-in rules across JS/TS/Python/Go/etc. covering OWASP
8
+ * Top 10, taint analysis, hardcoded secrets, regex misuse and language-
9
+ * specific anti-patterns.
10
+ *
11
+ * Same best-effort pattern as sonar-findings.js and osv-findings.js:
12
+ * - Missing binary or timeout returns {available: false, reason}.
13
+ * - 120s timeout (semgrep can be slow on large repos with --config auto).
14
+ * - Severity-grouped (ERROR / WARNING / INFO) and capped at
15
+ * MAX_SEMGREP_FINDINGS_IN_PROMPT (50) total entries with overflow
16
+ * markers.
17
+ * - The LLM is told these are deterministic and asked to fold them
18
+ * into the `security` dimension using the rule ID as the rule field.
19
+ */
20
+
21
+ import { execFile } from "node:child_process";
22
+ import { promisify } from "node:util";
23
+
24
+ const execFileAsync = promisify(execFile);
25
+
26
+ const SCAN_TIMEOUT_MS = 120_000;
27
+ const SEVERITY_ORDER = ["ERROR", "WARNING", "INFO"];
28
+
29
+ /**
30
+ * Run semgrep on the project and return a structured result.
31
+ *
32
+ * @param {string} projectDir - absolute path to the project root
33
+ * @param {object} [logger]
34
+ * @returns {Promise<{available: boolean, reason?: string, total?: number, findings?: object[]}>}
35
+ */
36
+ export async function collectSemgrepFindings(projectDir, logger = null) {
37
+ let stdout;
38
+ try {
39
+ const { stdout: out } = await execFileAsync(
40
+ "semgrep",
41
+ ["--config", "auto", "--json", "--quiet", `--timeout=${Math.floor(SCAN_TIMEOUT_MS / 1000)}`, projectDir || "."],
42
+ { cwd: projectDir, timeout: SCAN_TIMEOUT_MS + 5000, maxBuffer: 32 * 1024 * 1024 }
43
+ );
44
+ stdout = out;
45
+ } catch (err) {
46
+ if (err.code === "ENOENT") {
47
+ if (logger?.warn) logger.warn("semgrep not found in PATH — install via 'pipx install semgrep' or 'brew install semgrep'");
48
+ return { available: false, reason: "semgrep not installed" };
49
+ }
50
+ if (err.killed) {
51
+ if (logger?.warn) logger.warn(`semgrep timed out after ${SCAN_TIMEOUT_MS}ms`);
52
+ return { available: false, reason: `semgrep timed out after ${SCAN_TIMEOUT_MS}ms` };
53
+ }
54
+ // semgrep exits 1 when it finds blocking rules (ERROR severity), but
55
+ // still emits a valid JSON report on stdout. Rescue that case.
56
+ if (typeof err.stdout === "string" && err.stdout.trim().length > 0) {
57
+ stdout = err.stdout;
58
+ } else {
59
+ if (logger?.warn) logger.warn(`semgrep failed: ${err.message}`);
60
+ return { available: false, reason: `semgrep failed: ${err.message}` };
61
+ }
62
+ }
63
+
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(stdout);
67
+ } catch (err) {
68
+ if (logger?.warn) logger.warn(`semgrep output not parseable: ${err.message}`);
69
+ return { available: false, reason: "semgrep output not parseable as JSON" };
70
+ }
71
+
72
+ return { available: true, ...parseSemgrepOutput(parsed, projectDir) };
73
+ }
74
+
75
+ /**
76
+ * Convert semgrep's JSON shape into the flat findings list the prompt
77
+ * builder consumes. Semgrep emits results[] with rule id, severity,
78
+ * path, start/end line, message, fix hints.
79
+ *
80
+ * @param {object} raw - parsed semgrep --json output
81
+ * @param {string} [projectDir] - used to make paths repo-relative
82
+ * @returns {{total: number, findings: object[]}}
83
+ */
84
+ export function parseSemgrepOutput(raw, projectDir = "") {
85
+ const results = Array.isArray(raw?.results) ? raw.results : [];
86
+ const findings = [];
87
+ for (const r of results) {
88
+ const file = r.path && projectDir
89
+ ? r.path.startsWith(projectDir + "/") ? r.path.slice(projectDir.length + 1) : r.path
90
+ : r.path || "";
91
+ findings.push({
92
+ rule: r.check_id || "(unknown)",
93
+ severity: String(r.extra?.severity || "INFO").toUpperCase(),
94
+ file,
95
+ line: r.start?.line ?? 0,
96
+ endLine: r.end?.line ?? 0,
97
+ message: r.extra?.message || "",
98
+ fix: r.extra?.fix || r.extra?.fix_regex || null,
99
+ cwe: r.extra?.metadata?.cwe || null,
100
+ owasp: r.extra?.metadata?.owasp || null,
101
+ });
102
+ }
103
+ return { total: findings.length, findings };
104
+ }
105
+
106
+ /**
107
+ * Group findings by severity in canonical order (ERROR → WARNING → INFO).
108
+ */
109
+ export function groupFindingsBySeverity(findings) {
110
+ const groups = Object.fromEntries(SEVERITY_ORDER.map(s => [s, []]));
111
+ for (const f of findings || []) {
112
+ const sev = (f.severity || "INFO").toUpperCase();
113
+ if (groups[sev]) groups[sev].push(f);
114
+ else groups.INFO.push(f);
115
+ }
116
+ return groups;
117
+ }
@@ -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
+ }
package/src/bootstrap.js CHANGED
@@ -36,7 +36,7 @@ function getPackageVersion() {
36
36
  // ── Individual checks ────────────────────────────────────────────────
37
37
 
38
38
  async function checkGitRepo() {
39
- let ok = false;
39
+ let ok;
40
40
  try {
41
41
  ok = await ensureGitRepo();
42
42
  } catch { /* git may not be installed */
@@ -219,6 +219,31 @@ function formatBootstrapFailure(failures) {
219
219
  * Throws Error with actionable message if any prerequisite fails.
220
220
  */
221
221
  export async function ensureBootstrap(projectDir, config) {
222
+ // Plugin discovery runs unconditionally on every bootstrap, regardless
223
+ // of whether the bootstrap cache says environment is valid. A user
224
+ // dropping a new file into `.karajan/plugins/` expects it to be picked
225
+ // up without running `kj init` again.
226
+ //
227
+ // Pre-v2.7.5 this was never called — `loadPlugins()` was exported but
228
+ // never imported from production code, so any plugin the user added
229
+ // was silently ignored. Fixed in TSK-0329.
230
+ try {
231
+ const { loadPlugins } = await import("./plugins/loader.js");
232
+ await loadPlugins({ projectDir });
233
+ } catch { /* plugin system failed — non-blocking, continue bootstrap */ }
234
+
235
+ // TSK-0339: register the Planning Game adapter in the integrations registry
236
+ // when enabled. The orchestrator no longer imports `src/planning-game/*`
237
+ // directly — it calls `getIntegration("tracker")?.hookName(...)` instead.
238
+ // Other trackers (GitHub Projects, Jira, Linear, …) drop in by writing
239
+ // a sibling adapter that calls `registerIntegration("tracker", …)`.
240
+ if (config?.planning_game?.enabled) {
241
+ try {
242
+ const { registerPgAdapter } = await import("./planning-game/pipeline-adapter.js");
243
+ await registerPgAdapter();
244
+ } catch { /* PG registration failed — non-blocking, continue bootstrap */ }
245
+ }
246
+
222
247
  const cached = await readBootstrapFile(projectDir);
223
248
  if (isBootstrapValid(cached, projectDir)) {
224
249
  return; // Environment already validated
@@ -11,6 +11,8 @@
11
11
  * the session for human input.
12
12
  */
13
13
 
14
+ import { setBrainDecisions } from "../session/mutators.js";
15
+
14
16
  export const DEFAULT_MAX_DECISIONS = 20;
15
17
  export const INDECISION_REPEAT_THRESHOLD = 3; // 3 identical classifications in a row
16
18
 
@@ -42,7 +44,7 @@ export const INDECISION_REPEAT_THRESHOLD = 3; // 3 identical classifications in
42
44
  */
43
45
  export function createTracker(session, { max = DEFAULT_MAX_DECISIONS } = {}) {
44
46
  if (!Array.isArray(session.brainDecisions)) {
45
- session.brainDecisions = [];
47
+ setBrainDecisions(session, []);
46
48
  }
47
49
  return { session, max };
48
50
  }
@@ -125,7 +125,7 @@ function applyOverrides(roles, overrides) {
125
125
  * @param {{ forceRoles?: string[], skipRoles?: string[] }} [args.overrides]
126
126
  * @returns {Decision}
127
127
  */
128
- export function buildDecision({ triage, task, config = {}, overrides = {} } = {}) {
128
+ export function buildDecision({ triage, task: _task, config = {}, overrides = {} } = {}) {
129
129
  const level = triage?.level || "simple";
130
130
  const taskType = triage?.taskType || "sw";
131
131