karajan-code 4.10.0 → 4.11.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.10.0",
3
+ "version": "4.11.0",
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",
@@ -12,7 +12,9 @@ import { ragIndexCommand } from "./rag.js";
12
12
  import { renderPendingBlock, PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
13
13
  import { onnxConfig, persistOnnxChoice, resetEmptyStore } from "../rag/onnx-fallback.js";
14
14
  import { verifyBoardAccess } from "../environment/board-access.js";
15
+ import { pickStateBackend } from "../environment/board-select.js";
15
16
  import { ensurePrivacyList } from "../privacy/onboarding.js";
17
+ import { installProjectMcp } from "../environment/mcp-wiring.js";
16
18
  import { createWizard } from "../utils/wizard.js";
17
19
  import { hardenCommand } from "./harden.js";
18
20
  import { reviewGateCommand } from "./review-gate.js";
@@ -48,6 +50,23 @@ export function briefCommand({ config = null, flags = {}, role = null }) {
48
50
 
49
51
  export async function envInstallCommand({ config = null, logger = null, flags = {} }) {
50
52
  const projectDir = config?.projectDir || process.cwd();
53
+
54
+ // KJC-TSK-0709 — the board is chosen BEFORE the playbook renders (its
55
+ // tracking line depends on the backend): interactive installs ask when
56
+ // alternatives are reachable, headless installs name the default. The
57
+ // choice persists so it is only ever asked once.
58
+ try {
59
+ const wizard = process.stdin.isTTY && process.stdout.isTTY ? createWizard() : null;
60
+ const sel = await pickStateBackend({ projectDir, wizard, isTTY: Boolean(wizard), logger: console });
61
+ wizard?.close();
62
+ // The effective backend feeds the render on EVERY non-error path —
63
+ // chosen, declared-in-file or default — so the playbook never falls
64
+ // back to hu-board while the selection said otherwise.
65
+ config = { ...config, state_backend: sel.backend, board: sel.boardName ? { ...(config?.board || {}), name: sel.boardName } : config?.board };
66
+ } catch (err) {
67
+ console.log(`⚠ board selection failed (${err.message}) — continuing with the configured default`);
68
+ }
69
+
51
70
  const result = await installPlaybook({
52
71
  projectDir, target: flags.target || "all",
53
72
  stateBackend: config?.state_backend || "hu-board",
@@ -55,6 +74,14 @@ export async function envInstallCommand({ config = null, logger = null, flags =
55
74
  });
56
75
  console.log(`✓ Karajan playbook installed in: ${result.files.join(", ")}`);
57
76
 
77
+ // KJC-TSK-0711 — RAG as a native tool: agents use what is in their
78
+ // toolbox, so the official path must be cheaper than the grep shortcut.
79
+ try {
80
+ installProjectMcp({ projectDir, logger: console });
81
+ } catch (err) {
82
+ console.log(`⚠ could not wire kj-rag-mcp into .mcp.json: ${err.message}`);
83
+ }
84
+
58
85
  // KJC-TSK-0685 (user rule): Karajan does not run without a board — it is
59
86
  // what guarantees ordered, card-first work. Verify an OPERATIONAL access
60
87
  // path to the declared backend BEFORE anything else; no path → block
@@ -238,8 +238,10 @@ export const ConfigSchema = v.looseObject({
238
238
  // ENV-D1 (KJC-TSK-0642): where work items live — the integrated HU Board
239
239
  // or the user's Planning Game. The v4 playbook renders per backend.
240
240
  state_backend: v.optional(v.picklist(
241
- ["hu-board", "planning-game"],
242
- "state_backend must be \"hu-board\" or \"planning-game\""
241
+ // KJC-TSK-0709 caught the drift: "external" shipped in v4.5.0 (any
242
+ // board via the agent's own MCP/tools) but this picklist never learned it.
243
+ ["hu-board", "planning-game", "external"],
244
+ "state_backend must be \"hu-board\", \"planning-game\" or \"external\""
243
245
  )),
244
246
  review_rules: v.optional(v.string()),
245
247
  coder_rules: v.optional(v.string()),
@@ -42,6 +42,25 @@ function mcpConfigsMention(slug, projectDir, home) {
42
42
  return null;
43
43
  }
44
44
 
45
+ /**
46
+ * Boards reachable from this machine (KJC-TSK-0709): the bundled HU Board
47
+ * always; planning-game / external candidates when their MCP appears in a
48
+ * host config or a conventional token is exported. Presence-based, like
49
+ * verifyBoardAccess — an OPTION list, not a connectivity check.
50
+ */
51
+ export function detectAvailableBoards({ projectDir = process.cwd(), env = process.env, home = os.homedir() } = {}) {
52
+ const boards = [{ value: "hu-board", label: "HU Board (bundled, zero setup)" }];
53
+ if (mcpConfigsMention("planning-game", projectDir, home)) {
54
+ boards.push({ value: "planning-game", label: "Planning Game (MCP detected)" });
55
+ }
56
+ for (const [slug, envs] of Object.entries(TOKEN_CONVENTIONS)) {
57
+ const token = envs.find((k) => env[k]);
58
+ if (token) boards.push({ value: `external:${slug}`, label: `${slug} (token ${token})` });
59
+ else if (mcpConfigsMention(slug, projectDir, home)) boards.push({ value: `external:${slug}`, label: `${slug} (MCP detected)` });
60
+ }
61
+ return boards;
62
+ }
63
+
45
64
  export function verifyBoardAccess({ config = {}, projectDir = process.cwd(), env = process.env, home = os.homedir() } = {}) {
46
65
  const backend = config.state_backend || "hu-board";
47
66
 
@@ -0,0 +1,58 @@
1
+ /**
2
+ * board-select (KJC-TSK-0709) — never hu-board silently when alternatives
3
+ * are reachable. Field case 2026-08-02: an install defaulted to hu-board
4
+ * with the user's Planning Game MCP sitting configured and detectable.
5
+ * Interactive installs ASK and persist the choice; headless installs name
6
+ * the default and the exact line to change it. Declared = never asked.
7
+ */
8
+
9
+ import fs from "node:fs/promises";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { parse as parseYaml, parseDocument } from "yaml";
13
+ import { detectAvailableBoards } from "./board-access.js";
14
+ import { getConfigPath, getProjectConfigPath } from "../config.js";
15
+
16
+ // "Declared" means the USER wrote state_backend in a config FILE — the
17
+ // merged config object always carries the hu-board default (defaults.js),
18
+ // so it can never tell a choice from a fallback.
19
+ async function declaredStateBackend(projectDir) {
20
+ for (const p of [getProjectConfigPath(projectDir), getConfigPath()]) {
21
+ try {
22
+ const raw = parseYaml(await fs.readFile(p, "utf8")) || {};
23
+ if (raw.state_backend) return { backend: raw.state_backend, boardName: raw.board?.name || null };
24
+ } catch { /* missing or unreadable — keep looking */ }
25
+ }
26
+ return null;
27
+ }
28
+
29
+ async function persistStateBackend(projectDir, backend, boardName) {
30
+ const configPath = getProjectConfigPath(projectDir);
31
+ let source = "";
32
+ try { source = await fs.readFile(configPath, "utf8"); } catch { /* no config yet — create it */ }
33
+ const doc = parseDocument(source || "{}");
34
+ doc.setIn(["state_backend"], backend);
35
+ if (boardName) doc.setIn(["board", "name"], boardName);
36
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
37
+ await fs.writeFile(configPath, doc.toString(), "utf8");
38
+ return configPath;
39
+ }
40
+
41
+ export async function pickStateBackend({ projectDir = process.cwd(), home = os.homedir(), env = process.env, wizard = null, isTTY = false, logger = console } = {}) {
42
+ const declared = await declaredStateBackend(projectDir);
43
+ if (declared) return { ...declared, source: "declared" };
44
+ const boards = detectAvailableBoards({ projectDir, home, env });
45
+ if (boards.length <= 1) return { backend: "hu-board", boardName: null, source: "default" };
46
+ if (!wizard || !isTTY) {
47
+ logger.info?.(`⚠ board: defaulting to the bundled HU Board, but this machine can reach: ${boards.slice(1).map((b) => b.label).join(", ")} — switch with \`state_backend: planning-game\` (or \`external\` + board.name) in .karajan/kj.config.yml and re-run kj env install`);
48
+ return { backend: "hu-board", boardName: null, source: "default-informed" };
49
+ }
50
+ logger.info?.("Karajan needs a board (there is no 'none') — pick where card-first work lives:");
51
+ const value = await wizard.select("Which board governs this project?", boards);
52
+ const external = value.startsWith("external:");
53
+ const backend = external ? "external" : value;
54
+ const boardName = external ? value.slice("external:".length) : null;
55
+ await persistStateBackend(projectDir, backend, boardName);
56
+ logger.info?.(`✓ board: ${backend}${boardName ? ` (${boardName})` : ""} — persisted in .karajan/kj.config.yml`);
57
+ return { backend, boardName, source: "chosen" };
58
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * mcp-wiring — RAG as a NATIVE tool (KJC-TSK-0711). Field case 2026-08-03:
3
+ * agents in karajan projects grep code by hand because the RAG is only "a
4
+ * Bash command a text line told them about". Agents use the tools in their
5
+ * toolbox, so env install wires kj's RAG-only MCP server (`kj-rag-mcp`,
6
+ * ships with the package) into the project's `.mcp.json` — the official
7
+ * path must be cheaper than the shortcut. Merge preserves the user's
8
+ * entries; invalid JSON is left untouched (preserve, never clobber).
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ export function installProjectMcp({ projectDir = process.cwd(), logger = console } = {}) {
15
+ const mcpPath = join(projectDir, ".mcp.json");
16
+ let cfg = {};
17
+ if (existsSync(mcpPath)) {
18
+ try {
19
+ cfg = JSON.parse(readFileSync(mcpPath, "utf8"));
20
+ } catch {
21
+ logger.warn?.(`kj env: ${mcpPath} is not valid JSON — leaving it untouched (wire kj-rag-mcp manually for native RAG queries)`);
22
+ return { wired: false, reason: "invalid-json" };
23
+ }
24
+ }
25
+ if (cfg.mcpServers && typeof cfg.mcpServers !== "object") {
26
+ logger.warn?.(`kj env: ${mcpPath} has a non-object mcpServers — leaving it untouched`);
27
+ return { wired: false, reason: "unexpected-shape" };
28
+ }
29
+ cfg.mcpServers = cfg.mcpServers || {};
30
+ // Wired = the LAUNCH SPEC runs kj-rag-mcp (command or an arg, so `npx
31
+ // kj-rag-mcp` and absolute paths count) — never env/description fields.
32
+ const launchesRagMcp = (s) =>
33
+ Boolean(s) && typeof s === "object" && (
34
+ (typeof s.command === "string" && s.command.includes("kj-rag-mcp")) ||
35
+ (Array.isArray(s.args) && s.args.some((a) => typeof a === "string" && a.includes("kj-rag-mcp")))
36
+ );
37
+ const present = Object.values(cfg.mcpServers).some(launchesRagMcp);
38
+ if (!present) {
39
+ cfg.mcpServers["kj-rag"] = { command: "kj-rag-mcp", args: [] };
40
+ writeFileSync(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`);
41
+ logger.info?.("✓ kj_rag_query wired as a NATIVE tool (.mcp.json → kj-rag-mcp): querying the RAG is now the agent's cheapest path");
42
+ }
43
+ return { wired: true, added: !present };
44
+ }
@@ -57,7 +57,8 @@ carries a cross-AI verdict.
57
57
  Invariants (the git gates enforce these — they are not suggestions):
58
58
 
59
59
  - The project RAG answers before you assume: \`kj rag query\` — never guess
60
- what the codebase does.
60
+ what the codebase does. In MCP hosts the same index is the native
61
+ \`kj_rag_query\` tool: reach for it before grepping by hand.
61
62
  - ${trackingLine(stateBackend, boardName)}
62
63
  - Tests prove behavior: the failing test exists first (TDD), and the suite
63
64
  is never left red.