karajan-code 2.31.0 → 2.32.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/README.md +9 -0
- package/package.json +6 -1
- package/packages/hu-board/public/app.js +4 -1
- package/packages/hu-board/src/config-yaml.js +40 -5
- package/packages/hu-board/src/routes/rag.js +8 -2
- package/scripts/ai-attribution-guard.yml +9 -2
- package/src/cli/register-meta.js +12 -2
- package/src/orchestrator/stages/rag-context-stage.js +20 -2
- package/src/spec-review/refine-loop.js +1 -1
package/README.md
CHANGED
|
@@ -345,6 +345,15 @@ Opt out: set `telemetry: false` in `~/.karajan/kj.config.yml`
|
|
|
345
345
|
|
|
346
346
|
## Recent releases
|
|
347
347
|
|
|
348
|
+
> **v2.32.0 released** — Minor. **AI Harness Scorecard hardening (KJC-PCS-0051)** — Plan A closes five FAILs from the external scorecard audit in one sprint: Prettier `--check`, Coverage v8 reports, Conventional Commits enforcement, nightly drift detection, and an unsafe-code lint policy. Plus two bug fixes shipped alongside.
|
|
349
|
+
>
|
|
350
|
+
> - **Prettier `--check` CI job** (PR #868, KJC-TSK-0464): new `format` job blocks PRs whose formatting drifts from `.prettierrc.json`. Scope narrow at first (`.github/workflows/`, root config); future PRs fold in more dirs under the shrink-budget cap.
|
|
351
|
+
> - **Coverage v8 + CI artifact** (PR #870, KJC-TSK-0465): `vitest.config.js` emits `text + html + lcov` via `@vitest/coverage-v8`. New `coverage` job uploads `coverage/` (14-day retention). Per-glob thresholds enforced when the user opts in; `src/mcp/handlers/**` floor ratcheted to `70/60` to lock the current state — follow-up tracked to climb back to 80/80.
|
|
352
|
+
> - **Conventional Commits on PR head** (PR #872, KJC-TSK-0466): `wagoid/commitlint-github-action@v6` checks every PR commit against `.commitlintrc.json`. CI-side enforcement on top of the pre-commit local hook — bypassing husky no longer escapes the gate.
|
|
353
|
+
> - **Nightly drift workflow** (PR #873, KJC-TSK-0467): new `.github/workflows/nightly.yml` runs the full CI suite every night at 04:17 UTC against `main`. Failures auto-file/update a tracking issue tagged `drift` via `actions/github-script@v8`, so a flaky dep or upstream regression surfaces within 24 h instead of on the next unrelated PR.
|
|
354
|
+
> - **`eslint-plugin-security` policy** (PR #874, KJC-TSK-0468): `eslint.config.js` now blocks `eval`, `new Function`, dynamic `require`, `pseudoRandomBytes` and `mustache`-escape disabling as hard errors; flags `detect-non-literal-regexp` as warn. Noisy members of the recommended preset are intentionally NOT enabled.
|
|
355
|
+
> - **42 tests on `main` repaired** (PR #869, KJC-BUG-0065) and **`await openEditor` race in spec-reviewer refine-loop fixed** (PR #871, KJC-BUG-0066). The hardening sprint sits on a clean `main` again.
|
|
356
|
+
>
|
|
348
357
|
> **v2.31.0 released** — Minor. **Team-shared HU Board** — the full KJC-PRP-0002: opt-in HUs into a `.karajan-shared/` cohort, the board surfaces them with a `shared` badge, and per-HU `assignee` lets multiple machines work the same plan without trampling each other.
|
|
349
358
|
>
|
|
350
359
|
> - **`kj plan share` / `kj plan unshare`** (PRs #860, #862): mueve plans entre `~/.karajan/plans/` (local) y `<projectDir>/.karajan-shared/plans/` (compartido). Idempotente. El loader probará ambos automáticamente.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.0",
|
|
4
4
|
"description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0",
|
|
@@ -55,9 +55,12 @@
|
|
|
55
55
|
"setup": "node scripts/install.js",
|
|
56
56
|
"test": "vitest run",
|
|
57
57
|
"test:watch": "vitest",
|
|
58
|
+
"test:coverage": "vitest run --coverage",
|
|
58
59
|
"lint": "eslint src/",
|
|
59
60
|
"lint:fix": "eslint src/ --fix",
|
|
60
61
|
"lint:syntax": "((command -v rg >/dev/null 2>&1 && rg --files src tests | rg '\\.js$') || find src tests -type f -name '*.js') | while IFS= read -r f; do node --check \"$f\"; done",
|
|
62
|
+
"format:check": "prettier --check .",
|
|
63
|
+
"format:fix": "prettier --write .",
|
|
61
64
|
"typecheck": "npx -p typescript@5 tsc --noEmit -p tsconfig.json",
|
|
62
65
|
"validate": "npm run lint && npm run lint:syntax && npm test",
|
|
63
66
|
"mcp": "node src/mcp/server.js"
|
|
@@ -90,9 +93,11 @@
|
|
|
90
93
|
"esbuild": "^0.28.0",
|
|
91
94
|
"eslint": "^10.2.1",
|
|
92
95
|
"eslint-plugin-import-x": "^4.16.2",
|
|
96
|
+
"eslint-plugin-security": "^4.0.0",
|
|
93
97
|
"globals": "^17.5.0",
|
|
94
98
|
"lint-staged": "^16.4.0",
|
|
95
99
|
"postject": "^1.0.0-alpha.6",
|
|
100
|
+
"prettier": "^3.4.2",
|
|
96
101
|
"simple-git-hooks": "^2.13.1",
|
|
97
102
|
"vitest": "^4.0.0"
|
|
98
103
|
}
|
|
@@ -3352,7 +3352,10 @@ async function showConfigEditor(scope = 'global') {
|
|
|
3352
3352
|
<span style="font-size:0.85rem;color:var(--text-muted);">${f.value ? 'activado' : 'desactivado'}</span>
|
|
3353
3353
|
</label>`;
|
|
3354
3354
|
} else if (f.type === 'number') {
|
|
3355
|
-
|
|
3355
|
+
// KJC-BUG-0069 — honour the schema's `step` (e.g. 'any' for floats like
|
|
3356
|
+
// rag.search.alpha=0.6). HTML5 number inputs default to step=1, which
|
|
3357
|
+
// marks any fractional value as stepMismatch and refuses to commit.
|
|
3358
|
+
input = `<input type="number" id="${id}" data-key="${esc(f.key)}" value="${esc(String(f.value))}"${f.min != null ? ` min="${f.min}"` : ''}${f.max != null ? ` max="${f.max}"` : ''}${f.step != null ? ` step="${esc(String(f.step))}"` : ''} style="width:120px;padding:6px 8px;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);">`;
|
|
3356
3359
|
}
|
|
3357
3360
|
return `
|
|
3358
3361
|
<div style="margin-bottom:14px;">
|
|
@@ -235,11 +235,10 @@ export const EDITABLE_FIELDS = [
|
|
|
235
235
|
help: 'Orquestador IA central: enriquece feedback, comprime outputs y verifica cambios reales. Opt-in.',
|
|
236
236
|
default: false,
|
|
237
237
|
},
|
|
238
|
-
// v2.30.0 — RAG controls.
|
|
239
|
-
//
|
|
240
|
-
// rag.embedder.provider
|
|
241
|
-
//
|
|
242
|
-
// aquí (ver task #99).
|
|
238
|
+
// v2.30.0 — RAG controls. Cubren los tres ejes que afectan al
|
|
239
|
+
// retrieval en tiempo de pipeline: pre-carga (rag.preload.*),
|
|
240
|
+
// proveedor de embeddings (rag.embedder.provider) y parámetros de
|
|
241
|
+
// búsqueda (rag.search.*, cableados al stage en KJC-TSK-0463).
|
|
243
242
|
{
|
|
244
243
|
key: 'ragPreloadEnabled',
|
|
245
244
|
path: 'rag.preload.enabled',
|
|
@@ -280,6 +279,42 @@ export const EDITABLE_FIELDS = [
|
|
|
280
279
|
options: ['ollama', 'openai', 'voyage', 'cohere', 'mistral', 'onnx'],
|
|
281
280
|
default: 'ollama',
|
|
282
281
|
},
|
|
282
|
+
// KJC-TSK-0463 — search-time knobs ahora cableados al stage.
|
|
283
|
+
// mode/alpha controlan el fuse semantic vs keyword; rerank activa el
|
|
284
|
+
// cross-encoder opcional (más calidad, más latencia).
|
|
285
|
+
{
|
|
286
|
+
key: 'ragSearchMode',
|
|
287
|
+
path: 'rag.search.mode',
|
|
288
|
+
category: 'rag',
|
|
289
|
+
type: 'select',
|
|
290
|
+
label: 'Modo de búsqueda RAG',
|
|
291
|
+
help: '"hybrid" mezcla semántico + BM25 (recomendado). "semantic" sólo embeddings. "keyword" sólo BM25 (útil si los embeddings están desafinados).',
|
|
292
|
+
options: ['hybrid', 'semantic', 'keyword'],
|
|
293
|
+
default: 'hybrid',
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
key: 'ragSearchAlpha',
|
|
297
|
+
path: 'rag.search.alpha',
|
|
298
|
+
category: 'rag',
|
|
299
|
+
type: 'number',
|
|
300
|
+
label: 'Alpha del modo hybrid (0-1)',
|
|
301
|
+
help: 'Peso del scoring semántico vs keyword en modo hybrid. 1.0 = sólo semántico, 0.0 = sólo BM25, 0.6 (default) = inclinado a semántico.',
|
|
302
|
+
min: 0,
|
|
303
|
+
max: 1,
|
|
304
|
+
// KJC-BUG-0069 — number input defaults to step=1 (HTML5), so any
|
|
305
|
+
// non-integer default fails validity. Allow any fractional value here.
|
|
306
|
+
step: 'any',
|
|
307
|
+
default: 0.6,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
key: 'ragSearchRerank',
|
|
311
|
+
path: 'rag.search.rerank',
|
|
312
|
+
category: 'rag',
|
|
313
|
+
type: 'boolean',
|
|
314
|
+
label: 'Activar rerank cross-encoder',
|
|
315
|
+
help: 'Re-puntúa los topK supervivientes con un cross-encoder. Mejora calidad pero añade latencia y requiere modelo extra. Opt-in.',
|
|
316
|
+
default: false,
|
|
317
|
+
},
|
|
283
318
|
// KJC-PRP-0002 PR5: when the board scans plans, it visits both
|
|
284
319
|
// ~/.karajan/plans/<slug>/ (per-user cache) and .karajan-shared/plans/
|
|
285
320
|
// (team copy). If a planId lives in both, the second pass overwrites
|
|
@@ -24,12 +24,18 @@ router.get("/stats", (_req, res) => {
|
|
|
24
24
|
if (!existsSync(path)) return res.json({ ok: true, initialized: false, message: "rag.db not initialized — run `kj rag index <dir>` to start indexing.", embedder: embedder() });
|
|
25
25
|
try {
|
|
26
26
|
const db = new Database(path, { readonly: true, fileMustExist: true });
|
|
27
|
+
// KJC-BUG-0068 — readonly handler cannot run the project_slug migration
|
|
28
|
+
// (KJC-TSK-0438). Legacy DBs created pre-v2.27 lack the column; detect
|
|
29
|
+
// and fall back so the dashboard renders instead of crashing with 500.
|
|
30
|
+
const hasProjectSlug = db.prepare("PRAGMA table_info(chunks)").all().some((c) => c.name === "project_slug");
|
|
27
31
|
const total_chunks = db.prepare("SELECT COUNT(*) AS n FROM chunks").get().n;
|
|
28
32
|
const by_kind = db.prepare("SELECT kind, COUNT(*) AS n FROM chunks GROUP BY kind").all();
|
|
29
|
-
const by_project =
|
|
33
|
+
const by_project = hasProjectSlug
|
|
34
|
+
? db.prepare("SELECT COALESCE(project_slug, '(no slug)') AS project, COUNT(*) AS n FROM chunks GROUP BY project_slug ORDER BY n DESC LIMIT 20").all()
|
|
35
|
+
: [];
|
|
30
36
|
const last_indexed = db.prepare("SELECT MAX(created_at) AS ts FROM chunks").get().ts;
|
|
31
37
|
db.close();
|
|
32
|
-
res.json({ ok: true, initialized: true, db_path: path, db_size_bytes: statSync(path).size, total_chunks, by_kind, by_project, last_indexed, embedder: embedder() });
|
|
38
|
+
res.json({ ok: true, initialized: true, db_path: path, db_size_bytes: statSync(path).size, total_chunks, by_kind, by_project, last_indexed, embedder: embedder(), schema_legacy: !hasProjectSlug });
|
|
33
39
|
} catch (err) { res.status(500).json({ ok: false, error: String(err.message || err) }); }
|
|
34
40
|
});
|
|
35
41
|
|
|
@@ -20,8 +20,12 @@ jobs:
|
|
|
20
20
|
fetch-depth: 0
|
|
21
21
|
|
|
22
22
|
- name: Check PR diff for AI attribution
|
|
23
|
+
# KJC-BUG-0072 — never interpolate ${{ github.* }} directly inside a
|
|
24
|
+
# `run:` block on pull_request workflows. Pin to env vars instead.
|
|
25
|
+
env:
|
|
26
|
+
BASE_REF: ${{ github.base_ref }}
|
|
23
27
|
run: |
|
|
24
|
-
DIFF=$(git diff origin/${
|
|
28
|
+
DIFF=$(git diff "origin/${BASE_REF}...HEAD" \
|
|
25
29
|
-- ':!*.png' ':!*.jpg' ':!*.svg' ':!*.ico' \
|
|
26
30
|
':!*.lock' ':!package-lock.json' ':!yarn.lock' \
|
|
27
31
|
':!*.min.js' ':!*.min.css' \
|
|
@@ -54,8 +58,11 @@ jobs:
|
|
|
54
58
|
echo "AI attribution guard: clean"
|
|
55
59
|
|
|
56
60
|
- name: Check commit messages
|
|
61
|
+
# KJC-BUG-0072 — same hardening as the diff scan: env-pin the ref.
|
|
62
|
+
env:
|
|
63
|
+
BASE_REF: ${{ github.base_ref }}
|
|
57
64
|
run: |
|
|
58
|
-
COMMITS=$(git log --format='%s%n%b' origin/${
|
|
65
|
+
COMMITS=$(git log --format='%s%n%b' "origin/${BASE_REF}...HEAD")
|
|
59
66
|
|
|
60
67
|
COAUTH='Co-Authored-By:.*\b(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Cursor|Windsurf|Codeium)\b'
|
|
61
68
|
ATTR='(Generated|Created|Written|Built|Authored) (by|with|using) .*(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Cursor|Windsurf|Codeium)'
|
package/src/cli/register-meta.js
CHANGED
|
@@ -115,15 +115,25 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
115
115
|
.option("--scope <scope>", "plans | code | onboarding | all (default: all)", "all")
|
|
116
116
|
.option("--top-k <n>", "Number of hits to return (default: 5)", "5")
|
|
117
117
|
.option("--project <slug>", "Filter by project slug. Pass 'all' to query across every indexed project. Default: cwd basename")
|
|
118
|
-
|
|
118
|
+
// KJC-BUG-0070 — the global `--mode` flag maps to `review_mode` in
|
|
119
|
+
// src/config/overrides.js. We expose the rag-specific knob under
|
|
120
|
+
// `--rag-mode` to avoid the collision (which crashed the command on
|
|
121
|
+
// first call). Internally we rebind to `flags.mode` so the handler
|
|
122
|
+
// in src/commands/rag.js stays untouched.
|
|
123
|
+
.option("--rag-mode <mode>", "hybrid (default) | semantic (cosine only) | keyword (BM25 only)", "hybrid")
|
|
119
124
|
.option("--alpha <n>", "Hybrid weight for semantic component (0..1). Default 0.6", "0.6")
|
|
120
125
|
.option("--where <clause>", "Metadata filter, e.g. 'symbol=loadConfig' or 'hu_id=HU-003 AND kind=plan'")
|
|
121
126
|
.option("--rerank", "Apply cross-encoder rerank to top-K (needs @huggingface/transformers, slower but more precise)")
|
|
122
127
|
.option("--rerank-model <name>", "Cross-encoder model id. Default: Xenova/ms-marco-MiniLM-L-6-v2")
|
|
123
128
|
.option("--json", "Output the hits as JSON")
|
|
124
129
|
.action(async (text, flags) => {
|
|
130
|
+
// Stash the rag-specific mode and remove it from the flags object so
|
|
131
|
+
// applyScalarAndBooleanOverrides cannot see a `flags.mode` and write
|
|
132
|
+
// out.review_mode = "hybrid" — that would fail the schema validator.
|
|
133
|
+
const ragMode = flags.ragMode;
|
|
134
|
+
delete flags.ragMode;
|
|
125
135
|
await withConfig(pkgVersion, "rag-query", flags, async ({ config, logger }) => {
|
|
126
|
-
await ragQueryCommand({ text, config, logger, flags });
|
|
136
|
+
await ragQueryCommand({ text, config, logger, flags: { ...flags, mode: ragMode } });
|
|
127
137
|
});
|
|
128
138
|
});
|
|
129
139
|
|
|
@@ -11,8 +11,25 @@ import { query } from "../../rag/retriever.js";
|
|
|
11
11
|
|
|
12
12
|
const DEFAULT_TOP_K = 5;
|
|
13
13
|
const DEFAULT_SCOPE = "all";
|
|
14
|
+
const DEFAULT_SEARCH_MODE = "hybrid";
|
|
15
|
+
const DEFAULT_SEARCH_ALPHA = 0.6;
|
|
14
16
|
const MAX_CHUNK_CHARS = 600;
|
|
15
17
|
|
|
18
|
+
// KJC-TSK-0463 — pull search-time knobs from config so the HU Board
|
|
19
|
+
// modal (rag.search.{mode,alpha,rerank}) actually steers retrieval at
|
|
20
|
+
// pipeline time. Returns the args object query() expects; unset fields
|
|
21
|
+
// fall back to the retriever's own defaults.
|
|
22
|
+
export function resolveSearchOpts(config) {
|
|
23
|
+
const s = config?.rag?.search ?? {};
|
|
24
|
+
const out = {
|
|
25
|
+
mode: typeof s.mode === "string" ? s.mode : DEFAULT_SEARCH_MODE,
|
|
26
|
+
alpha: Number.isFinite(s.alpha) ? s.alpha : DEFAULT_SEARCH_ALPHA,
|
|
27
|
+
};
|
|
28
|
+
if (s.rerank && typeof s.rerank === "object") out.rerankOpts = s.rerank;
|
|
29
|
+
else if (s.rerank === true) out.rerankOpts = {};
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
/**
|
|
17
34
|
* Runs the RAG preload stage. Returns:
|
|
18
35
|
* { skipped: true, reason } — when disabled / empty / error
|
|
@@ -36,7 +53,8 @@ export async function runRagContextStage({ config, logger, emitter, eventBase, t
|
|
|
36
53
|
const topK = preload.topK || DEFAULT_TOP_K;
|
|
37
54
|
const scope = preload.scope || DEFAULT_SCOPE;
|
|
38
55
|
const embedder = makeEmbedder(config);
|
|
39
|
-
const
|
|
56
|
+
const searchOpts = resolveSearchOpts(config);
|
|
57
|
+
const hits = await query(db, embedder, task, { topK, scope, ...searchOpts });
|
|
40
58
|
if (hits.length === 0) return { skipped: true, reason: "no-hits" };
|
|
41
59
|
const lines = hits.map((h) => {
|
|
42
60
|
const label = h.metadata?.hu_id || h.metadata?.symbol || h.metadata?.headingPath?.join(" > ") || "block";
|
|
@@ -46,7 +64,7 @@ export async function runRagContextStage({ config, logger, emitter, eventBase, t
|
|
|
46
64
|
const contextBlock = `## Prior context from RAG (auto-retrieved, top ${hits.length})\n\n${lines.join("\n\n")}`;
|
|
47
65
|
emitProgress(emitter, makeEvent("rag-preload:hits", { ...eventBase, stage: "rag-preload" }, {
|
|
48
66
|
message: `RAG preload: injected ${hits.length} chunk(s) into the task context`,
|
|
49
|
-
detail: { topK, scope, hits: hits.length },
|
|
67
|
+
detail: { topK, scope, hits: hits.length, mode: searchOpts.mode, alpha: searchOpts.alpha, rerank: Boolean(searchOpts.rerankOpts) },
|
|
50
68
|
}));
|
|
51
69
|
return { skipped: false, contextBlock, hits };
|
|
52
70
|
} finally {
|
|
@@ -50,7 +50,7 @@ export async function runRefinePass({ spec, findings, role, projectDir, taskFile
|
|
|
50
50
|
} catch (err) { return { ok: false, error: `persist failed: ${err.message}`, hashChanged: false }; }
|
|
51
51
|
|
|
52
52
|
const before = sha256(refineRes.refinedSpec);
|
|
53
|
-
openEditor(v2Path, { logger, openEditorFn });
|
|
53
|
+
await openEditor(v2Path, { logger, openEditorFn });
|
|
54
54
|
let after;
|
|
55
55
|
try { after = await _fs.readFile(v2Path, "utf8"); }
|
|
56
56
|
catch (err) { return { ok: false, error: `read v2 failed: ${err.message}`, hashChanged: false }; }
|