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/package.json
CHANGED
|
@@ -267,7 +267,8 @@ async function renderBoard() {
|
|
|
267
267
|
try {
|
|
268
268
|
const res = await fetch('/api/rag/query', {
|
|
269
269
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
270
|
-
|
|
270
|
+
// KJC-TSK-0885: the scoped view searches its own project's index.
|
|
271
|
+
body: JSON.stringify({ text, topK: 5, scope: ragScope?.value || 'all', ...(scopedProjectSlug ? { project: scopedProjectSlug } : {}) }),
|
|
271
272
|
});
|
|
272
273
|
const body = await res.json();
|
|
273
274
|
if (!res.ok) { ragResults.innerHTML = `<span style="color:var(--color-red);">Error: ${esc(body.error || res.status)}</span>`; return; }
|
|
@@ -27,7 +27,10 @@ async function showConfigEditor(scope = 'global') {
|
|
|
27
27
|
// resuelve <projectDir> desde KJ_PROJECT_DIR || cwd.
|
|
28
28
|
let cfg;
|
|
29
29
|
try {
|
|
30
|
-
|
|
30
|
+
// KJC-TSK-0885: a scoped view (/p/<slug>, app.js) names its project, so
|
|
31
|
+
// 'project' is THAT project's file, not the daemon's cwd.
|
|
32
|
+
const project = scopedProjectSlug ? `&project=${encodeURIComponent(scopedProjectSlug)}` : '';
|
|
33
|
+
const r = await fetch(`/api/config?scope=${encodeURIComponent(scope)}${project}`);
|
|
31
34
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
32
35
|
cfg = await r.json();
|
|
33
36
|
} catch (err) {
|
|
@@ -163,7 +166,7 @@ async function showConfigEditor(scope = 'global') {
|
|
|
163
166
|
const r = await fetch('/api/config', {
|
|
164
167
|
method: 'PUT',
|
|
165
168
|
headers: { 'Content-Type': 'application/json' },
|
|
166
|
-
body: JSON.stringify({ patch, scope }),
|
|
169
|
+
body: JSON.stringify({ patch, scope, ...(scopedProjectSlug ? { project: scopedProjectSlug } : {}) }),
|
|
167
170
|
});
|
|
168
171
|
if (!r.ok) {
|
|
169
172
|
const body = await r.json().catch(() => ({}));
|
|
@@ -28,7 +28,8 @@ async function openConversationPanel() {
|
|
|
28
28
|
headers: { 'Content-Type': 'application/json' },
|
|
29
29
|
// Sin agente en el body: decide el servidor (kj go --window fija el
|
|
30
30
|
// elegido por env; sin él, claude).
|
|
31
|
-
|
|
31
|
+
// KJC-TSK-0885: la vista scoped (/p/<slug>) abre la terminal en SU proyecto.
|
|
32
|
+
body: JSON.stringify(scopedProjectSlug ? { project: scopedProjectSlug } : {}),
|
|
32
33
|
});
|
|
33
34
|
started = await res.json();
|
|
34
35
|
if (!res.ok) throw new Error(started.error || `HTTP ${res.status}`);
|
|
@@ -52,9 +52,11 @@ function getProjectDir() {
|
|
|
52
52
|
return process.env.KJ_PROJECT_DIR || process.cwd();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// KJC-TSK-0885: a scoped view passes its project's dir; only without one does
|
|
56
|
+
// the daemon's KJ_PROJECT_DIR || cwd stand in (dashboard, old clients).
|
|
57
|
+
function configPath(scope = 'global', projectDir = null) {
|
|
56
58
|
if (scope === 'project') {
|
|
57
|
-
return join(getProjectDir(), '.karajan', 'kj.config.yml');
|
|
59
|
+
return join(projectDir || getProjectDir(), '.karajan', 'kj.config.yml');
|
|
58
60
|
}
|
|
59
61
|
// Same path the parent uses (src/utils/paths.js::getKarajanHome).
|
|
60
62
|
// KJC-TSK-0420: delegate to db.js::getKjHome — gives us KARAJAN_HOME
|
|
@@ -63,7 +65,7 @@ function configPath(scope = 'global') {
|
|
|
63
65
|
return join(getKjHome(), 'kj.config.yml');
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
function backupPath(scope = 'global') { return `${configPath(scope)}.bak`; }
|
|
68
|
+
function backupPath(scope = 'global', projectDir = null) { return `${configPath(scope, projectDir)}.bak`; }
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* v2.30.0 — Categorías para agrupar los campos en el modal. El backend
|
|
@@ -433,13 +435,13 @@ function applyModelMode(parsed, mode) {
|
|
|
433
435
|
*
|
|
434
436
|
* Returns sane defaults when the file doesn't exist yet.
|
|
435
437
|
*/
|
|
436
|
-
export function readConfig({ scope = 'global' } = {}) {
|
|
438
|
+
export function readConfig({ scope = 'global', projectDir = null } = {}) {
|
|
437
439
|
// PR4 — accept scope. Default 'global' preserves the previous behavior;
|
|
438
440
|
// 'project' reads <projectDir>/.karajan/kj.config.yml.
|
|
439
441
|
if (!SCOPES.includes(scope)) {
|
|
440
442
|
throw new Error(`Scope inválido: ${scope}. Permitidos: ${SCOPES.join(', ')}`);
|
|
441
443
|
}
|
|
442
|
-
const p = configPath(scope);
|
|
444
|
+
const p = configPath(scope, projectDir);
|
|
443
445
|
let parsed = {};
|
|
444
446
|
let exists = false;
|
|
445
447
|
if (existsSync(p)) {
|
|
@@ -471,16 +473,16 @@ export function readConfig({ scope = 'global' } = {}) {
|
|
|
471
473
|
*
|
|
472
474
|
* @returns {{ path: string, written: boolean, applied: object[], errors: string[] }}
|
|
473
475
|
*/
|
|
474
|
-
export function writeConfigPatch(patch, { scope = 'global' } = {}) {
|
|
476
|
+
export function writeConfigPatch(patch, { scope = 'global', projectDir = null } = {}) {
|
|
475
477
|
// PR4 — same shape as readConfig: scope decides target file.
|
|
476
478
|
if (!SCOPES.includes(scope)) {
|
|
477
479
|
return { path: '', scope, written: false, applied: [], errors: [`Scope inválido: ${scope}. Permitidos: ${SCOPES.join(', ')}`] };
|
|
478
480
|
}
|
|
479
481
|
if (!patch || typeof patch !== 'object') {
|
|
480
|
-
return { path: configPath(scope), scope, written: false, applied: [], errors: ['patch debe ser un objeto'] };
|
|
482
|
+
return { path: configPath(scope, projectDir), scope, written: false, applied: [], errors: ['patch debe ser un objeto'] };
|
|
481
483
|
}
|
|
482
484
|
// Read current state (or empty if file doesn't exist yet).
|
|
483
|
-
const p = configPath(scope);
|
|
485
|
+
const p = configPath(scope, projectDir);
|
|
484
486
|
let parsed = {};
|
|
485
487
|
if (existsSync(p)) {
|
|
486
488
|
try { parsed = yaml.load(readFileSync(p, 'utf8'), { json: true }) || {}; }
|
|
@@ -517,7 +519,7 @@ export function writeConfigPatch(patch, { scope = 'global' } = {}) {
|
|
|
517
519
|
// existing file first so the user can recover.
|
|
518
520
|
const dir = dirname(p);
|
|
519
521
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
520
|
-
if (existsSync(p)) { try { copyFileSync(p, backupPath(scope)); } catch { /* best-effort */ } }
|
|
522
|
+
if (existsSync(p)) { try { copyFileSync(p, backupPath(scope, projectDir)); } catch { /* best-effort */ } }
|
|
521
523
|
const dump = yaml.dump(parsed, { lineWidth: 120 });
|
|
522
524
|
// mkstemp would be nicer but we want to stay in the same dir for
|
|
523
525
|
// a guaranteed-atomic rename across the same filesystem.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0885 (BRD-P2, ADR 0011): the project behind a scoped view.
|
|
3
|
+
*
|
|
4
|
+
* The daemon's cwd is not the project (one board serves every project), so a
|
|
5
|
+
* `/p/<slug>` view sends its slug and the server resolves the directory. It
|
|
6
|
+
* never takes a path from the client: it reads the `projectDir` the project's
|
|
7
|
+
* own plans recorded, and accepts it only if it exists and its slug is the one
|
|
8
|
+
* asked for, so a plan cannot point the board at another directory.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { getHuBoardPlansDirs } from './db.js';
|
|
14
|
+
import { deriveProjectIdFromDir } from './sync.js';
|
|
15
|
+
|
|
16
|
+
const SLUG_RE = /^[a-zA-Z0-9_.-]+$/;
|
|
17
|
+
|
|
18
|
+
/** @returns {string|null} the project's directory, or null when unresolvable. */
|
|
19
|
+
export function projectDirForSlug(slug, { plansDirs = getHuBoardPlansDirs() } = {}) {
|
|
20
|
+
if (typeof slug !== 'string' || !SLUG_RE.test(slug) || slug.startsWith('.')) return null;
|
|
21
|
+
for (const root of plansDirs) {
|
|
22
|
+
let names;
|
|
23
|
+
try { names = readdirSync(join(root, slug)); } catch { continue; }
|
|
24
|
+
for (const name of names.filter((n) => n.endsWith('.json'))) {
|
|
25
|
+
let dir;
|
|
26
|
+
try { dir = JSON.parse(readFileSync(join(root, slug, name), 'utf8'))?.projectDir; } catch { continue; }
|
|
27
|
+
if (typeof dir === 'string' && deriveProjectIdFromDir(dir) === slug && existsSync(dir)) return dir;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The project a request is about: none when it names none (dashboard), its
|
|
35
|
+
* directory when it resolves, and an Error that says why when it does not.
|
|
36
|
+
*/
|
|
37
|
+
export function requestProjectDir(project) {
|
|
38
|
+
if (project === undefined || project === null || project === '') return null;
|
|
39
|
+
const dir = projectDirForSlug(project);
|
|
40
|
+
if (!dir) throw new Error(`project "${project}" has no plan recording where it lives: run a kj command in it once`);
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
@@ -49,6 +49,7 @@ import { subscribe as subscribeEvents } from '../event-bus.js';
|
|
|
49
49
|
import { runKjCommand, listSupportedCommands } from '../command-runner.js';
|
|
50
50
|
import { runPreflight } from '../preflight.js';
|
|
51
51
|
import { readConfig, writeConfigPatch } from '../config-yaml.js';
|
|
52
|
+
import { requestProjectDir } from '../project-dir.js';
|
|
52
53
|
import { BOOT_TIME, PKG_VERSION } from '../boot-info.js';
|
|
53
54
|
import { addTombstone, getDb, isTombstoned as isTombstonedFn, removeTombstone, listTombstones } from '../db.js';
|
|
54
55
|
import { publish as publishEvent } from '../event-bus.js';
|
|
@@ -1095,7 +1096,8 @@ router.post('/projects/:id/run', (req, res) => {
|
|
|
1095
1096
|
router.get('/config', (req, res) => {
|
|
1096
1097
|
try {
|
|
1097
1098
|
const scope = req.query?.scope || 'global';
|
|
1098
|
-
|
|
1099
|
+
// KJC-TSK-0885: a scoped view names its project; the daemon's cwd is not it.
|
|
1100
|
+
const data = readConfig({ scope, projectDir: requestProjectDir(req.query?.project) });
|
|
1099
1101
|
res.json(data);
|
|
1100
1102
|
} catch (err) {
|
|
1101
1103
|
res.status(400).json({ error: err.message });
|
|
@@ -1116,7 +1118,9 @@ router.put('/config', (req, res) => {
|
|
|
1116
1118
|
try {
|
|
1117
1119
|
const patch = req.body?.patch;
|
|
1118
1120
|
const scope = req.body?.scope || 'global';
|
|
1119
|
-
|
|
1121
|
+
let projectDir;
|
|
1122
|
+
try { projectDir = requestProjectDir(req.body?.project); } catch (err) { return res.status(400).json({ error: err.message }); }
|
|
1123
|
+
const result = writeConfigPatch(patch, { scope, projectDir });
|
|
1120
1124
|
if (!result.written) {
|
|
1121
1125
|
return res.status(400).json({ error: 'No se pudo guardar la configuración', details: result });
|
|
1122
1126
|
}
|
|
@@ -1353,13 +1357,17 @@ router.post('/rag/query', async (req, res) => {
|
|
|
1353
1357
|
if (typeof text !== 'string' || text.length === 0) {
|
|
1354
1358
|
return res.status(400).json({ error: "'text' is required (non-empty string)" });
|
|
1355
1359
|
}
|
|
1360
|
+
// KJC-TSK-0885 (ADR 0011): a scoped view searches ITS project's index; the
|
|
1361
|
+
// machine-wide store (no filter) returned chunks of any repo.
|
|
1362
|
+
let projectDir;
|
|
1363
|
+
try { projectDir = requestProjectDir(req.body?.project); } catch (err) { return res.status(400).json({ error: err.message }); }
|
|
1356
1364
|
try {
|
|
1357
1365
|
const { openVecStore, countChunks } = await import('karajan-core/vec-store');
|
|
1358
1366
|
// KJC-TSK-0632: resolved from karajan-core — the board carries zero
|
|
1359
1367
|
// relative imports into the CLI src tree (see no-cli-imports test).
|
|
1360
1368
|
const { makeEmbedder } = await import('karajan-core/rag/embedders/factory');
|
|
1361
1369
|
const { query } = await import('karajan-core/rag/retriever');
|
|
1362
|
-
const db = openVecStore({ dim: 768 });
|
|
1370
|
+
const db = openVecStore({ dim: 768, ...(projectDir ? { path: path.join(projectDir, '.karajan', 'rag.db') } : {}) });
|
|
1363
1371
|
try {
|
|
1364
1372
|
if (countChunks(db) === 0) return res.json({ hits: [], empty: true, topK, scope });
|
|
1365
1373
|
const embedder = makeEmbedder({});
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { Router } from 'express';
|
|
10
10
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
11
11
|
|
|
12
|
+
import { requestProjectDir } from './project-dir.js';
|
|
13
|
+
|
|
12
14
|
const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
13
15
|
const WS_PATH = '/api/terminal/ws';
|
|
14
16
|
|
|
@@ -20,7 +22,9 @@ export function terminalRouter(manager, { defaultAgent = 'claude' } = {}) {
|
|
|
20
22
|
const router = Router();
|
|
21
23
|
router.post('/start', (req, res) => {
|
|
22
24
|
try {
|
|
23
|
-
|
|
25
|
+
// KJC-TSK-0885: la vista scoped nombra su proyecto; el servidor resuelve el dir.
|
|
26
|
+
const projectDir = requestProjectDir(req.body?.project);
|
|
27
|
+
res.json(manager.start({ agent: req.body?.agent ?? defaultAgent, ...(projectDir ? { cwd: projectDir } : {}) }));
|
|
24
28
|
} catch (err) {
|
|
25
29
|
res.status(400).json({ error: err.message });
|
|
26
30
|
}
|
|
@@ -47,8 +47,12 @@ export function createTerminalManager({ spawnPty, cwd, env = process.env, prompt
|
|
|
47
47
|
|
|
48
48
|
return {
|
|
49
49
|
/** Arranca (o devuelve) LA sesión — reutilizar la viva evita que cada
|
|
50
|
-
* recarga multiplique agentes.
|
|
51
|
-
|
|
50
|
+
* recarga multiplique agentes. KJC-TSK-0885: `cwd` es el proyecto de la
|
|
51
|
+
* vista; sin él, el del daemon. @param {{agent: string, cwd?: string}} params */
|
|
52
|
+
start({ agent, cwd: wanted = cwd }) {
|
|
53
|
+
if (session && session.cwd !== wanted) {
|
|
54
|
+
throw new Error(`terminal: ya hay una sesión viva en ${session.cwd}; termínala antes de abrir otra en ${wanted}.`);
|
|
55
|
+
}
|
|
52
56
|
if (session) return { termId: session.termId, token: session.token, reused: true };
|
|
53
57
|
// hasOwn: sin él, "constructor"/"__proto__" indexarían el prototipo.
|
|
54
58
|
const spec = Object.hasOwn(TERMINAL_AGENTS, agent) ? TERMINAL_AGENTS[agent] : null;
|
|
@@ -65,10 +69,11 @@ export function createTerminalManager({ spawnPty, cwd, env = process.env, prompt
|
|
|
65
69
|
name: 'xterm-256color',
|
|
66
70
|
cols: 120,
|
|
67
71
|
rows: 32,
|
|
68
|
-
cwd,
|
|
72
|
+
cwd: wanted,
|
|
69
73
|
env: cleanEnv,
|
|
70
74
|
});
|
|
71
75
|
const current = {
|
|
76
|
+
cwd: wanted,
|
|
72
77
|
termId: `term-${randomBytes(8).toString('hex')}`,
|
|
73
78
|
token: randomBytes(16).toString('hex'),
|
|
74
79
|
pty,
|
|
@@ -40,9 +40,16 @@ export function hintForReason(reason, image) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* KJC-BUG-0228 (issue #1840) — OPT-IN. La imagen por defecto es de un tercero y
|
|
45
|
+
* dejo de existir en el registro publico, asi que el paso venia activo, fallaba
|
|
46
|
+
* con "pull access denied" y se saltaba en cada `kj audit`. kj no puede depender
|
|
47
|
+
* por defecto de algo que otro puede borrar: quien quiera el scorecard lo pide,
|
|
48
|
+
* y quien tenga su propia imagen la declara en `audit.harness.image`.
|
|
49
|
+
*/
|
|
43
50
|
export function normalizeHarnessConfig(h = {}) {
|
|
44
51
|
return {
|
|
45
|
-
enabled: h
|
|
52
|
+
enabled: h?.enabled === true,
|
|
46
53
|
image: h.image || D.image,
|
|
47
54
|
pullTimeoutMs: pos(h.pull_timeout_ms, D.pullTimeoutMs),
|
|
48
55
|
assessTimeoutMs: pos(h.assess_timeout_ms, D.assessTimeoutMs),
|
|
@@ -20,7 +20,12 @@ const ts = () => new Date().toISOString().replaceAll(/[:.]/g, "-");
|
|
|
20
20
|
|
|
21
21
|
export async function runHarnessSection({ projectDir, harnessConfig = null, runAssess = runHarnessAssess } = {}) {
|
|
22
22
|
const cfg = normalizeHarnessConfig(harnessConfig || {});
|
|
23
|
-
|
|
23
|
+
// KJC-BUG-0228: desactivado por defecto, y DICHO. Un paso que desaparece en
|
|
24
|
+
// silencio del informe se lee como un paso aprobado.
|
|
25
|
+
if (!cfg.enabled) {
|
|
26
|
+
const asked = harnessConfig?.enabled === false;
|
|
27
|
+
return { ok: true, skipped: true, reason: asked ? "disabled" : "opt-in" };
|
|
28
|
+
}
|
|
24
29
|
const res = await runAssess(projectDir, cfg);
|
|
25
30
|
if (!res.ok) return { ok: false, error: res.error, reason: res.reason, hint: res.hint, image: cfg.image };
|
|
26
31
|
const outDir = path.join(projectDir, ".karajan", "audit-runs", ts());
|
|
@@ -38,7 +43,13 @@ export async function runHarnessSection({ projectDir, harnessConfig = null, runA
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export function formatHarnessSection(summary) {
|
|
41
|
-
if (!summary
|
|
46
|
+
if (!summary) return "";
|
|
47
|
+
// KJC-BUG-0228: cuando no corre porque nadie lo pidio, se dice como pedirlo.
|
|
48
|
+
// Callarlo dejaba un informe que parecia completo sin serlo.
|
|
49
|
+
if (summary.reason === "opt-in") {
|
|
50
|
+
return "## Harness Scorecard\n\n_not run: it is opt-in (`audit.harness.enabled: true`), and it needs a scorecard image you can pull — declare yours in `audit.harness.image`._\n";
|
|
51
|
+
}
|
|
52
|
+
if (summary.skipped) return "";
|
|
42
53
|
if (!summary.ok) {
|
|
43
54
|
const reason = summary.reason ? ` (${summary.reason})` : "";
|
|
44
55
|
const hint = summary.hint ? `\n\n${summary.hint}\n` : "";
|
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
* Red only when an index EXISTS and is incomplete or stale: a machine with
|
|
5
5
|
* no index yet (CI, a fresh clone) gets the command, not a failure.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { openProjectStore } from "../rag/project-store.js";
|
|
8
8
|
import { ragIndexCoverage } from "../rag/coverage.js";
|
|
9
|
+
import { ragExclude } from "../rag/indexable.js";
|
|
9
10
|
import { STRATEGY } from "./types.js";
|
|
10
11
|
|
|
11
|
-
export function createRagCoverageCheck({ openStore = (dim) =>
|
|
12
|
+
export function createRagCoverageCheck({ openStore = (dim, projectDir) => openProjectStore({ projectDir, dim }) } = {}) {
|
|
12
13
|
return {
|
|
13
14
|
name: "rag-coverage",
|
|
14
15
|
label: "RAG index coverage (ADR 0010)",
|
|
15
16
|
strategy: STRATEGY.NONE,
|
|
16
17
|
async detect({ config = {}, projectDir = process.cwd() } = {}) {
|
|
17
|
-
const db = openStore(config?.rag?.embedder?.dim || 768);
|
|
18
|
+
const db = openStore(config?.rag?.embedder?.dim || 768, projectDir);
|
|
18
19
|
try {
|
|
19
20
|
let c;
|
|
20
21
|
try {
|
|
21
|
-
c = await ragIndexCoverage(projectDir, { db });
|
|
22
|
+
c = await ragIndexCoverage(projectDir, { db, exclude: ragExclude(config) });
|
|
22
23
|
} catch (err) {
|
|
23
24
|
// Corrupt store, unreachable stamp, git failure: a failing check,
|
|
24
25
|
// never a silent pass. The only tolerated non-red state is "no index".
|
|
@@ -41,7 +42,10 @@ export function createRagCoverageCheck({ openStore = (dim) => openVecStore({ dim
|
|
|
41
42
|
const sample = gaps.slice(0, 3).join(", ") + (gaps.length > 3 ? ", …" : "");
|
|
42
43
|
return { ok: false, severity: "fail", detail: `${c.missing.length} source(s) outside the RAG index, ${c.stale.length} stale (${sample}) — ${fix}` };
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
// KJC-BUG-0221: lo que kj genera no se exige indexado, pero se dice —
|
|
46
|
+
// una exencion que no se ve es indistinguible de un gate que dejo de mirar.
|
|
47
|
+
const generated = c.generated?.length ? `, ${c.generated.length} generated by kj (not required)` : "";
|
|
48
|
+
return { ok: true, severity: "info", detail: `${c.indexed}/${c.total} sources indexed at ${c.lastIndexedCommit.slice(0, 9)}${generated}` };
|
|
45
49
|
} finally {
|
|
46
50
|
db.close();
|
|
47
51
|
}
|
package/src/checks/runner.js
CHANGED
|
@@ -92,8 +92,14 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
92
92
|
entries.map(async (entry) => {
|
|
93
93
|
if (entry.status === STATUS.SKIPPED) return;
|
|
94
94
|
const start = Date.now();
|
|
95
|
+
// Per-check override: checks that spawn slow tools (e.g. `npx <pkg>` cold
|
|
96
|
+
// start) declare detectTimeoutMs so the blanket pipeline timeout doesn't
|
|
97
|
+
// kill them. Falls back to the pipeline-wide timeoutMs.
|
|
98
|
+
const detectTimeoutMs = Number.isFinite(entry.check.detectTimeoutMs)
|
|
99
|
+
? entry.check.detectTimeoutMs
|
|
100
|
+
: timeoutMs;
|
|
95
101
|
try {
|
|
96
|
-
const result = await withTimeout(entry.check.detect({ config, signal }),
|
|
102
|
+
const result = await withTimeout(entry.check.detect({ config, signal }), detectTimeoutMs, entry.check.name);
|
|
97
103
|
entry.detectResult = result;
|
|
98
104
|
entry.runMs = Date.now() - start;
|
|
99
105
|
entry.status = resolveStatusFromDetect(entry.check, result);
|
|
@@ -112,9 +118,21 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
112
118
|
} catch (err) {
|
|
113
119
|
entry.runMs = Date.now() - start;
|
|
114
120
|
if (err instanceof TimeoutError) {
|
|
115
|
-
entry.detectResult = { ok: false, severity: "warn", detail: `Timeout after ${
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
entry.detectResult = { ok: false, severity: "warn", detail: `Timeout after ${detectTimeoutMs}ms` };
|
|
122
|
+
// A timeout on a degradable (optional) check must not block the
|
|
123
|
+
// pipeline: degrade to WARN and disable its feature flags, same as
|
|
124
|
+
// a regular failure (KJC-BUG-0049 semantics).
|
|
125
|
+
if (entry.check.degradable) {
|
|
126
|
+
for (const dotPath of entry.check.degradable.disables || []) {
|
|
127
|
+
setDotPath(overrides, dotPath, false);
|
|
128
|
+
}
|
|
129
|
+
entry.status = STATUS.WARN;
|
|
130
|
+
entry.detail = `${entry.detectResult.detail} → ${entry.check.degradable.warn}`;
|
|
131
|
+
entry.degraded = true;
|
|
132
|
+
} else {
|
|
133
|
+
entry.status = STATUS.TIMEOUT;
|
|
134
|
+
entry.detail = entry.detectResult.detail;
|
|
135
|
+
}
|
|
118
136
|
} else {
|
|
119
137
|
entry.detectResult = { ok: false, severity: "fail", detail: `Detection error: ${err.message}` };
|
|
120
138
|
entry.status = STATUS.FAIL;
|
|
@@ -172,7 +190,11 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
172
190
|
entries.filter((e) => e.remediated).map(async (entry) => {
|
|
173
191
|
const start = Date.now();
|
|
174
192
|
try {
|
|
175
|
-
const result = await withTimeout(
|
|
193
|
+
const result = await withTimeout(
|
|
194
|
+
entry.check.detect({ config: verifyConfig, signal }),
|
|
195
|
+
Number.isFinite(entry.check.detectTimeoutMs) ? entry.check.detectTimeoutMs : timeoutMs,
|
|
196
|
+
`${entry.check.name}:reverify`
|
|
197
|
+
);
|
|
176
198
|
entry.runMs += Date.now() - start;
|
|
177
199
|
if (!result.ok) {
|
|
178
200
|
entry.status = STATUS.FAIL;
|
package/src/checks/skills.js
CHANGED
|
@@ -23,6 +23,16 @@ export function createOpenSkillsCheck() {
|
|
|
23
23
|
strategy: STRATEGY.PROMPT,
|
|
24
24
|
applies: skillsEnabled,
|
|
25
25
|
describe: "Install OpenSkills globally: npm install -g openskills",
|
|
26
|
+
// Detection shells out to `npx openskills --version`; a cold npx cache
|
|
27
|
+
// downloads the package first (~8s observed), well above the pipeline's
|
|
28
|
+
// 3s blanket timeout. Must exceed openskills-client's internal 15s timeout.
|
|
29
|
+
detectTimeoutMs: 20_000,
|
|
30
|
+
// Optional feature: even if detection times out, degrade instead of
|
|
31
|
+
// blocking the whole preflight (skill injection is skipped for the run).
|
|
32
|
+
degradable: {
|
|
33
|
+
disables: ["skills.enabled"],
|
|
34
|
+
warn: "skill auto-install disabled for this run",
|
|
35
|
+
},
|
|
26
36
|
async detect() {
|
|
27
37
|
const available = await isOpenSkillsAvailable();
|
|
28
38
|
if (available) {
|
package/src/checks/types.js
CHANGED
|
@@ -59,7 +59,11 @@
|
|
|
59
59
|
* @property {DegradableSpec} [degradable] - KJC-BUG-0049: si está definido y el check falla, en lugar de abortar
|
|
60
60
|
* el preflight, se desactivan los flags listados en `disables` y se emite
|
|
61
61
|
* WARN con `warn`. Útil para features opcionales (auto_pr, auto_push,
|
|
62
|
-
* sonar advisory, etc.) que no son blockers absolutos.
|
|
62
|
+
* sonar advisory, etc.) que no son blockers absolutos. También aplica
|
|
63
|
+
* cuando el detect agota su timeout (timeout → WARN, no TIMEOUT).
|
|
64
|
+
* @property {number} [detectTimeoutMs] - per-check detect timeout override. Use for checks that spawn slow
|
|
65
|
+
* tools (e.g. `npx <pkg>` cold start) where the pipeline-wide
|
|
66
|
+
* timeoutMs would kill an otherwise healthy detect.
|
|
63
67
|
*/
|
|
64
68
|
|
|
65
69
|
/**
|
package/src/cli/register-meta.js
CHANGED
|
@@ -11,7 +11,7 @@ import { identityCommand } from "../commands/identity.js";
|
|
|
11
11
|
import { policyCommand } from "../commands/policy.js";
|
|
12
12
|
import { claimsCommand, claimsGateCommand } from "../commands/claims.js";
|
|
13
13
|
import { stewardSweepCommand } from "../commands/steward.js";
|
|
14
|
-
import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand, ragCoversCommand } from "../commands/rag.js";
|
|
14
|
+
import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand, ragCoversCommand, ragMigrateCommand } from "../commands/rag.js";
|
|
15
15
|
import { qmdQueryCommand } from "../commands/qmd.js";
|
|
16
16
|
import { ragMcpCommand } from "../commands/rag-mcp.js";
|
|
17
17
|
import { watchStartCommand, watchStopCommand, watchStatusCommand } from "../commands/watch.js";
|
|
@@ -480,6 +480,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
480
480
|
// KJC-BUG-0197: manipulación y desfase son hechos distintos y se leen
|
|
481
481
|
// distinto. Un desfase no es un defecto: los guardias que corren están
|
|
482
482
|
// sellados, solo son más viejos que las plantillas de este árbol.
|
|
483
|
+
else if (res.regenerated?.length && res.ok && !res.drift?.length) console.log(`sentinel verify: ${res.reason}`);
|
|
483
484
|
else if (res.drift?.length && res.ok) console.log(`sentinel verify: scripts sellados, con desfase (${res.drift.join(", ")}) — ${res.reason}`);
|
|
484
485
|
else console.log(res.ok ? "sentinel verify: scripts intactos" : `sentinel verify: modificados fuera de kj harden: ${res.mismatched.join(", ")} — restaura con kj harden`);
|
|
485
486
|
process.exitCode = res.ok ? 0 : 1;
|
|
@@ -526,6 +527,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
526
527
|
.description("Index plans + onboarding (and optionally project sources) into the local vector store")
|
|
527
528
|
.option("--with-sources", "Also index the projectDir's JS/TS files")
|
|
528
529
|
.option("--since <ref>", "Only re-index files changed since <ref> ('auto' uses the last indexed commit; falls back to full index if unset). KJC-TSK-0455.")
|
|
530
|
+
.option("--file <paths...>", "Index exactly these files (a missing one leaves the index): the remedy for a file absent from the index")
|
|
529
531
|
.option("--json", "Output the totals as JSON")
|
|
530
532
|
.action(async (flags) => {
|
|
531
533
|
await withConfig(pkgVersion, "rag-index", flags, async ({ config, logger }) => {
|
|
@@ -534,6 +536,15 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
534
536
|
});
|
|
535
537
|
// KJC-BUG-0216: el gate rag-first no puede pedir lo imposible. Esto responde
|
|
536
538
|
// si el RAG PUEDE hablar de un fichero, y si no, por que.
|
|
539
|
+
// KJC-TSK-0888 (ADR 0011): el paso al indice por proyecto, sin reindexar.
|
|
540
|
+
rag.command("migrate")
|
|
541
|
+
.description("Copy this project's chunks from the machine-wide index into its own .karajan/rag.db, embeddings included (no re-embedding); the global index is left untouched")
|
|
542
|
+
.option("--json", "Emit the result as JSON")
|
|
543
|
+
.action(async (flags) => {
|
|
544
|
+
await withConfig(pkgVersion, "rag-migrate", flags, async ({ config }) => {
|
|
545
|
+
await ragMigrateCommand({ config, flags });
|
|
546
|
+
});
|
|
547
|
+
});
|
|
537
548
|
rag.command("covers <file>")
|
|
538
549
|
.description("Whether the RAG can answer about a file: exit 0 when indexed, 1 with the reason it cannot (no adapter, skipped path, empty or stale index)")
|
|
539
550
|
.option("--json", "Emit the state as JSON")
|
|
@@ -564,7 +575,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
564
575
|
.option("--rag-mode <mode>", "hybrid (default) | semantic (cosine only) | keyword (BM25 only)", "hybrid")
|
|
565
576
|
.option("--alpha <n>", "Hybrid weight for semantic component (0..1). Default 0.6", "0.6")
|
|
566
577
|
.option("--where <clause>", "Metadata filter, e.g. 'symbol=loadConfig' or 'hu_id=HU-003 AND kind=plan'")
|
|
567
|
-
.option("--library", "Query the distilled engineering canon (pattern cards shipped with kj + ~/.karajan/library
|
|
578
|
+
.option("--library", "Query the distilled engineering canon (pattern cards shipped with kj + ~/.karajan/library, in ~/.karajan/library.db) instead of the project corpus; a project's own .karajan/library cards live in its index and answer normal queries. Cards carry problem signatures, when-NOT-to-apply and canonical citations (KJC-TSK-0697).")
|
|
568
579
|
.option("--rerank", "Apply cross-encoder rerank to top-K (needs @huggingface/transformers, slower but more precise)")
|
|
569
580
|
.option("--rerank-model <name>", "Cross-encoder model id. Default: Xenova/ms-marco-MiniLM-L-6-v2")
|
|
570
581
|
.option("--json", "Output the hits as JSON")
|
|
@@ -627,10 +638,10 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
627
638
|
await withConfig(pkgVersion, "watch:start", flags, async ({ config, logger }) => watchStartCommand({ config, logger, flags }));
|
|
628
639
|
});
|
|
629
640
|
watch.command("stop").description("Stop the watcher daemon").action(async () => {
|
|
630
|
-
await withConfig(pkgVersion, "watch:stop", {}, async ({ logger }) => watchStopCommand({ logger }));
|
|
641
|
+
await withConfig(pkgVersion, "watch:stop", {}, async ({ config, logger }) => watchStopCommand({ config, logger }));
|
|
631
642
|
});
|
|
632
643
|
watch.command("status").description("Report watcher daemon state").action(async () => {
|
|
633
|
-
await withConfig(pkgVersion, "watch:status", {}, async ({ logger }) => watchStatusCommand({ logger }));
|
|
644
|
+
await withConfig(pkgVersion, "watch:status", {}, async ({ config, logger }) => watchStatusCommand({ config, logger }));
|
|
634
645
|
});
|
|
635
646
|
|
|
636
647
|
program
|
package/src/commands/board.js
CHANGED
|
@@ -2,8 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import net from "node:net";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
6
|
import { getKarajanHome } from "../utils/paths.js";
|
|
7
|
+
import { projectSlug as slugOfDir } from "../plan/plan-store.js";
|
|
7
8
|
import { getPortOccupant } from "../utils/port-occupant.js";
|
|
8
9
|
|
|
9
10
|
const BOARD_DIR = path.resolve(import.meta.dirname, "../../packages/hu-board");
|
|
@@ -76,18 +77,29 @@ async function findAvailablePort(desiredPort, maxTries = 10) {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
* Build the board URL.
|
|
80
|
-
* the
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* real `/p/<slug>` path is not a route and silently falls back to the
|
|
84
|
-
* global dashboard. KJC-BUG-0093.
|
|
80
|
+
* Build the board URL. KJC-TSK-0884 (ADR 0011, board scoped): with a project,
|
|
81
|
+
* the scoped view `/p/<slug>`, one project with the multi-project affordances
|
|
82
|
+
* hidden (app.js scoped-mode, served by the SPA fallback; checked in a browser).
|
|
83
|
+
* It replaces KJC-BUG-0093's `#board/<slug>`, which kept the global dashboard.
|
|
85
84
|
* @param {number} port
|
|
86
85
|
* @param {string|null} [projectSlug]
|
|
87
86
|
* @returns {string}
|
|
88
87
|
*/
|
|
89
88
|
export function buildBoardUrl(port, projectSlug) {
|
|
90
|
-
return projectSlug ? boardUrl(port,
|
|
89
|
+
return projectSlug ? boardUrl(port, `/p/${encodeURIComponent(projectSlug)}`) : boardUrl(port);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* KJC-TSK-0884: the board slug of a project is its PARENT repo's, even from a
|
|
94
|
+
* `kj worktree` lane, or the lane opens an empty view.
|
|
95
|
+
*/
|
|
96
|
+
export function boardSlug(projectDir) {
|
|
97
|
+
let dir = projectDir;
|
|
98
|
+
try {
|
|
99
|
+
const common = execFileSync("git", ["-C", projectDir, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
100
|
+
if (path.basename(common) === ".git") dir = path.dirname(common);
|
|
101
|
+
} catch { /* not a git repo: the dir is the project */ }
|
|
102
|
+
return slugOfDir(dir);
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
/**
|
|
@@ -42,6 +42,8 @@ const CONTRACT_PATHS = [
|
|
|
42
42
|
"CLAUDE.md",
|
|
43
43
|
"AGENTS.md",
|
|
44
44
|
"GEMINI.md",
|
|
45
|
+
// KJC-TSK-0879: in a Rulesync repo kj's rules live in .rulesync/rules/karajan.md.
|
|
46
|
+
".rulesync",
|
|
45
47
|
];
|
|
46
48
|
const CONTRACT_MESSAGE = "chore(bootstrap): el contrato del método, para que quien clone lo herede";
|
|
47
49
|
|
package/src/commands/env.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { installPlaybook, renderPlaybook } from "../environment/playbook.js";
|
|
8
8
|
import { renderBrief, listBriefs } from "../environment/briefs.js";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
-
import {
|
|
10
|
+
import { projectSlug, getLastIndexedCommit } from "../rag/vec-store.js";
|
|
11
11
|
import { ragIndexCommand } from "./rag.js";
|
|
12
12
|
import { renderPendingBlock, PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
|
|
13
13
|
import { onnxConfig, persistOnnxChoice, resetEmptyStore } from "../rag/onnx-fallback.js";
|
|
@@ -19,13 +19,15 @@ import { createWizard } from "../utils/wizard.js";
|
|
|
19
19
|
import { hardenCommand } from "./harden.js";
|
|
20
20
|
import { reviewGateCommand } from "./review-gate.js";
|
|
21
21
|
import { join } from "node:path";
|
|
22
|
+
import { openProjectStore, projectDbPath } from "../rag/project-store.js";
|
|
23
|
+
import { maybeRulesyncGenerate } from "../utils/rulesync.js";
|
|
22
24
|
|
|
23
25
|
function hasRagIndex(config, projectDir) {
|
|
24
26
|
// KJC-BUG-0128: probing must never CREATE the store — openVecStore runs
|
|
25
27
|
// the DDL, and a table born at the default dim (768) breaks the ONNX
|
|
26
28
|
// fallback (384) later. No file → no index, without side effects.
|
|
27
|
-
if (!existsSync(
|
|
28
|
-
const db =
|
|
29
|
+
if (!existsSync(projectDbPath(projectDir))) return false;
|
|
30
|
+
const db = openProjectStore({ projectDir, dim: config?.rag?.embedder?.dim || 768 });
|
|
29
31
|
try { return Boolean(getLastIndexedCommit(db, projectSlug(projectDir))); }
|
|
30
32
|
finally { db.close(); }
|
|
31
33
|
}
|
|
@@ -74,6 +76,8 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
74
76
|
config, // KJC-TSK-0865: the panel travels with the method
|
|
75
77
|
});
|
|
76
78
|
console.log(`✓ Karajan playbook installed in: ${result.files.join(", ")}`);
|
|
79
|
+
// KJC-TSK-0879: in a Rulesync repo, spread the rules only if the project opted in.
|
|
80
|
+
maybeRulesyncGenerate({ projectDir, config, logger: console });
|
|
77
81
|
|
|
78
82
|
// KJC-TSK-0711 — RAG as a native tool: agents use what is in their
|
|
79
83
|
// toolbox, so the official path must be cheaper than the grep shortcut.
|
|
@@ -169,7 +173,7 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
169
173
|
// KJC-BUG-0128: an empty store may carry a vec table at the wrong
|
|
170
174
|
// dim (the old probe created it at 768) — reset it so ONNX (384)
|
|
171
175
|
// can index. A store with data is never touched.
|
|
172
|
-
if (!resetEmptyStore()) {
|
|
176
|
+
if (!resetEmptyStore(projectDir)) {
|
|
173
177
|
console.log(" the vector store already has data at another dimension — not switching automatically");
|
|
174
178
|
return false;
|
|
175
179
|
}
|