karajan-code 4.34.0 → 4.35.1
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/src/agents/base-agent.js +22 -0
- package/src/agents/kimi-agent.js +12 -3
- package/src/audit/harness-scorecard.js +8 -1
- package/src/audit/harness-section.js +13 -2
- package/src/checks/rag-coverage.js +4 -1
- package/src/checks/runner.js +27 -5
- package/src/checks/skills.js +10 -0
- package/src/checks/types.js +5 -1
- package/src/cli/register-meta.js +11 -1
- package/src/commands/rag.js +21 -0
- package/src/commands/review-gate.js +2 -2
- package/src/config/defaults.js +13 -1
- package/src/guards/output-guard.js +56 -4
- package/src/harden/config-templates.js +11 -0
- package/src/harden/harness-hooks.js +92 -3
- package/src/harden/sentinel-hooks.js +28 -3
- package/src/orchestrator/config-init.js +16 -102
- package/src/orchestrator/drivers/iteration-phases/guards.js +8 -1
- package/src/orchestrator/drivers/pre-loop.js +2 -3
- package/src/rag/coverage.js +56 -3
- package/src/review/card-first.js +12 -0
- package/src/review/gate-gitignore.js +55 -0
- package/src/review/kj-provenance.js +42 -0
- package/src/review/rag-ledger.js +12 -4
- package/src/review/rag-requirement.js +17 -3
- package/src/sonar/api.js +11 -2
- package/src/sonar/project-key.js +33 -0
- package/src/sonar/scanner.js +37 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { computeBaseRef, setSnapshot } from "../review/diff-generator.js";
|
|
8
|
-
import { ensureContractBlockPresent } from "../review/gate-gitignore.js";
|
|
8
|
+
import { ensureContractBlockPresent, excludeLocalArtifacts } from "../review/gate-gitignore.js";
|
|
9
9
|
import { revParse } from "../utils/git.js";
|
|
10
10
|
import { buildCoderPrompt } from "../prompts/coder.js";
|
|
11
11
|
import { buildReviewerPrompt } from "../prompts/reviewer.js";
|
|
@@ -55,26 +55,18 @@ export async function autoInit(projectDir, logger) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const universalIgnores = [".env", "*.log", ".DS_Store", ".reviews/"];
|
|
58
|
+
// KJC-BUG-0213 (issue #1734): el .gitignore es del equipo y viaja con el repo.
|
|
59
|
+
// Ahi solo entra el contrato v4, que el equipo entero hereda; los artefactos
|
|
60
|
+
// de una ejecucion son nuestros y locales, asi que van a .git/info/exclude.
|
|
61
|
+
// Lo que kj metia antes (.env, *.log, .DS_Store) era opinion sobre un
|
|
62
|
+
// proyecto ajeno, y aparecia en el diff de otra persona sin haberlo pedido.
|
|
64
63
|
try {
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
const missing = universalIgnores.filter(entry => !content.includes(entry));
|
|
70
|
-
if (missing.length > 0) {
|
|
71
|
-
const append = (content && !content.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n";
|
|
72
|
-
await fs.appendFile(gitignorePath, append, "utf8");
|
|
73
|
-
logger.info(`Created .gitignore with universal entries`);
|
|
74
|
-
}
|
|
75
|
-
await ensureContractBlockPresent(projectDir);
|
|
64
|
+
const contract = await ensureContractBlockPresent(projectDir);
|
|
65
|
+
if (contract.changed) logger.info("El contrato v4 se ha anadido a .gitignore (lo hereda quien clone)");
|
|
66
|
+
const local = await excludeLocalArtifacts(projectDir);
|
|
67
|
+
if (local.changed) logger.info(`Artefactos de kj excluidos en .git/info/exclude: ${local.added.join(", ")}`);
|
|
76
68
|
} catch (err) {
|
|
77
|
-
logger.warn(`Failed to
|
|
69
|
+
logger.warn(`Failed to prepare .gitignore: ${err.message}`);
|
|
78
70
|
}
|
|
79
71
|
|
|
80
72
|
const karajanDir = path.join(projectDir, ".karajan");
|
|
@@ -125,89 +117,11 @@ export async function autoInit(projectDir, logger) {
|
|
|
125
117
|
}
|
|
126
118
|
}
|
|
127
119
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
java: ["target/", "*.class", "*.jar", "*.war", ".gradle/", "build/", ".settings/", ".classpath", ".project"],
|
|
134
|
-
kotlin: ["target/", "*.class", "build/", ".gradle/", ".kotlin/"],
|
|
135
|
-
go: ["bin/", "*.exe", "vendor/"],
|
|
136
|
-
rust: ["target/", "Cargo.lock"],
|
|
137
|
-
ruby: ["vendor/bundle/", ".bundle/", "coverage/", "tmp/"],
|
|
138
|
-
php: ["vendor/", ".phpunit.result.cache", "storage/logs/"],
|
|
139
|
-
csharp: ["bin/", "obj/", "*.suo", "*.user", "packages/", ".vs/"],
|
|
140
|
-
swift: [".build/", "Packages/", "*.xcodeproj/", "DerivedData/"],
|
|
141
|
-
dart: [".dart_tool/", "build/", ".packages"],
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Update .gitignore with stack-specific entries after planner/architect decides the stack.
|
|
146
|
-
* Detects stack from: triage taskType, architect output, planner output, or task keywords.
|
|
147
|
-
*/
|
|
148
|
-
export async function updateGitignoreForStack(projectDir, { stageResults, task, logger }) {
|
|
149
|
-
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
150
|
-
const detected = new Set();
|
|
151
|
-
|
|
152
|
-
// From architect — if it chose layers/patterns, it may hint at the language
|
|
153
|
-
const arch = stageResults?.architect?.architecture;
|
|
154
|
-
if (arch) {
|
|
155
|
-
const archText = JSON.stringify(arch).toLowerCase();
|
|
156
|
-
for (const lang of Object.keys(STACK_GITIGNORE)) {
|
|
157
|
-
if (archText.includes(lang)) detected.add(lang);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// From planner — scan plan text for language keywords
|
|
162
|
-
const planText = (stageResults?.planner?.plan || "").toLowerCase();
|
|
163
|
-
// From task description
|
|
164
|
-
const taskText = (task || "").toLowerCase();
|
|
165
|
-
const combined = `${planText} ${taskText}`;
|
|
166
|
-
|
|
167
|
-
const langKeywords = {
|
|
168
|
-
javascript: ["node", "npm", "express", "react", "vue", "next", "vite", "vitest", "jest", "pnpm", "yarn", "javascript", "js"],
|
|
169
|
-
typescript: ["typescript", "tsx", "tsc"],
|
|
170
|
-
python: ["python", "django", "flask", "fastapi", "pip", "pytest", "poetry"],
|
|
171
|
-
java: ["java", "spring", "maven", "gradle", "junit"],
|
|
172
|
-
kotlin: ["kotlin", "ktor"],
|
|
173
|
-
go: ["golang", "go mod", "gin", "fiber"],
|
|
174
|
-
rust: ["rust", "cargo", "tokio"],
|
|
175
|
-
ruby: ["ruby", "rails", "gem", "bundler", "rspec"],
|
|
176
|
-
php: ["php", "laravel", "composer", "symfony"],
|
|
177
|
-
csharp: ["c#", "csharp", "dotnet", ".net", "aspnet"],
|
|
178
|
-
swift: ["swift", "swiftui", "vapor"],
|
|
179
|
-
dart: ["dart", "flutter"],
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
for (const [lang, keywords] of Object.entries(langKeywords)) {
|
|
183
|
-
if (keywords.some(kw => combined.includes(kw))) detected.add(lang);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (detected.size === 0) return;
|
|
187
|
-
|
|
188
|
-
// Collect all entries for detected stacks
|
|
189
|
-
const entries = [];
|
|
190
|
-
for (const lang of detected) {
|
|
191
|
-
entries.push(...(STACK_GITIGNORE[lang] || []));
|
|
192
|
-
}
|
|
193
|
-
const unique = [...new Set(entries)];
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
let content = "";
|
|
197
|
-
if (await exists(gitignorePath)) {
|
|
198
|
-
content = await fs.readFile(gitignorePath, "utf8");
|
|
199
|
-
}
|
|
200
|
-
const missing = unique.filter(entry => !content.includes(entry));
|
|
201
|
-
if (missing.length > 0) {
|
|
202
|
-
const header = `\n# ${[...detected].join(" + ")} project\n`;
|
|
203
|
-
const append = header + missing.join("\n") + "\n";
|
|
204
|
-
await fs.appendFile(gitignorePath, append, "utf8");
|
|
205
|
-
logger.info(`Updated .gitignore for ${[...detected].join(" + ")}: ${missing.join(", ")}`);
|
|
206
|
-
}
|
|
207
|
-
} catch (err) {
|
|
208
|
-
logger.warn(`Failed to update .gitignore for stack: ${err.message}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
120
|
+
// KJC-BUG-0213 (issue #1734): aqui vivia updateGitignoreForStack, que deducia
|
|
121
|
+
// el stack buscando palabras como "ts" o "js" en el texto del plan y de la
|
|
122
|
+
// tarea. Cualquier frase las contiene, asi que un proyecto Laravel acabo con
|
|
123
|
+
// reglas de TypeScript en SU .gitignore. Un acierto por casualidad no vale para
|
|
124
|
+
// escribir en un fichero que no es nuestro: la funcion se retira entera.
|
|
211
125
|
|
|
212
126
|
/**
|
|
213
127
|
* Load product context from well-known file locations.
|
|
@@ -24,10 +24,17 @@ export async function runGuardStages({ config, logger, emitter, eventBase, sessi
|
|
|
24
24
|
|
|
25
25
|
if (!outputEnabled && !perfEnabled) return { action: "ok" };
|
|
26
26
|
|
|
27
|
+
// KJC-BUG-0214 (issue #1733): los guards juzgan LO QUE HA HECHO ESTE RUN, no
|
|
28
|
+
// la rama entera. Con el merge-base contra la base, cualquier linea anadida
|
|
29
|
+
// en cualquier momento de la rama contaba como recien escrita, y una
|
|
30
|
+
// ejecucion se bloqueo por codigo preexistente que el coder no toco. La
|
|
31
|
+
// sesion ya guarda HEAD al arrancar precisamente para esto.
|
|
27
32
|
const baseBranch = config.base_branch || "main";
|
|
33
|
+
const runStart = session?.head_at_start || session?.session_start_sha || null;
|
|
28
34
|
let diff;
|
|
29
35
|
try {
|
|
30
|
-
const baseRef = await computeBaseRef({ baseBranch });
|
|
36
|
+
const baseRef = runStart || await computeBaseRef({ baseBranch });
|
|
37
|
+
if (!runStart) logger.warn("Guards: sin punto de arranque del run, se juzga la rama entera contra la base");
|
|
31
38
|
diff = await generateDiff({ baseRef, projectDir: config?.projectDir || null });
|
|
32
39
|
} catch {
|
|
33
40
|
logger.warn("Guards: could not generate diff, skipping");
|
|
@@ -57,7 +57,7 @@ import { maybeGenerateAutoHuBatch } from "./pre-loop-phases/auto-hu-batch.js";
|
|
|
57
57
|
|
|
58
58
|
import {
|
|
59
59
|
applyTriageOverrides, applyAutoSimplify, applyFlagOverrides,
|
|
60
|
-
resolvePipelinePolicies,
|
|
60
|
+
resolvePipelinePolicies,
|
|
61
61
|
} from "../config-init.js";
|
|
62
62
|
import { tryCiComment } from "../ci-integration.js";
|
|
63
63
|
import { getIntegration } from "../integrations.js";
|
|
@@ -322,7 +322,7 @@ export async function runPreLoopStages({ config, logger, emitter, eventBase, ses
|
|
|
322
322
|
// Hard fail if blocking checks failed (SonarQube enabled but not available)
|
|
323
323
|
if (!preflightResult.ok) {
|
|
324
324
|
const errorLines = (preflightResult.errors || [])
|
|
325
|
-
.map(e => ` - ${e.message}
|
|
325
|
+
.map(e => ` - ${e.message}${e.fix ? `\n Fix: ${e.fix}` : ""}`)
|
|
326
326
|
.join("\n");
|
|
327
327
|
throw new Error(
|
|
328
328
|
`Preflight FAILED — environment changed during session. Fix the issues and retry:\n${errorLines}`
|
|
@@ -350,7 +350,6 @@ export async function runPreLoopStages({ config, logger, emitter, eventBase, ses
|
|
|
350
350
|
|
|
351
351
|
// --- Update .gitignore with stack-specific entries based on planner/architect output ---
|
|
352
352
|
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
353
|
-
await updateGitignoreForStack(projectDir, { stageResults, task, logger });
|
|
354
353
|
|
|
355
354
|
// --- Auto-HU: when triage recommended decomposition and no manual huFile, generate HU batch ---
|
|
356
355
|
await maybeGenerateAutoHuBatch({
|
package/src/rag/coverage.js
CHANGED
|
@@ -8,10 +8,29 @@
|
|
|
8
8
|
* matchers are the single truth, here and in the indexer.
|
|
9
9
|
*/
|
|
10
10
|
import { execa } from "execa";
|
|
11
|
-
import { isAbsolute, relative } from "node:path";
|
|
11
|
+
import { extname, isAbsolute, normalize, relative } from "node:path";
|
|
12
12
|
import { detectAdaptersForProject, buildMatchers } from "../lang/registry.js";
|
|
13
|
+
import { GENERATED_CONFIG_FILES } from "../harden/config-templates.js";
|
|
13
14
|
import { getLastIndexedCommit, projectSlug } from "./vec-store.js";
|
|
14
15
|
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* KJC-BUG-0221 — `kj harden` instala commitlint.config.js y eslint.config.js, y
|
|
19
|
+
* la cobertura los exigia indexados, asi que `kj check` denunciaba como drift
|
|
20
|
+
* dos ficheros que kj mismo acababa de escribir: la herramienta creando el
|
|
21
|
+
* fichero y quejandose de el. El criterio es el que ya usa el presupuesto de
|
|
22
|
+
* LOC (src/review/loc-budget.js): lo generado no cuenta, porque nadie lo
|
|
23
|
+
* escribio, y el RAG existe para responder sobre el codigo del equipo.
|
|
24
|
+
*
|
|
25
|
+
* La lista sale de la propia tabla de plantillas de harden, para que anadir un
|
|
26
|
+
* config no obligue a acordarse de esta otra lista.
|
|
27
|
+
*/
|
|
28
|
+
export function isGeneratedByHarden(rel) {
|
|
29
|
+
if (!rel) return false;
|
|
30
|
+
const base = rel.split("/").pop();
|
|
31
|
+
return GENERATED_CONFIG_FILES.has(base);
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
35
|
* @param {string} projectDir
|
|
17
36
|
* @param {{db: object}} deps - an open vec store
|
|
@@ -21,7 +40,10 @@ export async function ragIndexCoverage(projectDir, { db }) {
|
|
|
21
40
|
const project = projectSlug(projectDir);
|
|
22
41
|
const matchers = buildMatchers(detectAdaptersForProject(projectDir));
|
|
23
42
|
const { stdout } = await execa("git", ["-C", projectDir, "ls-files", "-z"]);
|
|
24
|
-
const
|
|
43
|
+
const all = stdout.split("\0").filter((p) => p && matchers.isCodeFile(p) && !matchers.shouldSkip(p));
|
|
44
|
+
// Lo que kj genera se informa aparte: ni se exige indexado ni se esconde.
|
|
45
|
+
const generated = all.filter(isGeneratedByHarden);
|
|
46
|
+
const sources = all.filter((p) => !isGeneratedByHarden(p));
|
|
25
47
|
|
|
26
48
|
const rows = db.prepare("SELECT DISTINCT source FROM chunks WHERE kind = 'code' AND project_slug = ?").all(project);
|
|
27
49
|
const indexed = new Set(rows.map((r) => (isAbsolute(r.source) ? relative(projectDir, r.source) : r.source)));
|
|
@@ -44,5 +66,36 @@ export async function ragIndexCoverage(projectDir, { db }) {
|
|
|
44
66
|
stale = sources.filter((p) => indexed.has(p) && changed.has(p));
|
|
45
67
|
}
|
|
46
68
|
|
|
47
|
-
return { project, total: sources.length, indexed: sources.length - missing.length, missing, stale, lastIndexedCommit, absent: indexed.size === 0 };
|
|
69
|
+
return { project, total: sources.length, indexed: sources.length - missing.length, missing, stale, generated, lastIndexedCommit, absent: indexed.size === 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* KJC-BUG-0216 (issue #1807) — POR QUE el RAG no puede responder sobre un
|
|
74
|
+
* fichero. El gate rag-first exigia una respuesta sobre cualquier fichero
|
|
75
|
+
* tocado, y para un .astro (o .php, o cualquier lenguaje sin adapter) eso es
|
|
76
|
+
* imposible: el indexador nunca lo toma. La unica salida era KJ_ALLOW_NO_RAG
|
|
77
|
+
* puesta por costumbre en todos los comandos, y con ella puesta el gate deja de
|
|
78
|
+
* informar y borra la diferencia entre "no pude consultar" y "no quise".
|
|
79
|
+
*
|
|
80
|
+
* `canAnswer` es la respuesta que el gate necesita: solo cuando es true tiene
|
|
81
|
+
* sentido exigir que el RAG haya hablado de ese fichero.
|
|
82
|
+
*
|
|
83
|
+
* @returns {{state: "indexed"|"stale"|"not-indexable"|"index-empty", rel: string, canAnswer: boolean, reason: string}}
|
|
84
|
+
*/
|
|
85
|
+
export function fileIndexState(projectDir, file, { db }) {
|
|
86
|
+
const project = projectSlug(projectDir);
|
|
87
|
+
// `./src/a.js` y `src/a.js` son el mismo fichero: sin normalizar, el segundo
|
|
88
|
+
// salia "stale" estando indexado (catch de la review).
|
|
89
|
+
const rel = normalize(isAbsolute(file) ? relative(projectDir, file) : file);
|
|
90
|
+
const matchers = buildMatchers(detectAdaptersForProject(projectDir));
|
|
91
|
+
const rows = db.prepare("SELECT DISTINCT source FROM chunks WHERE project_slug = ?").all(project);
|
|
92
|
+
const indexed = new Set(rows.map((r) => normalize(isAbsolute(r.source) ? relative(projectDir, r.source) : r.source)));
|
|
93
|
+
|
|
94
|
+
if (indexed.has(rel)) return { state: "indexed", rel, canAnswer: true, reason: `the index holds chunks for ${rel}` };
|
|
95
|
+
if (indexed.size === 0) return { state: "index-empty", rel, canAnswer: false, reason: `nothing of this project is indexed yet: kj rag index` };
|
|
96
|
+
if (matchers.shouldSkip(rel)) return { state: "not-indexable", rel, canAnswer: false, reason: `${rel} lives under a path the indexer always skips` };
|
|
97
|
+
if (!matchers.isCodeFile(rel) && extname(rel).toLowerCase() !== ".md") {
|
|
98
|
+
return { state: "not-indexable", rel, canAnswer: false, reason: `no language adapter covers ${extname(rel) || "a file with no extension"}, so the indexer never takes ${rel}` };
|
|
99
|
+
}
|
|
100
|
+
return { state: "stale", rel, canAnswer: false, reason: `${rel} is indexable but absent from the index: kj rag index --since auto` };
|
|
48
101
|
}
|
package/src/review/card-first.js
CHANGED
|
@@ -93,6 +93,18 @@ export async function checkCardFirst({ config = {}, projectDir = process.cwd(),
|
|
|
93
93
|
? { ok: false, mode: "block", reason, level: v.level }
|
|
94
94
|
: { ok: true, mode: "warn", reason, level: v.level };
|
|
95
95
|
}
|
|
96
|
+
// KJC-TSK-0878 (issue #1371): "no he podido verificar" y "he verificado y
|
|
97
|
+
// esta mal" no son lo mismo, y ambas pueden merecer una parada. El proyecto
|
|
98
|
+
// decide si la primera para, y el mensaje dice cual de las dos es: un gate
|
|
99
|
+
// que aprueba lo que no ha comprobado da una confianza que no ha ganado.
|
|
100
|
+
// Sin adaptador declarado no hay nada que bloquear: la opcion no castiga a
|
|
101
|
+
// quien aun no tiene verify_cmd.
|
|
102
|
+
if (v.verified === null && config.board?.unverifiable === "block" && config.board?.verify_cmd) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false, mode: "block", level: v.level,
|
|
105
|
+
reason: `branch references ${ref} but the card could not be verified against ${boardName} — ${v.note} (board.unverifiable: block refuses to approve a check kj did not make)`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
96
108
|
return { ok: true, mode: "pass", ref, level: v.level, ...(v.note ? { note: v.note } : {}) };
|
|
97
109
|
}
|
|
98
110
|
const reason = `no card reference in branch "${branch}" — card-first on ${boardName}: create the card there and name the branch after it (e.g. feat/ABC-123-summary)`;
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
|
|
15
|
+
import { runCommand } from "../utils/process.js";
|
|
16
|
+
|
|
15
17
|
// KJC-BUG-0123 (issue #1268): exported as the SINGLE source of truth — kj
|
|
16
18
|
// init and the orchestrator's autoInit write this same block instead of a
|
|
17
19
|
// bare `.karajan/` exclude (which git cannot re-include children of).
|
|
@@ -48,6 +50,59 @@ export async function ensureContractBlockPresent(projectDir) {
|
|
|
48
50
|
return { changed: true };
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
/**
|
|
54
|
+
* KJC-BUG-0213 (issue #1734): lo que kj deja en el repo durante una ejecucion
|
|
55
|
+
* es suyo y es local, asi que se excluye donde no molesta a nadie:
|
|
56
|
+
* `.git/info/exclude` no viaja con el repo ni aparece en el diff de otro.
|
|
57
|
+
* La ruta se pregunta a git para que funcione tambien en un worktree, donde
|
|
58
|
+
* `.git` es un fichero y no un directorio.
|
|
59
|
+
*/
|
|
60
|
+
const LOCAL_ARTIFACTS = [
|
|
61
|
+
".reviews/", ".kj/", ".kj-ready.json",
|
|
62
|
+
// KJC-BUG-0220: lo que kj escribe DENTRO de .karajan/ y NO viaja. Sin esto
|
|
63
|
+
// se quedaba en tierra de nadie (ni versionado ni ignorado) y salia como `??`
|
|
64
|
+
// en cada git status del equipo. Se enumera en vez de ignorar .karajan/
|
|
65
|
+
// entero a proposito: kj.config.yml y las reglas del agente son del equipo, y
|
|
66
|
+
// esconderselas seria decidir por ellos. Los ficheros del supervisor viajan
|
|
67
|
+
// por su cauce sellado (ADR 0009) y tampoco entran aqui.
|
|
68
|
+
".karajan/harness/", // el arnes del anfitrion: cada maquina lo regenera
|
|
69
|
+
".karajan/policy-decisions.jsonl",
|
|
70
|
+
".karajan/policy-exceptions.jsonl",
|
|
71
|
+
".karajan/identity.local.yml", // identidad por clon (ADR 0005)
|
|
72
|
+
".karajan/sentinel-state.json",
|
|
73
|
+
".karajan/rag.db",
|
|
74
|
+
".karajan/audit-history.db",
|
|
75
|
+
".karajan/update-check.json",
|
|
76
|
+
".karajan/karajan.env",
|
|
77
|
+
".karajan/agent-skills/",
|
|
78
|
+
".karajan/audits/",
|
|
79
|
+
".karajan/reviews/",
|
|
80
|
+
".karajan/steward/",
|
|
81
|
+
".karajan/plans/",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
export async function excludeLocalArtifacts(projectDir, run = runCommand) {
|
|
85
|
+
let file = path.join(projectDir, ".git", "info", "exclude");
|
|
86
|
+
try {
|
|
87
|
+
const res = await run("git", ["-C", projectDir, "rev-parse", "--git-path", "info/exclude"]);
|
|
88
|
+
const out = res?.stdout?.trim();
|
|
89
|
+
if (res?.exitCode === 0 && out) file = path.isAbsolute(out) ? out : path.join(projectDir, out);
|
|
90
|
+
} catch { /* sin git utilizable: se intenta la ruta por defecto */ }
|
|
91
|
+
let text = "";
|
|
92
|
+
try { text = (await fs.readFile(file, "utf8")) || ""; } catch { /* aun no existe */ }
|
|
93
|
+
const lines = new Set(text.split("\n").map((l) => l.trim()));
|
|
94
|
+
const missing = LOCAL_ARTIFACTS.filter((a) => !lines.has(a));
|
|
95
|
+
if (missing.length === 0) return { changed: false, added: [] };
|
|
96
|
+
try {
|
|
97
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
98
|
+
const sep = text && !text.endsWith("\n") ? "\n" : "";
|
|
99
|
+
await fs.appendFile(file, `${sep}# Karajan run artifacts (local only, never shared)\n${missing.join("\n")}\n`, "utf8");
|
|
100
|
+
return { changed: true, added: missing };
|
|
101
|
+
} catch {
|
|
102
|
+
return { changed: false, added: [] };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
51
106
|
export async function ensureGateTrackable(projectDir) {
|
|
52
107
|
const file = path.join(projectDir, ".gitignore");
|
|
53
108
|
let text;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj-provenance — KJC-BUG-0222. Un gate que dice QUE falla sin decir CONTRA QUE
|
|
3
|
+
* compara ensena a saltarselo. El aviso del harness decia "does not match the
|
|
4
|
+
* installed kj" y ahi acababa, asi que en campo se leyo como "kj se
|
|
5
|
+
* desincroniza solo": una sesion se monto el ritual de ejecutar `kj harden`
|
|
6
|
+
* antes de cada review y de cada commit, veinte veces en un dia, sin
|
|
7
|
+
* preguntarse por que. La causa estaba entera en el dato que faltaba: ese kj
|
|
8
|
+
* estaba npm-linkado a un arbol de desarrollo cuyas plantillas cambiaron seis
|
|
9
|
+
* veces en dos dias.
|
|
10
|
+
*
|
|
11
|
+
* Saltarse un gate ritualmente es peor que no tenerlo, porque parece que se
|
|
12
|
+
* cumple.
|
|
13
|
+
*/
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { dirname, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Que kj esta juzgando: version, de donde sale y si es un paquete instalado o
|
|
22
|
+
* un arbol de desarrollo enlazado. Nunca lanza: esto SOLO adorna un mensaje, y
|
|
23
|
+
* un fallo aqui jamas puede cambiar el veredicto de un gate.
|
|
24
|
+
*
|
|
25
|
+
* @returns {{version: string|null, root: string, linked: boolean, label: string}}
|
|
26
|
+
*/
|
|
27
|
+
export function kjProvenance(moduleDir = here) {
|
|
28
|
+
const root = resolve(moduleDir, "..", "..");
|
|
29
|
+
let version = null;
|
|
30
|
+
try {
|
|
31
|
+
version = createRequire(import.meta.url)(resolve(root, "package.json")).version || null;
|
|
32
|
+
} catch { /* sin manifest legible: se dice lo que se sabe */ }
|
|
33
|
+
// Un kj instalado vive bajo node_modules. Cualquier otra ruta es un arbol de
|
|
34
|
+
// trabajo enlazado (npm link), y entonces sus plantillas se mueven con cada
|
|
35
|
+
// edicion de quien lo desarrolla, en TODOS los proyectos de la maquina.
|
|
36
|
+
const linked = !root.includes(`${"node_modules"}/karajan-code`) && !root.endsWith("node_modules");
|
|
37
|
+
const v = version ? `kj ${version}` : "an unknown kj";
|
|
38
|
+
const label = linked
|
|
39
|
+
? `${v} LINKED from ${root} (a development tree: its templates move with every edit there, in every project on this machine)`
|
|
40
|
+
: `${v} (${root})`;
|
|
41
|
+
return { version, root, linked, label };
|
|
42
|
+
}
|
package/src/review/rag-ledger.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import { verifySentinelScripts } from "../harden/sentinel-hooks.js";
|
|
12
|
+
import { resolveSentinelRoot, verifySentinelScripts } from "../harden/sentinel-hooks.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {string} projectDir
|
|
@@ -22,10 +22,18 @@ import { verifySentinelScripts } from "../harden/sentinel-hooks.js";
|
|
|
22
22
|
* the requirement fails closed until the human re-runs `kj harden`.
|
|
23
23
|
*/
|
|
24
24
|
export function readRagLedger(projectDir) {
|
|
25
|
-
|
|
25
|
+
// KJC-BUG-0223: el harness vive en la RAIZ del proyecto. Este modulo miraba
|
|
26
|
+
// el directorio recibido (en la practica, process.cwd()) mientras el
|
|
27
|
+
// verificador resolvia el toplevel de git, asi que desde un subdirectorio los
|
|
28
|
+
// dos juzgaban sobre arboles distintos y el gate bloqueaba siempre.
|
|
29
|
+
const root = resolveSentinelRoot(projectDir);
|
|
30
|
+
const harnessDir = path.join(root, ".karajan", "harness");
|
|
26
31
|
const harness = existsSync(harnessDir);
|
|
27
|
-
const check = harness ? verifySentinelScripts({ projectDir }) : { ok: false, mismatched: [] };
|
|
28
|
-
|
|
32
|
+
const check = harness ? verifySentinelScripts({ projectDir: root }) : { ok: false, mismatched: [] };
|
|
33
|
+
// KJC-BUG-0222: `drift` viaja hasta el mensaje. Un guard sellado que se ha
|
|
34
|
+
// quedado atras NO lo arregla `kj harden` (lo deja en paz a proposito), asi
|
|
35
|
+
// que mandar ese comando para ese fichero es un bucle.
|
|
36
|
+
const base = { root, harness, verified: harness && check.ok === true, mismatched: check.mismatched || [], drift: check.drift || [], queries: [], hits: [] };
|
|
29
37
|
let state;
|
|
30
38
|
try {
|
|
31
39
|
state = JSON.parse(readFileSync(path.join(harnessDir, "sentinel-state.json"), "utf8"));
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* concepts that are not in the diff. That list travels to the reviewer,
|
|
13
13
|
* because the bugs of 2026-09-16 were exactly the twin nobody touched.
|
|
14
14
|
*/
|
|
15
|
+
import { kjProvenance } from "./kj-provenance.js";
|
|
15
16
|
import { sourceFilesOf } from "./tests-with-code.js";
|
|
16
17
|
|
|
17
18
|
export const RAG_RULE_ID = "method.rag.code";
|
|
@@ -30,6 +31,17 @@ function liveGrant(standingExceptions, now) {
|
|
|
30
31
|
* @param {{available: boolean, reason?: string, queries: object[], hits: string[]}} [args.ledger]
|
|
31
32
|
* @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
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* KJC-BUG-0222 — el comando que resuelve ESTE estado. `kj harden` repone lo que
|
|
36
|
+
* nadie sello; un guard sellado que se ha quedado atras lo deja en paz a
|
|
37
|
+
* proposito (KJC-BUG-0193), y avanzarlo es acto humano.
|
|
38
|
+
*/
|
|
39
|
+
function harnessRemedy(drift = []) {
|
|
40
|
+
return drift.length > 0
|
|
41
|
+
? `the human runs \`kj harden\` to restore them, and \`kj harden --commit\` for the sealed ones (${drift.join(", ")}), which \`kj harden\` leaves untouched on purpose`
|
|
42
|
+
: "the human runs `kj harden` to regenerate it";
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles = [], ledger, standingExceptions = [], now = new Date(), env = {} }) {
|
|
34
46
|
const { sources } = sourceFilesOf(config, stagedFiles);
|
|
35
47
|
if (sources.length === 0) return { ok: true, mode: "docs-only" };
|
|
@@ -55,7 +67,9 @@ export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles =
|
|
|
55
67
|
// than the ledger) cannot vouch for anything: fail closed, like the tamper
|
|
56
68
|
// check does, until the human regenerates it.
|
|
57
69
|
if (ledger?.harness === true && ledger?.verified === false) {
|
|
58
|
-
|
|
70
|
+
// KJC-BUG-0222: decir CONTRA QUE no coincide. Sin ese dato el aviso se lee
|
|
71
|
+
// como "kj se desincroniza solo" y se automatiza el sintoma.
|
|
72
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the installed Sentinel harness does not match the templates of ${kjProvenance().label} (${(ledger.mismatched || []).join(", ") || "scripts missing"}): ${harnessRemedy(ledger.drift)} before code is reviewed. ${GRANT_HINT}` };
|
|
59
73
|
}
|
|
60
74
|
if (!ledger?.available) {
|
|
61
75
|
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}` };
|
|
@@ -82,7 +96,7 @@ export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles =
|
|
|
82
96
|
* block — the diff hash pins the file set, so a source missing from
|
|
83
97
|
* `covered` was never answered about, whatever the block claims.
|
|
84
98
|
*/
|
|
85
|
-
export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness = true, verified = true, mismatched = [], standingExceptions = [], now = new Date() }) {
|
|
99
|
+
export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness = true, verified = true, mismatched = [], drift = [], standingExceptions = [], now = new Date() }) {
|
|
86
100
|
const { sources } = sourceFilesOf(config, stagedFiles);
|
|
87
101
|
if (sources.length === 0) return { ok: true, mode: "docs-only" };
|
|
88
102
|
const grant = liveGrant(standingExceptions, now);
|
|
@@ -94,7 +108,7 @@ export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness =
|
|
|
94
108
|
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
109
|
}
|
|
96
110
|
if (!verified) {
|
|
97
|
-
return { ok: false, mode: "block", sources, reason: `${head} — the Sentinel harness does not match the
|
|
111
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the installed Sentinel harness does not match the templates of ${kjProvenance().label} (${mismatched.join(", ") || "scripts missing"}): ${harnessRemedy(drift)}. ${GRANT_HINT}` };
|
|
98
112
|
}
|
|
99
113
|
if (!rag) {
|
|
100
114
|
return { ok: false, mode: "block", sources, reason: `${head} — the verdict carries no rag block; run \`kj review --staged\` again. ${GRANT_HINT}` };
|
package/src/sonar/api.js
CHANGED
|
@@ -82,8 +82,17 @@ async function sonarFetch(config, urlPath) {
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* KJC-BUG-0226 (issue #1838) — de donde sale la clave que se CONSULTA. El
|
|
87
|
+
* scanner sube el analisis con la que el repo declara en su
|
|
88
|
+
* sonar-project.properties; preguntando por otra (la derivada del remote), el
|
|
89
|
+
* audit acababa listando issues de repos ajenos del mismo SonarQube. Pasando
|
|
90
|
+
* el directorio, la resolucion lee esa misma clave declarada.
|
|
91
|
+
*/
|
|
92
|
+
const queryCwd = (config) => config?.projectDir || process.cwd();
|
|
93
|
+
|
|
85
94
|
export async function getQualityGateStatus(config, projectKey = null) {
|
|
86
|
-
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey });
|
|
95
|
+
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey, cwd: queryCwd(config) });
|
|
87
96
|
const body = await sonarFetch(config, `/api/qualitygates/project_status?projectKey=${effectiveProjectKey}`);
|
|
88
97
|
|
|
89
98
|
try {
|
|
@@ -95,7 +104,7 @@ export async function getQualityGateStatus(config, projectKey = null) {
|
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
export async function getOpenIssues(config, projectKey = null) {
|
|
98
|
-
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey });
|
|
107
|
+
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey, cwd: queryCwd(config) });
|
|
99
108
|
const body = await sonarFetch(config, `/api/issues/search?projectKeys=${effectiveProjectKey}&statuses=OPEN`);
|
|
100
109
|
|
|
101
110
|
let parsedIssues = [];
|
package/src/sonar/project-key.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
2
5
|
import { runCommand } from "../utils/process.js";
|
|
3
6
|
|
|
4
7
|
function slug(value) {
|
|
@@ -55,6 +58,27 @@ function canonicalRepoId(remoteUrl) {
|
|
|
55
58
|
return `${host}/${segments.join("/")}`;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* KJC-BUG-0226 (issue #1838) — la clave que el REPO declara en su
|
|
63
|
+
* sonar-project.properties es la que el servidor conoce, porque es con la que
|
|
64
|
+
* el scanner sube el analisis. Sin leerla aqui, kj escaneaba un proyecto y
|
|
65
|
+
* preguntaba por otro (una clave derivada del remote), y el audit acababa
|
|
66
|
+
* listando issues de repos ajenos del mismo SonarQube.
|
|
67
|
+
*
|
|
68
|
+
* Sincrono y tolerante: sin fichero, ilegible o sin la clave, se sigue por
|
|
69
|
+
* donde se iba.
|
|
70
|
+
*/
|
|
71
|
+
function declaredProjectKey(cwd) {
|
|
72
|
+
try {
|
|
73
|
+
const raw = fs.readFileSync(path.join(cwd, "sonar-project.properties"), "utf8");
|
|
74
|
+
for (const line of raw.split("\n")) {
|
|
75
|
+
const m = /^\s*sonar\.projectKey\s*=\s*(.+?)\s*$/.exec(line);
|
|
76
|
+
if (m) return m[1];
|
|
77
|
+
}
|
|
78
|
+
} catch { /* sin properties legible: se deriva como siempre */ }
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
export async function resolveSonarProjectKey(config, options = {}) {
|
|
59
83
|
const explicit = String(
|
|
60
84
|
options.projectKey || process.env.KJ_SONAR_PROJECT_KEY || config?.sonarqube?.project_key || ""
|
|
@@ -63,6 +87,15 @@ export async function resolveSonarProjectKey(config, options = {}) {
|
|
|
63
87
|
return normalizeProjectKey(explicit);
|
|
64
88
|
}
|
|
65
89
|
|
|
90
|
+
// KJC-BUG-0226: lo que el repo declara manda sobre lo que kj derivaria, que
|
|
91
|
+
// es justo lo que el scanner ya hace al subir el analisis. Solo con un `cwd`
|
|
92
|
+
// explicito: quien no dice que directorio le importa se queda como estaba, y
|
|
93
|
+
// asi esto no cambia el comportamiento de nadie mas que el de la consulta.
|
|
94
|
+
if (options.cwd) {
|
|
95
|
+
const declared = declaredProjectKey(options.cwd);
|
|
96
|
+
if (declared) return normalizeProjectKey(declared);
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
const remote = await runCommand("git", ["config", "--get", "remote.origin.url"]);
|
|
67
100
|
const remoteUrl = String(remote.stdout || "").trim();
|
|
68
101
|
if (remote.exitCode !== 0 || !remoteUrl) {
|
package/src/sonar/scanner.js
CHANGED
|
@@ -50,8 +50,38 @@ export function indexedFilesFrom(scanOutput) {
|
|
|
50
50
|
return files;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* KJC-BUG-0225 — `.git` es un fichero (no un directorio) en un worktree
|
|
55
|
+
* enlazado y en un submodulo: exactamente los dos casos que JGit no sabe abrir.
|
|
56
|
+
* Sin proceso y sin depender de que git responda.
|
|
57
|
+
*/
|
|
58
|
+
export function isLinkedWorktree(dir) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.statSync(path.join(dir, ".git")).isFile();
|
|
61
|
+
} catch {
|
|
62
|
+
return false; // sin .git no hay SCM que desactivar
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* KJC-BUG-0225 (issue #1839) — dentro de un carril de `kj worktree start`, el
|
|
68
|
+
* plugin SCM del scanner (JGit) no sabe abrir un worktree enlazado (`.git` es
|
|
69
|
+
* un fichero que apunta a `.git/worktrees/<carril>`) y revienta el analisis
|
|
70
|
+
* entero. El pre-gate lo reportaba como "UNAVAILABLE" y la ejecucion seguia:
|
|
71
|
+
* todo cambio hecho como el metodo manda, un carril por tarea, se quedaba sin
|
|
72
|
+
* quality gate y sin un rojo que lo dijera.
|
|
73
|
+
*
|
|
74
|
+
* Se desactiva SOLO el SCM, que en un carril no aporta nada (alimenta blame y
|
|
75
|
+
* asignacion de autoria); las reglas, la cobertura y el resto del analisis se
|
|
76
|
+
* quedan exactamente igual.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectKey
|
|
79
|
+
* @param {object} scanner config del scanner (scm_disabled lo fuerza siempre)
|
|
80
|
+
* @param {{linkedWorktree?: boolean}} [where]
|
|
81
|
+
*/
|
|
82
|
+
export function buildScannerOpts(projectKey, scanner = {}, where = {}) {
|
|
54
83
|
const opts = [`-Dsonar.projectKey=${projectKey}`];
|
|
84
|
+
if (scanner.scm_disabled || where.linkedWorktree) opts.push("-Dsonar.scm.disabled=true");
|
|
55
85
|
// KJC-TSK-0838: verbose is what makes the scanner name every indexed file.
|
|
56
86
|
if (scanner.verbose) opts.push("-Dsonar.verbose=true");
|
|
57
87
|
if (scanner.sources) opts.push(`-Dsonar.sources=${scanner.sources}`);
|
|
@@ -317,11 +347,16 @@ export async function runSonarScan(config, projectKey = null, { verbose = false,
|
|
|
317
347
|
: null;
|
|
318
348
|
|
|
319
349
|
const pick = await pickSonarScanner(sonarConfig.scanner);
|
|
350
|
+
// KJC-BUG-0225: en un carril, el plugin SCM del scanner (JGit) no sabe abrir
|
|
351
|
+
// el worktree enlazado y tumba el analisis entero. La senal es la causa
|
|
352
|
+
// literal del error y no necesita proceso: en un worktree enlazado (y en un
|
|
353
|
+
// submodulo, que JGit trata igual) `.git` es un FICHERO, no un directorio.
|
|
354
|
+
const linkedWorktree = isLinkedWorktree(scanCwd);
|
|
320
355
|
const env = {
|
|
321
356
|
...process.env,
|
|
322
357
|
SONAR_HOST_URL: pick.type === "native" ? rawHost : host,
|
|
323
358
|
SONAR_TOKEN: token || "",
|
|
324
|
-
SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig)
|
|
359
|
+
SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig, { linkedWorktree })
|
|
325
360
|
};
|
|
326
361
|
|
|
327
362
|
let cmd, args;
|