karajan-code 4.38.0 → 4.40.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/src/audit/ai-slop-findings.js +4 -2
- package/src/audit/circular-deps.js +8 -7
- package/src/audit/webperf-input.js +3 -1
- package/src/checks/method.js +1 -1
- package/src/checks/release-check.js +3 -1
- package/src/cli/advanced-commands.js +1 -1
- package/src/cli/register-meta.js +112 -1
- package/src/cli/register-pipeline.js +2 -1
- package/src/commands/init.js +6 -2
- package/src/commands/review-gate.js +21 -22
- package/src/commands/rules-approve.js +59 -0
- package/src/commands/rules-compile.js +116 -0
- package/src/commands/rules-decide.js +51 -0
- package/src/commands/rules-review.js +44 -0
- package/src/commands/rules.js +169 -0
- package/src/config/loader.js +23 -1
- package/src/environment/adr.js +5 -2
- package/src/harden/config-templates.js +3 -0
- package/src/harden/harness-hooks.js +7 -85
- package/src/harden/hook-templates.js +6 -5
- package/src/harden/human-act.js +70 -0
- package/src/harden/phone-sign.js +26 -0
- package/src/harden/sentinel/pretooluse-rules.mjs +25 -0
- package/src/harden/sentinel/sentinel-bash-write.mjs +2 -6
- package/src/harden/sentinel/sentinel-discard.mjs +33 -4
- package/src/harden/sentinel/sentinel-rules.mjs +86 -0
- package/src/harden/sentinel/sentinel-shell.mjs +34 -2
- package/src/harden/sentinel/sessionstart.mjs +18 -9
- package/src/harden/sentinel-hooks.js +111 -194
- package/src/harden/supervisor-commit.js +13 -56
- package/src/mcp/handlers/run-handler.js +5 -0
- package/src/mcp/sovereignty-guard.js +16 -15
- package/src/orchestrator/preflight-checks.js +4 -3
- package/src/policy/supervisor-verify.js +20 -6
- package/src/privacy/scan.js +7 -0
- package/src/review/card-first.js +3 -5
- package/src/review/gate-gitignore.js +8 -0
- 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/rules/approval-view.js +49 -0
- package/src/rules/compiled.js +108 -0
- package/src/rules/coverage.js +23 -0
- package/src/rules/evaluate.js +60 -0
- package/src/rules/inventory.js +118 -0
- package/src/sonar/api.js +21 -0
- package/src/sonar/config-resolver.js +19 -1
- package/src/sonar/scanner.js +22 -3
- package/src/utils/run-log.js +74 -2
- package/src/utils/stack-detect.js +12 -0
|
@@ -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,11 +80,34 @@ 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
|
+
// KJC-BUG-0265: on the base branch what counts is work still there (the session's
|
|
96
|
+
// files dirty, or local commits the remote does not have), not the ledger: after a
|
|
97
|
+
// merged PR and a sync of main the ledger stays full while the base holds nothing.
|
|
98
|
+
// Anything git cannot answer (no upstream, odd path) counts as work: fail closed.
|
|
99
|
+
const baseHoldsWork = (files) => {
|
|
100
|
+
if (files.some((f) => !/^[A-Za-z0-9._/-]+$/.test(f))) return true;
|
|
101
|
+
try {
|
|
102
|
+
if (execSync("git status --porcelain -- " + files.join(" "), { cwd: ROOT, stdio: ["ignore", "pipe", "ignore"] }).toString().trim()) return true;
|
|
103
|
+
return execSync("git rev-list @{u}..HEAD", { cwd: ROOT, stdio: ["ignore", "pipe", "ignore"] }).toString().trim() !== "";
|
|
104
|
+
} catch { return true; }
|
|
105
|
+
};
|
|
106
|
+
export const violations = (s, branch, pending = pendingMoves(s)) => {
|
|
107
|
+
const v = pending.map(pendingText);
|
|
86
108
|
if (!s || !(s.edited_sources || []).length) return v;
|
|
87
109
|
if (bootstrapPhase()) return v;
|
|
88
|
-
if (BASE_BRANCHES.has(branch)) v.push("Fuentes editadas en la rama base '" + branch + "' — crea una rama: git checkout -b feat/<CARD-ID>-descripcion");
|
|
110
|
+
if (BASE_BRANCHES.has(branch)) { if (baseHoldsWork(s.edited_sources)) v.push("Fuentes editadas en la rama base '" + branch + "' — crea una rama: git checkout -b feat/<CARD-ID>-descripcion"); }
|
|
89
111
|
else if (branch && !CARD.test(branch)) v.push("La rama '" + branch + "' no referencia ninguna card — usa feat/<CARD-ID>-descripcion (y una card VIVA en el board)");
|
|
90
112
|
if (!(s.edited_tests || []).length && sessionAddsCode(s)) v.push("Fuentes editadas sin tocar un solo test (" + s.edited_sources.join(", ") + ") — escribe o actualiza el test que prueba el cambio");
|
|
91
113
|
return v;
|
|
@@ -105,26 +127,6 @@ const sessionAddsCode = (s) => {
|
|
|
105
127
|
return a === "-" || Number(a) > 0;
|
|
106
128
|
});
|
|
107
129
|
};
|
|
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
130
|
`;
|
|
129
131
|
|
|
130
132
|
const POST_BODY = `#!/usr/bin/env node
|
|
@@ -137,7 +139,6 @@ import { relative } from "node:path";
|
|
|
137
139
|
import { spawnSync } from "node:child_process";
|
|
138
140
|
import { CODE, TESTS, ROOT, CARD, branchOf, load, save, session } from "./sentinel-lib.mjs";
|
|
139
141
|
import { remindersFor } from "./sentinel-reminders.mjs";
|
|
140
|
-
const ESCAPES = ["KJ_ALLOW_WRITE", "KJ_ALLOW_REWRITE", "KJ_ALLOW_NO_CARD", "KJ_ALLOW_NO_TESTS", "KJ_ALLOW_PII", "KJ_ALLOW_POLICY", "KJ_ALLOW_IDENTITY", "KJ_ALLOW_BOARD", "KJ_ALLOW_NO_RAG", "KJ_ALLOW_NO_VERIFY"];
|
|
141
142
|
let raw = "";
|
|
142
143
|
process.stdin.on("data", (d) => { raw += d; });
|
|
143
144
|
process.stdin.on("end", () => {
|
|
@@ -293,11 +294,6 @@ process.stdin.on("end", () => {
|
|
|
293
294
|
const rel = relative(ROOT, file).replaceAll("\\\\", "/");
|
|
294
295
|
const bucket = TESTS.test(rel) ? s.edited_tests : CODE.test(rel) ? s.edited_sources : null;
|
|
295
296
|
if (bucket && !bucket.includes(rel)) bucket.push(rel);
|
|
296
|
-
for (const e of ESCAPES)
|
|
297
|
-
if (process.env[e] === "1" && !s.escapes.includes(e)) {
|
|
298
|
-
s.escapes.push(e);
|
|
299
|
-
(state.escape_events ||= []).push({ escape: e, tool, sid, ts: Date.now() });
|
|
300
|
-
}
|
|
301
297
|
// KJC-TSK-0910: the branch size while it is written, with the CI budget
|
|
302
298
|
// (kj pr-size), said once per threshold crossed. Context, never a block.
|
|
303
299
|
let sizeNote = null;
|
|
@@ -417,7 +413,6 @@ process.stdin.on("end", () => {
|
|
|
417
413
|
s.blocks = 0;
|
|
418
414
|
const notes = [];
|
|
419
415
|
if (claimsNote) notes.push(claimsNote);
|
|
420
|
-
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.");
|
|
421
416
|
if ((s.closed_cards || []).length) {
|
|
422
417
|
notes.push("karajan sentinel: card(s) cerrada(s) en este turno: " + s.closed_cards.join(", ") + " — el board las muestra: " + boardHint());
|
|
423
418
|
s.closed_cards = [];
|
|
@@ -454,10 +449,10 @@ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
|
454
449
|
import { spawnSync } from "node:child_process";
|
|
455
450
|
import { homedir } from "node:os";
|
|
456
451
|
import { fileURLToPath } from "node:url";
|
|
457
|
-
import { doc, CODE, TESTS, ROOT, BASE_BRANCHES, CARD, branchOf, foreignLane, load, save, session, violations,
|
|
452
|
+
import { doc, CODE, TESTS, ROOT, BASE_BRANCHES, CARD, branchOf, foreignLane, load, save, session, violations, boardGate, pendingMoves, pendingText } from "./sentinel-lib.mjs";
|
|
458
453
|
// KJC-TSK-0915 (ADR 0014): the shell reader is a real, unit-tested module copied here as is.
|
|
459
|
-
import { shellSegments, stripInertQuotes, stripTextOptionValues } from "./sentinel-shell.mjs";
|
|
460
|
-
import { DISCARD_VERBS, discardOf, foreignLost } from "./sentinel-discard.mjs";
|
|
454
|
+
import { optionValues, shellSegments, stripInertQuotes, stripTextOptionValues } from "./sentinel-shell.mjs";
|
|
455
|
+
import { DISCARD_VERBS, discardOf, foreignLost, removedFiles } from "./sentinel-discard.mjs";
|
|
461
456
|
import { shellWrites, writesRepo } from "./sentinel-bash-write.mjs";
|
|
462
457
|
const EDIT_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
|
|
463
458
|
// KJC-BUG-0204: el comando real lleva flags EN MEDIO del verbo
|
|
@@ -481,6 +476,21 @@ process.stdin.on("end", () => {
|
|
|
481
476
|
process.on("exit", (code) => {
|
|
482
477
|
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.");
|
|
483
478
|
});
|
|
479
|
+
// KJC-BUG-0238 (#1886): de quien es cada cambio. La primera vez que la
|
|
480
|
+
// sesion toca un fichero se anota si estaba limpio: solo entonces un
|
|
481
|
+
// descarte posterior pierde nada mas que lo de la sesion. Antes de
|
|
482
|
+
// cualquier escape; git que falla cuenta como sucio (fail-closed).
|
|
483
|
+
const noteFirstTouch = (relT) => {
|
|
484
|
+
const st = load();
|
|
485
|
+
const touch = (session(st, sid).first_touch ||= {});
|
|
486
|
+
// hasOwn + defineProperty: a file named __proto__ or toString is a path, not a prototype key.
|
|
487
|
+
if (Object.hasOwn(touch, relT)) return;
|
|
488
|
+
// --ignored: a user's ignored .env is theirs too (git clean -x deletes it).
|
|
489
|
+
const gs = spawnSync("git", ["-C", ROOT, "status", "--porcelain", "--ignored", "--", relT], { encoding: "utf8" });
|
|
490
|
+
const value = gs.status === 0 && String(gs.stdout).trim() === "" ? "clean" : "dirty";
|
|
491
|
+
Object.defineProperty(touch, relT, { value, enumerable: true, writable: true, configurable: true });
|
|
492
|
+
save(st);
|
|
493
|
+
};
|
|
484
494
|
// Self-protection (KJC-TSK-0715) rules run BEFORE any escape, including
|
|
485
495
|
// KJ_SENTINEL_OFF: the sentinel is not dismantled from inside a session —
|
|
486
496
|
// only the human, editing outside it.
|
|
@@ -493,27 +503,16 @@ process.stdin.on("end", () => {
|
|
|
493
503
|
}
|
|
494
504
|
// KJC-TSK-0920 (SNT-E): policies, gate settings and exclusions are the human's,
|
|
495
505
|
// like the supervisor: a session does not loosen the rules that govern it.
|
|
496
|
-
|
|
506
|
+
// .karajan/rules.yml (KJC-TSK-0947, ADR 0016): the compiled rules are the user's to approve.
|
|
507
|
+
// .karajan/rules.local.yml (KJC-TSK-0954, ADR 0017): the same rules, the ones from private sources.
|
|
508
|
+
if ([".karajan/policy.yml", ".karajan/kj.config.yml", ".karajan/rules.yml", ".karajan/rules.local.yml", ".ragignore"].includes(relT) || relT.endsWith("/.ragignore")) {
|
|
497
509
|
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"));
|
|
498
510
|
process.exit(2);
|
|
499
511
|
}
|
|
500
|
-
|
|
501
|
-
// sesion toca un fichero se anota si estaba limpio: solo entonces un
|
|
502
|
-
// descarte posterior pierde nada mas que lo de la sesion. Antes de
|
|
503
|
-
// cualquier escape; git que falla cuenta como sucio (fail-closed).
|
|
504
|
-
if (relT && !relT.startsWith("..") && !relT.startsWith("/")) {
|
|
505
|
-
const st = load();
|
|
506
|
-
const touch = (session(st, sid).first_touch ||= {});
|
|
507
|
-
// hasOwn + defineProperty: a file named __proto__ or toString is a path, not a prototype key.
|
|
508
|
-
if (!Object.hasOwn(touch, relT)) {
|
|
509
|
-
// --ignored: a user's ignored .env is theirs too (git clean -x deletes it).
|
|
510
|
-
const gs = spawnSync("git", ["-C", ROOT, "status", "--porcelain", "--ignored", "--", relT], { encoding: "utf8" });
|
|
511
|
-
const value = gs.status === 0 && String(gs.stdout).trim() === "" ? "clean" : "dirty";
|
|
512
|
-
Object.defineProperty(touch, relT, { value, enumerable: true, writable: true, configurable: true });
|
|
513
|
-
save(st);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
512
|
+
if (relT && !relT.startsWith("..") && !relT.startsWith("/")) noteFirstTouch(relT);
|
|
516
513
|
}
|
|
514
|
+
// KJC-BUG-0261: rm / git rm is the session touching those files, noted before they go.
|
|
515
|
+
if (tool === "Bash") for (const words of shellSegments(String(input.command || ""))) for (const f of removedFiles(words, ROOT)) noteFirstTouch(f);
|
|
517
516
|
// Any Bash that NAMES the supervisor's files is denied — a write-verb
|
|
518
517
|
// blocklist is bypassable (cp, dd, one-liners), and reading them is what
|
|
519
518
|
// the Read/Grep tools are for. Shell indirection (variables, globs,
|
|
@@ -536,8 +535,8 @@ process.stdin.on("end", () => {
|
|
|
536
535
|
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)) {
|
|
537
536
|
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;
|
|
538
537
|
let corpus = ghCmd;
|
|
539
|
-
|
|
540
|
-
|
|
538
|
+
// KJC-BUG-0245: the shell reader ends the path at ; && | (a regex took "pr.md;").
|
|
539
|
+
for (const bodyPath of optionValues(ghCmd, ["--body-file", "--notes-file", "--comment-file"])) {
|
|
541
540
|
try { corpus += "\\n" + readFileSync(bodyPath, "utf8"); } catch {
|
|
542
541
|
console.error("karajan sentinel: no puedo leer " + bodyPath + " para el escaneo de atribucion — sin escaneo no se publica." + doc("attribution"));
|
|
543
542
|
process.exit(2);
|
|
@@ -598,68 +597,11 @@ process.stdin.on("end", () => {
|
|
|
598
597
|
}
|
|
599
598
|
}
|
|
600
599
|
if (process.env.KJ_SENTINEL_OFF === "1") process.exit(0);
|
|
601
|
-
// KJC-
|
|
602
|
-
//
|
|
603
|
-
// prefijo jamas llegaba a process.env y el escape era inalcanzable
|
|
604
|
-
// (deadlock real del gate de release: la landing solo se pone verde
|
|
605
|
-
// tras el deploy y el deploy estaba bloqueado por el mismo gate). Un
|
|
606
|
-
// escape es una excepcion CONSCIENTE y auditada, no un boundary de
|
|
607
|
-
// seguridad (security sigue sin escape y la autoprotecion corre antes),
|
|
608
|
-
// asi que el TEXTO del comando es fuente valida: solo asignaciones
|
|
609
|
-
// LITERALES al inicio (tokens VAR=VAL antes del primer verbo) y solo
|
|
610
|
-
// sobre un comando SIMPLE — con ;|& $() backtick o salto de linea el
|
|
611
|
-
// prefijo shell no alcanzaria a los comandos posteriores (catch de
|
|
612
|
-
// codex: "X=1 true && npm publish" escaparia sin serlo), asi que el
|
|
613
|
-
// escape por texto se rechaza y queda la via env de siempre.
|
|
600
|
+
// ADR 0015 (KJC-TSK-0927): no gate has an escape any more, so the command
|
|
601
|
+
// text is read only by the identity lock below.
|
|
614
602
|
const CMD_TEXT = tool === "Bash" ? String(input.command || "") : "";
|
|
615
603
|
const ESC_TAB = String.fromCharCode(9);
|
|
616
604
|
const ESC_NL = String.fromCharCode(10);
|
|
617
|
-
const ESC_BT = String.fromCharCode(96);
|
|
618
|
-
const escOn = (name) => {
|
|
619
|
-
if (process.env[name] === "1") return true;
|
|
620
|
-
let found = false;
|
|
621
|
-
let i = 0;
|
|
622
|
-
while (i < CMD_TEXT.length) {
|
|
623
|
-
while (i < CMD_TEXT.length && (CMD_TEXT[i] === " " || CMD_TEXT[i] === ESC_TAB)) i += 1;
|
|
624
|
-
const start = i;
|
|
625
|
-
while (i < CMD_TEXT.length && CMD_TEXT[i] !== " " && CMD_TEXT[i] !== ESC_TAB && CMD_TEXT[i] !== ESC_NL) i += 1;
|
|
626
|
-
const tok = CMD_TEXT.slice(start, i);
|
|
627
|
-
const eq = tok.indexOf("=");
|
|
628
|
-
if (eq <= 0 || !/^[A-Z][A-Z0-9_]*$/.test(tok.slice(0, eq))) break;
|
|
629
|
-
if (tok === name + "=1") found = true;
|
|
630
|
-
}
|
|
631
|
-
if (!found) return false;
|
|
632
|
-
// KJC-BUG-0147: lo que va ENTRE COMILLAS no encadena comandos — un
|
|
633
|
-
// mensaje "fix(x): ... (KJC-TSK-1)" es un comando simple. Dentro de
|
|
634
|
-
// comillas dobles $ y backtick siguen expandiendo (se rechazan); en
|
|
635
|
-
// comillas simples todo es literal. Y si el escape esta pero se ignora,
|
|
636
|
-
// SE DICE: un escape ignorado en silencio era el bug.
|
|
637
|
-
const bad = escSimple();
|
|
638
|
-
if (bad === null) return true;
|
|
639
|
-
if (!escNoted.has(name)) {
|
|
640
|
-
escNoted.add(name);
|
|
641
|
-
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"));
|
|
642
|
-
}
|
|
643
|
-
return false;
|
|
644
|
-
};
|
|
645
|
-
const escNoted = new Set();
|
|
646
|
-
const escSimple = () => {
|
|
647
|
-
let q = null;
|
|
648
|
-
for (let i = 0; i < CMD_TEXT.length; i += 1) {
|
|
649
|
-
const c = CMD_TEXT[i];
|
|
650
|
-
if (q === "'") { if (c === "'") q = null; continue; }
|
|
651
|
-
if (q === '"') {
|
|
652
|
-
if (c === '"') q = null;
|
|
653
|
-
else if (c === "$" || c === ESC_BT) return c;
|
|
654
|
-
else if (c === "\\\\") i += 1;
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
if (c === "\\\\") { i += 1; continue; }
|
|
658
|
-
if (c === "'" || c === '"') { q = c; continue; }
|
|
659
|
-
if (c === ";" || c === "|" || c === "&" || c === ESC_NL || c === ESC_BT || c === "$" || c === "(" || c === ")") return c;
|
|
660
|
-
}
|
|
661
|
-
return q === null ? null : "comilla sin cerrar";
|
|
662
|
-
};
|
|
663
605
|
// IDN-B (KJC-TSK-0763, ADR 0005): identity lock — gh and mutating git never
|
|
664
606
|
// run under an account other than the one THIS CLONE declared (the incident:
|
|
665
607
|
// a gh comment without switch went out as the client account). Deny BEFORE
|
|
@@ -755,11 +697,8 @@ process.stdin.on("end", () => {
|
|
|
755
697
|
};
|
|
756
698
|
scan(CMD_TEXT, 0);
|
|
757
699
|
if (problems.length > 0) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
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"));
|
|
761
|
-
process.exit(2);
|
|
762
|
-
}
|
|
700
|
+
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"));
|
|
701
|
+
process.exit(2);
|
|
763
702
|
}
|
|
764
703
|
}
|
|
765
704
|
// MONO-0 (KJC-TSK-0737, ADR 0002): cada sesion muta solo SU worktree;
|
|
@@ -779,8 +718,7 @@ process.stdin.on("end", () => {
|
|
|
779
718
|
return d ? foreignLane(d) : null;
|
|
780
719
|
};
|
|
781
720
|
const laneDeny = (what, lane) => {
|
|
782
|
-
|
|
783
|
-
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"));
|
|
721
|
+
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"));
|
|
784
722
|
return true;
|
|
785
723
|
};
|
|
786
724
|
if (EDIT_TOOLS.includes(tool)) {
|
|
@@ -817,11 +755,8 @@ process.stdin.on("end", () => {
|
|
|
817
755
|
// directorio se deniega conservadoramente — el remedio es no
|
|
818
756
|
// necesitarlo (git -C, npm --prefix, rutas absolutas).
|
|
819
757
|
if (/(^|[;&|(\\s])(cd|pushd)([ \\t]|$)/.test(cmd)) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
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"));
|
|
823
|
-
process.exit(2);
|
|
824
|
-
}
|
|
758
|
+
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"));
|
|
759
|
+
process.exit(2);
|
|
825
760
|
}
|
|
826
761
|
// Absolute AND dot-relative tokens (../lane escapaba al primer scan —
|
|
827
762
|
// reviewer catch); relative se resuelve contra el cwd de la sesion.
|
|
@@ -836,11 +771,8 @@ process.stdin.on("end", () => {
|
|
|
836
771
|
// de tokens de abajo; el residuo (expansion anidada en segmentos
|
|
837
772
|
// runner) queda documentado como en la regla de ficheros PROTECTED.
|
|
838
773
|
if (/>{1,2}[ \\t]*["']?[$\`]/.test(cmd)) {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
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"));
|
|
842
|
-
process.exit(2);
|
|
843
|
-
}
|
|
774
|
+
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"));
|
|
775
|
+
process.exit(2);
|
|
844
776
|
}
|
|
845
777
|
// Sustitucion de comandos ($(...) o backticks) EJECUTA su contenido
|
|
846
778
|
// en cualquier posicion, y una ruta entrecomillada con espacios es
|
|
@@ -904,20 +836,14 @@ process.stdin.on("end", () => {
|
|
|
904
836
|
// KJC-BUG-0243: inert quoted text is not a substitution, and the prose value
|
|
905
837
|
// of a text option (--title, -m...) is not a path.
|
|
906
838
|
if (/\\$\\(|\`/.test(stripInertQuotes(cmd)) || quotedPathWithSpaces(stripTextOptionValues(cmd))) {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
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 (o KJ_ALLOW_CROSS_LANE=1, queda registrado)." + doc("cross-lane"));
|
|
910
|
-
process.exit(2);
|
|
911
|
-
}
|
|
839
|
+
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"));
|
|
840
|
+
process.exit(2);
|
|
912
841
|
}
|
|
913
842
|
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]*$/;
|
|
914
843
|
const segs = cmd.split(/&&|\\|\\||[;|\\n]/).map((s) => s.trim()).filter(Boolean);
|
|
915
844
|
if (!segs.every((s) => !/[$]/.test(s) || SAFE_EXP_SEG.test(s))) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
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"));
|
|
919
|
-
process.exit(2);
|
|
920
|
-
}
|
|
845
|
+
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"));
|
|
846
|
+
process.exit(2);
|
|
921
847
|
}
|
|
922
848
|
// ...incluidas relativas A PELO (los carriles kj viven en
|
|
923
849
|
// .kj/worktrees/). Un falso token (s/a/b/) resuelve al arbol propio
|
|
@@ -955,8 +881,8 @@ process.stdin.on("end", () => {
|
|
|
955
881
|
// Fail CLOSED por defecto (catches de codex; doctrina KJC-BUG-0095: un
|
|
956
882
|
// gate no se cae en silencio): solo exit 0 permite. kj inejecutable =
|
|
957
883
|
// deny duro (el remedio es restaurar kj); cualquier otro fallo de
|
|
958
|
-
// evaluacion = deny
|
|
959
|
-
//
|
|
884
|
+
// evaluacion = deny sin escape (ADR 0015): un typo de YAML lo corrige el
|
|
885
|
+
// usuario fuera de la sesion.
|
|
960
886
|
if ((EDIT_TOOLS.includes(tool) || tool === "Bash") && existsSync(resolve(ROOT, ".karajan", "policy.yml"))) {
|
|
961
887
|
// PL-C: el rol que ACTÚA (KJ_POLICY_ROLE, sembrado por los runners de
|
|
962
888
|
// kj run en los subprocesos) — el anfitrión-brain evalúa como coder.
|
|
@@ -970,11 +896,9 @@ process.stdin.on("end", () => {
|
|
|
970
896
|
let v = null;
|
|
971
897
|
try { v = JSON.parse(String(pres.stdout || "").trim().split("\\n").pop()); } catch { /* mensaje generico */ }
|
|
972
898
|
const secure = !!(v && v.class === "security");
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
process.exit(2);
|
|
977
|
-
}
|
|
899
|
+
// ADR 0015 (KJC-TSK-0932): no escape; a rule that misfires is fixed by PR.
|
|
900
|
+
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"));
|
|
901
|
+
process.exit(2);
|
|
978
902
|
} else if (pres.status !== 0) {
|
|
979
903
|
// KJC-BUG-0207: un kj que NO ARRANCA no es una violacion de policy. Con
|
|
980
904
|
// kj linkado al arbol, un error de sintaxis transitorio en src/ dejaba
|
|
@@ -999,9 +923,8 @@ process.stdin.on("end", () => {
|
|
|
999
923
|
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"));
|
|
1000
924
|
process.exit(2);
|
|
1001
925
|
}
|
|
1002
|
-
} else
|
|
1003
|
-
|
|
1004
|
-
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"));
|
|
926
|
+
} else {
|
|
927
|
+
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"));
|
|
1005
928
|
process.exit(2);
|
|
1006
929
|
}
|
|
1007
930
|
}
|
|
@@ -1022,11 +945,8 @@ process.stdin.on("end", () => {
|
|
|
1022
945
|
let blockOn = false;
|
|
1023
946
|
try { blockOn = /steward:\\s*["']?block/.test(readFileSync(join(ROOT, ".karajan", "kj.config.yml"), "utf8")); } catch { /* no config — no block */ }
|
|
1024
947
|
if (hard.length && blockOn) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
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"));
|
|
1028
|
-
process.exit(2);
|
|
1029
|
-
}
|
|
948
|
+
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"));
|
|
949
|
+
process.exit(2);
|
|
1030
950
|
}
|
|
1031
951
|
const st = load();
|
|
1032
952
|
const s = session(st, sid);
|
|
@@ -1043,11 +963,11 @@ process.stdin.on("end", () => {
|
|
|
1043
963
|
if (rel && CODE.test(rel) && !TESTS.test(rel)) {
|
|
1044
964
|
const branch = branchOf();
|
|
1045
965
|
const why = !branch ? null : BASE_BRANCHES.has(branch) ? "base" : !CARD.test(branch) ? "nocard" : null;
|
|
966
|
+
// ADR 0015 (KJC-TSK-0925): no escape. Without a card there is no work.
|
|
1046
967
|
if (why) {
|
|
1047
|
-
if (escOn("KJ_ALLOW_NO_CARD")) { recordEscape(sid, "KJ_ALLOW_NO_CARD", tool); process.exit(0); }
|
|
1048
968
|
console.error(why === "base"
|
|
1049
|
-
? "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.
|
|
1050
|
-
: "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.
|
|
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." + doc("card-first")
|
|
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." + doc("card-first"));
|
|
1051
971
|
process.exit(2);
|
|
1052
972
|
}
|
|
1053
973
|
// KJC-TSK-0848 (ADR 0010, RAG-B): the RAG must have answered about this
|
|
@@ -1070,15 +990,14 @@ process.stdin.on("end", () => {
|
|
|
1070
990
|
// KJC-BUG-0190: with nothing indexed, no query can cover anything —
|
|
1071
991
|
// the gate asks for kj rag index instead of denying the impossible.
|
|
1072
992
|
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;
|
|
993
|
+
// ADR 0015 (KJC-TSK-0925): no escape. The cases it covered (new file, empty
|
|
994
|
+
// index, not indexable) are decided by the gate itself.
|
|
1073
995
|
if (!covered) {
|
|
1074
|
-
|
|
1075
|
-
// Recorded ONCE per session: the escape is a conscious exception, not a per-edit tax.
|
|
1076
|
-
if (!(rs.escapes || []).includes("KJ_ALLOW_NO_RAG")) recordEscape(sid, "KJ_ALLOW_NO_RAG", tool);
|
|
1077
|
-
} else {
|
|
996
|
+
{
|
|
1078
997
|
// KJC-BUG-0216 (issue #1807): un .astro, un .php o cualquier fichero
|
|
1079
998
|
// sin adapter NO puede estar en el indice, asi que exigir una
|
|
1080
999
|
// respuesta del RAG sobre el es pedir lo imposible, y empujaba a
|
|
1081
|
-
// dejar
|
|
1000
|
+
// dejar el escape puesto para siempre. La exencion es SOLO
|
|
1082
1001
|
// esa: un indice vacio o por detras si tiene arreglo (kj rag index)
|
|
1083
1002
|
// y sigue bloqueando, porque si no el gate desapareceria en
|
|
1084
1003
|
// cualquier proyecto sin indexar. Nada legible = se bloquea.
|
|
@@ -1098,7 +1017,7 @@ process.stdin.on("end", () => {
|
|
|
1098
1017
|
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"));
|
|
1099
1018
|
process.exit(2);
|
|
1100
1019
|
} else {
|
|
1101
|
-
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>.
|
|
1020
|
+
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"));
|
|
1102
1021
|
process.exit(2);
|
|
1103
1022
|
}
|
|
1104
1023
|
}
|
|
@@ -1133,11 +1052,10 @@ process.stdin.on("end", () => {
|
|
|
1133
1052
|
// depender de como parta las palabras el separador (catch de la review).
|
|
1134
1053
|
const movesHooks = cmd.toLowerCase().includes("core.hookspath");
|
|
1135
1054
|
if ((inOrder(["git", "commit"], words) && words.some(skipsVerify)) || movesHooks) {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1055
|
+
// ADR 0015 (KJC-TSK-0926): no escape. A broken hook is fixed; skipping it,
|
|
1056
|
+
// if it ever must happen, is the user's act in their own terminal.
|
|
1057
|
+
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"));
|
|
1058
|
+
process.exit(2);
|
|
1141
1059
|
}
|
|
1142
1060
|
const MERGE = /\\bgh\\s+pr\\s+merge(\\s+(\\d+))?/;
|
|
1143
1061
|
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)
|
|
@@ -1145,20 +1063,14 @@ process.stdin.on("end", () => {
|
|
|
1145
1063
|
const pend = pendingMoves(load().sessions?.[sid]);
|
|
1146
1064
|
if (pend.length > 0 && (mergeM || ADVANCE.test(cmd))) {
|
|
1147
1065
|
// KJC-BUG-0198: una card partida en varias PRs no puede quedar bloqueada
|
|
1148
|
-
// por su primera mitad
|
|
1149
|
-
|
|
1150
|
-
const bg = spawnSync("kj", ["sentinel", "board-gate", "--session", sid, "--json"], { cwd: ROOT, encoding: "utf8" });
|
|
1151
|
-
let blocking = pend, carried = [];
|
|
1152
|
-
if (!bg.error && (bg.status === 0 || bg.status === 2)) {
|
|
1153
|
-
try { const d = JSON.parse(bg.stdout); blocking = d.blocking || []; carried = d.carried || []; } catch { /* sin respuesta legible: como siempre */ }
|
|
1154
|
-
}
|
|
1066
|
+
// por su primera mitad (boardGate, en la lib).
|
|
1067
|
+
const { blocking, carried } = boardGate(sid, pend);
|
|
1155
1068
|
for (const c of carried) console.error("karajan sentinel: board-sync arrastra " + c.card + " — " + c.why + ", se movera al cerrarla");
|
|
1069
|
+
// ADR 0015 (KJC-TSK-0925): no escape; an epic and a card split across PRs
|
|
1070
|
+
// are already decided above (KJC-BUG-0230, 0198).
|
|
1156
1071
|
if (blocking.length > 0) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
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"));
|
|
1160
|
-
process.exit(2);
|
|
1161
|
-
}
|
|
1072
|
+
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"));
|
|
1073
|
+
process.exit(2);
|
|
1162
1074
|
}
|
|
1163
1075
|
}
|
|
1164
1076
|
// (the pending entry itself is recorded by the PostToolUse hook, only once
|
|
@@ -1167,9 +1079,9 @@ process.stdin.on("end", () => {
|
|
|
1167
1079
|
// there misleads every future reader. When the command writes one from a
|
|
1168
1080
|
// file, kj crosses ITS data against this turn's outputs; the hook carries
|
|
1169
1081
|
// no policy (kj reads method_gates.claims; off = this check does not exist).
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1082
|
+
// KJC-BUG-0245: the path is read by the shell reader, so "pr.md;" is "pr.md".
|
|
1083
|
+
const body = /\\bgh\\s+pr\\s+(?:create|edit)\\b/.test(cmd) ? optionValues(cmd, ["--body-file"])[0] : undefined;
|
|
1084
|
+
if (body && transcript) {
|
|
1173
1085
|
const g = spawnSync("kj", ["claims", "gate", "--transcript", transcript, "--file", body], { cwd: ROOT, encoding: "utf8" });
|
|
1174
1086
|
if (!g.error && g.status === 2) {
|
|
1175
1087
|
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"));
|
|
@@ -1177,7 +1089,8 @@ process.stdin.on("end", () => {
|
|
|
1177
1089
|
}
|
|
1178
1090
|
}
|
|
1179
1091
|
if (isPublish(cmd)) {
|
|
1180
|
-
|
|
1092
|
+
// ADR 0015 (KJC-TSK-0932): no escape — KJC-BUG-0204 and 0259 keep the
|
|
1093
|
+
// check from blocking its own fix.
|
|
1181
1094
|
// KJC-BUG-0204: el check se evalua PARA este comando. Un item que
|
|
1182
1095
|
// declara remedied_by no bloquea el comando que lo repara (desplegar
|
|
1183
1096
|
// la landing era justo lo que el check pedia); publicar no se exime.
|
|
@@ -1194,19 +1107,15 @@ process.stdin.on("end", () => {
|
|
|
1194
1107
|
if (res.status !== 0) {
|
|
1195
1108
|
let items = "";
|
|
1196
1109
|
try { items = (parsed.checks || []).filter((c) => !c.ok && c.lifted !== true).map((c) => "\\n- " + c.name + ": " + c.detail).join(""); } catch { /* raw output */ }
|
|
1197
|
-
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(
|
|
1110
|
+
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"));
|
|
1198
1111
|
process.exit(2);
|
|
1199
1112
|
}
|
|
1200
1113
|
} else if (PUSH.test(cmd)) {
|
|
1201
|
-
// KJC-BUG-0147:
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1114
|
+
// KJC-BUG-0147: a pending move blocks the push like it blocks the commit.
|
|
1115
|
+
// ADR 0015 (KJC-TSK-0925): with no escape.
|
|
1116
|
+
// KJC-BUG-0262: a carried card does not block the push the commit let through.
|
|
1204
1117
|
const sess = load().sessions?.[sid];
|
|
1205
|
-
|
|
1206
|
-
if (v.length && pendingMoves(sess).length && escOn("KJ_ALLOW_BOARD")) {
|
|
1207
|
-
recordEscape(sid, "KJ_ALLOW_BOARD", tool);
|
|
1208
|
-
v = violations({ ...sess, pending_moves: [] }, branchOf());
|
|
1209
|
-
}
|
|
1118
|
+
const v = violations(sess, branchOf(), boardGate(sid, pendingMoves(sess)).blocking);
|
|
1210
1119
|
if (v.length) {
|
|
1211
1120
|
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"));
|
|
1212
1121
|
process.exit(2);
|
|
@@ -1231,6 +1140,9 @@ const SCRIPT_BODIES = {
|
|
|
1231
1140
|
"sentinel-bash-write.mjs": readFileSync(new URL("./sentinel/sentinel-bash-write.mjs", import.meta.url), "utf8"),
|
|
1232
1141
|
"sentinel-reminders.mjs": readFileSync(new URL("./sentinel/sentinel-reminders.mjs", import.meta.url), "utf8"),
|
|
1233
1142
|
"sessionstart.mjs": readFileSync(new URL("./sentinel/sessionstart.mjs", import.meta.url), "utf8"),
|
|
1143
|
+
// KJC-TSK-0946 (ADR 0016): the rules gate and its hook, for every tool call.
|
|
1144
|
+
"sentinel-rules.mjs": readFileSync(new URL("./sentinel/sentinel-rules.mjs", import.meta.url), "utf8"),
|
|
1145
|
+
"pretooluse-rules.mjs": readFileSync(new URL("./sentinel/pretooluse-rules.mjs", import.meta.url), "utf8"),
|
|
1234
1146
|
};
|
|
1235
1147
|
|
|
1236
1148
|
/**
|
|
@@ -1420,6 +1332,9 @@ export function installSentinelHooks({ projectDir = process.cwd(), logger = cons
|
|
|
1420
1332
|
entries: [
|
|
1421
1333
|
{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit", script: "pretooluse-sentinel.mjs" },
|
|
1422
1334
|
{ event: "PreToolUse", matcher: "Bash", script: "pretooluse-sentinel.mjs" },
|
|
1335
|
+
// KJC-TSK-0946 (ADR 0016): no matcher. The rules of the MD files are checked
|
|
1336
|
+
// before EVERY tool call, MCP ones included.
|
|
1337
|
+
{ event: "PreToolUse", script: "pretooluse-rules.mjs" },
|
|
1423
1338
|
{ event: "PostToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit", script: "posttooluse.mjs" },
|
|
1424
1339
|
// KJC-TSK-0765 board-sync: the merge (Bash) and the tracker move (MCP
|
|
1425
1340
|
// update_card) are observed AFTER the fact — found live: with only the
|
|
@@ -1433,6 +1348,8 @@ export function installSentinelHooks({ projectDir = process.cwd(), logger = cons
|
|
|
1433
1348
|
// KJC-TSK-0918 (ADR 0014): the critical rules back after a compaction or a resume.
|
|
1434
1349
|
{ event: "SessionStart", matcher: "compact", script: "sessionstart.mjs" },
|
|
1435
1350
|
{ event: "SessionStart", matcher: "resume", script: "sessionstart.mjs" },
|
|
1351
|
+
// KJC-TSK-0949: a new session hears of MD rules with no gate, and nothing else.
|
|
1352
|
+
{ event: "SessionStart", matcher: "startup", script: "sessionstart.mjs" },
|
|
1436
1353
|
],
|
|
1437
1354
|
});
|
|
1438
1355
|
return { scripts: [lib, post, stop, pre], wired, deferred };
|