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.
- package/README.md +1 -1
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/audit/basal-cost.js +92 -9
- package/src/audit/deterministic-summary.js +200 -0
- package/src/audit/osv-findings.js +162 -0
- package/src/audit/semgrep-findings.js +117 -0
- package/src/audit/sonar-findings.js +81 -0
- package/src/audit/webperf-input.js +90 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +3 -3
- package/src/checks/ci.js +2 -2
- package/src/checks/config-files.js +6 -6
- package/src/checks/dir-setup.js +4 -0
- package/src/checks/mcp-health.js +6 -0
- package/src/checks/ports.js +91 -29
- package/src/checks/rtk.js +1 -1
- package/src/checks/skills.js +5 -1
- package/src/checks/sonar.js +2 -2
- package/src/checks/system.js +3 -3
- package/src/checks/tokens.js +1 -1
- package/src/checks/types.js +0 -12
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +218 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +265 -30
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/doctor.js +1 -1
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +78 -0
- package/src/config.js +30 -632
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +11 -11
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +7 -3
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +301 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +431 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/feedback-queue.js +0 -1
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2077
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +81 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +20 -7
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/role-output-compressor.js +0 -1
- package/src/orchestrator/session-journal.js +4 -41
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +84 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -17
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -28
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +37 -6
- package/src/prompts/audit.js +206 -6
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/prompts/rtk-snippet.js +1 -1
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/audit-role.js +125 -13
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +2 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/api.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/display/event-handlers.js +1 -1
- package/src/utils/display/header.js +1 -1
- package/src/utils/display/session-summary.js +3 -3
- package/src/utils/display/solomon.js +1 -1
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/role-assigner.js +1 -1
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- 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
|
|
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
|
|
47
|
+
setBrainDecisions(session, []);
|
|
46
48
|
}
|
|
47
49
|
return { session, max };
|
|
48
50
|
}
|
package/src/brain/decisor.js
CHANGED
|
@@ -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
|
|