karajan-code 3.1.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.
Files changed (59) hide show
  1. package/README.md +12 -4
  2. package/package.json +6 -1
  3. package/packages/hu-board/package.json +3 -1
  4. package/packages/hu-board/public/index.html +1 -0
  5. package/packages/hu-board/public/styles.css +32 -0
  6. package/packages/hu-board/public/utils/board-view.js +10 -1
  7. package/packages/hu-board/public/utils/formatters.js +41 -0
  8. package/packages/hu-board/public/wiki.html +32 -0
  9. package/packages/hu-board/public/wiki.js +53 -0
  10. package/packages/hu-board/src/db.js +78 -0
  11. package/packages/hu-board/src/format.js +79 -0
  12. package/packages/hu-board/src/plan-mutations.js +4 -4
  13. package/packages/hu-board/src/routes/api.js +16 -1
  14. package/packages/hu-board/src/routes/rag.js +1 -1
  15. package/packages/hu-board/src/routes/wiki.js +43 -0
  16. package/packages/hu-board/src/run-tracker.js +1 -1
  17. package/packages/hu-board/src/server.js +4 -2
  18. package/packages/hu-board/src/sync.js +1 -1
  19. package/scripts/git-hooks/post-merge +13 -2
  20. package/src/audit/ai-slop-findings.js +102 -0
  21. package/src/audit/deterministic-summary.js +18 -0
  22. package/src/brain/standby-scheduler.js +10 -92
  23. package/src/brain/standby-store.js +12 -168
  24. package/src/checks/ai-trash.js +34 -0
  25. package/src/checks/qmd.js +41 -0
  26. package/src/checks/squeezr.js +36 -0
  27. package/src/cli/register-meta.js +21 -0
  28. package/src/cli/register-pipeline.js +3 -0
  29. package/src/commands/audit.js +6 -4
  30. package/src/commands/board.js +34 -0
  31. package/src/commands/doctor.js +6 -0
  32. package/src/commands/init.js +73 -8
  33. package/src/commands/qmd.js +86 -0
  34. package/src/commands/resume.js +6 -45
  35. package/src/commands/run.js +6 -50
  36. package/src/git/hu-snapshot.js +10 -105
  37. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +16 -0
  38. package/src/orchestrator/drivers/run-hu-batch.js +5 -1
  39. package/src/orchestrator/hu-sub-pipeline.js +52 -8
  40. package/src/plan/plan-hu-ops.js +16 -338
  41. package/src/plan/plan-id.js +4 -45
  42. package/src/plan/plan-validation.js +4 -82
  43. package/src/rag/vec-store.js +17 -253
  44. package/src/roles/audit-role.js +11 -1
  45. package/src/utils/ai-trash.js +48 -0
  46. package/src/utils/atomic-write.js +6 -43
  47. package/src/utils/cli-ask-question.js +76 -0
  48. package/src/utils/doc-links.js +2 -0
  49. package/src/utils/os-detect.js +10 -0
  50. package/src/utils/paths.js +14 -123
  51. package/src/utils/port-check.js +6 -45
  52. package/src/utils/process.js +4 -188
  53. package/src/utils/qmd-collection.js +72 -0
  54. package/src/utils/qmd-detect.js +18 -0
  55. package/src/utils/qmd-install.js +37 -0
  56. package/src/utils/run-registry.js +9 -116
  57. package/src/utils/shared-paths.js +12 -41
  58. package/src/utils/squeezr-detect.js +18 -0
  59. package/src/utils/squeezr-install.js +37 -0
@@ -10,13 +10,14 @@ import { fullScan, startWatcher } from './sync.js';
10
10
  import apiRoutes from './routes/api.js';
11
11
  import pipelineRoutes from './routes/pipeline.js';
12
12
  import ragRoutes from './routes/rag.js';
13
+ import wikiRoutes from './routes/wiki.js';
13
14
  import { authMiddleware } from './auth.js';
14
15
  import { getOrCreateToken, getTokenPath } from './token-store.js';
15
16
  import { reapZombieSessions } from './zombie-reaper.js';
16
17
  import { reapZombieHus } from './hu-zombie-reaper.js';
17
18
  import { setHuStatus as setHuStatusPlanMutation, setHuFailResult as setHuFailResultPlanMutation } from './plan-mutations.js';
18
19
  import { cleanupEphemeralProjects } from './ephemeral-cleaner.js';
19
- import { findAvailablePort as findAvailablePortBase } from '../../../src/utils/port-check.js';
20
+ import { findAvailablePort as findAvailablePortBase } from '@karajan/core/port-check';
20
21
 
21
22
  /**
22
23
  * Path to the PID file the CLI's `kj board start` / `kj board stop`
@@ -237,7 +238,7 @@ async function main() {
237
238
  // - cooldownUntil > now → re-programa setTimeout que dispara en cooldown
238
239
  // Idempotente (lockfile per session). Cero polling.
239
240
  try {
240
- const { reconcileAll } = await import('../../../src/brain/standby-scheduler.js');
241
+ const { reconcileAll } = await import('@karajan/core/standby-scheduler');
241
242
  const { spawn } = await import('node:child_process');
242
243
  const { join, dirname } = await import('node:path');
243
244
  const { fileURLToPath } = await import('node:url');
@@ -300,6 +301,7 @@ async function main() {
300
301
  app.use('/api', buildRateLimiter(), authMiddleware(), apiRoutes);
301
302
  app.use('/api/pipeline', authMiddleware(), pipelineRoutes);
302
303
  app.use('/api/rag', authMiddleware(), ragRoutes);
304
+ app.use('/api/wiki', authMiddleware(), wikiRoutes);
303
305
 
304
306
  // SPA fallback: serve index.html for non-API, non-static routes
305
307
  app.get('/{*splat}', (_req, res) => {
@@ -14,7 +14,7 @@ import {
14
14
  removeTombstone,
15
15
  } from './db.js';
16
16
  import { publish as publishEvent } from './event-bus.js';
17
- import { getSharedPlansDir, SHARED_DIR_NAME, isSharedPath } from '../../../src/utils/shared-paths.js';
17
+ import { getSharedPlansDir, SHARED_DIR_NAME, isSharedPath } from '@karajan/core/shared-paths';
18
18
  import { readConfig } from './config-yaml.js';
19
19
 
20
20
  /**
@@ -3,5 +3,16 @@
3
3
  # never serves stale code. Installed by `kj rag install-hooks`. The marker
4
4
  # in this comment is how the installer tells kj-managed hooks from user hooks.
5
5
  set -euo pipefail
6
- if ! command -v kj >/dev/null 2>&1; then exit 0; fi
7
- kj rag index --since auto >/dev/null 2>&1 || true
6
+ if command -v kj >/dev/null 2>&1; then
7
+ kj rag index --since auto >/dev/null 2>&1 || true
8
+ fi
9
+
10
+ # KJC-TSK-0507 — Refresh the per-project QMD semantic wiki when the merge
11
+ # touched indexable folders (docs/, .reviews/, .karajan/plans/). Background
12
+ # so it never blocks the user's `git pull` / `git merge`.
13
+ if command -v qmd >/dev/null 2>&1; then
14
+ if git diff-tree -r --name-only ORIG_HEAD HEAD 2>/dev/null \
15
+ | grep -qE '^(docs/|\.reviews/|\.karajan/plans/)'; then
16
+ (qmd update >/dev/null 2>&1 || true) &
17
+ fi
18
+ fi
@@ -0,0 +1,102 @@
1
+ // AI-slop tells detector — KJC-TSK-0503. Deterministic, offline,
2
+ // regex-based scanner over JS/TS source. Five categories:
3
+ // tautologic-comments, banner-separators, boilerplate-docstrings,
4
+ // magic-fallbacks, redundant-jsdoc. Score 100 = clean.
5
+
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+
9
+ const FILE_EXTS = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"]);
10
+ const IGNORE_DIRS = new Set([
11
+ "node_modules", "dist", "build", "coverage",
12
+ ".git", ".karajan", "public", ".next", ".nuxt", ".vercel", ".cache",
13
+ ]);
14
+ const VERB = "(?:returns?|gets?|sets?|fetches?|loads?|saves?|creates?|builds?|makes?|computes?|handles?|checks?|validates?)";
15
+ const LINE_PATTERNS = {
16
+ "banner-separators": /\/[/*]\s*[=*\-_#~]{5,}/,
17
+ "magic-fallbacks": /(\?\?|\|\|)\s*(?:""|''|null|undefined)\s*(?:\)|;|,|$)/,
18
+ "redundant-jsdoc": new RegExp(`@param\\s+\\{[^}]+\\}\\s+(\\w+)\\s+(?:-\\s*)?(?:the\\s+)?\\1\\s+(?:parameter|argument|value)`, "i"),
19
+ };
20
+ const TAUT_RE = new RegExp(`^\\s*//\\s*${VERB}\\s+(?:the\\s+|a\\s+|an\\s+)?(\\w+)\\s*\\.?\\s*$`, "i");
21
+ const DOC_RE = new RegExp(`^\\s*/\\*\\*\\s*${VERB}\\s+(?:the\\s+|a\\s+|an\\s+)?(\\w+)\\s*\\.?\\s*\\*/\\s*$`, "i");
22
+ const DECL_RE = /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|const|let|var|class)\s+(\w+)/;
23
+
24
+ export const CATEGORIES = Object.freeze([
25
+ "tautologic-comments", "banner-separators", "boilerplate-docstrings",
26
+ "magic-fallbacks", "redundant-jsdoc",
27
+ ]);
28
+
29
+ function nextDecl(lines, from) {
30
+ for (let j = from; j < Math.min(from + 3, lines.length); j++) {
31
+ const m = lines[j]?.match(DECL_RE);
32
+ if (m) return m[1];
33
+ }
34
+ return null;
35
+ }
36
+ const stem = (a, b) => { const al = a.toLowerCase(), bl = b.toLowerCase(); return al === bl || al.includes(bl) || bl.includes(al); };
37
+
38
+ export function scanText(relPath, text) {
39
+ const out = [];
40
+ const lines = text.split(/\r?\n/);
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i];
43
+ const lineNo = i + 1;
44
+ for (const [category, re] of Object.entries(LINE_PATTERNS)) {
45
+ if (re.test(line)) out.push({ path: relPath, line: lineNo, category, snippet: line.trim().slice(0, 100) });
46
+ }
47
+ const t = line.match(TAUT_RE);
48
+ if (t) {
49
+ const decl = nextDecl(lines, i + 1);
50
+ if (decl && stem(decl, t[1])) out.push({ path: relPath, line: lineNo, category: "tautologic-comments", snippet: line.trim().slice(0, 100) });
51
+ }
52
+ const d = line.match(DOC_RE);
53
+ if (d) {
54
+ const decl = nextDecl(lines, i + 1);
55
+ if (decl && stem(decl, d[1])) out.push({ path: relPath, line: lineNo, category: "boilerplate-docstrings", snippet: line.trim().slice(0, 100) });
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function listSourceFiles(root) {
62
+ const out = [];
63
+ const stack = [root];
64
+ while (stack.length) {
65
+ const dir = stack.pop();
66
+ let entries;
67
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
68
+ for (const e of entries) {
69
+ const full = path.join(dir, e.name);
70
+ if (e.isDirectory()) {
71
+ if (IGNORE_DIRS.has(e.name) || e.name.startsWith(".")) continue;
72
+ stack.push(full);
73
+ } else if (FILE_EXTS.has(path.extname(e.name))) out.push(full);
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+
79
+ export async function collectAiSlop(rootDir, opts = {}) {
80
+ const maxLOC = Number.isFinite(opts.maxLOC) ? opts.maxLOC : 200_000;
81
+ const maxWorst = Number.isFinite(opts.maxWorst) ? opts.maxWorst : 10;
82
+ try {
83
+ const findings = [];
84
+ let totalLOC = 0, filesScanned = 0;
85
+ for (const file of listSourceFiles(rootDir)) {
86
+ if (totalLOC >= maxLOC) break;
87
+ let content;
88
+ try { content = fs.readFileSync(file, "utf-8"); } catch { continue; }
89
+ findings.push(...scanText(path.relative(rootDir, file) || file, content));
90
+ totalLOC += content.split(/\r?\n/).length;
91
+ filesScanned++;
92
+ }
93
+ const byCategory = Object.fromEntries(CATEGORIES.map((c) => [c, 0]));
94
+ for (const f of findings) byCategory[f.category]++;
95
+ const density = totalLOC > 0 ? (findings.length / totalLOC) * 100 : 0;
96
+ const score = Math.max(0, Math.min(100, Math.round(100 - density * 100)));
97
+ const worst = findings.slice().sort((a, b) => (a.path === b.path ? a.line - b.line : a.path.localeCompare(b.path))).slice(0, maxWorst);
98
+ return { available: true, filesScanned, totalLOC, total: findings.length, score, byCategory, worst };
99
+ } catch (err) {
100
+ return { available: false, reason: err?.message || String(err) };
101
+ }
102
+ }
@@ -41,11 +41,28 @@ export function formatDeterministicSummary(ctx) {
41
41
  if (ctx.circularDeps) lines.push(...formatCircularDepsBlock(ctx.circularDeps));
42
42
  if (ctx.deadExports) lines.push(...formatDeadExportsBlock(ctx.deadExports));
43
43
  if (ctx.injectionFindings) lines.push(...formatInjectionBlock(ctx.injectionFindings));
44
+ if (ctx.aiSlop) lines.push(...formatAiSlopBlock(ctx.aiSlop));
44
45
  if (ctx.webperf) lines.push(...formatWebperfBlock(ctx.webperf));
45
46
 
46
47
  return lines.join("\n");
47
48
  }
48
49
 
50
+ function formatAiSlopBlock(slop) {
51
+ if (!slop.available) return ["### AI-slop tells (deterministic)", `- Status: not available — ${slop.reason || "scan failed"}`, ""];
52
+ const lines = ["### AI-slop tells (deterministic)", `- Files scanned: ${slop.filesScanned ?? 0}`, `- Score: ${slop.score}/100 (100 = clean)`, `- Findings: ${slop.total ?? 0}`];
53
+ if ((slop.total ?? 0) > 0) {
54
+ for (const [cat, n] of Object.entries(slop.byCategory || {})) {
55
+ if (n > 0) lines.push(` - ${cat}: ${n}`);
56
+ }
57
+ if (Array.isArray(slop.worst) && slop.worst.length > 0) {
58
+ lines.push(" - Worst offenders:");
59
+ for (const f of slop.worst) lines.push(` - ${f.path}:${f.line} [${f.category}] ${f.snippet}`);
60
+ }
61
+ }
62
+ lines.push("");
63
+ return lines;
64
+ }
65
+
49
66
  function formatInjectionBlock(inj) {
50
67
  if (!inj.available) return ["### Prompt-injection scan", `- Status: not available — ${inj.reason || "scan failed"}`, ""];
51
68
  const lines = ["### Prompt-injection scan (specs / domain / onboarding)", `- Files scanned: ${inj.scanned ?? 0}`, `- Findings: ${inj.total ?? 0}`];
@@ -278,5 +295,6 @@ export function deterministicContextHasFindings(ctx) {
278
295
  if (ctx.circularDeps?.available && (ctx.circularDeps.total ?? 0) > 0) return true;
279
296
  if (ctx.deadExports?.available && (ctx.deadExports.total ?? 0) > 0) return true;
280
297
  if (ctx.growthDelta && (Math.abs(ctx.growthDelta.lines || 0) > 100 || Math.abs(ctx.growthDelta.deps || 0) > 0)) return true;
298
+ if (ctx.aiSlop?.available && (ctx.aiSlop.total ?? 0) > 0) return true;
281
299
  return false;
282
300
  }
@@ -1,92 +1,10 @@
1
- // KJC-TSK-0414 PR 1: scheduler event-driven para hibernación.
2
- //
3
- // Diseño: setTimeout único por sesión, dispara EXACTAMENTE en cooldownUntil.
4
- // Cero polling. Si el proceso muere antes del fire, reconcileAll() al
5
- // arrancar el board re-programa los timeouts pendientes (o reanuda
6
- // inmediatamente los expirados).
7
-
8
- import { listPendingStandby, acquireStandbyLock, markStandbyDone } from "./standby-store.js";
9
-
10
- // Mapa sessionId → NodeJS.Timeout — accesible para test/cancel.
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
- // KJC-TSK-0414 PR 1: persistencia de sesiones hibernadas al disco.
2
- //
3
- // Cuando Brain decide hibernar (QUOTA_EXHAUSTED_DAILY con retryAfter > 5min),
4
- // serializa el estado completo del run en ~/.kj/standby/<sessionId>.json.
5
- // El proceso de kj run muere (exit 0) tras persistir; el board (o kj resume)
6
- // lo reanuda cuando llega cooldownUntil.
7
- //
8
- // Layout en disco:
9
- // ~/.kj/standby/
10
- // <sessionId>.json ← sesiones pendientes (scheduler les mete setTimeout)
11
- // <sessionId>.lock ← lockfile durante spawn de kj resume
12
- // done/
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
+ }
@@ -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 });