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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* hooks, which is why the guaranteed level requires Claude as host (ADR).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
import { join } from "node:path";
|
|
@@ -28,8 +28,7 @@ export function resolveSentinelRoot(dir = process.cwd()) {
|
|
|
28
28
|
|
|
29
29
|
const LIB_BODY = `// kj sentinel shared lib (KJC-TSK-0714) — managed by \`kj harden\`.
|
|
30
30
|
// Single source for every sentinel script: state, branch, classification,
|
|
31
|
-
//
|
|
32
|
-
import console from "node:console";
|
|
31
|
+
// and violations.
|
|
33
32
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
34
33
|
import { execSync, spawnSync } from "node:child_process";
|
|
35
34
|
import { dirname, join } from "node:path";
|
|
@@ -81,8 +80,20 @@ export const bootstrapPhase = () => {
|
|
|
81
80
|
try { execSync("git rev-parse --verify HEAD", { cwd: ROOT, stdio: ["ignore", "ignore", "ignore"] }); return false; }
|
|
82
81
|
catch { return true; }
|
|
83
82
|
};
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
// KJC-BUG-0198/0262: a card split across PRs is carried while another PR of it is
|
|
84
|
+
// open. kj decides with that checkable fact, the same answer for commit and push
|
|
85
|
+
// (the Stop gate still holds the turn: the board owes the move); when kj does not
|
|
86
|
+
// answer, every pending move blocks.
|
|
87
|
+
export const boardGate = (sid, pend) => {
|
|
88
|
+
if (!pend.length) return { blocking: [], carried: [] };
|
|
89
|
+
const bg = spawnSync("kj", ["sentinel", "board-gate", "--session", sid, "--json"], { cwd: ROOT, encoding: "utf8" });
|
|
90
|
+
if (!bg.error && (bg.status === 0 || bg.status === 2)) {
|
|
91
|
+
try { const d = JSON.parse(bg.stdout); return { blocking: d.blocking || [], carried: d.carried || [] }; } catch { /* unreadable: as always */ }
|
|
92
|
+
}
|
|
93
|
+
return { blocking: pend, carried: [] };
|
|
94
|
+
};
|
|
95
|
+
export const violations = (s, branch, pending = pendingMoves(s)) => {
|
|
96
|
+
const v = pending.map(pendingText);
|
|
86
97
|
if (!s || !(s.edited_sources || []).length) return v;
|
|
87
98
|
if (bootstrapPhase()) return v;
|
|
88
99
|
if (BASE_BRANCHES.has(branch)) v.push("Fuentes editadas en la rama base '" + branch + "' — crea una rama: git checkout -b feat/<CARD-ID>-descripcion");
|
|
@@ -105,26 +116,6 @@ const sessionAddsCode = (s) => {
|
|
|
105
116
|
return a === "-" || Number(a) > 0;
|
|
106
117
|
});
|
|
107
118
|
};
|
|
108
|
-
export const recordEscape = (sid, escape, tool) => {
|
|
109
|
-
const state = load();
|
|
110
|
-
const s = session(state, sid);
|
|
111
|
-
s.at = Date.now();
|
|
112
|
-
if (!s.escapes.includes(escape)) s.escapes.push(escape);
|
|
113
|
-
(state.escape_events ||= []).push({ escape, tool, sid, ts: Date.now() });
|
|
114
|
-
save(state);
|
|
115
|
-
// GOV-F (KJC-TSK-0768): el escape es una excepcion consciente — entra en el
|
|
116
|
-
// decision log hash-encadenado (kj policy seal), no solo en este estado.
|
|
117
|
-
// Best-effort CON aviso: fail-closed aqui encerraria la sesion justo cuando
|
|
118
|
-
// kj no carga, que es cuando mas se usa el escape (restaurar con git).
|
|
119
|
-
try {
|
|
120
|
-
const args = ["policy", "seal", "--escape", escape];
|
|
121
|
-
if (tool) args.push("--tool", String(tool));
|
|
122
|
-
const r = spawnSync("kj", args, { cwd: ROOT, encoding: "utf8" });
|
|
123
|
-
if (r.error || r.status !== 0) console.error("karajan sentinel: escape " + escape + " usado pero NO sellado en el decision log (kj policy seal " + (r.error ? r.error.message : "exit " + r.status) + ") — revisa kj; el escape queda solo en sentinel-state.json" + doc("escapes"));
|
|
124
|
-
} catch (e) {
|
|
125
|
-
console.error("karajan sentinel: escape " + escape + " usado pero NO sellado en el decision log: " + e.message + doc("escapes"));
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
119
|
`;
|
|
129
120
|
|
|
130
121
|
const POST_BODY = `#!/usr/bin/env node
|
|
@@ -136,7 +127,7 @@ import process from "node:process";
|
|
|
136
127
|
import { relative } from "node:path";
|
|
137
128
|
import { spawnSync } from "node:child_process";
|
|
138
129
|
import { CODE, TESTS, ROOT, CARD, branchOf, load, save, session } from "./sentinel-lib.mjs";
|
|
139
|
-
|
|
130
|
+
import { remindersFor } from "./sentinel-reminders.mjs";
|
|
140
131
|
let raw = "";
|
|
141
132
|
process.stdin.on("data", (d) => { raw += d; });
|
|
142
133
|
process.stdin.on("end", () => {
|
|
@@ -273,6 +264,15 @@ process.stdin.on("end", () => {
|
|
|
273
264
|
// "not found" counts as failure (KJC-BUG-0154): a move that moved nothing must not
|
|
274
265
|
// clear a pending — that would discard a LEGITIMATE one without touching the tracker.
|
|
275
266
|
if (moved && CLOSING.includes(moved[2].toLowerCase()) && !/error|fail|not found/i.test(text)) clearPending(moved[1].toUpperCase());
|
|
267
|
+
// KJC-TSK-0917 (ADR 0014): the method's reminders, after the action that precedes
|
|
268
|
+
// the one each rule is about; at most once every 25 Bash actions each. Never a block.
|
|
269
|
+
const st = load();
|
|
270
|
+
const ss = session(st, sid);
|
|
271
|
+
ss.step = (ss.step || 0) + 1;
|
|
272
|
+
const due = remindersFor(cmdText, { seen: ss.reminded || {}, step: ss.step });
|
|
273
|
+
for (const r of due) (ss.reminded ||= {})[r.id] = ss.step;
|
|
274
|
+
save(st);
|
|
275
|
+
if (due.length) process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: due.map((r) => r.say).join(" ") } }));
|
|
276
276
|
process.exit(0);
|
|
277
277
|
}
|
|
278
278
|
const file = input.file_path || input.notebook_path;
|
|
@@ -283,11 +283,6 @@ process.stdin.on("end", () => {
|
|
|
283
283
|
const rel = relative(ROOT, file).replaceAll("\\\\", "/");
|
|
284
284
|
const bucket = TESTS.test(rel) ? s.edited_tests : CODE.test(rel) ? s.edited_sources : null;
|
|
285
285
|
if (bucket && !bucket.includes(rel)) bucket.push(rel);
|
|
286
|
-
for (const e of ESCAPES)
|
|
287
|
-
if (process.env[e] === "1" && !s.escapes.includes(e)) {
|
|
288
|
-
s.escapes.push(e);
|
|
289
|
-
(state.escape_events ||= []).push({ escape: e, tool, sid, ts: Date.now() });
|
|
290
|
-
}
|
|
291
286
|
// KJC-TSK-0910: the branch size while it is written, with the CI budget
|
|
292
287
|
// (kj pr-size), said once per threshold crossed. Context, never a block.
|
|
293
288
|
let sizeNote = null;
|
|
@@ -407,7 +402,6 @@ process.stdin.on("end", () => {
|
|
|
407
402
|
s.blocks = 0;
|
|
408
403
|
const notes = [];
|
|
409
404
|
if (claimsNote) notes.push(claimsNote);
|
|
410
|
-
if ((s.escapes || []).length) notes.push("karajan sentinel: esta sesion uso " + s.escapes.length + " escape(s): " + s.escapes.join(", ") + " — decision registrada; detalle en kj sentinel status.");
|
|
411
405
|
if ((s.closed_cards || []).length) {
|
|
412
406
|
notes.push("karajan sentinel: card(s) cerrada(s) en este turno: " + s.closed_cards.join(", ") + " — el board las muestra: " + boardHint());
|
|
413
407
|
s.closed_cards = [];
|
|
@@ -444,7 +438,11 @@ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
|
444
438
|
import { spawnSync } from "node:child_process";
|
|
445
439
|
import { homedir } from "node:os";
|
|
446
440
|
import { fileURLToPath } from "node:url";
|
|
447
|
-
import { doc, CODE, TESTS, ROOT, BASE_BRANCHES, CARD, branchOf, foreignLane, load, save, session, violations,
|
|
441
|
+
import { doc, CODE, TESTS, ROOT, BASE_BRANCHES, CARD, branchOf, foreignLane, load, save, session, violations, boardGate, pendingMoves, pendingText } from "./sentinel-lib.mjs";
|
|
442
|
+
// KJC-TSK-0915 (ADR 0014): the shell reader is a real, unit-tested module copied here as is.
|
|
443
|
+
import { optionValues, shellSegments, stripInertQuotes, stripTextOptionValues } from "./sentinel-shell.mjs";
|
|
444
|
+
import { DISCARD_VERBS, discardOf, foreignLost, removedFiles } from "./sentinel-discard.mjs";
|
|
445
|
+
import { shellWrites, writesRepo } from "./sentinel-bash-write.mjs";
|
|
448
446
|
const EDIT_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
|
|
449
447
|
// KJC-BUG-0204: el comando real lleva flags EN MEDIO del verbo
|
|
450
448
|
// (firebase --account a@b --project p deploy --only hosting:main), asi que la
|
|
@@ -463,6 +461,25 @@ process.stdin.on("data", (d) => { raw += d; });
|
|
|
463
461
|
process.stdin.on("end", () => {
|
|
464
462
|
try {
|
|
465
463
|
const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, transcript_path: transcript = null } = JSON.parse(raw);
|
|
464
|
+
// KJC-TSK-0920 (SNT-E): every deny ends with the same line, whatever gate it was.
|
|
465
|
+
process.on("exit", (code) => {
|
|
466
|
+
if (code === 2) console.error("karajan: Karajan gobierna y se le obedece. No rodees el gate ni cambies la politica para pasarlo; si te parece injusto, diselo a tu usuario o usa kj report-issue.");
|
|
467
|
+
});
|
|
468
|
+
// KJC-BUG-0238 (#1886): de quien es cada cambio. La primera vez que la
|
|
469
|
+
// sesion toca un fichero se anota si estaba limpio: solo entonces un
|
|
470
|
+
// descarte posterior pierde nada mas que lo de la sesion. Antes de
|
|
471
|
+
// cualquier escape; git que falla cuenta como sucio (fail-closed).
|
|
472
|
+
const noteFirstTouch = (relT) => {
|
|
473
|
+
const st = load();
|
|
474
|
+
const touch = (session(st, sid).first_touch ||= {});
|
|
475
|
+
// hasOwn + defineProperty: a file named __proto__ or toString is a path, not a prototype key.
|
|
476
|
+
if (Object.hasOwn(touch, relT)) return;
|
|
477
|
+
// --ignored: a user's ignored .env is theirs too (git clean -x deletes it).
|
|
478
|
+
const gs = spawnSync("git", ["-C", ROOT, "status", "--porcelain", "--ignored", "--", relT], { encoding: "utf8" });
|
|
479
|
+
const value = gs.status === 0 && String(gs.stdout).trim() === "" ? "clean" : "dirty";
|
|
480
|
+
Object.defineProperty(touch, relT, { value, enumerable: true, writable: true, configurable: true });
|
|
481
|
+
save(st);
|
|
482
|
+
};
|
|
466
483
|
// Self-protection (KJC-TSK-0715) rules run BEFORE any escape, including
|
|
467
484
|
// KJ_SENTINEL_OFF: the sentinel is not dismantled from inside a session —
|
|
468
485
|
// only the human, editing outside it.
|
|
@@ -473,7 +490,16 @@ process.stdin.on("end", () => {
|
|
|
473
490
|
console.error("karajan sentinel: ese fichero es parte del supervisor (" + relT + ") — solo el humano desmonta el sentinel, editalo fuera de la sesion." + doc("supervisor"));
|
|
474
491
|
process.exit(2);
|
|
475
492
|
}
|
|
493
|
+
// KJC-TSK-0920 (SNT-E): policies, gate settings and exclusions are the human's,
|
|
494
|
+
// like the supervisor: a session does not loosen the rules that govern it.
|
|
495
|
+
if ([".karajan/policy.yml", ".karajan/kj.config.yml", ".ragignore"].includes(relT) || relT.endsWith("/.ragignore")) {
|
|
496
|
+
console.error("karajan sentinel: " + relT + " es configuracion de gobierno (politicas, gates, exclusiones): la cambia tu usuario, no la sesion. Si un gate te parece injusto, proponselo a tu usuario o usa kj report-issue." + doc("governance"));
|
|
497
|
+
process.exit(2);
|
|
498
|
+
}
|
|
499
|
+
if (relT && !relT.startsWith("..") && !relT.startsWith("/")) noteFirstTouch(relT);
|
|
476
500
|
}
|
|
501
|
+
// KJC-BUG-0261: rm / git rm is the session touching those files, noted before they go.
|
|
502
|
+
if (tool === "Bash") for (const words of shellSegments(String(input.command || ""))) for (const f of removedFiles(words, ROOT)) noteFirstTouch(f);
|
|
477
503
|
// Any Bash that NAMES the supervisor's files is denied — a write-verb
|
|
478
504
|
// blocklist is bypassable (cp, dd, one-liners), and reading them is what
|
|
479
505
|
// the Read/Grep tools are for. Shell indirection (variables, globs,
|
|
@@ -496,8 +522,8 @@ process.stdin.on("end", () => {
|
|
|
496
522
|
if (tool === "Bash" && /(^|[^a-zA-Z])gh([^a-zA-Z]|$)/.test(ghCmd) && /(^|[^a-z])(pr|issue|release)([^a-z]|$)/.test(ghCmd) && /(^|[^a-z])(create|edit|comment|review)([^a-z]|$)/.test(ghCmd)) {
|
|
497
523
|
const attrib = /co-authored-by:.{0,120}(claude|gpt|copilot|gemini)|(generated|written|created) (by|with).{0,120}(claude|gpt|copilot|gemini|codex)|generated with [[]?claude|🤖/is;
|
|
498
524
|
let corpus = ghCmd;
|
|
499
|
-
|
|
500
|
-
|
|
525
|
+
// KJC-BUG-0245: the shell reader ends the path at ; && | (a regex took "pr.md;").
|
|
526
|
+
for (const bodyPath of optionValues(ghCmd, ["--body-file", "--notes-file", "--comment-file"])) {
|
|
501
527
|
try { corpus += "\\n" + readFileSync(bodyPath, "utf8"); } catch {
|
|
502
528
|
console.error("karajan sentinel: no puedo leer " + bodyPath + " para el escaneo de atribucion — sin escaneo no se publica." + doc("attribution"));
|
|
503
529
|
process.exit(2);
|
|
@@ -535,69 +561,34 @@ process.stdin.on("end", () => {
|
|
|
535
561
|
process.exit(2);
|
|
536
562
|
}
|
|
537
563
|
}
|
|
564
|
+
// KJC-BUG-0238 (#1886): un agente no descarta cambios que no hizo. Sin
|
|
565
|
+
// escape y antes de KJ_SENTINEL_OFF: es perdida de datos del usuario, y
|
|
566
|
+
// guardarlo (git stash push) nunca pierde nada.
|
|
567
|
+
if (tool === "Bash") {
|
|
568
|
+
const cmdD = String(input.command || "");
|
|
569
|
+
// A git discard nested in $( ), <( ), backticks, eval, xargs or sh -c cannot be read: fail-closed.
|
|
570
|
+
const nested = ["$(", "<(", ">(", String.fromCharCode(96)].some((n) => cmdD.includes(n)) || /(^|[ ;&|])(eval|xargs|bash|sh|zsh|env)( |$)/.test(cmdD);
|
|
571
|
+
if (nested && /(^|[^a-z])git([^a-z]|$)/.test(cmdD) && DISCARD_VERBS.some((v) => cmdD.includes(v))) {
|
|
572
|
+
console.error("karajan sentinel: un descarte git dentro de $( ), backticks, eval, xargs o sh -c no es verificable — ejecutalo como comando simple." + doc("discard"));
|
|
573
|
+
process.exit(2);
|
|
574
|
+
}
|
|
575
|
+
const touch = load().sessions?.[sid]?.first_touch || {};
|
|
576
|
+
for (const words of shellSegments(cmdD)) {
|
|
577
|
+
const d = discardOf(words, ROOT);
|
|
578
|
+
const lost = d ? foreignLost(d, touch, ROOT) : [];
|
|
579
|
+
if (lost.length === 0) continue;
|
|
580
|
+
console.error(d.stash
|
|
581
|
+
? "karajan sentinel: git stash drop/clear borra trabajo guardado que puede no ser de esta sesion — pideselo a tu usuario." + doc("discard")
|
|
582
|
+
: "karajan sentinel: ese comando descarta cambios que esta sesion no hizo (" + lost.slice(0, 5).join(", ") + (lost.length > 5 ? ", +" + (lost.length - 5) : "") + ") — pueden ser trabajo sin commitear de tu usuario. Si estorban, guardalos recuperables (git stash push -- <ficheros>) y avisale; si el cambio es tuyo sobre su trabajo, deshazlo con Edit. Sin escape: es perdida de datos." + doc("discard"));
|
|
583
|
+
process.exit(2);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
538
586
|
if (process.env.KJ_SENTINEL_OFF === "1") process.exit(0);
|
|
539
|
-
// KJC-
|
|
540
|
-
//
|
|
541
|
-
// prefijo jamas llegaba a process.env y el escape era inalcanzable
|
|
542
|
-
// (deadlock real del gate de release: la landing solo se pone verde
|
|
543
|
-
// tras el deploy y el deploy estaba bloqueado por el mismo gate). Un
|
|
544
|
-
// escape es una excepcion CONSCIENTE y auditada, no un boundary de
|
|
545
|
-
// seguridad (security sigue sin escape y la autoprotecion corre antes),
|
|
546
|
-
// asi que el TEXTO del comando es fuente valida: solo asignaciones
|
|
547
|
-
// LITERALES al inicio (tokens VAR=VAL antes del primer verbo) y solo
|
|
548
|
-
// sobre un comando SIMPLE — con ;|& $() backtick o salto de linea el
|
|
549
|
-
// prefijo shell no alcanzaria a los comandos posteriores (catch de
|
|
550
|
-
// codex: "X=1 true && npm publish" escaparia sin serlo), asi que el
|
|
551
|
-
// escape por texto se rechaza y queda la via env de siempre.
|
|
587
|
+
// ADR 0015 (KJC-TSK-0927): no gate has an escape any more, so the command
|
|
588
|
+
// text is read only by the identity lock below.
|
|
552
589
|
const CMD_TEXT = tool === "Bash" ? String(input.command || "") : "";
|
|
553
590
|
const ESC_TAB = String.fromCharCode(9);
|
|
554
591
|
const ESC_NL = String.fromCharCode(10);
|
|
555
|
-
const ESC_BT = String.fromCharCode(96);
|
|
556
|
-
const escOn = (name) => {
|
|
557
|
-
if (process.env[name] === "1") return true;
|
|
558
|
-
let found = false;
|
|
559
|
-
let i = 0;
|
|
560
|
-
while (i < CMD_TEXT.length) {
|
|
561
|
-
while (i < CMD_TEXT.length && (CMD_TEXT[i] === " " || CMD_TEXT[i] === ESC_TAB)) i += 1;
|
|
562
|
-
const start = i;
|
|
563
|
-
while (i < CMD_TEXT.length && CMD_TEXT[i] !== " " && CMD_TEXT[i] !== ESC_TAB && CMD_TEXT[i] !== ESC_NL) i += 1;
|
|
564
|
-
const tok = CMD_TEXT.slice(start, i);
|
|
565
|
-
const eq = tok.indexOf("=");
|
|
566
|
-
if (eq <= 0 || !/^[A-Z][A-Z0-9_]*$/.test(tok.slice(0, eq))) break;
|
|
567
|
-
if (tok === name + "=1") found = true;
|
|
568
|
-
}
|
|
569
|
-
if (!found) return false;
|
|
570
|
-
// KJC-BUG-0147: lo que va ENTRE COMILLAS no encadena comandos — un
|
|
571
|
-
// mensaje "fix(x): ... (KJC-TSK-1)" es un comando simple. Dentro de
|
|
572
|
-
// comillas dobles $ y backtick siguen expandiendo (se rechazan); en
|
|
573
|
-
// comillas simples todo es literal. Y si el escape esta pero se ignora,
|
|
574
|
-
// SE DICE: un escape ignorado en silencio era el bug.
|
|
575
|
-
const bad = escSimple();
|
|
576
|
-
if (bad === null) return true;
|
|
577
|
-
if (!escNoted.has(name)) {
|
|
578
|
-
escNoted.add(name);
|
|
579
|
-
console.error("karajan sentinel: " + name + "=1 presente pero IGNORADO — el escape solo vale en un comando simple y este contiene " + JSON.stringify(bad) + " fuera de comillas simples (sin ; | & $ ( ) backtick ni salto de linea: parte el comando o usa -F fichero)" + doc("escapes"));
|
|
580
|
-
}
|
|
581
|
-
return false;
|
|
582
|
-
};
|
|
583
|
-
const escNoted = new Set();
|
|
584
|
-
const escSimple = () => {
|
|
585
|
-
let q = null;
|
|
586
|
-
for (let i = 0; i < CMD_TEXT.length; i += 1) {
|
|
587
|
-
const c = CMD_TEXT[i];
|
|
588
|
-
if (q === "'") { if (c === "'") q = null; continue; }
|
|
589
|
-
if (q === '"') {
|
|
590
|
-
if (c === '"') q = null;
|
|
591
|
-
else if (c === "$" || c === ESC_BT) return c;
|
|
592
|
-
else if (c === "\\\\") i += 1;
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
if (c === "\\\\") { i += 1; continue; }
|
|
596
|
-
if (c === "'" || c === '"') { q = c; continue; }
|
|
597
|
-
if (c === ";" || c === "|" || c === "&" || c === ESC_NL || c === ESC_BT || c === "$" || c === "(" || c === ")") return c;
|
|
598
|
-
}
|
|
599
|
-
return q === null ? null : "comilla sin cerrar";
|
|
600
|
-
};
|
|
601
592
|
// IDN-B (KJC-TSK-0763, ADR 0005): identity lock — gh and mutating git never
|
|
602
593
|
// run under an account other than the one THIS CLONE declared (the incident:
|
|
603
594
|
// a gh comment without switch went out as the client account). Deny BEFORE
|
|
@@ -693,11 +684,8 @@ process.stdin.on("end", () => {
|
|
|
693
684
|
};
|
|
694
685
|
scan(CMD_TEXT, 0);
|
|
695
686
|
if (problems.length > 0) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
console.error("karajan sentinel: identity lock (ADR 0005) —" + problems.map((p) => ESC_NL + "- " + p).join("") + ESC_NL + "(KJ_ALLOW_IDENTITY=1 = excepcion consciente, queda registrada)" + doc("identity"));
|
|
699
|
-
process.exit(2);
|
|
700
|
-
}
|
|
687
|
+
console.error("karajan sentinel: identity lock (ADR 0005) —" + problems.map((p) => ESC_NL + "- " + p).join("") + ESC_NL + "(sin escape, ADR 0015: pon la cuenta en el comando)" + doc("identity"));
|
|
688
|
+
process.exit(2);
|
|
701
689
|
}
|
|
702
690
|
}
|
|
703
691
|
// MONO-0 (KJC-TSK-0737, ADR 0002): cada sesion muta solo SU worktree;
|
|
@@ -717,8 +705,7 @@ process.stdin.on("end", () => {
|
|
|
717
705
|
return d ? foreignLane(d) : null;
|
|
718
706
|
};
|
|
719
707
|
const laneDeny = (what, lane) => {
|
|
720
|
-
|
|
721
|
-
console.error("karajan sentinel: " + what + " vive en otro carril (" + lane + ") de este repo — cada sesion muta solo SU worktree (MONO-0). Cruce deliberado: KJ_ALLOW_CROSS_LANE=1, queda registrado." + doc("cross-lane"));
|
|
708
|
+
console.error("karajan sentinel: " + what + " vive en otro carril (" + lane + ") de este repo — cada sesion muta solo SU worktree (MONO-0). Sin escape (ADR 0015): un cruce deliberado lo hace tu usuario en su terminal." + doc("cross-lane"));
|
|
722
709
|
return true;
|
|
723
710
|
};
|
|
724
711
|
if (EDIT_TOOLS.includes(tool)) {
|
|
@@ -747,18 +734,16 @@ process.stdin.on("end", () => {
|
|
|
747
734
|
// (find fuera: -delete/-exec mutan — reviewer catch; sus tokens de
|
|
748
735
|
// carril los caza el escaner de abajo.)
|
|
749
736
|
const READONLY = /^[ \\t]*(grep|rg|cat|head|tail|less|ls|wc|diff|stat|file|du|tree|git (log|show|diff|status|blame))\\b[^;|&<>$\`(){}\\n\\r]*$/;
|
|
750
|
-
|
|
737
|
+
// KJC-BUG-0243: operators inside inert quotes (grep -e "a|b") do not chain anything.
|
|
738
|
+
if (!READONLY.test(stripInertQuotes(cmd))) {
|
|
751
739
|
// cd/pushd invalida TODO razonamiento textual de rutas posteriores
|
|
752
740
|
// (carrera de bypasses confirmada en review: destino bare, con $,
|
|
753
741
|
// relativas post-cd...): en un comando NO-read-only, cambiar de
|
|
754
742
|
// directorio se deniega conservadoramente — el remedio es no
|
|
755
743
|
// necesitarlo (git -C, npm --prefix, rutas absolutas).
|
|
756
744
|
if (/(^|[;&|(\\s])(cd|pushd)([ \\t]|$)/.test(cmd)) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
console.error("karajan sentinel: cd/pushd en un comando mutador — las rutas posteriores no son verificables por el guard de carriles (MONO-0); usa git -C / npm --prefix / rutas ABSOLUTAS sin cd (o KJ_ALLOW_CROSS_LANE=1, queda registrado)." + doc("cross-lane"));
|
|
760
|
-
process.exit(2);
|
|
761
|
-
}
|
|
745
|
+
console.error("karajan sentinel: cd/pushd en un comando mutador — las rutas posteriores no son verificables por el guard de carriles (MONO-0); usa git -C / npm --prefix / rutas ABSOLUTAS sin cd." + doc("cross-lane"));
|
|
746
|
+
process.exit(2);
|
|
762
747
|
}
|
|
763
748
|
// Absolute AND dot-relative tokens (../lane escapaba al primer scan —
|
|
764
749
|
// reviewer catch); relative se resuelve contra el cwd de la sesion.
|
|
@@ -773,11 +758,8 @@ process.stdin.on("end", () => {
|
|
|
773
758
|
// de tokens de abajo; el residuo (expansion anidada en segmentos
|
|
774
759
|
// runner) queda documentado como en la regla de ficheros PROTECTED.
|
|
775
760
|
if (/>{1,2}[ \\t]*["']?[$\`]/.test(cmd)) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
console.error("karajan sentinel: redireccion con destino tras variable/sustitucion — el guard de carriles no puede verificarlo (MONO-0); usa una ruta LITERAL (o KJ_ALLOW_CROSS_LANE=1, queda registrado)." + doc("cross-lane"));
|
|
779
|
-
process.exit(2);
|
|
780
|
-
}
|
|
761
|
+
console.error("karajan sentinel: redireccion con destino tras variable/sustitucion — el guard de carriles no puede verificarlo (MONO-0); usa una ruta LITERAL." + doc("cross-lane"));
|
|
762
|
+
process.exit(2);
|
|
781
763
|
}
|
|
782
764
|
// Sustitucion de comandos ($(...) o backticks) EJECUTA su contenido
|
|
783
765
|
// en cualquier posicion, y una ruta entrecomillada con espacios es
|
|
@@ -838,21 +820,17 @@ process.stdin.on("end", () => {
|
|
|
838
820
|
if (q !== null) return true; // comilla sin cerrar: no verificable
|
|
839
821
|
return flush();
|
|
840
822
|
};
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
823
|
+
// KJC-BUG-0243: inert quoted text is not a substitution, and the prose value
|
|
824
|
+
// of a text option (--title, -m...) is not a path.
|
|
825
|
+
if (/\\$\\(|\`/.test(stripInertQuotes(cmd)) || quotedPathWithSpaces(stripTextOptionValues(cmd))) {
|
|
826
|
+
console.error("karajan sentinel: sustitucion de comandos o ruta entrecomillada con espacios en un comando mutador — no verificable por el guard de carriles (MONO-0); usa valores/rutas LITERALES sin sustitucion." + doc("cross-lane"));
|
|
827
|
+
process.exit(2);
|
|
847
828
|
}
|
|
848
829
|
const SAFE_EXP_SEG = /^([A-Za-z_][A-Za-z0-9_]*=[^ \\t]*[ \\t]*)*((npm|pnpm|yarn|vitest|jest|kj|gh|echo|printf|true|test)\\b|git[ \\t](?![^\\n]*(-C[ \\t]|--git-dir|--work-tree)))[^;|&\\n]*$|^[A-Za-z_][A-Za-z0-9_]*=[^;|&\\n]*$/;
|
|
849
830
|
const segs = cmd.split(/&&|\\|\\||[;|\\n]/).map((s) => s.trim()).filter(Boolean);
|
|
850
831
|
if (!segs.every((s) => !/[$]/.test(s) || SAFE_EXP_SEG.test(s))) {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
console.error("karajan sentinel: expansion de shell en una herramienta generica de fichero/interprete — el objetivo no es verificable por el guard de carriles (MONO-0); usa rutas LITERALES o un runner del toolchain (o KJ_ALLOW_CROSS_LANE=1, queda registrado)." + doc("cross-lane"));
|
|
854
|
-
process.exit(2);
|
|
855
|
-
}
|
|
832
|
+
console.error("karajan sentinel: expansion de shell en una herramienta generica de fichero/interprete — el objetivo no es verificable por el guard de carriles (MONO-0); usa rutas LITERALES o un runner del toolchain." + doc("cross-lane"));
|
|
833
|
+
process.exit(2);
|
|
856
834
|
}
|
|
857
835
|
// ...incluidas relativas A PELO (los carriles kj viven en
|
|
858
836
|
// .kj/worktrees/). Un falso token (s/a/b/) resuelve al arbol propio
|
|
@@ -868,6 +846,21 @@ process.stdin.on("end", () => {
|
|
|
868
846
|
}
|
|
869
847
|
}
|
|
870
848
|
}
|
|
849
|
+
// KJC-BUG-0237 (#1886): the repo is written through Edit/Write only, the path every gate
|
|
850
|
+
// guards. After the lane guard, whose message is the precise one for another lane.
|
|
851
|
+
if (tool === "Bash") {
|
|
852
|
+
let moved = false; // after cd/pushd/popd, a relative target cannot be placed: fail-closed
|
|
853
|
+
// $( ) and backticks run even inside double quotes: their bodies, read from the raw text, are commands too.
|
|
854
|
+
const raw = String(input.command || "");
|
|
855
|
+
const subs = [...[...raw.matchAll(/[$][(]([^()]*)[)]/g)].map((m) => m[1]), ...raw.split(String.fromCharCode(96)).filter((_, k) => k % 2 === 1)];
|
|
856
|
+
for (const words of [...shellSegments(raw), ...subs.flatMap((s) => shellSegments(s))]) {
|
|
857
|
+
if (["cd", "pushd", "popd"].includes(words[0])) moved = true;
|
|
858
|
+
const inRepo = shellWrites(words).filter((t) => writesRepo(t, ROOT) || (moved && !t.startsWith("/")));
|
|
859
|
+
if (inRepo.length === 0) continue;
|
|
860
|
+
console.error("karajan sentinel: escribir ficheros del repo desde Bash (" + inRepo.slice(0, 3).join(", ") + ") se salta los gates de Edit/Write — usa la tool Edit/Write; para renombrar, git mv; fuera del repo (/tmp, scratchpad) Bash es libre." + doc("bash-write"));
|
|
861
|
+
process.exit(2);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
871
864
|
// KJC-TSK-0734 (PL-B): con .karajan/policy.yml presente, la evaluacion
|
|
872
865
|
// la hace el MOTOR via kj policy eval --strict (exit 2 = deny, contrato
|
|
873
866
|
// PL-A); los defaults del supervisor viven en el motor y el check
|
|
@@ -875,8 +868,8 @@ process.stdin.on("end", () => {
|
|
|
875
868
|
// Fail CLOSED por defecto (catches de codex; doctrina KJC-BUG-0095: un
|
|
876
869
|
// gate no se cae en silencio): solo exit 0 permite. kj inejecutable =
|
|
877
870
|
// deny duro (el remedio es restaurar kj); cualquier otro fallo de
|
|
878
|
-
// evaluacion = deny
|
|
879
|
-
//
|
|
871
|
+
// evaluacion = deny sin escape (ADR 0015): un typo de YAML lo corrige el
|
|
872
|
+
// usuario fuera de la sesion.
|
|
880
873
|
if ((EDIT_TOOLS.includes(tool) || tool === "Bash") && existsSync(resolve(ROOT, ".karajan", "policy.yml"))) {
|
|
881
874
|
// PL-C: el rol que ACTÚA (KJ_POLICY_ROLE, sembrado por los runners de
|
|
882
875
|
// kj run en los subprocesos) — el anfitrión-brain evalúa como coder.
|
|
@@ -890,11 +883,9 @@ process.stdin.on("end", () => {
|
|
|
890
883
|
let v = null;
|
|
891
884
|
try { v = JSON.parse(String(pres.stdout || "").trim().split("\\n").pop()); } catch { /* mensaje generico */ }
|
|
892
885
|
const secure = !!(v && v.class === "security");
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
process.exit(2);
|
|
897
|
-
}
|
|
886
|
+
// ADR 0015 (KJC-TSK-0932): no escape; a rule that misfires is fixed by PR.
|
|
887
|
+
console.error("karajan sentinel: policy deny [" + ((v && v.rule_id) || "policy") + "] " + ((v && v.reason) || "la tool call viola la policy del proyecto") + (secure ? " [security — sin escape ni arbitraje]" : " (sin escape: si la regla falla, tu usuario corrige .karajan/policy.yml por PR)") + doc("policy"));
|
|
888
|
+
process.exit(2);
|
|
898
889
|
} else if (pres.status !== 0) {
|
|
899
890
|
// KJC-BUG-0207: un kj que NO ARRANCA no es una violacion de policy. Con
|
|
900
891
|
// kj linkado al arbol, un error de sintaxis transitorio en src/ dejaba
|
|
@@ -919,9 +910,8 @@ process.stdin.on("end", () => {
|
|
|
919
910
|
console.error("karajan sentinel: kj no arranca (" + broken[0] + (at ? " en " + at : "") + "), asi que la policy no se puede evaluar y NADA MAS pasa: edita " + (brokenFile || "el fichero que rompe kj") + " para arreglarlo. No es un deny de policy, es kj roto." + doc("policy"));
|
|
920
911
|
process.exit(2);
|
|
921
912
|
}
|
|
922
|
-
} else
|
|
923
|
-
|
|
924
|
-
console.error("karajan sentinel: kj policy eval fallo (exit " + pres.status + ") — la policy declarada no se pudo evaluar, deny por defecto; diagnostica con kj policy check y corrige .karajan/policy.yml fuera de la sesion (o KJ_ALLOW_POLICY=1 = excepcion consciente, queda registrada)." + doc("policy"));
|
|
913
|
+
} else {
|
|
914
|
+
console.error("karajan sentinel: kj policy eval fallo (exit " + pres.status + ") — la policy declarada no se pudo evaluar, deny por defecto; diagnostica con kj policy check; .karajan/policy.yml lo corrige tu usuario fuera de la sesion." + doc("policy"));
|
|
925
915
|
process.exit(2);
|
|
926
916
|
}
|
|
927
917
|
}
|
|
@@ -942,11 +932,8 @@ process.stdin.on("end", () => {
|
|
|
942
932
|
let blockOn = false;
|
|
943
933
|
try { blockOn = /steward:\\s*["']?block/.test(readFileSync(join(ROOT, ".karajan", "kj.config.yml"), "utf8")); } catch { /* no config — no block */ }
|
|
944
934
|
if (hard.length && blockOn) {
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
console.error("karajan sentinel: steward — el estado del proyecto bloquea empezar (barrido " + rep.sweptAt + "):" + String.fromCharCode(10) + hard.map((r) => "- " + r.id + ": " + (r.evidence || "") + " -> " + (r.remedy || r.renew || "kj steward sweep")).join(String.fromCharCode(10)) + String.fromCharCode(10) + "(KJ_ALLOW_STEWARD=1 = excepcion consciente de esta sesion, queda registrada)" + doc("steward"));
|
|
948
|
-
process.exit(2);
|
|
949
|
-
}
|
|
935
|
+
console.error("karajan sentinel: steward — el estado del proyecto bloquea empezar (barrido " + rep.sweptAt + "):" + String.fromCharCode(10) + hard.map((r) => "- " + r.id + ": " + (r.evidence || "") + " -> " + (r.remedy || r.renew || "kj steward sweep")).join(String.fromCharCode(10)) + String.fromCharCode(10) + "(sin escape, ADR 0015: si el trabajo sigue, tu usuario cambia method_gates.steward a inform)" + doc("steward"));
|
|
936
|
+
process.exit(2);
|
|
950
937
|
}
|
|
951
938
|
const st = load();
|
|
952
939
|
const s = session(st, sid);
|
|
@@ -963,11 +950,11 @@ process.stdin.on("end", () => {
|
|
|
963
950
|
if (rel && CODE.test(rel) && !TESTS.test(rel)) {
|
|
964
951
|
const branch = branchOf();
|
|
965
952
|
const why = !branch ? null : BASE_BRANCHES.has(branch) ? "base" : !CARD.test(branch) ? "nocard" : null;
|
|
953
|
+
// ADR 0015 (KJC-TSK-0925): no escape. Without a card there is no work.
|
|
966
954
|
if (why) {
|
|
967
|
-
if (escOn("KJ_ALLOW_NO_CARD")) { recordEscape(sid, "KJ_ALLOW_NO_CARD", tool); process.exit(0); }
|
|
968
955
|
console.error(why === "base"
|
|
969
|
-
? "karajan sentinel: no se editan fuentes en la rama base '" + branch + "' — crea la card (kj hu add) y la rama: git checkout -b feat/<CARD-ID>-descripcion.
|
|
970
|
-
: "karajan sentinel: la rama '" + branch + "' no referencia ninguna card — crea/mueve la card a running (kj hu add | kj hu move) y usa una rama feat/<CARD-ID>-descripcion.
|
|
956
|
+
? "karajan sentinel: no se editan fuentes en la rama base '" + branch + "' — crea la card (kj hu add) y la rama: git checkout -b feat/<CARD-ID>-descripcion." + doc("card-first")
|
|
957
|
+
: "karajan sentinel: la rama '" + branch + "' no referencia ninguna card — crea/mueve la card a running (kj hu add | kj hu move) y usa una rama feat/<CARD-ID>-descripcion." + doc("card-first"));
|
|
971
958
|
process.exit(2);
|
|
972
959
|
}
|
|
973
960
|
// KJC-TSK-0848 (ADR 0010, RAG-B): the RAG must have answered about this
|
|
@@ -990,15 +977,14 @@ process.stdin.on("end", () => {
|
|
|
990
977
|
// KJC-BUG-0190: with nothing indexed, no query can cover anything —
|
|
991
978
|
// the gate asks for kj rag index instead of denying the impossible.
|
|
992
979
|
const covered = rel.startsWith("..") || hits.includes(rel) || hits.some((h) => dirOf(h) === dirOf(rel)) || ((rs.rag_queries || []).length > 0 && fresh()) || rs.rag_index_empty === true;
|
|
980
|
+
// ADR 0015 (KJC-TSK-0925): no escape. The cases it covered (new file, empty
|
|
981
|
+
// index, not indexable) are decided by the gate itself.
|
|
993
982
|
if (!covered) {
|
|
994
|
-
|
|
995
|
-
// Recorded ONCE per session: the escape is a conscious exception, not a per-edit tax.
|
|
996
|
-
if (!(rs.escapes || []).includes("KJ_ALLOW_NO_RAG")) recordEscape(sid, "KJ_ALLOW_NO_RAG", tool);
|
|
997
|
-
} else {
|
|
983
|
+
{
|
|
998
984
|
// KJC-BUG-0216 (issue #1807): un .astro, un .php o cualquier fichero
|
|
999
985
|
// sin adapter NO puede estar en el indice, asi que exigir una
|
|
1000
986
|
// respuesta del RAG sobre el es pedir lo imposible, y empujaba a
|
|
1001
|
-
// dejar
|
|
987
|
+
// dejar el escape puesto para siempre. La exencion es SOLO
|
|
1002
988
|
// esa: un indice vacio o por detras si tiene arreglo (kj rag index)
|
|
1003
989
|
// y sigue bloqueando, porque si no el gate desapareceria en
|
|
1004
990
|
// cualquier proyecto sin indexar. Nada legible = se bloquea.
|
|
@@ -1018,7 +1004,7 @@ process.stdin.on("end", () => {
|
|
|
1018
1004
|
console.error("karajan sentinel: rag-first — el indice aun no tiene " + rel + " (" + absent + "); indexalo y consulta despues: kj_rag_query / kj rag query <que hace " + rel + ">" + doc("rag-first"));
|
|
1019
1005
|
process.exit(2);
|
|
1020
1006
|
} else {
|
|
1021
|
-
console.error("karajan sentinel: rag-first — el RAG no ha respondido sobre " + rel + " en esta sesion; consulta antes de tocarlo: kj_rag_query / kj rag query <que hace " + rel + " y donde mas vive ese concepto>.
|
|
1007
|
+
console.error("karajan sentinel: rag-first — el RAG no ha respondido sobre " + rel + " en esta sesion; consulta antes de tocarlo: kj_rag_query / kj rag query <que hace " + rel + " y donde mas vive ese concepto>." + doc("rag-first"));
|
|
1022
1008
|
process.exit(2);
|
|
1023
1009
|
}
|
|
1024
1010
|
}
|
|
@@ -1053,11 +1039,10 @@ process.stdin.on("end", () => {
|
|
|
1053
1039
|
// depender de como parta las palabras el separador (catch de la review).
|
|
1054
1040
|
const movesHooks = cmd.toLowerCase().includes("core.hookspath");
|
|
1055
1041
|
if ((inOrder(["git", "commit"], words) && words.some(skipsVerify)) || movesHooks) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1042
|
+
// ADR 0015 (KJC-TSK-0926): no escape. A broken hook is fixed; skipping it,
|
|
1043
|
+
// if it ever must happen, is the user's act in their own terminal.
|
|
1044
|
+
console.error("karajan sentinel: el gate de commit no se apaga con una bandera — el review cruzado y la policy corren en el hook, y saltarselos deja el diff sin veredicto. Si el hook esta roto, arreglalo; si de verdad hace falta saltarlo, lo hace tu usuario en su terminal." + doc("escapes"));
|
|
1045
|
+
process.exit(2);
|
|
1061
1046
|
}
|
|
1062
1047
|
const MERGE = /\\bgh\\s+pr\\s+merge(\\s+(\\d+))?/;
|
|
1063
1048
|
const ADVANCE = /\\bgit\\s+commit\\b|\\bgit\\s+push\\b|\\bgh\\s+pr\\s+create\\b/; // push also falls under violations() — explicit here too (review catch)
|
|
@@ -1065,20 +1050,14 @@ process.stdin.on("end", () => {
|
|
|
1065
1050
|
const pend = pendingMoves(load().sessions?.[sid]);
|
|
1066
1051
|
if (pend.length > 0 && (mergeM || ADVANCE.test(cmd))) {
|
|
1067
1052
|
// KJC-BUG-0198: una card partida en varias PRs no puede quedar bloqueada
|
|
1068
|
-
// por su primera mitad
|
|
1069
|
-
|
|
1070
|
-
const bg = spawnSync("kj", ["sentinel", "board-gate", "--session", sid, "--json"], { cwd: ROOT, encoding: "utf8" });
|
|
1071
|
-
let blocking = pend, carried = [];
|
|
1072
|
-
if (!bg.error && (bg.status === 0 || bg.status === 2)) {
|
|
1073
|
-
try { const d = JSON.parse(bg.stdout); blocking = d.blocking || []; carried = d.carried || []; } catch { /* sin respuesta legible: como siempre */ }
|
|
1074
|
-
}
|
|
1053
|
+
// por su primera mitad (boardGate, en la lib).
|
|
1054
|
+
const { blocking, carried } = boardGate(sid, pend);
|
|
1075
1055
|
for (const c of carried) console.error("karajan sentinel: board-sync arrastra " + c.card + " — " + c.why + ", se movera al cerrarla");
|
|
1056
|
+
// ADR 0015 (KJC-TSK-0925): no escape; an epic and a card split across PRs
|
|
1057
|
+
// are already decided above (KJC-BUG-0230, 0198).
|
|
1076
1058
|
if (blocking.length > 0) {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
console.error("karajan sentinel: board-sync — el metodo no avanza con cards mergeadas sin mover:\\n" + blocking.map((p) => "- " + pendingText(p)).join("\\n") + "\\n(KJ_ALLOW_BOARD=1 = excepcion consciente, queda registrada)" + doc("board-sync"));
|
|
1080
|
-
process.exit(2);
|
|
1081
|
-
}
|
|
1059
|
+
console.error("karajan sentinel: board-sync — el metodo no avanza con cards mergeadas sin mover:\\n" + blocking.map((p) => "- " + pendingText(p)).join("\\n") + doc("board-sync"));
|
|
1060
|
+
process.exit(2);
|
|
1082
1061
|
}
|
|
1083
1062
|
}
|
|
1084
1063
|
// (the pending entry itself is recorded by the PostToolUse hook, only once
|
|
@@ -1087,9 +1066,9 @@ process.stdin.on("end", () => {
|
|
|
1087
1066
|
// there misleads every future reader. When the command writes one from a
|
|
1088
1067
|
// file, kj crosses ITS data against this turn's outputs; the hook carries
|
|
1089
1068
|
// no policy (kj reads method_gates.claims; off = this check does not exist).
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1069
|
+
// KJC-BUG-0245: the path is read by the shell reader, so "pr.md;" is "pr.md".
|
|
1070
|
+
const body = /\\bgh\\s+pr\\s+(?:create|edit)\\b/.test(cmd) ? optionValues(cmd, ["--body-file"])[0] : undefined;
|
|
1071
|
+
if (body && transcript) {
|
|
1093
1072
|
const g = spawnSync("kj", ["claims", "gate", "--transcript", transcript, "--file", body], { cwd: ROOT, encoding: "utf8" });
|
|
1094
1073
|
if (!g.error && g.status === 2) {
|
|
1095
1074
|
console.error("karajan sentinel: claims — un dato del cuerpo de la PR esta DESMENTIDO por las salidas de este turno:\\n" + (g.stderr || "").trim() + "\\nVerificalo o marcalo como no comprobado antes de crear la PR. Detalle: kj claims check --transcript " + transcript + " --file " + body + doc("claims"));
|
|
@@ -1097,7 +1076,8 @@ process.stdin.on("end", () => {
|
|
|
1097
1076
|
}
|
|
1098
1077
|
}
|
|
1099
1078
|
if (isPublish(cmd)) {
|
|
1100
|
-
|
|
1079
|
+
// ADR 0015 (KJC-TSK-0932): no escape — KJC-BUG-0204 and 0259 keep the
|
|
1080
|
+
// check from blocking its own fix.
|
|
1101
1081
|
// KJC-BUG-0204: el check se evalua PARA este comando. Un item que
|
|
1102
1082
|
// declara remedied_by no bloquea el comando que lo repara (desplegar
|
|
1103
1083
|
// la landing era justo lo que el check pedia); publicar no se exime.
|
|
@@ -1114,19 +1094,15 @@ process.stdin.on("end", () => {
|
|
|
1114
1094
|
if (res.status !== 0) {
|
|
1115
1095
|
let items = "";
|
|
1116
1096
|
try { items = (parsed.checks || []).filter((c) => !c.ok && c.lifted !== true).map((c) => "\\n- " + c.name + ": " + c.detail).join(""); } catch { /* raw output */ }
|
|
1117
|
-
console.error("karajan sentinel: release check en ROJO — no se publica ni despliega hasta resolverlo:" + (items || "\\n- corre kj release check para el detalle") + "\\n(
|
|
1097
|
+
console.error("karajan sentinel: release check en ROJO — no se publica ni despliega hasta resolverlo:" + (items || "\\n- corre kj release check para el detalle") + "\\n(sin escape: se repara lo que el check nombra)" + doc("release"));
|
|
1118
1098
|
process.exit(2);
|
|
1119
1099
|
}
|
|
1120
1100
|
} else if (PUSH.test(cmd)) {
|
|
1121
|
-
// KJC-BUG-0147:
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1101
|
+
// KJC-BUG-0147: a pending move blocks the push like it blocks the commit.
|
|
1102
|
+
// ADR 0015 (KJC-TSK-0925): with no escape.
|
|
1103
|
+
// KJC-BUG-0262: a carried card does not block the push the commit let through.
|
|
1124
1104
|
const sess = load().sessions?.[sid];
|
|
1125
|
-
|
|
1126
|
-
if (v.length && pendingMoves(sess).length && escOn("KJ_ALLOW_BOARD")) {
|
|
1127
|
-
recordEscape(sid, "KJ_ALLOW_BOARD", tool);
|
|
1128
|
-
v = violations({ ...sess, pending_moves: [] }, branchOf());
|
|
1129
|
-
}
|
|
1105
|
+
const v = violations(sess, branchOf(), boardGate(sid, pendingMoves(sess)).blocking);
|
|
1130
1106
|
if (v.length) {
|
|
1131
1107
|
console.error("karajan sentinel: git push con el metodo en rojo:\\n" + v.map((x) => "- " + x).join("\\n") + "\\nResuelve antes de empujar. Estado: kj sentinel status" + doc("push-gate"));
|
|
1132
1108
|
process.exit(2);
|
|
@@ -1143,6 +1119,14 @@ const SCRIPT_BODIES = {
|
|
|
1143
1119
|
"posttooluse.mjs": POST_BODY,
|
|
1144
1120
|
"stop.mjs": STOP_BODY,
|
|
1145
1121
|
"pretooluse-sentinel.mjs": PRETOOL_BODY,
|
|
1122
|
+
// KJC-TSK-0915 (ADR 0014): real modules, copied byte for byte under the SAME name
|
|
1123
|
+
// (so they can import each other in both places). Being here, the installed
|
|
1124
|
+
// record, the tamper check and the human seal cover them as well.
|
|
1125
|
+
"sentinel-shell.mjs": readFileSync(new URL("./sentinel/sentinel-shell.mjs", import.meta.url), "utf8"),
|
|
1126
|
+
"sentinel-discard.mjs": readFileSync(new URL("./sentinel/sentinel-discard.mjs", import.meta.url), "utf8"),
|
|
1127
|
+
"sentinel-bash-write.mjs": readFileSync(new URL("./sentinel/sentinel-bash-write.mjs", import.meta.url), "utf8"),
|
|
1128
|
+
"sentinel-reminders.mjs": readFileSync(new URL("./sentinel/sentinel-reminders.mjs", import.meta.url), "utf8"),
|
|
1129
|
+
"sessionstart.mjs": readFileSync(new URL("./sentinel/sessionstart.mjs", import.meta.url), "utf8"),
|
|
1146
1130
|
};
|
|
1147
1131
|
|
|
1148
1132
|
/**
|
|
@@ -1246,6 +1230,7 @@ export function verifySentinelScripts({ projectDir, readFileFn = readFileSync, g
|
|
|
1246
1230
|
// first attempt at this fix, and the review was right to reject it.
|
|
1247
1231
|
const sealed = sealedByPath(root, gitShowFn);
|
|
1248
1232
|
const ownRecord = readInstalledRecord(dir);
|
|
1233
|
+
const hasRecord = existsSync(join(dir, INSTALLED_RECORD));
|
|
1249
1234
|
const drift = [];
|
|
1250
1235
|
const tampered = [];
|
|
1251
1236
|
const regenerated = [];
|
|
@@ -1254,6 +1239,10 @@ export function verifySentinelScripts({ projectDir, readFileFn = readFileSync, g
|
|
|
1254
1239
|
const hash = text === undefined ? null : sha256(text);
|
|
1255
1240
|
if (hash && sealed.get(`.karajan/harness/${name}`) === hash) drift.push(name);
|
|
1256
1241
|
else if (hash && ownRecord[name] === hash) regenerated.push(name);
|
|
1242
|
+
// KJC-TSK-0915: absent from an install kj RECORDED (installed.json exists), never
|
|
1243
|
+
// recorded and never sealed = a guard newer than this install, not a deleted
|
|
1244
|
+
// one. Writing it only adds. A dir with no record is not a kj install at all.
|
|
1245
|
+
else if (!hash && hasRecord && !Object.hasOwn(ownRecord, name) && !sealed.has(`.karajan/harness/${name}`)) regenerated.push(name);
|
|
1257
1246
|
else tampered.push(name);
|
|
1258
1247
|
}
|
|
1259
1248
|
// KJC-BUG-0224 caso 1: lo que kj escribio y nadie sello ni toco se pone al
|
|
@@ -1333,7 +1322,13 @@ export function installSentinelHooks({ projectDir = process.cwd(), logger = cons
|
|
|
1333
1322
|
// edit matchers wired, the board-sync recorder never ran in production.
|
|
1334
1323
|
{ event: "PostToolUse", matcher: "Bash", script: "posttooluse.mjs" },
|
|
1335
1324
|
{ event: "PostToolUse", matcher: "mcp__.*__update_card", script: "posttooluse.mjs" },
|
|
1325
|
+
// KJC-BUG-0236 (#1886): the rag-first ledger records MCP answers here;
|
|
1326
|
+
// unwired, the official tool never satisfied the gate.
|
|
1327
|
+
{ event: "PostToolUse", matcher: "mcp__.*__kj_rag_query", script: "posttooluse.mjs" },
|
|
1336
1328
|
{ event: "Stop", script: "stop.mjs" },
|
|
1329
|
+
// KJC-TSK-0918 (ADR 0014): the critical rules back after a compaction or a resume.
|
|
1330
|
+
{ event: "SessionStart", matcher: "compact", script: "sessionstart.mjs" },
|
|
1331
|
+
{ event: "SessionStart", matcher: "resume", script: "sessionstart.mjs" },
|
|
1337
1332
|
],
|
|
1338
1333
|
});
|
|
1339
1334
|
return { scripts: [lib, post, stop, pre], wired, deferred };
|