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.
Files changed (39) 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/role-fallback-chain.js +100 -0
  12. package/src/brain/with-brain-recovery.js +59 -3
  13. package/src/checks/action-pins.js +131 -0
  14. package/src/checks/project-checks.js +11 -0
  15. package/src/checks/repo-state.js +89 -0
  16. package/src/cli/advanced-commands.js +3 -0
  17. package/src/cli/register-meta.js +14 -0
  18. package/src/cli/register-pipeline.js +15 -1
  19. package/src/commands/bootstrap.js +131 -0
  20. package/src/commands/check.js +5 -0
  21. package/src/commands/code.js +57 -4
  22. package/src/commands/env.js +2 -1
  23. package/src/commands/go.js +9 -10
  24. package/src/commands/harden.js +3 -1
  25. package/src/commands/review-gate.js +28 -5
  26. package/src/environment/panel.js +52 -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 +7 -3
  30. package/src/harden/guidelines-templates.js +60 -10
  31. package/src/harden/hook-templates.js +4 -1
  32. package/src/privacy/diff-scope.js +58 -0
  33. package/src/privacy/scan.js +18 -0
  34. package/src/prompts/card-context.js +57 -0
  35. package/src/prompts/session-context.js +68 -0
  36. package/src/review/one-shot-review.js +5 -0
  37. package/src/review/ui-evidence.js +65 -0
  38. package/src/roles/agent-role.js +11 -2
  39. 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
- const added = diff.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1)).join("\n");
233
- const findings = scanText(added, { list: loadPrivacyList(), source: "<staged diff>" });
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}] added line ${f.line} → ${f.masked} — personal data? move it out before it ships`);
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}] on added line ${f.line} → ${f.masked}`);
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 { 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,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: GUIDELINES_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
- 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,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 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
+ /** 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
- `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`):",
@@ -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
+ }
@@ -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
+ }