karajan-code 2.8.0 → 2.10.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 +14 -1
- package/package.json +1 -1
- package/src/audit/agent-readiness.js +21 -3
- 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 +136 -0
- package/src/canvas/schema.js +1 -1
- package/src/checks/binaries.js +2 -2
- 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 +8 -0
- package/src/checks/rtk.js +1 -1
- package/src/checks/skills.js +5 -1
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +2 -2
- package/src/checks/tokens.js +1 -1
- package/src/checks/types.js +0 -12
- package/src/cli/register-meta.js +40 -2
- package/src/cli/register-pipeline.js +1 -0
- package/src/commands/audit.js +239 -28
- package/src/commands/board.js +7 -4
- package/src/commands/doctor.js +1 -1
- package/src/commands/webperf.js +164 -0
- package/src/config/defaults.js +1 -0
- package/src/config/overrides.js +1 -0
- package/src/config/schema.js +1 -0
- package/src/config/test-harness.js +0 -1
- package/src/mcp/handlers/run-handler.js +2 -2
- package/src/mcp/run-kj.js +1 -0
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +1 -0
- package/src/orchestrator/ci-integration.js +1 -1
- package/src/orchestrator/config-init.js +3 -1
- package/src/orchestrator/direct-actions.js +1 -1
- package/src/orchestrator/drivers/error-recovery.js +2 -2
- package/src/orchestrator/drivers/iteration-loop.js +3 -13
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +12 -0
- package/src/orchestrator/drivers/pre-loop.js +1 -4
- package/src/orchestrator/feedback-queue.js +0 -1
- package/src/orchestrator/integrations.js +0 -7
- package/src/orchestrator/role-output-compressor.js +0 -1
- package/src/orchestrator/session-journal.js +5 -41
- package/src/orchestrator/stages/perf-stage.js +140 -0
- package/src/orchestrator/stages/stage-classes.js +0 -2
- package/src/plan/plan-store.js +0 -14
- package/src/planning-game/client.js +0 -14
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/audit.js +206 -6
- package/src/prompts/rtk-snippet.js +1 -1
- package/src/roles/audit-role.js +125 -13
- package/src/roles/perf-role.js +122 -0
- package/src/skills/addyosmani-role-map.js +6 -0
- package/src/sonar/api.js +1 -1
- 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/role-assigner.js +1 -1
- package/src/webperf/scanner.js +209 -0
|
@@ -119,7 +119,7 @@ export async function handleStandbyResult({ stageResult, session, emitter, event
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
|
|
123
123
|
for (const alert of alerts) {
|
|
124
124
|
emitProgress(emitter, makeEvent("brain:rules-alert", { ...eventBase, stage: "brain" }, {
|
|
125
125
|
status: alert.severity === "critical" ? "fail" : "warn",
|
|
@@ -198,7 +198,7 @@ export async function handleSolomonCheck({ config, session, emitter, eventBase,
|
|
|
198
198
|
return { action: "continue" };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i }) {
|
|
202
202
|
if (!rulesResult.hasCritical || !askQuestion) return null;
|
|
203
203
|
|
|
204
204
|
const alertSummary = rulesResult.alerts
|
|
@@ -44,23 +44,13 @@ import { runQualityGateStages } from "./iteration-phases/quality-gates.js";
|
|
|
44
44
|
import { runReviewerGateStage } from "./iteration-phases/reviewer-gate.js";
|
|
45
45
|
import { handleApprovedReview } from "./iteration-phases/handle-approved.js";
|
|
46
46
|
|
|
47
|
-
// Re-export for back-compat with any external caller; the canonical
|
|
48
|
-
// definitions now live in ./iteration-phases/.
|
|
49
|
-
export { runCoderAndRefactorerStages } from "./iteration-phases/coder-and-refactorer.js";
|
|
50
|
-
export { runGuardStages } from "./iteration-phases/guards.js";
|
|
51
|
-
export { runQualityGateStages } from "./iteration-phases/quality-gates.js";
|
|
52
|
-
export { runReviewerGateStage } from "./iteration-phases/reviewer-gate.js";
|
|
53
|
-
export { handleApprovedReview } from "./iteration-phases/handle-approved.js";
|
|
54
|
-
|
|
55
47
|
// `runCoderAndRefactorerStages`, `runGuardStages`, `runQualityGateStages`,
|
|
56
48
|
// `runReviewerGateStage` and `handleApprovedReview` were extracted to
|
|
57
49
|
// ./iteration-phases/ in the v2.7.x audit follow-up (same pattern as
|
|
58
|
-
// pre-loop-phases/). The
|
|
59
|
-
//
|
|
60
|
-
// is unchanged.
|
|
61
|
-
|
|
50
|
+
// pre-loop-phases/). The back-compat re-exports were removed in
|
|
51
|
+
// KJC-TSK-0354 PR-D — no caller imported them via this module.
|
|
62
52
|
|
|
63
|
-
|
|
53
|
+
async function runSingleIteration(ctx) {
|
|
64
54
|
// Use plannedTask (HU-scoped or planner-enriched) over the raw original task.
|
|
65
55
|
// When running per-HU sub-pipelines, plannedTask is the HU's text, not the full spec.
|
|
66
56
|
const { config, logger, emitter, eventBase, session, iteration: i } = ctx;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
runTddCheckStage, runSonarStage, runSonarCloudStage,
|
|
19
19
|
} from "../../iteration-stages.js";
|
|
20
20
|
import { runImpeccableStage } from "../../post-loop-stages.js";
|
|
21
|
+
import { runPerfStage } from "../../stages/perf-stage.js";
|
|
21
22
|
import { tryCiComment } from "../../ci-integration.js";
|
|
22
23
|
|
|
23
24
|
export async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults, coderRole, pipelineFlags, brainCtx }) {
|
|
@@ -72,5 +73,16 @@ export async function runQualityGateStages({ config, logger, emitter, eventBase,
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
if (pipelineFlags?.perfEnabled) {
|
|
77
|
+
const perfResult = await runPerfStage({
|
|
78
|
+
config, logger, emitter, eventBase, session, trackBudget,
|
|
79
|
+
iteration: i, task, brainCtx
|
|
80
|
+
});
|
|
81
|
+
if (perfResult.stageResult) {
|
|
82
|
+
stageResults.perf = perfResult.stageResult;
|
|
83
|
+
}
|
|
84
|
+
if (perfResult.action === "continue") return { action: "continue" };
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
return { action: "ok" };
|
|
76
88
|
}
|
|
@@ -52,9 +52,6 @@ import { emitConfigDeprecations } from "./pre-loop-phases/config-deprecations.js
|
|
|
52
52
|
import { ensureAddyosmaniSkills } from "./pre-loop-phases/ensure-addyosmani-skills.js";
|
|
53
53
|
import { maybeGenerateAutoHuBatch } from "./pre-loop-phases/auto-hu-batch.js";
|
|
54
54
|
|
|
55
|
-
// Re-export for back-compat with any external caller; the canonical
|
|
56
|
-
// definitions now live in ./pre-loop-phases/.
|
|
57
|
-
export { ensureAddyosmaniSkills };
|
|
58
55
|
import {
|
|
59
56
|
applyTriageOverrides, applyAutoSimplify, applyFlagOverrides,
|
|
60
57
|
resolvePipelinePolicies, updateGitignoreForStack,
|
|
@@ -380,7 +377,7 @@ export async function runPreLoopStages({ config, logger, emitter, eventBase, ses
|
|
|
380
377
|
|
|
381
378
|
return { plannedTask, updatedConfig };
|
|
382
379
|
}
|
|
383
|
-
|
|
380
|
+
async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx }) {
|
|
384
381
|
let researchContext = null;
|
|
385
382
|
let plannedTask = task;
|
|
386
383
|
|
|
@@ -72,13 +72,6 @@ export function unregisterIntegration(name) {
|
|
|
72
72
|
registry.delete(name);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/**
|
|
76
|
-
* Clear the registry. Test-only; production never calls this.
|
|
77
|
-
*/
|
|
78
|
-
export function __clearIntegrations() {
|
|
79
|
-
registry.clear();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
75
|
/**
|
|
83
76
|
* List registered integration names. Useful for bootstrap logging.
|
|
84
77
|
* @returns {string[]}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// Creates .reviews/session_*/ directories containing markdown files per stage.
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { runCommand } from "../utils/process.js";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Create the journal directory for a session.
|
|
@@ -174,29 +173,6 @@ export function formatDecision({ timestamp, trigger, context, action, reasoning
|
|
|
174
173
|
return lines.join("\n");
|
|
175
174
|
}
|
|
176
175
|
|
|
177
|
-
// ── Tree of affected files ───────────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
export async function generateFileTree(baseRef) {
|
|
180
|
-
try {
|
|
181
|
-
const result = await runCommand("git", ["diff", "--name-status", `${baseRef}...HEAD`]);
|
|
182
|
-
if (result.exitCode !== 0 || !result.stdout?.trim()) return null;
|
|
183
|
-
|
|
184
|
-
const lines = ["# Affected Files\n"];
|
|
185
|
-
const statusMap = { M: "modified", A: "added", D: "deleted", R: "renamed" };
|
|
186
|
-
|
|
187
|
-
for (const line of result.stdout.trim().split("\n")) {
|
|
188
|
-
const [status, ...fileParts] = line.split("\t");
|
|
189
|
-
const file = fileParts.join("\t");
|
|
190
|
-
const label = statusMap[status?.[0]] || status;
|
|
191
|
-
lines.push(`- [${label}] ${file}`);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return lines.join("\n");
|
|
195
|
-
} catch {
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
176
|
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
201
177
|
|
|
202
178
|
export function generateSummary({ task, result, sessionId, iterations, durationMs, budget, stages, commits, files }) {
|
|
@@ -263,6 +239,7 @@ export function buildPlanSummary({ pipelineFlags, config, stageResults, task })
|
|
|
263
239
|
if (pipelineFlags.testerEnabled) activeStages.push("Tester");
|
|
264
240
|
if (pipelineFlags.securityEnabled) activeStages.push("Security");
|
|
265
241
|
if (pipelineFlags.impeccableEnabled) activeStages.push("Impeccable");
|
|
242
|
+
if (pipelineFlags.perfEnabled) activeStages.push("Perf");
|
|
266
243
|
lines.push(`│ Stages: ${activeStages.join(" → ")}`);
|
|
267
244
|
|
|
268
245
|
// Config
|
|
@@ -325,20 +302,7 @@ export async function writeDecisionsJournal(journalDir, decisionEntries) {
|
|
|
325
302
|
await writeJournalFile(journalDir, "decisions.md", content);
|
|
326
303
|
}
|
|
327
304
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const tree = await generateFileTree(baseRef);
|
|
333
|
-
if (tree) await writeJournalFile(journalDir, "tree.txt", tree);
|
|
334
|
-
return Boolean(tree);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Write the session summary.
|
|
339
|
-
*/
|
|
340
|
-
export async function writeSummaryJournal(journalDir, summaryData) {
|
|
341
|
-
const files = summaryData.files || [];
|
|
342
|
-
const content = generateSummary({ ...summaryData, files: [...files, "summary.md"] });
|
|
343
|
-
await writeJournalFile(journalDir, "summary.md", content);
|
|
344
|
-
}
|
|
305
|
+
// writeTreeJournal / writeSummaryJournal / generateFileTree were
|
|
306
|
+
// superseded in v2.7.x by src/session/journal/{tree-writer,summary-writer}.js
|
|
307
|
+
// (the V2 split). post-loop.js dynamically imports the V2 versions; this
|
|
308
|
+
// orchestrator-side copy is no longer wired anywhere. KJC-TSK-0354 PR-D.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PerfStage — webperf quality gate inside the iteration loop.
|
|
3
|
+
*
|
|
4
|
+
* KJC-TSK-0151. Wraps PerfRole and slots into the existing
|
|
5
|
+
* runQualityGateStages flow alongside Sonar and Impeccable. Designed
|
|
6
|
+
* to be the simplest possible quality gate:
|
|
7
|
+
*
|
|
8
|
+
* - PASS verdict (cwv.pass === true) → return ok, iteration continues.
|
|
9
|
+
* - FAIL verdict → set reviewer feedback with the blocking metrics
|
|
10
|
+
* so the coder receives concrete guidance next iteration. Returns
|
|
11
|
+
* `continue` to retry the iteration loop. No repeat-detector +
|
|
12
|
+
* retry-limit machinery: rerunning Lighthouse on the same URL
|
|
13
|
+
* gives the same result unless the coder changes something, and
|
|
14
|
+
* the iteration limit (max_iterations) is the natural ceiling.
|
|
15
|
+
* - Scanner unavailable (lighthouse missing, URL unreachable, timeout)
|
|
16
|
+
* → log a warn and return ok-skipped. Best-effort, never blocks
|
|
17
|
+
* the pipeline by itself.
|
|
18
|
+
*
|
|
19
|
+
* The role surface (PerfRole) is responsible for persisting the result
|
|
20
|
+
* to ~/.karajan/webperf/<slug>/last.json so subsequent `kj audit` runs
|
|
21
|
+
* pick it up via the auto-discovery added in #602.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { PerfRole } from "../../roles/perf-role.js";
|
|
25
|
+
import { setReviewerFeedback } from "../../session/mutators.js";
|
|
26
|
+
import { saveSession } from "../../session/store.js";
|
|
27
|
+
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build the human-readable feedback line that goes back to the coder
|
|
31
|
+
* on FAIL. Concrete enough that the LLM can act on it without re-reading
|
|
32
|
+
* the whole report.
|
|
33
|
+
*/
|
|
34
|
+
function buildFeedback(roleResult) {
|
|
35
|
+
const { result } = roleResult;
|
|
36
|
+
const score = typeof result.performanceScore === "number" ? result.performanceScore : null;
|
|
37
|
+
const blocking = (result.cwv?.blocking || []).map(b => `${b.metric.toUpperCase()}=${Math.round(b.value)} (poor>${b.threshold})`);
|
|
38
|
+
const top = (result.issues || []).slice(0, 3).map(i => `${i.id}${i.savingsMs ? ` (~${Math.round(i.savingsMs)}ms)` : ""}`);
|
|
39
|
+
|
|
40
|
+
const lines = [`Webperf gate FAILED${score !== null ? ` (score ${score}/100)` : ""}.`];
|
|
41
|
+
if (blocking.length > 0) lines.push(`Blocking metrics: ${blocking.join(", ")}.`);
|
|
42
|
+
if (top.length > 0) lines.push(`Top opportunities: ${top.join(", ")}.`);
|
|
43
|
+
lines.push("Address the blocking metric(s) before the next iteration; rerunning without code changes will produce the same verdict.");
|
|
44
|
+
return lines.join(" ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run the perf gate against config.webperf.url.
|
|
49
|
+
*
|
|
50
|
+
* @returns {Promise<{action: "ok"|"continue", stageResult?: object, summary?: string}>}
|
|
51
|
+
*/
|
|
52
|
+
export async function runPerfStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, task: _task, brainCtx }) {
|
|
53
|
+
if (!session) {
|
|
54
|
+
throw new Error("runPerfStage: `session` is required.");
|
|
55
|
+
}
|
|
56
|
+
logger.setContext({ iteration, stage: "perf" });
|
|
57
|
+
emitProgress(
|
|
58
|
+
emitter,
|
|
59
|
+
makeEvent("perf:start", { ...eventBase, stage: "perf" }, {
|
|
60
|
+
message: "WebPerf gate scanning",
|
|
61
|
+
detail: { url: config?.webperf?.url || null, preset: config?.webperf?.preset || "desktop" },
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const role = new PerfRole({ config, logger, emitter });
|
|
66
|
+
const roleResult = await role.execute();
|
|
67
|
+
|
|
68
|
+
// Track budget — the role doesn't spawn an LLM but it does consume wall
|
|
69
|
+
// time; surface that in the budget summary alongside the agent calls.
|
|
70
|
+
if (trackBudget) {
|
|
71
|
+
try { trackBudget("perf", { tokens_in: 0, tokens_out: 0, cost_usd: 0 }); } catch { /* best-effort */ }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Scanner unavailable → skip with warn. PerfRole returns ok=false for
|
|
75
|
+
// both "verdict FAIL" and "scanner failed", so we differentiate on the
|
|
76
|
+
// result.error field (set only by the second case).
|
|
77
|
+
if (!roleResult.ok && roleResult.result?.error) {
|
|
78
|
+
logger.warn(`Perf gate skipped: ${roleResult.result.error}`);
|
|
79
|
+
emitProgress(emitter, makeEvent("perf:end", { ...eventBase, stage: "perf" }, {
|
|
80
|
+
status: "warn",
|
|
81
|
+
message: `Perf gate skipped — ${roleResult.result.error}`,
|
|
82
|
+
}));
|
|
83
|
+
return { action: "ok", stageResult: { skipped: true, reason: roleResult.result.error } };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// PASS verdict
|
|
87
|
+
if (roleResult.ok === true) {
|
|
88
|
+
emitProgress(emitter, makeEvent("perf:end", { ...eventBase, stage: "perf" }, {
|
|
89
|
+
status: "ok",
|
|
90
|
+
message: roleResult.summary,
|
|
91
|
+
}));
|
|
92
|
+
return {
|
|
93
|
+
action: "ok",
|
|
94
|
+
stageResult: {
|
|
95
|
+
verdict: "PASS",
|
|
96
|
+
url: roleResult.result?.url,
|
|
97
|
+
performanceScore: roleResult.result?.performanceScore,
|
|
98
|
+
cwv: roleResult.result?.cwv,
|
|
99
|
+
savedPath: roleResult.result?.savedPath,
|
|
100
|
+
summary: roleResult.summary,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// FAIL verdict → push concrete feedback to the coder for next iteration.
|
|
106
|
+
const feedback = buildFeedback(roleResult);
|
|
107
|
+
setReviewerFeedback(session, feedback);
|
|
108
|
+
await saveSession(session);
|
|
109
|
+
|
|
110
|
+
// Brain: when enabled, surface perf feedback on the queue too.
|
|
111
|
+
if (brainCtx?.enabled) {
|
|
112
|
+
try {
|
|
113
|
+
const { processRoleOutput } = await import("../brain-coordinator.js");
|
|
114
|
+
processRoleOutput(brainCtx, {
|
|
115
|
+
roleName: "perf",
|
|
116
|
+
output: { verdict: "fail", summary: feedback },
|
|
117
|
+
iteration,
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logger.warn(`Perf gate: brain queue push failed (non-blocking): ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
emitProgress(emitter, makeEvent("perf:end", { ...eventBase, stage: "perf" }, {
|
|
125
|
+
status: "fail",
|
|
126
|
+
message: roleResult.summary,
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
action: "continue",
|
|
131
|
+
stageResult: {
|
|
132
|
+
verdict: "FAIL",
|
|
133
|
+
url: roleResult.result?.url,
|
|
134
|
+
performanceScore: roleResult.result?.performanceScore,
|
|
135
|
+
cwv: roleResult.result?.cwv,
|
|
136
|
+
issues: roleResult.result?.issues,
|
|
137
|
+
summary: feedback,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/src/plan/plan-store.js
CHANGED
|
@@ -114,20 +114,6 @@ export async function loadPlan(projectDir, planId) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
* Update a plan on disk (partial merge).
|
|
119
|
-
* @returns {Promise<boolean>}
|
|
120
|
-
*/
|
|
121
|
-
export async function updatePlan(projectDir, planId, patch) {
|
|
122
|
-
const plan = await loadPlan(projectDir, planId);
|
|
123
|
-
if (!plan) return false;
|
|
124
|
-
Object.assign(plan, patch);
|
|
125
|
-
plan.updatedAt = new Date().toISOString();
|
|
126
|
-
const filePath = path.join(plansDir(projectDir), `${planId}.json`);
|
|
127
|
-
await fs.writeFile(filePath, JSON.stringify(plan, null, 2), "utf8");
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
117
|
/**
|
|
132
118
|
* Delete a plan from disk.
|
|
133
119
|
* @returns {Promise<boolean>}
|
|
@@ -93,20 +93,6 @@ export async function createCard({ projectId, card, timeoutMs = DEFAULT_TIMEOUT_
|
|
|
93
93
|
return parseJsonResponse(response);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
export async function createAdr({ projectId, adr, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
97
|
-
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/adrs`;
|
|
98
|
-
const response = await fetchWithRetry(
|
|
99
|
-
url,
|
|
100
|
-
{
|
|
101
|
-
method: "POST",
|
|
102
|
-
headers: { "Content-Type": "application/json" },
|
|
103
|
-
body: JSON.stringify(adr)
|
|
104
|
-
},
|
|
105
|
-
timeoutMs
|
|
106
|
-
);
|
|
107
|
-
return parseJsonResponse(response);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
96
|
export async function relateCards({ projectId, sourceCardId, targetCardId, relationType, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
111
97
|
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards/relate`;
|
|
112
98
|
const response = await fetchWithRetry(
|
|
@@ -42,7 +42,7 @@ export async function initPgAdapter({ session: _session, config, logger, pgTaskI
|
|
|
42
42
|
* Handle PG task decomposition after triage.
|
|
43
43
|
* Creates subtask cards in PG when triage recommends decomposition.
|
|
44
44
|
*/
|
|
45
|
-
|
|
45
|
+
async function handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger }) {
|
|
46
46
|
const shouldDecompose = triageResult.stageResult?.shouldDecompose
|
|
47
47
|
&& triageResult.stageResult.subtasks?.length > 1
|
|
48
48
|
&& pgTaskId
|
package/src/prompts/audit.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
2
|
+
import { groupIssuesBySeverity } from "../audit/sonar-findings.js";
|
|
3
|
+
import { groupVulnerabilitiesBySeverity } from "../audit/osv-findings.js";
|
|
4
|
+
import { groupFindingsBySeverity as groupSemgrepBySeverity } from "../audit/semgrep-findings.js";
|
|
5
|
+
import { formatCwvVerdict } from "../audit/webperf-input.js";
|
|
6
|
+
|
|
7
|
+
const MAX_SONAR_ISSUES_IN_PROMPT = 50;
|
|
8
|
+
const MAX_OSV_VULNS_IN_PROMPT = 50;
|
|
9
|
+
const MAX_SEMGREP_FINDINGS_IN_PROMPT = 50;
|
|
2
10
|
|
|
3
11
|
const SUBAGENT_PREAMBLE = [
|
|
4
12
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -7,14 +15,20 @@ const SUBAGENT_PREAMBLE = [
|
|
|
7
15
|
"DO NOT modify any files — this is a read-only analysis. Only use: Read, Grep, Glob, Bash (for analysis commands like wc, find, git log, du, npm ls)."
|
|
8
16
|
].join(" ");
|
|
9
17
|
|
|
10
|
-
export const AUDIT_DIMENSIONS = ["security", "codeQuality", "performance", "architecture", "testing"];
|
|
18
|
+
export const AUDIT_DIMENSIONS = ["security", "codeQuality", "performance", "architecture", "testing", "accessibility"];
|
|
19
|
+
|
|
20
|
+
// Dimensions that only apply to frontend / fullstack projects. Auto-dropped
|
|
21
|
+
// from the default dimension list when stack detection knows the project
|
|
22
|
+
// is backend-only — preserves activation when the user explicitly opts in
|
|
23
|
+
// via --dimensions=accessibility (KJC-TSK-0359).
|
|
24
|
+
const FRONTEND_ONLY_DIMENSIONS = new Set(["accessibility"]);
|
|
11
25
|
|
|
12
26
|
const VALID_HEALTH = new Set(["good", "fair", "poor", "critical"]);
|
|
13
27
|
const VALID_SCORES = new Set(["A", "B", "C", "D", "F"]);
|
|
14
28
|
const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
|
|
15
29
|
const VALID_IMPACT = new Set(["high", "medium", "low"]);
|
|
16
30
|
|
|
17
|
-
export function buildAuditPrompt({ task, instructions, dimensions = null, context = null, basalCost = null, growthDelta = null }) {
|
|
31
|
+
export function buildAuditPrompt({ task, instructions, dimensions = null, context = null, basalCost = null, growthDelta = null, stack = null, sonarFindings = null, webperf = null, osvFindings = null, semgrepFindings = null }) {
|
|
18
32
|
const sections = [SUBAGENT_PREAMBLE];
|
|
19
33
|
|
|
20
34
|
if (instructions) {
|
|
@@ -27,9 +41,46 @@ export function buildAuditPrompt({ task, instructions, dimensions = null, contex
|
|
|
27
41
|
"DO NOT modify any files. This is a READ-ONLY analysis."
|
|
28
42
|
);
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
// Stack hint — when detected, tells the LLM what kind of project this is
|
|
45
|
+
// so it can filter out irrelevant heuristics (e.g. don't flag bundle size
|
|
46
|
+
// in an Express-only API; don't flag N+1 queries in a static Astro site).
|
|
47
|
+
// Detected by src/utils/stack-detect.js — KJC-TSK-0358.
|
|
48
|
+
if (stack && (stack.frameworks?.length > 0 || stack.language || stack.isFrontend || stack.isBackend)) {
|
|
49
|
+
const stackLines = ["## Project Stack"];
|
|
50
|
+
if (stack.language) stackLines.push(`- Language: ${stack.language}`);
|
|
51
|
+
if (stack.frameworks?.length) stackLines.push(`- Frameworks: ${stack.frameworks.join(", ")}`);
|
|
52
|
+
const tier = stack.isFullstack
|
|
53
|
+
? "fullstack (frontend + backend)"
|
|
54
|
+
: stack.isFrontend
|
|
55
|
+
? "frontend-only"
|
|
56
|
+
: stack.isBackend
|
|
57
|
+
? "backend-only"
|
|
58
|
+
: "unknown";
|
|
59
|
+
stackLines.push(`- Tier: ${tier}`);
|
|
60
|
+
if (stack.isFullstack) {
|
|
61
|
+
stackLines.push("Apply BOTH backend heuristics (queries, sync I/O, caching) AND frontend heuristics (bundle size, lazy loading, render-blocking) where each layer applies.");
|
|
62
|
+
} else if (stack.isFrontend) {
|
|
63
|
+
stackLines.push("This project is frontend-only. SKIP findings about N+1 queries, sync file I/O in request handlers, missing pagination on list endpoints, and other backend-specific patterns. Focus on bundle size, lazy loading, render-blocking resources, image optimisation, and accessibility hints.");
|
|
64
|
+
} else if (stack.isBackend) {
|
|
65
|
+
stackLines.push("This project is backend-only. SKIP findings about bundle size, lazy loading, render-blocking resources, image optimisation, and frontend accessibility. Focus on queries, sync I/O, pagination, caching, auth/authz, and dependency hygiene.");
|
|
66
|
+
}
|
|
67
|
+
sections.push(stackLines.join("\n"));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// When dimensions are explicit, honour them verbatim — even frontend-only
|
|
71
|
+
// dimensions on a backend project (the user knows what they want).
|
|
72
|
+
// When dimensions are absent, default to all but drop frontend-only
|
|
73
|
+
// dimensions if the stack is provably backend-only. Unknown stack falls
|
|
74
|
+
// back to the full list (no false negatives — better an extra section
|
|
75
|
+
// than a missing one).
|
|
76
|
+
let activeDimensions;
|
|
77
|
+
if (dimensions) {
|
|
78
|
+
activeDimensions = dimensions.filter(d => AUDIT_DIMENSIONS.includes(d));
|
|
79
|
+
} else if (stack && stack.isBackend && !stack.isFrontend) {
|
|
80
|
+
activeDimensions = AUDIT_DIMENSIONS.filter(d => !FRONTEND_ONLY_DIMENSIONS.has(d));
|
|
81
|
+
} else {
|
|
82
|
+
activeDimensions = AUDIT_DIMENSIONS;
|
|
83
|
+
}
|
|
33
84
|
|
|
34
85
|
if (activeDimensions.includes("security")) {
|
|
35
86
|
sections.push(
|
|
@@ -105,9 +156,32 @@ export function buildAuditPrompt({ task, instructions, dimensions = null, contex
|
|
|
105
156
|
);
|
|
106
157
|
}
|
|
107
158
|
|
|
159
|
+
if (activeDimensions.includes("accessibility")) {
|
|
160
|
+
sections.push(
|
|
161
|
+
"## Accessibility Analysis (WCAG 2.x)",
|
|
162
|
+
[
|
|
163
|
+
"- Missing alt text on <img> tags (decorative images need empty alt=\"\", not absent)",
|
|
164
|
+
"- Form inputs without associated <label> or aria-label",
|
|
165
|
+
"- Heading hierarchy violations (h1 → h3 skipping h2; multiple h1 per page)",
|
|
166
|
+
"- Buttons / links containing only icons without aria-label or visually-hidden text",
|
|
167
|
+
"- Interactive elements built from <div>/<span> without role + keyboard handlers",
|
|
168
|
+
"- ARIA roles misused (presentation on focusable elements, redundant on native semantics)",
|
|
169
|
+
"- Focus management gaps (modals without focus trap, no visible :focus styles, tabindex misuse)",
|
|
170
|
+
"- Colour-only signalling (status indicated by red/green only, no icon or text)",
|
|
171
|
+
"- Static contrast — flag suspicious low-contrast colour pairs in CSS variables / token files",
|
|
172
|
+
"- Missing lang attribute on <html>, missing skip-to-content link, missing landmarks"
|
|
173
|
+
].join("\n"),
|
|
174
|
+
"Skip findings about colour contrast that require runtime rendering (e.g. computed styles); flag the suspect CSS values and recommend an axe-core/Lighthouse runtime check."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// JSON schema — keep the per-dimension entries in sync with AUDIT_DIMENSIONS.
|
|
179
|
+
const schemaDimensions = AUDIT_DIMENSIONS
|
|
180
|
+
.map(d => `"${d}":{"score":"A|B|C|D|F","findings":[]}`)
|
|
181
|
+
.join(",");
|
|
108
182
|
sections.push(
|
|
109
183
|
"Return a single valid JSON object and nothing else.",
|
|
110
|
-
|
|
184
|
+
`JSON schema: {"ok":true,"result":{"summary":{"overallHealth":"good|fair|poor|critical","totalFindings":number,"critical":number,"high":number,"medium":number,"low":number},"dimensions":{${schemaDimensions}},"topRecommendations":[{"priority":number,"dimension":string,"action":string,"impact":"high|medium|low","effort":"high|medium|low"}]},"summary":string}`,
|
|
111
185
|
'Each finding: {"severity":"critical|high|medium|low","file":string,"line":number,"rule":string,"description":string,"recommendation":string}',
|
|
112
186
|
`Only include dimensions you were asked to analyze: ${activeDimensions.join(", ")}`
|
|
113
187
|
);
|
|
@@ -146,6 +220,132 @@ export function buildAuditPrompt({ task, instructions, dimensions = null, contex
|
|
|
146
220
|
sections.push(lines.join("\n"));
|
|
147
221
|
}
|
|
148
222
|
|
|
223
|
+
// Sonar findings — KJC-TSK-0361. Deterministic input from a tool the
|
|
224
|
+
// codebase already runs (kj run + SonarStage). The audit reads existing
|
|
225
|
+
// open issues + quality-gate state instead of re-scanning. Capped at
|
|
226
|
+
// MAX_SONAR_ISSUES_IN_PROMPT total entries (50) across all severities to
|
|
227
|
+
// bound prompt size; the LLM is told the truncated count so it can
|
|
228
|
+
// prioritise.
|
|
229
|
+
if (sonarFindings?.available && (sonarFindings.total > 0 || sonarFindings.qualityGate)) {
|
|
230
|
+
const lines = ["## SonarQube Findings"];
|
|
231
|
+
if (sonarFindings.qualityGate?.status) {
|
|
232
|
+
lines.push(`- Quality gate: ${sonarFindings.qualityGate.status}`);
|
|
233
|
+
}
|
|
234
|
+
if (sonarFindings.total > 0) {
|
|
235
|
+
lines.push(`- Total open issues: ${sonarFindings.total}`);
|
|
236
|
+
const groups = groupIssuesBySeverity(sonarFindings.issues);
|
|
237
|
+
let remaining = MAX_SONAR_ISSUES_IN_PROMPT;
|
|
238
|
+
for (const [severity, issues] of Object.entries(groups)) {
|
|
239
|
+
if (issues.length === 0) continue;
|
|
240
|
+
lines.push("");
|
|
241
|
+
lines.push(`### ${severity} (${issues.length})`);
|
|
242
|
+
const slice = issues.slice(0, Math.max(0, remaining));
|
|
243
|
+
for (const issue of slice) {
|
|
244
|
+
const loc = issue.component ? `${issue.component}${issue.line ? `:${issue.line}` : ""}` : "";
|
|
245
|
+
const rule = issue.rule ? ` [${issue.rule}]` : "";
|
|
246
|
+
lines.push(` - ${loc}${rule} ${issue.message || ""}`.trim());
|
|
247
|
+
}
|
|
248
|
+
if (issues.length > slice.length) {
|
|
249
|
+
lines.push(` - ... and ${issues.length - slice.length} more in ${severity}`);
|
|
250
|
+
}
|
|
251
|
+
remaining -= slice.length;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
lines.push("");
|
|
255
|
+
lines.push("Cross-reference these with your own findings. When your finding overlaps a Sonar rule, mention the rule ID in the `rule` field — that's a high-confidence match. Findings the LLM raises that Sonar can't see (architecture, naming, API design) are still valuable; mark them with rule names like \"AUDIT-ARCH-N\".");
|
|
256
|
+
sections.push(lines.join("\n"));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// WebPerf — KJC-TSK-0360. Two modes: a real CWV measurement when one is
|
|
260
|
+
// surfaced via config.webperf.lastResult (future `kj webperf` command)
|
|
261
|
+
// OR static frontend-perf hints for the LLM to look at the source code
|
|
262
|
+
// when no live measurement is available. Backend-only projects skip the
|
|
263
|
+
// section entirely (collectWebPerfInput returns available:false).
|
|
264
|
+
if (webperf?.available) {
|
|
265
|
+
const lines = ["## WebPerf — Frontend Performance"];
|
|
266
|
+
if (webperf.mode === "cwv-result") {
|
|
267
|
+
lines.push("Live Core Web Vitals measurement available — incorporate the verdict below into the performance dimension findings:");
|
|
268
|
+
lines.push("");
|
|
269
|
+
lines.push(formatCwvVerdict(webperf));
|
|
270
|
+
lines.push("");
|
|
271
|
+
lines.push("If a metric is `poor`, recommend a concrete code change (preconnect, lazy-load, image format, critical CSS, defer script). Cite the offending file or pattern when you can find it in the source.");
|
|
272
|
+
} else {
|
|
273
|
+
lines.push("No live CWV measurement was provided. Audit the source for these static frontend-perf patterns and include findings under the `performance` dimension:");
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push("- Render-blocking `<link rel=\"stylesheet\">` or `<script>` without `defer`/`async` in `<head>`");
|
|
276
|
+
lines.push("- Large JavaScript bundles imported eagerly that could be `await import(...)` or split by route");
|
|
277
|
+
lines.push("- Non-modern image formats (only .jpg/.png) where webp/avif are widely supported");
|
|
278
|
+
lines.push("- `<img>` without `width`/`height` attributes (causes CLS)");
|
|
279
|
+
lines.push("- `<img>` without `loading=\"lazy\"` for off-screen images, or videos auto-playing without `preload=\"metadata\"`");
|
|
280
|
+
lines.push("- Web fonts loaded without `font-display: swap` or without `<link rel=\"preconnect\">` to the font origin");
|
|
281
|
+
lines.push("- Whole-library imports for one helper (e.g. `import _ from 'lodash'` instead of `lodash/get`)");
|
|
282
|
+
lines.push("- Missing critical CSS extraction or above-the-fold inlining strategy in build config");
|
|
283
|
+
lines.push("- Large third-party scripts (analytics, A/B, chat widgets) without defer + facade pattern");
|
|
284
|
+
lines.push("- Unbounded list rendering without virtualisation for very long collections");
|
|
285
|
+
}
|
|
286
|
+
sections.push(lines.join("\n"));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// OSV vulnerabilities — KJC-TSK-0365. Deterministic CVE/GHSA findings
|
|
290
|
+
// from osv-scanner across the dependency manifest. Capped at
|
|
291
|
+
// MAX_OSV_VULNS_IN_PROMPT entries; the LLM is told the truncated count.
|
|
292
|
+
// Findings fold into the `security` dimension with the upstream
|
|
293
|
+
// identifier (CVE-/GHSA-) as the rule.
|
|
294
|
+
if (osvFindings?.available && (osvFindings.total > 0)) {
|
|
295
|
+
const lines = ["## OSV Vulnerabilities"];
|
|
296
|
+
lines.push(`- Total open vulnerabilities in dependencies: ${osvFindings.total}`);
|
|
297
|
+
const groups = groupVulnerabilitiesBySeverity(osvFindings.vulnerabilities);
|
|
298
|
+
let remaining = MAX_OSV_VULNS_IN_PROMPT;
|
|
299
|
+
for (const [severity, vulns] of Object.entries(groups)) {
|
|
300
|
+
if (vulns.length === 0) continue;
|
|
301
|
+
lines.push("");
|
|
302
|
+
lines.push(`### ${severity} (${vulns.length})`);
|
|
303
|
+
const slice = vulns.slice(0, Math.max(0, remaining));
|
|
304
|
+
for (const vuln of slice) {
|
|
305
|
+
const where = vuln.package ? `${vuln.package}@${vuln.version || "?"}` : "(unknown package)";
|
|
306
|
+
const fixed = vuln.fixed ? ` → fixed in ${vuln.fixed}` : "";
|
|
307
|
+
lines.push(` - ${vuln.id} ${where}${fixed}: ${vuln.summary || ""}`.trim());
|
|
308
|
+
}
|
|
309
|
+
if (vulns.length > slice.length) {
|
|
310
|
+
lines.push(` - ... and ${vulns.length - slice.length} more in ${severity}`);
|
|
311
|
+
}
|
|
312
|
+
remaining -= slice.length;
|
|
313
|
+
}
|
|
314
|
+
lines.push("");
|
|
315
|
+
lines.push("These CVE/GHSA findings are GROUND TRUTH — incorporate them into the `security` dimension. Use the OSV id (CVE-XXXX-XXXX or GHSA-xxxx-xxxx-xxxx) as the `rule` field. Recommend the fixed version when available.");
|
|
316
|
+
sections.push(lines.join("\n"));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Semgrep SAST findings — KJC-TSK-0366. Static analysis catches what
|
|
320
|
+
// sonar/CVE-DBs miss: SQL/Cmd injection, XSS, hardcoded secrets,
|
|
321
|
+
// taint flow, language-specific anti-patterns. Capped at
|
|
322
|
+
// MAX_SEMGREP_FINDINGS_IN_PROMPT entries; LLM is told these are
|
|
323
|
+
// deterministic and asked to use rule IDs as-is in findings.
|
|
324
|
+
if (semgrepFindings?.available && semgrepFindings.total > 0) {
|
|
325
|
+
const lines = ["## Semgrep SAST Findings"];
|
|
326
|
+
lines.push(`- Total findings: ${semgrepFindings.total}`);
|
|
327
|
+
const groups = groupSemgrepBySeverity(semgrepFindings.findings);
|
|
328
|
+
let remaining = MAX_SEMGREP_FINDINGS_IN_PROMPT;
|
|
329
|
+
for (const [severity, findings] of Object.entries(groups)) {
|
|
330
|
+
if (findings.length === 0) continue;
|
|
331
|
+
lines.push("");
|
|
332
|
+
lines.push(`### ${severity} (${findings.length})`);
|
|
333
|
+
const slice = findings.slice(0, Math.max(0, remaining));
|
|
334
|
+
for (const f of slice) {
|
|
335
|
+
const loc = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : "";
|
|
336
|
+
const cwe = f.cwe ? ` (${Array.isArray(f.cwe) ? f.cwe.join(", ") : f.cwe})` : "";
|
|
337
|
+
lines.push(` - ${loc} [${f.rule}]${cwe}: ${f.message || ""}`.trim());
|
|
338
|
+
}
|
|
339
|
+
if (findings.length > slice.length) {
|
|
340
|
+
lines.push(` - ... and ${findings.length - slice.length} more in ${severity}`);
|
|
341
|
+
}
|
|
342
|
+
remaining -= slice.length;
|
|
343
|
+
}
|
|
344
|
+
lines.push("");
|
|
345
|
+
lines.push("These SAST findings are GROUND TRUTH — fold ERROR-severity ones into the `security` dimension as critical/high findings. Use the semgrep rule id (e.g. javascript.express.security.audit.xss.direct-response-write) verbatim in the `rule` field; that lets the developer look it up in the semgrep registry.");
|
|
346
|
+
sections.push(lines.join("\n"));
|
|
347
|
+
}
|
|
348
|
+
|
|
149
349
|
if (context) {
|
|
150
350
|
sections.push(`## Context\n${context}`);
|
|
151
351
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* RTK (Rust Token Killer) prompt instruction snippet.
|
|
3
3
|
* Injected into coder/reviewer prompts when RTK is detected.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
const RTK_INSTRUCTIONS = [
|
|
6
6
|
"## Token Optimization (RTK detected)",
|
|
7
7
|
"RTK is installed. Prefix ALL Bash tool calls with `rtk` to reduce token usage:",
|
|
8
8
|
"- Use `rtk git status` instead of `git status`",
|