karajan-code 2.22.0 → 2.24.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 CHANGED
@@ -345,6 +345,49 @@ Opt out: set `telemetry: false` in `~/.karajan/kj.config.yml`
345
345
 
346
346
  ## Recent releases
347
347
 
348
+ > **v2.24.0 released** — Minor. **RAG Camino C — pre-loop auto-retrieval** (KJC-PCS-0049). After v2.23.0 taught the agents that `kj_rag_query` exists, Karajan now injects prior context for them automatically: a new pre-loop stage queries the vec store with the task description and prepends the top-K chunks to the task before any LLM call.
349
+ >
350
+ > Opt-in: `config.rag.preload.enabled = false` by default. Five guards before the retrieval fires (`disabled`, `no-task`, empty corpus, no hits, error). The stage never throws — best-effort enrichment, opt-out by default, opt-back-out on failure. When all guards pass, the task receives an extra block:
351
+ >
352
+ > ```markdown
353
+ > ## Prior context from RAG (auto-retrieved, top N)
354
+ >
355
+ > ### [plan · AUTH-1] /path/to/plan.json
356
+ >
357
+ > <chunk text, truncated 600 chars>
358
+ > ```
359
+ >
360
+ > Because `task` flows through `runPlanningPhases` to researcher / architect / planner via parameter passing, **one mutation feeds six consumers** — no per-stage prompt wrapper, no fan-out through `pipelineFlags`. Triage runs first because its `roleOverrides` gate the rest of the pipeline; the RAG stage runs right after, before `domainCurator`.
361
+ >
362
+ > Workflow:
363
+ >
364
+ > ```bash
365
+ > kj onboard
366
+ > kj rag index
367
+ > yq -i '.rag.preload.enabled = true' ~/.karajan/kj.config.yml
368
+ > kj run task.md # researcher/architect/planner/coder all see prior context
369
+ > ```
370
+ >
371
+ > **Coming in v2.25.0+**: Camino B (`/kj-rag-query` slash command for Skills hosts), Camino D (Brain decisor heuristic deciding **when** to pre-fetch based on task complexity — refinement on top of C), chokidar watcher for live re-indexing, AST source chunker, BM25 + cosine hybrid scoring.
372
+ >
373
+ > **v2.23.0 released** — Minor. **RAG exposed to agents and humans alike** (KJC-PCS-0049, Steps 7+8+Camino A). After v2.22.0's CLI MVP, the corpus is now reachable from three more places.
374
+ >
375
+ > 1. **MCP tools** `kj_rag_query` + `kj_rag_index` (PR #815). Any MCP-connected agent — Claude Desktop, Cursor, Claude Code, Karajan's own roles — can call them. Tool count 25 → 27. Empty store responds `empty: true` so agents have a deterministic recovery signal.
376
+ > 2. **HU Board RAG panel** (PR #816). New input + scope dropdown (All / Plans / Onboarding / Code) + Search button + results pane between the preflight panel and the kanban. `POST /api/rag/query` backs it.
377
+ > 3. **Role templates teach agents about the tool** (PR #817). `templates/roles/{coder,researcher,architect,planner,spec-reviewer}.md` each gain a tailored 'Prior context (RAG, opt-in)' section. Shared rule: when the store is empty, proceed without retrieval — do NOT block, do NOT ask the human to seed.
378
+ >
379
+ > Workflow:
380
+ >
381
+ > ```bash
382
+ > kj onboard
383
+ > kj rag index
384
+ > kj plan generate task.md --use-onboarding
385
+ > # From here on, every agent invocation can call kj_rag_query via MCP.
386
+ > # Humans get the panel on the Board.
387
+ > ```
388
+ >
389
+ > **Coming in v2.24.0+**: Camino B (`/kj-rag-query` slash command for hosts in Skills mode without MCP), Camino C (automatic pre-loop stage that pre-fetches retrieval and prepends it to the coder/researcher/architect prompt without the agent having to ask), Camino D (Brain decisor heuristic for when retrieval is worth the tokens), chokidar watcher for live re-indexing, AST-aware source chunker, BM25 + cosine hybrid scoring.
390
+ >
348
391
  > **v2.22.0 released** — Minor. **Project RAG MVP** (KJC-PCS-0049). Karajan now indexes its plans + onboarding briefs (and optionally project sources) into a local vector store and lets you query them semantically from the CLI. Six PRs.
349
392
  >
350
393
  > | Step | PR | Module |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.22.0",
3
+ "version": "2.24.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",
@@ -590,6 +590,18 @@ async function renderBoard() {
590
590
  Comprobando estado del proyecto…
591
591
  </div>
592
592
  </div>
593
+ <div id="rag-panel" class="rag-panel" style="margin:8px 0;display:flex;gap:8px;align-items:flex-start;flex-wrap:wrap;">
594
+ <input id="rag-query" type="text" placeholder="🔍 RAG search: ask anything about this project's plans / onboarding / code…"
595
+ style="flex:1;min-width:260px;padding:6px 10px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);" />
596
+ <select id="rag-scope" style="padding:6px 10px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);">
597
+ <option value="all">All</option><option value="plans">Plans</option><option value="onboarding">Onboarding</option><option value="code">Code</option>
598
+ </select>
599
+ <button id="rag-search-btn" class="control-btn"
600
+ style="padding:6px 12px;font-size:0.85rem;background:var(--color-blue,#3b82f6);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-weight:600;">
601
+ Search
602
+ </button>
603
+ <div id="rag-results" style="flex-basis:100%;font-size:0.8rem;color:var(--text-muted);"></div>
604
+ </div>
593
605
  <div id="plan-rollup-banner"></div>
594
606
  <div class="kanban">
595
607
  ${visibleColumns.map((c) => renderKanbanColumn(c.title, c.cls, c.rows)).join('')}
@@ -694,6 +706,39 @@ async function renderBoard() {
694
706
  }
695
707
  });
696
708
  }
709
+
710
+ // RAG search panel (KJC-PCS-0049 Step 8) — input + scope + render hits.
711
+ const ragInput = document.getElementById('rag-query');
712
+ const ragScope = document.getElementById('rag-scope');
713
+ const ragBtn = document.getElementById('rag-search-btn');
714
+ const ragResults = document.getElementById('rag-results');
715
+ async function runRagSearch() {
716
+ const text = ragInput?.value?.trim();
717
+ if (!text) return;
718
+ ragResults.innerHTML = '<em>Searching…</em>';
719
+ try {
720
+ const res = await fetch('/api/rag/query', {
721
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
722
+ body: JSON.stringify({ text, topK: 5, scope: ragScope?.value || 'all' }),
723
+ });
724
+ const body = await res.json();
725
+ if (!res.ok) { ragResults.innerHTML = `<span style="color:var(--color-red);">Error: ${esc(body.error || res.status)}</span>`; return; }
726
+ if (body.empty) { ragResults.innerHTML = '<em>No chunks indexed yet. Run <code>kj rag index</code> in this project\'s terminal first.</em>'; return; }
727
+ if (body.hits.length === 0) { ragResults.innerHTML = '<em>No hits.</em>'; return; }
728
+ ragResults.innerHTML = body.hits.map((h) => {
729
+ const label = h.metadata?.hu_id || h.metadata?.symbol || h.metadata?.headingPath?.join(' > ') || 'block';
730
+ const snippet = h.text.length > 240 ? `${esc(h.text.slice(0, 240))}…` : esc(h.text);
731
+ return `<div style="border-left:3px solid var(--color-blue,#3b82f6);padding:6px 10px;margin:6px 0;background:var(--bg-primary);">
732
+ <div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:4px;">[${esc(h.kind)} · ${esc(label)} · score=${h.score.toFixed(4)}] ${esc(h.source)}</div>
733
+ <div style="font-size:0.85rem;white-space:pre-wrap;">${snippet}</div>
734
+ </div>`;
735
+ }).join('');
736
+ } catch (err) {
737
+ ragResults.innerHTML = `<span style="color:var(--color-red);">Error: ${esc(err.message)}</span>`;
738
+ }
739
+ }
740
+ ragBtn?.addEventListener('click', runRagSearch);
741
+ ragInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') runRagSearch(); });
697
742
  } catch (err) {
698
743
  app.innerHTML = `<div class="empty-state"><div class="empty-state__title">Error loading board</div><div class="empty-state__text">${esc(err.message)}</div></div>`;
699
744
  }
@@ -1279,4 +1279,31 @@ router.post('/sync', (_req, res) => {
1279
1279
  }
1280
1280
  });
1281
1281
 
1282
+ /**
1283
+ * POST /api/rag/query - Semantic search over the indexed RAG corpus
1284
+ * (KJC-PCS-0049 Step 8). Body: { text, topK?, scope? }. Response:
1285
+ * { hits: [...], empty, topK, scope }. Empty store carries empty=true
1286
+ * so the UI can show an actionable hint instead of just empty results.
1287
+ */
1288
+ router.post('/rag/query', async (req, res) => {
1289
+ const { text, topK = 5, scope = 'all' } = req.body || {};
1290
+ if (typeof text !== 'string' || text.length === 0) {
1291
+ return res.status(400).json({ error: "'text' is required (non-empty string)" });
1292
+ }
1293
+ try {
1294
+ const { openVecStore, countChunks } = await import('../../../../src/rag/vec-store.js');
1295
+ const { OllamaEmbedder } = await import('../../../../src/rag/embedder.js');
1296
+ const { query } = await import('../../../../src/rag/retriever.js');
1297
+ const db = openVecStore({ dim: 768 });
1298
+ try {
1299
+ if (countChunks(db) === 0) return res.json({ hits: [], empty: true, topK, scope });
1300
+ const embedder = new OllamaEmbedder({});
1301
+ const hits = await query(db, embedder, text, { topK: Number(topK), scope });
1302
+ res.json({ hits, empty: false, topK, scope });
1303
+ } finally { db.close(); }
1304
+ } catch (err) {
1305
+ res.status(500).json({ error: err.message });
1306
+ }
1307
+ });
1308
+
1282
1309
  export default router;
@@ -0,0 +1,57 @@
1
+ // KJC-PCS-0049 Step 7 — MCP handlers calling src/rag/* directly. Imports
2
+ // from src/commands/* are forbidden by the layer-boundaries test
3
+ // (MCP and CLI are peer layers); we hit the pure rag/* modules instead.
4
+ import { countChunks, openVecStore } from "../../rag/vec-store.js";
5
+ import { OllamaEmbedder } from "../../rag/embedder.js";
6
+ import { indexProject } from "../../rag/indexer.js";
7
+ import { query } from "../../rag/retriever.js";
8
+ import { getKarajanHome } from "../../utils/paths.js";
9
+ import { resolveProjectDir, buildConfig, responseText, failPayload } from "../shared-helpers.js";
10
+
11
+ const silentLogger = {
12
+ info: () => {}, warn: () => {}, error: () => {}, debug: () => {}, setContext: () => {},
13
+ };
14
+
15
+ function makeEmbedder(config) {
16
+ const cfg = config?.rag?.embedder || {};
17
+ return new OllamaEmbedder({ url: cfg.url, model: cfg.model, dim: cfg.dim });
18
+ }
19
+
20
+ export async function handleRagQuery(args, server) {
21
+ const text = args?.text;
22
+ if (typeof text !== "string" || text.length === 0) {
23
+ return failPayload("kj_rag_query: 'text' is required and must be a non-empty string");
24
+ }
25
+ try {
26
+ const projectDir = await resolveProjectDir(server, args?.projectDir);
27
+ const config = await buildConfig({ ...args, projectDir }, "rag-query");
28
+ const topK = Number(args?.topK) || 5;
29
+ const scope = args?.scope || "all";
30
+ const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
31
+ try {
32
+ if (countChunks(db) === 0) return responseText({ hits: [], empty: true, topK, scope });
33
+ const hits = await query(db, makeEmbedder(config), text, { topK, scope });
34
+ return responseText({ hits, empty: false, topK, scope });
35
+ } finally { db.close(); }
36
+ } catch (err) {
37
+ return failPayload(`kj_rag_query failed: ${err.message}`);
38
+ }
39
+ }
40
+
41
+ export async function handleRagIndex(args, server) {
42
+ try {
43
+ const projectDir = await resolveProjectDir(server, args?.projectDir);
44
+ const config = await buildConfig({ ...args, projectDir }, "rag-index");
45
+ const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
46
+ try {
47
+ const totals = await indexProject(projectDir, {
48
+ db, embedder: makeEmbedder(config),
49
+ karajanHome: getKarajanHome(), logger: silentLogger,
50
+ withSources: Boolean(args?.withSources),
51
+ });
52
+ return responseText(totals);
53
+ } finally { db.close(); }
54
+ } catch (err) {
55
+ return failPayload(`kj_rag_index failed: ${err.message}`);
56
+ }
57
+ }
@@ -39,6 +39,7 @@ import {
39
39
  handleScan, handleBoard, handleUndo, handleClean
40
40
  } from "./handlers/management-handlers.js";
41
41
  import { handleHu, handleSkills, handleSuggest } from "./handlers/hu-handlers.js";
42
+ import { handleRagQuery, handleRagIndex } from "./handlers/rag-handler.js";
42
43
 
43
44
  export async function handleToolCall(name, args, server, extra) {
44
45
  const a = asObject(args);
@@ -68,6 +69,8 @@ export async function handleToolCall(name, args, server, extra) {
68
69
  kj_skills: (a) => handleSkills(a),
69
70
  kj_undo: (a, server) => handleUndo(a, server),
70
71
  kj_clean: (a) => handleClean(a),
72
+ kj_rag_query: (a, server) => handleRagQuery(a, server),
73
+ kj_rag_index: (a, server) => handleRagIndex(a, server),
71
74
  }[name];
72
75
  if (handler) {
73
76
  return handler(a, server, extra);
package/src/mcp/tools.js CHANGED
@@ -403,5 +403,30 @@ export const tools = [
403
403
  huDays: { type: "number", description: "Keep HU story batches for this many days. Default: 14." }
404
404
  }
405
405
  }
406
+ },
407
+ {
408
+ name: "kj_rag_query",
409
+ description: "Run a semantic search across the indexed Karajan plans, onboarding briefs and (optionally) project source code. Returns the top-K nearest chunks with their metadata so the agent can quote prior decisions, locate where a concept was last implemented, or pull architectural context without re-walking the repo. The store is empty until `kj_rag_index` (or `kj rag index` CLI) has been run at least once on this project; in that case the response carries `empty: true`.",
410
+ inputSchema: {
411
+ type: "object",
412
+ properties: {
413
+ text: { type: "string", description: "The natural-language query." },
414
+ topK: { type: "number", description: "Max number of hits to return. Default: 5." },
415
+ scope: { type: "string", description: "Filter by chunk kind: 'plans' | 'code' | 'onboarding' | 'all' (default)." },
416
+ projectDir: { type: "string", description: "Absolute path to the project directory" }
417
+ },
418
+ required: ["text"]
419
+ }
420
+ },
421
+ {
422
+ name: "kj_rag_index",
423
+ description: "(Re)index this project's Karajan-managed assets — every plan-*.json under ~/.karajan/plans/<slug>/ plus the onboarding brief at ~/.karajan/onboarding/<slug>.md — into the local vector store. Idempotent: a re-run replaces the chunks for each source, never appends duplicates. Pass `withSources: true` to also index the project's JS/TS files (skips node_modules / .git / dist / etc.). Required as a one-time bootstrap before kj_rag_query returns anything; safe to re-run after every `kj plan generate`.",
424
+ inputSchema: {
425
+ type: "object",
426
+ properties: {
427
+ withSources: { type: "boolean", description: "Also index .js/.ts files under projectDir. Default false." },
428
+ projectDir: { type: "string", description: "Absolute path to the project directory" }
429
+ }
430
+ }
406
431
  }
407
432
  ];
@@ -40,6 +40,7 @@ import {
40
40
  runResearcherStage, runArchitectStage, runPlannerStage,
41
41
  runDiscoverStage, runHuReviewerStage,
42
42
  } from "../pre-loop-stages.js";
43
+ import { runRagContextStage } from "../stages/rag-context-stage.js";
43
44
  // TSK-0336: triage goes through the StageRegistry / StageExecutor contract
44
45
  // so canRun / execute / onFailure are actually exercised in production.
45
46
  // The registry is a singleton with TriageStage / CoderStage / ReviewerStage
@@ -123,6 +124,20 @@ export async function runPreLoopStages({ config, logger, emitter, eventBase, ses
123
124
  applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
124
125
  await persistStage("triage", triageResult.stageResult);
125
126
 
127
+ // --- RAG preload (KJC-PCS-0049 Camino C, opt-in) ---
128
+ // Runs only when config.rag.preload.enabled === true. Queries the local
129
+ // vector store with the task description, prepends the top-K chunks to
130
+ // the task as a markdown block so researcher/architect/planner/coder
131
+ // all see prior context without having to call kj_rag_query themselves.
132
+ // Empty store / network / etc. degrade to no-op via the stage itself.
133
+ if (!resumeSkip("ragPreload")) {
134
+ const ragRes = await runRagContextStage({ config, logger, emitter, eventBase, task });
135
+ if (!ragRes.skipped && ragRes.contextBlock) {
136
+ task = `${task}\n\n${ragRes.contextBlock}`;
137
+ await persistStage("ragPreload", { hits: ragRes.hits.length, scope: config?.rag?.preload?.scope || "all" });
138
+ }
139
+ }
140
+
126
141
  // --- Brain decisor (opt-in, intent-driven routing) ---
127
142
  //
128
143
  // When enabled, translates triage output + task text + CLI overrides into a
@@ -0,0 +1,60 @@
1
+ // KJC-PCS-0049 Camino C — pre-loop RAG retrieval stage.
2
+ // Runs between triage and domainCurator. When `config.rag.preload.enabled`
3
+ // is true and the corpus has chunks, queries the retriever with the
4
+ // task text and returns a markdown block ready to prepend to the task.
5
+ // Failures degrade gracefully (warn + continue); the rest of the pipeline
6
+ // never sees an exception from this stage.
7
+ import { emitProgress, makeEvent } from "../../utils/events.js";
8
+ import { openVecStore, countChunks } from "../../rag/vec-store.js";
9
+ import { OllamaEmbedder } from "../../rag/embedder.js";
10
+ import { query } from "../../rag/retriever.js";
11
+
12
+ const DEFAULT_TOP_K = 5;
13
+ const DEFAULT_SCOPE = "all";
14
+ const MAX_CHUNK_CHARS = 600;
15
+
16
+ /**
17
+ * Runs the RAG preload stage. Returns:
18
+ * { skipped: true, reason } — when disabled / empty / error
19
+ * { skipped: false, contextBlock, hits } — markdown block + raw hits
20
+ *
21
+ * The caller decides whether to mutate `task` with the contextBlock. We
22
+ * keep that responsibility at the call site so this module stays pure.
23
+ */
24
+ export async function runRagContextStage({ config, logger, emitter, eventBase, task }) {
25
+ const preload = config?.rag?.preload;
26
+ if (!preload?.enabled) return { skipped: true, reason: "disabled" };
27
+ if (typeof task !== "string" || task.length === 0) return { skipped: true, reason: "no-task" };
28
+ try {
29
+ const dim = config?.rag?.embedder?.dim || 768;
30
+ const db = openVecStore({ dim });
31
+ try {
32
+ if (countChunks(db) === 0) {
33
+ logger?.info?.("[rag-preload] corpus is empty, skipping (run `kj rag index` to seed)");
34
+ return { skipped: true, reason: "empty" };
35
+ }
36
+ const topK = preload.topK || DEFAULT_TOP_K;
37
+ const scope = preload.scope || DEFAULT_SCOPE;
38
+ const cfg = config?.rag?.embedder || {};
39
+ const embedder = new OllamaEmbedder({ url: cfg.url, model: cfg.model, dim });
40
+ const hits = await query(db, embedder, task, { topK, scope });
41
+ if (hits.length === 0) return { skipped: true, reason: "no-hits" };
42
+ const lines = hits.map((h) => {
43
+ const label = h.metadata?.hu_id || h.metadata?.symbol || h.metadata?.headingPath?.join(" > ") || "block";
44
+ const text = h.text.length > MAX_CHUNK_CHARS ? `${h.text.slice(0, MAX_CHUNK_CHARS)}…` : h.text;
45
+ return `### [${h.kind} · ${label}] ${h.source}\n\n${text}`;
46
+ });
47
+ const contextBlock = `## Prior context from RAG (auto-retrieved, top ${hits.length})\n\n${lines.join("\n\n")}`;
48
+ emitProgress(emitter, makeEvent("rag-preload:hits", { ...eventBase, stage: "rag-preload" }, {
49
+ message: `RAG preload: injected ${hits.length} chunk(s) into the task context`,
50
+ detail: { topK, scope, hits: hits.length },
51
+ }));
52
+ return { skipped: false, contextBlock, hits };
53
+ } finally {
54
+ db.close();
55
+ }
56
+ } catch (err) {
57
+ logger?.warn?.(`[rag-preload] failed (non-blocking): ${err.message}`);
58
+ return { skipped: true, reason: "error", error: err.message };
59
+ }
60
+ }
@@ -70,3 +70,7 @@ Karajan projects MAY enforce a CI gate that fails any PR whose net delta exceeds
70
70
  - The gate counts the SUM of every changed file, not per-file. Tests count too. 5 files × 40 LOC = 200 = on the limit.
71
71
  - Excluded from the count: lockfiles, snapshots, `dist/`, `node_modules/`, generated `tests/_diet/`, `public/docs/`. Source + tests count.
72
72
  - Token-economy: oversized PRs get rejected at CI and the work is redone — partitioning upfront saves the round-trip.
73
+
74
+ ## Prior context (RAG, opt-in)
75
+
76
+ If a design decision touches an area the project already shaped (layering, patterns, contract style), call the `kj_rag_query` MCP tool with `{ text, topK: 3, scope: "all" }` and align your `tradeoffs` / `patterns` with what the prior plans already used. When the response carries `empty: true`, the corpus has not been indexed — proceed without it; do NOT block on retrieval.
@@ -110,3 +110,7 @@ Return a JSON object:
110
110
  "summary": "Human-readable summary of changes"
111
111
  }
112
112
  ```
113
+
114
+ ## Prior context (RAG, opt-in)
115
+
116
+ If you need to know how the project handled a similar concern before (auth, error model, retry policy, naming convention…), call the `kj_rag_query` MCP tool with `{ text, topK: 3, scope: "all" }`. It returns the closest chunks from prior plans + onboarding brief + indexed sources. When the response carries `empty: true`, the corpus has not been indexed yet — proceed without it; do NOT block on retrieval, and do NOT ask the human to run `kj rag index`. Use sparingly: one query per concern, not per file.
@@ -55,3 +55,7 @@ Karajan projects MAY enforce a CI gate that fails any PR whose net delta exceeds
55
55
  - The gate counts the SUM of every changed file, not per-file. Tests count too. 5 files × 40 LOC = 200 = on the limit.
56
56
  - Excluded from the count: lockfiles, snapshots, `dist/`, `node_modules/`, generated `tests/_diet/`, `public/docs/`. Source + tests count.
57
57
  - Token-economy: oversized PRs get rejected at CI and the work is redone — partitioning upfront saves the round-trip.
58
+
59
+ ## Prior context (RAG, opt-in)
60
+
61
+ Before emitting blocked_by graphs, dependencies, or reuse markers, query the local RAG corpus via the `kj_rag_query` MCP tool with `{ text, topK: 5, scope: "plans" }` to see how previous plans wired similar HUs. If a prior plan already produced the utility/spike the new plan would need, mark `reuse: ["<that-hu-id>"]` instead of re-implementing. When `empty: true`, the corpus has not been indexed — proceed without retrieval; do NOT block on it.
@@ -35,3 +35,7 @@ You are the **Researcher** in a multi-role AI pipeline. Your job is to investiga
35
35
  "summary": "Research complete: 5 files affected, 2 risks identified"
36
36
  }
37
37
  ```
38
+
39
+ ## Prior context (RAG, opt-in)
40
+
41
+ Before re-walking the repo, query the local RAG corpus via the `kj_rag_query` MCP tool with `{ text, topK: 5, scope: "plans" }` to surface decisions, hot files and ADR mentions captured in past plans + onboarding brief. When the response carries `empty: true`, the corpus has not been indexed yet — proceed with file-level research; do NOT block on retrieval. Quote retrieved evidence as bullet items under your `affects` list when relevant; do NOT paste raw chunks into the summary.
@@ -36,3 +36,7 @@ is the WORST of any finding, or `ok` if findings is empty.
36
36
 
37
37
  Direct, accionable. No moralising. `suggestion` must be concrete (a paste-able
38
38
  sentence), not vague advice.
39
+
40
+ ## Prior context (RAG, opt-in)
41
+
42
+ When evaluating whether the spec duplicates a prior plan's scope or contradicts an ADR, call the `kj_rag_query` MCP tool with `{ text, topK: 3, scope: "all" }` using a phrase from the spec as the query. If retrieved chunks describe overlapping HUs already approved, surface the conflict via a finding with `kind: "scope_overlap"` and reference the prior HU id. When `empty: true`, the corpus has not been indexed — proceed without retrieval; do NOT block on it.