karajan-code 4.3.0 → 4.4.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 +4 -2
- package/src/cli/register-meta.js +5 -1
- package/src/commands/env.js +25 -2
- package/src/commands/hu.js +24 -3
- package/src/commands/rag.js +4 -4
- package/src/config/defaults.js +5 -1
- package/src/mcp/handlers/rag-handler.js +3 -3
- package/src/orchestrator/stages/rag-context-stage.js +2 -2
- package/src/rag/auto-update.js +2 -2
- package/src/rag/governed-embedder.js +39 -0
- package/src/rag/onnx-fallback.js +42 -0
- package/src/rag/watcher.js +2 -2
- package/templates/coder-rules.md +8 -0
- package/templates/kj.config.yml +8 -0
- package/templates/review-rules.md +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.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",
|
|
@@ -109,11 +109,13 @@
|
|
|
109
109
|
"helmet": "^8.1.0",
|
|
110
110
|
"js-yaml": "^4.2.0",
|
|
111
111
|
"karajan-core": "^1.0.0",
|
|
112
|
+
"karajan-rag": "^1.2.0",
|
|
112
113
|
"knip": "^6.15.0",
|
|
113
114
|
"madge": "^8.0.0",
|
|
114
115
|
"sqlite-vec": "^0.1.9",
|
|
115
116
|
"valibot": "^1.4.1",
|
|
116
|
-
"web-tree-sitter": "^0.26.9"
|
|
117
|
+
"web-tree-sitter": "^0.26.9",
|
|
118
|
+
"yaml": "^2.9.0"
|
|
117
119
|
},
|
|
118
120
|
"devDependencies": {
|
|
119
121
|
"@eslint/js": "^10.0.1",
|
package/src/cli/register-meta.js
CHANGED
|
@@ -185,7 +185,11 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
185
185
|
const hu = program.command("hu").description("Track work in the HU Board from any host agent (card first)");
|
|
186
186
|
hu.command("add <title>")
|
|
187
187
|
.option("--id <shortId>", "Human-readable short id")
|
|
188
|
-
.option("--
|
|
188
|
+
.option("--ac <criterion>", "Acceptance criterion (repeatable; multiline strings split per line)", (v, acc) => [...acc, v], [])
|
|
189
|
+
.option("--tests <test>", "Acceptance test (repeatable; multiline strings split per line)", (v, acc) => [...acc, v], [])
|
|
190
|
+
.option("--scope <text>", "Scope of the story (files/areas)")
|
|
191
|
+
.option("--task-type <type>", "sw | infra | doc | add-tests | refactor")
|
|
192
|
+
.option("--criteria <text>", "Legacy alias for a single acceptance criterion")
|
|
189
193
|
.option("--json", "Machine-readable output")
|
|
190
194
|
.action(async (title, flags) => {
|
|
191
195
|
await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
|
package/src/commands/env.js
CHANGED
|
@@ -9,6 +9,7 @@ import { renderBrief, listBriefs } from "../environment/briefs.js";
|
|
|
9
9
|
import { openVecStore, projectSlug, getLastIndexedCommit } from "../rag/vec-store.js";
|
|
10
10
|
import { ragIndexCommand } from "./rag.js";
|
|
11
11
|
import { renderPendingBlock, PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
|
|
12
|
+
import { onnxConfig, persistOnnxChoice } from "../rag/onnx-fallback.js";
|
|
12
13
|
|
|
13
14
|
function hasRagIndex(config, projectDir) {
|
|
14
15
|
const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
@@ -59,6 +60,27 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
59
60
|
: { tool: `${provider} embedder`, action: "needs-user", reason: `the RAG cannot index: ${why} — check rag.embedder in kj config`, manualUrl: "kj config" };
|
|
60
61
|
console.log(renderPendingBlock([item], { retry: "kj rag index --with-sources (then re-run: kj env install)" }));
|
|
61
62
|
};
|
|
63
|
+
// KJC-TSK-0683: on limited machines (no Ollama, no Docker) the DEFAULT
|
|
64
|
+
// provider falls back to the built-in ONNX embedder instead of blocking.
|
|
65
|
+
// Only for the first index and only when the user didn't pick a provider
|
|
66
|
+
// explicitly — the election is persisted (dims differ, it must stick).
|
|
67
|
+
const explicitProvider = Boolean(config?.rag?.embedder?.provider);
|
|
68
|
+
const tryOnnxFallback = async (why) => {
|
|
69
|
+
if (explicitProvider) return false;
|
|
70
|
+
console.log(`⚠ default embedder unavailable (${why}) — trying the built-in ONNX embedder (no install needed)…`);
|
|
71
|
+
try {
|
|
72
|
+
const totals = await ragIndexCommand({ config: onnxConfig(config), logger, flags: { withSources: true } });
|
|
73
|
+
if ((totals?.indexed ?? 0) > 0) {
|
|
74
|
+
const p = await persistOnnxChoice(projectDir);
|
|
75
|
+
console.log(`✓ RAG indexed with the built-in ONNX embedder — persisted in ${p}`);
|
|
76
|
+
console.log(" For higher-quality embeddings later: install Ollama and run `kj rag index --rebuild`.");
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
} catch (fallbackErr) {
|
|
80
|
+
console.log(` ONNX fallback also failed: ${fallbackErr.message}`);
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
};
|
|
62
84
|
try {
|
|
63
85
|
if (hasRagIndex(config, projectDir)) {
|
|
64
86
|
console.log("✓ RAG index present");
|
|
@@ -66,11 +88,12 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
66
88
|
console.log("⏳ no RAG index for this project — building it (first time only)…");
|
|
67
89
|
const totals = await ragIndexCommand({ config, logger, flags: { withSources: true } });
|
|
68
90
|
if ((totals?.indexed ?? 0) === 0 && (totals?.files ?? 0) > 0) {
|
|
69
|
-
|
|
91
|
+
const why = `0 of ${totals.files} files indexed — is the embedder running?`;
|
|
92
|
+
if (!(await tryOnnxFallback(why))) blockRag(why);
|
|
70
93
|
}
|
|
71
94
|
}
|
|
72
95
|
} catch (err) {
|
|
73
|
-
blockRag(err.message);
|
|
96
|
+
if (!(await tryOnnxFallback(err.message))) blockRag(err.message);
|
|
74
97
|
}
|
|
75
98
|
}
|
|
76
99
|
if (result.exitCode !== PENDING_EXIT_CODE) {
|
package/src/commands/hu.js
CHANGED
|
@@ -122,15 +122,36 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
|
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
// KJC-TSK-0678 (issue #1296): complete HUs in ONE call. --ac / --tests
|
|
126
|
+
// are repeatable AND accept multiline strings; --criteria stays as the
|
|
127
|
+
// legacy single-criterion alias.
|
|
128
|
+
const TASK_TYPES = ["sw", "infra", "doc", "add-tests", "refactor"];
|
|
129
|
+
if (flags.taskType && !TASK_TYPES.includes(flags.taskType)) {
|
|
130
|
+
throw new Error(`invalid --task-type "${flags.taskType}" — valid: ${TASK_TYPES.join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
const splitLines = (v) => (Array.isArray(v) ? v : v ? [v] : [])
|
|
133
|
+
.flatMap((s) => String(s).split("\n")).map((s) => s.trim()).filter(Boolean);
|
|
125
134
|
const hu = addHu(plan, {
|
|
126
135
|
title,
|
|
127
136
|
short_id: flags.id || null,
|
|
128
|
-
acceptance_criteria: flags.
|
|
137
|
+
acceptance_criteria: [...splitLines(flags.ac), ...splitLines(flags.criteria)],
|
|
138
|
+
acceptance_tests: splitLines(flags.tests),
|
|
139
|
+
scope: flags.scope || null,
|
|
140
|
+
task_type: flags.taskType || undefined,
|
|
129
141
|
});
|
|
130
142
|
hu.created_by = creatorLabel();
|
|
131
143
|
await savePlan(projectDir, plan);
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
const missing = [];
|
|
145
|
+
if (hu.acceptance_criteria.length === 0) missing.push("acceptance criteria (--ac \"...\")");
|
|
146
|
+
if (hu.acceptance_tests.length === 0) missing.push("tests (--tests \"...\")");
|
|
147
|
+
if (missing.length > 0) {
|
|
148
|
+
console.warn(`⚠ incomplete spec — kj run's spec review will flag it. Missing: ${missing.join(", ")}`);
|
|
149
|
+
}
|
|
150
|
+
return emit({
|
|
151
|
+
id: hu.id, short_id: hu.short_id, status: hu.status, created_by: hu.created_by,
|
|
152
|
+
acceptance_criteria: hu.acceptance_criteria, acceptance_tests: hu.acceptance_tests,
|
|
153
|
+
scope: hu.scope, task_type: hu.task_type,
|
|
154
|
+
}, `✓ HU created: ${hu.short_id || hu.id} (pending, by ${hu.created_by}) — it shows up in \`kj board\``);
|
|
134
155
|
}
|
|
135
156
|
|
|
136
157
|
if (action === "move") {
|
package/src/commands/rag.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// kj rag query <text> [--scope plans|code|onboarding|all] [--top-k N] [--json]
|
|
4
4
|
// Closes the v2.22.0 RAG MVP end-to-end from the terminal.
|
|
5
5
|
import { openVecStore, countChunks, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "../rag/vec-store.js";
|
|
6
|
-
import {
|
|
6
|
+
import { makeGovernedEmbedder } from "../rag/governed-embedder.js";
|
|
7
7
|
import { indexProject, indexProjectDelta } from "../rag/indexer.js";
|
|
8
8
|
import { query } from "../rag/retriever.js";
|
|
9
9
|
import { installPostMergeHook, maybeAutoUpdate } from "../rag/auto-update.js";
|
|
@@ -19,7 +19,7 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
19
19
|
const db = openDb(config);
|
|
20
20
|
try {
|
|
21
21
|
const slug = projectSlug(projectDir);
|
|
22
|
-
const embedder =
|
|
22
|
+
const embedder = makeGovernedEmbedder(config);
|
|
23
23
|
const sinceFlag = flags.since;
|
|
24
24
|
// KJC-TSK-0455 — `--since auto` resolves to the last commit we indexed;
|
|
25
25
|
// an explicit ref is honoured as-is. Without a baseline (first-time
|
|
@@ -103,7 +103,7 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
103
103
|
const alpha = Math.max(0, Math.min(1, Number(flags.alpha) || 0.6));
|
|
104
104
|
const where = flags.where || null;
|
|
105
105
|
const rerankOpts = flags.rerank ? { model: flags.rerankModel } : null;
|
|
106
|
-
const hits = await query(db,
|
|
106
|
+
const hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project, mode, alpha, where, rerankOpts });
|
|
107
107
|
if (flags.json) {
|
|
108
108
|
process.stdout.write(`${JSON.stringify(hits)}\n`);
|
|
109
109
|
} else {
|
|
@@ -133,7 +133,7 @@ export async function ragEvalCommand({ config, logger, flags = {} }) {
|
|
|
133
133
|
const topK = Math.max(1, Number(flags.topK) || 10);
|
|
134
134
|
const detected = projectSlug(config?.projectDir || process.cwd());
|
|
135
135
|
const project = flags.project === "all" ? null : (flags.project || detected || null);
|
|
136
|
-
const embedder =
|
|
136
|
+
const embedder = makeGovernedEmbedder(config);
|
|
137
137
|
const runQuery = async (text, k) => query(db, embedder, text, { topK: k, scope: "all", project });
|
|
138
138
|
const report = await runEval(queries, runQuery, { topK, ks: [5, 10] });
|
|
139
139
|
if (flags.json) {
|
package/src/config/defaults.js
CHANGED
|
@@ -216,7 +216,11 @@ const DEFAULTS = {
|
|
|
216
216
|
// delta re-index before every `kj run`; `onCommit` is consulted by
|
|
217
217
|
// `kj rag install-hooks` to decide whether to install the post-merge
|
|
218
218
|
// hook. Both default ON; flip to false to opt out without uninstalling.
|
|
219
|
-
autoUpdate: { onCommit: true, onRun: true }
|
|
219
|
+
autoUpdate: { onCommit: true, onRun: true },
|
|
220
|
+
// KJC-TSK-0682 — sensitivity of the indexed code. Cloud embedders
|
|
221
|
+
// (openai/voyage/cohere/mistral) require an explicit "public"; the
|
|
222
|
+
// safe default blocks them (local ollama/onnx always allowed).
|
|
223
|
+
sensitivity: "internal"
|
|
220
224
|
},
|
|
221
225
|
// Opt-in. Set by the `kj init` wizard (explicit consent prompt in the OS
|
|
222
226
|
// locale). When the key is absent or false, no events are sent. See
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// from src/commands/* are forbidden by the layer-boundaries test
|
|
3
3
|
// (MCP and CLI are peer layers); we hit the pure rag/* modules instead.
|
|
4
4
|
import { countChunks, openVecStore } from "../../rag/vec-store.js";
|
|
5
|
-
import {
|
|
5
|
+
import { makeGovernedEmbedder } from "../../rag/governed-embedder.js";
|
|
6
6
|
import { indexProject } from "../../rag/indexer.js";
|
|
7
7
|
import { query } from "../../rag/retriever.js";
|
|
8
8
|
import { getKarajanHome } from "../../utils/paths.js";
|
|
@@ -25,7 +25,7 @@ export async function handleRagQuery(args, server) {
|
|
|
25
25
|
const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
26
26
|
try {
|
|
27
27
|
if (countChunks(db) === 0) return responseText({ hits: [], empty: true, topK, scope });
|
|
28
|
-
const hits = await query(db,
|
|
28
|
+
const hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope });
|
|
29
29
|
return responseText({ hits, empty: false, topK, scope });
|
|
30
30
|
} finally { db.close(); }
|
|
31
31
|
} catch (err) {
|
|
@@ -40,7 +40,7 @@ export async function handleRagIndex(args, server) {
|
|
|
40
40
|
const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
41
41
|
try {
|
|
42
42
|
const totals = await indexProject(projectDir, {
|
|
43
|
-
db, embedder:
|
|
43
|
+
db, embedder: makeGovernedEmbedder(config),
|
|
44
44
|
karajanHome: getKarajanHome(), logger: silentLogger,
|
|
45
45
|
withSources: Boolean(args?.withSources),
|
|
46
46
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// never sees an exception from this stage.
|
|
7
7
|
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
8
8
|
import { openVecStore, countChunks } from "../../rag/vec-store.js";
|
|
9
|
-
import {
|
|
9
|
+
import { makeGovernedEmbedder } from "../../rag/governed-embedder.js";
|
|
10
10
|
import { query } from "../../rag/retriever.js";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_TOP_K = 5;
|
|
@@ -52,7 +52,7 @@ export async function runRagContextStage({ config, logger, emitter, eventBase, t
|
|
|
52
52
|
}
|
|
53
53
|
const topK = preload.topK || DEFAULT_TOP_K;
|
|
54
54
|
const scope = preload.scope || DEFAULT_SCOPE;
|
|
55
|
-
const embedder =
|
|
55
|
+
const embedder = makeGovernedEmbedder(config);
|
|
56
56
|
const searchOpts = resolveSearchOpts(config);
|
|
57
57
|
const hits = await query(db, embedder, task, { topK, scope, ...searchOpts });
|
|
58
58
|
if (hits.length === 0) return { skipped: true, reason: "no-hits" };
|
package/src/rag/auto-update.js
CHANGED
|
@@ -8,7 +8,7 @@ import { execa } from "execa";
|
|
|
8
8
|
|
|
9
9
|
import { openVecStore, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
|
|
10
10
|
import { indexProjectDelta } from "./indexer.js";
|
|
11
|
-
import {
|
|
11
|
+
import { makeGovernedEmbedder } from "./governed-embedder.js";
|
|
12
12
|
|
|
13
13
|
const HOOK_SRC = resolve(fileURLToPath(import.meta.url), "../../../scripts/git-hooks/post-merge");
|
|
14
14
|
|
|
@@ -25,7 +25,7 @@ export async function maybeAutoUpdate({ projectDir, config, logger = console, fl
|
|
|
25
25
|
const since = getLastIndexedCommit(db, slug);
|
|
26
26
|
if (!since || since === head) return { skipped: true, head };
|
|
27
27
|
logger.info?.(`[rag] drift detected (${since.slice(0, 7)} → ${head.slice(0, 7)}); running delta update`);
|
|
28
|
-
const totals = await indexProjectDelta(projectDir, { db, embedder:
|
|
28
|
+
const totals = await indexProjectDelta(projectDir, { db, embedder: makeGovernedEmbedder(config), since, logger });
|
|
29
29
|
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
30
30
|
return { ran: true, totals };
|
|
31
31
|
} catch (err) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governed embedder (KJC-TSK-0682 — card filed by the karajan-rag session
|
|
3
|
+
* after its independent security audit closed the same risk there, H2).
|
|
4
|
+
* Chunks of project code must not travel to a cloud embedder without
|
|
5
|
+
* governance: local providers (ollama/onnx) pass through untouched; cloud
|
|
6
|
+
* providers (openai/voyage/cohere/mistral) require an EXPLICIT
|
|
7
|
+
* `rag.sensitivity: public` — the default (`internal`) blocks with an
|
|
8
|
+
* actionable error, never a silent downgrade. Even when allowed, every
|
|
9
|
+
* text is PII-redacted (karajan-rag's audited redactPII) before leaving
|
|
10
|
+
* the machine — defense in depth, not a substitute for the policy.
|
|
11
|
+
*/
|
|
12
|
+
import { redactPII } from "karajan-rag";
|
|
13
|
+
import { makeEmbedder } from "./embedders/factory.js";
|
|
14
|
+
|
|
15
|
+
const LOCAL_PROVIDERS = new Set(["ollama", "onnx"]);
|
|
16
|
+
const redact = (text) => redactPII(String(text)).text;
|
|
17
|
+
|
|
18
|
+
class RedactingEmbedder {
|
|
19
|
+
constructor(inner) { this.inner = inner; }
|
|
20
|
+
get dim() { return this.inner.dim; }
|
|
21
|
+
get model() { return this.inner.model; }
|
|
22
|
+
async embed(text) { return this.inner.embed(redact(text)); }
|
|
23
|
+
async embedBatch(texts) { return this.inner.embedBatch(texts.map(redact)); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Drop-in replacement for makeEmbedder(config) at every kj call site. */
|
|
27
|
+
export function makeGovernedEmbedder(config) {
|
|
28
|
+
const provider = config?.rag?.embedder?.provider || "ollama";
|
|
29
|
+
if (LOCAL_PROVIDERS.has(provider)) return makeEmbedder(config);
|
|
30
|
+
|
|
31
|
+
const sensitivity = config?.rag?.sensitivity || "internal";
|
|
32
|
+
if (sensitivity !== "public") {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`embedder "${provider}" sends your code to a cloud provider, but rag.sensitivity is "${sensitivity}". `
|
|
35
|
+
+ `Use a local embedder (rag.embedder.provider: ollama | onnx) or declare rag.sensitivity: public in kj.config.yml.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return new RedactingEmbedder(makeEmbedder(config));
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONNX embedder fallback (KJC-TSK-0683) — limited machines where installing
|
|
3
|
+
* Ollama/Docker is not viable still get a working RAG: the built-in ONNX
|
|
4
|
+
* embedder (all-MiniLM via @huggingface/transformers, in-process, CPU).
|
|
5
|
+
* The choice is PERSISTED in the project config: onnx (384) and ollama
|
|
6
|
+
* (768) have different vector dims, so the election must be sticky —
|
|
7
|
+
* an index can never be written by one and queried by the other.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { parseDocument } from "yaml";
|
|
12
|
+
import { getProjectConfigPath } from "../config/loader.js";
|
|
13
|
+
|
|
14
|
+
const ONNX_EMBEDDER = { provider: "onnx", dim: 384 };
|
|
15
|
+
|
|
16
|
+
/** The same config, with the embedder swapped to the built-in ONNX. */
|
|
17
|
+
export function onnxConfig(config) {
|
|
18
|
+
return {
|
|
19
|
+
...config,
|
|
20
|
+
rag: { ...(config?.rag || {}), embedder: { ...(config?.rag?.embedder || {}), ...ONNX_EMBEDDER } },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Persist the ONNX election in the PROJECT config file. Surgical edit via
|
|
26
|
+
* the yaml Document API: only rag.embedder.{provider,dim} change — the
|
|
27
|
+
* user's comments, ordering and every other key survive untouched.
|
|
28
|
+
*/
|
|
29
|
+
export async function persistOnnxChoice(projectDir) {
|
|
30
|
+
const configPath = getProjectConfigPath(projectDir);
|
|
31
|
+
let source = "";
|
|
32
|
+
try {
|
|
33
|
+
source = await fs.readFile(configPath, "utf8");
|
|
34
|
+
} catch { /* no project config yet — create it */ }
|
|
35
|
+
const doc = parseDocument(source || "{}");
|
|
36
|
+
for (const [key, value] of Object.entries(ONNX_EMBEDDER)) {
|
|
37
|
+
doc.setIn(["rag", "embedder", key], value);
|
|
38
|
+
}
|
|
39
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
40
|
+
await fs.writeFile(configPath, doc.toString(), "utf8");
|
|
41
|
+
return configPath;
|
|
42
|
+
}
|
package/src/rag/watcher.js
CHANGED
|
@@ -5,7 +5,7 @@ import { writeFileSync, readFileSync, existsSync, unlinkSync } from "node:fs";
|
|
|
5
5
|
import { extname, join } from "node:path";
|
|
6
6
|
import chokidar from "chokidar";
|
|
7
7
|
import { openVecStore, deleteChunksBySource, projectSlug } from "./vec-store.js";
|
|
8
|
-
import {
|
|
8
|
+
import { makeGovernedEmbedder } from "./governed-embedder.js";
|
|
9
9
|
import { indexFile } from "./indexer.js";
|
|
10
10
|
import { getKarajanHome } from "../utils/paths.js";
|
|
11
11
|
import { getAllCodeExtensions } from "../lang/registry.js";
|
|
@@ -27,7 +27,7 @@ export function startWatcher({ projectDir, config, logger = console, debounceMs
|
|
|
27
27
|
const slug = projectSlug(projectDir);
|
|
28
28
|
const dim = config?.rag?.embedder?.dim || 768;
|
|
29
29
|
const db = openVecStore({ dim });
|
|
30
|
-
const embedder =
|
|
30
|
+
const embedder = makeGovernedEmbedder(config);
|
|
31
31
|
const paths = [join(getKarajanHome(), "onboarding"), join(getKarajanHome(), "plans")];
|
|
32
32
|
if (withSources) paths.push(projectDir);
|
|
33
33
|
const watcher = chokidar.watch(paths, { ignoreInitial: true, persistent: true });
|
package/templates/coder-rules.md
CHANGED
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
- Do not modify code unrelated to the task.
|
|
24
24
|
- Follow existing code conventions and patterns in the repository.
|
|
25
25
|
|
|
26
|
+
## Environment isolation (parallel runs)
|
|
27
|
+
|
|
28
|
+
- Never hardcode ports, DB names, or docker project names — read them from env
|
|
29
|
+
vars, documenting each one in `.env.example`. When `KJ_LANE_SLOT` /
|
|
30
|
+
`KJ_PORT_OFFSET` exist in the env, derive per-task values from them so
|
|
31
|
+
parallel lanes never collide.
|
|
32
|
+
- Never commit a real `.env` — only `.env.example` is versioned.
|
|
33
|
+
|
|
26
34
|
## Tests location (MANDATORY)
|
|
27
35
|
|
|
28
36
|
- Tests live in a top-level `tests/` directory mirroring `src/` (e.g. a function in
|
package/templates/kj.config.yml
CHANGED
|
@@ -118,3 +118,11 @@ session:
|
|
|
118
118
|
# when exceeded; resume with `kj resume`. Set to null to remove the cap.
|
|
119
119
|
max_budget_usd: 5
|
|
120
120
|
fail_fast_repeats: 2
|
|
121
|
+
# Per-lane setup for parallel runs (worktrees). Runs INSIDE the fresh
|
|
122
|
+
# worktree instead of the default `npm ci`. Each lane receives
|
|
123
|
+
# KJ_LANE_SLOT (0,1,2…) and KJ_PORT_OFFSET (slot-derived) in its env —
|
|
124
|
+
# derive ports, DB names and docker project names from them so lanes
|
|
125
|
+
# never collide. The coder never orchestrates git; isolation is declared
|
|
126
|
+
# here. Example:
|
|
127
|
+
# worktree_setup: "npm ci && docker compose -p app_lane_$KJ_LANE_SLOT up -d db"
|
|
128
|
+
worktree_setup: null
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
- Only raise blocking issues for concrete production risks.
|
|
5
5
|
- Keep non-blocking suggestions separate.
|
|
6
6
|
|
|
7
|
+
## Environment isolation
|
|
8
|
+
|
|
9
|
+
- Reject changes that hardcode ports, DB names or service hostnames instead of
|
|
10
|
+
reading them from env vars, and any diff that adds a real `.env` (only
|
|
11
|
+
`.env.example` is versioned).
|
|
12
|
+
|
|
7
13
|
## File overwrite detection (BLOCKING)
|
|
8
14
|
|
|
9
15
|
- If the diff shows an entire file was replaced (massive deletions + additions instead of targeted edits), flag it as BLOCKING.
|