karajan-code 2.23.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,31 @@ 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
+ >
348
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.
349
374
  >
350
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.23.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",
@@ -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
+ }