karajan-code 4.70.0 → 4.70.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.70.0",
3
+ "version": "4.70.2",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -9,8 +9,43 @@
9
9
  * WARN in `kj doctor` with the exact remedy instead of a cryptic runtime crash.
10
10
  */
11
11
 
12
+ import { execFileSync } from "node:child_process";
13
+ import { resolve } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
12
16
  import { STRATEGY } from "./types.js";
13
17
 
18
+ // kj's own package root: where npm keeps its dependencies' approvals and builds.
19
+ const OWN_PKG_DIR = resolve(fileURLToPath(new URL("../..", import.meta.url)));
20
+
21
+ /** npm's major version, or null when npm is not there to ask. */
22
+ function defaultNpmMajor() {
23
+ try {
24
+ return Number.parseInt(execFileSync("npm", ["--version"], { encoding: "utf8", timeout: 10_000 }), 10) || null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * KJC-BUG-0426 (issue #2670): npm 12 runs a dependency's install script only when
32
+ * allowScripts covers it; reinstalling blocks it again. Approve, then rebuild, in kj's dir.
33
+ */
34
+ export const npmAllowScriptsRemedy = (pkgDir = OWN_PKG_DIR) =>
35
+ `cd "${pkgDir}" && npm install-scripts approve better-sqlite3 && npm install-scripts approve node-pty && npm rebuild better-sqlite3 node-pty`;
36
+
37
+ // The addon is installed but its native build is missing (a missing package needs a reinstall, not this).
38
+ const UNBUILT_ADDON = /bindings file|better_sqlite3\.node/;
39
+
40
+ /** A missing better-sqlite3 addon becomes an error that says how to build it; any other error passes as it is. */
41
+ export function explainNativeLoadError(err, { npmMajor = defaultNpmMajor, pkgDir = OWN_PKG_DIR } = {}) {
42
+ if (!UNBUILT_ADDON.test(String(err?.message ?? err))) return err;
43
+ const remedy = (npmMajor() ?? 0) >= 12
44
+ ? `npm 12 skipped its install script (allowScripts); build it with: ${npmAllowScriptsRemedy(pkgDir)}`
45
+ : "run kj doctor for the exact fix on this install";
46
+ return new Error(`better-sqlite3 native addon is not built, so the RAG cannot open its index — ${remedy}`, { cause: err });
47
+ }
48
+
14
49
  /** pnpm lays packages out under a `.pnpm` virtual store and symlinks them in. */
15
50
  function isPnpmInstall() {
16
51
  return (
@@ -25,7 +60,7 @@ async function defaultLoadDb() {
25
60
  new Database(":memory:").close();
26
61
  }
27
62
 
28
- export function createNativeBuildCheck({ loadDb = defaultLoadDb, isPnpm = isPnpmInstall } = {}) {
63
+ export function createNativeBuildCheck({ loadDb = defaultLoadDb, isPnpm = isPnpmInstall, npmMajor = defaultNpmMajor, pkgDir = OWN_PKG_DIR } = {}) {
29
64
  return {
30
65
  name: "native-build",
31
66
  label: "native modules (better-sqlite3)",
@@ -48,6 +83,14 @@ export function createNativeBuildCheck({ loadDb = defaultLoadDb, isPnpm = isPnpm
48
83
  fix: "Approve the build then reinstall: `pnpm approve-builds better-sqlite3` — or install with npm: `npm i -g @karajan-family/code`",
49
84
  };
50
85
  }
86
+ if ((npmMajor() ?? 0) >= 12) {
87
+ return {
88
+ ok: false,
89
+ severity: "warn",
90
+ detail: `better-sqlite3 failed to load: ${firstLine} — npm 12 skips dependency install scripts that allowScripts does not cover, so the native build never ran`,
91
+ fix: npmAllowScriptsRemedy(pkgDir),
92
+ };
93
+ }
51
94
  return {
52
95
  ok: false,
53
96
  severity: "warn",
@@ -219,7 +219,10 @@ export async function hardenCommand({
219
219
  const note = c.note ? ` (${c.note})` : "";
220
220
  logger.info?.(` • ${c.file}: ${c.action}${note}`);
221
221
  }
222
- for (const w of out.workflows) logger.info?.(` • ${w.file}: ${w.action}`);
222
+ for (const w of out.workflows) {
223
+ logger.info?.(` • ${w.file}: ${w.action}`);
224
+ if (w.note) logger.warn?.(`⚠ ${w.note}`);
225
+ }
223
226
  for (const g of out.guidelines) {
224
227
  logger.info?.(` • ${g.file}: ${g.action}`);
225
228
  // KJC-BUG-0206: quitar reglas no puede leerse como "updated" y nada más.
@@ -119,7 +119,9 @@ export async function loadConfig(projectDir) {
119
119
  // a .karajan dir from another repo, edited the wrong file, etc.) and
120
120
  // would silently behave like a global config without the defaults
121
121
  // people expect. Refuse with an actionable message.
122
- if (projectConfig && !globalExists) {
122
+ // KJC-BUG-0428 (issue #2694): a CI runner never has a global config; there the
123
+ // team's committed project config sits on the defaults instead.
124
+ if (projectConfig && !globalExists && !process.env.CI) {
123
125
  throw new Error(
124
126
  `Local config found at ${getProjectConfigPath(projectDir)} but no global config exists.\n` +
125
127
  "Local configs are merged ON TOP of the global one — run `kj init --global` first to create the base."
@@ -84,8 +84,9 @@ process.stdin.on("end", () => {
84
84
  // substitution) cannot be resolved from command text: that vector is
85
85
  // caught by the Stop gate's checksum verification, which blocks the turn.
86
86
  // KJC-TSK-1114: where the host sandbox denies writing them, the sandbox guards them instead.
87
+ // KJC-BUG-0425 (ADR 0036): text a command only publishes or stores (a PR description) is not a write.
87
88
  const SUPERVISOR_PATHS = [".claude/settings.json", ".karajan/hooks", ".karajan/harness"];
88
- if (tool === "Bash" && PROTECTED.test(String(input.command || "").replaceAll("\\", "/")) && !sandboxCovers(ROOT, SUPERVISOR_PATHS)) {
89
+ if (tool === "Bash" && PROTECTED.test(stripTextOnlyCommandValues(String(input.command || "")).replaceAll("\\", "/")) && !sandboxCovers(ROOT, SUPERVISOR_PATHS)) {
89
90
  console.error("karajan sentinel: los ficheros del supervisor no se tocan desde Bash — consultalos con la tool Read/Grep; solo el humano los modifica, fuera de la sesion." + doc("supervisor"));
90
91
  process.exit(2);
91
92
  }
@@ -174,7 +174,22 @@ const isGitCommit = (words) => {
174
174
  return words[0] === "git" && words[i] === "commit";
175
175
  };
176
176
 
177
- const isTextOnlyHead = (seg) => /^kj\s+(?:hu|adr|handoff)\s+add\b/.test(seg) || isGitCommit(seg.split(/\s+/));
177
+ // KJC-BUG-0425: gh pr/issue/release commands only publish their text to GitHub.
178
+ const isTextOnlyHead = (seg) => /^kj\s+(?:hu|adr|handoff)\s+add\b/.test(seg) || /^gh\s+(?:pr|issue|release)\s+(?:create|edit|comment|review)\b/.test(seg) || isGitCommit(seg.split(/\s+/));
179
+
180
+ // KJC-BUG-0425: a heredoc whose delimiter is quoted expands nothing; read from stdin
181
+ // by a text-only command, it is the text stored or published. The FIRST line equal to
182
+ // the delimiter must be the last one, or the shell would run what follows it.
183
+ const HEREDOC_HEAD = /^([^\n]*?)[ \t]*<<(-?)[ \t]*(['"])([A-Za-z_]\w*)\3[ \t]*$/;
184
+ const READS_STDIN = /(^|\s)(?:--body-file|--notes-file|--file|-F)(?:=|\s+)-(?=\s|$)/;
185
+ const quotedHeredocHead = (cmd) => {
186
+ const lines = cmd.replace(/\n$/, "").split("\n");
187
+ const m = lines.length > 1 ? HEREDOC_HEAD.exec(lines[0]) : null;
188
+ if (!m) return null;
189
+ const [, head, dash, , delim] = m;
190
+ const end = lines.findIndex((l, i) => i > 0 && (dash ? l.replace(/^\t+/, "") : l) === delim);
191
+ return end === lines.length - 1 && READS_STDIN.test(head) ? head : null;
192
+ };
178
193
 
179
194
  /**
180
195
  * KJC-BUG-0354: blank the quoted text values (stripTextOptionValues) only when the
@@ -185,9 +200,10 @@ const isTextOnlyHead = (seg) => /^kj\s+(?:hu|adr|handoff)\s+add\b/.test(seg) ||
185
200
  export const stripTextOnlyCommandValues = (cmd) => {
186
201
  // Only a whole input that is ONE simple text-storage command: a chained
187
202
  // segment could follow a shell function that turns the stored text into code.
188
- const bare = stripInertQuotes(cmd);
189
- if (splitOutsideQuotes(cmd).length !== 1 || /[(){}<>`$\n]/.test(bare) || !isTextOnlyHead(cmd.trim())) return cmd;
190
- return stripTextOptionValues(cmd);
203
+ const line = quotedHeredocHead(cmd) ?? cmd;
204
+ const bare = stripInertQuotes(line);
205
+ if (splitOutsideQuotes(line).length !== 1 || /[(){}<>`$\n]/.test(bare) || !isTextOnlyHead(line.trim())) return cmd;
206
+ return stripTextOptionValues(line);
191
207
  };
192
208
 
193
209
  /** Segments and the ; && || | between them, cut only outside quotes (the pieces join back to cmd). */
@@ -16,6 +16,13 @@ import { parse as parseYaml } from "yaml";
16
16
  import { upsertManagedBlock } from "../utils/managed-markers.js";
17
17
  import { extraWorkflowsFor, mutationWorkflowFor, qualityWorkflowFor, policyWorkflowFor, stewardWorkflowFor, WORKFLOWS } from "./workflow-templates.js";
18
18
 
19
+ // KJC-BUG-0424: on the PR that brings this change GitHub runs neither YAML (the base's
20
+ // listens to pull_request, the new one to pull_request_target), so policy-check never reports.
21
+ export const POLICY_TRIGGER_MOVE_NOTE =
22
+ "kj-policy.yml moves from pull_request to pull_request_target (judged from the base branch). " +
23
+ "The PR that brings this change gets no policy-check run: if policy-check is a required check, " +
24
+ "merge that one PR as an admin after the other checks pass; from then on the base runs the judge.";
25
+
19
26
  // La versión del kj que corre harden — es la que se pinea en el fallback
20
27
  // npx del workflow de policy (jamás @latest en CI). Lectura LAZY: en el
21
28
  // bundle SEA no hay package.json junto al módulo y una lectura eager a
@@ -125,7 +132,9 @@ export function installWorkflows({
125
132
  // pre-commit rechaza el fichero recién generado.
126
133
  writeFileSync(target, content.endsWith("\n") ? content : `${content}\n`);
127
134
  }
128
- results.push({ file: label, action });
135
+ // KJC-BUG-0424: the PR that moves the trigger cannot report policy-check.
136
+ const movesTrigger = wf.blockId === "wf-policy" && action !== "unchanged" && /^\s*pull_request:\s*$/m.test(source);
137
+ results.push({ file: label, action, ...(movesTrigger ? { note: POLICY_TRIGGER_MOVE_NOTE } : {}) });
129
138
  }
130
139
  return { dryRun, workflows: results };
131
140
  }
@@ -69,10 +69,13 @@ export const WORKFLOWS = [
69
69
  // npx PINEADO a la versión del kj que corrió harden — jamás @latest en CI.
70
70
  // KJC-BUG-0399: the judge is never the PR's code. A repo that ships kj itself
71
71
  // judges with the kj of the BASE branch, checked out apart; the PR is only data.
72
+ // KJC-BUG-0419: on `pull_request` GitHub runs the PR's own copy of this YAML, so a
73
+ // PR could rewrite its judge. `pull_request_target` runs the base's YAML; the PR
74
+ // is fetched into its own worktree, read by kj and never installed nor executed.
72
75
  export const policyWorkflowFor = (kjVersion, kjName = "karajan-code") => [
73
76
  "name: Policy",
74
77
  "on:",
75
- " pull_request:",
78
+ " pull_request_target:",
76
79
  "permissions:",
77
80
  " contents: read",
78
81
  "jobs:",
@@ -88,16 +91,23 @@ export const policyWorkflowFor = (kjVersion, kjName = "karajan-code") => [
88
91
  " - name: kj policy check (strict) on the PR diff",
89
92
  " env:",
90
93
  " BASE_REF: ${{ github.base_ref }}",
94
+ " PR_NUMBER: ${{ github.event.pull_request.number }}",
91
95
  " run: |",
92
96
  ' JUDGE="$RUNNER_TEMP/kj-judge"',
97
+ ' PR_TREE="$RUNNER_TEMP/kj-pr"',
93
98
  ' git fetch --no-tags origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}"',
99
+ ' git fetch --no-tags origin "+refs/pull/${PR_NUMBER}/head:refs/remotes/origin/kj-pr-head"',
94
100
  ' if git cat-file -e "origin/${BASE_REF}:bin/kj.js" 2>/dev/null; then',
95
101
  ' git worktree add --detach "$JUDGE" "origin/${BASE_REF}"',
96
102
  ' (cd "$JUDGE" && npm ci --ignore-scripts)',
97
103
  ' KJ="node $JUDGE/bin/kj.js"',
98
104
  " else",
99
- ` KJ="npx --yes ${kjName}@${kjVersion}"`,
105
+ ' mkdir -p "$JUDGE"',
106
+ ` (cd "$JUDGE" && echo '{"private":true}' > package.json && npm install --no-save --ignore-scripts --no-audit --no-fund ${kjName}@${kjVersion})`,
107
+ ` KJ="node $JUDGE/node_modules/${kjName}/bin/kj.js"`,
100
108
  " fi",
109
+ ' git worktree add --detach "$PR_TREE" origin/kj-pr-head',
110
+ ' cd "$PR_TREE"',
101
111
  ' $KJ policy check --range "origin/${BASE_REF}...HEAD" --strict',
102
112
  ].join("\n");
103
113
 
package/src/policy/add.js CHANGED
@@ -11,6 +11,7 @@ import yaml from "js-yaml";
11
11
  import { parsePolicy } from "@karajan-family/governance";
12
12
  import { createAgent } from "../agents/index.js";
13
13
  import { resolveRole } from "../config/role-resolver.js";
14
+ import { agentAnswerText } from "../review/parser.js";
14
15
 
15
16
  const PROMPT = (text) => `Traduce esta regla de proyecto hablada a un FRAGMENTO YAML del vocabulario
16
17
  cerrado v1 de policy de Karajan. Vocabulario COMPLETO (nada fuera de esto):
@@ -27,7 +28,8 @@ async function defaultTranslate({ text, config, logger }) {
27
28
  const role = resolveRole(config, "reviewer");
28
29
  const res = await createAgent(role.provider, config, logger).reviewTask({ prompt: PROMPT(text), role: "reviewer" });
29
30
  if (!res?.ok) throw new Error(`el agente traductor (${role.provider}) falló: ${res?.error || "sin salida"}`);
30
- return String(res.output || "").replace(/^```[a-z]*\n?|```\s*$/g, "").trim();
31
+ // KJC-BUG-0430 (issue #2695): claude answers with its stream-json; the answer is its final result.
32
+ return String(agentAnswerText(res.output) || "").replace(/^```[a-z]*\n?|```\s*$/g, "").trim();
31
33
  }
32
34
 
33
35
  // Merge conservador: las listas se CONCATENAN (añadir jamás borra), los
@@ -9,7 +9,8 @@
9
9
  import { existsSync } from "node:fs";
10
10
  import { sep } from "node:path";
11
11
 
12
- import { openVecStore, insertChunk, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
12
+ import { insertChunk, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
13
+ import { openVecStoreExplained as openVecStore } from "./project-store.js";
13
14
 
14
15
  export const countProjectChunks = (db, slug) => db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE project_slug = ?").get(slug).n;
15
16
 
@@ -17,6 +17,7 @@ import { execFileSync } from "node:child_process";
17
17
  import path from "node:path";
18
18
 
19
19
  import { openVecStore } from "./vec-store.js";
20
+ import { explainNativeLoadError } from "../checks/native-build.js";
20
21
  import { getKarajanHome } from "../utils/paths.js";
21
22
 
22
23
  function projectRoot(dir) {
@@ -39,10 +40,20 @@ export function libraryDbPath() {
39
40
  return process.env.KJ_LIBRARY_DB || path.join(getKarajanHome(), "library.db");
40
41
  }
41
42
 
43
+ // KJC-BUG-0426: an unbuilt better-sqlite3 addon says how to build it, not "bindings file".
44
+ // Every opening of a RAG store goes through here.
45
+ export function openVecStoreExplained(opts) {
46
+ try {
47
+ return openVecStore(opts);
48
+ } catch (err) {
49
+ throw explainNativeLoadError(err);
50
+ }
51
+ }
52
+
42
53
  export function openLibraryStore({ dim = 768 } = {}) {
43
- return openVecStore({ dim, path: libraryDbPath() });
54
+ return openVecStoreExplained({ dim, path: libraryDbPath() });
44
55
  }
45
56
 
46
57
  export function openProjectStore({ projectDir = process.cwd(), dim = 768 } = {}) {
47
- return openVecStore({ dim, path: projectDbPath(projectDir) });
58
+ return openVecStoreExplained({ dim, path: projectDbPath(projectDir) });
48
59
  }