karajan-code 4.35.1 → 4.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/hu-board/public/utils/board-view.js +2 -1
- package/packages/hu-board/public/utils/config-editor.js +5 -2
- package/packages/hu-board/public/utils/terminal-panel.js +2 -1
- package/packages/hu-board/src/config-yaml.js +11 -9
- package/packages/hu-board/src/project-dir.js +42 -0
- package/packages/hu-board/src/routes/api.js +11 -3
- package/packages/hu-board/src/terminal-wire.js +5 -1
- package/packages/hu-board/src/terminal.js +8 -3
- package/src/checks/rag-coverage.js +5 -4
- package/src/cli/advanced-commands.js +1 -1
- package/src/cli/register-meta.js +26 -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/pr-size.js +45 -0
- package/src/commands/rag.js +69 -10
- package/src/commands/review-gate.js +47 -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/guidelines-engine.js +24 -7
- package/src/harden/hook-templates.js +7 -0
- package/src/harden/phone-sign.js +28 -11
- package/src/harden/sentinel-hooks.js +82 -7
- package/src/lang/registry.js +2 -1
- package/src/mcp/handlers/rag-handler.js +14 -4
- package/src/orchestrator/drivers/post-loop.js +13 -11
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +3 -2
- package/src/orchestrator/stages/rag-context-stage.js +5 -2
- package/src/rag/auto-update.js +5 -3
- package/src/rag/coverage.js +13 -12
- package/src/rag/indexable.js +128 -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/comment-only.js +93 -0
- package/src/review/kj-provenance.js +26 -0
- package/src/review/tests-with-code.js +5 -2
- package/src/review/verdict-store.js +6 -1
- 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,
|
|
@@ -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".
|
|
@@ -34,7 +34,7 @@ export const ADVANCED_GROUPS = [
|
|
|
34
34
|
{ title: "Pipeline (piezas sueltas)", commands: ["autorun", "code", "review", "solomon", "agent", "scan", "tournament"] },
|
|
35
35
|
{ title: "Análisis pre-run", commands: ["discover", "triage", "researcher", "architect", "onboard", "brief"] },
|
|
36
36
|
{ title: "Búsqueda / RAG", commands: ["rag", "qmd", "watch"] },
|
|
37
|
-
{ title: "Calidad / auditoría", commands: ["audit", "check", "mutate", "webperf", "sonar", "privacy", "release", "policy", "claims", "steward"] },
|
|
37
|
+
{ title: "Calidad / auditoría", commands: ["audit", "check", "mutate", "webperf", "sonar", "privacy", "release", "policy", "claims", "steward", "pr-size"] },
|
|
38
38
|
{ title: "Sesión / board", commands: ["resume", "report", "board", "hu", "adr", "worktree", "undo", "standby", "sentinel", "identity"] },
|
|
39
39
|
{ title: "Infra / setup", commands: ["install-tools", "ollama", "skills", "roles", "agents", "env"] },
|
|
40
40
|
{ title: "Mantenimiento", commands: ["clean", "sync", "telemetry", "report-issue"] },
|
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;
|
|
@@ -521,11 +522,23 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
521
522
|
await privacyScanCommand({ paths, flags });
|
|
522
523
|
});
|
|
523
524
|
|
|
525
|
+
// KJC-TSK-0910: the branch's size while it is written, same budget as CI.
|
|
526
|
+
program.command("pr-size")
|
|
527
|
+
.description("Lines this branch adds against its base, with the CI budget (committed, uncommitted and new files)")
|
|
528
|
+
.option("--json", "Machine-readable result")
|
|
529
|
+
.action(async (flags) => {
|
|
530
|
+
await withConfig(pkgVersion, "pr-size", flags, async ({ config, logger }) => {
|
|
531
|
+
const { prSizeCommand } = await import("../commands/pr-size.js");
|
|
532
|
+
await prSizeCommand({ config, logger, flags });
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
524
536
|
const rag = program.command("rag").description("Retrieval-augmented search over Karajan plans, onboarding briefs and project code");
|
|
525
537
|
rag.command("index")
|
|
526
538
|
.description("Index plans + onboarding (and optionally project sources) into the local vector store")
|
|
527
539
|
.option("--with-sources", "Also index the projectDir's JS/TS files")
|
|
528
540
|
.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.")
|
|
541
|
+
.option("--file <paths...>", "Index exactly these files (a missing one leaves the index): the remedy for a file absent from the index")
|
|
529
542
|
.option("--json", "Output the totals as JSON")
|
|
530
543
|
.action(async (flags) => {
|
|
531
544
|
await withConfig(pkgVersion, "rag-index", flags, async ({ config, logger }) => {
|
|
@@ -534,6 +547,15 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
534
547
|
});
|
|
535
548
|
// KJC-BUG-0216: el gate rag-first no puede pedir lo imposible. Esto responde
|
|
536
549
|
// si el RAG PUEDE hablar de un fichero, y si no, por que.
|
|
550
|
+
// KJC-TSK-0888 (ADR 0011): el paso al indice por proyecto, sin reindexar.
|
|
551
|
+
rag.command("migrate")
|
|
552
|
+
.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")
|
|
553
|
+
.option("--json", "Emit the result as JSON")
|
|
554
|
+
.action(async (flags) => {
|
|
555
|
+
await withConfig(pkgVersion, "rag-migrate", flags, async ({ config }) => {
|
|
556
|
+
await ragMigrateCommand({ config, flags });
|
|
557
|
+
});
|
|
558
|
+
});
|
|
537
559
|
rag.command("covers <file>")
|
|
538
560
|
.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
561
|
.option("--json", "Emit the state as JSON")
|
|
@@ -564,7 +586,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
564
586
|
.option("--rag-mode <mode>", "hybrid (default) | semantic (cosine only) | keyword (BM25 only)", "hybrid")
|
|
565
587
|
.option("--alpha <n>", "Hybrid weight for semantic component (0..1). Default 0.6", "0.6")
|
|
566
588
|
.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
|
|
589
|
+
.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
590
|
.option("--rerank", "Apply cross-encoder rerank to top-K (needs @huggingface/transformers, slower but more precise)")
|
|
569
591
|
.option("--rerank-model <name>", "Cross-encoder model id. Default: Xenova/ms-marco-MiniLM-L-6-v2")
|
|
570
592
|
.option("--json", "Output the hits as JSON")
|
|
@@ -627,10 +649,10 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
627
649
|
await withConfig(pkgVersion, "watch:start", flags, async ({ config, logger }) => watchStartCommand({ config, logger, flags }));
|
|
628
650
|
});
|
|
629
651
|
watch.command("stop").description("Stop the watcher daemon").action(async () => {
|
|
630
|
-
await withConfig(pkgVersion, "watch:stop", {}, async ({ logger }) => watchStopCommand({ logger }));
|
|
652
|
+
await withConfig(pkgVersion, "watch:stop", {}, async ({ config, logger }) => watchStopCommand({ config, logger }));
|
|
631
653
|
});
|
|
632
654
|
watch.command("status").description("Report watcher daemon state").action(async () => {
|
|
633
|
-
await withConfig(pkgVersion, "watch:status", {}, async ({ logger }) => watchStatusCommand({ logger }));
|
|
655
|
+
await withConfig(pkgVersion, "watch:status", {}, async ({ config, logger }) => watchStatusCommand({ config, logger }));
|
|
634
656
|
});
|
|
635
657
|
|
|
636
658
|
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
|
}
|
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) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0910 — `kj pr-size`: the branch's size while it is being written,
|
|
3
|
+
* with the SAME budget the CI gate applies (loc-budget.js). It counts what is
|
|
4
|
+
* committed, what is not yet, and new files not yet added, against the merge
|
|
5
|
+
* base with the base branch, so the Sentinel can warn before the gate blocks.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { budgetedAdded } from "../review/loc-budget.js";
|
|
12
|
+
|
|
13
|
+
const git = (projectDir, args) =>
|
|
14
|
+
execFileSync("git", ["-C", projectDir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
15
|
+
|
|
16
|
+
function mergeBase(projectDir, base) {
|
|
17
|
+
for (const ref of [`origin/${base}`, base]) {
|
|
18
|
+
try { return git(projectDir, ["merge-base", "HEAD", ref]).trim(); } catch { /* try the next ref */ }
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`pr-size: no merge base with ${base} (nor origin/${base})`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @returns {Promise<{added: number, exempt: number, testAdded: number, base: string}>} */
|
|
24
|
+
export async function branchSize({ projectDir = process.cwd(), base = "main" } = {}) {
|
|
25
|
+
const from = mergeBase(projectDir, base);
|
|
26
|
+
// Working tree against the merge base: committed and uncommitted together.
|
|
27
|
+
let numstat = git(projectDir, ["diff", "--numstat", from]);
|
|
28
|
+
// New files not yet added are where a branch grows while it is written.
|
|
29
|
+
for (const file of git(projectDir, ["ls-files", "--others", "--exclude-standard"]).split("\n").filter(Boolean)) {
|
|
30
|
+
let text;
|
|
31
|
+
try { text = readFileSync(join(projectDir, file), "utf8"); } catch { continue; }
|
|
32
|
+
if (text.includes("\0")) continue; // binary
|
|
33
|
+
// Lines as git counts them: an empty file adds none.
|
|
34
|
+
const n = text === "" ? 0 : text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
35
|
+
numstat += `${n}\t0\t${file}\n`;
|
|
36
|
+
}
|
|
37
|
+
return { ...budgetedAdded(numstat), base };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function prSizeCommand({ config, logger = console, flags = {} }) {
|
|
41
|
+
const s = await branchSize({ projectDir: config?.projectDir || process.cwd(), base: config?.base_branch || "main" });
|
|
42
|
+
if (flags.json) process.stdout.write(`${JSON.stringify(s)}\n`);
|
|
43
|
+
else logger.info(`pr-size: ${s.added} line(s) added against ${s.base} (${s.testAdded} in tests, ${s.exempt} exempt)`);
|
|
44
|
+
return s;
|
|
45
|
+
}
|