karajan-code 4.37.0 → 4.39.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/packages/ai-trash/src/destructive-parser.js +34 -5
- package/packages/ai-trash/src/hook.js +28 -6
- package/scripts/esbuild-sea.config.mjs +9 -0
- package/src/checks/method.js +1 -1
- package/src/checks/release-check.js +3 -1
- package/src/cli/register-meta.js +3 -1
- package/src/cli/register-pipeline.js +2 -1
- package/src/commands/rag.js +21 -1
- package/src/commands/review-gate.js +21 -22
- package/src/harden/harness-hooks.js +7 -85
- package/src/harden/hook-templates.js +6 -5
- package/src/harden/sentinel/sentinel-bash-write.mjs +106 -0
- package/src/harden/sentinel/sentinel-discard.mjs +125 -0
- package/src/harden/sentinel/sentinel-reminders.mjs +57 -0
- package/src/harden/sentinel/sentinel-shell.mjs +110 -0
- package/src/harden/sentinel/sessionstart.mjs +36 -0
- package/src/harden/sentinel-hooks.js +172 -177
- package/src/harden/supervisor-commit.js +10 -1
- package/src/policy/supervisor-verify.js +8 -4
- package/src/rag/migrate.js +20 -3
- package/src/review/card-first.js +3 -5
- package/src/review/policy-gate.js +4 -6
- package/src/review/sonar-pregate.js +3 -3
- package/src/review/tests-with-code.js +3 -6
- package/src/sonar/api.js +21 -0
- package/src/sonar/scanner.js +22 -3
|
@@ -117,6 +117,16 @@ export async function commitSupervisorRegeneration({
|
|
|
117
117
|
`harden --commit es un acto humano y este proceso desciende de un agente (${anc.match}) — ni con pty falso ni con el entorno limpio (ADR 0009)`,
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
|
+
const run = gitFn || ((args) => execFileSync("git", args, { cwd: projectDir, encoding: "utf8" }));
|
|
121
|
+
// KJC-BUG-0244 (grebla #958): el commit sellado solo lleva el supervisor, pero
|
|
122
|
+
// un stage ajeno se quedaba en esta rama y el siguiente commit caía aquí con la
|
|
123
|
+
// card equivocada. Se para ANTES de pedir nada al humano y se nombra.
|
|
124
|
+
const foreign = run(["diff", "--cached", "--name-only"]).split("\n").filter((f) => f && !f.startsWith(HOOKS_PREFIX) && f !== PROVENANCE_FILE);
|
|
125
|
+
if (foreign.length > 0) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`harden --commit: hay cambios en el stage que no son del supervisor (${foreign.slice(0, 5).join(", ")}${foreign.length > 5 ? ", …" : ""}): commitéalos en su rama o sácalos del stage (git restore --staged <ficheros>) antes de sellar`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
120
130
|
// Capa 4 (test adversarial 6-sep: un huérfano a init con pty falso y
|
|
121
131
|
// prompts a ciegas llegó hasta aquí): nonce aleatorio tecleado de vuelta.
|
|
122
132
|
// Un alimentador ciego no conoce el código; automatizar su lectura exige
|
|
@@ -126,7 +136,6 @@ export async function commitSupervisorRegeneration({
|
|
|
126
136
|
if (answer !== nonce) {
|
|
127
137
|
throw new Error(`harden --commit: confirmación humana fallida (esperaba "${nonce}") — ADR 0009`);
|
|
128
138
|
}
|
|
129
|
-
const run = gitFn || ((args) => execFileSync("git", args, { cwd: projectDir, encoding: "utf8" }));
|
|
130
139
|
const drift = supervisorDrift({ projectDir, gitFn: run });
|
|
131
140
|
// La provenance describe SIEMPRE el estado COMPLETO del supervisor (cazado
|
|
132
141
|
// en el primer estreno real: un sello parcial pisaba al anterior y dejaba
|
|
@@ -24,7 +24,7 @@ const sha256 = (buf) => createHash("sha256").update(buf).digest("hex");
|
|
|
24
24
|
* @returns {{files: Set<string>, reason?: string}} rutas repo-relativas de
|
|
25
25
|
* supervisor cuyo estado actual está respaldado por la provenance sellada.
|
|
26
26
|
*/
|
|
27
|
-
export function verifiedSupervisorFiles({ projectDir }) {
|
|
27
|
+
export function verifiedSupervisorFiles({ projectDir, trackedOnly = false }) {
|
|
28
28
|
let prov;
|
|
29
29
|
try {
|
|
30
30
|
prov = JSON.parse(readFileSync(resolve(projectDir, PROVENANCE_FILE), "utf8"));
|
|
@@ -37,7 +37,11 @@ export function verifiedSupervisorFiles({ projectDir }) {
|
|
|
37
37
|
}
|
|
38
38
|
const ok = new Set();
|
|
39
39
|
const unjudgeable = new Set();
|
|
40
|
-
|
|
40
|
+
// KJC-BUG-0259 (user's decision, option A): history is judged with the kj of
|
|
41
|
+
// when it was committed. The harness guards are machine-local and never in the
|
|
42
|
+
// repo; the review and each PR's CI verify them when the seal is made. A check
|
|
43
|
+
// over the repo's history (release check) judges only what is versioned.
|
|
44
|
+
const entries = (Array.isArray(prov?.files) ? prov.files : []).filter((e) => !trackedOnly || !String(e?.file).startsWith(HARNESS_PREFIX));
|
|
41
45
|
// KJC-BUG-0211: desde KJC-BUG-0197 el sello cubre tambien los guardias del
|
|
42
46
|
// supervisor, y aqui solo se sabian verificar los hooks. Como `complete`
|
|
43
47
|
// exige que TODAS las entradas verifiquen, el sello no se podia ni commitear.
|
|
@@ -93,10 +97,10 @@ export function verifiedSupervisorFiles({ projectDir }) {
|
|
|
93
97
|
* `note` dice lo que este sitio NO ha podido juzgar, para que un levantamiento
|
|
94
98
|
* mas laxo de lo que parece no pase por silencio (KJC-BUG-0212).
|
|
95
99
|
*/
|
|
96
|
-
export function liftSealedSupervisorViolations({ projectDir, violations }) {
|
|
100
|
+
export function liftSealedSupervisorViolations({ projectDir, violations, trackedOnly = false }) {
|
|
97
101
|
const RULE = "defaults.supervisor.write";
|
|
98
102
|
if (!violations.some((v) => v.rule_id === RULE && v.file)) return { violations, lifted: 0 };
|
|
99
|
-
const sealed = verifiedSupervisorFiles({ projectDir });
|
|
103
|
+
const sealed = verifiedSupervisorFiles({ projectDir, trackedOnly });
|
|
100
104
|
// La violación sobre la PROPIA provenance solo se alza si la provenance
|
|
101
105
|
// entera verificó — a medias no describe la verdad y sigue denegada.
|
|
102
106
|
const liftable = (v) =>
|
package/src/rag/migrate.js
CHANGED
|
@@ -7,11 +7,22 @@
|
|
|
7
7
|
* toca: borrarla es decision del usuario, no efecto de una migracion.
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
+
import { sep } from "node:path";
|
|
10
11
|
|
|
11
12
|
import { openVecStore, insertChunk, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
|
|
12
13
|
|
|
13
14
|
export const countProjectChunks = (db, slug) => db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE project_slug = ?").get(slug).n;
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* KJC-BUG-0258: chunks of the project's OWN files (sources under projectDir), as
|
|
18
|
+
* opposed to its plans and briefs. A prefix compare, not LIKE: paths may hold % or _.
|
|
19
|
+
*/
|
|
20
|
+
export const countProjectSources = (db, slug, projectDir) => {
|
|
21
|
+
// path.sep: sources are stored as native paths (backslashes on Windows).
|
|
22
|
+
const prefix = projectDir.replace(/[\\/]+$/, "") + sep;
|
|
23
|
+
return db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE project_slug = ? AND substr(source, 1, ?) = ?").get(slug, prefix.length, prefix).n;
|
|
24
|
+
};
|
|
25
|
+
|
|
15
26
|
/**
|
|
16
27
|
* KJC-TSK-0882: que decir cuando el indice del proyecto no tiene nada suyo.
|
|
17
28
|
* Si la base global tiene sus chunks, migrar (segundos); si no, indexar.
|
|
@@ -29,9 +40,12 @@ export function emptyIndexRemedy({ slug, legacyPath, dim = 768 }) {
|
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
/**
|
|
32
|
-
*
|
|
43
|
+
* KJC-BUG-0255: `keep(source)` is the indexer's criterion today (generated,
|
|
44
|
+
* excluded, .ragignore, sensitive, gone). The global index was filled before
|
|
45
|
+
* those rules, so copying it blind brought vendor/*.min.js along as noise.
|
|
46
|
+
* @returns {{state: "migrated"|"already"|"nothing", migrated: number, skipped?: number, reason: string}}
|
|
33
47
|
*/
|
|
34
|
-
export function migrateProjectIndex({ slug, legacyPath, targetPath, dim = 768 }) {
|
|
48
|
+
export function migrateProjectIndex({ slug, legacyPath, targetPath, dim = 768, keep = () => true }) {
|
|
35
49
|
const nothing = { state: "nothing", migrated: 0, reason: `the global index holds nothing for ${slug}: kj rag index --with-sources` };
|
|
36
50
|
if (!existsSync(legacyPath)) return nothing;
|
|
37
51
|
|
|
@@ -49,8 +63,10 @@ export function migrateProjectIndex({ slug, legacyPath, targetPath, dim = 768 })
|
|
|
49
63
|
const rows = legacy.prepare("SELECT id, source, kind, text, metadata, content_hash FROM chunks WHERE project_slug = ?").all(slug);
|
|
50
64
|
if (rows.length === 0) return nothing;
|
|
51
65
|
const vecOf = legacy.prepare("SELECT embedding FROM vec_chunks WHERE rowid = ?");
|
|
66
|
+
let skipped = 0;
|
|
52
67
|
const copy = target.transaction(() => {
|
|
53
68
|
for (const r of rows) {
|
|
69
|
+
if (!keep(r.source)) { skipped++; continue; }
|
|
54
70
|
const raw = vecOf.get(BigInt(r.id))?.embedding;
|
|
55
71
|
if (!raw) continue; // un chunk sin vector no se puede buscar: no se inventa
|
|
56
72
|
const embedding = new Float32Array(raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength));
|
|
@@ -63,7 +79,8 @@ export function migrateProjectIndex({ slug, legacyPath, targetPath, dim = 768 })
|
|
|
63
79
|
const migrated = countProjectChunks(target, slug);
|
|
64
80
|
const parts = [`${migrated} chunks of ${slug} copied with their embeddings`];
|
|
65
81
|
if (stamp) parts.push(`indexed at ${stamp.slice(0, 9)}`);
|
|
66
|
-
|
|
82
|
+
if (skipped) parts.push(`${skipped} left out (the indexer would not take them today: generated, excluded or gone)`);
|
|
83
|
+
return { state: "migrated", migrated, skipped, reason: parts.join(", ") };
|
|
67
84
|
} finally {
|
|
68
85
|
legacy.close();
|
|
69
86
|
}
|
package/src/review/card-first.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* hu-board → the branch must reference a LIVE card (block by default);
|
|
5
5
|
* planning-game/external → card-shaped reference required (warn default,
|
|
6
6
|
* block via method_gates.card_first). Exemptions explicit and visible:
|
|
7
|
-
* base branch
|
|
7
|
+
* base branch and configured prefixes (releases). No escape (ADR 0015): a
|
|
8
|
+
* gate an agent can switch off is a suggestion.
|
|
8
9
|
*/
|
|
9
10
|
import { listPlans, loadPlan } from "../plan/plan-store.js";
|
|
10
11
|
import { escapeRegExp } from "../utils/escape-regexp.js";
|
|
@@ -48,10 +49,7 @@ async function findLiveCardInBranch(projectDir, branch) {
|
|
|
48
49
|
return closedHit;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
export async function checkCardFirst({ config = {}, projectDir = process.cwd(), branch,
|
|
52
|
-
if (env.KJ_ALLOW_NO_CARD === "1") {
|
|
53
|
-
return { ok: true, mode: "exempt", reason: "KJ_ALLOW_NO_CARD=1 (explicit escape hatch)" };
|
|
54
|
-
}
|
|
52
|
+
export async function checkCardFirst({ config = {}, projectDir = process.cwd(), branch, deps = {} }) {
|
|
55
53
|
const exempt = config.method_gates?.card_first_exempt_branches || DEFAULT_EXEMPT_PREFIXES;
|
|
56
54
|
if (exempt.some((p) => branch.startsWith(p))) {
|
|
57
55
|
return { ok: true, mode: "exempt", reason: `branch prefix exempt (${branch.split("/")[0]}/…)` };
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Adaptador del gate de policy en el flujo de review de karajan-code
|
|
3
3
|
* (GOV-A, KJC-TSK-0745). La decisión warn/deny/inexcepcionable/excepción
|
|
4
|
-
* vive en el kernel (@karajan-family/governance); aquí solo el dominio:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* registrado dice "este diff exacto".
|
|
4
|
+
* vive en el kernel (@karajan-family/governance); aquí solo el dominio: el
|
|
5
|
+
* artefacto es el diff staged. Sin excepción por diff (ADR 0015,
|
|
6
|
+
* KJC-TSK-0932): una regla que falla se corrige por PR.
|
|
8
7
|
*/
|
|
9
8
|
import { evaluateGate } from "@karajan-family/governance";
|
|
10
9
|
|
|
@@ -13,13 +12,12 @@ import { evaluateGate } from "@karajan-family/governance";
|
|
|
13
12
|
*/
|
|
14
13
|
export function evaluatePolicyGate({
|
|
15
14
|
policy, errors = [], role = "coder", files = [], netLinesAdded = null,
|
|
16
|
-
diffHashValue = null,
|
|
15
|
+
diffHashValue = null, recordException = () => {},
|
|
17
16
|
standingExceptions = [], now = new Date(),
|
|
18
17
|
}) {
|
|
19
18
|
return evaluateGate({
|
|
20
19
|
policy, errors, role, files, netLinesAdded,
|
|
21
20
|
artifactHash: diffHashValue,
|
|
22
|
-
exemption: { requested: env.KJ_ALLOW_POLICY === "1", justification: env.KJ_POLICY_REASON, hint: 'KJ_POLICY_REASON="por qué"' },
|
|
23
21
|
recordException: (entry) => recordException({ ...entry, scope: "este diff exacto" }),
|
|
24
22
|
standingExceptions, now,
|
|
25
23
|
});
|
|
@@ -110,9 +110,9 @@ export async function runSonarPregate({ config, stagedFiles = [], touchedLines =
|
|
|
110
110
|
}
|
|
111
111
|
for (const f of own) indexed.add(`${prefix}${f}`);
|
|
112
112
|
keys.push(scan.projectKey);
|
|
113
|
-
// The scan above ALWAYS runs before issues are read (single-flight)
|
|
114
|
-
//
|
|
115
|
-
//
|
|
113
|
+
// The scan above ALWAYS runs before issues are read (single-flight), and
|
|
114
|
+
// it returns once the server has processed it (KJC-BUG-0263: before, the
|
|
115
|
+
// upload returned first and the issues read were the previous analysis).
|
|
116
116
|
const res = await getOpenIssues(config, scan.projectKey);
|
|
117
117
|
totalProject += res.total ?? (res.issues || []).length;
|
|
118
118
|
// Issue components are package-relative: made repo-relative here.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `method_gates.tests_with_code`. Test-FIRST remains the reviewer's
|
|
6
6
|
* judgment; test-PRESENT belongs to the gate. Patterns come from the
|
|
7
7
|
* project's own `development.*` config (same ones headless enforces).
|
|
8
|
-
*
|
|
8
|
+
* No escape (ADR 0015): deletions and comment-only cleanups already pass.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const DEFAULT_TEST_PATTERNS = ["/tests/", "/__tests__/", ".test.", ".spec."];
|
|
@@ -28,10 +28,7 @@ export function sourceFilesOf(config = {}, files = []) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function checkTestsWithCode({ config = {}, stagedFiles = [], numstat = null
|
|
32
|
-
if (env.KJ_ALLOW_NO_TESTS === "1") {
|
|
33
|
-
return { ok: true, mode: "exempt", reason: "KJ_ALLOW_NO_TESTS=1 (explicit escape hatch)" };
|
|
34
|
-
}
|
|
31
|
+
export function checkTestsWithCode({ config = {}, stagedFiles = [], numstat = null }) {
|
|
35
32
|
const { sources, hasTests } = sourceFilesOf(config, stagedFiles);
|
|
36
33
|
|
|
37
34
|
if (sources.length === 0 || hasTests) return { ok: true, mode: "pass" };
|
|
@@ -51,7 +48,7 @@ export function checkTestsWithCode({ config = {}, stagedFiles = [], numstat = nu
|
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
const policy = config.method_gates?.tests_with_code || "warn";
|
|
54
|
-
const reason = `source changes without any test change (${sources.slice(0, 5).join(", ")}${sources.length > 5 ? "…" : ""}) — the failing test comes first
|
|
51
|
+
const reason = `source changes without any test change (${sources.slice(0, 5).join(", ")}${sources.length > 5 ? "…" : ""}) — the failing test comes first: add one`;
|
|
55
52
|
return policy === "block"
|
|
56
53
|
? { ok: false, mode: "block", sources, reason }
|
|
57
54
|
: { ok: true, mode: "warn", sources, reason };
|
package/src/sonar/api.js
CHANGED
|
@@ -82,6 +82,27 @@ async function sonarFetch(config, urlPath) {
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
const CE_FINAL = new Set(["SUCCESS", "FAILED", "CANCELED"]);
|
|
86
|
+
const sleepMs = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* KJC-BUG-0263 — the scanner uploads the analysis and returns; the server
|
|
90
|
+
* processes it in the background, and issues read before that task ends are
|
|
91
|
+
* the PREVIOUS analysis. Polls /api/ce/task until it reaches a final status.
|
|
92
|
+
* @returns {Promise<{ok: boolean, status: string, timedOut?: boolean}>}
|
|
93
|
+
*/
|
|
94
|
+
export async function waitForAnalysis(config, taskId, { timeoutMs = 120_000, intervalMs = 1000, sleep = sleepMs } = {}) {
|
|
95
|
+
const deadline = Date.now() + timeoutMs;
|
|
96
|
+
let status = "UNKNOWN";
|
|
97
|
+
while (Date.now() < deadline) {
|
|
98
|
+
const body = await sonarFetch(config, `/api/ce/task?id=${encodeURIComponent(taskId)}`);
|
|
99
|
+
try { status = JSON.parse(body).task?.status || "UNKNOWN"; } catch { status = "UNKNOWN"; }
|
|
100
|
+
if (CE_FINAL.has(status)) return { ok: status === "SUCCESS", status };
|
|
101
|
+
await sleep(intervalMs);
|
|
102
|
+
}
|
|
103
|
+
return { ok: false, status, timedOut: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
85
106
|
/**
|
|
86
107
|
* KJC-BUG-0226 (issue #1838) — de donde sale la clave que se CONSULTA. El
|
|
87
108
|
* scanner sube el analisis con la que el repo declara en su
|
package/src/sonar/scanner.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { runCommand } from "../utils/process.js";
|
|
6
6
|
import { sonarUp } from "./manager.js";
|
|
7
|
+
import { waitForAnalysis } from "./api.js";
|
|
7
8
|
import { resolveSonarProjectKey } from "./project-key.js";
|
|
8
9
|
import {
|
|
9
10
|
resolveSonarHost,
|
|
@@ -380,14 +381,32 @@ export async function runSonarScan(config, projectKey = null, { verbose = false,
|
|
|
380
381
|
}
|
|
381
382
|
|
|
382
383
|
const result = await runCommand(cmd, args, { timeout: scannerTimeout, env, cwd: scanCwd });
|
|
384
|
+
// KJC-BUG-0263: the scan is done when the server has processed it; whoever reads
|
|
385
|
+
// the issues next would otherwise get the previous analysis. Said out loud when
|
|
386
|
+
// the log names no task to wait for.
|
|
387
|
+
let ok = result.exitCode === 0;
|
|
388
|
+
let analysisNote = null;
|
|
389
|
+
if (ok) {
|
|
390
|
+
const taskId = /api\/ce\/task\?id=([\w-]+)/.exec(String(result.stdout || ""))?.[1];
|
|
391
|
+
if (taskId) {
|
|
392
|
+
const done = await waitForAnalysis({ ...config, sonarqube: { ...sonarConfig, host: apiHost } }, taskId);
|
|
393
|
+
if (!done.ok) {
|
|
394
|
+
ok = false;
|
|
395
|
+
analysisNote = `sonar: the server did not finish analysis ${taskId} (${done.status}${done.timedOut ? ", timed out" : ""}) — its issues would be the previous analysis`;
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
analysisNote = "sonar: the scanner log names no analysis task, so kj could not wait for the server — issues may belong to the previous analysis";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
383
401
|
return {
|
|
384
|
-
ok
|
|
402
|
+
ok,
|
|
385
403
|
projectKey: effectiveProjectKey,
|
|
386
404
|
cwd: scanCwd,
|
|
387
405
|
scanner: pick.type,
|
|
388
|
-
note,
|
|
406
|
+
note: [note, analysisNote].filter(Boolean).join("; ") || null,
|
|
389
407
|
stdout: result.stdout,
|
|
390
|
-
|
|
408
|
+
// A scan that failed on the server's side says so, not with the scanner's own log.
|
|
409
|
+
stderr: !ok && result.exitCode === 0 ? analysisNote : result.stderr,
|
|
391
410
|
exitCode: result.exitCode
|
|
392
411
|
};
|
|
393
412
|
}
|