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.
- package/ReadMe.md +41 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/init-helpers.d.ts +5 -1
- package/dist/cli/commands/init-helpers.js +12 -19
- package/dist/cli/commands/init.js +42 -8
- package/dist/cli/commands/quirk.d.ts +10 -0
- package/dist/cli/commands/quirk.js +141 -0
- package/dist/cli/commands/setup.js +15 -6
- package/dist/cli/commands/ui.js +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/core/config.d.ts +30 -0
- package/dist/core/config.js +26 -2
- package/dist/core/interfaces.d.ts +25 -1
- package/dist/core/runtime-overrides.d.ts +7 -0
- package/dist/core/runtime-overrides.js +15 -0
- package/dist/describer/anthropic.d.ts +4 -0
- package/dist/describer/anthropic.js +9 -2
- package/dist/describer/describer.d.ts +4 -0
- package/dist/describer/describer.js +8 -0
- package/dist/describer/gemini.d.ts +3 -0
- package/dist/describer/gemini.js +8 -2
- package/dist/indexer/pipeline.js +40 -0
- package/dist/opencode/system-guidance.d.ts +34 -0
- package/dist/opencode/system-guidance.js +127 -0
- package/dist/opencode/tools.d.ts +38 -0
- package/dist/opencode/tools.js +127 -0
- package/dist/plugin.js +204 -31
- package/dist/quirks/auto-capture.d.ts +14 -0
- package/dist/quirks/auto-capture.js +112 -0
- package/dist/quirks/monitor.d.ts +5 -0
- package/dist/quirks/monitor.js +23 -0
- package/dist/quirks/prompts.d.ts +1 -0
- package/dist/quirks/prompts.js +13 -0
- package/dist/quirks/quirk-store.d.ts +27 -0
- package/dist/quirks/quirk-store.js +217 -0
- package/dist/quirks/types.d.ts +20 -0
- package/dist/quirks/types.js +2 -0
- package/dist/retriever/keyword-index.js +16 -0
- package/dist/vectorstore/lancedb.d.ts +21 -11
- package/dist/vectorstore/lancedb.js +174 -18
- package/dist/vectorstore/memory.d.ts +2 -0
- package/dist/vectorstore/memory.js +6 -0
- package/dist/web/api.d.ts +3 -1
- package/dist/web/api.js +50 -1
- package/dist/web/server.d.ts +2 -1
- package/dist/web/server.js +3 -2
- package/dist/web/ui/index.html +163 -0
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -15,7 +15,7 @@ import { appendDebugLog } from "./core/fileLogger.js";
|
|
|
15
15
|
import { loadRuntimeOverrides, applyRuntimeOverrides } from "./core/runtime-overrides.js";
|
|
16
16
|
import { createBackgroundIndexer } from "./watcher.js";
|
|
17
17
|
import { createRagReadTool } from "./opencode/create-read-tool.js";
|
|
18
|
-
import { createFileSkeletonTool, createFindUsagesTool, createDescribeImageTool, } from "./opencode/tools.js";
|
|
18
|
+
import { createFileSkeletonTool, createFindUsagesTool, createDescribeImageTool, createRecallQuirksTool, createAddQuirkTool, } from "./opencode/tools.js";
|
|
19
19
|
import { resolveApiKey } from "./core/resolve-api-key.js";
|
|
20
20
|
import { consumePendingRagInjection } from "./core/rag-injection-flag.js";
|
|
21
21
|
import { loadDocProgress, markSubdirectoryDocumented } from "./core/doc-progress.js";
|
|
@@ -25,6 +25,9 @@ import { countTokens } from "./eval/token-counter.js";
|
|
|
25
25
|
import { checkForUpdate, getCurrentVersion, installLatestUpdate } from "./core/version-check.js";
|
|
26
26
|
import { loadAutoUpdateState, saveAutoUpdateState, shouldAttemptInstall } from "./core/auto-update-state.js";
|
|
27
27
|
import { destroyAllPooledConnections } from "./embedder/http.js";
|
|
28
|
+
import { listQuirks, lintQuirks, recallQuirks } from "./quirks/quirk-store.js";
|
|
29
|
+
import { autoCaptureQuirks } from "./quirks/auto-capture.js";
|
|
30
|
+
import { buildSystemGuidanceLines } from "./opencode/system-guidance.js";
|
|
28
31
|
import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
29
32
|
import path from "node:path";
|
|
30
33
|
import { fileURLToPath } from "node:url";
|
|
@@ -433,6 +436,10 @@ export function createRagHooks(options) {
|
|
|
433
436
|
// Track the current assistant message text per session for 2-message search queries.
|
|
434
437
|
const sessionAssistantMessageId = new Map();
|
|
435
438
|
const sessionAssistantText = new Map();
|
|
439
|
+
// Auto-capture maps: previous user request per session, tool results, and full transcript
|
|
440
|
+
const sessionPrevUserReq = new Map();
|
|
441
|
+
const sessionToolResults = new Map();
|
|
442
|
+
const sessionTranscript = new Map();
|
|
436
443
|
// Evaluation session logger — captures OpenCode events for analysis
|
|
437
444
|
const sessionLogger = createSessionLogger(options.storePath);
|
|
438
445
|
appendDebugLog(options.logFilePath, {
|
|
@@ -593,6 +600,40 @@ export function createRagHooks(options) {
|
|
|
593
600
|
error: err,
|
|
594
601
|
});
|
|
595
602
|
}
|
|
603
|
+
try {
|
|
604
|
+
const recallQuirksTool = createRecallQuirksTool({
|
|
605
|
+
store,
|
|
606
|
+
embedder,
|
|
607
|
+
cfg: effectiveCfg,
|
|
608
|
+
keywordIndex: keywordIndex,
|
|
609
|
+
storePath: options.storePath,
|
|
610
|
+
});
|
|
611
|
+
tools["recall_quirks"] = recallQuirksTool;
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
appendDebugLog(options.logFilePath, {
|
|
615
|
+
scope: "plugin",
|
|
616
|
+
message: "Failed to register recall_quirks tool",
|
|
617
|
+
error: err,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const addQuirkTool = createAddQuirkTool({
|
|
622
|
+
store,
|
|
623
|
+
embedder,
|
|
624
|
+
cfg: effectiveCfg,
|
|
625
|
+
keywordIndex: keywordIndex,
|
|
626
|
+
storePath: options.storePath,
|
|
627
|
+
});
|
|
628
|
+
tools["add_quirk"] = addQuirkTool;
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
appendDebugLog(options.logFilePath, {
|
|
632
|
+
scope: "plugin",
|
|
633
|
+
message: "Failed to register add_quirk tool",
|
|
634
|
+
error: err,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
596
637
|
if (readOverride) {
|
|
597
638
|
const readTool = createRagReadTool({
|
|
598
639
|
worktree: options.worktree,
|
|
@@ -651,8 +692,40 @@ export function createRagHooks(options) {
|
|
|
651
692
|
catch {
|
|
652
693
|
// Non-critical — must never throw
|
|
653
694
|
}
|
|
695
|
+
// Session-end extraction: when we see a non-message lifecycle event,
|
|
696
|
+
// attempt to extract quirks from the full transcript.
|
|
697
|
+
try {
|
|
698
|
+
if (!event.type.startsWith("message.")) {
|
|
699
|
+
const sessionID = event.sessionID;
|
|
700
|
+
if (sessionID) {
|
|
701
|
+
const transcript = sessionTranscript.get(sessionID);
|
|
702
|
+
if (transcript && transcript.length > 0) {
|
|
703
|
+
const effCfg = getEffectiveCfg();
|
|
704
|
+
const memCfg = effCfg.memory;
|
|
705
|
+
if (memCfg?.sessionEndExtraction && options.descriptionProvider) {
|
|
706
|
+
const qDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effCfg, storePath: options.storePath };
|
|
707
|
+
void autoCaptureQuirks(qDeps, options.descriptionProvider, transcript, effCfg, { captureAll: true }).catch(() => { });
|
|
708
|
+
}
|
|
709
|
+
boundedSet(sessionTranscript, sessionID, [], MAX_SESSION_MAP_SIZE);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
// Non-critical — must never throw
|
|
716
|
+
}
|
|
654
717
|
},
|
|
655
718
|
tool: tools,
|
|
719
|
+
async "tool.execute.after"(input, output) {
|
|
720
|
+
try {
|
|
721
|
+
const results = sessionToolResults.get(input.sessionID) ?? [];
|
|
722
|
+
results.push({ tool: input.tool, output: output.output ?? "" });
|
|
723
|
+
boundedSet(sessionToolResults, input.sessionID, results, MAX_SESSION_MAP_SIZE);
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
// Non-critical — must never throw
|
|
727
|
+
}
|
|
728
|
+
},
|
|
656
729
|
async "experimental.chat.system.transform"(_input, output) {
|
|
657
730
|
appendDebugLog(options.logFilePath, {
|
|
658
731
|
scope: "experimental.chat.system.transform",
|
|
@@ -660,36 +733,7 @@ export function createRagHooks(options) {
|
|
|
660
733
|
});
|
|
661
734
|
const cfg = getEffectiveCfg();
|
|
662
735
|
if (cfg.openCode.injectSystemPrompt !== false) {
|
|
663
|
-
const guidance =
|
|
664
|
-
"MANDATORY: OpenCodeRAG tools MUST be used before any code task:",
|
|
665
|
-
"- `search_semantic(query)`: retrieve relevant code chunks. Call BEFORE planning, editing, or answering. Accepts `pathHints` and `languageHints`.",
|
|
666
|
-
"- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
|
|
667
|
-
"- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
|
|
668
|
-
"- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
|
|
669
|
-
"",
|
|
670
|
-
"Decision tree — ALWAYS follow this order:",
|
|
671
|
-
"1. User mentions code behavior/architecture → `search_semantic(query)`",
|
|
672
|
-
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
673
|
-
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
674
|
-
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
675
|
-
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
676
|
-
"",
|
|
677
|
-
"Proactive triggers — you MUST call these tools when:",
|
|
678
|
-
"- User asks about code behavior, architecture, or implementation details",
|
|
679
|
-
"- User asks to edit, refactor, or fix code — call `find_usages` first",
|
|
680
|
-
"- User references files or functions you haven't read yet",
|
|
681
|
-
"- User says \"find\", \"search\", \"look up\", \"where is\", \"how does\"",
|
|
682
|
-
"- User refers to an image, screenshot, diagram, or visual asset",
|
|
683
|
-
"- Before answering ANY code-related question, retrieve context first",
|
|
684
|
-
"- Before reading ANY file, call `get_file_skeleton` to orient first",
|
|
685
|
-
"",
|
|
686
|
-
"Anti-patterns — NEVER do these:",
|
|
687
|
-
"- Reading full files without calling `get_file_skeleton` first (wastes tokens)",
|
|
688
|
-
"- Editing a function without calling `find_usages` first (breaks call sites)",
|
|
689
|
-
"- Answering code questions without calling `search_semantic` first (you guess at behavior)",
|
|
690
|
-
"- Using `grep`/`glob` when `search_semantic` would find the answer faster",
|
|
691
|
-
"- Treating image files as text — use `describe_image` instead of reading raw bytes",
|
|
692
|
-
];
|
|
736
|
+
const guidance = buildSystemGuidanceLines({ promptEnforcement: !!cfg.memory?.promptEnforcement });
|
|
693
737
|
output.system.unshift(guidance.join("\n"));
|
|
694
738
|
}
|
|
695
739
|
// Inject a one-time update prompt so the agent asks the user to install.
|
|
@@ -739,6 +783,30 @@ export function createRagHooks(options) {
|
|
|
739
783
|
});
|
|
740
784
|
if (text.length === 0)
|
|
741
785
|
return;
|
|
786
|
+
// Passive capture: process the completed exchange before overwriting session state
|
|
787
|
+
const prevUserReq = sessionPrevUserReq.get(input.sessionID) ?? "";
|
|
788
|
+
if (prevUserReq) {
|
|
789
|
+
const effCfg = getEffectiveCfg();
|
|
790
|
+
const memCfg = effCfg.memory;
|
|
791
|
+
if (options.descriptionProvider) {
|
|
792
|
+
const assistantText = sessionAssistantText.get(input.sessionID) ?? "";
|
|
793
|
+
const toolResults = sessionToolResults.get(input.sessionID) ?? [];
|
|
794
|
+
if (assistantText) {
|
|
795
|
+
const qDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effCfg, storePath: options.storePath };
|
|
796
|
+
const exchange = { userReq: prevUserReq, assistantText, toolResults };
|
|
797
|
+
if (memCfg?.passiveCapture) {
|
|
798
|
+
void autoCaptureQuirks(qDeps, options.descriptionProvider, [exchange], effCfg).catch(() => { });
|
|
799
|
+
}
|
|
800
|
+
if (memCfg?.sessionEndExtraction) {
|
|
801
|
+
const t = sessionTranscript.get(input.sessionID) ?? [];
|
|
802
|
+
t.push(exchange);
|
|
803
|
+
boundedSet(sessionTranscript, input.sessionID, t, MAX_SESSION_MAP_SIZE);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
sessionPrevUserReq.set(input.sessionID, text);
|
|
809
|
+
sessionToolResults.set(input.sessionID, []);
|
|
742
810
|
boundedSet(sessionLastMessage, input.sessionID, text, MAX_SESSION_MAP_SIZE);
|
|
743
811
|
// Handle /doc slash command
|
|
744
812
|
if (text.startsWith("/doc")) {
|
|
@@ -882,6 +950,77 @@ export function createRagHooks(options) {
|
|
|
882
950
|
}
|
|
883
951
|
return;
|
|
884
952
|
}
|
|
953
|
+
// Handle /quirk slash command
|
|
954
|
+
if (text.startsWith("/quirk")) {
|
|
955
|
+
const memoryCfg = getEffectiveCfg().memory;
|
|
956
|
+
if (!memoryCfg?.enabled) {
|
|
957
|
+
const parts = output?.parts ?? output?.message?.parts;
|
|
958
|
+
if (Array.isArray(parts) && parts.length > 0) {
|
|
959
|
+
const first = parts[0];
|
|
960
|
+
if (typeof first.text === "string") {
|
|
961
|
+
parts[0] = { ...first, text: "Quirk memory is not enabled. Set `memory.enabled` to `true` in opencode-rag.json." };
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const arg = text.slice(6).trim().toLowerCase();
|
|
967
|
+
const lines = [];
|
|
968
|
+
if (arg === "lint") {
|
|
969
|
+
const deps = { embedder, store, keywordIndex: keywordIndex, cfg: getEffectiveCfg(), storePath: options.storePath };
|
|
970
|
+
const issues = await lintQuirks(deps);
|
|
971
|
+
lines.push("## Quirk Lint", "");
|
|
972
|
+
if (issues.length === 0) {
|
|
973
|
+
lines.push("No issues found. All quirks look healthy.");
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
lines.push(`Found ${issues.length} issue(s):\n`);
|
|
977
|
+
for (const issue of issues) {
|
|
978
|
+
lines.push(`- ${issue}`);
|
|
979
|
+
}
|
|
980
|
+
lines.push("", "Fix the issues and consider updating or removing the flagged quirks.");
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
else if (arg.startsWith("add ")) {
|
|
984
|
+
lines.push("## Add Quirk", "", "Use the `add_quirk` tool to store a new experiential memory entry. Example:", "", '`content`: "npm install needs --legacy-peer-deps due to LanceDB peer dep conflicts"', '`quirkType`: "gotcha"', "`tags`: [\"npm\", \"lancedb\", \"installation\"]");
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
// Status / list
|
|
988
|
+
const deps = { embedder, store, keywordIndex: keywordIndex, cfg: getEffectiveCfg(), storePath: options.storePath };
|
|
989
|
+
const quirks = await listQuirks(deps);
|
|
990
|
+
lines.push("## Quirk Memory", "");
|
|
991
|
+
if (quirks.length === 0) {
|
|
992
|
+
lines.push("No quirks stored yet.");
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
lines.push(`${quirks.length} quirk(s) stored:\n`);
|
|
996
|
+
for (const q of quirks) {
|
|
997
|
+
const badge = q.quirkType ? `[\`${q.quirkType}\`] ` : "";
|
|
998
|
+
const tags = q.tags.length > 0 ? ` _(${q.tags.join(", ")})_` : "";
|
|
999
|
+
lines.push(`- ${badge}${q.content.slice(0, 100)}${tags} `);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const memCfg = getEffectiveCfg().memory;
|
|
1003
|
+
const flags = [
|
|
1004
|
+
memCfg?.autoInject ? "auto-inject" : "",
|
|
1005
|
+
memCfg?.passiveCapture ? "passive-capture" : "",
|
|
1006
|
+
memCfg?.promptEnforcement ? "prompt-enforce" : "",
|
|
1007
|
+
memCfg?.sessionEndExtraction ? "session-end" : "",
|
|
1008
|
+
].filter(Boolean);
|
|
1009
|
+
if (flags.length > 0) {
|
|
1010
|
+
lines.push(`Flags: ${flags.join(", ")}`);
|
|
1011
|
+
}
|
|
1012
|
+
lines.push("", "Commands:", "- `/quirk` — show this status", "- `/quirk lint` — health-check quirks", "- `/quirk add <text>` — instructions to use the add_quirk tool");
|
|
1013
|
+
}
|
|
1014
|
+
const quirkMsg = lines.join("\n");
|
|
1015
|
+
const parts = output?.parts ?? output?.message?.parts;
|
|
1016
|
+
if (Array.isArray(parts) && parts.length > 0) {
|
|
1017
|
+
const first = parts[0];
|
|
1018
|
+
if (typeof first.text === "string") {
|
|
1019
|
+
parts[0] = { ...first, text: quirkMsg };
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
885
1024
|
// Handle hotkey-triggered RAG injection (Ctrl+Enter / Ctrl+Alt+Enter)
|
|
886
1025
|
const pendingInjection = consumePendingRagInjection(options.storePath);
|
|
887
1026
|
if (pendingInjection) {
|
|
@@ -938,6 +1077,40 @@ export function createRagHooks(options) {
|
|
|
938
1077
|
});
|
|
939
1078
|
}
|
|
940
1079
|
}
|
|
1080
|
+
// Auto-inject quirks when memory.autoInject is enabled
|
|
1081
|
+
const memoryCfg = effectiveCfg.memory;
|
|
1082
|
+
if (memoryCfg?.autoInject) {
|
|
1083
|
+
try {
|
|
1084
|
+
const quirkDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effectiveCfg, storePath: options.storePath };
|
|
1085
|
+
const quirkResults = await recallQuirks(quirkDeps, searchQuery, { topK: 3 });
|
|
1086
|
+
if (quirkResults.length > 0) {
|
|
1087
|
+
const quirkLines = ["", "---", "⚠ **Quirk memory**", ""];
|
|
1088
|
+
for (const qr of quirkResults) {
|
|
1089
|
+
const m = qr.chunk.metadata;
|
|
1090
|
+
const badge = m.quirkType ? `[\`${m.quirkType}\`] ` : "";
|
|
1091
|
+
quirkLines.push(`- ${badge}${qr.chunk.content.slice(0, 200)}`);
|
|
1092
|
+
}
|
|
1093
|
+
const quirkBlock = quirkLines.join("\n");
|
|
1094
|
+
const parts = output?.parts ?? output?.message?.parts;
|
|
1095
|
+
if (Array.isArray(parts) && parts.length > 0) {
|
|
1096
|
+
const first = parts[0];
|
|
1097
|
+
if (typeof first.text === "string") {
|
|
1098
|
+
parts[0] = { ...first, text: first.text + "\n\n" + quirkBlock };
|
|
1099
|
+
}
|
|
1100
|
+
const msgParts = output?.message?.parts;
|
|
1101
|
+
if (Array.isArray(msgParts) && msgParts !== parts) {
|
|
1102
|
+
const mfirst = msgParts[0];
|
|
1103
|
+
if (typeof mfirst.text === "string") {
|
|
1104
|
+
msgParts[0] = { ...mfirst, text: mfirst.text + "\n\n" + quirkBlock };
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
// Non-critical — must never throw
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
941
1114
|
}
|
|
942
1115
|
appendDebugLog(options.logFilePath, {
|
|
943
1116
|
scope: "chat.message",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DescriptionProvider } from "../core/interfaces.js";
|
|
2
|
+
import type { RagConfig } from "../core/config.js";
|
|
3
|
+
import { type QuirkStoreDeps } from "./quirk-store.js";
|
|
4
|
+
export interface CaptureExchange {
|
|
5
|
+
userReq: string;
|
|
6
|
+
assistantText: string;
|
|
7
|
+
toolResults: {
|
|
8
|
+
tool: string;
|
|
9
|
+
output: string;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
export declare function autoCaptureQuirks(deps: QuirkStoreDeps, descriptionProvider: DescriptionProvider, exchanges: CaptureExchange[], cfg: RagConfig, opts?: {
|
|
13
|
+
captureAll?: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { addQuirk, recallQuirks, lexicalSimilarity } from "./quirk-store.js";
|
|
2
|
+
import { isQuirkAllowed } from "./monitor.js";
|
|
3
|
+
import { MEMORY_CAPTURE_SYSTEM_PROMPT } from "./prompts.js";
|
|
4
|
+
const ERROR_SIGNAL_RE = /(error|failure|fail|failed|err(?:or)?\s*:|stack\s*trace|exit\s+code\s*[1-9]|npm\s+ERR|build\s+failed|test\s+fail|cannot\s+find\s+module|command\s+failed|unresolved|conflict|peer.dep|not\s+found|enoent|eaccess|econnrefused)/i;
|
|
5
|
+
function detectErrorSignal(text) {
|
|
6
|
+
return ERROR_SIGNAL_RE.test(text);
|
|
7
|
+
}
|
|
8
|
+
function buildExchangeText(exchanges) {
|
|
9
|
+
const lines = [];
|
|
10
|
+
for (const ex of exchanges) {
|
|
11
|
+
lines.push("=== User ===");
|
|
12
|
+
lines.push(ex.userReq);
|
|
13
|
+
if (ex.toolResults.length > 0) {
|
|
14
|
+
for (const tr of ex.toolResults) {
|
|
15
|
+
lines.push(`--- Tool: ${tr.tool} ---`);
|
|
16
|
+
lines.push(tr.output.slice(0, 500));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
lines.push("=== Assistant ===");
|
|
20
|
+
lines.push(ex.assistantText);
|
|
21
|
+
}
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
}
|
|
24
|
+
function parseExtractionOutput(text) {
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const line of text.split("\n")) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed || trimmed === "NOTHING")
|
|
29
|
+
continue;
|
|
30
|
+
const pipeIdx = trimmed.indexOf("|");
|
|
31
|
+
if (pipeIdx <= 0)
|
|
32
|
+
continue;
|
|
33
|
+
const quirkType = trimmed.slice(0, pipeIdx).trim();
|
|
34
|
+
const content = trimmed.slice(pipeIdx + 1).trim();
|
|
35
|
+
if (!quirkType || !content || content.length > 200)
|
|
36
|
+
continue;
|
|
37
|
+
results.push({ quirkType, content });
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
async function extractQuirksFromText(provider, exchangeText) {
|
|
42
|
+
const raw = await provider.generateText(MEMORY_CAPTURE_SYSTEM_PROMPT, exchangeText);
|
|
43
|
+
return parseExtractionOutput(raw);
|
|
44
|
+
}
|
|
45
|
+
async function dedupCandidates(deps, exchangeHead, candidates, threshold) {
|
|
46
|
+
const existing = await recallQuirks(deps, exchangeHead, { topK: 5 });
|
|
47
|
+
if (existing.length === 0)
|
|
48
|
+
return candidates;
|
|
49
|
+
const existingContent = existing.map((r) => r.chunk.content);
|
|
50
|
+
return candidates.filter((c) => {
|
|
51
|
+
for (const ec of existingContent) {
|
|
52
|
+
if (lexicalSimilarity(c.content, ec) >= threshold) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export async function autoCaptureQuirks(deps, descriptionProvider, exchanges, cfg, opts) {
|
|
60
|
+
const memoryCfg = cfg.memory;
|
|
61
|
+
if (!memoryCfg)
|
|
62
|
+
return;
|
|
63
|
+
const maxPerTurn = memoryCfg.autoCaptureMaxPerTurn ?? 2;
|
|
64
|
+
const dedupThreshold = memoryCfg.autoCaptureDedupThreshold ?? 0.85;
|
|
65
|
+
const captureAll = opts?.captureAll ?? false;
|
|
66
|
+
if (!captureAll) {
|
|
67
|
+
const combined = exchanges.map((e) => e.userReq + "\n" + e.assistantText + "\n" + e.toolResults.map((t) => t.output).join("\n")).join("\n");
|
|
68
|
+
if (!detectErrorSignal(combined))
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const exchangeText = buildExchangeText(exchanges);
|
|
72
|
+
if (!exchangeText.trim())
|
|
73
|
+
return;
|
|
74
|
+
let candidates;
|
|
75
|
+
try {
|
|
76
|
+
candidates = await extractQuirksFromText(descriptionProvider, exchangeText);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (candidates.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
const exchangeHead = exchanges.map((e) => e.userReq).join(" ");
|
|
84
|
+
let deduped;
|
|
85
|
+
try {
|
|
86
|
+
deduped = await dedupCandidates(deps, exchangeHead, candidates, dedupThreshold);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
deduped = candidates;
|
|
90
|
+
}
|
|
91
|
+
let added = 0;
|
|
92
|
+
for (const c of deduped) {
|
|
93
|
+
if (added >= maxPerTurn)
|
|
94
|
+
break;
|
|
95
|
+
const allowed = isQuirkAllowed(c.content);
|
|
96
|
+
if (!allowed.ok)
|
|
97
|
+
continue;
|
|
98
|
+
try {
|
|
99
|
+
await addQuirk(deps, {
|
|
100
|
+
content: c.content,
|
|
101
|
+
quirkType: c.quirkType,
|
|
102
|
+
tags: ["auto-captured"],
|
|
103
|
+
confidence: 0.7,
|
|
104
|
+
});
|
|
105
|
+
added++;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// skip individual failures
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=auto-capture.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Patterns that are never allowed in quirk memory — the immutable trust boundary. */
|
|
2
|
+
const BLOCKED_PATTERNS = [
|
|
3
|
+
/\bskip\s+(?:\S+\s+)*?tests?\b/i,
|
|
4
|
+
/\bdisable\s+(lint|typecheck|strict)/i,
|
|
5
|
+
/\bignore\s+(all\s+)?errors/i,
|
|
6
|
+
/\brm\s+-rf\b/,
|
|
7
|
+
/\bforce\s+push\b/,
|
|
8
|
+
/\bgit\s+push\s+--force\b/,
|
|
9
|
+
/\bdelete\s+.+\.git/i,
|
|
10
|
+
/\btouch\s+(?:\.env|withheld)/i,
|
|
11
|
+
/\bbypass\s+(?:security|review|check)/i,
|
|
12
|
+
];
|
|
13
|
+
/** Check whether quirk content is allowed within the immutable environment boundary. */
|
|
14
|
+
export function isQuirkAllowed(content) {
|
|
15
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
16
|
+
const match = content.match(pattern);
|
|
17
|
+
if (match) {
|
|
18
|
+
return { ok: false, reason: `Content matches blocked pattern: "${match[0]}"` };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { ok: true };
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=monitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MEMORY_CAPTURE_SYSTEM_PROMPT: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const MEMORY_CAPTURE_SYSTEM_PROMPT = "You are a quirk-extraction assistant. Given a transcript excerpt of an AI agent resolving a " +
|
|
2
|
+
"coding problem, extract concise experiential quirks that are worth remembering.\n\n" +
|
|
3
|
+
"Rules:\n" +
|
|
4
|
+
"- Output exactly one line per quirk in the format: TYPE|content\n" +
|
|
5
|
+
"- TYPE must be one of: gotcha, preference, decision, environment-constraint\n" +
|
|
6
|
+
"- content is a single short sentence (max 200 chars)\n" +
|
|
7
|
+
"- If no quirks are present, output exactly: NOTHING\n" +
|
|
8
|
+
"- Do not include explanations, numbering, or markdown\n\n" +
|
|
9
|
+
"Examples:\n" +
|
|
10
|
+
"gotcha|npm install needs --legacy-peer-deps due to LanceDB peer dep conflicts\n" +
|
|
11
|
+
"preference|Use tsx over ts-node for running TypeScript scripts\n" +
|
|
12
|
+
"NOTHING";
|
|
13
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EmbeddingProvider, VectorStore, KeywordIndex, SearchResult } from "../core/interfaces.js";
|
|
2
|
+
import type { RagConfig } from "../core/config.js";
|
|
3
|
+
import type { Quirk, QuirkInput } from "./types.js";
|
|
4
|
+
/** Dependencies required by all quirk-store operations. */
|
|
5
|
+
export interface QuirkStoreDeps {
|
|
6
|
+
embedder: EmbeddingProvider;
|
|
7
|
+
store: VectorStore;
|
|
8
|
+
keywordIndex: KeywordIndex;
|
|
9
|
+
cfg: RagConfig;
|
|
10
|
+
storePath: string;
|
|
11
|
+
}
|
|
12
|
+
/** Add a new quirk to the vector store, keyword index, and audit log. */
|
|
13
|
+
export declare function addQuirk(deps: QuirkStoreDeps, input: QuirkInput): Promise<Quirk>;
|
|
14
|
+
/** Remove a quirk by its ID. */
|
|
15
|
+
export declare function removeQuirk(deps: QuirkStoreDeps, id: string): Promise<void>;
|
|
16
|
+
/** List all quirks sorted by lastObserved descending. */
|
|
17
|
+
export declare function listQuirks(deps: QuirkStoreDeps): Promise<Quirk[]>;
|
|
18
|
+
/** Recall quirks matching a query, with confidence re-weighting. */
|
|
19
|
+
export declare function recallQuirks(deps: QuirkStoreDeps, query: string, options?: {
|
|
20
|
+
topK?: number;
|
|
21
|
+
quirkType?: string;
|
|
22
|
+
tags?: string[];
|
|
23
|
+
}): Promise<SearchResult[]>;
|
|
24
|
+
/** Lint quirks for low confidence, staleness, duplicates, and orphan source refs. */
|
|
25
|
+
export declare function lintQuirks(deps: QuirkStoreDeps): Promise<string[]>;
|
|
26
|
+
/** Simple Jaccard-based lexical similarity (word overlap). */
|
|
27
|
+
export declare function lexicalSimilarity(a: string, b: string): number;
|