karajan-code 2.33.0 → 3.0.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/README.md +136 -36
- package/bin/kj-rag-mcp.js +75 -0
- package/docs/README.es.md +4 -4
- package/package.json +22 -16
- package/packages/hu-board/package.json +5 -2
- package/scripts/git-hooks/post-merge +7 -0
- package/src/audit/basal-cost.js +17 -12
- package/src/audit/deterministic-summary.js +20 -0
- package/src/audit/injection-findings.js +105 -0
- package/src/checks/hardware.js +150 -0
- package/src/checks/node.js +9 -10
- package/src/cli/register-meta.js +57 -1
- package/src/cli/register-pipeline.js +1 -0
- package/src/commands/doctor.js +2 -0
- package/src/commands/init.js +41 -1
- package/src/commands/rag-mcp.js +18 -0
- package/src/commands/rag.js +82 -8
- package/src/commands/run.js +8 -0
- package/src/commands/telemetry.js +70 -0
- package/src/config/defaults.js +11 -1
- package/src/lang/chunk-go.js +94 -0
- package/src/lang/chunk-java.js +90 -0
- package/src/lang/chunk-python.js +88 -0
- package/src/lang/chunk-rust.js +102 -0
- package/src/lang/go.js +14 -0
- package/src/lang/java.js +16 -0
- package/src/lang/js.js +8 -0
- package/src/lang/python.js +14 -0
- package/src/lang/registry.js +79 -0
- package/src/lang/rust.js +14 -0
- package/src/lang/tree-sitter-loader.js +49 -0
- package/src/onboarder/collectors/framework-multilang.js +64 -0
- package/src/onboarder/collectors/index.js +18 -1
- package/src/orchestrator/stages/coder-stage.js +3 -0
- package/src/orchestrator/stages/reviewer-stage.js +11 -0
- package/src/rag/auto-update.js +50 -0
- package/src/rag/chunker.js +7 -3
- package/src/rag/eval.js +61 -0
- package/src/rag/indexer.js +81 -10
- package/src/rag/retriever.js +57 -5
- package/src/rag/vec-store.js +69 -3
- package/src/rag/watcher.js +6 -4
- package/src/roles/audit-role.js +9 -1
- package/src/sonar/scanner.js +51 -16
- package/src/utils/manifest-sniff.js +82 -0
- package/src/utils/quota-simulator.js +56 -0
- package/src/utils/telemetry.js +34 -12
- package/templates/roles/onboarder.md +10 -1
- package/vendor/tree-sitter-grammars/README.md +21 -0
- package/vendor/tree-sitter-grammars/go.wasm +0 -0
- package/vendor/tree-sitter-grammars/java.wasm +0 -0
- package/vendor/tree-sitter-grammars/python.wasm +0 -0
- package/vendor/tree-sitter-grammars/rust.wasm +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-injection findings collector for `kj audit` (KJC-TSK-0498).
|
|
3
|
+
* Scans `.karajan/specs/**`, `.karajan/domain.md` and
|
|
4
|
+
* `.karajan/onboarding/**` reusing `src/utils/injection-guard.js`.
|
|
5
|
+
* Best-effort: missing folders return `scanned: 0`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { scanForInjection } from "../utils/injection-guard.js";
|
|
11
|
+
|
|
12
|
+
const MAX_FILE_BYTES = 256 * 1024;
|
|
13
|
+
const MAX_DEPTH = 6;
|
|
14
|
+
const SEVERITY_BY_TYPE = { directive: "HIGH", unicode: "MEDIUM", comment_block: "LOW" };
|
|
15
|
+
|
|
16
|
+
const SCAN_TARGETS = [
|
|
17
|
+
{ rel: ".karajan/specs", kind: "dir" },
|
|
18
|
+
{ rel: ".karajan/onboarding", kind: "dir" },
|
|
19
|
+
{ rel: ".karajan/domain.md", kind: "file" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export async function collectInjectionFindings(rootDir, logger = null) {
|
|
23
|
+
if (!rootDir) return { available: false, reason: "rootDir not provided" };
|
|
24
|
+
const findings = [];
|
|
25
|
+
let scanned = 0;
|
|
26
|
+
try {
|
|
27
|
+
for (const target of SCAN_TARGETS) {
|
|
28
|
+
const abs = path.join(rootDir, target.rel);
|
|
29
|
+
const files = target.kind === "dir"
|
|
30
|
+
? await listFiles(abs, 0)
|
|
31
|
+
: (await fileExists(abs)) ? [abs] : [];
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
scanned += 1;
|
|
34
|
+
const content = await safeRead(file);
|
|
35
|
+
if (!content) continue;
|
|
36
|
+
const result = scanForInjection(content);
|
|
37
|
+
if (result.clean) continue;
|
|
38
|
+
for (const f of result.findings) {
|
|
39
|
+
findings.push({
|
|
40
|
+
file: path.relative(rootDir, file),
|
|
41
|
+
type: f.type,
|
|
42
|
+
pattern: f.pattern,
|
|
43
|
+
snippet: f.snippet,
|
|
44
|
+
line: f.line,
|
|
45
|
+
severity: SEVERITY_BY_TYPE[f.type] || "LOW",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (logger?.warn) logger.warn(`injection-findings collector failed: ${err.message}`);
|
|
52
|
+
return { available: false, reason: `collector failed: ${err.message}` };
|
|
53
|
+
}
|
|
54
|
+
return { available: true, scanned, total: findings.length, findings };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function groupInjectionBySeverity(findings) {
|
|
58
|
+
const groups = { HIGH: [], MEDIUM: [], LOW: [] };
|
|
59
|
+
for (const f of findings || []) {
|
|
60
|
+
const sev = (f.severity || "LOW").toUpperCase();
|
|
61
|
+
if (groups[sev]) groups[sev].push(f);
|
|
62
|
+
else groups.LOW.push(f);
|
|
63
|
+
}
|
|
64
|
+
return groups;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function listFiles(dir, depth) {
|
|
68
|
+
if (depth > MAX_DEPTH) return [];
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const abs = path.join(dir, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
const nested = await listFiles(abs, depth + 1);
|
|
80
|
+
out.push(...nested);
|
|
81
|
+
} else if (entry.isFile()) {
|
|
82
|
+
out.push(abs);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function fileExists(file) {
|
|
89
|
+
try {
|
|
90
|
+
const s = await stat(file);
|
|
91
|
+
return s.isFile();
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function safeRead(file) {
|
|
98
|
+
try {
|
|
99
|
+
const s = await stat(file);
|
|
100
|
+
if (s.size > MAX_FILE_BYTES) return null;
|
|
101
|
+
return await readFile(file, "utf8");
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Advisory hardware checks: warn (not fail) so the pipeline keeps running.
|
|
2
|
+
// RAM < 4 GB silently OOM-kills the RAG embedder; disk < 5 GB corrupts
|
|
3
|
+
// Ollama model shards. GPU detection is best-effort and never throws.
|
|
4
|
+
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { statfs } from "node:fs/promises";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { STRATEGY } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const RAM_WARN_GB = 4;
|
|
11
|
+
const RAM_RECOMMENDED_GB = 8;
|
|
12
|
+
const DISK_WARN_GB = 5;
|
|
13
|
+
const DISK_RECOMMENDED_GB = 20;
|
|
14
|
+
const GPU_DETECT_TIMEOUT_MS = 1500;
|
|
15
|
+
|
|
16
|
+
const bytesToGb = (bytes) => Math.round((bytes / 1024 ** 3) * 10) / 10;
|
|
17
|
+
const info = (detail, extra) => ({ ok: true, severity: "info", detail, extra });
|
|
18
|
+
const warn = (detail, fix, extra) => ({ ok: false, severity: "warn", detail, fix, extra });
|
|
19
|
+
|
|
20
|
+
function createRamCheck() {
|
|
21
|
+
return {
|
|
22
|
+
name: "hw-ram",
|
|
23
|
+
label: "System RAM",
|
|
24
|
+
strategy: STRATEGY.MANUAL,
|
|
25
|
+
async detect() {
|
|
26
|
+
const totalGb = bytesToGb(os.totalmem());
|
|
27
|
+
const freeGb = bytesToGb(os.freemem());
|
|
28
|
+
const base = `${totalGb} GB total, ${freeGb} GB free`;
|
|
29
|
+
if (totalGb < RAM_WARN_GB) {
|
|
30
|
+
return warn(
|
|
31
|
+
`${base} (RAG embedder needs >=${RAM_WARN_GB} GB)`,
|
|
32
|
+
"Disable local RAG (kj init --no-rag) or switch rag.embedder.provider to openai/voyage/cohere.",
|
|
33
|
+
{ totalGb, freeGb, threshold: RAM_WARN_GB }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (totalGb < RAM_RECOMMENDED_GB) {
|
|
37
|
+
return info(`${base} (>=${RAM_RECOMMENDED_GB} GB recommended for parallel runs)`, { totalGb, freeGb });
|
|
38
|
+
}
|
|
39
|
+
return info(base, { totalGb, freeGb });
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createCpuCheck() {
|
|
45
|
+
return {
|
|
46
|
+
name: "hw-cpu",
|
|
47
|
+
label: "CPU",
|
|
48
|
+
strategy: STRATEGY.NONE,
|
|
49
|
+
async detect() {
|
|
50
|
+
const cpus = os.cpus();
|
|
51
|
+
const model = cpus[0]?.model?.trim() || "unknown";
|
|
52
|
+
const cores = cpus.length;
|
|
53
|
+
const arch = os.arch();
|
|
54
|
+
return info(`${cores} cores, ${arch} (${model})`, { cores, arch, model });
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createDiskCheck() {
|
|
60
|
+
return {
|
|
61
|
+
name: "hw-disk",
|
|
62
|
+
label: "Disk space (cwd)",
|
|
63
|
+
strategy: STRATEGY.MANUAL,
|
|
64
|
+
async detect() {
|
|
65
|
+
try {
|
|
66
|
+
const stat = await statfs(process.cwd());
|
|
67
|
+
const freeGb = bytesToGb(stat.bsize * stat.bavail);
|
|
68
|
+
if (freeGb < DISK_WARN_GB) {
|
|
69
|
+
return warn(
|
|
70
|
+
`${freeGb} GB free (>=${DISK_WARN_GB} GB recommended)`,
|
|
71
|
+
"Free disk space. Ollama models alone take 3-5 GB; the RAG store grows ~50 MB per 1k LOC indexed.",
|
|
72
|
+
{ freeGb, threshold: DISK_WARN_GB }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const note = freeGb < DISK_RECOMMENDED_GB ? ` (>=${DISK_RECOMMENDED_GB} GB recommended)` : "";
|
|
76
|
+
return info(`${freeGb} GB free${note}`, { freeGb });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return info(`Cannot read free disk space: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function runGpuCommand(cmd, args) {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
let stdout = "";
|
|
87
|
+
let settled = false;
|
|
88
|
+
const finish = (value) => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true;
|
|
91
|
+
resolve(value);
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
try { child.kill("SIGKILL"); } catch { /* noop */ }
|
|
97
|
+
finish(null);
|
|
98
|
+
}, GPU_DETECT_TIMEOUT_MS);
|
|
99
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
100
|
+
child.on("error", () => { clearTimeout(timer); finish(null); });
|
|
101
|
+
child.on("close", (code) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
finish(code === 0 ? stdout : null);
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
finish(null);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function detectGpu() {
|
|
112
|
+
const platform = process.platform;
|
|
113
|
+
if (platform === "linux" || platform === "win32") {
|
|
114
|
+
const out = await runGpuCommand("nvidia-smi", ["--query-gpu=name", "--format=csv,noheader"]);
|
|
115
|
+
const model = out?.split("\n").map((l) => l.trim()).find(Boolean);
|
|
116
|
+
if (model) return { vendor: "NVIDIA", model };
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (platform === "darwin") {
|
|
120
|
+
const out = await runGpuCommand("system_profiler", ["SPDisplaysDataType"]);
|
|
121
|
+
if (!out) return null;
|
|
122
|
+
const match = out.match(/Chipset Model:\s*(.+)/);
|
|
123
|
+
if (!match) return null;
|
|
124
|
+
const model = match[1].trim();
|
|
125
|
+
const vendor = /Apple/i.test(model) ? "Apple" : /AMD|Radeon/i.test(model) ? "AMD" : /Intel/i.test(model) ? "Intel" : "GPU";
|
|
126
|
+
return { vendor, model };
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createGpuCheck() {
|
|
132
|
+
return {
|
|
133
|
+
name: "hw-gpu",
|
|
134
|
+
label: "GPU",
|
|
135
|
+
strategy: STRATEGY.NONE,
|
|
136
|
+
async detect() {
|
|
137
|
+
const gpu = await detectGpu();
|
|
138
|
+
if (!gpu) {
|
|
139
|
+
return info("No discrete GPU detected (CPU-only)", { detected: false });
|
|
140
|
+
}
|
|
141
|
+
return info(`${gpu.vendor} ${gpu.model}`, { detected: true, vendor: gpu.vendor, model: gpu.model });
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getHardwareChecks() {
|
|
147
|
+
return [createRamCheck(), createCpuCheck(), createDiskCheck(), createGpuCheck()];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const __test = { RAM_WARN_GB, RAM_RECOMMENDED_GB, DISK_WARN_GB, DISK_RECOMMENDED_GB, GPU_DETECT_TIMEOUT_MS, bytesToGb, detectGpu };
|
package/src/checks/node.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node.js runtime version check. Requires Node >=
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* runtime.
|
|
10
|
-
* hints instead of a silent runtime crash.
|
|
2
|
+
* Node.js runtime version check. Requires Node >= 22 — the Active LTS
|
|
3
|
+
* baseline since Karajan v3.0.0 (KJC-TSK-0500, 2026-06-03). Node 20 was
|
|
4
|
+
* dropped because (a) Node 20 reaches EOL on 2026-04-30 and (b) three
|
|
5
|
+
* dependencies forced the bump: lint-staged 17 needs ≥22.22.1, commander
|
|
6
|
+
* 15 needs ≥22.12.0, and better-sqlite3 v12.10+ dropped Node 20 prebuilds.
|
|
7
|
+
* Strategy: manual — we cannot auto-upgrade the runtime. Users on older
|
|
8
|
+
* Node get a clear upgrade instruction with nvm + brew hints instead of a
|
|
9
|
+
* silent runtime crash.
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
12
|
import { STRATEGY } from "./types.js";
|
|
14
13
|
|
|
15
|
-
export const MIN_NODE_MAJOR =
|
|
14
|
+
export const MIN_NODE_MAJOR = 22;
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Accepts a version string in the form "v20.12.1" or "20.12.1" and returns
|
package/src/cli/register-meta.js
CHANGED
|
@@ -3,7 +3,8 @@ import { triageCommand } from "../commands/triage.js";
|
|
|
3
3
|
import { researcherCommand } from "../commands/researcher.js";
|
|
4
4
|
import { architectCommand } from "../commands/architect.js";
|
|
5
5
|
import { onboardCommand } from "../commands/onboard.js";
|
|
6
|
-
import { ragIndexCommand, ragQueryCommand } from "../commands/rag.js";
|
|
6
|
+
import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand } from "../commands/rag.js";
|
|
7
|
+
import { ragMcpCommand } from "../commands/rag-mcp.js";
|
|
7
8
|
import { watchStartCommand, watchStopCommand, watchStatusCommand } from "../commands/watch.js";
|
|
8
9
|
import { auditCommand } from "../commands/audit.js";
|
|
9
10
|
import { resumeCommand } from "../commands/resume.js";
|
|
@@ -12,6 +13,7 @@ import { webperfCommand } from "../commands/webperf.js";
|
|
|
12
13
|
import { undoCommand } from "../commands/undo.js";
|
|
13
14
|
import { syncCommand } from "../commands/sync.js";
|
|
14
15
|
import { cleanCommand } from "../commands/clean.js";
|
|
16
|
+
import { telemetryPreviewCommand, telemetryStatusCommand } from "../commands/telemetry.js";
|
|
15
17
|
import { withConfig } from "./_shared.js";
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -104,12 +106,21 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
104
106
|
rag.command("index")
|
|
105
107
|
.description("Index plans + onboarding (and optionally project sources) into the local vector store")
|
|
106
108
|
.option("--with-sources", "Also index the projectDir's JS/TS files")
|
|
109
|
+
.option("--since <ref>", "Only re-index files changed since <ref> ('auto' uses the last indexed commit; falls back to full index if unset). KJC-TSK-0455.")
|
|
107
110
|
.option("--json", "Output the totals as JSON")
|
|
108
111
|
.action(async (flags) => {
|
|
109
112
|
await withConfig(pkgVersion, "rag-index", flags, async ({ config, logger }) => {
|
|
110
113
|
await ragIndexCommand({ config, logger, flags });
|
|
111
114
|
});
|
|
112
115
|
});
|
|
116
|
+
rag.command("install-hooks")
|
|
117
|
+
.description("Install the post-merge git hook so the RAG index auto-refreshes after pulls/merges (KJC-TSK-0455)")
|
|
118
|
+
.option("--json", "Output the result as JSON")
|
|
119
|
+
.action(async (flags) => {
|
|
120
|
+
await withConfig(pkgVersion, "rag-install-hooks", flags, async ({ config, logger }) => {
|
|
121
|
+
await ragInstallHooksCommand({ config, logger, flags });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
113
124
|
rag.command("query <text>")
|
|
114
125
|
.description("Run a semantic query against the indexed RAG corpus")
|
|
115
126
|
.option("--scope <scope>", "plans | code | onboarding | all (default: all)", "all")
|
|
@@ -136,6 +147,30 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
136
147
|
await ragQueryCommand({ text, config, logger, flags: { ...flags, mode: ragMode } });
|
|
137
148
|
});
|
|
138
149
|
});
|
|
150
|
+
// KJC-TSK-0483 — `kj rag eval` runs the golden-query harness and reports
|
|
151
|
+
// recall@5/recall@10/MRR. `--min-recall` turns it into a CI gate.
|
|
152
|
+
rag.command("eval")
|
|
153
|
+
.description("Score the retriever against a golden query set (recall@k + MRR)")
|
|
154
|
+
.option("--golden <path>", "Path to golden queries JSON (default: tests/rag/golden-queries.json)")
|
|
155
|
+
.option("--top-k <n>", "Number of hits to retrieve per query (default: 10)", "10")
|
|
156
|
+
.option("--min-recall <n>", "Fail (exit 1) if aggregated recall@5 falls below this threshold")
|
|
157
|
+
.option("--project <slug>", "Filter by project slug. Pass 'all' to query across every indexed project.")
|
|
158
|
+
.option("--json", "Emit the full report as JSON")
|
|
159
|
+
.action(async (flags) => {
|
|
160
|
+
await withConfig(pkgVersion, "rag-eval", flags, async ({ config, logger }) => {
|
|
161
|
+
await ragEvalCommand({ config, logger, flags });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// KJC-TSK-0492 PR3 — `kj rag mcp` exposes the kj-rag-mcp binary under the
|
|
166
|
+
// familiar `kj rag <subcommand>` surface so users discover it without
|
|
167
|
+
// grepping bin/. The action dynamic-imports the binary, which takes over
|
|
168
|
+
// stdio for MCP transport; the CLI process never returns.
|
|
169
|
+
rag.command("mcp")
|
|
170
|
+
.description("Start the standalone RAG MCP server (kj_rag_query + kj_rag_index only). Same as running `kj-rag-mcp` directly")
|
|
171
|
+
.action(async () => {
|
|
172
|
+
await ragMcpCommand();
|
|
173
|
+
});
|
|
139
174
|
|
|
140
175
|
// KJC-TSK-0441 — `kj watch [start|stop|status]` for live RAG re-index.
|
|
141
176
|
const watch = program.command("watch").description("Live RAG re-index daemon");
|
|
@@ -287,6 +322,27 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
287
322
|
});
|
|
288
323
|
});
|
|
289
324
|
|
|
325
|
+
// KJC-TSK-0496 — surface the exact telemetry payload without sending it,
|
|
326
|
+
// and report whether telemetry is currently enabled. Read-only inspection
|
|
327
|
+
// commands so users can audit the contract without grepping the source.
|
|
328
|
+
const telemetry = program.command("telemetry").description("Inspect what Karajan telemetry sends and whether it's enabled (never opens a socket)");
|
|
329
|
+
telemetry.command("preview")
|
|
330
|
+
.description("Print the exact JSON payload for every event type (install, cli_command, pipeline_complete) — no network call")
|
|
331
|
+
.option("--json", "Emit the full preview as JSON")
|
|
332
|
+
.action(async (flags) => {
|
|
333
|
+
await withConfig(pkgVersion, "telemetry:preview", flags, async ({ config, logger }) => {
|
|
334
|
+
await telemetryPreviewCommand({ config, logger, version: pkgVersion, json: Boolean(flags.json) });
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
telemetry.command("status")
|
|
338
|
+
.description("Report whether telemetry is enabled and where the endpoint lives")
|
|
339
|
+
.option("--json", "Emit the status as JSON")
|
|
340
|
+
.action(async (flags) => {
|
|
341
|
+
await withConfig(pkgVersion, "telemetry:status", flags, async ({ config, logger }) => {
|
|
342
|
+
await telemetryStatusCommand({ config, logger, json: Boolean(flags.json) });
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
290
346
|
program
|
|
291
347
|
.command("clean")
|
|
292
348
|
.description("Garbage-collect stale plans, sessions and HU batches (dry-run by default)")
|
|
@@ -110,6 +110,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
110
110
|
.option("--skip-role <role...>", "Force one or more roles OFF regardless of triage (e.g. --skip-role tester security)")
|
|
111
111
|
.option("--force-role <role...>", "Force one or more roles ON regardless of triage")
|
|
112
112
|
.option("--skip-spec-review", "Bypass the spec-reviewer pre-pipeline audit")
|
|
113
|
+
.option("--no-rag-update", "Skip the pre-run RAG drift check (KJC-TSK-0455). Same as config.rag.autoUpdate.onRun=false.")
|
|
113
114
|
.option("--dry-run", "Show what would be executed without running anything")
|
|
114
115
|
.option("--json", "Output JSON only (no styled display)")
|
|
115
116
|
.option("-q, --quiet", "Show only stage status lines, suppress raw agent output (default)")
|
package/src/commands/doctor.js
CHANGED
|
@@ -29,6 +29,7 @@ import { getMcpHealthChecks } from "../checks/mcp-health.js";
|
|
|
29
29
|
import { getSkillsChecks } from "../checks/skills.js";
|
|
30
30
|
import { getDirSetupChecks } from "../checks/dir-setup.js";
|
|
31
31
|
import { getProjectChecks } from "../checks/project-checks.js";
|
|
32
|
+
import { getHardwareChecks } from "../checks/hardware.js";
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Build the list of Check objects applicable to the current config.
|
|
@@ -50,6 +51,7 @@ function buildChecks(config, { projectOnly = false } = {}) {
|
|
|
50
51
|
return [
|
|
51
52
|
...getSystemChecks(),
|
|
52
53
|
...getNodeChecks(),
|
|
54
|
+
...getHardwareChecks(),
|
|
53
55
|
...getDirSetupChecks(),
|
|
54
56
|
...getConfigFileChecks(),
|
|
55
57
|
...getBinaryChecks(),
|
package/src/commands/init.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createWizard, isTTY } from "../utils/wizard.js";
|
|
|
12
12
|
import { runCommand } from "../utils/process.js";
|
|
13
13
|
import { getInstallCommand } from "../utils/os-detect.js";
|
|
14
14
|
import { detectOsLocale, SUPPORTED_LANGUAGES } from "../utils/locale.js";
|
|
15
|
+
import { buildTelemetryPayload, sendTelemetryEvent } from "../utils/telemetry.js";
|
|
15
16
|
import { detectRtk } from "../utils/rtk-detect.js";
|
|
16
17
|
import { installRtk } from "../utils/rtk-install.js";
|
|
17
18
|
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
@@ -256,6 +257,42 @@ async function askBoardSecurity(wizard, config, logger) {
|
|
|
256
257
|
logger.info(` -> HU Board port: ${config.hu_board.port}`);
|
|
257
258
|
}
|
|
258
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Explicit, opt-in telemetry consent. We do NOT inherit a "Karajan-installed
|
|
262
|
+
* users want to help" assumption — silence and missing config keys both mean
|
|
263
|
+
* "no". The prompt is rendered in the OS locale (EN/ES today; falls back to
|
|
264
|
+
* EN for everything else) so users see it in their own language even before
|
|
265
|
+
* they've set `config.language`. See src/utils/telemetry.js and the
|
|
266
|
+
* "Privacy & Telemetry" section of README.md for what gets sent.
|
|
267
|
+
*/
|
|
268
|
+
async function askTelemetry(wizard, config, logger) {
|
|
269
|
+
const locale = detectOsLocale();
|
|
270
|
+
const prompts = {
|
|
271
|
+
es: {
|
|
272
|
+
question: "¿Quieres ayudar a mejorar Karajan enviando telemetría anónima al proyecto?",
|
|
273
|
+
detail: " (versión, OS, comandos, duración del pipeline — sin código, sin tareas, sin datos personales)",
|
|
274
|
+
previewIntro: " Ejemplo del payload exacto que se enviaría al ejecutar `kj run` (puedes inspeccionar más con `kj telemetry preview`):",
|
|
275
|
+
enabled: " -> Telemetría: activada (¡gracias!)",
|
|
276
|
+
disabled: " -> Telemetría: desactivada",
|
|
277
|
+
},
|
|
278
|
+
en: {
|
|
279
|
+
question: "Help improve Karajan by sending anonymous telemetry to the project?",
|
|
280
|
+
detail: " (version, OS, commands, pipeline duration — no code, no tasks, no personal data)",
|
|
281
|
+
previewIntro: " Example of the exact payload that would be sent when you run `kj run` (inspect more with `kj telemetry preview`):",
|
|
282
|
+
enabled: " -> Telemetry: enabled (thank you!)",
|
|
283
|
+
disabled: " -> Telemetry: disabled",
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
const t = prompts[locale] || prompts.en;
|
|
287
|
+
logger.info(t.detail);
|
|
288
|
+
const example = buildTelemetryPayload("cli_command", { version: "x.y.z", command: "run" });
|
|
289
|
+
logger.info(t.previewIntro);
|
|
290
|
+
logger.info(` ${JSON.stringify(example)}`);
|
|
291
|
+
const enabled = await wizard.confirm(t.question, false);
|
|
292
|
+
config.telemetry = enabled;
|
|
293
|
+
logger.info(enabled ? t.enabled : t.disabled);
|
|
294
|
+
}
|
|
295
|
+
|
|
259
296
|
async function runWizard(config, logger) {
|
|
260
297
|
const agents = await detectAvailableAgents();
|
|
261
298
|
const available = agents.filter((a) => a.available);
|
|
@@ -318,6 +355,10 @@ async function runWizard(config, logger) {
|
|
|
318
355
|
await askBoardSecurity(wizard, config, logger);
|
|
319
356
|
}
|
|
320
357
|
|
|
358
|
+
logger.info("");
|
|
359
|
+
logger.info("Privacy:");
|
|
360
|
+
await askTelemetry(wizard, config, logger);
|
|
361
|
+
|
|
321
362
|
logger.info("");
|
|
322
363
|
} finally {
|
|
323
364
|
wizard.close();
|
|
@@ -735,7 +776,6 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
735
776
|
await writeInitConfig(configPath, config);
|
|
736
777
|
|
|
737
778
|
// Telemetry: anonymous install event (non-blocking)
|
|
738
|
-
const { sendTelemetryEvent } = await import("../utils/telemetry.js");
|
|
739
779
|
const { readFileSync } = await import("node:fs");
|
|
740
780
|
const { fileURLToPath } = await import("node:url");
|
|
741
781
|
try {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `kj rag mcp` — spawn the standalone RAG MCP server inline.
|
|
7
|
+
*
|
|
8
|
+
* KJC-TSK-0492 PR3. Adds CLI discoverability for the `kj-rag-mcp` binary
|
|
9
|
+
* so external IDE users can find it via the same `kj rag <subcommand>`
|
|
10
|
+
* surface they already know. The implementation dynamic-imports the
|
|
11
|
+
* binary entry so behavior stays identical to running `kj-rag-mcp`
|
|
12
|
+
* directly — same handlers, same transport, same orphan guard.
|
|
13
|
+
*/
|
|
14
|
+
export async function ragMcpCommand() {
|
|
15
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const binPath = path.resolve(moduleDir, "../../bin/kj-rag-mcp.js");
|
|
17
|
+
await import(pathToFileURL(binPath).href);
|
|
18
|
+
}
|
package/src/commands/rag.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// kj rag index [--project <slug>] [--with-sources]
|
|
3
3
|
// kj rag query <text> [--scope plans|code|onboarding|all] [--top-k N] [--json]
|
|
4
4
|
// Closes the v2.22.0 RAG MVP end-to-end from the terminal.
|
|
5
|
-
import { openVecStore, countChunks, projectSlug } from "../rag/vec-store.js";
|
|
5
|
+
import { openVecStore, countChunks, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "../rag/vec-store.js";
|
|
6
6
|
import { makeEmbedder } from "../rag/embedders/factory.js";
|
|
7
|
-
import { indexProject } from "../rag/indexer.js";
|
|
7
|
+
import { indexProject, indexProjectDelta } from "../rag/indexer.js";
|
|
8
8
|
import { query } from "../rag/retriever.js";
|
|
9
|
+
import { installPostMergeHook } from "../rag/auto-update.js";
|
|
10
|
+
import { loadGoldenQueries, runEval } from "../rag/eval.js";
|
|
9
11
|
import { getKarajanHome } from "../utils/paths.js";
|
|
10
12
|
|
|
11
13
|
function openDb(config) {
|
|
@@ -16,15 +18,39 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
16
18
|
const projectDir = config?.projectDir || process.cwd();
|
|
17
19
|
const db = openDb(config);
|
|
18
20
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const slug = projectSlug(projectDir);
|
|
22
|
+
const embedder = makeEmbedder(config);
|
|
23
|
+
const sinceFlag = flags.since;
|
|
24
|
+
// KJC-TSK-0455 — `--since auto` resolves to the last commit we indexed;
|
|
25
|
+
// an explicit ref is honoured as-is. Without a baseline (first-time
|
|
26
|
+
// index) we fall back to a full reindex with a friendly warning so the
|
|
27
|
+
// hook in PR2 stays a no-op-friendly entrypoint.
|
|
28
|
+
let since = null;
|
|
29
|
+
if (sinceFlag) {
|
|
30
|
+
since = sinceFlag === "auto" ? getLastIndexedCommit(db, slug) : sinceFlag;
|
|
31
|
+
if (!since) logger.warn?.("[rag] --since auto: no previous index recorded; running full index");
|
|
32
|
+
}
|
|
33
|
+
let totals;
|
|
34
|
+
if (since) {
|
|
35
|
+
try {
|
|
36
|
+
totals = await indexProjectDelta(projectDir, { db, embedder, since, logger });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
logger.warn?.(`[rag] delta index failed (${err.message}); falling back to full index`);
|
|
39
|
+
totals = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!totals) {
|
|
43
|
+
totals = await indexProject(projectDir, {
|
|
44
|
+
db, embedder,
|
|
45
|
+
karajanHome: getKarajanHome(), logger,
|
|
46
|
+
withSources: Boolean(flags.withSources),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
24
50
|
if (flags.json) {
|
|
25
51
|
process.stdout.write(`${JSON.stringify(totals)}\n`);
|
|
26
52
|
} else {
|
|
27
|
-
logger.info(`[rag] indexed ${totals.indexed} chunk(s) across ${totals.files} file(s) (${totals.failed} failed)`);
|
|
53
|
+
logger.info(`[rag] indexed ${totals.indexed} chunk(s) across ${totals.files} file(s) (${totals.failed} failed${totals.deleted ? `, ${totals.deleted} chunks deleted` : ""})`);
|
|
28
54
|
}
|
|
29
55
|
return totals;
|
|
30
56
|
} finally {
|
|
@@ -32,6 +58,18 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
32
58
|
}
|
|
33
59
|
}
|
|
34
60
|
|
|
61
|
+
// KJC-TSK-0455 — `kj rag install-hooks` wires the post-merge hook so the
|
|
62
|
+
// local index keeps up after `git pull` / merges. Opt-in to keep us from
|
|
63
|
+
// silently writing under `.git/hooks/`; pre-run drift check covers users
|
|
64
|
+
// who never run it.
|
|
65
|
+
export async function ragInstallHooksCommand({ config, logger, flags = {} }) {
|
|
66
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
67
|
+
const res = installPostMergeHook({ projectDir, logger });
|
|
68
|
+
if (flags.json) process.stdout.write(`${JSON.stringify(res)}\n`);
|
|
69
|
+
else if (res.installed) logger.info(`[rag] installed post-merge hook at ${res.target}`);
|
|
70
|
+
return res;
|
|
71
|
+
}
|
|
72
|
+
|
|
35
73
|
export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
36
74
|
if (!text) throw new Error("kj rag query: text argument required");
|
|
37
75
|
const db = openDb(config);
|
|
@@ -75,3 +113,39 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
75
113
|
db.close();
|
|
76
114
|
}
|
|
77
115
|
}
|
|
116
|
+
|
|
117
|
+
// KJC-TSK-0483 — `kj rag eval`. Runs the retriever against a golden query
|
|
118
|
+
// set and reports recall@5, recall@10 and MRR per query and aggregated.
|
|
119
|
+
// Exit code != 0 when `--min-recall` is set and the aggregated recall@5
|
|
120
|
+
// falls below the threshold — lets CI fail noisily on retrieval regressions.
|
|
121
|
+
const DEFAULT_GOLDEN = "tests/rag/golden-queries.json";
|
|
122
|
+
export async function ragEvalCommand({ config, logger, flags = {} }) {
|
|
123
|
+
const db = openDb(config);
|
|
124
|
+
try {
|
|
125
|
+
const goldenPath = flags.golden || DEFAULT_GOLDEN;
|
|
126
|
+
const queries = loadGoldenQueries(goldenPath);
|
|
127
|
+
const topK = Math.max(1, Number(flags.topK) || 10);
|
|
128
|
+
const detected = projectSlug(config?.projectDir || process.cwd());
|
|
129
|
+
const project = flags.project === "all" ? null : (flags.project || detected || null);
|
|
130
|
+
const embedder = makeEmbedder(config);
|
|
131
|
+
const runQuery = async (text, k) => query(db, embedder, text, { topK: k, scope: "all", project });
|
|
132
|
+
const report = await runEval(queries, runQuery, { topK, ks: [5, 10] });
|
|
133
|
+
if (flags.json) {
|
|
134
|
+
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
135
|
+
} else {
|
|
136
|
+
logger.info(`[rag-eval] ${report.aggregate.n} queries · recall@5=${report.aggregate.recall["@5"].toFixed(3)} · recall@10=${report.aggregate.recall["@10"].toFixed(3)} · MRR=${report.aggregate.mrr.toFixed(3)}`);
|
|
137
|
+
for (const r of report.results) {
|
|
138
|
+
const rank = r.firstRank == null ? "—" : `#${r.firstRank}`;
|
|
139
|
+
logger.info(` [${rank}] ${r.query}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const minRecall = Number(flags.minRecall);
|
|
143
|
+
if (Number.isFinite(minRecall) && report.aggregate.recall["@5"] < minRecall) {
|
|
144
|
+
logger.error?.(`[rag-eval] recall@5=${report.aggregate.recall["@5"].toFixed(3)} below --min-recall=${minRecall}`);
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
}
|
|
147
|
+
return report;
|
|
148
|
+
} finally {
|
|
149
|
+
db.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/commands/run.js
CHANGED
|
@@ -12,6 +12,7 @@ import { resolveRole } from "../config.js";
|
|
|
12
12
|
import { parseCardId } from "../planning-game/adapter.js";
|
|
13
13
|
import { confirmCwd } from "../utils/cwd-confirm.js";
|
|
14
14
|
import { runSpecReview } from "../spec-review/run-spec-review.js";
|
|
15
|
+
import { maybeAutoUpdate } from "../rag/auto-update.js";
|
|
15
16
|
|
|
16
17
|
function createCliAskQuestion(opts = {}) {
|
|
17
18
|
const { sessionId = null } = opts;
|
|
@@ -84,6 +85,13 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// KJC-TSK-0455 — Pre-run drift check. Default ON; opt out with
|
|
89
|
+
// --no-rag-update (flag) or config.rag.autoUpdate.onRun=false. If the
|
|
90
|
+
// local index is behind HEAD we re-embed the delta before the agents
|
|
91
|
+
// start reading from the RAG store; on any failure we log and move on
|
|
92
|
+
// so a broken vector store never blocks a run.
|
|
93
|
+
await maybeAutoUpdate({ projectDir, config, logger, flags });
|
|
94
|
+
|
|
87
95
|
// Best-effort session cleanup before starting
|
|
88
96
|
try {
|
|
89
97
|
const { cleanupExpiredSessions } = await import("../session-cleanup.js");
|