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.
Files changed (68) 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 +21 -1
  7. package/packages/hu-board/public/utils/formatters.js +68 -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 +137 -0
  11. package/packages/hu-board/src/format.js +124 -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/agents/aider-agent.js +42 -1
  21. package/src/agents/claude-agent.js +14 -3
  22. package/src/agents/codex-agent.js +44 -7
  23. package/src/agents/gemini-agent.js +37 -1
  24. package/src/agents/opencode-agent.js +34 -1
  25. package/src/audit/ai-slop-findings.js +102 -0
  26. package/src/audit/deterministic-summary.js +18 -0
  27. package/src/brain/standby-scheduler.js +10 -92
  28. package/src/brain/standby-store.js +12 -168
  29. package/src/checks/ai-trash.js +34 -0
  30. package/src/checks/qmd.js +41 -0
  31. package/src/checks/squeezr.js +36 -0
  32. package/src/cli/register-meta.js +21 -0
  33. package/src/cli/register-pipeline.js +3 -0
  34. package/src/commands/audit.js +6 -4
  35. package/src/commands/board.js +34 -0
  36. package/src/commands/doctor.js +6 -0
  37. package/src/commands/init.js +73 -8
  38. package/src/commands/qmd.js +86 -0
  39. package/src/commands/resume.js +6 -45
  40. package/src/commands/run.js +6 -50
  41. package/src/git/hu-snapshot.js +10 -105
  42. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +27 -0
  43. package/src/orchestrator/drivers/run-hu-batch.js +5 -1
  44. package/src/orchestrator/flow-runner.js +10 -2
  45. package/src/orchestrator/hu-sub-pipeline.js +82 -8
  46. package/src/plan/plan-hu-ops.js +16 -338
  47. package/src/plan/plan-id.js +4 -45
  48. package/src/plan/plan-validation.js +4 -82
  49. package/src/rag/vec-store.js +17 -253
  50. package/src/roles/audit-role.js +11 -1
  51. package/src/session/journal/summary-writer.js +55 -1
  52. package/src/utils/ai-trash.js +48 -0
  53. package/src/utils/atomic-write.js +6 -43
  54. package/src/utils/budget.js +38 -9
  55. package/src/utils/cli-ask-question.js +76 -0
  56. package/src/utils/doc-links.js +2 -0
  57. package/src/utils/os-detect.js +10 -0
  58. package/src/utils/paths.js +14 -123
  59. package/src/utils/port-check.js +6 -45
  60. package/src/utils/process.js +4 -188
  61. package/src/utils/qmd-collection.js +72 -0
  62. package/src/utils/qmd-detect.js +18 -0
  63. package/src/utils/qmd-install.js +37 -0
  64. package/src/utils/run-registry.js +9 -116
  65. package/src/utils/shared-paths.js +12 -41
  66. package/src/utils/squeezr-detect.js +18 -0
  67. package/src/utils/squeezr-install.js +37 -0
  68. package/src/utils/telemetry.js +43 -0
@@ -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 });
@@ -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");
@@ -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
@@ -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
  }