karajan-code 2.34.0 → 3.1.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 +120 -38
- package/bin/kj-rag-mcp.js +75 -0
- package/docs/README.es.md +5 -5
- package/package.json +22 -17
- package/packages/hu-board/package.json +5 -2
- package/packages/hu-board/public/app.js +81 -4025
- package/packages/hu-board/public/index.html +20 -0
- package/packages/hu-board/public/utils/api.js +139 -0
- package/packages/hu-board/public/utils/board-view.js +453 -0
- package/packages/hu-board/public/utils/command-launcher.js +213 -0
- package/packages/hu-board/public/utils/config-editor.js +181 -0
- package/packages/hu-board/public/utils/dashboard-view.js +103 -0
- package/packages/hu-board/public/utils/formatters.js +232 -0
- package/packages/hu-board/public/utils/graph-view.js +216 -0
- package/packages/hu-board/public/utils/hu-actions.js +224 -0
- package/packages/hu-board/public/utils/init-listeners.js +180 -0
- package/packages/hu-board/public/utils/log-panel.js +222 -0
- package/packages/hu-board/public/utils/modals.js +290 -0
- package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
- package/packages/hu-board/public/utils/preflight-view.js +89 -0
- package/packages/hu-board/public/utils/project-actions.js +143 -0
- package/packages/hu-board/public/utils/project-picker-view.js +139 -0
- package/packages/hu-board/public/utils/run-launcher.js +169 -0
- package/packages/hu-board/public/utils/server-push.js +252 -0
- package/packages/hu-board/public/utils/sessions-view.js +199 -0
- package/packages/hu-board/public/utils/story-detail-view.js +324 -0
- package/packages/hu-board/public/utils/story-edit-form.js +271 -0
- package/packages/hu-board/src/config-yaml.js +14 -0
- package/packages/hu-board/src/db.js +15 -2
- package/packages/hu-board/src/routes/api.js +27 -9
- package/scripts/audit-test-diet.mjs +151 -0
- package/src/audit/deterministic-summary.js +20 -0
- package/src/audit/harness-scorecard.js +52 -3
- package/src/audit/harness-section.js +6 -2
- package/src/audit/injection-findings.js +105 -0
- package/src/checks/hardware.js +150 -0
- package/src/checks/node.js +9 -10
- package/src/checks/runner.js +10 -0
- package/src/cli/register-meta.js +48 -2
- package/src/cli/register-plan.js +10 -1
- package/src/commands/clean.js +62 -0
- package/src/commands/doctor.js +2 -0
- package/src/commands/init.js +41 -1
- package/src/commands/plan/generate.js +9 -3
- package/src/commands/rag-mcp.js +18 -0
- package/src/commands/sync.js +30 -2
- package/src/commands/telemetry.js +70 -0
- package/src/config/defaults.js +9 -1
- package/src/config/schema.js +2 -0
- package/src/orchestrator/config-init.js +1 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
- package/src/orchestrator/stages/coder-stage.js +11 -2
- package/src/orchestrator/stages/reviewer-stage.js +11 -0
- package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
- package/src/orchestrator/stages/tool-judge-stage.js +53 -0
- package/src/prompts/tool-correctness-judge.js +31 -0
- package/src/review/git-stash-helper.js +104 -0
- package/src/review/tdd-discipline.js +90 -0
- package/src/roles/audit-role.js +9 -1
- package/src/roles/tool-correctness-judge-role.js +78 -0
- package/src/session/mutators.js +4 -0
- package/src/sonar/scanner.js +51 -16
- package/src/sync/apply-canvas-patch.js +55 -0
- package/src/utils/paths.js +12 -0
- package/src/utils/quota-simulator.js +56 -0
- package/src/utils/repo-cleaner.js +79 -0
- package/src/utils/telemetry.js +34 -12
- package/src/utils/tool-call-extractor.js +56 -0
- package/src/utils/vector-store-orphans.js +89 -0
|
@@ -2,11 +2,44 @@
|
|
|
2
2
|
// One-shot container: detect Docker, auto-pull on first run (cache hit
|
|
3
3
|
// thereafter), execute `assess <repo>` with the project mounted read-only.
|
|
4
4
|
// No compose, no port discovery — assess is a single process that exits.
|
|
5
|
+
//
|
|
6
|
+
// KJC-BUG-0077 — classify failures (docker_missing / image_inaccessible /
|
|
7
|
+
// auth_required / network_error / unknown) and emit an actionable hint
|
|
8
|
+
// alongside the raw stderr so `kj audit` surfaces something better than
|
|
9
|
+
// the opaque "pull access denied" line the registry returns.
|
|
5
10
|
import { runCommand } from "../utils/process.js";
|
|
6
11
|
|
|
7
12
|
const D = { image: "markmishaev76/ai-harness-scorecard:latest", pullTimeoutMs: 300000, assessTimeoutMs: 180000 };
|
|
8
13
|
const pos = (v, d) => (Number(v) > 0 ? Number(v) : d);
|
|
9
14
|
|
|
15
|
+
const DOCKER_FAILURE_PATTERNS = [
|
|
16
|
+
{ reason: "image_inaccessible", re: /pull access denied|repository does not exist|manifest unknown|not found: manifest|denied: requested access/i },
|
|
17
|
+
{ reason: "auth_required", re: /unauthorized|authentication required|login required|requires authentication/i },
|
|
18
|
+
{ reason: "network_error", re: /no such host|connection refused|dial tcp|i\/o timeout|TLS handshake|net\/http|temporary failure in name resolution/i },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function classifyDockerFailure(stderr = "") {
|
|
22
|
+
const s = String(stderr);
|
|
23
|
+
for (const { reason, re } of DOCKER_FAILURE_PATTERNS) {
|
|
24
|
+
if (re.test(s)) return reason;
|
|
25
|
+
}
|
|
26
|
+
return "unknown";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hintForReason(reason, image) {
|
|
30
|
+
switch (reason) {
|
|
31
|
+
case "image_inaccessible":
|
|
32
|
+
return `Image \`${image}\` is not accessible from the public registry (it may have been removed, renamed, or made private). Either run \`kj audit --no-harness\` to skip this step, or override \`audit.harness.image\` with a mirror you control.`;
|
|
33
|
+
case "auth_required":
|
|
34
|
+
return `Pulling \`${image}\` requires registry authentication. Run \`docker login <registry>\` first, or override \`audit.harness.image\` with a public mirror.`;
|
|
35
|
+
case "network_error":
|
|
36
|
+
return `Network error reaching the registry while pulling \`${image}\`. Retry once connectivity is back; \`kj audit --no-harness\` skips this step in the meantime.`;
|
|
37
|
+
case "docker_missing":
|
|
38
|
+
return `Docker is not available on this machine. Install Docker (or run \`kj audit --no-harness\` to silence this).`;
|
|
39
|
+
default: return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
10
43
|
export function normalizeHarnessConfig(h = {}) {
|
|
11
44
|
return {
|
|
12
45
|
enabled: h.enabled !== false,
|
|
@@ -32,20 +65,36 @@ export async function ensureHarnessImage(harness = null) {
|
|
|
32
65
|
const cfg = normalizeHarnessConfig(harness || {});
|
|
33
66
|
if (await isHarnessImagePresent(cfg.image)) return { pulled: false, reused: true, image: cfg.image };
|
|
34
67
|
const r = await runCommand("docker", ["pull", cfg.image], { timeout: cfg.pullTimeoutMs });
|
|
35
|
-
if (r.exitCode !== 0)
|
|
68
|
+
if (r.exitCode !== 0) {
|
|
69
|
+
const stderr = r.stderr?.trim() || `exit ${r.exitCode}`;
|
|
70
|
+
const reason = classifyDockerFailure(stderr);
|
|
71
|
+
return { pulled: false, reused: false, image: cfg.image, error: stderr, reason };
|
|
72
|
+
}
|
|
36
73
|
return { pulled: true, reused: false, image: cfg.image };
|
|
37
74
|
}
|
|
38
75
|
|
|
39
76
|
export async function runHarnessAssess(repoPath, harness = null) {
|
|
40
77
|
const cfg = normalizeHarnessConfig(harness || {});
|
|
78
|
+
if (!(await isDockerAvailable())) {
|
|
79
|
+
return { ok: false, reason: "docker_missing", image: cfg.image, error: "Docker daemon is not available", hint: hintForReason("docker_missing", cfg.image) };
|
|
80
|
+
}
|
|
81
|
+
const pull = await ensureHarnessImage(cfg);
|
|
82
|
+
if (pull.error) {
|
|
83
|
+
const reason = pull.reason || "pull_failed";
|
|
84
|
+
return { ok: false, reason, image: cfg.image, error: pull.error, hint: hintForReason(reason, cfg.image) };
|
|
85
|
+
}
|
|
41
86
|
const r = await runCommand(
|
|
42
87
|
"docker",
|
|
43
88
|
["run", "--rm", "-v", `${repoPath}:/repo:ro`, cfg.image, "assess", "/repo", "--format", "json"],
|
|
44
89
|
{ timeout: cfg.assessTimeoutMs },
|
|
45
90
|
);
|
|
46
|
-
if (r.exitCode !== 0)
|
|
91
|
+
if (r.exitCode !== 0) {
|
|
92
|
+
const stderr = r.stderr?.trim() || r.stdout?.trim() || `exit ${r.exitCode}`;
|
|
93
|
+
const reason = classifyDockerFailure(stderr);
|
|
94
|
+
return { ok: false, reason, image: cfg.image, error: stderr, hint: hintForReason(reason, cfg.image) };
|
|
95
|
+
}
|
|
47
96
|
try {
|
|
48
97
|
const parsed = JSON.parse(r.stdout);
|
|
49
98
|
return { ok: true, score: parsed.overall_score, grade: parsed.grade, raw: parsed };
|
|
50
|
-
} catch (e) { return { ok: false, error: `Invalid JSON from harness: ${e.message}` }; }
|
|
99
|
+
} catch (e) { return { ok: false, reason: "invalid_json", error: `Invalid JSON from harness: ${e.message}` }; }
|
|
51
100
|
}
|
|
@@ -22,7 +22,7 @@ export async function runHarnessSection({ projectDir, harnessConfig = null, runA
|
|
|
22
22
|
const cfg = normalizeHarnessConfig(harnessConfig || {});
|
|
23
23
|
if (!cfg.enabled) return { ok: true, skipped: true, reason: "disabled" };
|
|
24
24
|
const res = await runAssess(projectDir, cfg);
|
|
25
|
-
if (!res.ok) return { ok: false, error: res.error };
|
|
25
|
+
if (!res.ok) return { ok: false, error: res.error, reason: res.reason, hint: res.hint, image: cfg.image };
|
|
26
26
|
const outDir = path.join(projectDir, ".karajan", "audit-runs", ts());
|
|
27
27
|
await fs.mkdir(outDir, { recursive: true });
|
|
28
28
|
const scorecardPath = path.join(outDir, "scorecard.json");
|
|
@@ -39,7 +39,11 @@ export async function runHarnessSection({ projectDir, harnessConfig = null, runA
|
|
|
39
39
|
|
|
40
40
|
export function formatHarnessSection(summary) {
|
|
41
41
|
if (!summary || summary.skipped) return "";
|
|
42
|
-
if (!summary.ok)
|
|
42
|
+
if (!summary.ok) {
|
|
43
|
+
const reason = summary.reason ? ` (${summary.reason})` : "";
|
|
44
|
+
const hint = summary.hint ? `\n\n${summary.hint}\n` : "";
|
|
45
|
+
return `## Harness Scorecard\n\n_skipped${reason}: ${summary.error}_${hint}\n`;
|
|
46
|
+
}
|
|
43
47
|
const lines = [
|
|
44
48
|
`## Harness Health Score: ${summary.score}/100 (${summary.grade})`,
|
|
45
49
|
"",
|
|
@@ -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/checks/runner.js
CHANGED
|
@@ -249,8 +249,15 @@ function summarize(reports) {
|
|
|
249
249
|
*
|
|
250
250
|
* @example setDotPath({}, "git.auto_pr", false) // → { git: { auto_pr: false } }
|
|
251
251
|
*/
|
|
252
|
+
// KJC-BUG-0076: prototype-pollution guard. dot-paths come from check
|
|
253
|
+
// remediation outputs (internal but LLM-generated), so a malformed
|
|
254
|
+
// `__proto__.x` would mutate Object.prototype. Skip the whole write
|
|
255
|
+
// when any segment matches a forbidden key.
|
|
256
|
+
const UNSAFE_DOT_KEY_RE = /^(?:__proto__|constructor|prototype)$/;
|
|
257
|
+
|
|
252
258
|
function setDotPath(target, dotPath, value) {
|
|
253
259
|
const parts = String(dotPath).split(".");
|
|
260
|
+
if (parts.some((p) => UNSAFE_DOT_KEY_RE.test(p))) return;
|
|
254
261
|
let cursor = target;
|
|
255
262
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
256
263
|
const k = parts[i];
|
|
@@ -314,3 +321,6 @@ export function toLegacyShape(runReport) {
|
|
|
314
321
|
fix: c.fix ?? null,
|
|
315
322
|
}));
|
|
316
323
|
}
|
|
324
|
+
|
|
325
|
+
// Exposed for prototype-pollution regression tests (KJC-BUG-0076).
|
|
326
|
+
export const __pollutionInternals = { setDotPath };
|
package/src/cli/register-meta.js
CHANGED
|
@@ -4,6 +4,7 @@ import { researcherCommand } from "../commands/researcher.js";
|
|
|
4
4
|
import { architectCommand } from "../commands/architect.js";
|
|
5
5
|
import { onboardCommand } from "../commands/onboard.js";
|
|
6
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
|
/**
|
|
@@ -160,6 +162,16 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
160
162
|
});
|
|
161
163
|
});
|
|
162
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
|
+
});
|
|
174
|
+
|
|
163
175
|
// KJC-TSK-0441 — `kj watch [start|stop|status]` for live RAG re-index.
|
|
164
176
|
const watch = program.command("watch").description("Live RAG re-index daemon");
|
|
165
177
|
watch.command("start").description("Start the watcher daemon").option("--foreground", "Run in foreground (do not detach)").option("--with-sources", "Watch project source files too").action(async (flags) => {
|
|
@@ -301,12 +313,34 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
301
313
|
|
|
302
314
|
program
|
|
303
315
|
.command("sync")
|
|
304
|
-
.description("Detect drift between code and the latest plan. Read-only
|
|
316
|
+
.description("Detect drift between code and the latest plan. Read-only by default; --apply writes a patch back to the Canvas.")
|
|
305
317
|
.option("--plan <planId>", "Sync against a specific plan instead of the latest")
|
|
306
318
|
.option("--json", "Emit machine-readable JSON instead of human output")
|
|
319
|
+
.option("--apply", "Write the drift report into the source Canvas (creates a .bak backup)")
|
|
307
320
|
.action(async (flags) => {
|
|
308
321
|
await withConfig(pkgVersion, "sync", flags, async ({ config, logger }) => {
|
|
309
|
-
await syncCommand({ config, logger, planId: flags.plan, json: flags.json });
|
|
322
|
+
await syncCommand({ config, logger, planId: flags.plan, json: flags.json, apply: Boolean(flags.apply) });
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// KJC-TSK-0496 — surface the exact telemetry payload without sending it,
|
|
327
|
+
// and report whether telemetry is currently enabled. Read-only inspection
|
|
328
|
+
// commands so users can audit the contract without grepping the source.
|
|
329
|
+
const telemetry = program.command("telemetry").description("Inspect what Karajan telemetry sends and whether it's enabled (never opens a socket)");
|
|
330
|
+
telemetry.command("preview")
|
|
331
|
+
.description("Print the exact JSON payload for every event type (install, cli_command, pipeline_complete) — no network call")
|
|
332
|
+
.option("--json", "Emit the full preview as JSON")
|
|
333
|
+
.action(async (flags) => {
|
|
334
|
+
await withConfig(pkgVersion, "telemetry:preview", flags, async ({ config, logger }) => {
|
|
335
|
+
await telemetryPreviewCommand({ config, logger, version: pkgVersion, json: Boolean(flags.json) });
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
telemetry.command("status")
|
|
339
|
+
.description("Report whether telemetry is enabled and where the endpoint lives")
|
|
340
|
+
.option("--json", "Emit the status as JSON")
|
|
341
|
+
.action(async (flags) => {
|
|
342
|
+
await withConfig(pkgVersion, "telemetry:status", flags, async ({ config, logger }) => {
|
|
343
|
+
await telemetryStatusCommand({ config, logger, json: Boolean(flags.json) });
|
|
310
344
|
});
|
|
311
345
|
});
|
|
312
346
|
|
|
@@ -319,10 +353,22 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
319
353
|
.option("--draft-days <n>", "Keep draft plans for N days (default: 60)")
|
|
320
354
|
.option("--session-days <n>", "Keep finalised sessions for N days (default: 7)")
|
|
321
355
|
.option("--hu-days <n>", "Keep HU story batches for N days (default: 14)")
|
|
356
|
+
.option("--repo", "Also report repo-level candidates (merged branches, dist/, coverage/, *.tmp, *.bak)")
|
|
357
|
+
.option("--repo-days <n>", "Age threshold for repo artifacts in days (default: 7)")
|
|
358
|
+
.option("--repo-base <ref>", "Base ref to detect merged branches (default: origin/main)")
|
|
359
|
+
.option("--vector-stores", "Also report orphan RAG vector-store entries in ~/.karajan/rag.db")
|
|
360
|
+
.option("--project-roots <list>", "Colon-separated directories to scan for live projects (default: ~/ws_*, ~/projects, ~/code)")
|
|
361
|
+
.option("--all", "Shortcut for --repo --vector-stores (full read-only cleanup audit)")
|
|
322
362
|
.action(async (flags) => {
|
|
363
|
+
const allOn = Boolean(flags.all);
|
|
323
364
|
await cleanCommand({
|
|
324
365
|
yes: Boolean(flags.yes),
|
|
325
366
|
nuke: Boolean(flags.nuke),
|
|
367
|
+
repo: Boolean(flags.repo) || allOn,
|
|
368
|
+
repoDays: flags.repoDays ? Number(flags.repoDays) : undefined,
|
|
369
|
+
repoBase: flags.repoBase || undefined,
|
|
370
|
+
vectorStores: Boolean(flags.vectorStores) || allOn,
|
|
371
|
+
projectRoots: flags.projectRoots ? flags.projectRoots.split(":").filter(Boolean) : undefined,
|
|
326
372
|
planDays: flags.planDays ? Number(flags.planDays) : undefined,
|
|
327
373
|
draftDays: flags.draftDays ? Number(flags.draftDays) : undefined,
|
|
328
374
|
sessionDays: flags.sessionDays ? Number(flags.sessionDays) : undefined,
|
package/src/cli/register-plan.js
CHANGED
|
@@ -93,7 +93,16 @@ export function registerPlan(program, { pkgVersion }) {
|
|
|
93
93
|
// Norms + Safeguards ride along under explicit headings the
|
|
94
94
|
// coder prompt can grep for.
|
|
95
95
|
const taskFromCanvas = canvasToTask(validation.canvas);
|
|
96
|
-
|
|
96
|
+
// KJC-TSK-0348: remember WHERE the Canvas lived on disk so
|
|
97
|
+
// `kj sync --apply` later knows which file to patch.
|
|
98
|
+
let sourceCanvasPath = null;
|
|
99
|
+
if (typeof flags?.taskFile === "string" && flags.taskFile.length > 0) {
|
|
100
|
+
const path = await import("node:path");
|
|
101
|
+
sourceCanvasPath = path.isAbsolute(flags.taskFile)
|
|
102
|
+
? flags.taskFile
|
|
103
|
+
: path.resolve(config.projectDir || process.cwd(), flags.taskFile);
|
|
104
|
+
}
|
|
105
|
+
await planGenerateCommand({ task: taskFromCanvas, config, logger, flags, sourceCanvasPath });
|
|
97
106
|
return;
|
|
98
107
|
}
|
|
99
108
|
await planGenerateCommand({
|
package/src/commands/clean.js
CHANGED
|
@@ -19,11 +19,18 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { runManualGC, summarizeGC, nukeBoardDb } from "../utils/garbage-collector.js";
|
|
22
|
+
import { collectRepoCandidates } from "../utils/repo-cleaner.js";
|
|
23
|
+
import { collectVectorStoreOrphans } from "../utils/vector-store-orphans.js";
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* @param {Object} opts
|
|
25
27
|
* @param {boolean} [opts.yes=false] - when false, dry-run only
|
|
26
28
|
* @param {boolean} [opts.nuke=false] - retention=0 for every bucket + wipe board DB
|
|
29
|
+
* @param {boolean} [opts.repo=false] - also report repo-level candidates (branches, dist, *.tmp, *.bak)
|
|
30
|
+
* @param {number} [opts.repoDays=7] - age threshold for repo artifacts (default 7d)
|
|
31
|
+
* @param {string} [opts.repoBase="origin/main"]
|
|
32
|
+
* @param {boolean} [opts.vectorStores=false] - also report orphan RAG vector-store entries
|
|
33
|
+
* @param {string[]} [opts.projectRoots] - directories scanned to resolve slugs (default: ~/ws_*, ~/projects, ~/code)
|
|
27
34
|
* @param {number} [opts.planDays=30]
|
|
28
35
|
* @param {number} [opts.draftDays=60]
|
|
29
36
|
* @param {number} [opts.sessionDays=7]
|
|
@@ -79,5 +86,60 @@ export async function cleanCommand(opts = {}) {
|
|
|
79
86
|
console.log(summarizeGC(result));
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
if (opts.repo) await printRepoReport(opts);
|
|
90
|
+
if (opts.vectorStores) await printVectorStoreReport(opts);
|
|
91
|
+
|
|
82
92
|
return { ok: true, removed: result.removed.length, bytesFreed: result.bytesFreed };
|
|
83
93
|
}
|
|
94
|
+
|
|
95
|
+
async function printRepoReport(opts) {
|
|
96
|
+
const { candidates, errors } = await collectRepoCandidates({
|
|
97
|
+
maxAgeDays: opts.repoDays ?? 7,
|
|
98
|
+
mergedTo: opts.repoBase ?? "origin/main",
|
|
99
|
+
});
|
|
100
|
+
console.log("");
|
|
101
|
+
if (!candidates.length) {
|
|
102
|
+
console.log("[clean:repo] no repo-level candidates — branches/dist/tmp are tidy.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(`[clean:repo] ${candidates.length} candidate(s) — review and run the command to remove:`);
|
|
106
|
+
for (const c of candidates) {
|
|
107
|
+
console.log(` - ${c.kind.padEnd(11)} ${c.target}`);
|
|
108
|
+
console.log(` ${c.reason}`);
|
|
109
|
+
console.log(` $ ${c.command}`);
|
|
110
|
+
}
|
|
111
|
+
if (errors.length) {
|
|
112
|
+
console.log(`[clean:repo] ${errors.length} non-blocking error(s):`);
|
|
113
|
+
for (const e of errors) console.log(` - ${e.stage}: ${e.error}`);
|
|
114
|
+
}
|
|
115
|
+
console.log("[clean:repo] read-only — commands are printed, never executed.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function printVectorStoreReport(opts) {
|
|
119
|
+
const { rows, orphans, errors } = await collectVectorStoreOrphans({
|
|
120
|
+
projectRoots: opts.projectRoots,
|
|
121
|
+
});
|
|
122
|
+
console.log("");
|
|
123
|
+
if (errors.length && !rows.length) {
|
|
124
|
+
console.log("[clean:rag] could not read rag.db — RAG may not be initialised yet.");
|
|
125
|
+
for (const e of errors) console.log(` - ${e.stage}: ${e.error}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log(`[clean:rag] ${rows.length} project_slug(s) indexed in rag.db:`);
|
|
129
|
+
for (const r of rows) {
|
|
130
|
+
const tag = r.orphan ? " ORPHAN" : "";
|
|
131
|
+
const when = r.lastAt || "n/a";
|
|
132
|
+
console.log(` - ${r.slug.padEnd(28)} chunks=${String(r.chunkCount).padStart(5)} last=${when}${tag}`);
|
|
133
|
+
}
|
|
134
|
+
if (!orphans.length) {
|
|
135
|
+
console.log("[clean:rag] no orphans — every indexed slug resolves to a live project directory.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log(`[clean:rag] ${orphans.length} orphan(s) — review and run to purge:`);
|
|
140
|
+
for (const o of orphans) {
|
|
141
|
+
console.log(` - ${o.slug}`);
|
|
142
|
+
console.log(` $ ${o.command}`);
|
|
143
|
+
}
|
|
144
|
+
console.log("[clean:rag] read-only — commands are printed, never executed.");
|
|
145
|
+
}
|
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(),
|