karajan-code 4.35.0 → 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/audit/harness-scorecard.js +8 -1
- package/src/audit/harness-section.js +13 -2
- package/src/checks/rag-coverage.js +9 -5
- package/src/checks/runner.js +27 -5
- package/src/checks/skills.js +10 -0
- package/src/checks/types.js +5 -1
- 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 +15 -6
- package/src/commands/watch.js +13 -10
- package/src/config/defaults.js +3 -2
- package/src/environment/playbook.js +10 -3
- package/src/harden/config-templates.js +11 -0
- package/src/harden/guidelines-engine.js +24 -7
- package/src/harden/harness-hooks.js +92 -3
- package/src/harden/sentinel-hooks.js +80 -8
- 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/drivers/pre-loop.js +1 -1
- package/src/orchestrator/stages/rag-context-stage.js +5 -2
- package/src/rag/auto-update.js +5 -3
- package/src/rag/coverage.js +36 -13
- 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/gate-gitignore.js +23 -1
- package/src/review/kj-provenance.js +68 -0
- package/src/review/rag-ledger.js +12 -4
- package/src/review/rag-requirement.js +17 -3
- package/src/review/verdict-store.js +6 -1
- package/src/sonar/api.js +11 -2
- package/src/sonar/project-key.js +33 -0
- package/src/sonar/scanner.js +37 -2
- package/src/start/sweep.js +4 -3
- package/src/utils/rulesync.js +61 -0
package/src/commands/harden.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createWizard, isTTY } from "../utils/wizard.js";
|
|
|
17
17
|
import { commitSupervisorRegeneration } from "../harden/supervisor-commit.js";
|
|
18
18
|
import { installConfigsForRoots } from "../harden/config-engine.js";
|
|
19
19
|
import { installGuidelines } from "../harden/guidelines-engine.js";
|
|
20
|
+
import { maybeRulesyncGenerate } from "../utils/rulesync.js";
|
|
20
21
|
import { commandsForLanguage } from "../harden/hook-commands.js";
|
|
21
22
|
import { installHooks } from "../harden/harden-engine.js";
|
|
22
23
|
import { installHarnessHooks } from "../harden/harness-hooks.js";
|
|
@@ -76,6 +77,18 @@ export async function resolveCmds(projectDir, language) {
|
|
|
76
77
|
return cmds;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* KJC-BUG-0233: loadConfig returns `{ config, ... }`; reading `base_branch` off
|
|
82
|
+
* the result was always undefined, so the guard always protected "main".
|
|
83
|
+
*/
|
|
84
|
+
export async function resolveBaseBranch(projectDir, load = loadConfig) {
|
|
85
|
+
try {
|
|
86
|
+
return (await load(projectDir))?.config?.base_branch || "main";
|
|
87
|
+
} catch {
|
|
88
|
+
return "main"; // no kj config: the default stands
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
export async function hardenCommand({
|
|
80
93
|
projectDir = process.cwd(),
|
|
81
94
|
profile = "standard",
|
|
@@ -139,10 +152,7 @@ export async function hardenCommand({
|
|
|
139
152
|
// KJC-TSK-0648: branch-first guard — the base branch only moves via PR.
|
|
140
153
|
// Resolved from the project's kj config (default "main"); best-effort so
|
|
141
154
|
// harden keeps working on repos that never ran kj init.
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
baseBranch = (await loadConfig(projectDir))?.base_branch || "main";
|
|
145
|
-
} catch { /* no kj config — default stands */ }
|
|
155
|
+
const baseBranch = await resolveBaseBranch(projectDir);
|
|
146
156
|
let result;
|
|
147
157
|
try {
|
|
148
158
|
result = await installHooks({ projectDir, profile, cmds, dryRun, baseBranch });
|
|
@@ -177,6 +187,12 @@ export async function hardenCommand({
|
|
|
177
187
|
// KJC-BUG-0199 (issue #1773): the same detected language the workflows and
|
|
178
188
|
// the configs already use — a Python project must not be told to use `const`.
|
|
179
189
|
const gl = withGuidelines ? installGuidelines({ projectDir, language: roots[0]?.language ?? null, dryRun }) : null;
|
|
190
|
+
// KJC-TSK-0879: in a Rulesync repo, spread the rules only if the project opted in.
|
|
191
|
+
if (withGuidelines && !dryRun) {
|
|
192
|
+
let kjConfig = null;
|
|
193
|
+
try { kjConfig = (await loadConfig(projectDir))?.config; } catch { /* no kj config: not opted in */ }
|
|
194
|
+
maybeRulesyncGenerate({ projectDir, config: kjConfig, logger });
|
|
195
|
+
}
|
|
180
196
|
const out = {
|
|
181
197
|
ok: true,
|
|
182
198
|
...result,
|
|
@@ -202,6 +218,12 @@ export async function hardenCommand({
|
|
|
202
218
|
logger.info?.(` • ${g.file}: ${g.action}`);
|
|
203
219
|
// KJC-BUG-0206: quitar reglas no puede leerse como "updated" y nada más.
|
|
204
220
|
if (g.shrank) logger.warn?.(` ⚠ ${g.file} queda con MENOS reglas que antes — revisa si el lenguaje detectado es el que crees (kj check lo dice)`);
|
|
221
|
+
// KJC-BUG-0232: cuales. Si una es tuya, vivia dentro del bloque: va fuera.
|
|
222
|
+
if (g.removed?.length) {
|
|
223
|
+
for (const line of g.removed.slice(0, 10)) logger.warn?.(` - ${line.length > 120 ? `${line.slice(0, 117)}...` : line}`);
|
|
224
|
+
if (g.removed.length > 10) logger.warn?.(` (+${g.removed.length - 10} más)`);
|
|
225
|
+
logger.warn?.(" Si alguna es vuestra, estaba dentro del bloque gestionado: recuperadla de git y ponedla fuera de él.");
|
|
226
|
+
}
|
|
205
227
|
}
|
|
206
228
|
if (!dryRun) logger.info?.("core.hooksPath set. Verify later with `kj check`.");
|
|
207
229
|
// IDN-A (KJC-TSK-0762): a hardened clone declares who works it. Captured
|
package/src/commands/init.js
CHANGED
|
@@ -344,7 +344,8 @@ async function runWizard(config, logger) {
|
|
|
344
344
|
config.sonarqube.enabled = enableSonar;
|
|
345
345
|
logger.info(` -> SonarQube: ${enableSonar ? "enabled" : "disabled"}`);
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
// KJC-TSK-0884: on by default, like Sonar; answering no is a declared opt-out.
|
|
348
|
+
const enableHuBoard = await wizard.confirm("Enable the HU Board — a local web UI to track user stories (HUs)?", true);
|
|
348
349
|
config.hu_board = config.hu_board || {};
|
|
349
350
|
config.hu_board.enabled = enableHuBoard;
|
|
350
351
|
if (enableHuBoard) {
|
|
@@ -512,13 +512,11 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
512
512
|
// vitest / test mode, and never when `--json` is active.
|
|
513
513
|
if (plan.hus.length > 0 && !process.env.VITEST && process.env.NODE_ENV !== "test") {
|
|
514
514
|
try {
|
|
515
|
-
const { startBoard, renderBoardBanner } = await import("../board.js");
|
|
516
|
-
const { projectSlug: slugFor } = await import("../../plan/plan-store.js");
|
|
515
|
+
const { startBoard, renderBoardBanner, boardSlug } = await import("../board.js");
|
|
517
516
|
const boardPort = config?.hu_board?.port ?? 4000;
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
const boardResult = await startBoard(boardPort, { projectSlug: slugFor(projectDir) });
|
|
517
|
+
// KJC-TSK-0884: this project's scoped view (/p/<slug>), the parent
|
|
518
|
+
// repo's even from a worktree lane, never the global dashboard.
|
|
519
|
+
const boardResult = await startBoard(boardPort, { projectSlug: boardSlug(projectDir) });
|
|
522
520
|
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
523
521
|
console.log(renderBoardBanner({ url: boardResult.url, status, projectName: plan.name }));
|
|
524
522
|
} catch (err) {
|
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
|
}
|
|
@@ -386,8 +395,8 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
386
395
|
console.log(`⚠ rag [${RAG_RULE_ID}]: the pipeline verdict carries no session ledger — the headless pipeline does not consult the RAG yet; sealed as a warn`);
|
|
387
396
|
if (!warnIds.includes(RAG_RULE_ID)) warnIds.push(RAG_RULE_ID);
|
|
388
397
|
} else if (res.ok) {
|
|
389
|
-
const { harness, verified, mismatched } = readRagLedger(projectDir);
|
|
390
|
-
const rreq = checkRagVerdict({ config, stagedFiles:
|
|
398
|
+
const { harness, verified, mismatched, drift } = readRagLedger(projectDir);
|
|
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 };
|
|
@@ -118,6 +118,17 @@ export const GO_CONFIGS = [{ file: ".golangci.yml", blockId: "golangci", style:
|
|
|
118
118
|
export const PHPSTAN_BODY = ["parameters:", " level: 6", " paths:", " - src", " - tests"].join("\n");
|
|
119
119
|
export const PHP_CONFIGS = [{ file: "phpstan.neon", blockId: "phpstan", style: "hash", body: PHPSTAN_BODY }];
|
|
120
120
|
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* KJC-BUG-0221 — los nombres que kj ESCRIBE en el proyecto. Lo consume la
|
|
124
|
+
* cobertura del RAG para no denunciar como drift un fichero que kj acaba de
|
|
125
|
+
* generar. Se deriva de las tablas de arriba: anadir un config aqui no obliga
|
|
126
|
+
* a acordarse de una segunda lista en otro modulo.
|
|
127
|
+
*/
|
|
128
|
+
export const GENERATED_CONFIG_FILES = new Set(
|
|
129
|
+
[...UNIVERSAL_CONFIGS, ...JS_CONFIGS, ...PY_CONFIGS, ...GO_CONFIGS, ...PHP_CONFIGS].map((c) => c.file),
|
|
130
|
+
);
|
|
131
|
+
|
|
121
132
|
/** Lint/format configs by detected language. Unknown ⇒ universal only. */
|
|
122
133
|
export const CONFIGS_BY_LANGUAGE = {
|
|
123
134
|
javascript: JS_CONFIGS,
|
|
@@ -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
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* supports tool hooks.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
12
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
|
|
@@ -18,7 +19,11 @@ const SCRIPT_BODY = `#!/usr/bin/env node
|
|
|
18
19
|
// so a gate bug never bricks the session.
|
|
19
20
|
import console from "node:console";
|
|
20
21
|
import process from "node:process";
|
|
21
|
-
import { existsSync } from "node:fs";
|
|
22
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
23
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
// El guard vive en <repo>/.karajan/harness/, asi que el arbol es dos arriba.
|
|
26
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
22
27
|
let raw = "";
|
|
23
28
|
process.stdin.on("data", (d) => { raw += d; });
|
|
24
29
|
process.stdin.on("end", () => {
|
|
@@ -30,10 +35,75 @@ process.stdin.on("end", () => {
|
|
|
30
35
|
process.exit(2);
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
|
-
if (tool === "Bash"
|
|
38
|
+
if (tool === "Bash") {
|
|
34
39
|
const cmd = String(input.command || "");
|
|
40
|
+
// KJC-BUG-0227: el escape tambien viaja en el PROPIO comando, como en el
|
|
41
|
+
// resto de guardias. Mirando solo process.env el agente no tenia salida:
|
|
42
|
+
// un export dentro del comando jamas llega a este proceso.
|
|
43
|
+
// Solo cuentan las asignaciones DEL PRINCIPIO, que son las que el shell
|
|
44
|
+
// aplica al comando. Buscarla en cualquier parte del texto dejaba pasar
|
|
45
|
+
// un "echo KJ_ALLOW_REWRITE=1; python3 ..." (catch de la review).
|
|
46
|
+
const toks = cmd.trim().split(/\\s+/);
|
|
47
|
+
let i = 0;
|
|
48
|
+
let named = false;
|
|
49
|
+
while (i < toks.length && /^[A-Z][A-Z0-9_]*=/.test(toks[i])) {
|
|
50
|
+
if (toks[i] === "KJ_ALLOW_REWRITE=1") named = true;
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
// Una asignacion delante solo alcanza a SU comando, asi que el escape del
|
|
54
|
+
// comando vale unicamente en un comando SIMPLE: en "VAR=1 true && python"
|
|
55
|
+
// o "VAR=1 && python" el python nunca la recibe (catch de la review). Lo
|
|
56
|
+
// que va entre comillas no encadena nada, asi que no cuenta.
|
|
57
|
+
// Tampoco es simple si hay sustitucion: el proceso de dentro de $( ) no
|
|
58
|
+
// hereda la asignacion (catch de la review). Mismo criterio que el resto
|
|
59
|
+
// de guardias del Sentinel.
|
|
60
|
+
// Dentro de comillas SIMPLES todo es literal; dentro de dobles, un ; no
|
|
61
|
+
// encadena pero $( ) si ejecuta. Por eso se miran dos cosas distintas.
|
|
62
|
+
const noSingle = cmd.replaceAll(/'[^']*'/g, "");
|
|
63
|
+
const substitutes = noSingle.includes("$(") || noSingle.includes("\\u0060");
|
|
64
|
+
const bare = noSingle.replaceAll(/"[^"]*"/g, "");
|
|
65
|
+
const simple = !substitutes && !/[;&|]/.test(bare) && !bare.includes("\\n");
|
|
66
|
+
const cmdEscape = named && simple && i < toks.length;
|
|
67
|
+
const escaped = process.env.KJ_ALLOW_REWRITE === "1" || cmdEscape;
|
|
68
|
+
// Un escape ignorado EN SILENCIO era el bug: si esta puesto y no vale, se dice.
|
|
69
|
+
if (named && !cmdEscape) console.error("kj tool gate: KJ_ALLOW_REWRITE=1 presente pero IGNORADO — solo vale delante del comando y en uno simple (sin ; | & fuera de comillas).");
|
|
35
70
|
const writes = /open\\s*\\([^)]*["'][wa]["']|>\\s*\\S+\\.json\\b/.test(cmd);
|
|
36
|
-
|
|
71
|
+
// KJC-BUG-0227: y solo se vigila lo que esta DENTRO del arbol. Un fichero
|
|
72
|
+
// temporal del scratchpad no es asunto de este guard, y bloquearlo
|
|
73
|
+
// ensenaba a rodearlo. Sin ruta reconocible se vigila: el lado seguro.
|
|
74
|
+
const targets = cmd.match(/[\\w./-]+\\.json\\b/g) || [];
|
|
75
|
+
// Solo se exime lo que se puede establecer CON CERTEZA: una ruta absoluta
|
|
76
|
+
// fuera del arbol. Una relativa depende del cwd del shell, que este hook
|
|
77
|
+
// no conoce — "cd packages && ... open(\\"../package.json\\")" escribe
|
|
78
|
+
// dentro del repo (catch de la review), asi que se sigue vigilando.
|
|
79
|
+
// Un enlace puede apuntar dentro: /tmp/link.json escribiria en el repo
|
|
80
|
+
// (catch de la review). Se resuelve el padre real antes de clasificar, y
|
|
81
|
+
// si no se puede resolver, se vigila.
|
|
82
|
+
// Fuera del arbol es SALIR de el, no que el nombre empiece por dos puntos:
|
|
83
|
+
// <repo>/..config.json esta dentro (catch de la review).
|
|
84
|
+
const escapesRoot = (rel) => rel === ".." || rel.startsWith("../");
|
|
85
|
+
const outsideForSure = (t) => {
|
|
86
|
+
if (!t.startsWith("/")) return false;
|
|
87
|
+
const abs = resolve(t);
|
|
88
|
+
try {
|
|
89
|
+
const root = realpathSync(ROOT);
|
|
90
|
+
// El propio fichero puede SER el enlace (/tmp/link.json -> repo/cfg.json),
|
|
91
|
+
// asi que se resuelve entero cuando existe; si aun no existe, manda su
|
|
92
|
+
// directorio real.
|
|
93
|
+
if (existsSync(abs)) return escapesRoot(relative(root, realpathSync(abs)));
|
|
94
|
+
const parts = abs.split("/");
|
|
95
|
+
const name = parts.pop();
|
|
96
|
+
return escapesRoot(relative(root, realpathSync(parts.join("/") || "/") + "/" + name));
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// Con expansion del shell no se sabe a donde apunta nada: "$PWD/cfg.json"
|
|
102
|
+
// deja el trozo "/cfg.json", que parece absoluto y ajeno sin serlo (catch
|
|
103
|
+
// de la review). Ante la duda se vigila.
|
|
104
|
+
const expands = /[$~]/.test(cmd) || cmd.includes("\\u0060");
|
|
105
|
+
const mine = expands || targets.length === 0 || !targets.every(outsideForSure);
|
|
106
|
+
if (!escaped && mine && /json\\.dumps?\\s*\\(/.test(cmd) && writes) {
|
|
37
107
|
console.error("kj tool gate: reserializing a whole JSON file makes the diff unreviewable — make targeted edits instead (KJ_ALLOW_REWRITE=1 to override consciously).");
|
|
38
108
|
process.exit(2);
|
|
39
109
|
}
|
|
@@ -100,8 +170,27 @@ export function mergeClaudeHooks({ projectDir, logger = console, entries }) {
|
|
|
100
170
|
return { wired: true };
|
|
101
171
|
}
|
|
102
172
|
|
|
173
|
+
/**
|
|
174
|
+
* La raiz del proyecto: el toplevel de git, o el directorio dado si no hay git.
|
|
175
|
+
* Vive aqui y no se importa de sentinel-hooks porque ese modulo importa de
|
|
176
|
+
* este: el ciclo rompe la carga.
|
|
177
|
+
*/
|
|
178
|
+
function resolveProjectRoot(dir) {
|
|
179
|
+
try {
|
|
180
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || dir;
|
|
181
|
+
} catch {
|
|
182
|
+
return dir;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
103
186
|
/** Write the script and merge the PreToolUse entries into .claude/settings.json. */
|
|
104
187
|
export function installHarnessHooks({ projectDir = process.cwd(), logger = console } = {}) {
|
|
188
|
+
// KJC-BUG-0223: el harness es del proyecto y vive en su raiz. Instalado desde
|
|
189
|
+
// un subdirectorio sembraba ahi un segundo `.karajan/harness` que no gobierna
|
|
190
|
+
// nada, y el gate seguia mirando otro sitio.
|
|
191
|
+
const root = resolveProjectRoot(projectDir);
|
|
192
|
+
if (root !== projectDir) logger?.info?.(`kj harden: el harness es del proyecto, se instala en su raíz (${root})`);
|
|
193
|
+
projectDir = root;
|
|
105
194
|
const scriptAbs = writeHarnessScript(projectDir, "pretooluse.mjs", SCRIPT_BODY);
|
|
106
195
|
const { wired } = mergeClaudeHooks({
|
|
107
196
|
projectDir,
|