karajan-code 4.32.0 → 4.34.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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/agents/aider-agent.js +2 -12
  3. package/src/agents/base-agent.js +9 -20
  4. package/src/agents/claude-agent.js +2 -12
  5. package/src/agents/codex-agent.js +2 -12
  6. package/src/agents/dead-models.js +74 -0
  7. package/src/agents/gemini-agent.js +2 -12
  8. package/src/agents/model-errors.js +35 -0
  9. package/src/agents/opencode-agent.js +2 -12
  10. package/src/brain/agent-error-classifier.js +19 -1
  11. package/src/brain/one-shot-policy.js +26 -0
  12. package/src/brain/role-fallback-chain.js +100 -0
  13. package/src/brain/with-brain-recovery.js +63 -4
  14. package/src/checks/action-pins.js +131 -0
  15. package/src/checks/project-checks.js +6 -0
  16. package/src/checks/release-check.js +51 -5
  17. package/src/cli/register-meta.js +42 -2
  18. package/src/cli/register-pipeline.js +11 -1
  19. package/src/commands/check.js +5 -0
  20. package/src/commands/code.js +59 -4
  21. package/src/commands/env.js +2 -1
  22. package/src/commands/harden.js +12 -3
  23. package/src/commands/policy.js +21 -14
  24. package/src/commands/report.js +28 -0
  25. package/src/commands/review-gate.js +25 -15
  26. package/src/environment/panel.js +85 -0
  27. package/src/environment/playbook.js +7 -6
  28. package/src/harden/config-templates.js +7 -1
  29. package/src/harden/guidelines-engine.js +14 -4
  30. package/src/harden/guidelines-templates.js +69 -10
  31. package/src/harden/harness-hooks.js +6 -0
  32. package/src/harden/hook-templates.js +4 -1
  33. package/src/harden/sentinel-hooks.js +227 -24
  34. package/src/harden/supervisor-commit.js +41 -1
  35. package/src/harden/workflow-templates.js +9 -3
  36. package/src/policy/supervisor-verify.js +45 -11
  37. package/src/privacy/diff-scope.js +58 -0
  38. package/src/privacy/scan.js +18 -0
  39. package/src/prompts/card-context.js +57 -0
  40. package/src/prompts/session-context.js +68 -0
  41. package/src/review/board-pending.js +68 -0
  42. package/src/review/loc-budget.js +97 -0
  43. package/src/roles/agent-role.js +11 -2
  44. package/src/roles/audit-role.js +28 -2
@@ -22,6 +22,8 @@ import { checkCardFirst } from "../review/card-first.js";
22
22
  import { liftSealedSupervisorViolations } from "../policy/supervisor-verify.js";
23
23
  import { checkTestsWithCode } from "../review/tests-with-code.js";
24
24
  import { loadPrivacyList, scanText } from "../privacy/scan.js";
25
+ import { isGeneratedPath, splitAddedByFile } from "../privacy/diff-scope.js";
26
+ import { budgetedAdded, budgetedNet } from "../review/loc-budget.js";
25
27
  import { checkStagedDiff, loadPolicy } from "../policy/engine.js";
26
28
  import { loadStandingExceptions, recordPolicyException } from "../policy/exceptions.js";
27
29
  import { policyFileHash, recordGateDecision } from "../policy/decisions.js";
@@ -202,14 +204,17 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
202
204
  const sizeSource = locInv ? `policy [${locInv.id}]` : "method_gates";
203
205
  if (sizeWarn > 0) {
204
206
  const numstat = await rawDiff(flags.range, ["--numstat"]);
205
- const added = numstat.split("\n").reduce((acc, l) => acc + (Number(l.split("\t")[0]) || 0), 0);
207
+ // KJC-BUG-0205: the budget caps CODE growth, so generated output and human
208
+ // documentation are exempt — the same rule CI applies. Counting everything
209
+ // here warned 234 lines on a doc-only PR that CI counted at 104, and two
210
+ // implementations of one rule that disagree teach people to trust neither.
211
+ const { added, exempt, testAdded } = budgetedAdded(numstat);
212
+ // Said out loud: an exemption nobody sees is indistinguishable from a gate
213
+ // that is not looking.
214
+ if (exempt > 0) console.log(`ℓ pr-size: ${exempt} line(s) exempt from the budget (generated output and human docs; AI-rule files always count)`);
206
215
  if (added > sizeWarn) {
207
216
  // KJC-TSK-0795 AC5: say how much of the weight is the module's OWN tests —
208
217
  // partitioning is a decision, and it must never split code from its tests.
209
- const testAdded = numstat.split("\n").reduce((acc, l) => {
210
- const [a, , ...f] = l.split("\t");
211
- return acc + (/\/tests?\/|__tests__\/|\.test\.|\.spec\./.test(`/${f.join("\t")}`) ? Number(a) || 0 : 0);
212
- }, 0);
213
218
  const split = testAdded > 0 ? ` (${added - testAdded} source + ${testAdded} accompanying tests — partition by feature, never code from its tests)` : "";
214
219
  const sizePolicy = config?.method_gates?.pr_size || "warn";
215
220
  if (process.env.KJ_ALLOW_LARGE_PR === "1") {
@@ -230,17 +235,24 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
230
235
  // don't publish. In the SEA binary the privacy module is stubbed and
231
236
  // throws: the gate degrades with a note instead of crashing.
232
237
  try {
233
- const added = diff.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1)).join("\n");
234
- const findings = scanText(added, { list: loadPrivacyList(), source: "<staged diff>" });
238
+ // KJC-BUG-0203: per file, so a finding can name where it lives and so
239
+ // build output is judged as what it is. Generic heuristics are silenced
240
+ // there (nobody typed a minified bundle); a denylist hit is not, because
241
+ // the incident behind this scanner was personal data inside a build.
242
+ const list = loadPrivacyList();
243
+ const findings = splitAddedByFile(diff).flatMap(({ file, added }) => {
244
+ const found = scanText(added, { list, source: file });
245
+ return isGeneratedPath(file) ? found.filter((f) => f.severity === "block") : found;
246
+ });
235
247
  const blocks = findings.filter((f) => f.severity === "block");
236
248
  const warns = findings.filter((f) => f.severity === "warn");
237
- for (const f of warns) console.log(`⚠ privacy: [${f.type}] added line ${f.line} → ${f.masked} — personal data? move it out before it ships`);
249
+ for (const f of warns) console.log(`⚠ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked} — personal data? move it out before it ships`);
238
250
  const hardened = config?.privacy?.generic === "block" && warns.length > 0;
239
251
  if (blocks.length > 0 || hardened) {
240
252
  if (process.env.KJ_ALLOW_PII === "1") {
241
253
  console.log(`⚠ privacy exempt: ${blocks.length} denylist hit(s) — KJ_ALLOW_PII=1 (explicit escape hatch)`);
242
254
  } else {
243
- for (const f of blocks) console.log(`✗ privacy: [${f.type}] on added line ${f.line} → ${f.masked}`);
255
+ for (const f of blocks) console.log(`✗ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked}`);
244
256
  const reason = `${blocks.length || warns.length} personal-data finding(s) in the staged diff — this must not reach the repo (KJ_ALLOW_PII=1 to override consciously)`;
245
257
  console.log(`✗ privacy gate: ${reason}`);
246
258
  process.exitCode = 1;
@@ -281,12 +293,10 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
281
293
  // hooks. enforcement=warn avisa; deny cierra salvo excepción probatoria
282
294
  // (KJ_ALLOW_POLICY=1 + KJ_POLICY_REASON, registrada con identidad y hash
283
295
  // del diff); class=security cierra sin escape y sin arbitraje.
284
- let net = 0;
285
- for (const l of (await rawDiff(flags.range, ["--numstat"])).split("\n")) {
286
- const [a, r] = l.trim().split(/\s+/);
287
- if (a && a !== "-") net += Number(a) || 0;
288
- if (r && r !== "-") net -= Number(r) || 0;
289
- }
296
+ // KJC-BUG-0208: el mismo modulo que decide el presupuesto decide el metrico
297
+ // del invariante. Sumar aqui el numstat entero daba dos cifras para la misma
298
+ // regla en dos lineas seguidas del mismo comando.
299
+ const { net } = budgetedNet(await rawDiff(flags.range, ["--numstat"]));
290
300
  // GOV-B (KJC-TSK-0746): las permanentes concedidas (kj policy grant)
291
301
  // eximen su regla mientras viven; el descarte de líneas corruptas se dice.
292
302
  const std = loadStandingExceptions(projectDir);
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The project's panel, said out loud (KJC-TSK-0865).
3
+ *
4
+ * The user chooses who writes and who reviews. `kj run` honoured that choice
5
+ * because it launched the agents itself; since v4 the HOST agent orchestrates,
6
+ * and nothing carried the choice into the session, so the host wrote the code
7
+ * and the user found out by accident.
8
+ *
9
+ * The panel line travels with the playbook, which is what every host reads at
10
+ * session start. It ANNOUNCES, it never blocks: the host may still write the
11
+ * code, and then it says so instead of staying quiet.
12
+ */
13
+ import { resolveRole } from "../config/role-resolver.js";
14
+ import { policyFileHash, recordGateDecision } from "../policy/decisions.js";
15
+ import { readIdentity } from "../identity/store.js";
16
+
17
+ /** Roles worth announcing: who writes, who reviews, who arbitrates. */
18
+ export const PANEL_ROLES = ["coder", "reviewer", "solomon"];
19
+
20
+ /**
21
+ * @returns {{coder: string|null, reviewer: string|null, solomon: string|null}}
22
+ * the provider declared (or inherited) for each panel role.
23
+ */
24
+ export function resolvePanel(config) {
25
+ const panel = {};
26
+ for (const role of PANEL_ROLES) panel[role] = resolveRole(config, role).provider || null;
27
+ return panel;
28
+ }
29
+
30
+ /**
31
+ * One agent-facing line naming the panel, or null when the project declares
32
+ * no coder and no reviewer. A project without a panel gets no line at all:
33
+ * announcing "coder: nobody" would spend attention to say nothing.
34
+ */
35
+ export function panelLine(config) {
36
+ const { coder, reviewer, solomon } = resolvePanel(config);
37
+ if (!coder && !reviewer) return null;
38
+ const parts = [`coder=${coder || "unset"}`, `reviewer=${reviewer || "unset"}`];
39
+ if (solomon && solomon !== coder) parts.push(`solomon=${solomon}`);
40
+ return [
41
+ `- The panel is your user's, not yours: ${parts.join(", ")}.`,
42
+ " When the coder is not you, the writing is ITS job; write it yourself",
43
+ " anyway and say so in your closing message, never in silence.",
44
+ ].join("\n");
45
+ }
46
+
47
+ /**
48
+ * KJC-TSK-0868: the host writing the code while another coder is declared is
49
+ * not an offence to block, it is a fact to record. Announcing at session start
50
+ * is not enough: the user chose who writes and finds out it did not happen by
51
+ * accident, one turn at a time.
52
+ *
53
+ * @returns {{coder: string, host: string}|null} null when there is nothing to
54
+ * record: no coder declared, no host detected, or the host IS the coder.
55
+ */
56
+ export function panelDeviation(config, host) {
57
+ const { coder } = resolvePanel(config);
58
+ if (!coder || !host || coder === host) return null;
59
+ return { coder, host };
60
+ }
61
+
62
+ /**
63
+ * KJC-TSK-0873: the log holds both sides. A deviation alone cannot say whether
64
+ * the panel is honoured almost always or almost never, and those are different
65
+ * situations. The compliance seat is written where the declared coder is
66
+ * actually invoked (`kj code`); the deviation one when the turn ends.
67
+ */
68
+ export function sealPanel({ projectDir, coder, host = null, honoured = false }) {
69
+ const who = readIdentity(projectDir);
70
+ return recordGateDecision(projectDir, {
71
+ decision: honoured ? "ok" : "exempt", chokepoint: "panel", escape: null, tool: null,
72
+ coder, host: honoured ? coder : host,
73
+ who: who ? { gh: who.gh_user ?? null, git: who.git_email ?? null, grade: "declarada" } : null,
74
+ policy_hash: policyFileHash(projectDir),
75
+ });
76
+ }
77
+
78
+ /** Human-facing one-liner for `kj check` and friends. */
79
+ export function panelSummary(config) {
80
+ const { coder, reviewer, solomon } = resolvePanel(config);
81
+ if (!coder && !reviewer) return "panel: not declared (no coder, no reviewer)";
82
+ const parts = [`coder ${coder || "unset"}`, `reviewer ${reviewer || "unset"}`];
83
+ if (solomon && solomon !== coder) parts.push(`solomon ${solomon}`);
84
+ return `panel: ${parts.join(" · ")}`;
85
+ }
@@ -12,6 +12,7 @@
12
12
  import fs from "node:fs/promises";
13
13
  import path from "node:path";
14
14
  import { upsertManagedBlock } from "../utils/managed-markers.js";
15
+ import { panelLine } from "./panel.js";
15
16
 
16
17
  // AB-A (KJC-TSK-0650): any agent can be the brain. AGENTS.md is the
17
18
  // emerging standard (Codex, Cursor and most new CLIs read it); GEMINI.md
@@ -48,7 +49,7 @@ function trackingLine(stateBackend, boardName) {
48
49
  // AB-C2 (KJC-TSK-0653): outcome-first — invariants over step scripts.
49
50
  // Frontier models choose their own path best; what they need explicit are
50
51
  // the limits. The git gates enforce these regardless of what any brain does.
51
- const playbookBody = (stateBackend, boardName) => `# Karajan method (v4)
52
+ const playbookBody = (stateBackend, boardName, panel) => `# Karajan method (v4)
52
53
 
53
54
  You are the orchestrator; Karajan governs. A task is DONE when its
54
55
  done-statement is literally true, the full suite is green, and every commit
@@ -68,7 +69,7 @@ Invariants (the git gates enforce these — they are not suggestions):
68
69
  - Every diff is reviewed by a DIFFERENT AI before it is committed
69
70
  (\`kj review --staged\`): verdicts bind to the exact diff — change the code
70
71
  and it must be reviewed again. Disagree with a rejection? \`kj solomon\`.
71
- - Security findings are never overridable — not even by arbitration. You
72
+ ${panel ? `${panel}\n` : ""}- Security findings are never overridable — not even by arbitration. You
72
73
  absorb the security role: \`kj brief security\` states what must be true.
73
74
  Task touches auth, user input, secrets, network or deps? Run
74
75
  \`kj audit --security\` (zero tokens) and remediate BEFORE the review.
@@ -100,15 +101,15 @@ kj announces a newer version? Tell your user what it brings and ask —
100
101
  never run \`kj update\` on your own.
101
102
  `;
102
103
 
103
- export function renderPlaybook({ stateBackend = "hu-board", boardName = null } = {}) {
104
- return playbookBody(stateBackend, boardName);
104
+ export function renderPlaybook({ stateBackend = "hu-board", boardName = null, config = null } = {}) {
105
+ return playbookBody(stateBackend, boardName, panelLine(config));
105
106
  }
106
107
 
107
108
  /**
108
109
  * Install/refresh the playbook block in the target agent files.
109
110
  * User content outside the managed block is never touched.
110
111
  */
111
- export async function installPlaybook({ projectDir, target = "all", version = "1", stateBackend = "hu-board", boardName = null }) {
112
+ export async function installPlaybook({ projectDir, target = "all", version = "1", stateBackend = "hu-board", boardName = null, config = null }) {
112
113
  if (!PLAYBOOK_TARGETS.includes(target)) {
113
114
  throw new Error(`unknown target "${target}" — use one of: ${PLAYBOOK_TARGETS.join(", ")}`);
114
115
  }
@@ -118,7 +119,7 @@ export async function installPlaybook({ projectDir, target = "all", version = "1
118
119
  let source = "";
119
120
  try { source = await fs.readFile(fullPath, "utf8"); } catch { /* new file */ }
120
121
  const { content, action } = upsertManagedBlock({
121
- source, blockId: "playbook", version, body: renderPlaybook({ stateBackend, boardName }), style: "html",
122
+ source, blockId: "playbook", version, body: renderPlaybook({ stateBackend, boardName, config }), style: "html",
122
123
  note: "do not edit: regenerated by kj env install",
123
124
  });
124
125
  if (action !== "unchanged") await fs.writeFile(fullPath, content);
@@ -90,9 +90,14 @@ export const GOLANGCI_BODY = [
90
90
  ].join("\n");
91
91
 
92
92
  /** Language-agnostic configs (every stack gets these). */
93
+ // KJC-BUG-0201 (issue #1774, reported from the field): commitlint used to live
94
+ // here, so a Python repo needed a Node runtime just to validate commit
95
+ // messages. The generated commit-msg hook ALREADY enforces that contract in
96
+ // pure sh (Conventional Commits, the 100-char cap, the AI-attribution ban), so
97
+ // nothing is lost by scoping the tool to the stack that already has Node. The
98
+ // GUARANTEE is language-agnostic; the tool does not have to be.
93
99
  export const UNIVERSAL_CONFIGS = [
94
100
  { file: ".editorconfig", blockId: "editorconfig", style: "hash", body: EDITORCONFIG_BODY },
95
- { file: "commitlint.config.js", blockId: "commitlint", style: "slash", body: COMMITLINT_BODY },
96
101
  ];
97
102
 
98
103
  /**
@@ -102,6 +107,7 @@ export const UNIVERSAL_CONFIGS = [
102
107
  * tool is decorative, and kj never generates a demand it didn't satisfy.
103
108
  */
104
109
  export const JS_CONFIGS = [
110
+ { file: "commitlint.config.js", blockId: "commitlint", style: "slash", body: COMMITLINT_BODY },
105
111
  { file: "eslint.config.js", blockId: "eslint", style: "slash", body: ESLINT_BODY, requires: "eslint" },
106
112
  { file: ".prettierrc.json", json: true, body: PRETTIER_BODY, requires: "prettier" },
107
113
  ];
@@ -9,7 +9,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
 
11
11
  import { upsertManagedBlock } from "../utils/managed-markers.js";
12
- import { GUIDELINES_BODY } from "./guidelines-templates.js";
12
+ import { guidelinesBody } from "./guidelines-templates.js";
13
13
 
14
14
  const BLOCK_VERSION = 1;
15
15
  const TARGETS = ["AGENTS.md", "CLAUDE.md"];
@@ -28,7 +28,13 @@ export function stripDevHooksBlock(source) {
28
28
  return { content: `${before}${joiner}${after}`, migrated: true };
29
29
  }
30
30
 
31
- export function installGuidelines({ projectDir = process.cwd(), dryRun = false } = {}) {
31
+ const countLines = (text) => String(text || "").split("\n").filter((l) => l.trim()).length;
32
+
33
+ export function installGuidelines({ projectDir = process.cwd(), dryRun = false, language = null } = {}) {
34
+ // KJC-BUG-0199 (issue #1773): the rules that enter the agent's context must
35
+ // be the rules of THIS project's language. kj harden already detects it for
36
+ // the configs; the guidelines just never asked.
37
+ const body = guidelinesBody(language);
32
38
  const results = [];
33
39
  for (const file of TARGETS) {
34
40
  const target = join(projectDir, file);
@@ -38,11 +44,15 @@ export function installGuidelines({ projectDir = process.cwd(), dryRun = false }
38
44
  source,
39
45
  blockId: "guidelines",
40
46
  version: BLOCK_VERSION,
41
- body: GUIDELINES_BODY,
47
+ body,
42
48
  style: "html",
43
49
  });
50
+ // KJC-BUG-0206: harden removing rules is a fact worth saying. It silently
51
+ // dropped six lines from this repo's own files because the detected language
52
+ // had no block, and "updated" read like an improvement.
53
+ const shrank = countLines(content) < countLines(raw);
44
54
  if (!dryRun && (migrated || action !== "unchanged")) writeFileSync(target, content);
45
- results.push({ file, action: migrated ? "migrated" : action });
55
+ results.push({ file, action: migrated ? "migrated" : action, ...(shrank ? { shrank: true } : {}) });
46
56
  }
47
57
  return { dryRun, guidelines: results };
48
58
  }
@@ -7,14 +7,21 @@
7
7
  * bloat dilutes the signal. Distilled from the dev-toolkit guidelines.
8
8
  */
9
9
 
10
- export const GUIDELINES_BODY = [
10
+ // KJC-BUG-0199 (issue #1773, reported from the field): this used to be ONE
11
+ // fixed text, so a Python project was told to use `const`, target ES2025 and
12
+ // prefer ES modules over require. These rules enter the agent's context on
13
+ // EVERY run, so the noise is not harmless: it dilutes the signal and states
14
+ // things that do not apply. The configs were already language-aware (ruff for
15
+ // Python, golangci for Go); the guidelines had not caught up.
16
+ //
17
+ // Shape: a CORE that holds in any language, plus one block per language. A
18
+ // language kj does not know gets the core ALONE, never another language's
19
+ // rules, which is the failure this fixes.
20
+ const CORE = [
11
21
  "# Project guidelines (kj harden)",
12
22
  "",
13
23
  "## Code",
14
- "- SOLID, DRY, KISS, YAGNI. `const` by default; arrow callbacks; template literals.",
15
- "- ES2025 target: never `var`, `document.write`, `alert`/`confirm`/`prompt`, `escape`/`unescape`, `substr`.",
16
- " Prefer modern APIs (`structuredClone`, `Object.groupBy`, `Array.at`/`findLast`/`toSorted`, optional chaining, `??`).",
17
- "- ES modules (`import`/`export`), never `require` in new code. Names in English, descriptive.",
24
+ "- SOLID, DRY, KISS, YAGNI. Names in English, descriptive.",
18
25
  "- No silent fallbacks: the system works or fails loudly. Validate and sanitize all input.",
19
26
  "",
20
27
  "## Commits & PRs",
@@ -26,11 +33,63 @@ export const GUIDELINES_BODY = [
26
33
  "- Test-first. Run the tests after each meaningful change. Never skip tests.",
27
34
  "",
28
35
  "## Security",
29
- "- Never commit secrets, keys or tokens. Parameterized queries; sanitize output against XSS.",
30
- "",
31
- "## UI/UX",
32
- "- No native `alert`/`confirm`/`prompt` — use the app's modal system. Loading states; accessible; mobile-first.",
36
+ "- Never commit secrets, keys or tokens. Parameterized queries; sanitize output against injection.",
33
37
  "",
34
38
  "## Files",
35
39
  "- Edit existing files in place; never overwrite a whole file to make a small change.",
36
- ].join("\n");
40
+ ];
41
+
42
+ /** Per-language rules. A language absent from here gets the core alone. */
43
+ export const LANGUAGE_GUIDELINES = {
44
+ javascript: [
45
+ "",
46
+ "## JavaScript / TypeScript",
47
+ "- `const` by default; arrow callbacks; template literals.",
48
+ "- ES2025 target: never `var`, `document.write`, `alert`/`confirm`/`prompt`, `escape`/`unescape`, `substr`.",
49
+ " Prefer modern APIs (`structuredClone`, `Object.groupBy`, `Array.at`/`findLast`/`toSorted`, optional chaining, `??`).",
50
+ "- ES modules (`import`/`export`), never `require` in new code. Avoid `any` in TypeScript.",
51
+ "",
52
+ "## UI/UX",
53
+ "- No native `alert`/`confirm`/`prompt` — use the app's modal system. Loading states; accessible; mobile-first.",
54
+ ],
55
+ python: [
56
+ "",
57
+ "## Python",
58
+ "- PEP 8, and type hints on every public signature. Prefer `pathlib` over string paths.",
59
+ "- Never a bare `except:` — catch what you can handle and let the rest fail loudly.",
60
+ "- f-strings over concatenation or `%`. Comprehensions when they read better than a loop, not by default.",
61
+ "- Tooling and dependencies declared in `pyproject.toml`; no other language's runtime imposed on the project.",
62
+ ],
63
+ go: [
64
+ "",
65
+ "## Go",
66
+ "- `gofmt` is not negotiable. Errors are values: wrap with `%w` and handle them where the caller can decide.",
67
+ "- No naked returns in long functions; accept interfaces, return structs.",
68
+ "- Concurrency with a purpose: a goroutine with no way to stop it is a leak.",
69
+ ],
70
+ rust: [
71
+ "",
72
+ "## Rust",
73
+ "- `cargo fmt` and `cargo clippy` clean before a commit.",
74
+ "- No `unwrap()` outside tests: propagate with `?` and let the type say what can fail.",
75
+ "- `unsafe` needs a comment naming the invariant that makes it sound.",
76
+ ],
77
+ };
78
+
79
+ /**
80
+ * Languages that share another's block (KJC-BUG-0206). TypeScript is the same
81
+ * ecosystem as JavaScript, and the block already says so in its heading; but the
82
+ * stack detector reports `typescript` for a repo with a tsconfig, so the lookup
83
+ * missed and harden ERASED the rules a project already had.
84
+ */
85
+ const ALIASES = { typescript: "javascript" };
86
+
87
+ /** The guidelines body for a language (unknown or absent ⇒ the core alone). */
88
+ export function guidelinesBody(language = null) {
89
+ const key = String(language || "").toLowerCase();
90
+ const extra = LANGUAGE_GUIDELINES[ALIASES[key] ?? key] ?? [];
91
+ return [...CORE, ...extra].join("\n");
92
+ }
93
+
94
+ /** Back-compat for callers that predate the language split. */
95
+ export const GUIDELINES_BODY = guidelinesBody("javascript");
@@ -44,6 +44,12 @@ process.stdin.on("end", () => {
44
44
  `;
45
45
 
46
46
  /** Write a managed hook script under .karajan/harness/ and return its path. */
47
+ /**
48
+ * KJC-BUG-0211: el cuerpo canonico de los guardias que instala ESTE modulo,
49
+ * para que quien verifica un sello pueda RECOMPUTAR en vez de fiarse del hash.
50
+ */
51
+ export const HARNESS_BODIES = { "pretooluse.mjs": SCRIPT_BODY };
52
+
47
53
  export function writeHarnessScript(projectDir, name, body) {
48
54
  const dir = join(projectDir, ".karajan", "harness");
49
55
  mkdirSync(dir, { recursive: true });
@@ -126,7 +126,10 @@ export function hookBody(hook, cmds = {}, { globalHooksDir = null, baseBranch =
126
126
  "# KJC-BUG-0170: the generated supervisor hooks (.karajan/hooks/pre-commit,",
127
127
  "# commit-msg) also CONTAIN the pattern by design — the bootstrap commit",
128
128
  "# that versions them must not self-detect in the consumer repo.",
129
- `if git diff --cached -- . ':(exclude).github/workflows/kj-no-ai-attribution.yml' ':(exclude)src/harden/hook-templates.js' ':(exclude)src/harden/workflow-templates.js' ':(exclude)src/harden/sentinel-hooks.js' ':(exclude)scripts/ai-attribution-guard.yml' ':(exclude)tests/harden/attribution-guard.test.js' ':(exclude)tests/harden/sentinel-hooks.test.js' ':(exclude).karajan/hooks/pre-commit' ':(exclude).karajan/hooks/commit-msg' | grep '^+' | grep -qiE '${AI_ATTRIBUTION}|generated with \\[?claude'; then`,
129
+ "# KJC-BUG-0200 (issue #1772): the same is true of .karajan/harness — the",
130
+ "# whole DIRECTORY is excluded, not one file, so a supervisor script added",
131
+ "# later cannot bring the bug back.",
132
+ `if git diff --cached -- . ':(exclude).github/workflows/kj-no-ai-attribution.yml' ':(exclude)src/harden/hook-templates.js' ':(exclude)src/harden/workflow-templates.js' ':(exclude)src/harden/sentinel-hooks.js' ':(exclude)scripts/ai-attribution-guard.yml' ':(exclude)tests/harden/attribution-guard.test.js' ':(exclude)tests/harden/sentinel-hooks.test.js' ':(exclude).karajan/hooks/pre-commit' ':(exclude).karajan/hooks/commit-msg' ':(exclude).karajan/harness/' | grep '^+' | grep -qiE '${AI_ATTRIBUTION}|generated with \\[?claude'; then`,
130
133
  " echo 'kj harden: AI attribution is not allowed in committed content'; exit 1",
131
134
  "fi",
132
135
  "# v4 review gate (ENV-C1, opt-in via `kj review --install-gate`):",