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/rag/library.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* library — the distilled engineering canon as its own RAG collection
|
|
3
|
-
* (KJC-TSK-0697).
|
|
4
|
-
* and
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* (KJC-TSK-0697). KJC-TSK-0883 (ADR 0011): the machine canon (`<pkg>/library/`
|
|
4
|
+
* and `~/.karajan/library/`) indexes into its own ~/.karajan/library.db under
|
|
5
|
+
* project="library", reached by `kj rag query --library`; a project's own cards
|
|
6
|
+
* (`<project>/.karajan/library/`) go to that project's index under its slug. The architect
|
|
7
7
|
* consults it to ground the greenfield alternative (KJC-TSK-0696).
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -20,10 +20,16 @@ export const LIBRARY_PROJECT = "library";
|
|
|
20
20
|
|
|
21
21
|
const SHIPPED_LIBRARY_DIR = fileURLToPath(new URL("../../library", import.meta.url));
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Existing library dirs, shipped canon first. KJC-TSK-0883: `scope` "machine"
|
|
25
|
+
* is the common canon (kj's + the user's), "project" the project's own cards.
|
|
26
|
+
*/
|
|
27
|
+
export function libraryDirs({ pkgLibraryDir = SHIPPED_LIBRARY_DIR, home = os.homedir(), projectDir = process.cwd(), scope = "all" } = {}) {
|
|
28
|
+
const machine = [pkgLibraryDir, join(home, ".karajan", "library")];
|
|
29
|
+
const own = [join(projectDir, ".karajan", "library")];
|
|
30
|
+
const byScope = { machine, project: own, all: [...machine, ...own] };
|
|
31
|
+
if (!byScope[scope]) throw new Error(`libraryDirs: unknown scope "${scope}" (machine | project | all)`);
|
|
32
|
+
return byScope[scope].filter((d) => existsSync(d));
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -31,9 +37,9 @@ export function libraryDirs({ pkgLibraryDir = SHIPPED_LIBRARY_DIR, home = os.hom
|
|
|
31
37
|
* re-index per source and identical bodies skip the embedder (content-hash
|
|
32
38
|
* dedup within the library project).
|
|
33
39
|
*/
|
|
34
|
-
export async function indexLibrary({ db, embedder, logger = console, pkgLibraryDir, home, projectDir } = {}) {
|
|
40
|
+
export async function indexLibrary({ db, embedder, logger = console, pkgLibraryDir, home, projectDir, scope = "all", project = LIBRARY_PROJECT } = {}) {
|
|
35
41
|
let indexed = 0, failed = 0, files = 0;
|
|
36
|
-
for (const dir of libraryDirs({ pkgLibraryDir, home, projectDir })) {
|
|
42
|
+
for (const dir of libraryDirs({ pkgLibraryDir, home, projectDir, scope })) {
|
|
37
43
|
for (const name of readdirSync(dir)) {
|
|
38
44
|
if (extname(name).toLowerCase() !== ".md") continue;
|
|
39
45
|
const path = join(dir, name);
|
|
@@ -42,13 +48,13 @@ export async function indexLibrary({ db, embedder, logger = console, pkgLibraryD
|
|
|
42
48
|
const hashed = chunks.map((ch) => ({ ch, contentHash: createHash("sha256").update(ch.text).digest("hex") }));
|
|
43
49
|
// Unchanged card (every chunk body already known) → skip before the
|
|
44
50
|
// delete, so idempotent re-runs never touch the embedder.
|
|
45
|
-
if (hashed.length && hashed.every(({ contentHash }) => findChunkByHash(db, contentHash,
|
|
51
|
+
if (hashed.length && hashed.every(({ contentHash }) => findChunkByHash(db, contentHash, project))) continue;
|
|
46
52
|
deleteChunksBySource(db, path);
|
|
47
53
|
for (const { ch, contentHash } of hashed) {
|
|
48
54
|
try {
|
|
49
|
-
if (findChunkByHash(db, contentHash,
|
|
55
|
+
if (findChunkByHash(db, contentHash, project)) continue;
|
|
50
56
|
const embedding = await embedder.embed(ch.text);
|
|
51
|
-
insertChunk(db, { source: path, kind: "library", text: ch.text, metadata: ch.metadata, embedding, project
|
|
57
|
+
insertChunk(db, { source: path, kind: "library", text: ch.text, metadata: ch.metadata, embedding, project, contentHash });
|
|
52
58
|
indexed += 1;
|
|
53
59
|
} catch (err) {
|
|
54
60
|
failed += 1;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0888 (RAG-P1a, ADR 0011) — pasar al indice por proyecto sin volver a
|
|
3
|
+
* embeber. Los chunks de UN proyecto se copian de la base global de la maquina
|
|
4
|
+
* a <raiz>/.karajan/rag.db con sus embeddings tal cual, junto con su marca de
|
|
5
|
+
* last_indexed_commit para que el auto-update siga por delta. Cero recomputo
|
|
6
|
+
* (en esta maquina, karajan-code son 23.109 chunks), y la base global no se
|
|
7
|
+
* toca: borrarla es decision del usuario, no efecto de una migracion.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
import { openVecStore, insertChunk, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
|
|
12
|
+
|
|
13
|
+
export const countProjectChunks = (db, slug) => db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE project_slug = ?").get(slug).n;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* KJC-TSK-0882: que decir cuando el indice del proyecto no tiene nada suyo.
|
|
17
|
+
* Si la base global tiene sus chunks, migrar (segundos); si no, indexar.
|
|
18
|
+
*/
|
|
19
|
+
export function emptyIndexRemedy({ slug, legacyPath, dim = 768 }) {
|
|
20
|
+
if (slug && existsSync(legacyPath)) {
|
|
21
|
+
const legacy = openVecStore({ dim, path: legacyPath });
|
|
22
|
+
try {
|
|
23
|
+
if (countProjectChunks(legacy, slug) > 0) return "kj rag migrate";
|
|
24
|
+
} finally {
|
|
25
|
+
legacy.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return "kj rag index --with-sources";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @returns {{state: "migrated"|"already"|"nothing", migrated: number, reason: string}}
|
|
33
|
+
*/
|
|
34
|
+
export function migrateProjectIndex({ slug, legacyPath, targetPath, dim = 768 }) {
|
|
35
|
+
const nothing = { state: "nothing", migrated: 0, reason: `the global index holds nothing for ${slug}: kj rag index --with-sources` };
|
|
36
|
+
if (!existsSync(legacyPath)) return nothing;
|
|
37
|
+
|
|
38
|
+
const target = openVecStore({ dim, path: targetPath });
|
|
39
|
+
try {
|
|
40
|
+
// Se cuentan los de ESTE proyecto, no el total: un rag.db viejo en
|
|
41
|
+
// .karajan/ puede traer chunks de otro (visto en esta maquina: 42 de otro
|
|
42
|
+
// repo), y contar el total diria "ya migrado" sin haber copiado nada.
|
|
43
|
+
const mine = countProjectChunks(target, slug);
|
|
44
|
+
if (mine > 0) {
|
|
45
|
+
return { state: "already", migrated: 0, reason: `the project index already holds ${mine} chunks of ${slug}: nothing to migrate` };
|
|
46
|
+
}
|
|
47
|
+
const legacy = openVecStore({ dim, path: legacyPath });
|
|
48
|
+
try {
|
|
49
|
+
const rows = legacy.prepare("SELECT id, source, kind, text, metadata, content_hash FROM chunks WHERE project_slug = ?").all(slug);
|
|
50
|
+
if (rows.length === 0) return nothing;
|
|
51
|
+
const vecOf = legacy.prepare("SELECT embedding FROM vec_chunks WHERE rowid = ?");
|
|
52
|
+
const copy = target.transaction(() => {
|
|
53
|
+
for (const r of rows) {
|
|
54
|
+
const raw = vecOf.get(BigInt(r.id))?.embedding;
|
|
55
|
+
if (!raw) continue; // un chunk sin vector no se puede buscar: no se inventa
|
|
56
|
+
const embedding = new Float32Array(raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength));
|
|
57
|
+
insertChunk(target, { source: r.source, kind: r.kind, text: r.text, metadata: r.metadata, embedding, project: slug, contentHash: r.content_hash });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
copy();
|
|
61
|
+
const stamp = getLastIndexedCommit(legacy, slug);
|
|
62
|
+
if (stamp) setLastIndexedCommit(target, slug, stamp);
|
|
63
|
+
const migrated = countProjectChunks(target, slug);
|
|
64
|
+
const parts = [`${migrated} chunks of ${slug} copied with their embeddings`];
|
|
65
|
+
if (stamp) parts.push(`indexed at ${stamp.slice(0, 9)}`);
|
|
66
|
+
return { state: "migrated", migrated, reason: parts.join(", ") };
|
|
67
|
+
} finally {
|
|
68
|
+
legacy.close();
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
target.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/rag/onnx-fallback.js
CHANGED
|
@@ -12,7 +12,7 @@ import path from "node:path";
|
|
|
12
12
|
import Database from "better-sqlite3";
|
|
13
13
|
import { parseDocument } from "yaml";
|
|
14
14
|
import { getProjectConfigPath } from "../config/loader.js";
|
|
15
|
-
import {
|
|
15
|
+
import { projectDbPath } from "./project-store.js";
|
|
16
16
|
|
|
17
17
|
const ONNX_EMBEDDER = { provider: "onnx", dim: 384 };
|
|
18
18
|
|
|
@@ -24,8 +24,10 @@ const ONNX_EMBEDDER = { provider: "onnx", dim: 384 };
|
|
|
24
24
|
* that already HAS chunks is never touched — switching dims there would
|
|
25
25
|
* destroy other projects' indexes.
|
|
26
26
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// KJC-TSK-0882 (ADR 0011): el store es DEL PROYECTO, asi que el fallback a ONNX
|
|
28
|
+
// decide mirando el indice de este proyecto y nunca el de otro.
|
|
29
|
+
export function resetEmptyStore(projectDir = process.cwd()) {
|
|
30
|
+
const storePath = projectDbPath(projectDir);
|
|
29
31
|
if (!existsSync(storePath)) return true;
|
|
30
32
|
// Read-only, schema-agnostic inspection: no DDL, no vec extension, no
|
|
31
33
|
// dimension assumptions — the store is only ever OPENED to be counted.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0882 (RAG-P1, ADR 0011) — el indice del RAG es DEL PROYECTO.
|
|
3
|
+
*
|
|
4
|
+
* Hasta aqui era una sola base por maquina (~/.karajan/rag.db): 427 MB, 19
|
|
5
|
+
* proyectos y 49.825 chunks en un indice vectorial sin particion. La KNN sacaba
|
|
6
|
+
* los vecinos mas cercanos de TODA la maquina y filtraba por proyecto despues,
|
|
7
|
+
* asi que un proyecto pequeno podia recibir cero resultados con el chunk
|
|
8
|
+
* perfecto indexado, y cinco consultas ni siquiera filtraban (una de ellas, el
|
|
9
|
+
* preload que entra en el prompt del coder).
|
|
10
|
+
*
|
|
11
|
+
* Ahora cada proyecto abre <raiz git>/.karajan/rag.db. La raiz se resuelve por
|
|
12
|
+
* git para que desde un subdirectorio se vea el mismo indice. KJ_RAG_DB sigue
|
|
13
|
+
* mandando cuando esta puesta. El store de karajan-core ya acepta `path`, asi
|
|
14
|
+
* que esto no toca core.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
import { openVecStore } from "./vec-store.js";
|
|
20
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
21
|
+
|
|
22
|
+
function projectRoot(dir) {
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || dir;
|
|
25
|
+
} catch {
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Ruta del indice del proyecto. */
|
|
31
|
+
export function projectDbPath(projectDir = process.cwd()) {
|
|
32
|
+
if (process.env.KJ_RAG_DB) return process.env.KJ_RAG_DB;
|
|
33
|
+
return path.join(projectRoot(projectDir), ".karajan", "rag.db");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Abre el indice del proyecto. El caller es dueno del close(). */
|
|
37
|
+
/** KJC-TSK-0883: the machine canon has its own store, never a project's. */
|
|
38
|
+
export function libraryDbPath() {
|
|
39
|
+
return process.env.KJ_LIBRARY_DB || path.join(getKarajanHome(), "library.db");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function openLibraryStore({ dim = 768 } = {}) {
|
|
43
|
+
return openVecStore({ dim, path: libraryDbPath() });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function openProjectStore({ projectDir = process.cwd(), dim = 768 } = {}) {
|
|
47
|
+
return openVecStore({ dim, path: projectDbPath(projectDir) });
|
|
48
|
+
}
|
package/src/rag/watcher.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
// KJC-TSK-0441 (RAG v2.28.0 PR1) — chokidar watcher. Live re-index of plans
|
|
2
2
|
// + onboarding + (opt) sources, debounced. PID file arbitrates a single daemon.
|
|
3
3
|
// KJC-TSK-0482 — source matcher derivado del registry (multi-lang: JS/Py/Rust/Go/Java).
|
|
4
|
-
import { writeFileSync, readFileSync, existsSync, unlinkSync } from "node:fs";
|
|
5
|
-
import { extname, join } from "node:path";
|
|
4
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
5
|
+
import { dirname, extname, join } from "node:path";
|
|
6
6
|
import chokidar from "chokidar";
|
|
7
|
-
import {
|
|
7
|
+
import { deleteChunksBySource, projectSlug } from "./vec-store.js";
|
|
8
|
+
import { openProjectStore } from "./project-store.js";
|
|
8
9
|
import { makeGovernedEmbedder } from "./governed-embedder.js";
|
|
9
10
|
import { indexFile } from "./indexer.js";
|
|
10
11
|
import { getKarajanHome } from "../utils/paths.js";
|
|
11
12
|
import { getAllCodeExtensions } from "../lang/registry.js";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
// KJC-TSK-0883 (ADR 0011): un watcher por proyecto, cada uno con su PID.
|
|
15
|
+
export const pidFilePath = (projectDir = process.cwd()) => join(getKarajanHome(), "watchers", `${projectSlug(projectDir)}.pid`);
|
|
16
|
+
|
|
17
|
+
/** Solo lo de ESTE proyecto: sus planes, su onboarding y (opt) sus fuentes. */
|
|
18
|
+
export function watchedPaths(projectDir, { withSources = false } = {}) {
|
|
19
|
+
const slug = projectSlug(projectDir);
|
|
20
|
+
const paths = [join(getKarajanHome(), "onboarding", `${slug}.md`), join(getKarajanHome(), "plans", slug)];
|
|
21
|
+
return withSources ? [...paths, projectDir] : paths;
|
|
22
|
+
}
|
|
14
23
|
const DEFAULT_DEBOUNCE_MS = 1000;
|
|
15
|
-
const SKIP_SEGMENTS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".karajan", ".next", ".kj", "_diet"]);
|
|
24
|
+
const SKIP_SEGMENTS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".karajan", ".next", ".kj", "_diet", ".claude"]);
|
|
16
25
|
const SOURCE_EXTS = new Set(getAllCodeExtensions().map((e) => e.toLowerCase()));
|
|
17
26
|
|
|
18
27
|
export function isWatchable(path) {
|
|
@@ -26,11 +35,9 @@ export function startWatcher({ projectDir, config, logger = console, debounceMs
|
|
|
26
35
|
if (!projectDir) throw new Error("startWatcher: projectDir is required");
|
|
27
36
|
const slug = projectSlug(projectDir);
|
|
28
37
|
const dim = config?.rag?.embedder?.dim || 768;
|
|
29
|
-
const db =
|
|
38
|
+
const db = openProjectStore({ projectDir, dim });
|
|
30
39
|
const embedder = makeGovernedEmbedder(config);
|
|
31
|
-
const
|
|
32
|
-
if (withSources) paths.push(projectDir);
|
|
33
|
-
const watcher = chokidar.watch(paths, { ignoreInitial: true, persistent: true });
|
|
40
|
+
const watcher = chokidar.watch(watchedPaths(projectDir, { withSources }), { ignoreInitial: true, persistent: true });
|
|
34
41
|
const pending = new Map();
|
|
35
42
|
const flush = async (p) => {
|
|
36
43
|
pending.delete(p);
|
|
@@ -49,11 +56,15 @@ export function startWatcher({ projectDir, config, logger = console, debounceMs
|
|
|
49
56
|
};
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
export function writePidFile(pid = process.pid) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
export function writePidFile(projectDir, pid = process.pid) {
|
|
60
|
+
const file = pidFilePath(projectDir);
|
|
61
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
62
|
+
writeFileSync(file, String(pid), "utf8");
|
|
63
|
+
}
|
|
64
|
+
export function clearPidFile(projectDir) { if (existsSync(pidFilePath(projectDir))) unlinkSync(pidFilePath(projectDir)); }
|
|
65
|
+
export function readPidFile(projectDir) {
|
|
66
|
+
if (!existsSync(pidFilePath(projectDir))) return null;
|
|
67
|
+
const pid = Number(readFileSync(pidFilePath(projectDir), "utf8").trim());
|
|
57
68
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
58
69
|
}
|
|
59
70
|
export function isPidAlive(pid) {
|
|
@@ -57,7 +57,29 @@ export async function ensureContractBlockPresent(projectDir) {
|
|
|
57
57
|
* La ruta se pregunta a git para que funcione tambien en un worktree, donde
|
|
58
58
|
* `.git` es un fichero y no un directorio.
|
|
59
59
|
*/
|
|
60
|
-
const LOCAL_ARTIFACTS = [
|
|
60
|
+
const LOCAL_ARTIFACTS = [
|
|
61
|
+
".reviews/", ".kj/", ".kj-ready.json",
|
|
62
|
+
// KJC-BUG-0220: lo que kj escribe DENTRO de .karajan/ y NO viaja. Sin esto
|
|
63
|
+
// se quedaba en tierra de nadie (ni versionado ni ignorado) y salia como `??`
|
|
64
|
+
// en cada git status del equipo. Se enumera en vez de ignorar .karajan/
|
|
65
|
+
// entero a proposito: kj.config.yml y las reglas del agente son del equipo, y
|
|
66
|
+
// esconderselas seria decidir por ellos. Los ficheros del supervisor viajan
|
|
67
|
+
// por su cauce sellado (ADR 0009) y tampoco entran aqui.
|
|
68
|
+
".karajan/harness/", // el arnes del anfitrion: cada maquina lo regenera
|
|
69
|
+
".karajan/policy-decisions.jsonl",
|
|
70
|
+
".karajan/policy-exceptions.jsonl",
|
|
71
|
+
".karajan/identity.local.yml", // identidad por clon (ADR 0005)
|
|
72
|
+
".karajan/sentinel-state.json",
|
|
73
|
+
".karajan/rag.db",
|
|
74
|
+
".karajan/audit-history.db",
|
|
75
|
+
".karajan/update-check.json",
|
|
76
|
+
".karajan/karajan.env",
|
|
77
|
+
".karajan/agent-skills/",
|
|
78
|
+
".karajan/audits/",
|
|
79
|
+
".karajan/reviews/",
|
|
80
|
+
".karajan/steward/",
|
|
81
|
+
".karajan/plans/",
|
|
82
|
+
];
|
|
61
83
|
|
|
62
84
|
export async function excludeLocalArtifacts(projectDir, run = runCommand) {
|
|
63
85
|
let file = path.join(projectDir, ".git", "info", "exclude");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj-provenance — KJC-BUG-0222. Un gate que dice QUE falla sin decir CONTRA QUE
|
|
3
|
+
* compara ensena a saltarselo. El aviso del harness decia "does not match the
|
|
4
|
+
* installed kj" y ahi acababa, asi que en campo se leyo como "kj se
|
|
5
|
+
* desincroniza solo": una sesion se monto el ritual de ejecutar `kj harden`
|
|
6
|
+
* antes de cada review y de cada commit, veinte veces en un dia, sin
|
|
7
|
+
* preguntarse por que. La causa estaba entera en el dato que faltaba: ese kj
|
|
8
|
+
* estaba npm-linkado a un arbol de desarrollo cuyas plantillas cambiaron seis
|
|
9
|
+
* veces en dos dias.
|
|
10
|
+
*
|
|
11
|
+
* Saltarse un gate ritualmente es peor que no tenerlo, porque parece que se
|
|
12
|
+
* cumple.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { dirname, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Que kj esta juzgando: version, de donde sale y si es un paquete instalado o
|
|
23
|
+
* un arbol de desarrollo enlazado. Nunca lanza: esto SOLO adorna un mensaje, y
|
|
24
|
+
* un fallo aqui jamas puede cambiar el veredicto de un gate.
|
|
25
|
+
*
|
|
26
|
+
* @returns {{version: string|null, root: string, linked: boolean, label: string}}
|
|
27
|
+
*/
|
|
28
|
+
export function kjProvenance(moduleDir = here) {
|
|
29
|
+
const root = resolve(moduleDir, "..", "..");
|
|
30
|
+
let version = null;
|
|
31
|
+
try {
|
|
32
|
+
version = createRequire(import.meta.url)(resolve(root, "package.json")).version || null;
|
|
33
|
+
} catch { /* sin manifest legible: se dice lo que se sabe */ }
|
|
34
|
+
// Un kj instalado vive bajo node_modules. Cualquier otra ruta es un arbol de
|
|
35
|
+
// trabajo enlazado (npm link), y entonces sus plantillas se mueven con cada
|
|
36
|
+
// edicion de quien lo desarrolla, en TODOS los proyectos de la maquina.
|
|
37
|
+
const linked = !root.includes(`${"node_modules"}/karajan-code`) && !root.endsWith("node_modules");
|
|
38
|
+
const v = version ? `kj ${version}` : "an unknown kj";
|
|
39
|
+
const label = linked
|
|
40
|
+
? `${v} LINKED from ${root} (a development tree: its templates move with every edit there, in every project on this machine)`
|
|
41
|
+
: `${v} (${root})`;
|
|
42
|
+
return { version, root, linked, label };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const gitIn = (root) => (args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* KJC-TSK-0886: el sello de que kj emite un veredicto. En un arbol enlazado la
|
|
49
|
+
* version no basta (el mismo package.json vale para dos estados del disco), asi
|
|
50
|
+
* que lleva rama y commit del working tree. Nunca lanza: adorna, no decide.
|
|
51
|
+
*/
|
|
52
|
+
export function kjStamp({ provenance = kjProvenance(), git = gitIn(provenance.root) } = {}) {
|
|
53
|
+
const stamp = { version: provenance.version, linked: provenance.linked };
|
|
54
|
+
if (!provenance.linked) return stamp;
|
|
55
|
+
try {
|
|
56
|
+
return { ...stamp, branch: git(["rev-parse", "--abbrev-ref", "HEAD"]).trim(), commit: git(["rev-parse", "HEAD"]).trim() };
|
|
57
|
+
} catch {
|
|
58
|
+
return stamp;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const STAMP_KEYS = ["version", "linked", "branch", "commit"];
|
|
63
|
+
export const stampDiffers = (a, b) => Boolean(a && b) && STAMP_KEYS.some((k) => a[k] !== b[k]);
|
|
64
|
+
|
|
65
|
+
export function stampLabel(s) {
|
|
66
|
+
const where = s.branch ? ` (${s.branch}@${(s.commit || "").slice(0, 7)})` : "";
|
|
67
|
+
return `kj ${s.version ?? "unknown"}${where}`;
|
|
68
|
+
}
|
package/src/review/rag-ledger.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import { verifySentinelScripts } from "../harden/sentinel-hooks.js";
|
|
12
|
+
import { resolveSentinelRoot, verifySentinelScripts } from "../harden/sentinel-hooks.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {string} projectDir
|
|
@@ -22,10 +22,18 @@ import { verifySentinelScripts } from "../harden/sentinel-hooks.js";
|
|
|
22
22
|
* the requirement fails closed until the human re-runs `kj harden`.
|
|
23
23
|
*/
|
|
24
24
|
export function readRagLedger(projectDir) {
|
|
25
|
-
|
|
25
|
+
// KJC-BUG-0223: el harness vive en la RAIZ del proyecto. Este modulo miraba
|
|
26
|
+
// el directorio recibido (en la practica, process.cwd()) mientras el
|
|
27
|
+
// verificador resolvia el toplevel de git, asi que desde un subdirectorio los
|
|
28
|
+
// dos juzgaban sobre arboles distintos y el gate bloqueaba siempre.
|
|
29
|
+
const root = resolveSentinelRoot(projectDir);
|
|
30
|
+
const harnessDir = path.join(root, ".karajan", "harness");
|
|
26
31
|
const harness = existsSync(harnessDir);
|
|
27
|
-
const check = harness ? verifySentinelScripts({ projectDir }) : { ok: false, mismatched: [] };
|
|
28
|
-
|
|
32
|
+
const check = harness ? verifySentinelScripts({ projectDir: root }) : { ok: false, mismatched: [] };
|
|
33
|
+
// KJC-BUG-0222: `drift` viaja hasta el mensaje. Un guard sellado que se ha
|
|
34
|
+
// quedado atras NO lo arregla `kj harden` (lo deja en paz a proposito), asi
|
|
35
|
+
// que mandar ese comando para ese fichero es un bucle.
|
|
36
|
+
const base = { root, harness, verified: harness && check.ok === true, mismatched: check.mismatched || [], drift: check.drift || [], queries: [], hits: [] };
|
|
29
37
|
let state;
|
|
30
38
|
try {
|
|
31
39
|
state = JSON.parse(readFileSync(path.join(harnessDir, "sentinel-state.json"), "utf8"));
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* concepts that are not in the diff. That list travels to the reviewer,
|
|
13
13
|
* because the bugs of 2026-09-16 were exactly the twin nobody touched.
|
|
14
14
|
*/
|
|
15
|
+
import { kjProvenance } from "./kj-provenance.js";
|
|
15
16
|
import { sourceFilesOf } from "./tests-with-code.js";
|
|
16
17
|
|
|
17
18
|
export const RAG_RULE_ID = "method.rag.code";
|
|
@@ -30,6 +31,17 @@ function liveGrant(standingExceptions, now) {
|
|
|
30
31
|
* @param {{available: boolean, reason?: string, queries: object[], hits: string[]}} [args.ledger]
|
|
31
32
|
* @returns {{ok: boolean, mode: "docs-only"|"pass"|"granted"|"block", reason?: string, grant?: object, sources?: string[], queries?: number, covered?: string[], uncovered?: string[], twinsUntouched?: string[]}}
|
|
32
33
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* KJC-BUG-0222 — el comando que resuelve ESTE estado. `kj harden` repone lo que
|
|
36
|
+
* nadie sello; un guard sellado que se ha quedado atras lo deja en paz a
|
|
37
|
+
* proposito (KJC-BUG-0193), y avanzarlo es acto humano.
|
|
38
|
+
*/
|
|
39
|
+
function harnessRemedy(drift = []) {
|
|
40
|
+
return drift.length > 0
|
|
41
|
+
? `the human runs \`kj harden\` to restore them, and \`kj harden --commit\` for the sealed ones (${drift.join(", ")}), which \`kj harden\` leaves untouched on purpose`
|
|
42
|
+
: "the human runs `kj harden` to regenerate it";
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles = [], ledger, standingExceptions = [], now = new Date(), env = {} }) {
|
|
34
46
|
const { sources } = sourceFilesOf(config, stagedFiles);
|
|
35
47
|
if (sources.length === 0) return { ok: true, mode: "docs-only" };
|
|
@@ -55,7 +67,9 @@ export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles =
|
|
|
55
67
|
// than the ledger) cannot vouch for anything: fail closed, like the tamper
|
|
56
68
|
// check does, until the human regenerates it.
|
|
57
69
|
if (ledger?.harness === true && ledger?.verified === false) {
|
|
58
|
-
|
|
70
|
+
// KJC-BUG-0222: decir CONTRA QUE no coincide. Sin ese dato el aviso se lee
|
|
71
|
+
// como "kj se desincroniza solo" y se automatiza el sintoma.
|
|
72
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the installed Sentinel harness does not match the templates of ${kjProvenance().label} (${(ledger.mismatched || []).join(", ") || "scripts missing"}): ${harnessRemedy(ledger.drift)} before code is reviewed. ${GRANT_HINT}` };
|
|
59
73
|
}
|
|
60
74
|
if (!ledger?.available) {
|
|
61
75
|
return { ok: false, mode: "block", sources, reason: `${head} — ${ledger?.reason || "no session ledger"}: consult the RAG about the change (kj_rag_query / kj rag query) and run \`kj review --staged\` again. ${GRANT_HINT}` };
|
|
@@ -82,7 +96,7 @@ export function checkRagRequirement({ config = {}, stagedFiles = [], newFiles =
|
|
|
82
96
|
* block — the diff hash pins the file set, so a source missing from
|
|
83
97
|
* `covered` was never answered about, whatever the block claims.
|
|
84
98
|
*/
|
|
85
|
-
export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness = true, verified = true, mismatched = [], standingExceptions = [], now = new Date() }) {
|
|
99
|
+
export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness = true, verified = true, mismatched = [], drift = [], standingExceptions = [], now = new Date() }) {
|
|
86
100
|
const { sources } = sourceFilesOf(config, stagedFiles);
|
|
87
101
|
if (sources.length === 0) return { ok: true, mode: "docs-only" };
|
|
88
102
|
const grant = liveGrant(standingExceptions, now);
|
|
@@ -94,7 +108,7 @@ export function checkRagVerdict({ config = {}, stagedFiles = [], rag, harness =
|
|
|
94
108
|
return { ok: false, mode: "block", sources, reason: `${head} — no Sentinel harness in this tree, so no session ledger can exist: install it with \`kj harden\`. ${GRANT_HINT}` };
|
|
95
109
|
}
|
|
96
110
|
if (!verified) {
|
|
97
|
-
return { ok: false, mode: "block", sources, reason: `${head} — the Sentinel harness does not match the
|
|
111
|
+
return { ok: false, mode: "block", sources, reason: `${head} — the installed Sentinel harness does not match the templates of ${kjProvenance().label} (${mismatched.join(", ") || "scripts missing"}): ${harnessRemedy(drift)}. ${GRANT_HINT}` };
|
|
98
112
|
}
|
|
99
113
|
if (!rag) {
|
|
100
114
|
return { ok: false, mode: "block", sources, reason: `${head} — the verdict carries no rag block; run \`kj review --staged\` again. ${GRANT_HINT}` };
|
|
@@ -13,6 +13,7 @@ import crypto from "node:crypto";
|
|
|
13
13
|
import fs from "node:fs/promises";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { runCommand } from "../utils/process.js";
|
|
16
|
+
import { kjStamp, stampDiffers, stampLabel } from "./kj-provenance.js";
|
|
16
17
|
|
|
17
18
|
const STORE_DIR = path.join(".karajan", "reviews");
|
|
18
19
|
|
|
@@ -35,6 +36,7 @@ export async function saveVerdict(projectDir, diff, verdict) {
|
|
|
35
36
|
const hash = diffHash(diff);
|
|
36
37
|
const record = {
|
|
37
38
|
...verdict,
|
|
39
|
+
kj: kjStamp(), // KJC-TSK-0886: que kj lo emitio; nunca lo dicta quien llama
|
|
38
40
|
diffHash: hash,
|
|
39
41
|
timestamp: new Date().toISOString(),
|
|
40
42
|
};
|
|
@@ -143,5 +145,8 @@ export async function checkVerdict(projectDir, diff) {
|
|
|
143
145
|
if (verdict.verdict !== "approved") {
|
|
144
146
|
return { ok: false, verdict, reason: `review was rejected by ${verdict.reviewer} — fix the issues and run \`kj review\` again` };
|
|
145
147
|
}
|
|
146
|
-
|
|
148
|
+
// KJC-TSK-0886: otro kj comprobando no bloquea, pero deja rastro.
|
|
149
|
+
const now = kjStamp();
|
|
150
|
+
if (!stampDiffers(verdict.kj, now)) return { ok: true, verdict };
|
|
151
|
+
return { ok: true, verdict, note: `verdict from ${stampLabel(verdict.kj)}, checked by ${stampLabel(now)}` };
|
|
147
152
|
}
|
package/src/sonar/api.js
CHANGED
|
@@ -82,8 +82,17 @@ async function sonarFetch(config, urlPath) {
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* KJC-BUG-0226 (issue #1838) — de donde sale la clave que se CONSULTA. El
|
|
87
|
+
* scanner sube el analisis con la que el repo declara en su
|
|
88
|
+
* sonar-project.properties; preguntando por otra (la derivada del remote), el
|
|
89
|
+
* audit acababa listando issues de repos ajenos del mismo SonarQube. Pasando
|
|
90
|
+
* el directorio, la resolucion lee esa misma clave declarada.
|
|
91
|
+
*/
|
|
92
|
+
const queryCwd = (config) => config?.projectDir || process.cwd();
|
|
93
|
+
|
|
85
94
|
export async function getQualityGateStatus(config, projectKey = null) {
|
|
86
|
-
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey });
|
|
95
|
+
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey, cwd: queryCwd(config) });
|
|
87
96
|
const body = await sonarFetch(config, `/api/qualitygates/project_status?projectKey=${effectiveProjectKey}`);
|
|
88
97
|
|
|
89
98
|
try {
|
|
@@ -95,7 +104,7 @@ export async function getQualityGateStatus(config, projectKey = null) {
|
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
export async function getOpenIssues(config, projectKey = null) {
|
|
98
|
-
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey });
|
|
107
|
+
const effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey, cwd: queryCwd(config) });
|
|
99
108
|
const body = await sonarFetch(config, `/api/issues/search?projectKeys=${effectiveProjectKey}&statuses=OPEN`);
|
|
100
109
|
|
|
101
110
|
let parsedIssues = [];
|
package/src/sonar/project-key.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
2
5
|
import { runCommand } from "../utils/process.js";
|
|
3
6
|
|
|
4
7
|
function slug(value) {
|
|
@@ -55,6 +58,27 @@ function canonicalRepoId(remoteUrl) {
|
|
|
55
58
|
return `${host}/${segments.join("/")}`;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* KJC-BUG-0226 (issue #1838) — la clave que el REPO declara en su
|
|
63
|
+
* sonar-project.properties es la que el servidor conoce, porque es con la que
|
|
64
|
+
* el scanner sube el analisis. Sin leerla aqui, kj escaneaba un proyecto y
|
|
65
|
+
* preguntaba por otro (una clave derivada del remote), y el audit acababa
|
|
66
|
+
* listando issues de repos ajenos del mismo SonarQube.
|
|
67
|
+
*
|
|
68
|
+
* Sincrono y tolerante: sin fichero, ilegible o sin la clave, se sigue por
|
|
69
|
+
* donde se iba.
|
|
70
|
+
*/
|
|
71
|
+
function declaredProjectKey(cwd) {
|
|
72
|
+
try {
|
|
73
|
+
const raw = fs.readFileSync(path.join(cwd, "sonar-project.properties"), "utf8");
|
|
74
|
+
for (const line of raw.split("\n")) {
|
|
75
|
+
const m = /^\s*sonar\.projectKey\s*=\s*(.+?)\s*$/.exec(line);
|
|
76
|
+
if (m) return m[1];
|
|
77
|
+
}
|
|
78
|
+
} catch { /* sin properties legible: se deriva como siempre */ }
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
export async function resolveSonarProjectKey(config, options = {}) {
|
|
59
83
|
const explicit = String(
|
|
60
84
|
options.projectKey || process.env.KJ_SONAR_PROJECT_KEY || config?.sonarqube?.project_key || ""
|
|
@@ -63,6 +87,15 @@ export async function resolveSonarProjectKey(config, options = {}) {
|
|
|
63
87
|
return normalizeProjectKey(explicit);
|
|
64
88
|
}
|
|
65
89
|
|
|
90
|
+
// KJC-BUG-0226: lo que el repo declara manda sobre lo que kj derivaria, que
|
|
91
|
+
// es justo lo que el scanner ya hace al subir el analisis. Solo con un `cwd`
|
|
92
|
+
// explicito: quien no dice que directorio le importa se queda como estaba, y
|
|
93
|
+
// asi esto no cambia el comportamiento de nadie mas que el de la consulta.
|
|
94
|
+
if (options.cwd) {
|
|
95
|
+
const declared = declaredProjectKey(options.cwd);
|
|
96
|
+
if (declared) return normalizeProjectKey(declared);
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
const remote = await runCommand("git", ["config", "--get", "remote.origin.url"]);
|
|
67
100
|
const remoteUrl = String(remote.stdout || "").trim();
|
|
68
101
|
if (remote.exitCode !== 0 || !remoteUrl) {
|
package/src/sonar/scanner.js
CHANGED
|
@@ -50,8 +50,38 @@ export function indexedFilesFrom(scanOutput) {
|
|
|
50
50
|
return files;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* KJC-BUG-0225 — `.git` es un fichero (no un directorio) en un worktree
|
|
55
|
+
* enlazado y en un submodulo: exactamente los dos casos que JGit no sabe abrir.
|
|
56
|
+
* Sin proceso y sin depender de que git responda.
|
|
57
|
+
*/
|
|
58
|
+
export function isLinkedWorktree(dir) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.statSync(path.join(dir, ".git")).isFile();
|
|
61
|
+
} catch {
|
|
62
|
+
return false; // sin .git no hay SCM que desactivar
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* KJC-BUG-0225 (issue #1839) — dentro de un carril de `kj worktree start`, el
|
|
68
|
+
* plugin SCM del scanner (JGit) no sabe abrir un worktree enlazado (`.git` es
|
|
69
|
+
* un fichero que apunta a `.git/worktrees/<carril>`) y revienta el analisis
|
|
70
|
+
* entero. El pre-gate lo reportaba como "UNAVAILABLE" y la ejecucion seguia:
|
|
71
|
+
* todo cambio hecho como el metodo manda, un carril por tarea, se quedaba sin
|
|
72
|
+
* quality gate y sin un rojo que lo dijera.
|
|
73
|
+
*
|
|
74
|
+
* Se desactiva SOLO el SCM, que en un carril no aporta nada (alimenta blame y
|
|
75
|
+
* asignacion de autoria); las reglas, la cobertura y el resto del analisis se
|
|
76
|
+
* quedan exactamente igual.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectKey
|
|
79
|
+
* @param {object} scanner config del scanner (scm_disabled lo fuerza siempre)
|
|
80
|
+
* @param {{linkedWorktree?: boolean}} [where]
|
|
81
|
+
*/
|
|
82
|
+
export function buildScannerOpts(projectKey, scanner = {}, where = {}) {
|
|
54
83
|
const opts = [`-Dsonar.projectKey=${projectKey}`];
|
|
84
|
+
if (scanner.scm_disabled || where.linkedWorktree) opts.push("-Dsonar.scm.disabled=true");
|
|
55
85
|
// KJC-TSK-0838: verbose is what makes the scanner name every indexed file.
|
|
56
86
|
if (scanner.verbose) opts.push("-Dsonar.verbose=true");
|
|
57
87
|
if (scanner.sources) opts.push(`-Dsonar.sources=${scanner.sources}`);
|
|
@@ -317,11 +347,16 @@ export async function runSonarScan(config, projectKey = null, { verbose = false,
|
|
|
317
347
|
: null;
|
|
318
348
|
|
|
319
349
|
const pick = await pickSonarScanner(sonarConfig.scanner);
|
|
350
|
+
// KJC-BUG-0225: en un carril, el plugin SCM del scanner (JGit) no sabe abrir
|
|
351
|
+
// el worktree enlazado y tumba el analisis entero. La senal es la causa
|
|
352
|
+
// literal del error y no necesita proceso: en un worktree enlazado (y en un
|
|
353
|
+
// submodulo, que JGit trata igual) `.git` es un FICHERO, no un directorio.
|
|
354
|
+
const linkedWorktree = isLinkedWorktree(scanCwd);
|
|
320
355
|
const env = {
|
|
321
356
|
...process.env,
|
|
322
357
|
SONAR_HOST_URL: pick.type === "native" ? rawHost : host,
|
|
323
358
|
SONAR_TOKEN: token || "",
|
|
324
|
-
SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig)
|
|
359
|
+
SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig, { linkedWorktree })
|
|
325
360
|
};
|
|
326
361
|
|
|
327
362
|
let cmd, args;
|