@theglitchking/semantic-memory 1.2.2 → 1.5.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/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +137 -0
- package/ROADMAP.md +5 -2
- package/commands/healthcheck.md +13 -11
- package/dist/{chunk-UQSASB7Q.js → chunk-2EELGTLX.js} +32 -3
- package/dist/chunk-2EELGTLX.js.map +1 -0
- package/dist/chunk-5ZKNO6ZD.js +96 -0
- package/dist/chunk-5ZKNO6ZD.js.map +1 -0
- package/dist/chunk-7M3JZOYH.js +105 -0
- package/dist/chunk-7M3JZOYH.js.map +1 -0
- package/dist/chunk-A3KYHKB5.js +176 -0
- package/dist/chunk-A3KYHKB5.js.map +1 -0
- package/dist/{chunk-CYWPDFNI.js → chunk-CSXZ7QUU.js} +4 -203
- package/dist/chunk-CSXZ7QUU.js.map +1 -0
- package/dist/chunk-FM26NFTR.js +272 -0
- package/dist/chunk-FM26NFTR.js.map +1 -0
- package/dist/chunk-NTYYS53O.js +91 -0
- package/dist/chunk-NTYYS53O.js.map +1 -0
- package/dist/{chunk-E2Z4J4MK.js → chunk-OWZWMB4P.js} +70 -2
- package/dist/chunk-OWZWMB4P.js.map +1 -0
- package/dist/chunk-S4KW7VH4.js +203 -0
- package/dist/chunk-S4KW7VH4.js.map +1 -0
- package/dist/{chunk-HODDRXJK.js → chunk-V45NJIHI.js} +4 -2
- package/dist/chunk-V45NJIHI.js.map +1 -0
- package/dist/chunk-WSU4QIPB.js +311 -0
- package/dist/chunk-WSU4QIPB.js.map +1 -0
- package/dist/cli/index.js +265 -65
- package/dist/cli/index.js.map +1 -1
- package/dist/core/index.d.ts +39 -2
- package/dist/core/index.js +17 -11
- package/dist/decay-ELJUKVQG.js +14 -0
- package/dist/dossier-2UG724LB.js +39 -0
- package/dist/healthcheck-IMB47LZE.js +19 -0
- package/dist/indexer-YW4O3VVM.js +8 -0
- package/dist/lexicon-5PXQEMUF.js +26 -0
- package/dist/lexicon-5PXQEMUF.js.map +1 -0
- package/dist/lint-XUQ7JDYX.js +16 -0
- package/dist/lint-XUQ7JDYX.js.map +1 -0
- package/dist/mcp/server.js +478 -40
- package/dist/mcp/server.js.map +1 -1
- package/dist/profile-LXUEF7TX.js +20 -0
- package/dist/profile-LXUEF7TX.js.map +1 -0
- package/dist/{schema-F3RTY6SQ.js → schema-ATFCLC6Y.js} +2 -2
- package/dist/schema-ATFCLC6Y.js.map +1 -0
- package/dist/{search-27WYAI24.js → search-AHKEFI2B.js} +2 -2
- package/dist/telemetry-GB2TEHVE.js +21 -0
- package/dist/telemetry-GB2TEHVE.js.map +1 -0
- package/hooks/vault-context.js +270 -11
- package/package.json +2 -1
- package/skills/vault-first/SKILL.md +18 -0
- package/dist/chunk-CAN2GWUG.js +0 -88
- package/dist/chunk-CAN2GWUG.js.map +0 -1
- package/dist/chunk-CYWPDFNI.js.map +0 -1
- package/dist/chunk-E2Z4J4MK.js.map +0 -1
- package/dist/chunk-HODDRXJK.js.map +0 -1
- package/dist/chunk-UQSASB7Q.js.map +0 -1
- package/dist/healthcheck-E4GI5RMZ.js +0 -14
- package/dist/indexer-KD6UTXZB.js +0 -8
- package/dist/lint-GMXQF4RW.js +0 -11
- /package/dist/{healthcheck-E4GI5RMZ.js.map → decay-ELJUKVQG.js.map} +0 -0
- /package/dist/{indexer-KD6UTXZB.js.map → dossier-2UG724LB.js.map} +0 -0
- /package/dist/{lint-GMXQF4RW.js.map → healthcheck-IMB47LZE.js.map} +0 -0
- /package/dist/{schema-F3RTY6SQ.js.map → indexer-YW4O3VVM.js.map} +0 -0
- /package/dist/{search-27WYAI24.js.map → search-AHKEFI2B.js.map} +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PROFILE_SECTIONS,
|
|
3
|
+
SPEAKER_PROFILE_PATH,
|
|
4
|
+
initSpeakerProfile,
|
|
5
|
+
readSpeakerProfile,
|
|
6
|
+
speakerProfileTemplate,
|
|
7
|
+
updateProfileSection
|
|
8
|
+
} from "./chunk-7M3JZOYH.js";
|
|
9
|
+
import "./chunk-WSU4QIPB.js";
|
|
10
|
+
import "./chunk-S4KW7VH4.js";
|
|
11
|
+
import "./chunk-PZ5AY32C.js";
|
|
12
|
+
export {
|
|
13
|
+
PROFILE_SECTIONS,
|
|
14
|
+
SPEAKER_PROFILE_PATH,
|
|
15
|
+
initSpeakerProfile,
|
|
16
|
+
readSpeakerProfile,
|
|
17
|
+
speakerProfileTemplate,
|
|
18
|
+
updateProfileSection
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=profile-LXUEF7TX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
installDefaultSchema,
|
|
3
3
|
loadSchema,
|
|
4
4
|
validateNote
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OWZWMB4P.js";
|
|
6
6
|
import "./chunk-PZ5AY32C.js";
|
|
7
7
|
export {
|
|
8
8
|
installDefaultSchema,
|
|
9
9
|
loadSchema,
|
|
10
10
|
validateNote
|
|
11
11
|
};
|
|
12
|
-
//# sourceMappingURL=schema-
|
|
12
|
+
//# sourceMappingURL=schema-ATFCLC6Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./chunk-25EIQWXL.js";
|
|
7
7
|
import {
|
|
8
8
|
Indexer
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-V45NJIHI.js";
|
|
10
10
|
import "./chunk-PZ5AY32C.js";
|
|
11
11
|
|
|
12
12
|
// src/cli/search.ts
|
|
@@ -199,4 +199,4 @@ function extractKeywords(query) {
|
|
|
199
199
|
export {
|
|
200
200
|
runSearch
|
|
201
201
|
};
|
|
202
|
-
//# sourceMappingURL=search-
|
|
202
|
+
//# sourceMappingURL=search-AHKEFI2B.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendEvent,
|
|
3
|
+
computeSelectionStats,
|
|
4
|
+
readSelectionLog,
|
|
5
|
+
recordSearchForCorrelation,
|
|
6
|
+
selectionLogPath,
|
|
7
|
+
telemetryEnabled,
|
|
8
|
+
wasRecentlySearched
|
|
9
|
+
} from "./chunk-NTYYS53O.js";
|
|
10
|
+
import "./chunk-S4KW7VH4.js";
|
|
11
|
+
import "./chunk-PZ5AY32C.js";
|
|
12
|
+
export {
|
|
13
|
+
appendEvent,
|
|
14
|
+
computeSelectionStats,
|
|
15
|
+
readSelectionLog,
|
|
16
|
+
recordSearchForCorrelation,
|
|
17
|
+
selectionLogPath,
|
|
18
|
+
telemetryEnabled,
|
|
19
|
+
wasRecentlySearched
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=telemetry-GB2TEHVE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/hooks/vault-context.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Fails open: any error logs to stderr and emits an empty additionalContext.
|
|
12
12
|
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
15
15
|
import { join, resolve, basename, dirname } from "node:path";
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
@@ -132,13 +132,42 @@ function queryRecentLogViaCli(cliBin, vaultPath, sinceIso, limit = 20) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// Injection hygiene (v1.4 Phase 2). The <vault-context> block is per-prompt
|
|
136
|
+
// overhead, so it earns its tokens or stays out:
|
|
137
|
+
// - score gate: if the best hit is weak, inject nothing (a "thanks"/"proceed"
|
|
138
|
+
// prompt shouldn't drag six 0.2-score hits into context). Tunable via
|
|
139
|
+
// SEMANTIC_MEMORY_INJECT_MIN_SCORE.
|
|
140
|
+
// - dedup by basename: collapse archive/x.md + archive/pre-restructure/x.md to
|
|
141
|
+
// one slot (keep the highest-scored), so retired-doc twins stop burning slots.
|
|
142
|
+
// - top-3, not top-6.
|
|
143
|
+
// - one compact instructions line — the full cite-or-deflect rule lives once in
|
|
144
|
+
// CLAUDE.md and the vault-first skill, not repeated in full every prompt.
|
|
145
|
+
const INJECT_MIN_SCORE = Number(process.env.SEMANTIC_MEMORY_INJECT_MIN_SCORE) || 0.35;
|
|
146
|
+
|
|
147
|
+
function dedupeByBasename(hits) {
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const h of hits) {
|
|
151
|
+
const base = String(h.path || "").split("/").pop();
|
|
152
|
+
if (seen.has(base)) continue;
|
|
153
|
+
seen.add(base);
|
|
154
|
+
out.push(h);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
export function formatContextBlock(source, query, hits) {
|
|
136
160
|
if (!hits || hits.length === 0) return "";
|
|
161
|
+
// Score gate: skip injection entirely when nothing is strongly relevant.
|
|
162
|
+
const topScore = Math.max(...hits.map((h) => (typeof h.score === "number" ? h.score : 0)));
|
|
163
|
+
if (topScore < INJECT_MIN_SCORE) return "";
|
|
164
|
+
|
|
165
|
+
const shown = dedupeByBasename(hits).slice(0, 3);
|
|
137
166
|
const lines = [];
|
|
138
167
|
lines.push(`<vault-context source="${source}" query="${escapeAttr(query)}">`);
|
|
139
168
|
lines.push(`The vault (semantic-memory) was proactively searched. Top hits:`);
|
|
140
169
|
lines.push("");
|
|
141
|
-
for (const h of
|
|
170
|
+
for (const h of shown) {
|
|
142
171
|
const score = typeof h.score === "number" ? h.score.toFixed(2) : "";
|
|
143
172
|
lines.push(`- \`${h.path}\`${score ? ` (score: ${score})` : ""}`);
|
|
144
173
|
if (h.snippet) {
|
|
@@ -147,11 +176,217 @@ export function formatContextBlock(source, query, hits) {
|
|
|
147
176
|
}
|
|
148
177
|
}
|
|
149
178
|
lines.push("");
|
|
150
|
-
lines.push(`Instructions:
|
|
179
|
+
lines.push(`Instructions: Apply cite-or-deflect ONLY to project prose lookups (how/why/where does X work here) — cite the hit paths, or say "not in vault". For meta/tool/debug/status/directive/conversational prompts, ignore this block silently. Do NOT narrate "X unrelated" or "not in vault" on those.`);
|
|
151
180
|
lines.push(`</vault-context>`);
|
|
152
181
|
return lines.join("\n");
|
|
153
182
|
}
|
|
154
183
|
|
|
184
|
+
// --- Tier-1 lexicon query expansion (v1.4 Phase 6, deterministic) ---
|
|
185
|
+
// Before embedding a user utterance, substitute/augment it with the canonical
|
|
186
|
+
// targets of any learned alias phrases it contains, so "is the flaky thing fixed?"
|
|
187
|
+
// searches as "...the flaky thing... src/core/indexer.ts". Pure string/table
|
|
188
|
+
// lookup — no LLM, no spawn, reads the derived lexicon-cache.json only.
|
|
189
|
+
|
|
190
|
+
function normalizePhrase(s) {
|
|
191
|
+
return String(s || "").toLowerCase().trim().replace(/\s+/g, " ");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readLexiconAliases(projectRoot) {
|
|
195
|
+
try {
|
|
196
|
+
const cachePath = join(projectRoot, ".claude", ".semantic-memory", "lexicon-cache.json");
|
|
197
|
+
if (!existsSync(cachePath)) return [];
|
|
198
|
+
const data = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
199
|
+
return Array.isArray(data?.aliases) ? data.aliases : [];
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function expandQueryViaLexicon(projectRoot, query) {
|
|
206
|
+
const aliases = readLexiconAliases(projectRoot);
|
|
207
|
+
if (aliases.length === 0) return query;
|
|
208
|
+
const nq = normalizePhrase(query);
|
|
209
|
+
const canonicals = [];
|
|
210
|
+
for (const a of aliases) {
|
|
211
|
+
const phrases = Array.isArray(a.phrases) ? a.phrases : [];
|
|
212
|
+
if (phrases.some((p) => p && nq.includes(normalizePhrase(p)))) {
|
|
213
|
+
if (a.canonical && !canonicals.includes(a.canonical)) canonicals.push(a.canonical);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return canonicals.length ? `${query} ${canonicals.join(" ")}` : query;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// --- Two-hop retrieval (v1.5 Phase 8): utterance → entity → dossier ---
|
|
220
|
+
// When the prompt names a dossier entity (or one of its aliases), inject the
|
|
221
|
+
// dossier head — Purpose + Current state — FIRST, ahead of semantic hits. This is
|
|
222
|
+
// the "resident expert pulls the component's file" move: entity-first, not file-first.
|
|
223
|
+
// Deterministic table lookup against the derived dossier-cache.json — no LLM, no spawn.
|
|
224
|
+
|
|
225
|
+
function readDossierCache(projectRoot) {
|
|
226
|
+
try {
|
|
227
|
+
const cachePath = join(projectRoot, ".claude", ".semantic-memory", "dossier-cache.json");
|
|
228
|
+
if (!existsSync(cachePath)) return [];
|
|
229
|
+
const data = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
230
|
+
return Array.isArray(data?.dossiers) ? data.dossiers : [];
|
|
231
|
+
} catch {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function resolveDossierForPrompt(projectRoot, query) {
|
|
237
|
+
const dossiers = readDossierCache(projectRoot);
|
|
238
|
+
if (dossiers.length === 0) return null;
|
|
239
|
+
const nq = normalizePhrase(query);
|
|
240
|
+
let best = null;
|
|
241
|
+
let bestLen = 0;
|
|
242
|
+
for (const d of dossiers) {
|
|
243
|
+
const keys = [d.entity, ...(Array.isArray(d.aliases) ? d.aliases : [])]
|
|
244
|
+
.map(normalizePhrase)
|
|
245
|
+
.filter(Boolean);
|
|
246
|
+
for (const k of keys) {
|
|
247
|
+
if (nq.includes(k) && k.length > bestLen) {
|
|
248
|
+
best = d;
|
|
249
|
+
bestLen = k.length;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return best;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function formatDossierHead(dossier) {
|
|
257
|
+
if (!dossier || !dossier.path) return "";
|
|
258
|
+
const lines = [];
|
|
259
|
+
lines.push(`<vault-dossier entity="${escapeAttr(dossier.entity || "")}" path="${escapeAttr(dossier.path)}">`);
|
|
260
|
+
lines.push(`This utterance names a tracked entity. Its dossier is the first place to look:`);
|
|
261
|
+
if (dossier.purpose) lines.push(`- purpose: ${String(dossier.purpose).replace(/\s+/g, " ").slice(0, 240)}`);
|
|
262
|
+
if (dossier.current_state) lines.push(`- current state: ${String(dossier.current_state).replace(/\s+/g, " ").slice(0, 240)}`);
|
|
263
|
+
lines.push(`Read \`${dossier.path}\` (its Incident log is the fix history) before answering about ${dossier.entity}.`);
|
|
264
|
+
lines.push(`</vault-dossier>`);
|
|
265
|
+
return lines.join("\n");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --- Session paging (v1.5 Phase 10): memory as the context window's swap space ---
|
|
269
|
+
// SessionStart pages IN a curated digest — latest session digest, active task,
|
|
270
|
+
// dossier current-states, mode — instead of a broad whole-vault semantic sweep.
|
|
271
|
+
// Fewer tokens, more signal. Falls back to search when there's nothing to page in.
|
|
272
|
+
|
|
273
|
+
/** Newest sessions/*.md digest note: its title + first content paragraph. */
|
|
274
|
+
function readLatestSessionDigest(vaultPath) {
|
|
275
|
+
try {
|
|
276
|
+
const dir = join(vaultPath, "sessions");
|
|
277
|
+
if (!existsSync(dir)) return null;
|
|
278
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
279
|
+
if (files.length === 0) return null;
|
|
280
|
+
let newest = null;
|
|
281
|
+
let newestMs = -1;
|
|
282
|
+
for (const f of files) {
|
|
283
|
+
const p = join(dir, f);
|
|
284
|
+
try {
|
|
285
|
+
const ms = statSync(p).mtimeMs;
|
|
286
|
+
if (ms > newestMs) { newestMs = ms; newest = p; }
|
|
287
|
+
} catch { /* skip */ }
|
|
288
|
+
}
|
|
289
|
+
if (!newest) return null;
|
|
290
|
+
const raw = readFileSync(newest, "utf8");
|
|
291
|
+
const body = raw.replace(/^---[\s\S]*?---\s*/, ""); // strip frontmatter
|
|
292
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
293
|
+
const title = titleMatch ? titleMatch[1].trim() : basename(newest).replace(/\.md$/, "");
|
|
294
|
+
const para = body
|
|
295
|
+
.replace(/^#.*$/m, "")
|
|
296
|
+
.split(/\n\s*\n/)
|
|
297
|
+
.map((s) => s.trim())
|
|
298
|
+
.find((s) => s && !s.startsWith("#"));
|
|
299
|
+
return {
|
|
300
|
+
path: `sessions/${basename(newest)}`,
|
|
301
|
+
title,
|
|
302
|
+
head: para ? para.replace(/\s+/g, " ").slice(0, 300) : "",
|
|
303
|
+
};
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Dossier current-state one-liners from the derived cache (capped). */
|
|
310
|
+
function readDossierStates(projectRoot, cap = 6) {
|
|
311
|
+
const dossiers = readDossierCache(projectRoot);
|
|
312
|
+
return dossiers
|
|
313
|
+
.filter((d) => d && d.current_state)
|
|
314
|
+
.slice(0, cap)
|
|
315
|
+
.map((d) => ({ entity: d.entity, path: d.path, state: String(d.current_state).replace(/\s+/g, " ").slice(0, 160) }));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Build the SessionStart curated digest block. Returns "" when there's nothing
|
|
320
|
+
* durable to page in (no digest note, no dossier states, no active task) — the
|
|
321
|
+
* caller then falls back to the broad semantic search.
|
|
322
|
+
*/
|
|
323
|
+
function buildSessionStartDigest(projectRoot, vaultPath) {
|
|
324
|
+
const digest = readLatestSessionDigest(vaultPath);
|
|
325
|
+
const states = readDossierStates(projectRoot);
|
|
326
|
+
const session = readActiveSession(projectRoot);
|
|
327
|
+
const mode = readMode(projectRoot);
|
|
328
|
+
if (!digest && states.length === 0 && !session) return "";
|
|
329
|
+
|
|
330
|
+
const lines = [`<vault-session-digest mode="${escapeAttr(mode)}">`];
|
|
331
|
+
lines.push(`Paged in from the vault (curated — not a broad search):`);
|
|
332
|
+
if (session) {
|
|
333
|
+
lines.push(`- active task: ${String(session.task || "(unnamed)").slice(0, 160)} (session ${session.id})`);
|
|
334
|
+
}
|
|
335
|
+
if (digest) {
|
|
336
|
+
lines.push(`- last session digest \`${digest.path}\`: ${digest.head || digest.title}`);
|
|
337
|
+
}
|
|
338
|
+
if (states.length > 0) {
|
|
339
|
+
lines.push(`- tracked entities (current state):`);
|
|
340
|
+
for (const s of states) lines.push(` · ${s.entity} — ${s.state} \`${s.path}\``);
|
|
341
|
+
}
|
|
342
|
+
lines.push(`Read the digest or a dossier before re-deriving prior context.`);
|
|
343
|
+
lines.push(`</vault-session-digest>`);
|
|
344
|
+
return lines.join("\n");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// --- Speaker profile (v1.5 Phase 11): the model of THIS human ---
|
|
348
|
+
// Read profile/speaker.md and inject a capped, placeholder-stripped head at
|
|
349
|
+
// SessionStart, so every session starts knowing how this person communicates.
|
|
350
|
+
|
|
351
|
+
function readSpeakerProfileBlock(vaultPath, maxLines = 24) {
|
|
352
|
+
try {
|
|
353
|
+
const abs = join(vaultPath, "profile", "speaker.md");
|
|
354
|
+
if (!existsSync(abs)) return "";
|
|
355
|
+
const raw = readFileSync(abs, "utf8");
|
|
356
|
+
const body = raw.replace(/^---[\s\S]*?---\s*/, "").trim();
|
|
357
|
+
const kept = [];
|
|
358
|
+
for (const line of body.split("\n")) {
|
|
359
|
+
if (/^#{1,6}\s/.test(line)) { kept.push(line.replace(/^#+\s*/, "").trim() + ":"); continue; }
|
|
360
|
+
const t = line.trim();
|
|
361
|
+
if (t === "" || /^_.*_$/.test(t)) continue; // drop placeholder prompts
|
|
362
|
+
kept.push(t);
|
|
363
|
+
}
|
|
364
|
+
// Drop trailing section labels with no content under them.
|
|
365
|
+
while (kept.length && kept[kept.length - 1].endsWith(":")) kept.pop();
|
|
366
|
+
if (kept.length === 0) return "";
|
|
367
|
+
const lines = [`<vault-speaker-profile path="profile/speaker.md">`];
|
|
368
|
+
lines.push(`How this human communicates (learned — apply, don't recite):`);
|
|
369
|
+
for (const l of kept.slice(0, maxLines)) lines.push(` ${l}`);
|
|
370
|
+
lines.push(`</vault-speaker-profile>`);
|
|
371
|
+
return lines.join("\n");
|
|
372
|
+
} catch {
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Stop-time session-digest instruction: draft a durable digest of this session's
|
|
379
|
+
* decisions/resolutions/task-state as a PROPOSAL (decision Q10 — automatic draft,
|
|
380
|
+
* human-reviewable, graduated via synthesize_promote). Appended to the session-close
|
|
381
|
+
* prompt so a formal session boundary always offers to file its memory.
|
|
382
|
+
*/
|
|
383
|
+
function formatSessionDigestPrompt(session) {
|
|
384
|
+
return [
|
|
385
|
+
"",
|
|
386
|
+
`After finishing, page this session's durable state to the vault: call \`mcp__semantic-vault__synthesize_note\` with \`from_session: true\`, \`proposal: true\`, \`proposal_subdir: "sessions"\`, and \`suggested_path: "sessions/${session.id}.md"\` — capturing decisions made, resolutions (symptom→cause→fix), and remaining task state. It files as a reviewable proposal; \`synthesize_promote\` graduates it if worth keeping. This is memory paging, not busywork: next session pages it back in instead of re-deriving.`,
|
|
387
|
+
].join("\n");
|
|
388
|
+
}
|
|
389
|
+
|
|
155
390
|
function escapeAttr(s) {
|
|
156
391
|
return String(s).replace(/["<>&]/g, (c) => ({ '"': """, "<": "<", ">": ">", "&": "&" })[c]);
|
|
157
392
|
}
|
|
@@ -271,6 +506,9 @@ const CAPTURE_CUES = [
|
|
|
271
506
|
// (a/the gotcha, gotcha:, gotchas, gotcha is/was/here/with) so bare filler doesn't misfire.
|
|
272
507
|
/\b(?:a|the|one|another)\s+gotcha\b|\bgotcha[:s]\b|\bgotcha (?:is|was|here|with)\b|\bworkaround\b|\bhack\b/i,
|
|
273
508
|
/\bnew convention\b|\bfrom now on\b|\bgoing forward\b/i,
|
|
509
|
+
// Speaker-calibration corrections (v1.5 Phase 11): the user telling you how THEY
|
|
510
|
+
// talk — "no, when I say X I mean Y", "what I mean by X is", "I actually meant".
|
|
511
|
+
/\bwhen i say\b[\s\S]{0,40}\bi mean\b|\bwhat i mean by\b|\bi actually meant\b|\bwhen i said\b[\s\S]{0,40}\bi meant\b/i,
|
|
274
512
|
];
|
|
275
513
|
|
|
276
514
|
// Cue detection must scan the user's own prose, NOT quoted machinery. Pasting
|
|
@@ -367,16 +605,25 @@ async function handleSessionStart(projectRoot, vaultPath, cliBin) {
|
|
|
367
605
|
logMaybeModeChange(cliBin, vaultPath, priorMode, "vault-first");
|
|
368
606
|
}
|
|
369
607
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
608
|
+
// Phase 10: prefer a curated digest (last session digest + active task + dossier
|
|
609
|
+
// current-states) over a broad whole-vault semantic sweep. Fewer tokens, more
|
|
610
|
+
// signal. Only fall back to the seeded search when there's nothing to page in.
|
|
611
|
+
const digestBlock = buildSessionStartDigest(projectRoot, vaultPath);
|
|
612
|
+
let searchBlock = "";
|
|
613
|
+
if (!digestBlock) {
|
|
614
|
+
const query = sessionSeedQuery(projectRoot);
|
|
615
|
+
debug(`sessionstart seed query: "${query}"`);
|
|
616
|
+
const hits = runSearchCli(cliBin, ["--notes", vaultPath, "--limit", "6", query], 30_000);
|
|
617
|
+
searchBlock = hits && hits.length > 0 ? formatContextBlock("sessionstart", query, hits) : "";
|
|
618
|
+
} else {
|
|
619
|
+
debug("sessionstart: paging in curated digest (skipping broad search)");
|
|
620
|
+
}
|
|
373
621
|
|
|
374
622
|
// State-delta preload — 14-day window per Phase 4.5 design.
|
|
375
623
|
const sinceMs = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
|
376
624
|
const sinceIso = new Date(sinceMs).toISOString();
|
|
377
625
|
const recentEntries = queryRecentLogViaCli(cliBin, vaultPath, sinceIso, 30);
|
|
378
626
|
|
|
379
|
-
const searchBlock = hits && hits.length > 0 ? formatContextBlock("sessionstart", query, hits) : "";
|
|
380
627
|
const stateBlock = formatStateDeltaBlock(sinceIso, recentEntries);
|
|
381
628
|
|
|
382
629
|
// Phase 8 (drift detection): inline fast-tier check. Hard requirements: <100ms total,
|
|
@@ -387,7 +634,9 @@ async function handleSessionStart(projectRoot, vaultPath, cliBin) {
|
|
|
387
634
|
return "";
|
|
388
635
|
});
|
|
389
636
|
|
|
390
|
-
const
|
|
637
|
+
const profileBlock = readSpeakerProfileBlock(vaultPath);
|
|
638
|
+
|
|
639
|
+
const parts = [driftBlock, profileBlock, stateBlock, digestBlock, searchBlock].filter(Boolean);
|
|
391
640
|
emit("SessionStart", parts.join("\n\n"));
|
|
392
641
|
}
|
|
393
642
|
|
|
@@ -572,6 +821,7 @@ async function handleStop(projectRoot) {
|
|
|
572
821
|
} else {
|
|
573
822
|
lines.push(`Before ending: either run \`mcp__semantic-vault__session_run\` with at least one verification command (tests/lint), or call \`mcp__semantic-vault__session_finish\` with \`verified: false\` and a \`reason\` waiving verification (e.g. doc-only edits). session_finish refuses without one of these paths.`);
|
|
574
823
|
}
|
|
824
|
+
lines.push(formatSessionDigestPrompt(session));
|
|
575
825
|
lines.push(`</vault-session-close>`);
|
|
576
826
|
resetCapturePending(projectRoot);
|
|
577
827
|
emitStop(lines.join("\n"), true);
|
|
@@ -627,6 +877,7 @@ async function handleStop(projectRoot) {
|
|
|
627
877
|
}
|
|
628
878
|
lines.push("");
|
|
629
879
|
lines.push(`Before ending: for each still-unsynthesized item above, either call \`mcp__semantic-vault__synthesize_note\` to file it with provenance, or explicitly acknowledge that it was already captured / not worth capturing.`);
|
|
880
|
+
lines.push(`If a cue is a speaker correction (how the user talks — "when I say X I mean Y"), update the speaker profile via \`mcp__semantic-vault__manage_profile\` (action:update_section) instead of synthesize_note.`);
|
|
630
881
|
lines.push(`</vault-capture-prompt>`);
|
|
631
882
|
resetCapturePending(projectRoot);
|
|
632
883
|
emitStop(lines.join("\n"), true);
|
|
@@ -654,18 +905,26 @@ async function handlePrompt(projectRoot, vaultPath, cliBin, input) {
|
|
|
654
905
|
emit("UserPromptSubmit", "");
|
|
655
906
|
return;
|
|
656
907
|
}
|
|
657
|
-
|
|
908
|
+
// Tier-1 lexicon expansion: search with the alias-expanded query, but keep the
|
|
909
|
+
// original prompt for display in the block.
|
|
910
|
+
const searchQuery = expandQueryViaLexicon(projectRoot, prompt);
|
|
911
|
+
const hits = runSearchCli(cliBin, ["--notes", vaultPath, "--limit", "8", searchQuery], 30_000);
|
|
658
912
|
state.recent = [fp, ...recent].slice(0, 10);
|
|
659
913
|
saveFingerprints(projectRoot, state);
|
|
660
914
|
const cue = detectCaptureCue(prompt);
|
|
661
915
|
if (cue) {
|
|
662
916
|
addCapturePending(projectRoot, prompt, cue);
|
|
663
917
|
}
|
|
918
|
+
// Two-hop: if the utterance names a tracked entity, lead with its dossier head.
|
|
919
|
+
const dossier = resolveDossierForPrompt(projectRoot, prompt);
|
|
920
|
+
const dossierHead = dossier ? formatDossierHead(dossier) : "";
|
|
664
921
|
if (!hits || hits.length === 0) {
|
|
665
|
-
|
|
922
|
+
// Even with no semantic hits, a resolved dossier is worth injecting.
|
|
923
|
+
emit("UserPromptSubmit", dossierHead);
|
|
666
924
|
return;
|
|
667
925
|
}
|
|
668
|
-
const
|
|
926
|
+
const searchBlock = formatContextBlock("prompt", prompt.slice(0, 120), hits);
|
|
927
|
+
const block = [dossierHead, searchBlock].filter(Boolean).join("\n");
|
|
669
928
|
emit("UserPromptSubmit", block);
|
|
670
929
|
}
|
|
671
930
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theglitchking/semantic-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Unified memory-layer plugin for Claude Code. One MCP server, multiple typed corpora (vault, code, plans, docs, research, project-map), unified cross-corpus knowledge graph, conditional tool registration, in-process drift detection, and human↔code translation. Rebrand of @theglitchking/semantic-sidekick at v1.0 — see CHANGELOG for migration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/core/index.js",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"dev": "tsup --watch",
|
|
37
37
|
"test": "vitest run",
|
|
38
38
|
"test:watch": "vitest",
|
|
39
|
+
"eval": "vitest run test/retrieval-eval",
|
|
39
40
|
"lint": "tsc --noEmit",
|
|
40
41
|
"prepublishOnly": "npm run build",
|
|
41
42
|
"postinstall": "node scripts/link-skills.js"
|
|
@@ -65,6 +65,24 @@ Do not silently pivot to web search, do not guess from training data, and do not
|
|
|
65
65
|
|
|
66
66
|
If you had to answer from outside the vault (web search, training knowledge, the user telling you something new), offer once to file a note: *"Worth capturing this in the vault as `gotchas/foo.md`?"* — then let the user decide. This is the ingest loop's entry point; don't force it, but don't skip it.
|
|
67
67
|
|
|
68
|
+
### 5. Propose an alias when you resolve the user's phrasing to a concrete artifact (v1.4 lexicon)
|
|
69
|
+
|
|
70
|
+
When the user refers to something in *their own words* — "the flaky thing", "the deploy dance", "that indexer race" — and you resolve it to a concrete artifact (a note path or code symbol), that resolution is worth keeping so the next occurrence routes in one hop. **Propose, don't auto-add** (a wrong alias silently misroutes every future query with that phrase):
|
|
71
|
+
|
|
72
|
+
- Offer once: *"Want me to remember that \"the flaky thing\" → `src/core/indexer.ts`? (manage_lexicon add)"*
|
|
73
|
+
- On confirmation, call `manage_lexicon({action:"add", canonical:"src/core/indexer.ts", phrases:["the flaky thing"]})`. Repeat observations bump `evidence_count` automatically.
|
|
74
|
+
- Only propose when you actually resolved a *vague* reference to a *specific* artifact this turn — not for literal names the user already spelled out.
|
|
75
|
+
- Scan the user's own prose only; never mine phrases from pasted tool output or quoted machinery.
|
|
76
|
+
- If `lint_vault({checks:["alias_conflicts"]})` flags a phrase mapping to two targets, surface it and ask the user which one wins — don't guess.
|
|
77
|
+
|
|
78
|
+
### 6. Rewrite a terse utterance before deciding the vault has nothing (v1.4, Tier 2)
|
|
79
|
+
|
|
80
|
+
The UserPromptSubmit hook already expands your query through the learned lexicon (Tier 1, deterministic). But when the utterance is terse or indexical — "it's doing the thing again", "did that work?", "is it fixed?" — the injected hits may still be weak because a two-word query has little to match expository note prose. Before concluding "not in vault":
|
|
81
|
+
|
|
82
|
+
- Rewrite the utterance using the current conversation's subject (what "it"/"that"/"the thing" refers to) into a concrete query, then re-search once via `search_hybrid`.
|
|
83
|
+
- Cap it at one rewrite round — don't loop searching for an exact match that doesn't exist (that's the bounded-rounds lesson).
|
|
84
|
+
- If the rewrite resolves the referent to a concrete artifact, that's a §5 alias-capture opportunity — offer it.
|
|
85
|
+
|
|
68
86
|
## Boundary conditions — when vault-first does NOT fire
|
|
69
87
|
|
|
70
88
|
- **Code-symbol lookups.** "Where is `parseAuth` defined?" → `Grep`. "Show me the `User` model." → `Glob`. If the answer is a file + line, the vault is the wrong tool.
|
package/dist/chunk-CAN2GWUG.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadSchema,
|
|
3
|
-
validateNote
|
|
4
|
-
} from "./chunk-E2Z4J4MK.js";
|
|
5
|
-
|
|
6
|
-
// src/core/lint.ts
|
|
7
|
-
import { readFile } from "fs/promises";
|
|
8
|
-
import { basename, join } from "path";
|
|
9
|
-
import { glob } from "glob";
|
|
10
|
-
import matter from "gray-matter";
|
|
11
|
-
async function lintVault(notesPath, opts = {}) {
|
|
12
|
-
const schema = opts.schemaOverride ?? await loadSchema(notesPath);
|
|
13
|
-
const files = await glob(opts.pathGlob ?? "**/*.md", {
|
|
14
|
-
cwd: notesPath,
|
|
15
|
-
ignore: ["node_modules/**", ".semantic-sidekick-index/**", "vault.schema.yml"]
|
|
16
|
-
});
|
|
17
|
-
const findings = [];
|
|
18
|
-
const knownNames = /* @__PURE__ */ new Set();
|
|
19
|
-
const rawByPath = /* @__PURE__ */ new Map();
|
|
20
|
-
for (const rel of files) {
|
|
21
|
-
try {
|
|
22
|
-
const raw = await readFile(join(notesPath, rel), "utf-8");
|
|
23
|
-
rawByPath.set(rel, raw);
|
|
24
|
-
knownNames.add(basename(rel).replace(/\.md$/, ""));
|
|
25
|
-
} catch {
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
for (const [rel, raw] of rawByPath) {
|
|
29
|
-
findings.push(...validateNote(rel, raw, schema, { todayIso: opts.todayIso }));
|
|
30
|
-
findings.push(...findBrokenLinks(rel, raw, knownNames));
|
|
31
|
-
}
|
|
32
|
-
const byRule = {
|
|
33
|
-
schema_violations: findings.filter((f) => f.rule === "schema_violations"),
|
|
34
|
-
missing_provenance: findings.filter((f) => f.rule === "missing_provenance"),
|
|
35
|
-
stale: findings.filter((f) => f.rule === "stale"),
|
|
36
|
-
broken_links: findings.filter((f) => f.rule === "broken_links")
|
|
37
|
-
};
|
|
38
|
-
const errors = findings.filter((f) => f.severity === "error").length;
|
|
39
|
-
const warnings = findings.filter((f) => f.severity === "warn").length;
|
|
40
|
-
return {
|
|
41
|
-
schemaPath: join(notesPath, "vault.schema.yml"),
|
|
42
|
-
findings,
|
|
43
|
-
byRule,
|
|
44
|
-
counts: { errors, warnings, filesLinted: files.length }
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function findBrokenLinks(relPath, rawFileContent, knownNames) {
|
|
48
|
-
const { content } = matter(rawFileContent);
|
|
49
|
-
const findings = [];
|
|
50
|
-
const stripped = content.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "");
|
|
51
|
-
const re = /\[\[([^\]\|]+)(?:\|[^\]]+)?\]\]/g;
|
|
52
|
-
const seen = /* @__PURE__ */ new Set();
|
|
53
|
-
let m;
|
|
54
|
-
while ((m = re.exec(stripped)) !== null) {
|
|
55
|
-
const target = m[1].trim().split("#")[0].split("/").pop().replace(/\.md$/, "");
|
|
56
|
-
if (seen.has(target)) continue;
|
|
57
|
-
seen.add(target);
|
|
58
|
-
if (!knownNames.has(target)) {
|
|
59
|
-
findings.push({
|
|
60
|
-
path: relPath,
|
|
61
|
-
rule: "broken_links",
|
|
62
|
-
severity: "warn",
|
|
63
|
-
message: `wikilink [[${target}]] has no matching note`
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return findings;
|
|
68
|
-
}
|
|
69
|
-
function formatLintReport(report, opts = {}) {
|
|
70
|
-
const target = opts.rule ? report.byRule[opts.rule] : report.findings;
|
|
71
|
-
if (target.length === 0) {
|
|
72
|
-
return `clean: 0 findings across ${report.counts.filesLinted} files`;
|
|
73
|
-
}
|
|
74
|
-
const lines = [
|
|
75
|
-
`${target.length} finding${target.length === 1 ? "" : "s"} (errors=${report.counts.errors}, warnings=${report.counts.warnings}, files=${report.counts.filesLinted})`,
|
|
76
|
-
""
|
|
77
|
-
];
|
|
78
|
-
for (const f of target) {
|
|
79
|
-
lines.push(`[${f.severity}] ${f.rule}: ${f.path} \u2014 ${f.message}`);
|
|
80
|
-
}
|
|
81
|
-
return lines.join("\n");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export {
|
|
85
|
-
lintVault,
|
|
86
|
-
formatLintReport
|
|
87
|
-
};
|
|
88
|
-
//# sourceMappingURL=chunk-CAN2GWUG.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/lint.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"gray-matter\";\nimport { loadSchema, validateNote, type LintFinding, type VaultSchema } from \"./schema.js\";\n\nexport interface LintReport {\n schemaPath: string;\n findings: LintFinding[];\n byRule: {\n schema_violations: LintFinding[];\n missing_provenance: LintFinding[];\n stale: LintFinding[];\n broken_links: LintFinding[];\n };\n counts: {\n errors: number;\n warnings: number;\n filesLinted: number;\n };\n}\n\nexport async function lintVault(\n notesPath: string,\n opts: { pathGlob?: string; schemaOverride?: VaultSchema; todayIso?: string } = {}\n): Promise<LintReport> {\n const schema = opts.schemaOverride ?? (await loadSchema(notesPath));\n const files = await glob(opts.pathGlob ?? \"**/*.md\", {\n cwd: notesPath,\n ignore: [\"node_modules/**\", \".semantic-sidekick-index/**\", \"vault.schema.yml\"],\n });\n\n const findings: LintFinding[] = [];\n // Build known-note set first, to make broken-link detection O(1) per link.\n const knownNames = new Set<string>();\n const rawByPath = new Map<string, string>();\n for (const rel of files) {\n try {\n const raw = await readFile(join(notesPath, rel), \"utf-8\");\n rawByPath.set(rel, raw);\n // Wikilink names match on basename-without-ext (obsidian-style)\n knownNames.add(basename(rel).replace(/\\.md$/, \"\"));\n } catch {\n /* skip */\n }\n }\n\n for (const [rel, raw] of rawByPath) {\n findings.push(...validateNote(rel, raw, schema, { todayIso: opts.todayIso }));\n findings.push(...findBrokenLinks(rel, raw, knownNames));\n }\n\n const byRule = {\n schema_violations: findings.filter((f) => f.rule === \"schema_violations\"),\n missing_provenance: findings.filter((f) => f.rule === \"missing_provenance\"),\n stale: findings.filter((f) => f.rule === \"stale\"),\n broken_links: findings.filter((f) => f.rule === \"broken_links\"),\n };\n const errors = findings.filter((f) => f.severity === \"error\").length;\n const warnings = findings.filter((f) => f.severity === \"warn\").length;\n\n return {\n schemaPath: join(notesPath, \"vault.schema.yml\"),\n findings,\n byRule,\n counts: { errors, warnings, filesLinted: files.length },\n };\n}\n\nfunction findBrokenLinks(\n relPath: string,\n rawFileContent: string,\n knownNames: Set<string>\n): LintFinding[] {\n const { content } = matter(rawFileContent);\n const findings: LintFinding[] = [];\n // Match [[target]] and [[target|alias]] — skip fenced code and inline code.\n const stripped = content\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]+`/g, \"\");\n const re = /\\[\\[([^\\]\\|]+)(?:\\|[^\\]]+)?\\]\\]/g;\n const seen = new Set<string>();\n let m: RegExpExecArray | null;\n while ((m = re.exec(stripped)) !== null) {\n const target = m[1].trim().split(\"#\")[0].split(\"/\").pop()!.replace(/\\.md$/, \"\");\n if (seen.has(target)) continue;\n seen.add(target);\n if (!knownNames.has(target)) {\n findings.push({\n path: relPath,\n rule: \"broken_links\",\n severity: \"warn\",\n message: `wikilink [[${target}]] has no matching note`,\n });\n }\n }\n return findings;\n}\n\nexport function formatLintReport(report: LintReport, opts: { rule?: keyof LintReport[\"byRule\"] } = {}): string {\n const target = opts.rule ? report.byRule[opts.rule] : report.findings;\n if (target.length === 0) {\n return `clean: 0 findings across ${report.counts.filesLinted} files`;\n }\n const lines = [\n `${target.length} finding${target.length === 1 ? \"\" : \"s\"} (errors=${report.counts.errors}, warnings=${report.counts.warnings}, files=${report.counts.filesLinted})`,\n \"\",\n ];\n for (const f of target) {\n lines.push(`[${f.severity}] ${f.rule}: ${f.path} — ${f.message}`);\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|