karajan-code 3.1.0 → 3.3.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 +12 -4
- package/package.json +6 -1
- package/packages/hu-board/package.json +3 -1
- package/packages/hu-board/public/index.html +1 -0
- package/packages/hu-board/public/styles.css +32 -0
- package/packages/hu-board/public/utils/board-view.js +21 -1
- package/packages/hu-board/public/utils/formatters.js +68 -0
- package/packages/hu-board/public/wiki.html +32 -0
- package/packages/hu-board/public/wiki.js +53 -0
- package/packages/hu-board/src/db.js +137 -0
- package/packages/hu-board/src/format.js +124 -0
- package/packages/hu-board/src/plan-mutations.js +4 -4
- package/packages/hu-board/src/routes/api.js +16 -1
- 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/git-hooks/post-merge +13 -2
- package/src/agents/aider-agent.js +42 -1
- package/src/agents/claude-agent.js +14 -3
- package/src/agents/codex-agent.js +44 -7
- package/src/agents/gemini-agent.js +37 -1
- package/src/agents/opencode-agent.js +34 -1
- package/src/audit/ai-slop-findings.js +102 -0
- package/src/audit/deterministic-summary.js +18 -0
- 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/squeezr.js +36 -0
- package/src/cli/register-meta.js +21 -0
- package/src/cli/register-pipeline.js +3 -0
- package/src/commands/audit.js +6 -4
- package/src/commands/board.js +34 -0
- package/src/commands/doctor.js +6 -0
- package/src/commands/init.js +73 -8
- package/src/commands/qmd.js +86 -0
- package/src/commands/resume.js +6 -45
- package/src/commands/run.js +6 -50
- package/src/git/hu-snapshot.js +10 -105
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +27 -0
- package/src/orchestrator/drivers/run-hu-batch.js +5 -1
- package/src/orchestrator/flow-runner.js +10 -2
- package/src/orchestrator/hu-sub-pipeline.js +82 -8
- 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/rag/vec-store.js +17 -253
- package/src/roles/audit-role.js +11 -1
- package/src/session/journal/summary-writer.js +55 -1
- package/src/utils/ai-trash.js +48 -0
- package/src/utils/atomic-write.js +6 -43
- package/src/utils/budget.js +38 -9
- 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 -123
- 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/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/telemetry.js +43 -0
|
@@ -1,92 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const _scheduled = new Map();
|
|
12
|
-
|
|
13
|
-
export function _resetScheduledForTests() {
|
|
14
|
-
for (const t of _scheduled.values()) clearTimeout(t);
|
|
15
|
-
_scheduled.clear();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Programa un único setTimeout que dispara `onResume(sessionId)` en cooldownUntil.
|
|
20
|
-
*
|
|
21
|
-
* @param {object} args
|
|
22
|
-
* @param {string} args.sessionId
|
|
23
|
-
* @param {string} args.cooldownUntil - ISO timestamp
|
|
24
|
-
* @param {Function} args.onResume - callback(sessionId) cuando llega el momento
|
|
25
|
-
* @param {object} [args.logger]
|
|
26
|
-
* @returns {{ scheduled: boolean, msUntil?: number, immediate?: boolean }}
|
|
27
|
-
*/
|
|
28
|
-
export function scheduleResume({ sessionId, cooldownUntil, onResume, logger }) {
|
|
29
|
-
if (!sessionId || !cooldownUntil || typeof onResume !== "function") {
|
|
30
|
-
return { scheduled: false };
|
|
31
|
-
}
|
|
32
|
-
cancelScheduled(sessionId);
|
|
33
|
-
const targetMs = new Date(cooldownUntil).getTime();
|
|
34
|
-
if (Number.isNaN(targetMs)) return { scheduled: false };
|
|
35
|
-
const delay = targetMs - Date.now();
|
|
36
|
-
if (delay <= 0) {
|
|
37
|
-
// Ya expiró — reanudar inmediato (caso: board reiniciado tras llegar la hora).
|
|
38
|
-
logger?.info?.(`[standby-scheduler] ${sessionId}: cooldown ya expirado, resume inmediato`);
|
|
39
|
-
Promise.resolve().then(() => onResume(sessionId)).catch(() => { /* swallow */ });
|
|
40
|
-
return { scheduled: true, immediate: true };
|
|
41
|
-
}
|
|
42
|
-
const timer = setTimeout(() => {
|
|
43
|
-
_scheduled.delete(sessionId);
|
|
44
|
-
logger?.info?.(`[standby-scheduler] ${sessionId}: cooldown alcanzado, disparando resume`);
|
|
45
|
-
try { onResume(sessionId); } catch (err) { logger?.warn?.(`[standby-scheduler] onResume threw: ${err.message}`); }
|
|
46
|
-
}, delay);
|
|
47
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
48
|
-
_scheduled.set(sessionId, timer);
|
|
49
|
-
logger?.info?.(`[standby-scheduler] ${sessionId}: scheduled in ${Math.round(delay / 1000)}s (until ${cooldownUntil})`);
|
|
50
|
-
return { scheduled: true, msUntil: delay };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Cancela el setTimeout de una sesión (caso: usuario hizo kj resume manual). */
|
|
54
|
-
export function cancelScheduled(sessionId) {
|
|
55
|
-
const t = _scheduled.get(sessionId);
|
|
56
|
-
if (!t) return false;
|
|
57
|
-
clearTimeout(t);
|
|
58
|
-
_scheduled.delete(sessionId);
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Reconciliación al arrancar el board / kj. Lee todas las sesiones pendientes
|
|
64
|
-
* y para cada una:
|
|
65
|
-
* - cooldownUntil < now → onResume inmediato
|
|
66
|
-
* - cooldownUntil > now → re-programa setTimeout
|
|
67
|
-
*
|
|
68
|
-
* Idempotente: el lockfile en standby-store previene dobles spawns si
|
|
69
|
-
* reconcileAll() corre dos veces concurrentes (board reload).
|
|
70
|
-
*
|
|
71
|
-
* @param {object} args
|
|
72
|
-
* @param {Function} args.onResume - callback(sessionId)
|
|
73
|
-
* @param {object} [args.logger]
|
|
74
|
-
* @returns {{ scheduled: number, immediate: number, total: number }}
|
|
75
|
-
*/
|
|
76
|
-
export function reconcileAll({ onResume, logger }) {
|
|
77
|
-
const sessions = listPendingStandby();
|
|
78
|
-
let scheduled = 0;
|
|
79
|
-
let immediate = 0;
|
|
80
|
-
for (const s of sessions) {
|
|
81
|
-
if (!s?.sessionId || !s?.cooldownUntil) continue;
|
|
82
|
-
const r = scheduleResume({ sessionId: s.sessionId, cooldownUntil: s.cooldownUntil, onResume, logger });
|
|
83
|
-
if (r.scheduled && r.immediate) immediate += 1;
|
|
84
|
-
else if (r.scheduled) scheduled += 1;
|
|
85
|
-
}
|
|
86
|
-
logger?.info?.(`[standby-scheduler] reconcile: ${sessions.length} sesiones (${immediate} resume inmediato, ${scheduled} programadas)`);
|
|
87
|
-
return { scheduled, immediate, total: sessions.length };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Re-export para que callers no tengan que importar standby-store si solo
|
|
91
|
-
// necesitan el flow scheduler → lock → done.
|
|
92
|
-
export { acquireStandbyLock, markStandbyDone };
|
|
1
|
+
// Shim de compatibilidad: standby-scheduler vive ahora en @karajan/core.
|
|
2
|
+
// Mantiene la ruta para callers internos del CLI.
|
|
3
|
+
export {
|
|
4
|
+
_resetScheduledForTests,
|
|
5
|
+
scheduleResume,
|
|
6
|
+
cancelScheduled,
|
|
7
|
+
reconcileAll,
|
|
8
|
+
acquireStandbyLock,
|
|
9
|
+
markStandbyDone,
|
|
10
|
+
} from "@karajan/core/standby-scheduler";
|
|
@@ -1,168 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// <sessionId>.json ← finalizadas (consumibles por GC en TSK-0414 PR3)
|
|
14
|
-
|
|
15
|
-
import fs from "node:fs";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import { writeJsonAtomicSync } from "../utils/atomic-write.js";
|
|
18
|
-
import { resolveHome } from "../utils/paths.js";
|
|
19
|
-
|
|
20
|
-
const FILENAME_RE = /^[a-zA-Z0-9._-]+\.json$/;
|
|
21
|
-
|
|
22
|
-
// Delegates to the unified resolver. PR 3 of KJC-PCS-0047 switched
|
|
23
|
-
// the default to `.karajan` — hibernated sessions now persist under
|
|
24
|
-
// `~/.karajan/standby/`, migrated automatically from the legacy
|
|
25
|
-
// `~/.kj/standby/` on the next `kj` invocation.
|
|
26
|
-
function getKjHome() {
|
|
27
|
-
return resolveHome({ defaultSegment: ".karajan" });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function standbyDir() {
|
|
31
|
-
return path.join(getKjHome(), "standby");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function standbyDoneDir() {
|
|
35
|
-
return path.join(standbyDir(), "done");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function sanitizeId(sessionId) {
|
|
39
|
-
if (!sessionId || typeof sessionId !== "string") throw new Error("sessionId requerido");
|
|
40
|
-
return sessionId.replaceAll(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Persiste el estado completo de un run hibernado.
|
|
45
|
-
*
|
|
46
|
-
* @param {object} state - shape:
|
|
47
|
-
* { sessionId, planId?, huId?, role, iteration?, prompt, agentConfig,
|
|
48
|
-
* retryCount?, cooldownUntil, reason, snapshot_sha?, outcome_so_far? }
|
|
49
|
-
* @returns {string} path absoluto del JSON escrito
|
|
50
|
-
*/
|
|
51
|
-
export function persistStandby(state) {
|
|
52
|
-
if (!state || !state.sessionId) throw new Error("state.sessionId requerido");
|
|
53
|
-
if (!state.cooldownUntil) throw new Error("state.cooldownUntil requerido");
|
|
54
|
-
const dir = standbyDir();
|
|
55
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
-
const file = path.join(dir, `${sanitizeId(state.sessionId)}.json`);
|
|
57
|
-
const payload = {
|
|
58
|
-
...state,
|
|
59
|
-
createdAt: state.createdAt || new Date().toISOString(),
|
|
60
|
-
persistedAt: new Date().toISOString(),
|
|
61
|
-
};
|
|
62
|
-
writeJsonAtomicSync(file, payload);
|
|
63
|
-
return file;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Only these env vars are carried into the standby snapshot. NEVER
|
|
67
|
-
// persist the whole `process.env` — that would leak API keys and other
|
|
68
|
-
// secrets into ~/.kj/standby/*.json. `kj standby resume` re-spawns with
|
|
69
|
-
// the live env merged on top, so the snapshot only needs what is needed
|
|
70
|
-
// to relaunch from the right place.
|
|
71
|
-
const STANDBY_ENV_ALLOWLIST = ["KJ_HOME", "HOME", "PATH"];
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Builds the `sessionState` object that `withBrainRecovery` persists when
|
|
75
|
-
* a run hibernates. Pulls the session id, the relaunch metadata and an
|
|
76
|
-
* allowlisted env subset so `kj standby resume` can re-spawn the command.
|
|
77
|
-
* @param {object} args
|
|
78
|
-
* @param {object} args.session - the pipeline session ({ id, plan_id? })
|
|
79
|
-
* @param {object} [args.config] - resolved config ({ projectDir?, plan? })
|
|
80
|
-
* @param {string|null} [args.huId]
|
|
81
|
-
* @returns {object|null} sessionState, or null when there is no session id
|
|
82
|
-
*/
|
|
83
|
-
export function buildStandbyState({ session, config = {}, huId = null } = {}) {
|
|
84
|
-
if (!session?.id) return null;
|
|
85
|
-
const env = {};
|
|
86
|
-
for (const key of STANDBY_ENV_ALLOWLIST) {
|
|
87
|
-
if (process.env[key] != null) env[key] = process.env[key];
|
|
88
|
-
}
|
|
89
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
90
|
-
if (key.startsWith("KJ_") && value != null) env[key] = value;
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
sessionId: session.id,
|
|
94
|
-
planId: session.plan_id || config.plan || null,
|
|
95
|
-
huId: huId || null,
|
|
96
|
-
argv: process.argv.slice(2),
|
|
97
|
-
cwd: config.projectDir || process.cwd(),
|
|
98
|
-
env,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* @param {string} sessionId
|
|
104
|
-
* @returns {object|null}
|
|
105
|
-
*/
|
|
106
|
-
export function loadStandby(sessionId) {
|
|
107
|
-
const file = path.join(standbyDir(), `${sanitizeId(sessionId)}.json`);
|
|
108
|
-
if (!fs.existsSync(file)) return null;
|
|
109
|
-
try {
|
|
110
|
-
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
111
|
-
} catch {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Lista todas las sesiones pendientes (no las de done/).
|
|
118
|
-
* @returns {object[]}
|
|
119
|
-
*/
|
|
120
|
-
export function listPendingStandby() {
|
|
121
|
-
const dir = standbyDir();
|
|
122
|
-
if (!fs.existsSync(dir)) return [];
|
|
123
|
-
return fs.readdirSync(dir)
|
|
124
|
-
.filter((f) => FILENAME_RE.test(f))
|
|
125
|
-
.map((f) => {
|
|
126
|
-
try {
|
|
127
|
-
return JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
|
128
|
-
} catch {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
.filter(Boolean);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Mueve una sesión a done/ — consumible por GC en TSK-0414 PR3.
|
|
137
|
-
* @param {string} sessionId
|
|
138
|
-
* @returns {boolean} true si se movió
|
|
139
|
-
*/
|
|
140
|
-
export function markStandbyDone(sessionId) {
|
|
141
|
-
const id = sanitizeId(sessionId);
|
|
142
|
-
const src = path.join(standbyDir(), `${id}.json`);
|
|
143
|
-
if (!fs.existsSync(src)) return false;
|
|
144
|
-
const doneDir = standbyDoneDir();
|
|
145
|
-
fs.mkdirSync(doneDir, { recursive: true });
|
|
146
|
-
const dst = path.join(doneDir, `${id}-${Date.now()}.json`);
|
|
147
|
-
fs.renameSync(src, dst);
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Lockfile para evitar dobles spawns de kj resume sobre la misma sesión.
|
|
153
|
-
* Devuelve { acquired: true, release: () => void } o { acquired: false } si ya hay lock.
|
|
154
|
-
*
|
|
155
|
-
* @param {string} sessionId
|
|
156
|
-
* @returns {{ acquired: boolean, release?: Function }}
|
|
157
|
-
*/
|
|
158
|
-
export function acquireStandbyLock(sessionId) {
|
|
159
|
-
const id = sanitizeId(sessionId);
|
|
160
|
-
const lockFile = path.join(standbyDir(), `${id}.lock`);
|
|
161
|
-
try {
|
|
162
|
-
fs.mkdirSync(standbyDir(), { recursive: true });
|
|
163
|
-
fs.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
|
|
164
|
-
return { acquired: true, release: () => { try { fs.unlinkSync(lockFile); } catch { /* */ } } };
|
|
165
|
-
} catch {
|
|
166
|
-
return { acquired: false };
|
|
167
|
-
}
|
|
168
|
-
}
|
|
1
|
+
// Shim de compatibilidad: standby-store vive ahora en @karajan/core.
|
|
2
|
+
// Mantiene la ruta para callers internos del CLI.
|
|
3
|
+
export {
|
|
4
|
+
standbyDir,
|
|
5
|
+
standbyDoneDir,
|
|
6
|
+
persistStandby,
|
|
7
|
+
buildStandbyState,
|
|
8
|
+
loadStandby,
|
|
9
|
+
listPendingStandby,
|
|
10
|
+
markStandbyDone,
|
|
11
|
+
acquireStandbyLock,
|
|
12
|
+
} from "@karajan/core/standby-store";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai-trash safety net availability check (KJC-TSK-0391).
|
|
3
|
+
* Surfaces a WARN in `kj doctor` when the `kj-trash` binary is missing,
|
|
4
|
+
* since the PreToolUse hook cannot snapshot destructive ops without it.
|
|
5
|
+
* The remediation is `kj-trash install --claude-code` once the binary is
|
|
6
|
+
* on PATH (the workspace ships it with karajan-code).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { detectAiTrash } from "../utils/ai-trash.js";
|
|
10
|
+
import { STRATEGY } from "./types.js";
|
|
11
|
+
|
|
12
|
+
function createAiTrashCheck() {
|
|
13
|
+
return {
|
|
14
|
+
name: "ai-trash",
|
|
15
|
+
label: "ai-trash safety net",
|
|
16
|
+
strategy: STRATEGY.MANUAL,
|
|
17
|
+
async detect() {
|
|
18
|
+
const res = await detectAiTrash();
|
|
19
|
+
if (res.available) {
|
|
20
|
+
return { ok: true, severity: "info", detail: "kj-trash present — preventive safety net active" };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
severity: "warn",
|
|
25
|
+
detail: "kj-trash not found — destructive ops unprotected",
|
|
26
|
+
fix: "Reinstall karajan-code (kj-trash ships with the workspace) and run `kj-trash install --claude-code`",
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getAiTrashChecks() {
|
|
33
|
+
return [createAiTrashCheck()];
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QMD availability check. Manual strategy — installation
|
|
3
|
+
* depends on the host platform and is handled by getInstallCommand().
|
|
4
|
+
*
|
|
5
|
+
* QMD (Quick Markdown Search) provides semantic search over the
|
|
6
|
+
* user's wiki / docs / notes collections. Karajan ships it as a
|
|
7
|
+
* default integration (mirror of Squeezr): kj init installs it,
|
|
8
|
+
* kj doctor reports its presence.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { runCommand } from "../utils/process.js";
|
|
12
|
+
import { getInstallCommand } from "../utils/os-detect.js";
|
|
13
|
+
import { withDocLink } from "../utils/doc-links.js";
|
|
14
|
+
import { STRATEGY } from "./types.js";
|
|
15
|
+
|
|
16
|
+
function createQmdCheck() {
|
|
17
|
+
return {
|
|
18
|
+
name: "qmd",
|
|
19
|
+
label: "QMD",
|
|
20
|
+
strategy: STRATEGY.MANUAL,
|
|
21
|
+
async detect() {
|
|
22
|
+
try {
|
|
23
|
+
const res = await runCommand("qmd", ["--version"]);
|
|
24
|
+
if (res.exitCode === 0) {
|
|
25
|
+
return { ok: true, severity: "info", detail: `${res.stdout.trim()} — semantic wiki active` };
|
|
26
|
+
}
|
|
27
|
+
} catch { /* not installed */ }
|
|
28
|
+
const installCmd = getInstallCommand("qmd");
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
severity: "warn",
|
|
32
|
+
detail: "Not found — semantic wiki via QMD available",
|
|
33
|
+
fix: withDocLink(`Install: ${installCmd}`, "qmd_install"),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getQmdChecks() {
|
|
40
|
+
return [createQmdCheck()];
|
|
41
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squeezr availability check. Manual strategy — installation
|
|
3
|
+
* depends on the host platform and is handled by getInstallCommand().
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { runCommand } from "../utils/process.js";
|
|
7
|
+
import { getInstallCommand } from "../utils/os-detect.js";
|
|
8
|
+
import { withDocLink } from "../utils/doc-links.js";
|
|
9
|
+
import { STRATEGY } from "./types.js";
|
|
10
|
+
|
|
11
|
+
function createSqueezrCheck() {
|
|
12
|
+
return {
|
|
13
|
+
name: "squeezr",
|
|
14
|
+
label: "Squeezr",
|
|
15
|
+
strategy: STRATEGY.MANUAL,
|
|
16
|
+
async detect() {
|
|
17
|
+
try {
|
|
18
|
+
const res = await runCommand("squeezr", ["--version"]);
|
|
19
|
+
if (res.exitCode === 0) {
|
|
20
|
+
return { ok: true, severity: "info", detail: `${res.stdout.trim()} — context compression active` };
|
|
21
|
+
}
|
|
22
|
+
} catch { /* not installed */ }
|
|
23
|
+
const installCmd = getInstallCommand("squeezr");
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
severity: "warn",
|
|
27
|
+
detail: "Not found — context compression via Squeezr available",
|
|
28
|
+
fix: withDocLink(`Install: ${installCmd}`, "squeezr_install"),
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getSqueezrChecks() {
|
|
35
|
+
return [createSqueezrCheck()];
|
|
36
|
+
}
|
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 { qmdQueryCommand } from "../commands/qmd.js";
|
|
7
8
|
import { ragMcpCommand } from "../commands/rag-mcp.js";
|
|
8
9
|
import { watchStartCommand, watchStopCommand, watchStatusCommand } from "../commands/watch.js";
|
|
9
10
|
import { auditCommand } from "../commands/audit.js";
|
|
@@ -172,6 +173,24 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
172
173
|
await ragMcpCommand();
|
|
173
174
|
});
|
|
174
175
|
|
|
176
|
+
// KJC-TSK-0509 — `kj qmd query` is a thin wrapper around the upstream
|
|
177
|
+
// `qmd` binary that auto-resolves the project's collection (rag = agent
|
|
178
|
+
// context; qmd = human wiki). One subcommand for now; index/add live in
|
|
179
|
+
// `kj init` and `kj rag install-hooks` (post-merge reindex).
|
|
180
|
+
const qmd = program.command("qmd").description("Semantic search over the per-project QMD wiki (docs/, .reviews/, .karajan/plans/)");
|
|
181
|
+
qmd.command("query <text>")
|
|
182
|
+
.description("Run a qmd query against the wiki collection of the current project")
|
|
183
|
+
.option("--collection <name>", "Override the auto-resolved <slug>-<suffix> collection name")
|
|
184
|
+
.option("--scope <name>", "docs (default) | reviews | plans", "docs")
|
|
185
|
+
.option("--top-k <n>", "Number of hits to return (1..50, default 10)", "10")
|
|
186
|
+
.option("--fast", "Skip the LLM reranker (faster, slightly noisier hits)")
|
|
187
|
+
.option("--json", "Emit the raw JSON array for piping to jq")
|
|
188
|
+
.action(async (text, flags) => {
|
|
189
|
+
await withConfig(pkgVersion, "qmd-query", flags, async ({ config, logger }) => {
|
|
190
|
+
await qmdQueryCommand({ text, config, logger, flags });
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
175
194
|
// KJC-TSK-0441 — `kj watch [start|stop|status]` for live RAG re-index.
|
|
176
195
|
const watch = program.command("watch").description("Live RAG re-index daemon");
|
|
177
196
|
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) => {
|
|
@@ -197,6 +216,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
197
216
|
.option("--no-osv", "Skip the OSV-Scanner vulnerability collector. Findings are also skipped automatically when osv-scanner is not installed.")
|
|
198
217
|
.option("--no-semgrep", "Skip the Semgrep SAST collector. Findings are also skipped automatically when semgrep is not installed (install via 'pipx install semgrep' or 'brew install semgrep').")
|
|
199
218
|
.option("--no-harness", "Skip the AI Harness Scorecard stage (Docker one-shot). Auto-skipped when Docker is unavailable. Each run is also persisted to .karajan/audit-history.db (KJC-TSK-0472) for diff/trend tracking.")
|
|
219
|
+
.option("--no-ai-slop", "Skip the AI-slop tells collector (deterministic regex scan for tautologic comments, banner separators, boilerplate docstrings, magic fallbacks, redundant JSDoc). KJC-TSK-0503.")
|
|
200
220
|
.option("--trend", "Append a sparkline of the harness score across the last 10 runs (uses .karajan/audit-history.db). KJC-TSK-0473.")
|
|
201
221
|
.option("--report-file <path>", "Write the audit report to disk in addition to stdout. <path> may be a file (extension drives format: .md or .json) or a directory (creates audit-<ISO>.<md|json> inside). $KJ_AUDIT_REPORT_DIR env var is used as default directory if no --report-file is given.")
|
|
202
222
|
.option("--deterministic-only", "Skip the LLM analysis entirely. Print/persist only the deterministic findings (basalCost, sonar, stack, growth-delta, webperf). Zero tokens spent. Compatible with --report-file and --json.")
|
|
@@ -220,6 +240,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
220
240
|
noOsv: flags.osv === false,
|
|
221
241
|
noSemgrep: flags.semgrep === false,
|
|
222
242
|
noHarness: flags.harness === false,
|
|
243
|
+
noAiSlop: flags.aiSlop === false,
|
|
223
244
|
reportFile: flags.reportFile || null,
|
|
224
245
|
deterministicOnly: Boolean(flags.deterministicOnly),
|
|
225
246
|
yes: Boolean(flags.yes),
|
|
@@ -27,6 +27,9 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
27
27
|
.option("--global", "Save config to ~/.karajan/kj.config.yml (skip the scope wizard)")
|
|
28
28
|
.option("--local", "Save config to ./.karajan/kj.config.yml (skip the scope wizard)")
|
|
29
29
|
.option("--no-ollama", "Skip the RAG embedder bootstrap (Ollama-in-Docker). Useful on modest hardware or when an external embedder will be wired manually")
|
|
30
|
+
.option("--no-rtk", "Skip the RTK auto-install (token savings on Bash outputs). Karajan still runs but burns more tokens")
|
|
31
|
+
.option("--no-squeezr", "Skip the Squeezr auto-install (context compression). Karajan still runs but burns more tokens")
|
|
32
|
+
.option("--no-qmd", "Skip the QMD auto-install + collection registration (semantic wiki over docs/, .reviews/ and plans/)")
|
|
30
33
|
.action(async (flags) => {
|
|
31
34
|
await withConfig(pkgVersion, "init", flags, async ({ config: _config, logger }) => {
|
|
32
35
|
await initCommand({ logger, flags });
|
package/src/commands/audit.js
CHANGED
|
@@ -216,7 +216,7 @@ function loadAuditHistoryContext(projectDir, currentHarness, { showTrend = false
|
|
|
216
216
|
finally { try { db?.close(); } catch { /* noop */ } }
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, json, deterministicOnly, yes }) {
|
|
219
|
+
function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, noAiSlop, json, deterministicOnly, yes }) {
|
|
220
220
|
const parts = ["kj audit"];
|
|
221
221
|
if (task && task !== "Analyze the full codebase") parts.push(JSON.stringify(task));
|
|
222
222
|
if (dimensions && dimensions !== "all") parts.push(`--dimensions=${dimensions}`);
|
|
@@ -224,13 +224,14 @@ function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHar
|
|
|
224
224
|
if (noOsv) parts.push("--no-osv");
|
|
225
225
|
if (noSemgrep) parts.push("--no-semgrep");
|
|
226
226
|
if (noHarness) parts.push("--no-harness");
|
|
227
|
+
if (noAiSlop) parts.push("--no-ai-slop");
|
|
227
228
|
if (deterministicOnly) parts.push("--deterministic-only");
|
|
228
229
|
if (yes) parts.push("--yes");
|
|
229
230
|
if (json) parts.push("--json");
|
|
230
231
|
return parts.join(" ");
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, noHarness = false, reportFile = null, deterministicOnly = false, yes = false, trend = false, promptFn = null }) {
|
|
234
|
+
export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, noHarness = false, noAiSlop = false, reportFile = null, deterministicOnly = false, yes = false, trend = false, promptFn = null }) {
|
|
234
235
|
// --agent-readiness is a STANDALONE, deterministic, LLM-free audit
|
|
235
236
|
// dimension. It scores any third-party repo for AI-agent readability
|
|
236
237
|
// (llms.txt presence, page token budgets, robots allowlist, etc.).
|
|
@@ -269,6 +270,7 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
269
270
|
noSonar,
|
|
270
271
|
noOsv,
|
|
271
272
|
noSemgrep,
|
|
273
|
+
noAiSlop,
|
|
272
274
|
};
|
|
273
275
|
const deterministicCtx = await role.collectDeterministic(roleInput);
|
|
274
276
|
const deterministicMd = formatDeterministicSummary(deterministicCtx);
|
|
@@ -324,7 +326,7 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
324
326
|
if (reportPath.endsWith(".json")) {
|
|
325
327
|
payload = JSON.stringify({ deterministic: deterministicCtx, harnessScore: harnessJson, harnessScoreDiff: history.diff, mode: "deterministic-only" }, null, 2);
|
|
326
328
|
} else {
|
|
327
|
-
const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, json, deterministicOnly, yes }));
|
|
329
|
+
const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, noAiSlop, json, deterministicOnly, yes }));
|
|
328
330
|
payload = `${header}${deterministicMd}\n${harnessMd}${historyMd}`;
|
|
329
331
|
}
|
|
330
332
|
await fs.writeFile(reportPath, payload, "utf8");
|
|
@@ -385,7 +387,7 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
385
387
|
: { ...roleResult, harnessScore: harnessJson, harnessScoreDiff: history.diff, usage: usage || undefined };
|
|
386
388
|
payload = JSON.stringify(jsonPayload, null, 2);
|
|
387
389
|
} else {
|
|
388
|
-
const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, json, deterministicOnly, yes }));
|
|
390
|
+
const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, noAiSlop, json, deterministicOnly, yes }));
|
|
389
391
|
payload = header + stdoutContent + "\n";
|
|
390
392
|
}
|
|
391
393
|
await fs.writeFile(reportPath, payload, "utf8");
|
package/src/commands/board.js
CHANGED
|
@@ -122,6 +122,29 @@ export function waitForEarlyExit(child, logPath) {
|
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// KJC-BUG-0080: waitForEarlyExit only catches an exit, not a hung
|
|
126
|
+
// daemon that survived the window without ever bind()ing the port.
|
|
127
|
+
// Poll /api/dashboard until it answers or the readiness budget runs
|
|
128
|
+
// out, so we never announce "HU Board started" for an unreachable
|
|
129
|
+
// server. Overridable with KJ_BOARD_READY_TIMEOUT_MS (tests set it
|
|
130
|
+
// low to stay fast).
|
|
131
|
+
export async function waitForBoardReachable(port, totalTimeoutMs) {
|
|
132
|
+
const envTimeout = Number(process.env.KJ_BOARD_READY_TIMEOUT_MS);
|
|
133
|
+
const budget = Number.isFinite(envTimeout) && envTimeout >= 0
|
|
134
|
+
? envTimeout
|
|
135
|
+
: (Number.isFinite(totalTimeoutMs) ? totalTimeoutMs : 5000);
|
|
136
|
+
const intervalMs = budget <= 200 ? Math.max(10, Math.floor(budget / 5)) : 200;
|
|
137
|
+
const deadline = Date.now() + budget;
|
|
138
|
+
while (Date.now() < deadline) {
|
|
139
|
+
const remaining = deadline - Date.now();
|
|
140
|
+
const probeTimeout = Math.min(750, Math.max(50, remaining));
|
|
141
|
+
if (await isBoardReachable(port, probeTimeout)) return true;
|
|
142
|
+
const wait = Math.min(intervalMs, deadline - Date.now());
|
|
143
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
125
148
|
export async function startBoard(desiredPort = 4000, opts = {}) {
|
|
126
149
|
const { projectSlug = null, bind = "127.0.0.1" } = opts;
|
|
127
150
|
const existingPid = readPid();
|
|
@@ -199,6 +222,17 @@ export async function startBoard(desiredPort = 4000, opts = {}) {
|
|
|
199
222
|
if (failure) {
|
|
200
223
|
throw new Error(failure);
|
|
201
224
|
}
|
|
225
|
+
// KJC-BUG-0080: even after a clean window, the server may have
|
|
226
|
+
// failed to bind (e.g. port race vs the parent's findAvailablePort)
|
|
227
|
+
// or be still booting. Probe /api/dashboard before announcing
|
|
228
|
+
// success — without this the CLI used to print "HU Board started
|
|
229
|
+
// at http://localhost:4000" while :4000 actually returned nothing.
|
|
230
|
+
const reachable = await waitForBoardReachable(port);
|
|
231
|
+
if (!reachable) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`HU Board daemon spawned (PID ${child.pid}) but did not answer on http://localhost:${port} in time. See ${logPath} for the cause.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
202
236
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
203
237
|
// Detach for real: `detached: true` only sets up the new session;
|
|
204
238
|
// without `unref()` the parent's event loop still holds a reference
|
package/src/commands/doctor.js
CHANGED
|
@@ -22,6 +22,9 @@ import { getOllamaChecks } from "../checks/ollama.js";
|
|
|
22
22
|
import { getHarnessScorecardChecks } from "../checks/harness-scorecard.js";
|
|
23
23
|
import { getCiChecks } from "../checks/ci.js";
|
|
24
24
|
import { getRtkChecks } from "../checks/rtk.js";
|
|
25
|
+
import { getSqueezrChecks } from "../checks/squeezr.js";
|
|
26
|
+
import { getAiTrashChecks } from "../checks/ai-trash.js";
|
|
27
|
+
import { getQmdChecks } from "../checks/qmd.js";
|
|
25
28
|
import { getNodeChecks } from "../checks/node.js";
|
|
26
29
|
import { getPortChecks } from "../checks/ports.js";
|
|
27
30
|
import { getTokenChecks } from "../checks/tokens.js";
|
|
@@ -64,6 +67,9 @@ function buildChecks(config, { projectOnly = false } = {}) {
|
|
|
64
67
|
...getSkillsChecks(),
|
|
65
68
|
...getCiChecks(),
|
|
66
69
|
...getRtkChecks(),
|
|
70
|
+
...getSqueezrChecks(),
|
|
71
|
+
...getAiTrashChecks(),
|
|
72
|
+
...getQmdChecks(),
|
|
67
73
|
...getProjectChecks({ projectDir }),
|
|
68
74
|
];
|
|
69
75
|
}
|