karajan-code 4.65.0 → 4.66.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.
@@ -61,7 +61,10 @@ const cut = (text, limit) => {
61
61
  */
62
62
  export function describeSupervisorChanges({ projectDir, files, gitFn = defaultGit(projectDir), scan = scanText }) {
63
63
  let budget = DIFF_LIMIT_TOTAL;
64
- return files.map(({ file, deleted }) => {
64
+ return files.map(({ file, deleted, local }) => {
65
+ // KJC-TSK-1207: a machine-local file (ignored host settings) is sealed by its hash
66
+ // alone; its content never leaves the machine, whatever the scan would say of it.
67
+ if (local === true) return { file, status: "local", added: 0, removed: 0, summary: "machine-local, sealed by its hash", diff: "[content not sent: machine-local file, sealed by its hash only]", truncated: false };
65
68
  const tracked = inHead(gitFn, file);
66
69
  const present = !deleted && existsSync(join(projectDir, file));
67
70
  let status = "modified";
@@ -30,6 +30,15 @@ const HARNESS_PREFIX = ".karajan/harness/";
30
30
  // with the repo is sealed with the supervisor, so a policy change enters only
31
31
  // with the human's signed act. kj.config.yml is machine-local: CI never sees it.
32
32
  export const GOVERNANCE_FILES = [".karajan/policy.yml", ".karajan/rules.yml"];
33
+ // KJC-BUG-0400: the CI workflows are sealed the same way, by hash: a PR could
34
+ // otherwise rewrite the check that judges it and pass with a forged seal.
35
+ const WORKFLOW_FILE = /^\.github\/workflows\/[^/]+\.ya?ml$/;
36
+ // KJC-TSK-1207 (ADR 0030): with the guards run from the installed kj, the host
37
+ // settings decide which guard runs. Committed where the project versions them;
38
+ // where it ignores .claude/ (machine-local, like the guards) only their hash travels.
39
+ export const HOST_SETTINGS = ".claude/settings.json";
40
+ /** A file the seal covers by its hash alone: the governance config, every CI workflow and the host settings. */
41
+ export const isSealedConfig = (file) => GOVERNANCE_FILES.includes(file) || WORKFLOW_FILE.test(file) || file === HOST_SETTINGS;
33
42
 
34
43
  const sha256 = (abs) => createHash("sha256").update(readFileSync(abs)).digest("hex");
35
44
 
@@ -83,7 +92,7 @@ export async function commitSupervisorRegeneration({
83
92
  // KJC-BUG-0244 (grebla #958): el commit sellado solo lleva el supervisor, pero
84
93
  // un stage ajeno se quedaba en esta rama y el siguiente commit caía aquí con la
85
94
  // card equivocada. Se para ANTES de pedir nada al humano y se nombra.
86
- const foreign = run(["diff", "--cached", "--name-only"]).split("\n").filter((f) => f && !f.startsWith(HOOKS_PREFIX) && f !== PROVENANCE_FILE && !GOVERNANCE_FILES.includes(f));
95
+ const foreign = run(["diff", "--cached", "--name-only"]).split("\n").filter((f) => f && !f.startsWith(HOOKS_PREFIX) && f !== PROVENANCE_FILE && !isSealedConfig(f));
87
96
  if (foreign.length > 0) {
88
97
  throw new Error(
89
98
  `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`,
@@ -92,8 +101,15 @@ export async function commitSupervisorRegeneration({
92
101
  // Capa 4 (test adversarial 6-sep: un huérfano a init con pty falso y
93
102
  // prompts a ciegas llegó hasta aquí): nonce aleatorio tecleado de vuelta.
94
103
  seal.confirm(deps.confirm);
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));
104
+ const tracked = new Set(run(["ls-files", "--", ...GOVERNANCE_FILES, HOST_SETTINGS, ".github/workflows"]).split("\n").filter(Boolean));
105
+ // Local only when git ignores it; untracked but not ignored, it is committed with the seal.
106
+ const gitIgnores = (file) => { try { run(["check-ignore", "-q", "--", file]); return true; } catch { return false; } };
107
+ const settingsHere = !tracked.has(HOST_SETTINGS) && existsSync(join(projectDir, HOST_SETTINGS));
108
+ const localSettings = settingsHere && gitIgnores(HOST_SETTINGS) ? [HOST_SETTINGS] : [];
109
+ const newSettings = settingsHere && localSettings.length === 0 ? [HOST_SETTINGS] : [];
110
+ const onDisk = (() => { try { return readdirSync(join(projectDir, ".github", "workflows")).map((f) => `.github/workflows/${f}`); } catch { return []; } })();
111
+ const candidates = [...new Set([...GOVERNANCE_FILES, ...onDisk, ...tracked, ...newSettings])].filter(isSealedConfig).sort();
112
+ const governance = candidates.filter((f) => existsSync(join(projectDir, f)) || tracked.has(f));
97
113
  const governanceDrift = governance.length === 0 ? [] : run(["status", "--porcelain", "--", ...governance]).split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
98
114
  const drift = [...supervisorDrift({ projectDir, gitFn: run }), ...governanceDrift];
99
115
  // La provenance describe SIEMPRE el estado COMPLETO del supervisor (cazado
@@ -103,6 +119,7 @@ export async function commitSupervisorRegeneration({
103
119
  ...readdirSync(join(projectDir, HOOKS_PREFIX)).map((f) => HOOKS_PREFIX + f),
104
120
  ...harnessGuards(projectDir),
105
121
  ...governance.filter((f) => existsSync(join(projectDir, f))),
122
+ ...localSettings,
106
123
  ].sort();
107
124
  // KJC-BUG-0197: un guardia BORRADO se anota como borrado, igual que un hook.
108
125
  // No hacía falta para desencadenar el re-sello (la lista encoge, así que la
@@ -117,7 +134,7 @@ export async function commitSupervisorRegeneration({
117
134
  .map((f) => f?.file)
118
135
  .filter((f) => typeof f === "string" && f.startsWith(HARNESS_PREFIX) && !existsSync(join(projectDir, f)));
119
136
  const hashed = [
120
- ...current.map((file) => ({ file, sha256: sha256(join(projectDir, file)) })),
137
+ ...current.map((file) => ({ file, sha256: sha256(join(projectDir, file)), ...(localSettings.includes(file) ? { local: true } : {}) })),
121
138
  ...drift.filter((f) => !existsSync(join(projectDir, f))).map((file) => ({ file, deleted: true })),
122
139
  ...goneGuards.map((file) => ({ file, deleted: true })),
123
140
  ];
@@ -67,6 +67,8 @@ export const WORKFLOWS = [
67
67
  // PL-C (KJC-TSK-0735) — tier C del ADR 0001: re-check merge-blocking en CI
68
68
  // (cubre el hook local manipulado). Solo se siembra con policy declarada;
69
69
  // npx PINEADO a la versión del kj que corrió harden — jamás @latest en CI.
70
+ // KJC-BUG-0399: the judge is never the PR's code. A repo that ships kj itself
71
+ // judges with the kj of the BASE branch, checked out apart; the PR is only data.
70
72
  export const policyWorkflowFor = (kjVersion, kjName = "karajan-code") => [
71
73
  "name: Policy",
72
74
  "on:",
@@ -87,7 +89,15 @@ export const policyWorkflowFor = (kjVersion, kjName = "karajan-code") => [
87
89
  " env:",
88
90
  " BASE_REF: ${{ github.base_ref }}",
89
91
  " run: |",
90
- ` if [ -f bin/kj.js ]; then npm ci --ignore-scripts && KJ="node bin/kj.js"; else KJ="npx --yes ${kjName}@${kjVersion}"; fi`,
92
+ ' JUDGE="$RUNNER_TEMP/kj-judge"',
93
+ ' git fetch --no-tags origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}"',
94
+ ' if git cat-file -e "origin/${BASE_REF}:bin/kj.js" 2>/dev/null; then',
95
+ ' git worktree add --detach "$JUDGE" "origin/${BASE_REF}"',
96
+ ' (cd "$JUDGE" && npm ci --ignore-scripts)',
97
+ ' KJ="node $JUDGE/bin/kj.js"',
98
+ " else",
99
+ ` KJ="npx --yes ${kjName}@${kjVersion}"`,
100
+ " fi",
91
101
  ' $KJ policy check --range "origin/${BASE_REF}...HEAD" --strict',
92
102
  ].join("\n");
93
103
 
@@ -4,15 +4,20 @@
4
4
  * rewritten from the issues every time kj reads them. It is a view: changing it is done
5
5
  * with kj hu move, never from the web, and the board refuses to edit it.
6
6
  */
7
- import { savePlan } from "../plan/plan-store.js";
7
+ import { rm } from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { plansDir, savePlan } from "../plan/plan-store.js";
8
10
 
9
- export const MIRROR_PLAN_ID = "plan_github-board";
11
+ // KJC-BUG-0397: the board reads plan-*.json only and groups a plan by its projectDir;
12
+ // the mirror was plan_github-board.json with no projectDir, so it was never shown.
13
+ export const MIRROR_PLAN_ID = "plan-github-board";
14
+ const LEGACY_MIRROR_FILE = "plan_github-board.json";
10
15
 
11
16
  /** The shared board as a plan the web board can show, marked read-only. */
12
17
  export async function writeTeamMirror(projectDir, rows, { repo }) {
13
18
  const now = new Date().toISOString();
14
19
  const plan = {
15
- version: 2, planId: MIRROR_PLAN_ID, name: "github", alias: "github",
20
+ version: 2, planId: MIRROR_PLAN_ID, name: "github", alias: "github", projectDir,
16
21
  task: `Shared HU Board: GitHub Issues of ${repo}`,
17
22
  status: "ready", readonly: "github", createdAt: now,
18
23
  hus: rows.map((r) => ({
@@ -22,5 +27,6 @@ export async function writeTeamMirror(projectDir, rows, { repo }) {
22
27
  })),
23
28
  };
24
29
  await savePlan(projectDir, plan);
30
+ await rm(path.join(plansDir(projectDir), LEGACY_MIRROR_FILE), { force: true });
25
31
  return plan;
26
32
  }
@@ -35,7 +35,8 @@ export const SUPERVISOR_DEFAULTS = [
35
35
  // KJC-TSK-1033 (owner's decision, 13-oct): a policy change is authorized where
36
36
  // it is USED (commit, CI, release), whatever wrote it: only the human's seal lifts it.
37
37
  id: "defaults.governance.write",
38
- pattern: /(^|\/)\.karajan\/(policy|rules)\.yml\b/,
38
+ // KJC-BUG-0400: the CI workflows too; a PR could rewrite the check that judges it.
39
+ pattern: /(^|\/)\.karajan\/(policy|rules)\.yml\b|(^|\/)\.github\/workflows\/[^/\s]+\.ya?ml\b/,
39
40
  message: "es configuración de gobierno: solo entra sellada por la persona (kj harden --commit, firmado desde su móvil)",
40
41
  },
41
42
  ];
@@ -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 { GOVERNANCE_FILES, PROVENANCE_FILE } from "../harden/supervisor-commit.js";
14
+ import { HOST_SETTINGS, isSealedConfig, PROVENANCE_FILE } from "../harden/supervisor-commit.js";
15
15
 
16
16
  const HOOKS_PREFIX = ".karajan/hooks/";
17
17
  const HARNESS_PREFIX = ".karajan/harness/";
@@ -69,11 +69,15 @@ export function verifiedSupervisorFiles({ projectDir, trackedOnly = false }) {
69
69
  if (typeof entry?.file !== "string") continue;
70
70
  // KJC-TSK-1032: the governance config has no canonical render; the sealed
71
71
  // hash, signed by the roster where there is one, is the proof.
72
- if (GOVERNANCE_FILES.includes(entry.file)) {
72
+ if (isSealedConfig(entry.file)) {
73
73
  const abs = resolve(projectDir, entry.file);
74
74
  let st = null;
75
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);
76
+ // KJC-TSK-1207: host settings sealed as ignored by git (local) live only where
77
+ // they were sealed; absent from this checkout they cannot be judged here (CI,
78
+ // another clone). Versioned ones travel with the repo, so absent they fail.
79
+ if (st === null && entry.local === true && entry.file === HOST_SETTINGS) unjudgeable.add(entry.file);
80
+ else if (entry.deleted === true ? st === null : st?.isFile() && sha256(readFileSync(abs)) === entry.sha256) ok.add(entry.file);
77
81
  continue;
78
82
  }
79
83
  const kind = ROOTS.find(([prefix]) => entry.file.startsWith(prefix));