karajan-code 4.35.1 → 4.36.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/package.json +1 -1
- package/packages/hu-board/public/utils/board-view.js +2 -1
- package/packages/hu-board/public/utils/config-editor.js +5 -2
- package/packages/hu-board/public/utils/terminal-panel.js +2 -1
- package/packages/hu-board/src/config-yaml.js +11 -9
- package/packages/hu-board/src/project-dir.js +42 -0
- package/packages/hu-board/src/routes/api.js +11 -3
- package/packages/hu-board/src/terminal-wire.js +5 -1
- package/packages/hu-board/src/terminal.js +8 -3
- package/src/checks/rag-coverage.js +5 -4
- package/src/cli/register-meta.js +15 -4
- package/src/commands/board.js +20 -8
- package/src/commands/bootstrap.js +2 -0
- package/src/commands/env.js +8 -4
- package/src/commands/harden.js +26 -4
- package/src/commands/init.js +2 -1
- package/src/commands/plan/generate.js +4 -6
- package/src/commands/rag.js +69 -10
- package/src/commands/review-gate.js +14 -5
- package/src/commands/watch.js +13 -10
- package/src/config/defaults.js +3 -2
- package/src/environment/playbook.js +10 -3
- package/src/harden/guidelines-engine.js +24 -7
- package/src/harden/sentinel-hooks.js +68 -7
- package/src/lang/registry.js +2 -1
- package/src/mcp/handlers/rag-handler.js +14 -4
- package/src/orchestrator/drivers/post-loop.js +13 -11
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +3 -2
- package/src/orchestrator/stages/rag-context-stage.js +5 -2
- package/src/rag/auto-update.js +5 -3
- package/src/rag/coverage.js +13 -12
- package/src/rag/indexable.js +97 -0
- package/src/rag/indexer.js +31 -13
- package/src/rag/library.js +19 -13
- package/src/rag/migrate.js +73 -0
- package/src/rag/onnx-fallback.js +5 -3
- package/src/rag/project-store.js +48 -0
- package/src/rag/watcher.js +25 -14
- package/src/review/kj-provenance.js +26 -0
- package/src/review/verdict-store.js +6 -1
- package/src/start/sweep.js +4 -3
- package/src/utils/rulesync.js +61 -0
package/src/commands/rag.js
CHANGED
|
@@ -1,19 +1,49 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
1
3
|
// KJC-PCS-0049 Step 6 — `kj rag` command group. Two subcommands:
|
|
2
4
|
// kj rag index [--project <slug>] [--with-sources]
|
|
3
5
|
// kj rag query <text> [--scope plans|code|onboarding|all] [--top-k N] [--json]
|
|
4
6
|
// Closes the v2.22.0 RAG MVP end-to-end from the terminal.
|
|
5
|
-
import {
|
|
7
|
+
import { countChunks, deleteChunksBySource, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "../rag/vec-store.js";
|
|
6
8
|
import { makeGovernedEmbedder } from "../rag/governed-embedder.js";
|
|
7
|
-
import { indexProject, indexProjectDelta } from "../rag/indexer.js";
|
|
9
|
+
import { indexFile, indexProject, indexProjectDelta, sourceKind } from "../rag/indexer.js";
|
|
10
|
+
import { indexableReason, ragExclude } from "../rag/indexable.js";
|
|
8
11
|
import { fileIndexState } from "../rag/coverage.js";
|
|
9
12
|
import { query } from "../rag/retriever.js";
|
|
10
13
|
import { installPostMergeHook, maybeAutoUpdate } from "../rag/auto-update.js";
|
|
11
14
|
import { indexLibrary, LIBRARY_PROJECT } from "../rag/library.js";
|
|
12
15
|
import { loadGoldenQueries, runEval } from "../rag/eval.js";
|
|
13
16
|
import { getKarajanHome } from "../utils/paths.js";
|
|
17
|
+
import { countProjectChunks, emptyIndexRemedy, migrateProjectIndex } from "../rag/migrate.js";
|
|
18
|
+
import { openLibraryStore, openProjectStore, projectDbPath } from "../rag/project-store.js";
|
|
14
19
|
|
|
20
|
+
// KJC-TSK-0882 (ADR 0011): el indice es del proyecto, no de la maquina.
|
|
15
21
|
function openDb(config) {
|
|
16
|
-
return
|
|
22
|
+
return openProjectStore({ projectDir: config?.projectDir || process.cwd(), dim: config?.rag?.embedder?.dim || 768 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function indexFiles({ files, projectDir, db, embedder, slug, logger, json, exclude }) {
|
|
26
|
+
const totals = { indexed: 0, failed: 0, files: 0, deleted: 0 };
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const abs = isAbsolute(file) ? file : join(projectDir, file);
|
|
29
|
+
if (!existsSync(abs)) {
|
|
30
|
+
totals.deleted += deleteChunksBySource(db, abs); // gone from disk: gone from the index
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// KJC-TSK-0891: the same criterion and kind as the full walk, or the
|
|
34
|
+
// remedy rag-first names would index nothing for a .mdx or a .astro.
|
|
35
|
+
const out = indexableReason(relative(projectDir, abs), abs, { exclude, projectDir });
|
|
36
|
+
if (out) {
|
|
37
|
+
logger.warn?.(`[rag] ${out}: not indexed`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const r = await indexFile(abs, { db, embedder, logger, project: slug, kind: sourceKind(abs) });
|
|
41
|
+
totals.indexed += r.indexed; totals.failed += r.failed; totals.files += 1;
|
|
42
|
+
}
|
|
43
|
+
const deleted = totals.deleted ? `, ${totals.deleted} chunks deleted` : "";
|
|
44
|
+
if (json) process.stdout.write(`${JSON.stringify(totals)}\n`);
|
|
45
|
+
else logger.info(`[rag] indexed ${totals.indexed} chunk(s) across ${totals.files} file(s) (${totals.failed} failed${deleted})`);
|
|
46
|
+
return totals;
|
|
17
47
|
}
|
|
18
48
|
|
|
19
49
|
export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
@@ -22,6 +52,9 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
22
52
|
try {
|
|
23
53
|
const slug = projectSlug(projectDir);
|
|
24
54
|
const embedder = makeGovernedEmbedder(config);
|
|
55
|
+
// KJC-TSK-0891: exactly these files. The remedy rag-first names for a file
|
|
56
|
+
// missing from the index: `--since auto` only reaches what changed.
|
|
57
|
+
if (flags.file?.length) return await indexFiles({ files: flags.file, projectDir, db, embedder, slug, logger, json: flags.json, exclude: ragExclude(config) });
|
|
25
58
|
const sinceFlag = flags.since;
|
|
26
59
|
// KJC-TSK-0455 — `--since auto` resolves to the last commit we indexed;
|
|
27
60
|
// an explicit ref is honoured as-is. Without a baseline (first-time
|
|
@@ -35,7 +68,7 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
35
68
|
let totals;
|
|
36
69
|
if (since) {
|
|
37
70
|
try {
|
|
38
|
-
totals = await indexProjectDelta(projectDir, { db, embedder, since, logger });
|
|
71
|
+
totals = await indexProjectDelta(projectDir, { db, embedder, since, logger, exclude: ragExclude(config) });
|
|
39
72
|
} catch (err) {
|
|
40
73
|
logger.warn?.(`[rag] delta index failed (${err.message}); falling back to full index`);
|
|
41
74
|
totals = null;
|
|
@@ -46,13 +79,17 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
46
79
|
db, embedder,
|
|
47
80
|
karajanHome: getKarajanHome(), logger,
|
|
48
81
|
withSources: Boolean(flags.withSources),
|
|
82
|
+
exclude: ragExclude(config),
|
|
49
83
|
});
|
|
50
84
|
}
|
|
51
85
|
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
52
86
|
// KJC-TSK-0697 — the library corpus refreshes on every index run.
|
|
53
87
|
// Best-effort and cheap: a handful of cards, content-hash dedup.
|
|
88
|
+
// KJC-TSK-0883: el canon de la maquina a su base; las fichas del proyecto, a la suya.
|
|
54
89
|
try {
|
|
55
|
-
|
|
90
|
+
const lib = openLibraryStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
91
|
+
try { await indexLibrary({ db: lib, embedder, logger, scope: "machine" }); } finally { lib.close(); }
|
|
92
|
+
await indexLibrary({ db, embedder, logger, projectDir, scope: "project", project: slug });
|
|
56
93
|
} catch (err) {
|
|
57
94
|
logger.warn?.(`[rag] library index failed: ${err.message}`);
|
|
58
95
|
}
|
|
@@ -87,7 +124,8 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
87
124
|
// (--no-rag-update / config.rag.autoUpdate); failures degrade to a warn
|
|
88
125
|
// inside maybeAutoUpdate and the query proceeds with the current index.
|
|
89
126
|
await maybeAutoUpdate({ projectDir: config?.projectDir || process.cwd(), config, logger, flags });
|
|
90
|
-
|
|
127
|
+
// KJC-TSK-0883: --library abre la base del canon, no la del proyecto.
|
|
128
|
+
const db = flags.library ? openLibraryStore({ dim: config?.rag?.embedder?.dim || 768 }) : openDb(config);
|
|
91
129
|
try {
|
|
92
130
|
const topK = Math.max(1, Number(flags.topK) || 5);
|
|
93
131
|
const scope = flags.scope || "all";
|
|
@@ -107,9 +145,12 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
107
145
|
// deterministic recovery signal. The CLI was emitting just `[]`, which
|
|
108
146
|
// is indistinguishable from "query returned zero hits over a populated
|
|
109
147
|
// store" and forced consumers to parse stderr.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
148
|
+
// KJC-TSK-0882: cuenta lo de ESTE proyecto; chunks ajenos no son respuesta.
|
|
149
|
+
if (countChunks(db) === 0 || (project && countProjectChunks(db, project) === 0)) {
|
|
150
|
+
// La library se rellena en cada `kj rag index`; migrar la llevaria al proyecto.
|
|
151
|
+
const remedy = library ? "kj rag index" : emptyIndexRemedy({ slug: project, legacyPath: join(getKarajanHome(), "rag.db"), dim: config?.rag?.embedder?.dim || 768 });
|
|
152
|
+
logger.warn(`[rag] ${library ? "the library index" : "this project's index"} holds no chunks of ${project || "any project"}: this is not an answer, run ${remedy}`);
|
|
153
|
+
if (flags.json) process.stdout.write(`${JSON.stringify({ hits: [], empty: true, topK, scope, remedy })}\n`);
|
|
113
154
|
return [];
|
|
114
155
|
}
|
|
115
156
|
const mode = flags.mode || "hybrid";
|
|
@@ -183,7 +224,7 @@ export async function ragCoversCommand({ file, config, flags = {} }) {
|
|
|
183
224
|
const projectDir = config?.projectDir || process.cwd();
|
|
184
225
|
const db = openDb(config);
|
|
185
226
|
try {
|
|
186
|
-
const state = fileIndexState(projectDir, file, { db });
|
|
227
|
+
const state = fileIndexState(projectDir, file, { db, exclude: ragExclude(config) });
|
|
187
228
|
if (flags.json) process.stdout.write(`${JSON.stringify(state)}\n`);
|
|
188
229
|
else process.stdout.write(`${state.state}: ${state.reason}\n`);
|
|
189
230
|
if (!state.canAnswer) process.exitCode = 1;
|
|
@@ -192,3 +233,21 @@ export async function ragCoversCommand({ file, config, flags = {} }) {
|
|
|
192
233
|
db.close();
|
|
193
234
|
}
|
|
194
235
|
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* KJC-TSK-0888 (RAG-P1a, ADR 0011) — `kj rag migrate`: copia los chunks de este
|
|
239
|
+
* proyecto desde la base global a su propio indice, con sus embeddings, sin
|
|
240
|
+
* volver a embeber. La base global no se toca.
|
|
241
|
+
*/
|
|
242
|
+
export async function ragMigrateCommand({ config, flags = {} }) {
|
|
243
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
244
|
+
const res = migrateProjectIndex({
|
|
245
|
+
slug: projectSlug(projectDir),
|
|
246
|
+
legacyPath: join(getKarajanHome(), "rag.db"),
|
|
247
|
+
targetPath: projectDbPath(projectDir),
|
|
248
|
+
dim: config?.rag?.embedder?.dim || 768,
|
|
249
|
+
});
|
|
250
|
+
if (flags.json) process.stdout.write(`${JSON.stringify(res)}\n`);
|
|
251
|
+
else process.stdout.write(`rag migrate: ${res.state} — ${res.reason}\n`);
|
|
252
|
+
return res;
|
|
253
|
+
}
|
|
@@ -188,6 +188,14 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
188
188
|
// KJC-TSK-0687 (MG-B): the verifiable half of TDD — sources without a
|
|
189
189
|
// single test change warn (block via method_gates.tests_with_code).
|
|
190
190
|
const changedFiles = (await rawDiff(flags.range, ["--name-only"])).split("\n").map((f) => f.trim()).filter(Boolean);
|
|
191
|
+
// KJC-BUG-0229: un fichero BORRADO no es una fuente que el scan pueda indexar
|
|
192
|
+
// ni sobre la que el RAG pueda responder, asi que exigirlo era pedir lo
|
|
193
|
+
// imposible: una PR que retiraba 45 ficheros se paro dos veces y acumulo tres
|
|
194
|
+
// excepciones por la misma causa mecanica. Los requisitos de Sonar y de RAG
|
|
195
|
+
// se evaluan sobre lo que sigue VIVO tras el cambio (A, C, M, R); policy, LOC
|
|
196
|
+
// y tests-with-code siguen viendo el diff entero, porque ahi un borrado si
|
|
197
|
+
// cuenta. El borrado danino lo cubre la review cruzada, que lee todo el diff.
|
|
198
|
+
const liveFiles = (await rawDiff(flags.range, ["--name-only", "--diff-filter=ACMR"])).split("\n").map((f) => f.trim()).filter(Boolean);
|
|
191
199
|
|
|
192
200
|
// KJC-TSK-0688 (MG-C) + KJC-TSK-0691 (MG-E): oversized-diff policy.
|
|
193
201
|
// Field case: a 522-line PR sailed past the warning with a
|
|
@@ -357,6 +365,7 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
357
365
|
}
|
|
358
366
|
}
|
|
359
367
|
let res = await checkVerdict(projectDir, diff);
|
|
368
|
+
if (res.note) console.log(`ℹ ${res.note}`);
|
|
360
369
|
// KJC-TSK-0838: a reviewed diff with code enters only if the verdict's
|
|
361
370
|
// sonar block proves the analysis ran and covered every staged source.
|
|
362
371
|
// The headless pipeline stamps its verdict after its own sonar stage and
|
|
@@ -371,7 +380,7 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
371
380
|
res = { ok: false, verdict: res.verdict, reason: `Sonar is mandatory for code — ${why}` };
|
|
372
381
|
}
|
|
373
382
|
} else if (res.ok) {
|
|
374
|
-
const req = checkSonarRequirement({ config, stagedFiles:
|
|
383
|
+
const req = checkSonarRequirement({ config, stagedFiles: liveFiles, sonar: res.verdict.sonar, standingExceptions: std.standing });
|
|
375
384
|
if (!req.ok) res = { ok: false, verdict: res.verdict, reason: req.reason };
|
|
376
385
|
else if (req.mode === "granted") console.log(formatSonarGrant(req.grant));
|
|
377
386
|
}
|
|
@@ -387,7 +396,7 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
387
396
|
if (!warnIds.includes(RAG_RULE_ID)) warnIds.push(RAG_RULE_ID);
|
|
388
397
|
} else if (res.ok) {
|
|
389
398
|
const { harness, verified, mismatched, drift } = readRagLedger(projectDir);
|
|
390
|
-
const rreq = checkRagVerdict({ config, stagedFiles:
|
|
399
|
+
const rreq = checkRagVerdict({ config, stagedFiles: liveFiles, rag: res.verdict.rag, harness, verified, mismatched, drift, standingExceptions: std.standing });
|
|
391
400
|
if (!rreq.ok) res = { ok: false, verdict: res.verdict, reason: rreq.reason };
|
|
392
401
|
else if (rreq.mode === "granted" && rreq.grant) console.log(formatRagGrant(rreq.grant));
|
|
393
402
|
}
|
|
@@ -414,7 +423,7 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
414
423
|
if (flags.sonar !== false) {
|
|
415
424
|
// KJC-TSK-0795 AC3: only issues on lines this diff ADDS may veto.
|
|
416
425
|
const touchedLines = addedLinesByFile(await rawDiff(flags.range, ["--unified=0"]));
|
|
417
|
-
const pre = await runSonarPregate({ config, stagedFiles:
|
|
426
|
+
const pre = await runSonarPregate({ config, stagedFiles: liveFiles, touchedLines, logger });
|
|
418
427
|
if (!pre.available) {
|
|
419
428
|
// KJC-BUG-0156 (issue #1543): a scan that FAILED must not read like one
|
|
420
429
|
// more config warning — it hid a dead quality gate for 8 straight PRs.
|
|
@@ -459,7 +468,7 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
459
468
|
// KJC-TSK-0838: fail-CLOSED for code. Sonar disabled, down, skipped by
|
|
460
469
|
// flag, or blind to a staged source — the diff does not reach the reviewer.
|
|
461
470
|
// Docs-only diffs pass; a live human grant on the rule is the only escape.
|
|
462
|
-
const sonarReq = checkSonarRequirement({ config, stagedFiles:
|
|
471
|
+
const sonarReq = checkSonarRequirement({ config, stagedFiles: liveFiles, sonar: sonarRecord, standingExceptions: std.standing });
|
|
463
472
|
if (!sonarReq.ok) {
|
|
464
473
|
console.log(`✗ ${sonarReq.reason}`);
|
|
465
474
|
process.exitCode = 1;
|
|
@@ -476,7 +485,7 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
476
485
|
// the 16-sep bugs were exactly the twin nobody touched.
|
|
477
486
|
const ledger = readRagLedger(projectDir);
|
|
478
487
|
const newFiles = (await rawDiff(flags.range, ["--name-only", "--diff-filter=A"])).split("\n").map((f) => f.trim()).filter(Boolean);
|
|
479
|
-
const ragReq = checkRagRequirement({ config, stagedFiles:
|
|
488
|
+
const ragReq = checkRagRequirement({ config, stagedFiles: liveFiles, newFiles, ledger, standingExceptions: std.standing });
|
|
480
489
|
if (!ragReq.ok) {
|
|
481
490
|
console.log(`✗ ${ragReq.reason}`);
|
|
482
491
|
process.exitCode = 1;
|
package/src/commands/watch.js
CHANGED
|
@@ -8,13 +8,13 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
8
8
|
|
|
9
9
|
export async function watchStartCommand({ config, logger, flags = {} }) {
|
|
10
10
|
const projectDir = config?.projectDir || process.cwd();
|
|
11
|
-
const existing = readPidFile();
|
|
11
|
+
const existing = readPidFile(projectDir);
|
|
12
12
|
if (existing && isPidAlive(existing)) { logger.info(`Watcher already running (pid ${existing}).`); return { pid: existing, started: false }; }
|
|
13
|
-
if (existing) clearPidFile();
|
|
13
|
+
if (existing) clearPidFile(projectDir);
|
|
14
14
|
if (flags.foreground) {
|
|
15
|
-
writePidFile();
|
|
15
|
+
writePidFile(projectDir);
|
|
16
16
|
const stop = startWatcher({ projectDir, config, logger, withSources: !!flags.withSources });
|
|
17
|
-
const shutdown = async () => { await stop(); clearPidFile(); process.exit(0); };
|
|
17
|
+
const shutdown = async () => { await stop(); clearPidFile(projectDir); process.exit(0); };
|
|
18
18
|
process.on("SIGINT", shutdown).on("SIGTERM", shutdown);
|
|
19
19
|
logger.info(`Watcher started in foreground (pid ${process.pid}). Ctrl+C to stop.`);
|
|
20
20
|
return { pid: process.pid, started: true };
|
|
@@ -26,18 +26,21 @@ export async function watchStartCommand({ config, logger, flags = {} }) {
|
|
|
26
26
|
return { pid: child.pid, started: true };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// KJC-TSK-0883: stop/status son del watcher de ESTE proyecto, resuelto como en start.
|
|
30
|
+
export async function watchStopCommand({ config, logger }) {
|
|
31
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
32
|
+
const pid = readPidFile(projectDir);
|
|
31
33
|
if (!pid) { logger.info("No watcher running."); return { stopped: false }; }
|
|
32
|
-
if (!isPidAlive(pid)) { clearPidFile(); logger.info(`Stale PID file removed (process ${pid} was not alive).`); return { stopped: false, stale: true }; }
|
|
34
|
+
if (!isPidAlive(pid)) { clearPidFile(projectDir); logger.info(`Stale PID file removed (process ${pid} was not alive).`); return { stopped: false, stale: true }; }
|
|
33
35
|
try { process.kill(pid, "SIGTERM"); } catch (err) { logger.warn(`Could not signal pid ${pid}: ${err.message}`); }
|
|
34
|
-
clearPidFile();
|
|
36
|
+
clearPidFile(projectDir);
|
|
35
37
|
logger.info(`Watcher (pid ${pid}) stopped.`);
|
|
36
38
|
return { stopped: true, pid };
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
export async function watchStatusCommand({ logger }) {
|
|
40
|
-
const
|
|
41
|
+
export async function watchStatusCommand({ config, logger }) {
|
|
42
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
43
|
+
const pid = readPidFile(projectDir);
|
|
41
44
|
const alive = pid && isPidAlive(pid);
|
|
42
45
|
if (alive) logger.info(`Watcher running (pid ${pid}).`);
|
|
43
46
|
else if (pid) logger.warn(`Stale PID file (process ${pid} not alive). Run \`kj watch stop\` to clean up.`);
|
package/src/config/defaults.js
CHANGED
|
@@ -179,10 +179,11 @@ const DEFAULTS = {
|
|
|
179
179
|
test_inclusions: "**/*.test.js,**/*.spec.js,**/tests/**,**/__tests__/**"
|
|
180
180
|
}
|
|
181
181
|
},
|
|
182
|
+
// KJC-TSK-0884 (ADR 0011): on by default, like Sonar; a declared false opts out.
|
|
182
183
|
hu_board: {
|
|
183
|
-
enabled:
|
|
184
|
+
enabled: true,
|
|
184
185
|
port: 4000,
|
|
185
|
-
auto_start:
|
|
186
|
+
auto_start: true
|
|
186
187
|
},
|
|
187
188
|
language: "en",
|
|
188
189
|
hu_language: "en",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { upsertManagedBlock } from "../utils/managed-markers.js";
|
|
15
|
+
import { detectRulesync, RULESYNC_RULE, rulesyncRuleSeed } from "../utils/rulesync.js";
|
|
15
16
|
import { panelLine } from "./panel.js";
|
|
16
17
|
|
|
17
18
|
// AB-A (KJC-TSK-0650): any agent can be the brain. AGENTS.md is the
|
|
@@ -114,15 +115,21 @@ export async function installPlaybook({ projectDir, target = "all", version = "1
|
|
|
114
115
|
throw new Error(`unknown target "${target}" — use one of: ${PLAYBOOK_TARGETS.join(", ")}`);
|
|
115
116
|
}
|
|
116
117
|
const files = [];
|
|
117
|
-
|
|
118
|
+
// KJC-TSK-0879: in a Rulesync repo its rule source is the only file of truth;
|
|
119
|
+
// `rulesync generate` would erase a block written into the agent files.
|
|
120
|
+
const rulesync = detectRulesync(projectDir);
|
|
121
|
+
for (const file of rulesync ? [RULESYNC_RULE] : TARGET_FILES[target]) {
|
|
118
122
|
const fullPath = path.join(projectDir, file);
|
|
119
|
-
let source = "";
|
|
123
|
+
let source = rulesync ? rulesyncRuleSeed() : "";
|
|
120
124
|
try { source = await fs.readFile(fullPath, "utf8"); } catch { /* new file */ }
|
|
121
125
|
const { content, action } = upsertManagedBlock({
|
|
122
126
|
source, blockId: "playbook", version, body: renderPlaybook({ stateBackend, boardName, config }), style: "html",
|
|
123
127
|
note: "do not edit: regenerated by kj env install",
|
|
124
128
|
});
|
|
125
|
-
if (action !== "unchanged")
|
|
129
|
+
if (action !== "unchanged") {
|
|
130
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
131
|
+
await fs.writeFile(fullPath, content);
|
|
132
|
+
}
|
|
126
133
|
files.push(file);
|
|
127
134
|
}
|
|
128
135
|
return { files, target };
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* content the user keeps outside the markers. Idempotent.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
10
|
|
|
11
11
|
import { upsertManagedBlock } from "../utils/managed-markers.js";
|
|
12
|
+
import { detectRulesync, RULESYNC_RULE, rulesyncRuleSeed } from "../utils/rulesync.js";
|
|
12
13
|
import { guidelinesBody } from "./guidelines-templates.js";
|
|
13
14
|
|
|
14
15
|
const BLOCK_VERSION = 1;
|
|
@@ -28,7 +29,12 @@ export function stripDevHooksBlock(source) {
|
|
|
28
29
|
return { content: `${before}${joiner}${after}`, migrated: true };
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
+
const nonEmpty = (text) => String(text || "").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
33
|
+
const countLines = (text) => nonEmpty(text).length;
|
|
34
|
+
|
|
35
|
+
// KJC-BUG-0232: "do not edit" se leia como "no toques esto", no como "lo que
|
|
36
|
+
// escribas aqui se borra". Solo este bloque: los hooks sellados no cambian.
|
|
37
|
+
const GUIDELINES_NOTE = "regenerated by kj harden: anything written inside is lost; keep your own rules outside this block";
|
|
32
38
|
|
|
33
39
|
export function installGuidelines({ projectDir = process.cwd(), dryRun = false, language = null } = {}) {
|
|
34
40
|
// KJC-BUG-0199 (issue #1773): the rules that enter the agent's context must
|
|
@@ -36,9 +42,13 @@ export function installGuidelines({ projectDir = process.cwd(), dryRun = false,
|
|
|
36
42
|
// the configs; the guidelines just never asked.
|
|
37
43
|
const body = guidelinesBody(language);
|
|
38
44
|
const results = [];
|
|
39
|
-
|
|
45
|
+
// KJC-TSK-0879: with Rulesync, its source is the file of truth; writing into
|
|
46
|
+
// CLAUDE.md/AGENTS.md would be erased by the next `rulesync generate`.
|
|
47
|
+
const rulesync = detectRulesync(projectDir);
|
|
48
|
+
for (const file of rulesync ? [RULESYNC_RULE] : TARGETS) {
|
|
40
49
|
const target = join(projectDir, file);
|
|
41
|
-
const
|
|
50
|
+
const seed = rulesync ? rulesyncRuleSeed() : "";
|
|
51
|
+
const raw = existsSync(target) ? readFileSync(target, "utf8") : seed;
|
|
42
52
|
const { content: source, migrated } = stripDevHooksBlock(raw);
|
|
43
53
|
const { content, action } = upsertManagedBlock({
|
|
44
54
|
source,
|
|
@@ -46,13 +56,20 @@ export function installGuidelines({ projectDir = process.cwd(), dryRun = false,
|
|
|
46
56
|
version: BLOCK_VERSION,
|
|
47
57
|
body,
|
|
48
58
|
style: "html",
|
|
59
|
+
note: GUIDELINES_NOTE,
|
|
49
60
|
});
|
|
50
61
|
// KJC-BUG-0206: harden removing rules is a fact worth saying. It silently
|
|
51
62
|
// dropped six lines from this repo's own files because the detected language
|
|
52
63
|
// had no block, and "updated" read like an improvement.
|
|
53
64
|
const shrank = countLines(content) < countLines(raw);
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
// KJC-BUG-0232: decir CUALES se van, o el aviso obliga a salir a git diff.
|
|
66
|
+
const kept = new Set(nonEmpty(content));
|
|
67
|
+
const removed = shrank ? nonEmpty(raw).filter((l) => !kept.has(l) && !l.includes("kj:managed:")) : [];
|
|
68
|
+
if (!dryRun && (migrated || action !== "unchanged")) {
|
|
69
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
70
|
+
writeFileSync(target, content);
|
|
71
|
+
}
|
|
72
|
+
results.push({ file, action: migrated ? "migrated" : action, ...(shrank ? { shrank: true, removed } : {}) });
|
|
56
73
|
}
|
|
57
74
|
return { dryRun, guidelines: results };
|
|
58
75
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* hooks, which is why the guaranteed level requires Claude as host (ADR).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
11
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
import { join } from "node:path";
|
|
@@ -131,6 +131,7 @@ const POST_BODY = `#!/usr/bin/env node
|
|
|
131
131
|
// kj sentinel state writer (KJC-TSK-0713) — managed by \`kj harden\`.
|
|
132
132
|
// Records deterministic method facts per session; never blocks, never fails
|
|
133
133
|
// a tool call (PostToolUse, always exit 0).
|
|
134
|
+
import console from "node:console";
|
|
134
135
|
import process from "node:process";
|
|
135
136
|
import { relative } from "node:path";
|
|
136
137
|
import { spawnSync } from "node:child_process";
|
|
@@ -256,7 +257,16 @@ process.stdin.on("end", () => {
|
|
|
256
257
|
const state = load();
|
|
257
258
|
const s = session(state, sid);
|
|
258
259
|
const ref = CARD.exec(headRef || branchOf() || "");
|
|
259
|
-
|
|
260
|
+
// KJC-BUG-0230: una EPICA (…-PCS-…) no tiene estado que mover: el PG
|
|
261
|
+
// la deja en To Do por mas status que reciba y kj hu move no la
|
|
262
|
+
// conoce. Registrarla como pendiente era un bloqueo sin cauce. Se dice
|
|
263
|
+
// en el momento, para que no parezca que el board dejo de mirar.
|
|
264
|
+
const epic = ref && /-PCS-/i.test(ref[0]);
|
|
265
|
+
if (epic) {
|
|
266
|
+
console.error("karajan sentinel: board-sync — la PR #" + mergedPr + " pertenece a la epica " + ref[0].toUpperCase() + ", que no tiene estado que mover: no queda pendiente (su cierre va en la descripcion de la epica)");
|
|
267
|
+
} else {
|
|
268
|
+
(s.pending_moves ||= []).push({ card: ref ? ref[0].toUpperCase() : null, pr: mergedPr, at: Date.now(), ...(headRef ? {} : { head: "unknown" }) });
|
|
269
|
+
}
|
|
260
270
|
save(state);
|
|
261
271
|
}
|
|
262
272
|
const moved = /kj\\s+hu\\s+move\\s+([A-Za-z0-9-]+)\\s+([a-z&-]+)/i.exec(String(input.command || ""));
|
|
@@ -980,9 +990,19 @@ process.stdin.on("end", () => {
|
|
|
980
990
|
// cualquier proyecto sin indexar. Nada legible = se bloquea.
|
|
981
991
|
const cov = spawnSync("kj", ["rag", "covers", rel, "--json"], { cwd: ROOT, encoding: "utf8" });
|
|
982
992
|
let blind = null;
|
|
983
|
-
|
|
993
|
+
let absent = null;
|
|
994
|
+
try {
|
|
995
|
+
const c = JSON.parse(String(cov.stdout || ""));
|
|
996
|
+
if (c.state === "not-indexable") blind = c.reason;
|
|
997
|
+
// KJC-TSK-0891: si el indice no tiene el fichero, consultar no puede
|
|
998
|
+
// devolverlo; el remedio es indexarlo, y se nombra.
|
|
999
|
+
if (c.state === "stale" || c.state === "index-empty") absent = c.reason;
|
|
1000
|
+
} catch { blind = null; }
|
|
984
1001
|
if (blind) {
|
|
985
1002
|
console.error("karajan sentinel: rag-first — " + blind + " — no se te exige consultar lo que el indice no puede tener; el gate lo anota y sigue" + doc("rag-first"));
|
|
1003
|
+
} else if (absent) {
|
|
1004
|
+
console.error("karajan sentinel: rag-first — el indice aun no tiene " + rel + " (" + absent + "); indexalo y consulta despues: kj_rag_query / kj rag query <que hace " + rel + ">" + doc("rag-first"));
|
|
1005
|
+
process.exit(2);
|
|
986
1006
|
} else {
|
|
987
1007
|
console.error("karajan sentinel: rag-first — el RAG no ha respondido sobre " + rel + " en esta sesion; consulta antes de tocarlo: kj_rag_query / kj rag query <que hace " + rel + " y donde mas vive ese concepto>. (KJ_ALLOW_NO_RAG=1 = excepcion consciente, queda registrada)" + doc("rag-first"));
|
|
988
1008
|
process.exit(2);
|
|
@@ -1159,7 +1179,33 @@ function sealedByPath(projectDir, gitShowFn) {
|
|
|
1159
1179
|
}
|
|
1160
1180
|
|
|
1161
1181
|
/**
|
|
1162
|
-
*
|
|
1182
|
+
* KJC-BUG-0224: la procedencia PROPIA del harness, lo que `kj harden` escribio
|
|
1183
|
+
* (sha256 por guardia). Distinto del sello humano: no concede nada, solo dice
|
|
1184
|
+
* "esto lo puso kj", y lo unico que habilita es regenerar desde el kj
|
|
1185
|
+
* instalado. Forjarlo, por tanto, solo consigue que el guardia se restaure.
|
|
1186
|
+
*/
|
|
1187
|
+
const INSTALLED_RECORD = "installed.json";
|
|
1188
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
1189
|
+
|
|
1190
|
+
function readInstalledRecord(dir) {
|
|
1191
|
+
try {
|
|
1192
|
+
const files = JSON.parse(readFileSync(join(dir, INSTALLED_RECORD), "utf8"))?.files;
|
|
1193
|
+
return files && typeof files === "object" ? files : {};
|
|
1194
|
+
} catch {
|
|
1195
|
+
return {};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function writeInstalledRecord(dir) {
|
|
1200
|
+
const files = {};
|
|
1201
|
+
for (const name of Object.keys(SCRIPT_BODIES)) {
|
|
1202
|
+
try { files[name] = sha256(readFileSync(join(dir, name), "utf8")); } catch { /* sin fichero no hay nada que registrar */ }
|
|
1203
|
+
}
|
|
1204
|
+
writeFileSync(join(dir, INSTALLED_RECORD), `${JSON.stringify({ files }, null, 2)}\n`);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* @returns {{ok: boolean, mismatched: string[], drift?: string[], regenerated?: string[], reason?: string}}
|
|
1163
1209
|
* `mismatched` is TAMPERING: a guard holds content nobody sealed, and that is
|
|
1164
1210
|
* what the hooks block on. `drift` is a sealed guard whose templates have
|
|
1165
1211
|
* moved on, which `kj harden` applies and which is not a defect.
|
|
@@ -1185,16 +1231,30 @@ export function verifySentinelScripts({ projectDir, readFileFn = readFileSync, g
|
|
|
1185
1231
|
// cannot be borrowed by also editing a template: that was the hole in the
|
|
1186
1232
|
// first attempt at this fix, and the review was right to reject it.
|
|
1187
1233
|
const sealed = sealedByPath(root, gitShowFn);
|
|
1234
|
+
const ownRecord = readInstalledRecord(dir);
|
|
1188
1235
|
const drift = [];
|
|
1189
1236
|
const tampered = [];
|
|
1237
|
+
const regenerated = [];
|
|
1190
1238
|
for (const name of mismatched) {
|
|
1191
1239
|
const text = installed.get(name);
|
|
1192
|
-
const
|
|
1193
|
-
if (
|
|
1240
|
+
const hash = text === undefined ? null : sha256(text);
|
|
1241
|
+
if (hash && sealed.get(`.karajan/harness/${name}`) === hash) drift.push(name);
|
|
1242
|
+
else if (hash && ownRecord[name] === hash) regenerated.push(name);
|
|
1194
1243
|
else tampered.push(name);
|
|
1195
1244
|
}
|
|
1196
|
-
|
|
1245
|
+
// KJC-BUG-0224 caso 1: lo que kj escribio y nadie sello ni toco se pone al
|
|
1246
|
+
// dia solo. Es lo que hacia el ritual de `kj harden`, hecho por la maquina.
|
|
1247
|
+
if (regenerated.length > 0) {
|
|
1248
|
+
for (const name of regenerated) writeHarnessScript(root, name, SCRIPT_BODIES[name]);
|
|
1249
|
+
writeInstalledRecord(dir);
|
|
1250
|
+
}
|
|
1251
|
+
const done = regenerated.length ? { regenerated } : {};
|
|
1252
|
+
if (tampered.length > 0) return { ok: false, mismatched: tampered, ...(drift.length ? { drift } : {}), ...done };
|
|
1253
|
+
if (drift.length === 0) {
|
|
1254
|
+
return { ok: true, mismatched: [], ...done, reason: `kj avanzó y estos guardias eran los que kj escribió: regenerados (${regenerated.join(", ")})` };
|
|
1255
|
+
}
|
|
1197
1256
|
return {
|
|
1257
|
+
...done,
|
|
1198
1258
|
ok: true,
|
|
1199
1259
|
mismatched: [],
|
|
1200
1260
|
drift,
|
|
@@ -1243,6 +1303,7 @@ export function installSentinelHooks({ projectDir = process.cwd(), logger = cons
|
|
|
1243
1303
|
}
|
|
1244
1304
|
return writeHarnessScript(projectDir, name, body);
|
|
1245
1305
|
});
|
|
1306
|
+
writeInstalledRecord(join(projectDir, ".karajan", "harness"));
|
|
1246
1307
|
if (deferred.length > 0) {
|
|
1247
1308
|
logger?.info?.(`kj harden: ${deferred.join(", ")} se queda como está — es lo que un humano selló, y avanzarlo es suyo: kj harden --commit`);
|
|
1248
1309
|
}
|
package/src/lang/registry.js
CHANGED
|
@@ -14,7 +14,8 @@ import { RUST_ADAPTER } from "./rust.js";
|
|
|
14
14
|
// Segmentos siempre excluidos del walk, independientes de stack: VCS,
|
|
15
15
|
// outputs de build, scratch de Karajan, sandbox del audit. Lo que viva
|
|
16
16
|
// aquí jamás se indexa.
|
|
17
|
-
|
|
17
|
+
// KJC-BUG-0231: `.claude` guarda worktrees de subagentes, copias enteras del repo.
|
|
18
|
+
const COMMON_SKIP_SEGMENTS = [".git", "dist", "build", "coverage", ".karajan", ".kj", "_diet", ".claude"];
|
|
18
19
|
|
|
19
20
|
const REGISTRY = [JS_ADAPTER, PYTHON_ADAPTER, RUST_ADAPTER, GO_ADAPTER, JAVA_ADAPTER];
|
|
20
21
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// KJC-PCS-0049 Step 7 — MCP handlers calling src/rag/* directly. Imports
|
|
2
2
|
// from src/commands/* are forbidden by the layer-boundaries test
|
|
3
3
|
// (MCP and CLI are peer layers); we hit the pure rag/* modules instead.
|
|
4
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { countChunks, projectSlug } from "../../rag/vec-store.js";
|
|
6
|
+
import { openProjectStore } from "../../rag/project-store.js";
|
|
7
|
+
import { countProjectChunks, emptyIndexRemedy } from "../../rag/migrate.js";
|
|
5
8
|
import { makeGovernedEmbedder } from "../../rag/governed-embedder.js";
|
|
6
9
|
import { indexProject } from "../../rag/indexer.js";
|
|
10
|
+
import { ragExclude } from "../../rag/indexable.js";
|
|
7
11
|
import { query } from "../../rag/retriever.js";
|
|
8
12
|
import { getKarajanHome } from "../../utils/paths.js";
|
|
9
13
|
import { resolveProjectDir, buildConfig, responseText, failPayload } from "../shared-helpers.js";
|
|
@@ -28,9 +32,14 @@ export async function handleRagQuery(args, server) {
|
|
|
28
32
|
// projectDir, `project: "all"` disables the filter, `project: <slug>`
|
|
29
33
|
// overrides it.
|
|
30
34
|
const project = args?.project === "all" ? null : (args?.project || projectSlug(projectDir) || null);
|
|
31
|
-
const
|
|
35
|
+
const dim = config?.rag?.embedder?.dim || 768;
|
|
36
|
+
// KJC-TSK-0883 (ADR 0011): el indice del proyecto, como la CLI (0882).
|
|
37
|
+
const db = openProjectStore({ projectDir, dim });
|
|
32
38
|
try {
|
|
33
|
-
if (countChunks(db) === 0
|
|
39
|
+
if (countChunks(db) === 0 || (project && countProjectChunks(db, project) === 0)) {
|
|
40
|
+
const remedy = emptyIndexRemedy({ slug: project, legacyPath: join(getKarajanHome(), "rag.db"), dim });
|
|
41
|
+
return responseText({ hits: [], empty: true, topK, scope, project, remedy });
|
|
42
|
+
}
|
|
34
43
|
const hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project });
|
|
35
44
|
return responseText({ hits, empty: false, topK, scope, project });
|
|
36
45
|
} finally { db.close(); }
|
|
@@ -43,12 +52,13 @@ export async function handleRagIndex(args, server) {
|
|
|
43
52
|
try {
|
|
44
53
|
const projectDir = await resolveProjectDir(server, args?.projectDir);
|
|
45
54
|
const config = await buildConfig({ ...args, projectDir }, "rag-index");
|
|
46
|
-
const db =
|
|
55
|
+
const db = openProjectStore({ projectDir, dim: config?.rag?.embedder?.dim || 768 });
|
|
47
56
|
try {
|
|
48
57
|
const totals = await indexProject(projectDir, {
|
|
49
58
|
db, embedder: makeGovernedEmbedder(config),
|
|
50
59
|
karajanHome: getKarajanHome(), logger: silentLogger,
|
|
51
60
|
withSources: Boolean(args?.withSources),
|
|
61
|
+
exclude: ragExclude(config), // KJC-TSK-0891
|
|
52
62
|
});
|
|
53
63
|
return responseText(totals);
|
|
54
64
|
} finally { db.close(); }
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { generateDiff } from "../../review/diff-generator.js";
|
|
25
25
|
import { finalizeGitAutomation } from "../../git/automation.js";
|
|
26
26
|
import { listCommitsBetween, listFilesChangedSince } from "../../utils/git.js";
|
|
27
|
-
import { loadPlan
|
|
27
|
+
import { loadPlan } from "../../plan/plan-store.js";
|
|
28
28
|
import { computePlanAdherenceScore } from "../../audit/plan-adherence.js";
|
|
29
29
|
import { saveSession, markSessionStatus } from "../../session/store.js";
|
|
30
30
|
import {
|
|
@@ -334,22 +334,24 @@ export async function handleMaxIterationsReached({ session, budgetSummary, emitt
|
|
|
334
334
|
return { approved: false, sessionId: session.id, reason: "max_iterations" };
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
/**
|
|
338
|
+
* KJC-TSK-0884 (ADR 0011): the board starts by itself, like Sonar. Only a
|
|
339
|
+
* declared false on either switch opts out; nothing declared means on.
|
|
340
|
+
*/
|
|
341
|
+
export const boardAutoStartWanted = (config) => config?.hu_board?.auto_start !== false && config?.hu_board?.enabled !== false;
|
|
342
|
+
|
|
337
343
|
export async function tryAutoStartBoard(config, logger, emitter, eventBase) {
|
|
338
|
-
|
|
339
|
-
// (enabled && auto_start) was confusing because `enabled` was never the
|
|
340
|
-
// user-facing switch — only auto_start was. This matches the AC: "kj run
|
|
341
|
-
// auto-inicia el board server si hu_board.auto_start es true".
|
|
342
|
-
if (!config.hu_board?.auto_start) return;
|
|
344
|
+
if (!boardAutoStartWanted(config)) return;
|
|
343
345
|
// Never auto-start during vitest: races the PID file and starts a detached
|
|
344
346
|
// process that outlives the test run.
|
|
345
347
|
if (process.env.VITEST || process.env.NODE_ENV === "test") return;
|
|
346
348
|
|
|
347
349
|
try {
|
|
348
|
-
const { startBoard, renderBoardBanner } = await import("../../commands/board.js");
|
|
349
|
-
const boardPort = config.hu_board
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
const slug = config.projectDir ?
|
|
350
|
+
const { startBoard, renderBoardBanner, boardSlug } = await import("../../commands/board.js");
|
|
351
|
+
const boardPort = config.hu_board?.port || 4000;
|
|
352
|
+
// KJC-TSK-0884: the project's scoped view (/p/<slug>), the parent repo's
|
|
353
|
+
// even from a worktree lane.
|
|
354
|
+
const slug = config.projectDir ? boardSlug(config.projectDir) : null;
|
|
353
355
|
const boardResult = await startBoard(boardPort, { projectSlug: slug });
|
|
354
356
|
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
355
357
|
// Highlighted URL box — visible regardless of log level (matches the
|
|
@@ -122,9 +122,10 @@ export async function maybeGenerateAutoHuBatch({
|
|
|
122
122
|
// node process around after the suite (TSK-0273).
|
|
123
123
|
if (process.env.VITEST || process.env.NODE_ENV === "test") return;
|
|
124
124
|
try {
|
|
125
|
-
const { startBoard, renderBoardBanner } = await import("../../../commands/board.js");
|
|
125
|
+
const { startBoard, renderBoardBanner, boardSlug } = await import("../../../commands/board.js");
|
|
126
126
|
const desiredPort = session.config_snapshot?.hu_board?.port ?? 4000;
|
|
127
|
-
|
|
127
|
+
// KJC-TSK-0884: its project's scoped view, not the global dashboard.
|
|
128
|
+
const boardResult = await startBoard(desiredPort, { projectSlug: projectDir ? boardSlug(projectDir) : null });
|
|
128
129
|
const url = boardResult.url;
|
|
129
130
|
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
130
131
|
const projectName = batch.projectName || "Auto-generated HUs";
|