karajan-code 4.48.0 → 4.48.3
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 +6 -1
- package/src/commands/harden.js +1 -1
- package/src/config/defaults.js +2 -1
- package/src/harden/sentinel-hooks.js +5 -1
- package/src/harden/workflow-engine.js +5 -1
- package/src/harden/workflow-templates.js +10 -2
- package/src/policy/supervisor-verify.js +26 -1
- package/src/rag/auto-update.js +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "4.48.
|
|
3
|
+
"version": "4.48.3",
|
|
4
4
|
"description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0",
|
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
"@babel/parser": "^7.29.7",
|
|
105
105
|
"@karajan-family/governance": "^0.1.0",
|
|
106
106
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
107
|
+
"@xterm/xterm": "^6.0.0",
|
|
107
108
|
"better-sqlite3": "^12.10.0",
|
|
108
109
|
"chokidar": "^5.0.0",
|
|
109
110
|
"commander": "^15.0.0",
|
|
@@ -120,8 +121,12 @@
|
|
|
120
121
|
"sqlite-vec": "^0.1.9",
|
|
121
122
|
"valibot": "^1.4.1",
|
|
122
123
|
"web-tree-sitter": "^0.26.9",
|
|
124
|
+
"ws": "^8.21.3",
|
|
123
125
|
"yaml": "^2.9.0"
|
|
124
126
|
},
|
|
127
|
+
"optionalDependencies": {
|
|
128
|
+
"node-pty": "^1.1.0"
|
|
129
|
+
},
|
|
125
130
|
"devDependencies": {
|
|
126
131
|
"@eslint/js": "^10.0.1",
|
|
127
132
|
"@vitest/coverage-v8": "^4.1.8",
|
package/src/commands/harden.js
CHANGED
|
@@ -186,7 +186,7 @@ export async function hardenCommand({
|
|
|
186
186
|
const cfg = withConfig ? installConfigsForRoots({ projectDir, roots, dryRun }) : null;
|
|
187
187
|
const withCi = ci && profile !== "minimal";
|
|
188
188
|
const wf = withCi
|
|
189
|
-
? installWorkflows({ projectDir, language: roots[0]?.language ?? null, profile, mutation, dryRun })
|
|
189
|
+
? installWorkflows({ projectDir, language: roots[0]?.language ?? null, root: roots[0]?.dir ?? ".", profile, mutation, dryRun })
|
|
190
190
|
: null;
|
|
191
191
|
const withGuidelines = guidelines && profile !== "minimal";
|
|
192
192
|
// KJC-BUG-0199 (issue #1773): the same detected language the workflows and
|
package/src/config/defaults.js
CHANGED
|
@@ -254,7 +254,8 @@ const DEFAULTS = {
|
|
|
254
254
|
// delta re-index before every `kj run`; `onCommit` is consulted by
|
|
255
255
|
// `kj rag install-hooks` to decide whether to install the post-merge
|
|
256
256
|
// hook. Both default ON; flip to false to opt out without uninstalling.
|
|
257
|
-
|
|
257
|
+
// maxFiles (KJC-BUG-0304): past this drift the pre-query refresh answers from the index as it is.
|
|
258
|
+
autoUpdate: { onCommit: true, onRun: true, maxFiles: 25 },
|
|
258
259
|
// KJC-TSK-0682 — sensitivity of the indexed code. Cloud embedders
|
|
259
260
|
// (openai/voyage/cohere/mistral) require an explicit "public"; the
|
|
260
261
|
// safe default blocks them (local ollama/onnx always allowed).
|
|
@@ -1049,7 +1049,11 @@ process.stdin.on("end", () => {
|
|
|
1049
1049
|
// above rules on it) and the ledger only holds in-repo paths.
|
|
1050
1050
|
// KJC-BUG-0190: with nothing indexed, no query can cover anything —
|
|
1051
1051
|
// the gate asks for kj rag index instead of denying the impossible.
|
|
1052
|
-
|
|
1052
|
+
// KJC-BUG-0308: a barrel only re-exports its folder, so the RAG answers with
|
|
1053
|
+
// what it re-exports and never with the barrel: a hit in its own subtree covers it.
|
|
1054
|
+
const BARRELS = ["index.js", "index.ts", "index.mjs", "index.cjs", "index.jsx", "index.tsx", "__init__.py", "mod.rs"];
|
|
1055
|
+
const barrelCovered = dirOf(rel) !== "" && BARRELS.includes(rel.slice(rel.lastIndexOf("/") + 1)) && hits.some((h) => h.startsWith(dirOf(rel) + "/"));
|
|
1056
|
+
const covered = rel.startsWith("..") || hits.includes(rel) || hits.some((h) => dirOf(h) === dirOf(rel)) || barrelCovered || ((rs.rag_queries || []).length > 0 && fresh()) || rs.rag_index_empty === true;
|
|
1053
1057
|
// ADR 0015 (KJC-TSK-0925): no escape. The cases it covered (new file, empty
|
|
1054
1058
|
// index, not indexable) are decided by the gate itself.
|
|
1055
1059
|
if (!covered) {
|
|
@@ -73,10 +73,14 @@ export function installWorkflows({
|
|
|
73
73
|
mutation = false,
|
|
74
74
|
dryRun = false,
|
|
75
75
|
kjVersion = null,
|
|
76
|
+
root = ".",
|
|
76
77
|
} = {}) {
|
|
77
78
|
const dir = join(projectDir, WORKFLOWS_DIR);
|
|
78
79
|
const pm = detectPackageManager(projectDir);
|
|
79
|
-
|
|
80
|
+
// KJC-BUG-0309: the Quality workflow reads the package manager and the lint
|
|
81
|
+
// script where the language root is, and runs its steps there.
|
|
82
|
+
const appDir = join(projectDir, root);
|
|
83
|
+
const quality = qualityWorkflowFor(language, detectPackageManager(appDir), hasLintScript(appDir), root);
|
|
80
84
|
const extras = extraWorkflowsFor({ profile, publishable: isPublishableNpm(projectDir), pm });
|
|
81
85
|
const mut = mutation ? mutationWorkflowFor(language, pm) : null;
|
|
82
86
|
// PL-C: el tier C solo existe donde el proyecto DECLARA policy. Versión a
|
|
@@ -166,13 +166,21 @@ const QUALITY_BY_LANGUAGE = {
|
|
|
166
166
|
* `lint:false` drops the JS lint step entirely (project has no lint script,
|
|
167
167
|
* KJC-BUG-0137) — mirroring the local hook, which also omits it.
|
|
168
168
|
*/
|
|
169
|
-
export function qualityWorkflowFor(language, pm = "npm", lint = true) {
|
|
169
|
+
export function qualityWorkflowFor(language, pm = "npm", lint = true, root = ".") {
|
|
170
170
|
const c = PM_COMMANDS[pm] || PM_COMMANDS.npm;
|
|
171
171
|
const body =
|
|
172
172
|
language === "javascript" || language === "typescript"
|
|
173
173
|
? header([...nodeSetupSteps(pm), ...(lint ? [` - run: ${c.lint}`] : []), ` - run: ${c.test}`])
|
|
174
174
|
: QUALITY_BY_LANGUAGE[language];
|
|
175
|
-
return body ? { file: "kj-quality.yml", blockId: "wf-quality", body } : null;
|
|
175
|
+
return body ? { file: "kj-quality.yml", blockId: "wf-quality", body: inRoot(body, root) } : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// KJC-BUG-0309 (#2023): a repo whose app lives in a subdirectory runs every step
|
|
179
|
+
// there. The root goes into YAML, so it is a plain relative path or nothing.
|
|
180
|
+
function inRoot(body, root) {
|
|
181
|
+
if (root === "." || root === "") return body;
|
|
182
|
+
if (!/^[\w.-]+(\/[\w.-]+)*$/.test(root) || root.split("/").includes("..")) throw new Error(`kj harden: "${root}" is not a safe directory for the Quality workflow`);
|
|
183
|
+
return body.replace(" runs-on: ubuntu-latest\n", ` runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: ${root}\n`);
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
// Caps the net LOC delta of a PR (strict profile only — it is opinionated).
|
|
@@ -20,6 +20,16 @@ const HARNESS_PREFIX = ".karajan/harness/";
|
|
|
20
20
|
const SAFE_DIR = /^(\$HOME)?(\/[\w.@%+~-]+)+$/;
|
|
21
21
|
|
|
22
22
|
const sha256 = (buf) => createHash("sha256").update(buf).digest("hex");
|
|
23
|
+
// KJC-BUG-0311: the kj judging now, to tell a seal made by another version. Read
|
|
24
|
+
// lazily and never fatal (a bundled binary may not carry package.json): unknown
|
|
25
|
+
// means no seal counts as "older", so the strict re-render rule applies.
|
|
26
|
+
let kjVersionCache;
|
|
27
|
+
const kjVersion = () => {
|
|
28
|
+
if (kjVersionCache === undefined) {
|
|
29
|
+
try { kjVersionCache = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version ?? null; } catch { kjVersionCache = null; }
|
|
30
|
+
}
|
|
31
|
+
return kjVersionCache;
|
|
32
|
+
};
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* @returns {{files: Set<string>, reason?: string}} rutas repo-relativas de
|
|
@@ -41,6 +51,10 @@ export function verifiedSupervisorFiles({ projectDir, trackedOnly = false }) {
|
|
|
41
51
|
if (!signed.ok) return { files: new Set(), reason: signed.reason };
|
|
42
52
|
const ok = new Set();
|
|
43
53
|
const unjudgeable = new Set();
|
|
54
|
+
const current = kjVersion();
|
|
55
|
+
const olderSeal = current && typeof prov?.kj_version === "string" && prov.kj_version !== current ? prov.kj_version : null;
|
|
56
|
+
let vouched = false;
|
|
57
|
+
let stale = false;
|
|
44
58
|
// KJC-BUG-0259 (user's decision, option A): history is judged with the kj of
|
|
45
59
|
// when it was committed. The harness guards are machine-local and never in the
|
|
46
60
|
// repo; the review and each PR's CI verify them when the seal is made. A check
|
|
@@ -87,13 +101,23 @@ export function verifiedSupervisorFiles({ projectDir, trackedOnly = false }) {
|
|
|
87
101
|
continue;
|
|
88
102
|
}
|
|
89
103
|
if (canonical != null && sha256(Buffer.from(canonical, "utf8")) === entry.sha256) ok.add(entry.file);
|
|
104
|
+
// KJC-BUG-0311 (#2052): sealed with another kj whose template this kj cannot
|
|
105
|
+
// re-render. A phone of the roster signed these very bytes: that vouches for
|
|
106
|
+
// them. Without that signature recomputation is the only proof, so it stays
|
|
107
|
+
// denied (an unsigned provenance could claim any version to skip it).
|
|
108
|
+
else if (olderSeal && signed.required) { ok.add(entry.file); vouched = true; }
|
|
109
|
+
else if (olderSeal) stale = true;
|
|
90
110
|
}
|
|
91
111
|
// `complete` = ninguna entrada FALLO y alguna verifico. Exigir que todas
|
|
92
112
|
// verifiquen hacia imposible sellar desde cualquier sitio que no fuera la
|
|
93
113
|
// maquina que sello, porque los guardias no viajan con el repo (0212).
|
|
94
114
|
const judged = (e) => ok.has(e.file) || unjudgeable.has(e.file);
|
|
95
115
|
const complete = ok.size > 0 && entries.every((e) => typeof e?.file === "string" && judged(e));
|
|
96
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
files: ok, complete, unjudgeable, signed: signed.required,
|
|
118
|
+
...(vouched ? { vouchedOlder: olderSeal } : {}),
|
|
119
|
+
...(stale ? { reason: `sellado con kj ${olderSeal} y este kj (${current}) no puede recomputar su plantilla, y sin firma del padrón nada más lo respalda: re-séllalo con kj harden --commit` } : {}),
|
|
120
|
+
};
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
/**
|
|
@@ -117,6 +141,7 @@ export function liftSealedSupervisorViolations({ projectDir, violations, tracked
|
|
|
117
141
|
...(unjudged > 0 ? [`${unjudged} guardia(s) del sello no existen en este checkout y no se han podido comprobar aquí`] : []),
|
|
118
142
|
// KJC-TSK-0964: what the signature did or did not prove is said, never assumed.
|
|
119
143
|
...(sealed.signed === false ? ["proyecto sin padrón de firmantes: el sello no lleva firma verificable y su garantía es solo local"] : []),
|
|
144
|
+
...(sealed.vouchedOlder ? [`sellado con kj ${sealed.vouchedOlder}: este kj no puede recomputar su plantilla y lo respalda la firma del padrón sobre esos bytes`] : []),
|
|
120
145
|
...(kept.length === violations.length && sealed.reason ? [sealed.reason] : []),
|
|
121
146
|
];
|
|
122
147
|
return {
|
package/src/rag/auto-update.js
CHANGED
|
@@ -12,6 +12,8 @@ import { ragExclude } from "./indexable.js";
|
|
|
12
12
|
import { makeGovernedEmbedder } from "./governed-embedder.js";
|
|
13
13
|
import { openProjectStore } from "./project-store.js";
|
|
14
14
|
|
|
15
|
+
// The pre-query refresh reindexes at most this many changed files (config.rag.autoUpdate.maxFiles).
|
|
16
|
+
export const DEFAULT_MAX_DRIFT_FILES = 25;
|
|
15
17
|
const HOOK_SRC = resolve(fileURLToPath(import.meta.url), "../../../scripts/git-hooks/post-merge");
|
|
16
18
|
|
|
17
19
|
export async function maybeAutoUpdate({ projectDir, config, logger = console, flags = {} } = {}) {
|
|
@@ -26,6 +28,16 @@ export async function maybeAutoUpdate({ projectDir, config, logger = console, fl
|
|
|
26
28
|
try {
|
|
27
29
|
const since = getLastIndexedCommit(db, slug);
|
|
28
30
|
if (!since || since === head) return { skipped: true, head };
|
|
31
|
+
// KJC-BUG-0304: a drift of hundreds of files (an index weeks old) reindexed the
|
|
32
|
+
// repo for half an hour before the query answered. Past the bound the query
|
|
33
|
+
// answers from the index as it is, and says how to bring it up to date.
|
|
34
|
+
const maxFiles = config?.rag?.autoUpdate?.maxFiles ?? DEFAULT_MAX_DRIFT_FILES;
|
|
35
|
+
const { stdout } = await execa("git", ["-C", projectDir, "diff", "--name-only", since, "HEAD"]);
|
|
36
|
+
const changed = stdout.split("\n").filter(Boolean).length;
|
|
37
|
+
if (changed > maxFiles) {
|
|
38
|
+
logger.warn?.(`[rag] ${changed} file(s) changed since the last index (more than ${maxFiles}): answering from the index as it is; bring it up to date with kj rag index --since auto`);
|
|
39
|
+
return { skipped: true, reason: "drift-too-large", changed, head };
|
|
40
|
+
}
|
|
29
41
|
logger.info?.(`[rag] drift detected (${since.slice(0, 7)} → ${head.slice(0, 7)}); running delta update`);
|
|
30
42
|
const totals = await indexProjectDelta(projectDir, { db, embedder: makeGovernedEmbedder(config), since, logger, exclude: ragExclude(config) });
|
|
31
43
|
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|