karajan-code 3.0.0 → 3.2.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 +14 -6
- package/docs/README.es.md +1 -1
- package/package.json +8 -2
- package/packages/hu-board/package.json +3 -1
- package/packages/hu-board/public/app.js +81 -4025
- package/packages/hu-board/public/index.html +21 -0
- package/packages/hu-board/public/styles.css +32 -0
- package/packages/hu-board/public/utils/api.js +139 -0
- package/packages/hu-board/public/utils/board-view.js +462 -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 +273 -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/public/wiki.html +32 -0
- package/packages/hu-board/public/wiki.js +53 -0
- package/packages/hu-board/src/config-yaml.js +14 -0
- package/packages/hu-board/src/db.js +93 -2
- package/packages/hu-board/src/format.js +79 -0
- package/packages/hu-board/src/plan-mutations.js +4 -4
- package/packages/hu-board/src/routes/api.js +43 -10
- package/packages/hu-board/src/routes/rag.js +1 -1
- package/packages/hu-board/src/routes/wiki.js +43 -0
- package/packages/hu-board/src/run-tracker.js +1 -1
- package/packages/hu-board/src/server.js +4 -2
- package/packages/hu-board/src/sync.js +1 -1
- package/scripts/audit-test-diet.mjs +151 -0
- package/scripts/git-hooks/post-merge +13 -2
- package/src/audit/ai-slop-findings.js +102 -0
- package/src/audit/deterministic-summary.js +18 -0
- package/src/audit/harness-scorecard.js +52 -3
- package/src/audit/harness-section.js +6 -2
- package/src/brain/standby-scheduler.js +10 -92
- package/src/brain/standby-store.js +12 -168
- package/src/checks/ai-trash.js +34 -0
- package/src/checks/qmd.js +41 -0
- package/src/checks/runner.js +10 -0
- package/src/checks/squeezr.js +36 -0
- package/src/cli/register-meta.js +36 -2
- package/src/cli/register-pipeline.js +3 -0
- package/src/cli/register-plan.js +10 -1
- package/src/commands/audit.js +6 -4
- package/src/commands/board.js +34 -0
- package/src/commands/clean.js +62 -0
- package/src/commands/doctor.js +6 -0
- package/src/commands/init.js +73 -8
- package/src/commands/plan/generate.js +9 -3
- package/src/commands/qmd.js +86 -0
- package/src/commands/resume.js +6 -45
- package/src/commands/run.js +6 -50
- package/src/commands/sync.js +30 -2
- package/src/config/defaults.js +5 -0
- package/src/config/schema.js +2 -0
- package/src/git/hu-snapshot.js +10 -105
- package/src/orchestrator/config-init.js +1 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +16 -0
- package/src/orchestrator/drivers/run-hu-batch.js +5 -1
- package/src/orchestrator/hu-sub-pipeline.js +52 -8
- package/src/orchestrator/stages/coder-stage.js +8 -2
- package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
- package/src/orchestrator/stages/tool-judge-stage.js +53 -0
- package/src/plan/plan-hu-ops.js +16 -338
- package/src/plan/plan-id.js +4 -45
- package/src/plan/plan-validation.js +4 -82
- package/src/prompts/tool-correctness-judge.js +31 -0
- package/src/rag/vec-store.js +17 -253
- package/src/review/git-stash-helper.js +104 -0
- package/src/review/tdd-discipline.js +90 -0
- package/src/roles/audit-role.js +11 -1
- package/src/roles/tool-correctness-judge-role.js +78 -0
- package/src/session/mutators.js +4 -0
- package/src/sync/apply-canvas-patch.js +55 -0
- package/src/utils/ai-trash.js +48 -0
- package/src/utils/atomic-write.js +6 -43
- package/src/utils/cli-ask-question.js +76 -0
- package/src/utils/doc-links.js +2 -0
- package/src/utils/os-detect.js +10 -0
- package/src/utils/paths.js +14 -111
- package/src/utils/port-check.js +6 -45
- package/src/utils/process.js +4 -188
- package/src/utils/qmd-collection.js +72 -0
- package/src/utils/qmd-detect.js +18 -0
- package/src/utils/qmd-install.js +37 -0
- package/src/utils/repo-cleaner.js +79 -0
- package/src/utils/run-registry.js +9 -116
- package/src/utils/shared-paths.js +12 -41
- package/src/utils/squeezr-detect.js +18 -0
- package/src/utils/squeezr-install.js +37 -0
- package/src/utils/tool-call-extractor.js +56 -0
- package/src/utils/vector-store-orphans.js +89 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runCommand } from "./process.js";
|
|
2
|
+
import { getInstallCommand } from "./os-detect.js";
|
|
3
|
+
import { detectQmd } from "./qmd-detect.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Install QMD using the OS-appropriate command.
|
|
7
|
+
* @param {object} logger - Logger with info/warn methods
|
|
8
|
+
* @returns {Promise<{ ok: boolean, version: string|null, error: string|null }>}
|
|
9
|
+
*/
|
|
10
|
+
export async function installQmd(logger) {
|
|
11
|
+
const command = getInstallCommand("qmd");
|
|
12
|
+
|
|
13
|
+
logger.info("Installing QMD...");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = await runCommand("sh", ["-c", command], { timeout: 120_000 });
|
|
17
|
+
|
|
18
|
+
if (result.exitCode !== 0) {
|
|
19
|
+
const error = (result.stderr || "").trim() || `exit code ${result.exitCode}`;
|
|
20
|
+
logger.warn(`QMD install failed: ${error}`);
|
|
21
|
+
return { ok: false, version: null, error };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const check = await detectQmd();
|
|
25
|
+
if (check.available) {
|
|
26
|
+
logger.info(`QMD ${check.version || ""} installed successfully.`);
|
|
27
|
+
return { ok: true, version: check.version, error: null };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
logger.warn("QMD install command succeeded but qmd binary not found in PATH.");
|
|
31
|
+
return { ok: false, version: null, error: "Binary not found after install" };
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const error = err.message || String(err);
|
|
34
|
+
logger.warn(`QMD install failed: ${error}`);
|
|
35
|
+
return { ok: false, version: null, error };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level cleaner — KJC-TSK-0499 PR1. Read-only collector that
|
|
3
|
+
* surfaces stale local artifacts so the user can decide what to drop.
|
|
4
|
+
* Detects: branches merged into a base ref, branches with `[gone]`
|
|
5
|
+
* upstream, top-level `dist/` `coverage/` `logs/` older than N days,
|
|
6
|
+
* `*.tmp` / `*.bak` older than N days. Never spawns destructive ops.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { execa } from "execa";
|
|
11
|
+
|
|
12
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const STALE_DIRS = ["dist", "coverage", "logs"];
|
|
14
|
+
const STALE_FILE_RX = /\.(tmp|bak)$/;
|
|
15
|
+
|
|
16
|
+
async function safeGit(cwd, args) {
|
|
17
|
+
try {
|
|
18
|
+
const { stdout, exitCode } = await execa("git", args, { cwd, reject: false });
|
|
19
|
+
return exitCode === 0 ? stdout : "";
|
|
20
|
+
} catch { return ""; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function collectBranches(cwd, baseRef) {
|
|
24
|
+
const items = [];
|
|
25
|
+
const baseShort = baseRef.replace(/^origin\//, "");
|
|
26
|
+
const current = (await safeGit(cwd, ["symbolic-ref", "--short", "HEAD"])).trim();
|
|
27
|
+
const merged = await safeGit(cwd, ["branch", "--merged", baseRef, "--format=%(refname:short)"]);
|
|
28
|
+
for (const raw of merged.split("\n")) {
|
|
29
|
+
const name = raw.trim();
|
|
30
|
+
if (!name || name === current || name === baseShort) continue;
|
|
31
|
+
items.push({ kind: "branch", target: name, reason: `merged into ${baseRef}`, command: `git branch -d ${name}` });
|
|
32
|
+
}
|
|
33
|
+
for (const line of (await safeGit(cwd, ["branch", "-vv"])).split("\n")) {
|
|
34
|
+
const m = line.match(/^[\s*]+(\S+)\s+\S+\s+\[[^:]+: gone\]/);
|
|
35
|
+
if (m && m[1] !== current) items.push({ kind: "branch-gone", target: m[1], reason: "upstream gone", command: `git branch -D ${m[1]}` });
|
|
36
|
+
}
|
|
37
|
+
return items;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function ageMs(p) {
|
|
41
|
+
try { return Date.now() - (await fs.stat(p)).mtimeMs; } catch { return -1; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function collectStale(cwd, maxAgeDays) {
|
|
45
|
+
const cutoff = maxAgeDays * DAY_MS;
|
|
46
|
+
const items = [];
|
|
47
|
+
for (const name of STALE_DIRS) {
|
|
48
|
+
const full = path.join(cwd, name);
|
|
49
|
+
if (await ageMs(full) > cutoff) {
|
|
50
|
+
items.push({ kind: "dir", target: full, reason: `${name}/ older than ${maxAgeDays}d`, command: `rm -rf ${full}` });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let entries = [];
|
|
54
|
+
try { entries = await fs.readdir(cwd, { withFileTypes: true }); } catch { /* cwd unreadable */ }
|
|
55
|
+
for (const e of entries) {
|
|
56
|
+
if (!e.isFile() || !STALE_FILE_RX.test(e.name)) continue;
|
|
57
|
+
const full = path.join(cwd, e.name);
|
|
58
|
+
if (await ageMs(full) > cutoff) {
|
|
59
|
+
items.push({ kind: "file", target: full, reason: `${path.extname(e.name)} older than ${maxAgeDays}d`, command: `rm -f ${full}` });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return items;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Discover repo-level cleanup candidates.
|
|
67
|
+
* @param {object} opts
|
|
68
|
+
* @param {string} [opts.cwd=process.cwd()]
|
|
69
|
+
* @param {number} [opts.maxAgeDays=7]
|
|
70
|
+
* @param {string} [opts.mergedTo="origin/main"]
|
|
71
|
+
* @returns {Promise<{candidates: Array, errors: Array}>}
|
|
72
|
+
*/
|
|
73
|
+
export async function collectRepoCandidates({ cwd = process.cwd(), maxAgeDays = 7, mergedTo = "origin/main" } = {}) {
|
|
74
|
+
const candidates = [];
|
|
75
|
+
const errors = [];
|
|
76
|
+
try { candidates.push(...await collectBranches(cwd, mergedTo)); } catch (e) { errors.push({ stage: "branches", error: String(e?.message ?? e) }); }
|
|
77
|
+
try { candidates.push(...await collectStale(cwd, maxAgeDays)); } catch (e) { errors.push({ stage: "stale", error: String(e?.message ?? e) }); }
|
|
78
|
+
return { candidates, errors };
|
|
79
|
+
}
|
|
@@ -1,116 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* El board lee ese directorio en startup + cada vez que sirve el endpoint
|
|
11
|
-
* `/api/runs/:planId/active`, descartando los registros con PID muerto
|
|
12
|
-
* (filtrado con `process.kill(pid, 0)`). Resultado: bidireccionalidad
|
|
13
|
-
* total terminal ↔ board.
|
|
14
|
-
*
|
|
15
|
-
* Compartido entre el CLI (src/commands/run.js) y el board
|
|
16
|
-
* (packages/hu-board/src/run-tracker.js). Vive en src/utils/ porque
|
|
17
|
-
* el board ya importa cosas de src/ sin problema.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import fs from "node:fs";
|
|
21
|
-
import path from "node:path";
|
|
22
|
-
import { writeJsonAtomicSync } from "./atomic-write.js";
|
|
23
|
-
import { getRunsDir } from "./paths.js";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Directorio raíz donde viven los registros de run.
|
|
27
|
-
* Delega en `getRunsDir()` (KJC-TSK-0420) — respeta KARAJAN_HOME,
|
|
28
|
-
* KJ_HOME (con warning de deprecación), VITEST tmp y default.
|
|
29
|
-
*/
|
|
30
|
-
export function runsDir() {
|
|
31
|
-
return getRunsDir();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Registra un nuevo run en disco. El runId es derivado del PID + timestamp
|
|
36
|
-
* para ser único por proceso.
|
|
37
|
-
*
|
|
38
|
-
* @param {{ pid: number, planId?: string|null, huIds?: string[]|null, projectDir?: string|null, source?: string }} info
|
|
39
|
-
* @returns {string|null} runId o null si la escritura falló (no bloquea al caller).
|
|
40
|
-
*/
|
|
41
|
-
export function registerRun(info = {}) {
|
|
42
|
-
if (!info?.pid) return null;
|
|
43
|
-
try {
|
|
44
|
-
fs.mkdirSync(runsDir(), { recursive: true });
|
|
45
|
-
// Sufijo random para evitar colisiones cuando 2 runs del mismo PID
|
|
46
|
-
// se registran en el mismo ms (raro en CLI, posible en tests).
|
|
47
|
-
const runId = `run-${Date.now()}-${info.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
48
|
-
const filePath = path.join(runsDir(), `${runId}.json`);
|
|
49
|
-
const entry = {
|
|
50
|
-
runId,
|
|
51
|
-
pid: info.pid,
|
|
52
|
-
planId: info.planId || null,
|
|
53
|
-
huIds: Array.isArray(info.huIds) ? info.huIds : null,
|
|
54
|
-
projectDir: info.projectDir || null,
|
|
55
|
-
source: info.source || "cli",
|
|
56
|
-
startedAt: new Date().toISOString(),
|
|
57
|
-
};
|
|
58
|
-
writeJsonAtomicSync(filePath, entry);
|
|
59
|
-
return runId;
|
|
60
|
-
} catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Borra el registro de un run. Idempotente — silencia ENOENT.
|
|
67
|
-
*
|
|
68
|
-
* @param {string} runId
|
|
69
|
-
*/
|
|
70
|
-
export function unregisterRun(runId) {
|
|
71
|
-
if (!runId) return;
|
|
72
|
-
try {
|
|
73
|
-
fs.unlinkSync(path.join(runsDir(), `${runId}.json`));
|
|
74
|
-
} catch {
|
|
75
|
-
// Idempotente
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Lista todos los runs activos en disco, filtrando los con PID muerto.
|
|
81
|
-
* Los muertos se borran del FS (auto-cleanup defensivo).
|
|
82
|
-
*
|
|
83
|
-
* @param {{ planId?: string }} [filter] - si se pasa planId, devuelve solo los runs de ese plan.
|
|
84
|
-
* @returns {Array<{runId, pid, planId, huIds, projectDir, source, startedAt}>}
|
|
85
|
-
*/
|
|
86
|
-
export function listActiveRuns(filter = {}) {
|
|
87
|
-
const dir = runsDir();
|
|
88
|
-
if (!fs.existsSync(dir)) return [];
|
|
89
|
-
const out = [];
|
|
90
|
-
let files;
|
|
91
|
-
try { files = fs.readdirSync(dir); } catch { return []; }
|
|
92
|
-
for (const f of files) {
|
|
93
|
-
if (!f.endsWith(".json")) continue;
|
|
94
|
-
const filePath = path.join(dir, f);
|
|
95
|
-
let entry;
|
|
96
|
-
try { entry = JSON.parse(fs.readFileSync(filePath, "utf8")); }
|
|
97
|
-
catch { try { fs.unlinkSync(filePath); } catch { /* swallow */ } continue; }
|
|
98
|
-
if (!isAlive(entry.pid)) {
|
|
99
|
-
try { fs.unlinkSync(filePath); } catch { /* swallow */ }
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (filter.planId && entry.planId !== filter.planId) continue;
|
|
103
|
-
out.push(entry);
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Verifica si un PID sigue vivo. `process.kill(pid, 0)` NO envía
|
|
110
|
-
* señal — solo lanza ESRCH si el proceso no existe.
|
|
111
|
-
*/
|
|
112
|
-
function isAlive(pid) {
|
|
113
|
-
if (!pid) return false;
|
|
114
|
-
try { process.kill(pid, 0); return true; }
|
|
115
|
-
catch (err) { return err.code === "EPERM"; }
|
|
116
|
-
}
|
|
1
|
+
// Shim: run-registry now lives in @karajan/core/run-registry so the
|
|
2
|
+
// hu-board workspace can consume it without a relative dep on the CLI
|
|
3
|
+
// src tree. KJC-TSK-0511 PR3.
|
|
4
|
+
export {
|
|
5
|
+
runsDir,
|
|
6
|
+
registerRun,
|
|
7
|
+
unregisterRun,
|
|
8
|
+
listActiveRuns,
|
|
9
|
+
} from "@karajan/core/run-registry";
|
|
@@ -1,41 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* (`packages/hu-board/src/sync.js`) — diverging the constant breaks the
|
|
14
|
-
* watcher silently. One source of truth lives here.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
|
|
19
|
-
export const SHARED_DIR_NAME = ".karajan-shared";
|
|
20
|
-
|
|
21
|
-
export function getSharedRoot(projectDir) {
|
|
22
|
-
return path.join(projectDir, SHARED_DIR_NAME);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function getSharedPlansDir(projectDir) {
|
|
26
|
-
return path.join(getSharedRoot(projectDir), "plans");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function getSharedHuStoriesDir(projectDir) {
|
|
30
|
-
return path.join(getSharedRoot(projectDir), "hu-stories");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Quick check used by callers that decide whether a plan came from local
|
|
35
|
-
* (`~/.karajan/plans/`) or from the shared dir. The HU Board uses this to
|
|
36
|
-
* stamp a `source` column on each record so the UI can render a 🌐 badge.
|
|
37
|
-
*/
|
|
38
|
-
export function isSharedPath(p) {
|
|
39
|
-
if (!p) return false;
|
|
40
|
-
return p.split(path.sep).includes(SHARED_DIR_NAME);
|
|
41
|
-
}
|
|
1
|
+
// Shim — the real implementation now lives in @karajan/core/shared-paths.
|
|
2
|
+
// Kept here so CLI callers (src/**/*.js) keep importing
|
|
3
|
+
// "../utils/shared-paths.js" without churn during KJC-TSK-0511 (decouple
|
|
4
|
+
// @karajan/hu-board). Will be retired once every consumer migrates to
|
|
5
|
+
// the @karajan/core import.
|
|
6
|
+
export {
|
|
7
|
+
SHARED_DIR_NAME,
|
|
8
|
+
getSharedRoot,
|
|
9
|
+
getSharedPlansDir,
|
|
10
|
+
getSharedHuStoriesDir,
|
|
11
|
+
isSharedPath,
|
|
12
|
+
} from "@karajan/core/shared-paths";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runCommand } from "./process.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect whether Squeezr is installed and available.
|
|
5
|
+
* @returns {Promise<{ available: boolean, version: string|null }>}
|
|
6
|
+
*/
|
|
7
|
+
export async function detectSqueezr() {
|
|
8
|
+
try {
|
|
9
|
+
const result = await runCommand("squeezr", ["--version"]);
|
|
10
|
+
if (result.exitCode === 0) {
|
|
11
|
+
const version = (result.stdout || "").trim() || null;
|
|
12
|
+
return { available: true, version };
|
|
13
|
+
}
|
|
14
|
+
return { available: false, version: null };
|
|
15
|
+
} catch { /* squeezr binary not found */
|
|
16
|
+
return { available: false, version: null };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runCommand } from "./process.js";
|
|
2
|
+
import { getInstallCommand } from "./os-detect.js";
|
|
3
|
+
import { detectSqueezr } from "./squeezr-detect.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Install Squeezr using the OS-appropriate command.
|
|
7
|
+
* @param {object} logger - Logger with info/warn methods
|
|
8
|
+
* @returns {Promise<{ ok: boolean, version: string|null, error: string|null }>}
|
|
9
|
+
*/
|
|
10
|
+
export async function installSqueezr(logger) {
|
|
11
|
+
const command = getInstallCommand("squeezr");
|
|
12
|
+
|
|
13
|
+
logger.info("Installing Squeezr...");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = await runCommand("sh", ["-c", command], { timeout: 120_000 });
|
|
17
|
+
|
|
18
|
+
if (result.exitCode !== 0) {
|
|
19
|
+
const error = (result.stderr || "").trim() || `exit code ${result.exitCode}`;
|
|
20
|
+
logger.warn(`Squeezr install failed: ${error}`);
|
|
21
|
+
return { ok: false, version: null, error };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const check = await detectSqueezr();
|
|
25
|
+
if (check.available) {
|
|
26
|
+
logger.info(`Squeezr ${check.version || ""} installed successfully.`);
|
|
27
|
+
return { ok: true, version: check.version, error: null };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
logger.warn("Squeezr install command succeeded but squeezr binary not found in PATH.");
|
|
31
|
+
return { ok: false, version: null, error: "Binary not found after install" };
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const error = err.message || String(err);
|
|
34
|
+
logger.warn(`Squeezr install failed: ${error}`);
|
|
35
|
+
return { ok: false, version: null, error };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// KJC-TSK-0375 PR2 — Extract structured tool calls from an agent transcript.
|
|
2
|
+
//
|
|
3
|
+
// The ToolCorrectnessJudgeRole (PR1) consumes a list of `{tool, args}`
|
|
4
|
+
// objects. The coder writes a raw transcript (provider-dependent format) to
|
|
5
|
+
// disk per iteration. This module bridges the two: parse the transcript,
|
|
6
|
+
// return the ordered list of tool invocations.
|
|
7
|
+
//
|
|
8
|
+
// Provider-agnostic interface; Claude stream-json is the only format
|
|
9
|
+
// implemented today. New providers add a branch below — keep parsers
|
|
10
|
+
// pure (no I/O), so the role tests can mock transcripts trivially.
|
|
11
|
+
|
|
12
|
+
const PROVIDER_PARSERS = {
|
|
13
|
+
claude: parseClaudeStreamJson,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract tool calls from a transcript string.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} args
|
|
20
|
+
* @param {string} args.transcript - raw agent output (stdout/stderr buffer).
|
|
21
|
+
* @param {string} [args.provider] - "claude" (default). Unknown returns [].
|
|
22
|
+
* @returns {Array<{tool:string, args:object}>}
|
|
23
|
+
*/
|
|
24
|
+
export function extractToolCalls({ transcript, provider = "claude" } = {}) {
|
|
25
|
+
if (typeof transcript !== "string" || transcript.length === 0) return [];
|
|
26
|
+
const parser = PROVIDER_PARSERS[provider];
|
|
27
|
+
if (!parser) return [];
|
|
28
|
+
return parser(transcript);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Claude emits one JSON event per line. We care about `assistant` events
|
|
32
|
+
// whose `message.content` is an array containing `tool_use` blocks with
|
|
33
|
+
// `{type:"tool_use", name, input}`. Lines that fail to parse, that are
|
|
34
|
+
// not `assistant`, or whose content is not an array are silently skipped:
|
|
35
|
+
// the transcript also carries `system`, `result`, `rate_limit_event`, etc.
|
|
36
|
+
function parseClaudeStreamJson(transcript) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const raw of transcript.split("\n")) {
|
|
39
|
+
const line = raw.trim();
|
|
40
|
+
if (!line || line[0] !== "{") continue;
|
|
41
|
+
let obj;
|
|
42
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
43
|
+
if (obj?.type !== "assistant") continue;
|
|
44
|
+
const content = obj?.message?.content;
|
|
45
|
+
if (!Array.isArray(content)) continue;
|
|
46
|
+
for (const block of content) {
|
|
47
|
+
if (block?.type === "tool_use" && typeof block.name === "string") {
|
|
48
|
+
out.push({
|
|
49
|
+
tool: block.name,
|
|
50
|
+
args: block.input && typeof block.input === "object" ? block.input : {},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG vector-store orphan collector — KJC-TSK-0499 PR2.
|
|
3
|
+
*
|
|
4
|
+
* Enumerates distinct `project_slug` rows in `~/.karajan/rag.db`,
|
|
5
|
+
* augments each with chunk count + last_indexed_{at,commit}, and
|
|
6
|
+
* marks as orphan the slugs whose basename does not resolve to a
|
|
7
|
+
* directory in any of the configured `projectRoots`.
|
|
8
|
+
*
|
|
9
|
+
* Read-only. Returns rows + a copy-paste sqlite3 command per orphan.
|
|
10
|
+
* Deliberate inverse of the RAG indexer: never opens the DB for write.
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import { dbPath, openVecStore, projectSlug } from "../rag/vec-store.js";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_ROOTS = [
|
|
18
|
+
path.join(os.homedir(), "ws_npm-packages"),
|
|
19
|
+
path.join(os.homedir(), "ws_firebase"),
|
|
20
|
+
path.join(os.homedir(), "projects"),
|
|
21
|
+
path.join(os.homedir(), "code"),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
async function listDirs(root) {
|
|
25
|
+
try {
|
|
26
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
27
|
+
return entries.filter((e) => e.isDirectory()).map((e) => path.join(root, e.name));
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function buildSlugIndex(roots) {
|
|
34
|
+
const index = new Map();
|
|
35
|
+
for (const root of roots) {
|
|
36
|
+
for (const dir of await listDirs(root)) {
|
|
37
|
+
const slug = projectSlug(dir);
|
|
38
|
+
if (slug && !index.has(slug)) index.set(slug, dir);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return index;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function purgeCommand(slug) {
|
|
45
|
+
const slugSafe = slug.replace(/'/g, "''");
|
|
46
|
+
return `sqlite3 ${dbPath()} "DELETE FROM chunks WHERE project_slug='${slugSafe}'; DELETE FROM vec_store_meta WHERE project_slug='${slugSafe}';"`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Collect RAG vector-store orphans.
|
|
51
|
+
* @param {object} [opts]
|
|
52
|
+
* @param {string[]} [opts.projectRoots] — directories to look for live
|
|
53
|
+
* projects whose basename matches each slug. Defaults to the usual
|
|
54
|
+
* `~/ws_*`, `~/projects`, `~/code` set.
|
|
55
|
+
* @returns {Promise<{rows: Array, orphans: Array, errors: Array}>}
|
|
56
|
+
*/
|
|
57
|
+
export async function collectVectorStoreOrphans({ projectRoots = DEFAULT_ROOTS } = {}) {
|
|
58
|
+
const rows = [];
|
|
59
|
+
const errors = [];
|
|
60
|
+
let db;
|
|
61
|
+
try { db = openVecStore(); } catch (e) {
|
|
62
|
+
return { rows, orphans: [], errors: [{ stage: "open", error: String(e?.message ?? e) }] };
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const stats = db.prepare(`
|
|
66
|
+
SELECT COALESCE(c.project_slug, '<null>') AS slug,
|
|
67
|
+
COUNT(c.id) AS chunkCount,
|
|
68
|
+
COALESCE(m.last_indexed_commit, '') AS lastCommit,
|
|
69
|
+
COALESCE(m.last_indexed_at, '') AS lastAt
|
|
70
|
+
FROM chunks c
|
|
71
|
+
LEFT JOIN vec_store_meta m ON m.project_slug = c.project_slug
|
|
72
|
+
GROUP BY c.project_slug
|
|
73
|
+
ORDER BY chunkCount DESC
|
|
74
|
+
`).all();
|
|
75
|
+
const slugIndex = await buildSlugIndex(projectRoots);
|
|
76
|
+
for (const r of stats) {
|
|
77
|
+
const resolved = slugIndex.get(r.slug) || null;
|
|
78
|
+
rows.push({ ...r, resolvedDir: resolved, orphan: !resolved && r.slug !== "<null>" });
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
errors.push({ stage: "query", error: String(e?.message ?? e) });
|
|
82
|
+
} finally {
|
|
83
|
+
try { db.close(); } catch { /* close errors are non-fatal */ }
|
|
84
|
+
}
|
|
85
|
+
const orphans = rows
|
|
86
|
+
.filter((r) => r.orphan)
|
|
87
|
+
.map((r) => ({ ...r, command: purgeCommand(r.slug) }));
|
|
88
|
+
return { rows, orphans, errors };
|
|
89
|
+
}
|