karajan-code 4.29.1 → 4.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/postinstall.js +5 -1
- package/scripts/verify-pack-mcp.mjs +76 -0
- package/scripts/verify-pack.mjs +29 -0
- package/src/audit/deterministic-summary.js +24 -0
- package/src/audit/env-key-findings.js +162 -0
- package/src/checks/method.js +37 -2
- package/src/checks/rag-coverage.js +50 -0
- package/src/cli/register-pipeline.js +1 -0
- package/src/commands/check.js +16 -5
- package/src/commands/env.js +12 -0
- package/src/commands/go.js +17 -2
- package/src/commands/harden.js +4 -1
- package/src/commands/hu.js +13 -1
- package/src/commands/init.js +38 -5
- package/src/commands/review-gate.js +50 -2
- package/src/harden/harness-hooks.js +2 -0
- package/src/harden/hook-templates.js +3 -2
- package/src/harden/sentinel-hooks.js +92 -5
- package/src/harden/workflow-engine.js +6 -1
- package/src/harden/workflow-templates.js +1 -2
- package/src/identity/bootstrap.js +2 -2
- package/src/rag/coverage.js +48 -0
- package/src/review/one-shot-review.js +3 -0
- package/src/review/rag-ledger.js +41 -0
- package/src/review/rag-requirement.js +127 -0
- package/src/review/solomon-arbitration.js +6 -1
- package/src/roles/audit-role.js +12 -2
- package/src/utils/pending-user-action.js +7 -4
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0849 (ADR 0010, RAG-C) — a diff that touches source files enters
|
|
3
|
+
* the review only if the session's RAG ledger (RAG-A) shows the RAG answered
|
|
4
|
+
* about every staged source: the file itself or a sibling of its directory.
|
|
5
|
+
* A file NEW in the diff cannot have been returned yet: it counts as covered
|
|
6
|
+
* once the session consulted at all (the ADR's fallback A, same as the
|
|
7
|
+
* Sentinel gate). Docs-only diffs are exempt. The only other way through is
|
|
8
|
+
* a live HUMAN grant on `method.rag.code` — never an env var: an escape the
|
|
9
|
+
* agent can set for itself is the hole this closes.
|
|
10
|
+
*
|
|
11
|
+
* The block also names the TWINS: sources the RAG returned for the same
|
|
12
|
+
* concepts that are not in the diff. That list travels to the reviewer,
|
|
13
|
+
* because the bugs of 2026-09-16 were exactly the twin nobody touched.
|
|
14
|
+
*/
|
|
15
|
+
import { sourceFilesOf } from "./tests-with-code.js";
|
|
16
|
+
|
|
17
|
+
export const RAG_RULE_ID = "method.rag.code";
|
|
18
|
+
|
|
19
|
+
const GRANT_HINT = `Only a human grant lifts this: kj policy grant --rule ${RAG_RULE_ID} --until <iso> --reason "<why>"`;
|
|
20
|
+
const dirOf = (p) => (p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : "");
|
|
21
|
+
|
|
22
|
+
function liveGrant(standingExceptions, now) {
|
|
23
|
+
return standingExceptions.find((e) => e.rule_id === RAG_RULE_ID && new Date(e.expiresAt).getTime() > now.getTime()) || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} args
|
|
28
|
+
* @param {string[]} args.stagedFiles - every path of the diff
|
|
29
|
+
* @param {string[]} [args.newFiles] - paths ADDED by the diff (not in HEAD)
|
|
30
|
+
* @param {{available: boolean, reason?: string, queries: object[], hits: string[]}} [args.ledger]
|
|
31
|
+
* @returns {{ok: boolean, mode: "docs-only"|"pass"|"granted"|"block", reason?: string, grant?: object, sources?: string[], queries?: number, covered?: string[], uncovered?: string[], twinsUntouched?: string[]}}
|
|
32
|
+
*/
|
|
33
|
+
export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles = [], ledger, standingExceptions = [], now = new Date(), env = {} }) {
|
|
34
|
+
const { sources } = sourceFilesOf(config, stagedFiles);
|
|
35
|
+
if (sources.length === 0) return { ok: true, mode: "docs-only" };
|
|
36
|
+
|
|
37
|
+
const grant = liveGrant(standingExceptions, now);
|
|
38
|
+
if (grant) return { ok: true, mode: "granted", grant, sources };
|
|
39
|
+
|
|
40
|
+
// An env override is REJECTED here, explicitly: whatever KJ_ALLOW_* the
|
|
41
|
+
// process carries is named in the reason and changes nothing. The Sentinel
|
|
42
|
+
// escape opens an edit; it never opens the review.
|
|
43
|
+
const ignored = Object.keys(env || {}).filter((k) => k.startsWith("KJ_ALLOW_") && env[k] === "1");
|
|
44
|
+
const overrideNote = ignored.length > 0 ? ` (${ignored.join(", ")} is not honoured here)` : "";
|
|
45
|
+
const head = `The RAG must have answered about code before it is reviewed (${sources.length} staged source${sources.length === 1 ? "" : "s"})${overrideNote}`;
|
|
46
|
+
// KJC-BUG-0192: no Sentinel in this tree used to PASS, which made "do not
|
|
47
|
+
// harden" the comfortable way around the gate. Nobody recording the session
|
|
48
|
+
// is a reason to install the harness, or to ask for a grant, never a silent
|
|
49
|
+
// exemption: the proof of the method cannot depend on the project choosing
|
|
50
|
+
// to install whoever records it.
|
|
51
|
+
if (ledger?.harness === false) {
|
|
52
|
+
return { ok: false, mode: "block", sources, reason: `${head} — no Sentinel harness in this tree, so no session ledger can exist: install it with \`kj harden\`. ${GRANT_HINT}` };
|
|
53
|
+
}
|
|
54
|
+
// A harness that does not match the installed kj (edited, emptied, or older
|
|
55
|
+
// than the ledger) cannot vouch for anything: fail closed, like the tamper
|
|
56
|
+
// check does, until the human regenerates it.
|
|
57
|
+
if (ledger?.harness === true && ledger?.verified === false) {
|
|
58
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the Sentinel harness does not match the installed kj (${(ledger.mismatched || []).join(", ") || "scripts missing"}): the human runs \`kj harden\` to regenerate it before code is reviewed. ${GRANT_HINT}` };
|
|
59
|
+
}
|
|
60
|
+
if (!ledger?.available) {
|
|
61
|
+
return { ok: false, mode: "block", sources, reason: `${head} — ${ledger?.reason || "no session ledger"}: consult the RAG about the change (kj_rag_query / kj rag query) and run \`kj review --staged\` again. ${GRANT_HINT}` };
|
|
62
|
+
}
|
|
63
|
+
const hits = ledger.hits || [];
|
|
64
|
+
const queries = (ledger.queries || []).length;
|
|
65
|
+
const fresh = new Set(newFiles);
|
|
66
|
+
const answered = (f) => hits.includes(f) || hits.some((h) => dirOf(h) === dirOf(f)) || (fresh.has(f) && queries > 0);
|
|
67
|
+
const covered = sources.filter(answered);
|
|
68
|
+
const uncovered = sources.filter((f) => !answered(f));
|
|
69
|
+
const staged = new Set(stagedFiles);
|
|
70
|
+
const twinsUntouched = hits.filter((h) => !staged.has(h));
|
|
71
|
+
const evidence = { queries, covered, uncovered, twinsUntouched };
|
|
72
|
+
if (uncovered.length > 0) {
|
|
73
|
+
const list = `${uncovered.slice(0, 5).join(", ")}${uncovered.length > 5 ? "…" : ""}`;
|
|
74
|
+
return { ok: false, mode: "block", sources, ...evidence, reason: `${head} — no query of this session returned ${uncovered.length} of them (${list}): ask what they do and where else that concept lives, then run \`kj review --staged\` again. ${GRANT_HINT}` };
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, mode: "pass", sources, ...evidence };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The pre-commit side (`kj review --check`): the verdict's `rag` block is the
|
|
81
|
+
* evidence, recomputed against the CURRENT staged sources like the sonar
|
|
82
|
+
* block — the diff hash pins the file set, so a source missing from
|
|
83
|
+
* `covered` was never answered about, whatever the block claims.
|
|
84
|
+
*/
|
|
85
|
+
export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness = true, verified = true, mismatched = [], standingExceptions = [], now = new Date() }) {
|
|
86
|
+
const { sources } = sourceFilesOf(config, stagedFiles);
|
|
87
|
+
if (sources.length === 0) return { ok: true, mode: "docs-only" };
|
|
88
|
+
const grant = liveGrant(standingExceptions, now);
|
|
89
|
+
if (grant) return { ok: true, mode: "granted", grant, sources };
|
|
90
|
+
const head = `The RAG must have answered about code before it is committed (${sources.length} staged source${sources.length === 1 ? "" : "s"})`;
|
|
91
|
+
// KJC-BUG-0192: same rule on the --check side, so the commit gate cannot be
|
|
92
|
+
// opened by removing the supervisor either.
|
|
93
|
+
if (!harness) {
|
|
94
|
+
return { ok: false, mode: "block", sources, reason: `${head} — no Sentinel harness in this tree, so no session ledger can exist: install it with \`kj harden\`. ${GRANT_HINT}` };
|
|
95
|
+
}
|
|
96
|
+
if (!verified) {
|
|
97
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the Sentinel harness does not match the installed kj (${mismatched.join(", ") || "scripts missing"}): the human runs \`kj harden\` to regenerate it. ${GRANT_HINT}` };
|
|
98
|
+
}
|
|
99
|
+
if (!rag) {
|
|
100
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the verdict carries no rag block; run \`kj review --staged\` again. ${GRANT_HINT}` };
|
|
101
|
+
}
|
|
102
|
+
// The block's mode is a claim, never an authorization: a grant or a missing
|
|
103
|
+
// harness only count when they hold NOW (checked above). So the block can
|
|
104
|
+
// only prove coverage — a "granted" or "no-harness" it recorded has lapsed.
|
|
105
|
+
if (rag.mode !== "pass" || !Array.isArray(rag.covered)) {
|
|
106
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the verdict's rag block is malformed (mode ${JSON.stringify(rag.mode ?? null)}, covered ${Array.isArray(rag.covered) ? "list" : typeof rag.covered}); run \`kj review --staged\` again. ${GRANT_HINT}` };
|
|
107
|
+
}
|
|
108
|
+
const coveredSet = new Set(rag.covered);
|
|
109
|
+
const uncovered = sources.filter((f) => !coveredSet.has(f));
|
|
110
|
+
if (uncovered.length > 0) {
|
|
111
|
+
const list = `${uncovered.slice(0, 5).join(", ")}${uncovered.length > 5 ? "…" : ""}`;
|
|
112
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the session ledger never covered ${uncovered.length} of them (${list}); consult the RAG and run \`kj review --staged\` again. ${GRANT_HINT}` };
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, mode: "pass", sources };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The `rag` block stored in the verdict, bound to the diff hash like `sonar`. */
|
|
118
|
+
export function ragBlock(req, ledger) {
|
|
119
|
+
return {
|
|
120
|
+
mode: req.mode,
|
|
121
|
+
sessionId: ledger?.sessionId || null,
|
|
122
|
+
queries: req.queries ?? (ledger?.queries || []).length,
|
|
123
|
+
covered: req.covered || [],
|
|
124
|
+
uncovered: req.uncovered || [],
|
|
125
|
+
twinsUntouched: req.twinsUntouched || [],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -102,10 +102,15 @@ export async function runSolomonArbitration({
|
|
|
102
102
|
position, ruling: parsed.ruling, reasoning: parsed.reasoning || "", solomon,
|
|
103
103
|
originalVerdict: { reviewer: verdict.reviewer, issues: verdict.issues },
|
|
104
104
|
};
|
|
105
|
+
// KJC-BUG-0183: the arbiter judges the reviewer's objections, not the
|
|
106
|
+
// evidence bound to the diff (sonar, rag blocks). That evidence was computed
|
|
107
|
+
// for this exact hash and travels with the overriding verdict — without it
|
|
108
|
+
// `kj review --check` refuses every arbitrated diff.
|
|
109
|
+
const evidence = { ...(verdict.sonar ? { sonar: verdict.sonar } : {}), ...(verdict.rag ? { rag: verdict.rag } : {}) };
|
|
105
110
|
const record = parsed.ruling === "approve"
|
|
106
111
|
? await saveVerdict(projectDir, diff, {
|
|
107
112
|
verdict: "approved", reviewer: `solomon:${solomon}`, host: hostAgent || null,
|
|
108
|
-
issues: [], summary: `Arbitration overrode ${verdict.reviewer}'s rejection: ${parsed.reasoning || ""}`.trim(), arbitration,
|
|
113
|
+
issues: [], summary: `Arbitration overrode ${verdict.reviewer}'s rejection: ${parsed.reasoning || ""}`.trim(), arbitration, ...evidence,
|
|
109
114
|
})
|
|
110
115
|
: await saveVerdict(projectDir, diff, { ...verdict, arbitration });
|
|
111
116
|
|
package/src/roles/audit-role.js
CHANGED
|
@@ -14,6 +14,7 @@ import { collectCircularDeps } from "../audit/circular-deps.js";
|
|
|
14
14
|
import { collectDeadExports } from "../audit/dead-exports.js";
|
|
15
15
|
import { collectInjectionFindings } from "../audit/injection-findings.js";
|
|
16
16
|
import { collectAiSlop } from "../audit/ai-slop-findings.js";
|
|
17
|
+
import { collectEnvKeyFindings } from "../audit/env-key-findings.js";
|
|
17
18
|
|
|
18
19
|
function parseDimensions(dimensionsStr) {
|
|
19
20
|
if (!dimensionsStr || dimensionsStr === "all") return null;
|
|
@@ -77,6 +78,7 @@ export class AuditRole extends AgentRole {
|
|
|
77
78
|
let injectionFindings = null;
|
|
78
79
|
let infraFindings = null;
|
|
79
80
|
let aiSlop = null;
|
|
81
|
+
let envKeys = null;
|
|
80
82
|
if (!securityOnly) {
|
|
81
83
|
try {
|
|
82
84
|
basalCost = await measureBasalCost(projectDir);
|
|
@@ -154,13 +156,20 @@ export class AuditRole extends AgentRole {
|
|
|
154
156
|
aiSlop = await collectAiSlop(projectDir);
|
|
155
157
|
} catch { /* ai-slop scan is best-effort */ }
|
|
156
158
|
}
|
|
159
|
+
// KJC-TSK-0845: one key, two resolution rules = a migration left half-way
|
|
160
|
+
// (KJ_HOME → KARAJAN_HOME). Deterministic, offline, best-effort.
|
|
161
|
+
if (!securityOnly) {
|
|
162
|
+
try {
|
|
163
|
+
envKeys = await collectEnvKeyFindings(projectDir);
|
|
164
|
+
} catch { /* env-key scan is best-effort */ }
|
|
165
|
+
}
|
|
157
166
|
// STW-A (KJC-TSK-0789 AC5): record that the security surface was looked
|
|
158
167
|
// at, so the Steward can age it — GREBLA went 79 days with "never".
|
|
159
168
|
try {
|
|
160
169
|
mkdirSync(dirname(securityAuditMarkerPath(projectDir)), { recursive: true });
|
|
161
170
|
writeFileSync(securityAuditMarkerPath(projectDir), JSON.stringify({ at: new Date().toISOString(), mode: securityOnly ? "security" : "full" }));
|
|
162
171
|
} catch { /* recording is best-effort — the audit itself already ran */ }
|
|
163
|
-
return { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings, circularDeps, deadExports, injectionFindings, infraFindings, aiSlop };
|
|
172
|
+
return { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings, circularDeps, deadExports, injectionFindings, infraFindings, aiSlop, envKeys };
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
/**
|
|
@@ -176,7 +185,7 @@ export class AuditRole extends AgentRole {
|
|
|
176
185
|
const context = typeof input === "object" ? input?.context || null : null;
|
|
177
186
|
const dimensions = typeof rawDimensions === "string" ? parseDimensions(rawDimensions) : rawDimensions;
|
|
178
187
|
|
|
179
|
-
const { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings, circularDeps, deadExports } = deterministicCtx;
|
|
188
|
+
const { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings, circularDeps, deadExports, envKeys } = deterministicCtx;
|
|
180
189
|
|
|
181
190
|
const provider = this.resolveProvider();
|
|
182
191
|
const agent = this.createAgentInstance(provider);
|
|
@@ -217,6 +226,7 @@ export class AuditRole extends AgentRole {
|
|
|
217
226
|
semgrepFindings: semgrepFindings?.available ? semgrepFindings : undefined,
|
|
218
227
|
circularDeps: circularDeps?.available ? circularDeps : undefined,
|
|
219
228
|
deadExports: deadExports?.available ? deadExports : undefined,
|
|
229
|
+
envKeys: envKeys?.available ? envKeys : undefined,
|
|
220
230
|
provider
|
|
221
231
|
},
|
|
222
232
|
summary: buildSummary(parsed),
|
|
@@ -64,12 +64,15 @@ export function collectPending(results, { tty = Boolean(process.stdin.isTTY), ye
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** The PENDING USER ACTION block: exact commands for THIS OS, then wait. */
|
|
67
|
-
export function renderPendingBlock(pending, { platform = process.platform, retry = "kj install-tools" } = {}) {
|
|
67
|
+
export function renderPendingBlock(pending, { platform = process.platform, retry = "kj install-tools", why = null } = {}) {
|
|
68
68
|
const lines = [
|
|
69
69
|
"════ PENDING USER ACTION ════════════════════════════════════════",
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// `why` names the real cause when it is not an install (KJC-BUG-0188:
|
|
71
|
+
// an undeclared identity needs the human, not a package manager).
|
|
72
|
+
...(why
|
|
73
|
+
? [`kj could not finish by itself: ${why}.`]
|
|
74
|
+
: ["kj could not finish this install by itself (sudo or a platform", "installer is required)."]),
|
|
75
|
+
"Karajan needs a COMPLETE environment — do not continue degraded.",
|
|
73
76
|
"",
|
|
74
77
|
"Run in YOUR terminal:",
|
|
75
78
|
"",
|