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.
- package/package.json +1 -1
- package/src/agents/aider-agent.js +2 -12
- package/src/agents/base-agent.js +9 -20
- package/src/agents/claude-agent.js +2 -12
- package/src/agents/codex-agent.js +2 -12
- package/src/agents/dead-models.js +74 -0
- package/src/agents/gemini-agent.js +2 -12
- package/src/agents/model-errors.js +35 -0
- package/src/agents/opencode-agent.js +2 -12
- package/src/brain/agent-error-classifier.js +19 -1
- package/src/brain/one-shot-policy.js +26 -0
- package/src/brain/role-fallback-chain.js +100 -0
- package/src/brain/with-brain-recovery.js +63 -4
- package/src/checks/action-pins.js +131 -0
- package/src/checks/project-checks.js +6 -0
- package/src/checks/release-check.js +51 -5
- package/src/cli/register-meta.js +42 -2
- package/src/cli/register-pipeline.js +11 -1
- package/src/commands/check.js +5 -0
- package/src/commands/code.js +59 -4
- package/src/commands/env.js +2 -1
- package/src/commands/harden.js +12 -3
- package/src/commands/policy.js +21 -14
- package/src/commands/report.js +28 -0
- package/src/commands/review-gate.js +25 -15
- package/src/environment/panel.js +85 -0
- package/src/environment/playbook.js +7 -6
- package/src/harden/config-templates.js +7 -1
- package/src/harden/guidelines-engine.js +14 -4
- package/src/harden/guidelines-templates.js +69 -10
- package/src/harden/harness-hooks.js +6 -0
- package/src/harden/hook-templates.js +4 -1
- package/src/harden/sentinel-hooks.js +227 -24
- package/src/harden/supervisor-commit.js +41 -1
- package/src/harden/workflow-templates.js +9 -3
- package/src/policy/supervisor-verify.js +45 -11
- package/src/privacy/diff-scope.js +58 -0
- package/src/privacy/scan.js +18 -0
- package/src/prompts/card-context.js +57 -0
- package/src/prompts/session-context.js +68 -0
- package/src/review/board-pending.js +68 -0
- package/src/review/loc-budget.js +97 -0
- package/src/roles/agent-role.js +11 -2
- 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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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}]
|
|
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}]
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
]
|
|
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
|
-
|
|
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`):",
|