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
package/src/commands/env.js
CHANGED
|
@@ -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
|
package/src/config/schema.js
CHANGED
|
@@ -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
|
-
|
|
242
|
-
|
|
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.
|