opencode-rag-plugin 1.17.3 → 1.18.1

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.
Files changed (49) hide show
  1. package/ReadMe.md +41 -1
  2. package/dist/cli/commands/index.d.ts +1 -0
  3. package/dist/cli/commands/index.js +1 -0
  4. package/dist/cli/commands/init-helpers.d.ts +5 -1
  5. package/dist/cli/commands/init-helpers.js +12 -19
  6. package/dist/cli/commands/init.js +42 -8
  7. package/dist/cli/commands/quirk.d.ts +10 -0
  8. package/dist/cli/commands/quirk.js +141 -0
  9. package/dist/cli/commands/setup.js +15 -6
  10. package/dist/cli/commands/ui.js +1 -1
  11. package/dist/cli/index.js +2 -1
  12. package/dist/core/config.d.ts +30 -0
  13. package/dist/core/config.js +26 -2
  14. package/dist/core/interfaces.d.ts +25 -1
  15. package/dist/core/runtime-overrides.d.ts +7 -0
  16. package/dist/core/runtime-overrides.js +15 -0
  17. package/dist/describer/anthropic.d.ts +4 -0
  18. package/dist/describer/anthropic.js +9 -2
  19. package/dist/describer/describer.d.ts +4 -0
  20. package/dist/describer/describer.js +8 -0
  21. package/dist/describer/gemini.d.ts +3 -0
  22. package/dist/describer/gemini.js +8 -2
  23. package/dist/indexer/pipeline.js +40 -0
  24. package/dist/opencode/system-guidance.d.ts +34 -0
  25. package/dist/opencode/system-guidance.js +127 -0
  26. package/dist/opencode/tools.d.ts +38 -0
  27. package/dist/opencode/tools.js +127 -0
  28. package/dist/plugin.js +204 -31
  29. package/dist/quirks/auto-capture.d.ts +14 -0
  30. package/dist/quirks/auto-capture.js +112 -0
  31. package/dist/quirks/monitor.d.ts +5 -0
  32. package/dist/quirks/monitor.js +23 -0
  33. package/dist/quirks/prompts.d.ts +1 -0
  34. package/dist/quirks/prompts.js +13 -0
  35. package/dist/quirks/quirk-store.d.ts +27 -0
  36. package/dist/quirks/quirk-store.js +217 -0
  37. package/dist/quirks/types.d.ts +20 -0
  38. package/dist/quirks/types.js +2 -0
  39. package/dist/retriever/keyword-index.js +16 -0
  40. package/dist/vectorstore/lancedb.d.ts +21 -11
  41. package/dist/vectorstore/lancedb.js +174 -18
  42. package/dist/vectorstore/memory.d.ts +2 -0
  43. package/dist/vectorstore/memory.js +6 -0
  44. package/dist/web/api.d.ts +3 -1
  45. package/dist/web/api.js +50 -1
  46. package/dist/web/server.d.ts +2 -1
  47. package/dist/web/server.js +3 -2
  48. package/dist/web/ui/index.html +163 -0
  49. package/package.json +1 -1
@@ -122,6 +122,21 @@ export function applyRuntimeOverrides(cfg, overrides) {
122
122
  merged.imageDescription.model = overrides.imageDescription.model;
123
123
  }
124
124
  }
125
+ if (overrides.memory) {
126
+ if (!merged.memory)
127
+ merged.memory = { ...DEFAULT_CONFIG.memory };
128
+ const m = merged.memory;
129
+ if (overrides.memory.passiveCapture !== undefined)
130
+ m.passiveCapture = overrides.memory.passiveCapture;
131
+ if (overrides.memory.promptEnforcement !== undefined)
132
+ m.promptEnforcement = overrides.memory.promptEnforcement;
133
+ if (overrides.memory.sessionEndExtraction !== undefined)
134
+ m.sessionEndExtraction = overrides.memory.sessionEndExtraction;
135
+ if (overrides.memory.autoCaptureMaxPerTurn !== undefined)
136
+ m.autoCaptureMaxPerTurn = overrides.memory.autoCaptureMaxPerTurn;
137
+ if (overrides.memory.autoCaptureDedupThreshold !== undefined)
138
+ m.autoCaptureDedupThreshold = overrides.memory.autoCaptureDedupThreshold;
139
+ }
125
140
  if (overrides.tui) {
126
141
  merged.tui = {
127
142
  ...DEFAULT_CONFIG.tui,
@@ -18,6 +18,10 @@ export declare class AnthropicDescriptionProvider implements DescriptionProvider
18
18
  /** @inheritdoc */
19
19
  generateDescription(chunk: Chunk): Promise<string>;
20
20
  /** @inheritdoc */
21
+ generateText(system: string, user: string, opts?: {
22
+ timeoutMs?: number;
23
+ }): Promise<string>;
24
+ /** @inheritdoc */
21
25
  generateBatchDescriptions(chunks: Chunk[], logger?: DescriptionLogger, opts?: BatchDescriptionOptions): Promise<Map<string, string>>;
22
26
  /**
23
27
  * Sends a request to the Anthropic Messages API with retry and exponential backoff.
@@ -25,6 +25,13 @@ export class AnthropicDescriptionProvider {
25
25
  return this.chatRequest(messages, this.config.timeoutMs ?? 60000);
26
26
  }
27
27
  /** @inheritdoc */
28
+ async generateText(system, user, opts) {
29
+ const messages = [
30
+ { role: "user", content: user },
31
+ ];
32
+ return this.chatRequest(messages, opts?.timeoutMs ?? this.config.timeoutMs ?? 60000, system);
33
+ }
34
+ /** @inheritdoc */
28
35
  async generateBatchDescriptions(chunks, logger, opts) {
29
36
  const log = logger ?? { info: (msg) => process.stderr.write(`${msg}\n`), warn: (msg) => process.stderr.write(`${msg}\n`), debug: (msg) => process.stderr.write(`${msg}\n`) };
30
37
  const concurrency = this.config.batchConcurrency ?? 3;
@@ -60,10 +67,10 @@ export class AnthropicDescriptionProvider {
60
67
  * @returns The trimmed response text from the model.
61
68
  * @throws When all retry attempts are exhausted or the response is empty.
62
69
  */
63
- async chatRequest(messages, timeoutMs) {
70
+ async chatRequest(messages, timeoutMs, systemOverride) {
64
71
  const baseUrl = this.config.baseUrl.replace(/\/+$/, "");
65
72
  const apiKey = this.config.apiKey ?? "";
66
- const systemPrompt = this.config.systemPrompt;
73
+ const systemPrompt = systemOverride ?? this.config.systemPrompt;
67
74
  const body = {
68
75
  model: this.config.model,
69
76
  max_tokens: 4096,
@@ -18,6 +18,10 @@ export declare class LlmDescriptionProvider implements DescriptionProvider {
18
18
  /** @inheritdoc */
19
19
  generateDescription(chunk: Chunk): Promise<string>;
20
20
  /** @inheritdoc */
21
+ generateText(system: string, user: string, opts?: {
22
+ timeoutMs?: number;
23
+ }): Promise<string>;
24
+ /** @inheritdoc */
21
25
  generateBatchDescriptions(chunks: Chunk[], logger?: DescriptionLogger, opts?: BatchDescriptionOptions): Promise<Map<string, string>>;
22
26
  /**
23
27
  * Sends a chat completion request to the LLM API with retry and exponential backoff.
@@ -26,6 +26,14 @@ export class LlmDescriptionProvider {
26
26
  return this.chatRequest(messages, this.config.timeoutMs ?? 60000);
27
27
  }
28
28
  /** @inheritdoc */
29
+ async generateText(system, user, opts) {
30
+ const messages = [
31
+ { role: "system", content: system },
32
+ { role: "user", content: user },
33
+ ];
34
+ return this.chatRequest(messages, opts?.timeoutMs ?? this.config.timeoutMs ?? 60000);
35
+ }
36
+ /** @inheritdoc */
29
37
  async generateBatchDescriptions(chunks, logger, opts) {
30
38
  const log = logger ?? { info: (msg) => process.stderr.write(`${msg}\n`), warn: (msg) => process.stderr.write(`${msg}\n`), debug: (msg) => process.stderr.write(`${msg}\n`) };
31
39
  const concurrency = this.config.batchConcurrency ?? 3;
@@ -12,6 +12,9 @@ export declare class GeminiDescriptionProvider implements DescriptionProvider {
12
12
  private readonly config;
13
13
  constructor(config: DescriptionConfig);
14
14
  generateDescription(chunk: Chunk): Promise<string>;
15
+ generateText(system: string, user: string, opts?: {
16
+ timeoutMs?: number;
17
+ }): Promise<string>;
15
18
  generateBatchDescriptions(chunks: Chunk[], logger?: DescriptionLogger, opts?: BatchDescriptionOptions): Promise<Map<string, string>>;
16
19
  private chatRequest;
17
20
  }
@@ -21,6 +21,12 @@ export class GeminiDescriptionProvider {
21
21
  ];
22
22
  return this.chatRequest(contents, this.config.timeoutMs ?? 60000);
23
23
  }
24
+ async generateText(system, user, opts) {
25
+ const contents = [
26
+ { role: "user", parts: [{ text: user }] },
27
+ ];
28
+ return this.chatRequest(contents, opts?.timeoutMs ?? this.config.timeoutMs ?? 60000, system);
29
+ }
24
30
  async generateBatchDescriptions(chunks, logger, opts) {
25
31
  const log = logger ?? { info: (msg) => process.stderr.write(`${msg}\n`), warn: (msg) => process.stderr.write(`${msg}\n`), debug: (msg) => process.stderr.write(`${msg}\n`) };
26
32
  const concurrency = this.config.batchConcurrency ?? 3;
@@ -46,11 +52,11 @@ export class GeminiDescriptionProvider {
46
52
  log.debug(`[describer] Descriptions generated: ${result.size}/${total}`);
47
53
  return result;
48
54
  }
49
- async chatRequest(contents, timeoutMs) {
55
+ async chatRequest(contents, timeoutMs, systemOverride) {
50
56
  const baseUrl = this.config.baseUrl.replace(/\/+$/, "");
51
57
  const apiKey = this.config.apiKey ?? "";
52
58
  const model = this.config.model;
53
- const systemPrompt = this.config.systemPrompt;
59
+ const systemPrompt = systemOverride ?? this.config.systemPrompt;
54
60
  const allParts = [{ text: systemPrompt }];
55
61
  for (const c of contents) {
56
62
  allParts.push(...c.parts);
@@ -169,7 +169,10 @@ async function runIndexPassInner(options, logger) {
169
169
  const workspaceFiles = await scanWorkspaceFiles(options.cwd, options.config, logger, options.force ? undefined : manifest, filterPaths, options.imageVisionProvider, descCache);
170
170
  const scanSec = ((Date.now() - scanStart) / 1000).toFixed(1);
171
171
  logger.info(`Workspace scan complete: ${workspaceFiles.length} files in ${scanSec}s`);
172
+ logger.info(`Querying vector store for existing chunk count...`);
173
+ const storeCountStart = Date.now();
172
174
  const existingCount = await options.store.count();
175
+ logger.info(`Store has ${existingCount} existing chunks (${((Date.now() - storeCountStart) / 1000).toFixed(1)}s)`);
173
176
  // Detect data loss: if the store has far fewer chunks than the manifest expects,
174
177
  // treat it as a corrupt store (e.g. schema migration dropped the old table).
175
178
  if (!options.force && manifestStatus === "ok" && existingCount > 0) {
@@ -276,6 +279,11 @@ async function runIndexPassInner(options, logger) {
276
279
  logger.debug(`Processing ${workspaceFiles.length} files (${newCount} new, ${modifiedCount} modified, ${unchangedCount} unchanged)...`);
277
280
  const limit = pLimit(options.config.indexing.concurrency);
278
281
  const deferDescriptions = !!options.descriptionProvider;
282
+ const activeCount = workspaceFiles.filter((f) => !f.isEmpty && !f.isTooSmall && (!manifest.files[f.normalizedPath] || manifest.files[f.normalizedPath].hash !== f.hash)).length;
283
+ logger.info(`Chunking ${activeCount} file(s)...`);
284
+ let chunkedDone = 0;
285
+ const chunkProgressInterval = Math.max(1, Math.floor(activeCount / 20));
286
+ const chunkPhaseStart = Date.now();
279
287
  const prepared = await Promise.all(workspaceFiles.map((file) => limit(async () => {
280
288
  const fileLabel = path.relative(options.cwd, file.normalizedPath).replace(/\\/g, "/");
281
289
  const isActive = !file.isEmpty && !file.isTooSmall &&
@@ -284,11 +292,20 @@ async function runIndexPassInner(options, logger) {
284
292
  options.progress?.startFile(fileLabel);
285
293
  }
286
294
  const prep = await prepareFile(file, options.cwd, manifest.files[file.normalizedPath], options.config, options.keywordIndex, options.descriptionProvider, logger, deferDescriptions, descHash);
295
+ if (isActive) {
296
+ chunkedDone++;
297
+ if (chunkedDone % chunkProgressInterval === 0 || chunkedDone === activeCount) {
298
+ const pct = ((chunkedDone / activeCount) * 100).toFixed(1);
299
+ logger.info(` Chunking: ${chunkedDone}/${activeCount} files (${pct}%)`);
300
+ }
301
+ }
287
302
  if (prep.earlyResult && isActive) {
288
303
  options.progress?.finishFile(fileLabel);
289
304
  }
290
305
  return prep;
291
306
  })));
307
+ const chunkPhaseSec = ((Date.now() - chunkPhaseStart) / 1000).toFixed(1);
308
+ logger.info(`Chunking complete: ${activeCount} files in ${chunkPhaseSec}s`);
292
309
  const aborted = () => options.abortSignal?.aborted ?? false;
293
310
  // Shared progress logger: "stage <file> (chunk i/n) — X/total remaining (P%)".
294
311
  const logChunkProgress = (stage, fileLabel, index, count, completed, total) => {
@@ -302,6 +319,7 @@ async function runIndexPassInner(options, logger) {
302
319
  const allChunks = [];
303
320
  const oversizedChunks = [];
304
321
  const maxContentChars = options.config.description?.maxContentChars;
322
+ logger.info(`Description phase: ${deferredPreps.length} files with chunks to describe`);
305
323
  for (const prep of deferredPreps) {
306
324
  for (const chunk of prep.chunks) {
307
325
  if (chunk.metadata.contentType !== "image") {
@@ -469,6 +487,8 @@ async function runIndexPassInner(options, logger) {
469
487
  for (const prep of deferredPreps) {
470
488
  prep.textToEmbed = buildTextsToEmbed(prep.chunks, prep.relPath, prep.metaHeader ?? "", prep.docPrefix ?? "", prep.isImageFile ?? false);
471
489
  }
490
+ const totalDescribedChunks = deferredPreps.reduce((s, p) => s + (p.chunks?.length ?? 0), 0);
491
+ logger.info(`Description phase complete: ${totalDescribedChunks} chunks across ${deferredPreps.length} files`);
472
492
  }
473
493
  }
474
494
  // Cross-file embedding batch: collect all texts into a single queue,
@@ -530,12 +550,15 @@ async function runIndexPassInner(options, logger) {
530
550
  let embeddedDone = 0;
531
551
  const allTexts = embedQueue.map(item => item.text);
532
552
  let allEmbeddings = [];
553
+ const embedPhaseStart = Date.now();
533
554
  if (allTexts.length > 0) {
555
+ logger.info(`Embedding phase: ${allTexts.length} texts in batches of ${batchSize}...`);
534
556
  try {
535
557
  allEmbeddings = await embedBatch(options.embedder, allTexts, batchSize, "document", defaultConcurrency, (completed, total) => {
536
558
  embeddedDone = completed;
537
559
  logChunkProgress("Embedding", "", completed, total, embeddedDone, totalEmbedChunks);
538
560
  });
561
+ logger.info(`Embedding complete: ${allTexts.length} texts in ${((Date.now() - embedPhaseStart) / 1000).toFixed(1)}s`);
539
562
  }
540
563
  catch (err) {
541
564
  logger.warn(` Global embedding failed: ${err.message}`);
@@ -564,6 +587,8 @@ async function runIndexPassInner(options, logger) {
564
587
  }
565
588
  // ── Phase 3: Store + manifest update per file (parallel) ──────────────
566
589
  const filesToStore = prepared.filter((p) => !earlyWorkerResults.has(prepared.indexOf(p)) && p.chunks && (p.textToEmbed?.length ?? 0) > 0).length;
590
+ const storePhaseStart = Date.now();
591
+ logger.info(`Store phase: storing ${filesToStore} file(s) into vector database...`);
567
592
  let storedFiles = 0;
568
593
  const storeLimit = pLimit(options.config.indexing.concurrency);
569
594
  const storeResults = await Promise.all(prepared.map((prep, fi) => storeLimit(async () => {
@@ -648,6 +673,8 @@ async function runIndexPassInner(options, logger) {
648
673
  entry.size = size;
649
674
  }
650
675
  }
676
+ const storePhaseSec = ((Date.now() - storePhaseStart) / 1000).toFixed(1);
677
+ logger.info(`Store phase complete: ${storedFiles} files stored in ${storePhaseSec}s (${stats.totalChunks} total chunks)`);
651
678
  aggregateStats(stats, finalResults);
652
679
  // Update timestamps; advance lastGitCommit ONLY on a complete pass
653
680
  manifest.lastIndexedAt = Date.now();
@@ -696,6 +723,19 @@ async function runIndexPassInner(options, logger) {
696
723
  // a successful swap this points to the new data; after an abort it's the old).
697
724
  await saveManifest(options.storePath, manifest);
698
725
  await options.keywordIndex?.save(options.storePath);
726
+ // Compact fragments and prune old version manifests so countRows() can't
727
+ // hang on accumulated versions from many add/delete cycles.
728
+ if (!aborted()) {
729
+ logger.info("Optimizing vector store (compacting fragments, pruning old versions)...");
730
+ const optimizeStart = Date.now();
731
+ try {
732
+ await effectiveStore.optimize?.();
733
+ logger.info(`Vector store optimized in ${((Date.now() - optimizeStart) / 1000).toFixed(1)}s`);
734
+ }
735
+ catch (err) {
736
+ logger.warn(`Vector store optimization failed: ${err.message}`);
737
+ }
738
+ }
699
739
  // Persist description cache
700
740
  await descCache.save();
701
741
  // Count from the store — after a successful swap, the original handle has
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @fileoverview Single source of truth for the mandatory OpenCodeRAG tool-usage guidance
3
+ * that is injected into both the runtime system prompt (via plugin.ts) and the AGENTS.md
4
+ * directive (via init-helpers.ts).
5
+ */
6
+ /** Sentinel marker that opens the managed section. */
7
+ export declare const BEGIN_MARKER = "<!-- BEGIN opencode-rag -->";
8
+ /** Sentinel marker that closes the managed section. */
9
+ export declare const END_MARKER = "<!-- END opencode-rag -->";
10
+ /**
11
+ * The always-on mandatory guidance lines — tool list, decision tree,
12
+ * proactive triggers, and anti-patterns. Used verbatim by the runtime
13
+ * system prompt injector.
14
+ */
15
+ export declare const MANDATORY_GUIDANCE_LINES: readonly string[];
16
+ /**
17
+ * The conditional quirk-capture enforcement lines. Only included when
18
+ * `memory.promptEnforcement` is true.
19
+ */
20
+ export declare const QUIRK_ENFORCEMENT_LINES: readonly string[];
21
+ /**
22
+ * Build the flat guidance lines array for the runtime system-prompt injector.
23
+ * Returns a new array each call.
24
+ */
25
+ export declare function buildSystemGuidanceLines(opts: {
26
+ promptEnforcement: boolean;
27
+ }): string[];
28
+ /**
29
+ * Build the markdown-formatted AGENTS.md directive section, wrapped in sentinel
30
+ * markers so `mergeAgentsMdContent` can replace it in place on re-runs.
31
+ */
32
+ export declare function buildAgentsMdDirective(opts: {
33
+ promptEnforcement: boolean;
34
+ }): string;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @fileoverview Single source of truth for the mandatory OpenCodeRAG tool-usage guidance
3
+ * that is injected into both the runtime system prompt (via plugin.ts) and the AGENTS.md
4
+ * directive (via init-helpers.ts).
5
+ */
6
+ /** Sentinel marker that opens the managed section. */
7
+ export const BEGIN_MARKER = "<!-- BEGIN opencode-rag -->";
8
+ /** Sentinel marker that closes the managed section. */
9
+ export const END_MARKER = "<!-- END opencode-rag -->";
10
+ /**
11
+ * The always-on mandatory guidance lines — tool list, decision tree,
12
+ * proactive triggers, and anti-patterns. Used verbatim by the runtime
13
+ * system prompt injector.
14
+ */
15
+ export const MANDATORY_GUIDANCE_LINES = [
16
+ "MANDATORY: OpenCodeRAG tools MUST be used before any code task:",
17
+ "- `search_semantic(query)`: retrieve relevant code chunks. Call BEFORE planning, editing, or answering. Accepts `pathHints` and `languageHints`.",
18
+ "- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
19
+ "- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
20
+ "- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
21
+ "- `recall_quirks(query)`: query experiential quirk memory (gotchas, preferences, decisions). Call when you hit an error or need to recall known pitfalls.",
22
+ "- `add_quirk(content)`: store a new experiential memory. Call when you discover a non-obvious fact, gotcha, or coding convention.",
23
+ "",
24
+ "Decision tree — ALWAYS follow this order:",
25
+ "1. User mentions code behavior/architecture → `search_semantic(query)`",
26
+ "2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
27
+ "3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
28
+ "4. User asks a code question → `search_semantic` to gather context before answering",
29
+ "5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
30
+ "6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
31
+ "7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
32
+ "",
33
+ "Proactive triggers — you MUST call these tools when:",
34
+ "- User asks about code behavior, architecture, or implementation details",
35
+ "- User asks to edit, refactor, or fix code — call `find_usages` first",
36
+ "- User references files or functions you haven't read yet",
37
+ "- User says \"find\", \"search\", \"look up\", \"where is\", \"how does\"",
38
+ "- User refers to an image, screenshot, diagram, or visual asset",
39
+ "- Before answering ANY code-related question, retrieve context first",
40
+ "- Before reading ANY file, call `get_file_skeleton` to orient first",
41
+ "",
42
+ "Anti-patterns — NEVER do these:",
43
+ "- Reading full files without calling `get_file_skeleton` first (wastes tokens)",
44
+ "- Editing a function without calling `find_usages` first (breaks call sites)",
45
+ "- Answering code questions without calling `search_semantic` first (you guess at behavior)",
46
+ "- Using `grep`/`glob` when `search_semantic` would find the answer faster",
47
+ "- Treating image files as text — use `describe_image` instead of reading raw bytes",
48
+ "- Using `npx opencode-rag quirk` shell commands instead of the built-in `add_quirk` / `recall_quirks` tools (the tools are faster, already loaded in-process, and go through the trust monitor)",
49
+ ];
50
+ /**
51
+ * The conditional quirk-capture enforcement lines. Only included when
52
+ * `memory.promptEnforcement` is true.
53
+ */
54
+ export const QUIRK_ENFORCEMENT_LINES = [
55
+ "",
56
+ "MANDATORY quirk capture rules — you MUST call `add_quirk` when:",
57
+ "- A build, test, or type-check command fails and you resolve it",
58
+ "- You discover an undocumented library constraint, peer dep, or workaround",
59
+ "- You learn an environment-specific requirement (OS, tool version, etc.)",
60
+ "- You make a design decision that future sessions should remember",
61
+ "- You resolve a gotcha that cost more than one attempt",
62
+ "",
63
+ "Anti-pattern — NEVER finish a coding session without adding quirks for resolved errors.",
64
+ ];
65
+ /**
66
+ * Build the flat guidance lines array for the runtime system-prompt injector.
67
+ * Returns a new array each call.
68
+ */
69
+ export function buildSystemGuidanceLines(opts) {
70
+ const lines = [...MANDATORY_GUIDANCE_LINES];
71
+ if (opts.promptEnforcement) {
72
+ lines.push(...QUIRK_ENFORCEMENT_LINES);
73
+ }
74
+ return lines;
75
+ }
76
+ /**
77
+ * Build the markdown-formatted AGENTS.md directive section, wrapped in sentinel
78
+ * markers so `mergeAgentsMdContent` can replace it in place on re-runs.
79
+ */
80
+ export function buildAgentsMdDirective(opts) {
81
+ const lines = [
82
+ BEGIN_MARKER,
83
+ "## Code Navigation",
84
+ "",
85
+ "ALWAYS use OpenCodeRAG tools before reading or editing:",
86
+ "- **Search first** — `search_semantic(query)` instead of grep/glob",
87
+ "- **Skeleton before read** — `get_file_skeleton(filePath)` then read specific lines",
88
+ "- **Usages before edit** — `find_usages(symbolName)` before modifying any symbol",
89
+ "- **Images via describe** — `describe_image(filePath)` — never read raw bytes",
90
+ "- **Recall quirks** — `recall_quirks(query)` when you hit a known pitfall",
91
+ "- **Add quirks** — `add_quirk(content)` when you discover a non-obvious fact",
92
+ "",
93
+ "If no results, run `opencode-rag index`.",
94
+ "",
95
+ "### Decision tree — ALWAYS follow this order",
96
+ "1. User mentions code behavior/architecture → `search_semantic(query)`",
97
+ "2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
98
+ "3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
99
+ "4. User asks a code question → `search_semantic` to gather context before answering",
100
+ "5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
101
+ "6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
102
+ "7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
103
+ "",
104
+ "### Proactive triggers — you MUST call these tools when",
105
+ "- User asks about code behavior, architecture, or implementation details",
106
+ "- User asks to edit, refactor, or fix code — call `find_usages` first",
107
+ "- User references files or functions you haven't read yet",
108
+ "- User says \"find\", \"search\", \"look up\", \"where is\", \"how does\"",
109
+ "- User refers to an image, screenshot, diagram, or visual asset",
110
+ "- Before answering ANY code-related question, retrieve context first",
111
+ "- Before reading ANY file, call `get_file_skeleton` to orient first",
112
+ "",
113
+ "### Anti-patterns — NEVER do these",
114
+ "- Reading full files without calling `get_file_skeleton` first (wastes tokens)",
115
+ "- Editing a function without calling `find_usages` first (breaks call sites)",
116
+ "- Answering code questions without calling `search_semantic` first (you guess at behavior)",
117
+ "- Using `grep`/`glob` when `search_semantic` would find the answer faster",
118
+ "- Treating image files as text — use `describe_image` instead of reading raw bytes",
119
+ "- Using `npx opencode-rag quirk` shell commands instead of the built-in `add_quirk` / `recall_quirks` tools (the tools are faster, already loaded in-process, and go through the trust monitor)",
120
+ ];
121
+ if (opts.promptEnforcement) {
122
+ lines.push("", "### MANDATORY quirk capture rules — you MUST call `add_quirk` when", "- A build, test, or type-check command fails and you resolve it", "- You discover an undocumented library constraint, peer dep, or workaround", "- You learn an environment-specific requirement (OS, tool version, etc.)", "- You make a design decision that future sessions should remember", "- You resolve a gotcha that cost more than one attempt", "- NEVER finish a coding session without adding quirks for resolved errors.");
123
+ }
124
+ lines.push(END_MARKER);
125
+ return lines.join("\n");
126
+ }
127
+ //# sourceMappingURL=system-guidance.js.map
@@ -67,3 +67,41 @@ export declare function createDescribeImageTool(options: DescribeImageToolOption
67
67
  * @returns A tool definition suitable for OpenCode plugin registration.
68
68
  */
69
69
  export declare function createFindUsagesTool(options: FindUsagesToolOptions): ToolDefinition;
70
+ /** Options for creating the `recall_quirks` tool. */
71
+ export interface RecallQuirksToolOptions {
72
+ store: VectorStore;
73
+ embedder: EmbeddingProvider;
74
+ cfg: RagConfig;
75
+ keywordIndex: KeywordIndex;
76
+ storePath: string;
77
+ }
78
+ /**
79
+ * Create the `recall_quirks` tool.
80
+ *
81
+ * Queries experiential quirk memory (gotchas, preferences, decisions,
82
+ * environment constraints) via hybrid vector-keyword search filtered to
83
+ * `kind === "quirk"`. Results are re-weighted by confidence.
84
+ *
85
+ * @param options - Store, embedder, config, keyword index, store path.
86
+ * @returns A tool definition suitable for OpenCode plugin registration.
87
+ */
88
+ export declare function createRecallQuirksTool(options: RecallQuirksToolOptions): ToolDefinition;
89
+ /** Options for creating the `add_quirk` tool. */
90
+ export interface AddQuirkToolOptions {
91
+ store: VectorStore;
92
+ embedder: EmbeddingProvider;
93
+ cfg: RagConfig;
94
+ keywordIndex: KeywordIndex;
95
+ storePath: string;
96
+ }
97
+ /**
98
+ * Create the `add_quirk` tool.
99
+ *
100
+ * Stores a new quirk into experiential memory. The quirk is embedded as a
101
+ * synthetic chunk (kind="quirk") in the vector store and indexed in the
102
+ * keyword index. An audit trail is written to quirks.jsonl.
103
+ *
104
+ * @param options - Store, embedder, config, keyword index, store path.
105
+ * @returns A tool definition suitable for OpenCode plugin registration.
106
+ */
107
+ export declare function createAddQuirkTool(options: AddQuirkToolOptions): ToolDefinition;
@@ -20,6 +20,7 @@ import { Parser } from "web-tree-sitter";
20
20
  import { initParser, loadLanguage, walkTree } from "../chunker/grammar.js";
21
21
  import { readFileSync } from "node:fs";
22
22
  import { resolveWorkspacePath } from "./tool-args.js";
23
+ import { addQuirk, recallQuirks } from "../quirks/quirk-store.js";
23
24
  const SKELETON_CONFIGS = {
24
25
  ".ts": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration"] },
25
26
  ".tsx": { grammarName: "typescript", nodeTypes: ["function_declaration", "method_definition", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration", "arrow_function"] },
@@ -493,4 +494,130 @@ export function createFindUsagesTool(options) {
493
494
  },
494
495
  });
495
496
  }
497
+ /**
498
+ * Create the `recall_quirks` tool.
499
+ *
500
+ * Queries experiential quirk memory (gotchas, preferences, decisions,
501
+ * environment constraints) via hybrid vector-keyword search filtered to
502
+ * `kind === "quirk"`. Results are re-weighted by confidence.
503
+ *
504
+ * @param options - Store, embedder, config, keyword index, store path.
505
+ * @returns A tool definition suitable for OpenCode plugin registration.
506
+ */
507
+ export function createRecallQuirksTool(options) {
508
+ const { store, embedder, cfg, keywordIndex, storePath } = options;
509
+ return tool({
510
+ description: "Query experiential quirk memory — gotchas, preferences, decisions, and " +
511
+ "environment constraints discovered during previous sessions. " +
512
+ "Leverage when you encounter an error, need to recall how something works, " +
513
+ "or want to avoid known pitfalls. Results are filtered by confidence and " +
514
+ "relevance.",
515
+ args: {
516
+ query: tool.schema.string().min(1, "A search query is required."),
517
+ topK: tool.schema.number().int().min(1).max(25).optional(),
518
+ quirkType: tool.schema.string().optional(),
519
+ tags: tool.schema.array(tool.schema.string().min(1)).max(10).optional(),
520
+ },
521
+ async execute(args) {
522
+ try {
523
+ const deps = { embedder, store, keywordIndex, cfg, storePath };
524
+ const results = await recallQuirks(deps, args.query, {
525
+ topK: args.topK ?? 10,
526
+ quirkType: args.quirkType,
527
+ tags: args.tags,
528
+ });
529
+ if (results.length === 0) {
530
+ return {
531
+ title: "Quirk memory recall",
532
+ output: "No quirks matched your query.",
533
+ metadata: { tool: "recall_quirks", query: args.query, matches: 0 },
534
+ };
535
+ }
536
+ const lines = [];
537
+ lines.push(`**${results.length} quirk(s) found:**\n`);
538
+ for (const r of results) {
539
+ const m = r.chunk.metadata;
540
+ const badge = m.quirkType ? `[\`${m.quirkType}\`] ` : "";
541
+ const tags = m.tags && m.tags.length > 0 ? ` _(${m.tags.join(", ")})_` : "";
542
+ const confidence = m.confidence != null
543
+ ? ` confidence=${(m.confidence * 100).toFixed(0)}%`
544
+ : "";
545
+ const score = r.score.toFixed(3);
546
+ lines.push(`- ${badge}${r.chunk.content}${tags}`, ` _score=${score}${confidence}_`);
547
+ }
548
+ return {
549
+ title: `Quirk memory recall (${results.length})`,
550
+ output: lines.join("\n"),
551
+ metadata: {
552
+ tool: "recall_quirks",
553
+ query: args.query,
554
+ matches: results.length,
555
+ },
556
+ };
557
+ }
558
+ catch (err) {
559
+ return {
560
+ title: "Quirk memory recall",
561
+ output: `Recall failed: ${err instanceof Error ? err.message : String(err)}`,
562
+ metadata: { tool: "recall_quirks", error: String(err) },
563
+ };
564
+ }
565
+ },
566
+ });
567
+ }
568
+ /**
569
+ * Create the `add_quirk` tool.
570
+ *
571
+ * Stores a new quirk into experiential memory. The quirk is embedded as a
572
+ * synthetic chunk (kind="quirk") in the vector store and indexed in the
573
+ * keyword index. An audit trail is written to quirks.jsonl.
574
+ *
575
+ * @param options - Store, embedder, config, keyword index, store path.
576
+ * @returns A tool definition suitable for OpenCode plugin registration.
577
+ */
578
+ export function createAddQuirkTool(options) {
579
+ const { store, embedder, cfg, keywordIndex, storePath } = options;
580
+ return tool({
581
+ description: "Store a new experiential memory (quirk) — a gotcha, preference, decision, or " +
582
+ "environment constraint you discovered. The quirk is embedded and indexed so " +
583
+ "future sessions can recall it via `recall_quirks`. Use when you hit a " +
584
+ "non-obvious behavior, workaround, or coding convention.",
585
+ args: {
586
+ content: tool.schema.string().min(1, "Quirk content is required."),
587
+ quirkType: tool.schema
588
+ .string()
589
+ .optional(),
590
+ tags: tool.schema.array(tool.schema.string().min(1)).max(10).optional(),
591
+ sourceRef: tool.schema.string().optional(),
592
+ },
593
+ async execute(args) {
594
+ try {
595
+ const deps = { embedder, store, keywordIndex, cfg, storePath };
596
+ const quirk = await addQuirk(deps, {
597
+ content: args.content,
598
+ quirkType: args.quirkType,
599
+ tags: args.tags,
600
+ sourceRef: args.sourceRef,
601
+ });
602
+ return {
603
+ title: "Quirk added",
604
+ output: `**Quirk recorded** (id: \`${quirk.id}\`)\n\`${args.quirkType ?? "general"}\` | confidence=${(quirk.confidence * 100).toFixed(0)}% | tags=${(quirk.tags ?? []).join(", ") || "none"}`,
605
+ metadata: {
606
+ tool: "add_quirk",
607
+ quirkId: quirk.id,
608
+ quirkType: quirk.quirkType,
609
+ confidence: quirk.confidence,
610
+ },
611
+ };
612
+ }
613
+ catch (err) {
614
+ return {
615
+ title: "Quirk add",
616
+ output: `Failed to add quirk: ${err instanceof Error ? err.message : String(err)}`,
617
+ metadata: { tool: "add_quirk", error: String(err) },
618
+ };
619
+ }
620
+ },
621
+ });
622
+ }
496
623
  //# sourceMappingURL=tools.js.map