karajan-code 4.31.1 → 4.33.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/role-fallback-chain.js +100 -0
- package/src/brain/with-brain-recovery.js +59 -3
- package/src/checks/action-pins.js +131 -0
- package/src/checks/project-checks.js +11 -0
- package/src/checks/repo-state.js +89 -0
- package/src/cli/advanced-commands.js +3 -0
- package/src/cli/register-meta.js +14 -0
- package/src/cli/register-pipeline.js +15 -1
- package/src/commands/bootstrap.js +131 -0
- package/src/commands/check.js +5 -0
- package/src/commands/code.js +57 -4
- package/src/commands/env.js +2 -1
- package/src/commands/go.js +9 -10
- package/src/commands/harden.js +3 -1
- package/src/commands/review-gate.js +28 -5
- package/src/environment/panel.js +52 -0
- package/src/environment/playbook.js +7 -6
- package/src/harden/config-templates.js +7 -1
- package/src/harden/guidelines-engine.js +7 -3
- package/src/harden/guidelines-templates.js +60 -10
- package/src/harden/hook-templates.js +4 -1
- 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/one-shot-review.js +5 -0
- package/src/review/ui-evidence.js +65 -0
- package/src/roles/agent-role.js +11 -2
- package/src/start/project-script.js +84 -0
|
@@ -15,12 +15,14 @@ import { ensureGateTrackable } from "../review/gate-gitignore.js";
|
|
|
15
15
|
import { runSonarPregate, formatSonarFinding, addedLinesByFile } from "../review/sonar-pregate.js";
|
|
16
16
|
import { checkSonarRequirement, SONAR_RULE_ID } from "../review/sonar-requirement.js";
|
|
17
17
|
import { checkRagRequirement, checkRagVerdict, ragBlock, RAG_RULE_ID } from "../review/rag-requirement.js";
|
|
18
|
+
import { checkUiEvidence, uiBlock } from "../review/ui-evidence.js";
|
|
18
19
|
import { readRagLedger } from "../review/rag-ledger.js";
|
|
19
20
|
import { runMutationPregate, formatSurvivor } from "../review/mutation-pregate.js";
|
|
20
21
|
import { checkCardFirst } from "../review/card-first.js";
|
|
21
22
|
import { liftSealedSupervisorViolations } from "../policy/supervisor-verify.js";
|
|
22
23
|
import { checkTestsWithCode } from "../review/tests-with-code.js";
|
|
23
24
|
import { loadPrivacyList, scanText } from "../privacy/scan.js";
|
|
25
|
+
import { isGeneratedPath, splitAddedByFile } from "../privacy/diff-scope.js";
|
|
24
26
|
import { checkStagedDiff, loadPolicy } from "../policy/engine.js";
|
|
25
27
|
import { loadStandingExceptions, recordPolicyException } from "../policy/exceptions.js";
|
|
26
28
|
import { policyFileHash, recordGateDecision } from "../policy/decisions.js";
|
|
@@ -229,17 +231,24 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
229
231
|
// don't publish. In the SEA binary the privacy module is stubbed and
|
|
230
232
|
// throws: the gate degrades with a note instead of crashing.
|
|
231
233
|
try {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
// KJC-BUG-0203: per file, so a finding can name where it lives and so
|
|
235
|
+
// build output is judged as what it is. Generic heuristics are silenced
|
|
236
|
+
// there (nobody typed a minified bundle); a denylist hit is not, because
|
|
237
|
+
// the incident behind this scanner was personal data inside a build.
|
|
238
|
+
const list = loadPrivacyList();
|
|
239
|
+
const findings = splitAddedByFile(diff).flatMap(({ file, added }) => {
|
|
240
|
+
const found = scanText(added, { list, source: file });
|
|
241
|
+
return isGeneratedPath(file) ? found.filter((f) => f.severity === "block") : found;
|
|
242
|
+
});
|
|
234
243
|
const blocks = findings.filter((f) => f.severity === "block");
|
|
235
244
|
const warns = findings.filter((f) => f.severity === "warn");
|
|
236
|
-
for (const f of warns) console.log(`⚠ privacy: [${f.type}]
|
|
245
|
+
for (const f of warns) console.log(`⚠ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked} — personal data? move it out before it ships`);
|
|
237
246
|
const hardened = config?.privacy?.generic === "block" && warns.length > 0;
|
|
238
247
|
if (blocks.length > 0 || hardened) {
|
|
239
248
|
if (process.env.KJ_ALLOW_PII === "1") {
|
|
240
249
|
console.log(`⚠ privacy exempt: ${blocks.length} denylist hit(s) — KJ_ALLOW_PII=1 (explicit escape hatch)`);
|
|
241
250
|
} else {
|
|
242
|
-
for (const f of blocks) console.log(`✗ privacy: [${f.type}]
|
|
251
|
+
for (const f of blocks) console.log(`✗ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked}`);
|
|
243
252
|
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)`;
|
|
244
253
|
console.log(`✗ privacy gate: ${reason}`);
|
|
245
254
|
process.exitCode = 1;
|
|
@@ -482,6 +491,20 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
482
491
|
+ twins.map((t) => `- ${t}`).join("\n");
|
|
483
492
|
}
|
|
484
493
|
|
|
494
|
+
// BOOT-D (KJC-TSK-0863): green is not proof for what a person SEES. kj does
|
|
495
|
+
// not drive the browser (the host has it); it demands the walkthrough and
|
|
496
|
+
// records it in the verdict, bound to the diff like sonar and rag. It WARNS:
|
|
497
|
+
// the last proof, never the only one, and a gate that fires often teaches
|
|
498
|
+
// people to skip gates.
|
|
499
|
+
const walked = Array.isArray(flags.walked) ? flags.walked : [];
|
|
500
|
+
const uiReq = checkUiEvidence({
|
|
501
|
+
stagedFiles: changedFiles,
|
|
502
|
+
evidence: walked.length > 0 ? { walked, tool: flags.walkedWith ?? null } : null,
|
|
503
|
+
standingExceptions: std.standing,
|
|
504
|
+
});
|
|
505
|
+
if (uiReq.warn) console.log(`⚠ ${uiReq.reason}`);
|
|
506
|
+
const uiRecord = uiBlock(uiReq);
|
|
507
|
+
|
|
485
508
|
// MUT-A (KJC-TSK-0716): mutation pre-gate — opt-in (method_gates.mutation),
|
|
486
509
|
// SOLO en --staged (jamás en pre-commit: cuesta minutos; y jamás en --range:
|
|
487
510
|
// el scope es el ÍNDICE y anotaría trabajo ajeno — catch de codex). block
|
|
@@ -504,7 +527,7 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
504
527
|
}
|
|
505
528
|
}
|
|
506
529
|
|
|
507
|
-
const record = await runOneShotReview({ diff, task, config, logger, projectDir, sonar: sonarRecord, rag: ragRecord });
|
|
530
|
+
const record = await runOneShotReview({ diff, task, config, logger, projectDir, sonar: sonarRecord, rag: ragRecord, ui: uiRecord });
|
|
508
531
|
printVerdict(record);
|
|
509
532
|
process.exitCode = record.verdict === "approved" ? 0 : 1;
|
|
510
533
|
return record;
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
|
|
15
|
+
/** Roles worth announcing: who writes, who reviews, who arbitrates. */
|
|
16
|
+
export const PANEL_ROLES = ["coder", "reviewer", "solomon"];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @returns {{coder: string|null, reviewer: string|null, solomon: string|null}}
|
|
20
|
+
* the provider declared (or inherited) for each panel role.
|
|
21
|
+
*/
|
|
22
|
+
export function resolvePanel(config) {
|
|
23
|
+
const panel = {};
|
|
24
|
+
for (const role of PANEL_ROLES) panel[role] = resolveRole(config, role).provider || null;
|
|
25
|
+
return panel;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One agent-facing line naming the panel, or null when the project declares
|
|
30
|
+
* no coder and no reviewer. A project without a panel gets no line at all:
|
|
31
|
+
* announcing "coder: nobody" would spend attention to say nothing.
|
|
32
|
+
*/
|
|
33
|
+
export function panelLine(config) {
|
|
34
|
+
const { coder, reviewer, solomon } = resolvePanel(config);
|
|
35
|
+
if (!coder && !reviewer) return null;
|
|
36
|
+
const parts = [`coder=${coder || "unset"}`, `reviewer=${reviewer || "unset"}`];
|
|
37
|
+
if (solomon && solomon !== coder) parts.push(`solomon=${solomon}`);
|
|
38
|
+
return [
|
|
39
|
+
`- The panel is your user's, not yours: ${parts.join(", ")}.`,
|
|
40
|
+
" When the coder is not you, the writing is ITS job; write it yourself",
|
|
41
|
+
" anyway and say so in your closing message, never in silence.",
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Human-facing one-liner for `kj check` and friends. */
|
|
46
|
+
export function panelSummary(config) {
|
|
47
|
+
const { coder, reviewer, solomon } = resolvePanel(config);
|
|
48
|
+
if (!coder && !reviewer) return "panel: not declared (no coder, no reviewer)";
|
|
49
|
+
const parts = [`coder ${coder || "unset"}`, `reviewer ${reviewer || "unset"}`];
|
|
50
|
+
if (solomon && solomon !== coder) parts.push(`solomon ${solomon}`);
|
|
51
|
+
return `panel: ${parts.join(" · ")}`;
|
|
52
|
+
}
|
|
@@ -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,11 @@ 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
|
+
export function installGuidelines({ projectDir = process.cwd(), dryRun = false, language = null } = {}) {
|
|
32
|
+
// KJC-BUG-0199 (issue #1773): the rules that enter the agent's context must
|
|
33
|
+
// be the rules of THIS project's language. kj harden already detects it for
|
|
34
|
+
// the configs; the guidelines just never asked.
|
|
35
|
+
const body = guidelinesBody(language);
|
|
32
36
|
const results = [];
|
|
33
37
|
for (const file of TARGETS) {
|
|
34
38
|
const target = join(projectDir, file);
|
|
@@ -38,7 +42,7 @@ export function installGuidelines({ projectDir = process.cwd(), dryRun = false }
|
|
|
38
42
|
source,
|
|
39
43
|
blockId: "guidelines",
|
|
40
44
|
version: BLOCK_VERSION,
|
|
41
|
-
body
|
|
45
|
+
body,
|
|
42
46
|
style: "html",
|
|
43
47
|
});
|
|
44
48
|
if (!dryRun && (migrated || action !== "unchanged")) writeFileSync(target, content);
|
|
@@ -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,54 @@ 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
|
+
/** The guidelines body for a language (unknown or absent ⇒ the core alone). */
|
|
80
|
+
export function guidelinesBody(language = null) {
|
|
81
|
+
const extra = LANGUAGE_GUIDELINES[String(language || "").toLowerCase()] ?? [];
|
|
82
|
+
return [...CORE, ...extra].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Back-compat for callers that predate the language split. */
|
|
86
|
+
export const GUIDELINES_BODY = guidelinesBody("javascript");
|
|
@@ -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`):",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which part of a diff the privacy gate reads, and how (KJC-BUG-0203).
|
|
3
|
+
*
|
|
4
|
+
* The gate used to concatenate every added line of the diff and scan the lump.
|
|
5
|
+
* Two consequences: a finding could not name the file it came from, and build
|
|
6
|
+
* output got judged as if a person had written it. A minified Starlight page
|
|
7
|
+
* warned twice for `[phone]` over an Astro class hash, which is the kind of
|
|
8
|
+
* false alarm that teaches people to skip the gate.
|
|
9
|
+
*
|
|
10
|
+
* The exemption is by SEVERITY, not by file. Generic heuristics (a shape that
|
|
11
|
+
* could be a phone, a card, an id) are silenced on generated output, where
|
|
12
|
+
* nobody typed anything. A denylist hit is NOT: the incident that created this
|
|
13
|
+
* scanner was personal emails published on a landing, which is exactly a
|
|
14
|
+
* denylist datum inside a build artifact.
|
|
15
|
+
*
|
|
16
|
+
* `kj privacy scan <dir>` is untouched. Scanning an artifact you are about to
|
|
17
|
+
* publish must keep looking at everything, generated or not.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Paths that are build output in this project's own vocabulary. */
|
|
21
|
+
const GENERATED = [
|
|
22
|
+
/(^|\/)dist\//,
|
|
23
|
+
/(^|\/)build\//,
|
|
24
|
+
/(^|\/)coverage\//,
|
|
25
|
+
/(^|\/)node_modules\//,
|
|
26
|
+
/(^|\/)\.astro\//,
|
|
27
|
+
/(^|\/)public\/docs\//,
|
|
28
|
+
/\.min\.(js|css)$/,
|
|
29
|
+
/\.map$/,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** @param {string} file @returns {boolean} */
|
|
33
|
+
export function isGeneratedPath(file) {
|
|
34
|
+
if (!file) return false;
|
|
35
|
+
return GENERATED.some((re) => re.test(file));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Split a unified diff into its added lines, per file.
|
|
40
|
+
* @param {string} diff
|
|
41
|
+
* @returns {Array<{file: string, added: string}>} files with at least one added line
|
|
42
|
+
*/
|
|
43
|
+
export function splitAddedByFile(diff) {
|
|
44
|
+
const out = [];
|
|
45
|
+
let current = null;
|
|
46
|
+
for (const line of String(diff || "").split("\n")) {
|
|
47
|
+
const header = /^diff --git a\/(?:.+) b\/(.+)$/.exec(line);
|
|
48
|
+
if (header) {
|
|
49
|
+
current = { file: header[1], lines: [] };
|
|
50
|
+
out.push(current);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!current) continue;
|
|
54
|
+
// `+++ b/x` is the header, not content; `+` alone is an added blank line.
|
|
55
|
+
if (line.startsWith("+") && !line.startsWith("+++")) current.lines.push(line.slice(1));
|
|
56
|
+
}
|
|
57
|
+
return out.filter((f) => f.lines.length > 0).map((f) => ({ file: f.file, added: f.lines.join("\n") }));
|
|
58
|
+
}
|
package/src/privacy/scan.js
CHANGED
|
@@ -73,6 +73,24 @@ const FIREBASE_KEY_HINT = "si es la clave web de Firebase (publica por diseno):
|
|
|
73
73
|
const CONTEXT_DISCARDS = [
|
|
74
74
|
{ type: "git-sha", re: /\b(?:[0-9a-f]{64}|[0-9a-f]{40})\b/g },
|
|
75
75
|
{ type: "doc-domain-email", re: /\b[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)*(?:example\.(?:com|org|net)|test|invalid|localhost|example)\b/g },
|
|
76
|
+
// KJC-BUG-0202: an HU Board id is `HU-<Date.now()>-<n>`, and Date.now() is
|
|
77
|
+
// 13 digits — exactly a card's length, so every project using the default
|
|
78
|
+
// board got a credit-card warning over its own card ids. The rule covers
|
|
79
|
+
// the family (an alphabetic prefix, a dash, a long digit run: HU-…, KJC-…,
|
|
80
|
+
// any tracker's) and NOTHING else.
|
|
81
|
+
//
|
|
82
|
+
// It deliberately does not go further, and it took three rejections to get
|
|
83
|
+
// this narrow:
|
|
84
|
+
// 1. discarding every card-shaped run that fails Luhn — wrong, a
|
|
85
|
+
// truncated or mistyped card also fails Luhn and is still sensitive;
|
|
86
|
+
// 2. any alphabetic prefix — `card-4111111111111111` swallowed;
|
|
87
|
+
// 3. any uppercase prefix with any digits — `CARD-4111111111111111-1`
|
|
88
|
+
// swallowed.
|
|
89
|
+
// So the middle segment must be an actual millisecond timestamp: exactly 13
|
|
90
|
+
// digits starting 16-19 (years 2022-2033). No card range begins with a 1
|
|
91
|
+
// (Visa 4, Mastercard 2/5, Amex 3, Discover 6), so a card number cannot
|
|
92
|
+
// occupy that slot at any length.
|
|
93
|
+
{ type: "tracker-id", re: /\b[A-Z]{2,6}-1[6-9]\d{11}-\d{1,4}\b/g },
|
|
76
94
|
];
|
|
77
95
|
|
|
78
96
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The coder gets the CARD, not just a sentence (KJC-TSK-0864).
|
|
3
|
+
*
|
|
4
|
+
* `kj code` handed the agent a bare task string: no card, no acceptance
|
|
5
|
+
* criteria, not even the project boundary the pipeline always passed. That is
|
|
6
|
+
* the gap the user hit, because when the host orchestrates instead of
|
|
7
|
+
* `kj run`, `kj code` IS the way the declared coder gets invoked, and a coder
|
|
8
|
+
* without the card writes to a sentence instead of to a contract.
|
|
9
|
+
*
|
|
10
|
+
* A local HU is read from the board. A reference kj cannot read (an external
|
|
11
|
+
* board lives in the host's own MCP, not here) travels as the reference it is
|
|
12
|
+
* and says so: the coder learns the card exists and where to ask for it,
|
|
13
|
+
* instead of receiving a silently empty context.
|
|
14
|
+
*/
|
|
15
|
+
import { getHu } from "../hu/store.js";
|
|
16
|
+
|
|
17
|
+
/** Ids minted by the HU Board; anything else belongs to an external board. */
|
|
18
|
+
const LOCAL_HU = /^HU-/i;
|
|
19
|
+
|
|
20
|
+
const externalSection = (ref) =>
|
|
21
|
+
[
|
|
22
|
+
`## Card ${ref}`,
|
|
23
|
+
"",
|
|
24
|
+
`This work is tracked as ${ref} on the project's own board, which kj cannot read from here.`,
|
|
25
|
+
"Treat the task below as the card's statement, and if something the card should answer is missing, ASK instead of guessing.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
const localSection = (hu) => {
|
|
29
|
+
const lines = [`## Card ${hu.id} — ${hu.title}`, ""];
|
|
30
|
+
if (hu.description?.trim()) lines.push(hu.description.trim(), "");
|
|
31
|
+
lines.push(`Status on the board: ${hu.status}. The card is the contract: done means its statement is literally true.`);
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {{projectDir: string, ref: string|null, deps?: {getHu?: Function}}} args
|
|
37
|
+
* @returns {Promise<null|{huId: string, title: string|null, section: string,
|
|
38
|
+
* acceptanceTests: Array|null, external: boolean}>}
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveCardContext({ projectDir, ref, deps = {} }) {
|
|
41
|
+
if (!ref?.trim()) return null;
|
|
42
|
+
const id = ref.trim();
|
|
43
|
+
if (!LOCAL_HU.test(id)) {
|
|
44
|
+
return { huId: id, title: null, section: externalSection(id), acceptanceTests: null, external: true };
|
|
45
|
+
}
|
|
46
|
+
// A missing local HU is an error, never an empty context: the caller asked
|
|
47
|
+
// for a card by id and kj either delivers it or says it does not exist.
|
|
48
|
+
const hu = await (deps.getHu || getHu)(projectDir, id);
|
|
49
|
+
const criteria = hu.acceptanceCriteria?.trim();
|
|
50
|
+
return {
|
|
51
|
+
huId: hu.id,
|
|
52
|
+
title: hu.title,
|
|
53
|
+
section: localSection(hu),
|
|
54
|
+
acceptanceTests: criteria ? [{ type: "gherkin", content: criteria }] : null,
|
|
55
|
+
external: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the coder knows before it writes (KJC-TSK-0864).
|
|
3
|
+
*
|
|
4
|
+
* The method's first invariant is "the RAG answers before you assume", and
|
|
5
|
+
* `kj run` honours it through the researcher stage. `kj code` did not: the
|
|
6
|
+
* declared coder started from a sentence and guessed the codebase. So the
|
|
7
|
+
* session asks the index on the coder's behalf and hands it what came back.
|
|
8
|
+
*
|
|
9
|
+
* Retrieval is best-effort and LOUD: an empty or unreachable index warns and
|
|
10
|
+
* the coder works without project context, because blocking here would stop
|
|
11
|
+
* work in a repo that simply has no index yet. What it never does is stay
|
|
12
|
+
* quiet about it.
|
|
13
|
+
*/
|
|
14
|
+
import { ragQueryCommand } from "../commands/rag.js";
|
|
15
|
+
|
|
16
|
+
/** Hits to carry and how much of each: the prompt pays for every line. */
|
|
17
|
+
const TOP_K = 5;
|
|
18
|
+
const EXCERPT = 400;
|
|
19
|
+
|
|
20
|
+
const label = (hit) =>
|
|
21
|
+
hit.metadata?.symbol || hit.metadata?.hu_id || hit.metadata?.headingPath?.join(" > ") || hit.kind || "block";
|
|
22
|
+
|
|
23
|
+
/** @returns {string|null} the agent-facing section, or null when there is nothing to say. */
|
|
24
|
+
export function ragSection(hits) {
|
|
25
|
+
if (!Array.isArray(hits) || hits.length === 0) return null;
|
|
26
|
+
const lines = [
|
|
27
|
+
"## What the project's RAG index answers about this task",
|
|
28
|
+
"",
|
|
29
|
+
"Retrieved for you, so you do not guess what the codebase does. These are excerpts: open the file before changing it.",
|
|
30
|
+
"",
|
|
31
|
+
];
|
|
32
|
+
for (const hit of hits) {
|
|
33
|
+
const text = hit.text || "";
|
|
34
|
+
lines.push(`### ${hit.source} · ${label(hit)}`);
|
|
35
|
+
lines.push("```", text.length > EXCERPT ? `${text.slice(0, EXCERPT)}…` : text, "```", "");
|
|
36
|
+
}
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @returns {Promise<null|{section: string|null, sources: string[]}>}
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveRagContext({ task, config, logger, deps = {} }) {
|
|
44
|
+
const run = deps.ragQueryCommand || ragQueryCommand;
|
|
45
|
+
// The index warns on stdout in CLI mode; here the hits are the product, so
|
|
46
|
+
// its narration is swallowed and only OUR verdict reaches the user.
|
|
47
|
+
const quiet = { info: () => {}, warn: () => {}, error: () => {} };
|
|
48
|
+
let hits;
|
|
49
|
+
try {
|
|
50
|
+
hits = await run({ text: task, config, logger: quiet, flags: { topK: TOP_K } });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
logger?.warn?.(`rag context unavailable (${err.message}) — the coder writes without project context`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!hits?.length) {
|
|
56
|
+
logger?.warn?.("the RAG index returned nothing for this task — the coder writes without project context");
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const sources = [...new Set(hits.map((h) => h.source))];
|
|
60
|
+
logger?.info?.(`RAG context: ${sources.length} file(s) — ${sources.join(", ")}`);
|
|
61
|
+
return { section: ragSection(hits), sources };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The task the coder reads: everything it was given, then its own statement. */
|
|
65
|
+
export function composeTask(task, sections = []) {
|
|
66
|
+
const kept = sections.filter(Boolean);
|
|
67
|
+
return kept.length ? `${kept.join("\n\n")}\n\n## Task\n\n${task}` : task;
|
|
68
|
+
}
|
|
@@ -47,6 +47,8 @@ export async function runOneShotReview({
|
|
|
47
47
|
sonar = null,
|
|
48
48
|
// KJC-TSK-0849 (ADR 0010): what the session's RAG ledger proved, same place.
|
|
49
49
|
rag = null,
|
|
50
|
+
// BOOT-D (KJC-TSK-0863): the walkthrough of what a person sees, same place.
|
|
51
|
+
ui = null,
|
|
50
52
|
hostAgent = detectHostAgent(),
|
|
51
53
|
createAgentFn = createAgent,
|
|
52
54
|
detectAgents = detectAvailableAgents,
|
|
@@ -129,6 +131,9 @@ export async function runOneShotReview({
|
|
|
129
131
|
summary: parsed.summary || parsed.raw_summary || "",
|
|
130
132
|
...(sonar ? { sonar } : {}),
|
|
131
133
|
...(rag ? { rag } : {}),
|
|
134
|
+
// BOOT-D (KJC-TSK-0863): the walkthrough of what a person sees travels with
|
|
135
|
+
// the verdict, bound to this diff, like the other two proofs.
|
|
136
|
+
...(ui ? { ui } : {}),
|
|
132
137
|
confidence: parsed.confidence ?? null,
|
|
133
138
|
});
|
|
134
139
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOOT-D (KJC-TSK-0863) — the done-statement of something a person SEES is not
|
|
3
|
+
* proved by a green suite.
|
|
4
|
+
*
|
|
5
|
+
* The Anthropic write-up on long-running agents reports it as an observed
|
|
6
|
+
* failure: the agent marked features complete without checking them. This
|
|
7
|
+
* codebase has the same hole. `impeccable` gives an AI OPINION about the diff,
|
|
8
|
+
* which is precisely what KJC-TSK-0726 rejects; what is missing is the proof
|
|
9
|
+
* that the route works when a person walks it.
|
|
10
|
+
*
|
|
11
|
+
* kj does not drive the browser: the host already has Chrome DevTools, and
|
|
12
|
+
* duplicating that would tie kj to one runner. kj DEMANDS the evidence and
|
|
13
|
+
* records it in the verdict, bound to the diff, exactly as it does with sonar
|
|
14
|
+
* and rag.
|
|
15
|
+
*
|
|
16
|
+
* It WARNS, it does not block. The article itself admits the browser misses
|
|
17
|
+
* native modals, so this is the last proof, never the only one — and a gate
|
|
18
|
+
* that fails often teaches people to skip gates, which is the doctrine behind
|
|
19
|
+
* every other rule here.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const UI_RULE_ID = "method.ui.evidence";
|
|
23
|
+
|
|
24
|
+
// What a person can actually see. Deliberately narrow: a false demand costs
|
|
25
|
+
// more than a missed one while this only warns.
|
|
26
|
+
const VISIBLE = /\.(jsx|tsx|vue|svelte|astro|css|scss|sass|less|html)$/i;
|
|
27
|
+
|
|
28
|
+
const liveGrant = (standingExceptions, now) => (standingExceptions || []).find((e) => {
|
|
29
|
+
if (e?.rule_id !== UI_RULE_ID) return false;
|
|
30
|
+
const until = Date.parse(e?.expiresAt ?? "");
|
|
31
|
+
return Number.isFinite(until) && until > now.getTime();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @returns {{ok: boolean, mode: "not-visible"|"walked"|"granted"|"missing", visible: string[], warn?: boolean, reason?: string, evidence?: object, grant?: object}}
|
|
36
|
+
*/
|
|
37
|
+
export function checkUiEvidence({ stagedFiles = [], evidence = null, standingExceptions = [], now = new Date() } = {}) {
|
|
38
|
+
const visible = stagedFiles.filter((f) => VISIBLE.test(String(f)));
|
|
39
|
+
if (visible.length === 0) return { ok: true, mode: "not-visible", visible };
|
|
40
|
+
|
|
41
|
+
const walked = Array.isArray(evidence?.walked) ? evidence.walked.filter(Boolean) : [];
|
|
42
|
+
if (walked.length > 0) return { ok: true, mode: "walked", visible, evidence: { walked, tool: evidence.tool ?? null } };
|
|
43
|
+
|
|
44
|
+
const grant = liveGrant(standingExceptions, now);
|
|
45
|
+
if (grant) return { ok: true, mode: "granted", visible, grant };
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
warn: true,
|
|
50
|
+
mode: "missing",
|
|
51
|
+
visible,
|
|
52
|
+
reason: `verde no es prueba: ${visible.length} fichero(s) que una persona VE (${visible.join(", ")}) sin recorrido comprobado — recórrelo como lo haría ella y deja constancia`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The block that travels inside the verdict, bound to the diff like sonar's. */
|
|
57
|
+
export function uiBlock(req) {
|
|
58
|
+
if (!req || req.mode === "not-visible") return null;
|
|
59
|
+
return {
|
|
60
|
+
mode: req.mode,
|
|
61
|
+
walked: req.evidence?.walked ?? [],
|
|
62
|
+
tool: req.evidence?.tool ?? null,
|
|
63
|
+
visible: req.visible ?? [],
|
|
64
|
+
};
|
|
65
|
+
}
|