karajan-code 4.50.0 → 4.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.50.0",
3
+ "version": "4.51.1",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -29,7 +29,7 @@ async function withoutSetupInstalls(projectDir, setup, violations) {
29
29
  const installed = new Set(added.exitCode === 0 ? added.stdout.split("\n").map((s) => s.trim()).filter(Boolean) : []);
30
30
  const kept = [];
31
31
  for (const v of violations) {
32
- if (v.rule_id === "defaults.supervisor.write" && installed.has(v.file)) {
32
+ if ((v.rule_id === "defaults.supervisor.write" || v.rule_id === "defaults.governance.write") && installed.has(v.file)) {
33
33
  const changed = await runCommand("git", ["-C", projectDir, "diff", "--quiet", setup, "HEAD", "--", v.file]);
34
34
  if (changed.exitCode === 0) continue;
35
35
  }
@@ -26,6 +26,10 @@ const HOOKS_PREFIX = ".karajan/hooks/";
26
26
  // veredictos son locales por diseño); lo que viaja y se firma es su HUELLA,
27
27
  // dentro de una procedencia que sí está trackeada.
28
28
  const HARNESS_PREFIX = ".karajan/harness/";
29
+ // KJC-TSK-1032 (owner's decision, 13-oct): the governance config that travels
30
+ // with the repo is sealed with the supervisor, so a policy change enters only
31
+ // with the human's signed act. kj.config.yml is machine-local: CI never sees it.
32
+ export const GOVERNANCE_FILES = [".karajan/policy.yml", ".karajan/rules.yml"];
29
33
 
30
34
  const sha256 = (abs) => createHash("sha256").update(readFileSync(abs)).digest("hex");
31
35
 
@@ -79,7 +83,7 @@ export async function commitSupervisorRegeneration({
79
83
  // KJC-BUG-0244 (grebla #958): el commit sellado solo lleva el supervisor, pero
80
84
  // un stage ajeno se quedaba en esta rama y el siguiente commit caía aquí con la
81
85
  // card equivocada. Se para ANTES de pedir nada al humano y se nombra.
82
- const foreign = run(["diff", "--cached", "--name-only"]).split("\n").filter((f) => f && !f.startsWith(HOOKS_PREFIX) && f !== PROVENANCE_FILE);
86
+ const foreign = run(["diff", "--cached", "--name-only"]).split("\n").filter((f) => f && !f.startsWith(HOOKS_PREFIX) && f !== PROVENANCE_FILE && !GOVERNANCE_FILES.includes(f));
83
87
  if (foreign.length > 0) {
84
88
  throw new Error(
85
89
  `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`,
@@ -88,13 +92,17 @@ export async function commitSupervisorRegeneration({
88
92
  // Capa 4 (test adversarial 6-sep: un huérfano a init con pty falso y
89
93
  // prompts a ciegas llegó hasta aquí): nonce aleatorio tecleado de vuelta.
90
94
  seal.confirm(deps.confirm);
91
- const drift = supervisorDrift({ projectDir, gitFn: run });
95
+ const tracked = new Set(run(["ls-files", "--", ...GOVERNANCE_FILES]).split("\n").filter(Boolean));
96
+ const governance = GOVERNANCE_FILES.filter((f) => existsSync(join(projectDir, f)) || tracked.has(f));
97
+ const governanceDrift = governance.length === 0 ? [] : run(["status", "--porcelain", "--", ...governance]).split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
98
+ const drift = [...supervisorDrift({ projectDir, gitFn: run }), ...governanceDrift];
92
99
  // La provenance describe SIEMPRE el estado COMPLETO del supervisor (cazado
93
100
  // en el primer estreno real: un sello parcial pisaba al anterior y dejaba
94
101
  // ficheros sin respaldo). Borrados del drift ⇒ deleted (catch de codex).
95
102
  const current = [
96
103
  ...readdirSync(join(projectDir, HOOKS_PREFIX)).map((f) => HOOKS_PREFIX + f),
97
104
  ...harnessGuards(projectDir),
105
+ ...governance.filter((f) => existsSync(join(projectDir, f))),
98
106
  ].sort();
99
107
  // KJC-BUG-0197: un guardia BORRADO se anota como borrado, igual que un hook.
100
108
  // No hacía falta para desencadenar el re-sello (la lista encoge, así que la
@@ -159,7 +167,7 @@ export async function commitSupervisorRegeneration({
159
167
  // Por DIRECTORIO, no por fichero: un rename ya staged deja la ruta vieja
160
168
  // sin existir y `git add -- <vieja>` falla; `-A` sobre el dir del
161
169
  // supervisor versiona altas, cambios, borrados y renombrados por igual.
162
- run(["add", "-A", "--", PROVENANCE_FILE, ".karajan/hooks"]);
170
+ run(["add", "-A", "--", PROVENANCE_FILE, ".karajan/hooks", ...governance]);
163
171
  run([
164
172
  "commit",
165
173
  "--no-verify",
@@ -168,6 +176,7 @@ export async function commitSupervisorRegeneration({
168
176
  "--",
169
177
  PROVENANCE_FILE,
170
178
  ".karajan/hooks",
179
+ ...governance,
171
180
  ]);
172
181
  logger.info?.(`harden --commit: ${hashed.length} fichero(s) de supervisor versionados con procedencia sellada`);
173
182
  return { committed: true, files: hashed };
package/src/policy/add.js CHANGED
@@ -44,7 +44,15 @@ function mergeFragment(doc, frag) {
44
44
  }
45
45
  }
46
46
  }
47
- if (frag.invariants) out.invariants = [...(doc.invariants || []), ...frag.invariants];
47
+ // KJC-BUG-0355: a threshold on a metric already measured CHANGES that invariant
48
+ // (keeping its id): a second one beside it would leave the stricter ruling.
49
+ const invariants = [...(doc.invariants || [])];
50
+ for (const inv of frag.invariants || []) {
51
+ const same = invariants.findIndex((cur) => cur?.kind === inv?.kind && cur?.metric === inv?.metric);
52
+ if (same === -1) invariants.push(inv);
53
+ else invariants[same] = { ...invariants[same], ...inv, id: invariants[same].id };
54
+ }
55
+ if (frag.invariants) out.invariants = invariants;
48
56
  return out;
49
57
  }
50
58
 
@@ -31,6 +31,13 @@ export const SUPERVISOR_DEFAULTS = [
31
31
  pattern: /\.claude\/settings\.json\b|\.karajan\/(hooks|harness)\/|\.karajan\/supervisor-provenance\.json\b/,
32
32
  message: "nombra ficheros del supervisor — solo el humano los modifica, fuera de la sesión",
33
33
  },
34
+ {
35
+ // KJC-TSK-1033 (owner's decision, 13-oct): a policy change is authorized where
36
+ // it is USED (commit, CI, release), whatever wrote it: only the human's seal lifts it.
37
+ id: "defaults.governance.write",
38
+ pattern: /(^|\/)\.karajan\/(policy|rules)\.yml\b/,
39
+ message: "es configuración de gobierno: solo entra sellada por la persona (kj harden --commit, firmado desde su móvil)",
40
+ },
34
41
  ];
35
42
 
36
43
  // Tools del harness: mapeo acción → capability del kernel. Registro cerrado
@@ -11,7 +11,7 @@ import { resolve, sep } from "node:path";
11
11
  import { renderCanonicalHook } from "../harden/harden-engine.js";
12
12
  import { provenanceSignature } from "../harden/phone-sign.js";
13
13
  import { canonicalHarnessBody } from "../harden/sentinel-hooks.js";
14
- import { PROVENANCE_FILE } from "../harden/supervisor-commit.js";
14
+ import { GOVERNANCE_FILES, PROVENANCE_FILE } from "../harden/supervisor-commit.js";
15
15
 
16
16
  const HOOKS_PREFIX = ".karajan/hooks/";
17
17
  const HARNESS_PREFIX = ".karajan/harness/";
@@ -67,6 +67,15 @@ export function verifiedSupervisorFiles({ projectDir, trackedOnly = false }) {
67
67
  const ROOTS = [HOOKS_PREFIX, HARNESS_PREFIX].map((p) => [p, resolve(projectDir, p)]);
68
68
  for (const entry of entries) {
69
69
  if (typeof entry?.file !== "string") continue;
70
+ // KJC-TSK-1032: the governance config has no canonical render; the sealed
71
+ // hash, signed by the roster where there is one, is the proof.
72
+ if (GOVERNANCE_FILES.includes(entry.file)) {
73
+ const abs = resolve(projectDir, entry.file);
74
+ let st = null;
75
+ try { st = lstatSync(abs); } catch { /* absent */ }
76
+ if (entry.deleted === true ? st === null : st?.isFile() && sha256(readFileSync(abs)) === entry.sha256) ok.add(entry.file);
77
+ continue;
78
+ }
70
79
  const kind = ROOTS.find(([prefix]) => entry.file.startsWith(prefix));
71
80
  if (!kind) continue;
72
81
  const [prefix, root] = kind;
@@ -130,13 +139,14 @@ export function verifiedSupervisorFiles({ projectDir, trackedOnly = false }) {
130
139
  * mas laxo de lo que parece no pase por silencio (KJC-BUG-0212).
131
140
  */
132
141
  export function liftSealedSupervisorViolations({ projectDir, violations, trackedOnly = false }) {
133
- const RULE = "defaults.supervisor.write";
134
- if (!violations.some((v) => v.rule_id === RULE && v.file)) return { violations, lifted: 0 };
142
+ // KJC-TSK-1033: the governance config is lifted by the same seal.
143
+ const RULES = new Set(["defaults.supervisor.write", "defaults.governance.write"]);
144
+ if (!violations.some((v) => RULES.has(v.rule_id) && v.file)) return { violations, lifted: 0 };
135
145
  const sealed = verifiedSupervisorFiles({ projectDir, trackedOnly });
136
146
  // La violación sobre la PROPIA provenance solo se alza si la provenance
137
147
  // entera verificó — a medias no describe la verdad y sigue denegada.
138
148
  const liftable = (v) =>
139
- v.rule_id === RULE
149
+ RULES.has(v.rule_id)
140
150
  && v.file
141
151
  && (sealed.files.has(v.file) || (v.file === PROVENANCE_FILE && sealed.complete === true));
142
152
  const kept = violations.filter((v) => !liftable(v));