@theglitchking/semantic-memory 1.2.2 → 1.5.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 (65) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +154 -0
  4. package/ROADMAP.md +5 -2
  5. package/commands/healthcheck.md +13 -11
  6. package/dist/{chunk-UQSASB7Q.js → chunk-2EELGTLX.js} +32 -3
  7. package/dist/chunk-2EELGTLX.js.map +1 -0
  8. package/dist/chunk-5ZKNO6ZD.js +96 -0
  9. package/dist/chunk-5ZKNO6ZD.js.map +1 -0
  10. package/dist/chunk-7M3JZOYH.js +105 -0
  11. package/dist/chunk-7M3JZOYH.js.map +1 -0
  12. package/dist/chunk-A3KYHKB5.js +176 -0
  13. package/dist/chunk-A3KYHKB5.js.map +1 -0
  14. package/dist/{chunk-CYWPDFNI.js → chunk-CSXZ7QUU.js} +4 -203
  15. package/dist/chunk-CSXZ7QUU.js.map +1 -0
  16. package/dist/chunk-FM26NFTR.js +272 -0
  17. package/dist/chunk-FM26NFTR.js.map +1 -0
  18. package/dist/chunk-NTYYS53O.js +91 -0
  19. package/dist/chunk-NTYYS53O.js.map +1 -0
  20. package/dist/{chunk-E2Z4J4MK.js → chunk-OWZWMB4P.js} +70 -2
  21. package/dist/chunk-OWZWMB4P.js.map +1 -0
  22. package/dist/chunk-S4KW7VH4.js +203 -0
  23. package/dist/chunk-S4KW7VH4.js.map +1 -0
  24. package/dist/{chunk-HODDRXJK.js → chunk-V45NJIHI.js} +4 -2
  25. package/dist/chunk-V45NJIHI.js.map +1 -0
  26. package/dist/chunk-WSU4QIPB.js +311 -0
  27. package/dist/chunk-WSU4QIPB.js.map +1 -0
  28. package/dist/cli/index.js +265 -65
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/core/index.d.ts +39 -2
  31. package/dist/core/index.js +17 -11
  32. package/dist/decay-ELJUKVQG.js +14 -0
  33. package/dist/dossier-2UG724LB.js +39 -0
  34. package/dist/healthcheck-IMB47LZE.js +19 -0
  35. package/dist/indexer-YW4O3VVM.js +8 -0
  36. package/dist/lexicon-5PXQEMUF.js +26 -0
  37. package/dist/lexicon-5PXQEMUF.js.map +1 -0
  38. package/dist/lint-XUQ7JDYX.js +16 -0
  39. package/dist/lint-XUQ7JDYX.js.map +1 -0
  40. package/dist/mcp/server.js +478 -40
  41. package/dist/mcp/server.js.map +1 -1
  42. package/dist/profile-LXUEF7TX.js +20 -0
  43. package/dist/profile-LXUEF7TX.js.map +1 -0
  44. package/dist/{schema-F3RTY6SQ.js → schema-ATFCLC6Y.js} +2 -2
  45. package/dist/schema-ATFCLC6Y.js.map +1 -0
  46. package/dist/{search-27WYAI24.js → search-AHKEFI2B.js} +2 -2
  47. package/dist/telemetry-GB2TEHVE.js +21 -0
  48. package/dist/telemetry-GB2TEHVE.js.map +1 -0
  49. package/hooks/vault-context.js +270 -11
  50. package/package.json +16 -1
  51. package/skills/vault-first/SKILL.md +18 -0
  52. package/dist/chunk-CAN2GWUG.js +0 -88
  53. package/dist/chunk-CAN2GWUG.js.map +0 -1
  54. package/dist/chunk-CYWPDFNI.js.map +0 -1
  55. package/dist/chunk-E2Z4J4MK.js.map +0 -1
  56. package/dist/chunk-HODDRXJK.js.map +0 -1
  57. package/dist/chunk-UQSASB7Q.js.map +0 -1
  58. package/dist/healthcheck-E4GI5RMZ.js +0 -14
  59. package/dist/indexer-KD6UTXZB.js +0 -8
  60. package/dist/lint-GMXQF4RW.js +0 -11
  61. /package/dist/{healthcheck-E4GI5RMZ.js.map → decay-ELJUKVQG.js.map} +0 -0
  62. /package/dist/{indexer-KD6UTXZB.js.map → dossier-2UG724LB.js.map} +0 -0
  63. /package/dist/{lint-GMXQF4RW.js.map → healthcheck-IMB47LZE.js.map} +0 -0
  64. /package/dist/{schema-F3RTY6SQ.js.map → indexer-YW4O3VVM.js.map} +0 -0
  65. /package/dist/{search-27WYAI24.js.map → search-AHKEFI2B.js.map} +0 -0
@@ -1,19 +1,53 @@
1
+ import {
2
+ SPEAKER_PROFILE_PATH,
3
+ initSpeakerProfile,
4
+ readSpeakerProfile,
5
+ updateProfileSection
6
+ } from "../chunk-7M3JZOYH.js";
1
7
  import {
2
8
  FrontmatterManager,
3
9
  NoteCrud,
4
- SessionManager,
5
10
  TagManager,
6
11
  Watcher,
7
12
  applyPatch,
8
13
  buildIngestChangeSet,
9
14
  buildPromoteChangeSet,
10
15
  buildSynthesizeChangeSet,
11
- deriveSessionDir,
12
16
  inspectAgentsContract,
13
17
  regenDirectoryIndex,
14
18
  regenIndexesForPaths,
15
19
  regenerateAgentsContract
16
- } from "../chunk-CYWPDFNI.js";
20
+ } from "../chunk-CSXZ7QUU.js";
21
+ import {
22
+ lintVault
23
+ } from "../chunk-FM26NFTR.js";
24
+ import {
25
+ appendEvent,
26
+ readSelectionLog,
27
+ recordSearchForCorrelation,
28
+ wasRecentlySearched
29
+ } from "../chunk-NTYYS53O.js";
30
+ import {
31
+ addAlias,
32
+ compileLexicon,
33
+ expandQuery,
34
+ loadLexiconCache,
35
+ removeAlias
36
+ } from "../chunk-A3KYHKB5.js";
37
+ import {
38
+ appendIncident,
39
+ compileDossiers,
40
+ extractSection,
41
+ initDossier,
42
+ listDossiers,
43
+ resolveDossierPath,
44
+ setCurrentState
45
+ } from "../chunk-WSU4QIPB.js";
46
+ import {
47
+ SessionManager,
48
+ deriveProjectRoot,
49
+ deriveSessionDir
50
+ } from "../chunk-S4KW7VH4.js";
17
51
  import {
18
52
  Embedder,
19
53
  GraphBuilder,
@@ -22,17 +56,19 @@ import {
22
56
  } from "../chunk-25EIQWXL.js";
23
57
  import {
24
58
  Indexer
25
- } from "../chunk-HODDRXJK.js";
59
+ } from "../chunk-V45NJIHI.js";
26
60
  import {
27
61
  logEvent,
28
62
  logQuery
29
63
  } from "../chunk-JPQHNG3R.js";
30
- import {
31
- lintVault
32
- } from "../chunk-CAN2GWUG.js";
33
64
  import {
34
65
  installDefaultSchema
35
- } from "../chunk-E2Z4J4MK.js";
66
+ } from "../chunk-OWZWMB4P.js";
67
+ import {
68
+ computeDecay,
69
+ loadDecayConfig,
70
+ normalizeVerifiedDate
71
+ } from "../chunk-5ZKNO6ZD.js";
36
72
  import {
37
73
  __export
38
74
  } from "../chunk-PZ5AY32C.js";
@@ -43,10 +79,83 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
43
79
 
44
80
  // src/mcp/context.ts
45
81
  import { mkdir, readFile, writeFile } from "fs/promises";
82
+ import { join as join3 } from "path";
83
+ import { existsSync as existsSync3 } from "fs";
84
+
85
+ // src/core/path-class.ts
86
+ import { existsSync, readFileSync } from "fs";
46
87
  import { join } from "path";
47
- import { existsSync } from "fs";
88
+ import { parse as parseYaml } from "yaml";
89
+ import { minimatch } from "minimatch";
90
+ var DEFAULT_PATH_CLASS_CONFIG = {
91
+ enabled: true,
92
+ rules: [{ glob: "archive/**", multiplier: 0.3 }]
93
+ };
94
+ function pathClassMultiplier(path, config) {
95
+ if (!config.enabled) return 1;
96
+ for (const rule of config.rules) {
97
+ if (typeof rule.multiplier !== "number") continue;
98
+ if (minimatch(path, rule.glob)) return rule.multiplier;
99
+ }
100
+ return 1;
101
+ }
102
+ function loadPathClassConfig(vaultPath) {
103
+ try {
104
+ const schemaPath = join(vaultPath, "vault.schema.yml");
105
+ if (!existsSync(schemaPath)) return DEFAULT_PATH_CLASS_CONFIG;
106
+ const parsed = parseYaml(readFileSync(schemaPath, "utf-8"));
107
+ const p = parsed?.path_class;
108
+ if (!p || typeof p !== "object") return DEFAULT_PATH_CLASS_CONFIG;
109
+ return {
110
+ enabled: typeof p.enabled === "boolean" ? p.enabled : DEFAULT_PATH_CLASS_CONFIG.enabled,
111
+ rules: Array.isArray(p.rules) ? p.rules.filter((r) => !!r && typeof r.glob === "string" && typeof r.multiplier === "number") : DEFAULT_PATH_CLASS_CONFIG.rules
112
+ };
113
+ } catch {
114
+ return DEFAULT_PATH_CLASS_CONFIG;
115
+ }
116
+ }
117
+
118
+ // src/core/usage-boost.ts
119
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
120
+ import { join as join2 } from "path";
121
+ import { parse as parseYaml2 } from "yaml";
122
+ var DEFAULT_USAGE_BOOST_CONFIG = {
123
+ enabled: true,
124
+ cap: 1.5,
125
+ per_citation: 0.1
126
+ };
127
+ function loadUsageBoostConfig(vaultPath) {
128
+ try {
129
+ const schemaPath = join2(vaultPath, "vault.schema.yml");
130
+ if (!existsSync2(schemaPath)) return { ...DEFAULT_USAGE_BOOST_CONFIG };
131
+ const parsed = parseYaml2(readFileSync2(schemaPath, "utf-8"));
132
+ const u = parsed?.usage_boost;
133
+ if (!u || typeof u !== "object") return { ...DEFAULT_USAGE_BOOST_CONFIG };
134
+ return {
135
+ enabled: typeof u.enabled === "boolean" ? u.enabled : DEFAULT_USAGE_BOOST_CONFIG.enabled,
136
+ cap: typeof u.cap === "number" && u.cap >= 1 ? u.cap : DEFAULT_USAGE_BOOST_CONFIG.cap,
137
+ per_citation: typeof u.per_citation === "number" && u.per_citation >= 0 ? u.per_citation : DEFAULT_USAGE_BOOST_CONFIG.per_citation
138
+ };
139
+ } catch {
140
+ return { ...DEFAULT_USAGE_BOOST_CONFIG };
141
+ }
142
+ }
143
+ function computeUsageBoost(citations, config) {
144
+ if (!config.enabled || citations <= 0) return 1;
145
+ return Math.min(config.cap, 1 + citations * config.per_citation);
146
+ }
147
+ async function loadCitationCounts(vaultPath) {
148
+ const events = await readSelectionLog(vaultPath);
149
+ const counts = {};
150
+ for (const e of events) {
151
+ if (e.kind === "selection") counts[e.note_path] = (counts[e.note_path] ?? 0) + 1;
152
+ }
153
+ return counts;
154
+ }
155
+
156
+ // src/mcp/context.ts
48
157
  async function buildContext(notesPath, options = {}) {
49
- const indexPath = join(notesPath, ".semantic-sidekick-index");
158
+ const indexPath = join3(notesPath, ".semantic-sidekick-index");
50
159
  await mkdir(indexPath, { recursive: true });
51
160
  const indexer = new Indexer(notesPath);
52
161
  const embedder = new Embedder(options.model, options.workers, options.batchSize, options.quantized);
@@ -66,9 +175,9 @@ async function buildContext(notesPath, options = {}) {
66
175
  try {
67
176
  indexState = "loading";
68
177
  await embedder.init();
69
- const metaPath = join(indexPath, "meta.json");
178
+ const metaPath = join3(indexPath, "meta.json");
70
179
  let meta = null;
71
- if (existsSync(metaPath)) {
180
+ if (existsSync3(metaPath)) {
72
181
  try {
73
182
  meta = JSON.parse(await readFile(metaPath, "utf-8"));
74
183
  if (meta?.model && meta.model !== embedder.getModel()) {
@@ -168,7 +277,7 @@ async function buildContext(notesPath, options = {}) {
168
277
  newVector.save(indexPath),
169
278
  embedder.saveEmbeddings(cumulativeEmbeddings, indexPath),
170
279
  indexer.saveDocsCache(indexPath, newDocs),
171
- writeFile(join(indexPath, "meta.json"), JSON.stringify({
280
+ writeFile(join3(indexPath, "meta.json"), JSON.stringify({
172
281
  model: embedder.getModel(),
173
282
  dimensions: embedder.getDimensions(),
174
283
  totalChunks: allChunks.length,
@@ -212,6 +321,72 @@ async function buildContext(notesPath, options = {}) {
212
321
  if (doc?.loadPriority === void 0) return score;
213
322
  return score * (1 + (doc.loadPriority - 5) * 0.04);
214
323
  }
324
+ let decayConfig = null;
325
+ function getDecayConfig() {
326
+ if (!decayConfig) decayConfig = loadDecayConfig(notesPath);
327
+ return decayConfig;
328
+ }
329
+ function applyDecay(r) {
330
+ const cfg = getDecayConfig();
331
+ if (!cfg.enabled) return r;
332
+ const doc = docByPath.get(r.path);
333
+ if (!doc) return r;
334
+ const fm = doc.frontmatter ?? {};
335
+ const d = computeDecay({
336
+ type: typeof fm.type === "string" ? fm.type : void 0,
337
+ last_verified: normalizeVerifiedDate(fm.last_verified),
338
+ evergreen: fm.evergreen === true,
339
+ config: cfg
340
+ });
341
+ const scored = { ...r, score: r.score * d.multiplier };
342
+ if (d.multiplier < 1) {
343
+ return {
344
+ ...scored,
345
+ decay: {
346
+ multiplier: Math.round(d.multiplier * 1e3) / 1e3,
347
+ age_days: Math.round(d.age_days),
348
+ effective_half_life: d.effective_half_life,
349
+ reason: d.reason
350
+ }
351
+ };
352
+ }
353
+ return scored;
354
+ }
355
+ let pathClassConfig = null;
356
+ function getPathClassConfig() {
357
+ if (!pathClassConfig) pathClassConfig = loadPathClassConfig(notesPath);
358
+ return pathClassConfig;
359
+ }
360
+ function applyPathClass(r) {
361
+ const m = pathClassMultiplier(r.path, getPathClassConfig());
362
+ return m === 1 ? r : { ...r, score: r.score * m };
363
+ }
364
+ let usageBoostConfig = null;
365
+ function getUsageBoostConfig() {
366
+ if (!usageBoostConfig) usageBoostConfig = loadUsageBoostConfig(notesPath);
367
+ return usageBoostConfig;
368
+ }
369
+ let citationCounts = {};
370
+ let citationLoadedAtMs = 0;
371
+ const CITATION_TTL_MS = 15e3;
372
+ async function refreshUsageBoost() {
373
+ if (!getUsageBoostConfig().enabled) return;
374
+ const now = Date.now();
375
+ if (now - citationLoadedAtMs < CITATION_TTL_MS && citationLoadedAtMs > 0) return;
376
+ try {
377
+ citationCounts = await loadCitationCounts(notesPath);
378
+ citationLoadedAtMs = now;
379
+ } catch {
380
+ }
381
+ }
382
+ function applyUsageBoost(r) {
383
+ const cfg = getUsageBoostConfig();
384
+ if (!cfg.enabled) return r;
385
+ const citations = citationCounts[r.path] ?? 0;
386
+ const m = computeUsageBoost(citations, cfg);
387
+ if (m === 1) return r;
388
+ return { ...r, score: r.score * m, usage: { citations, multiplier: Math.round(m * 1e3) / 1e3 } };
389
+ }
215
390
  function applyDateFilter(results, modifiedAfter, modifiedBefore) {
216
391
  if (!modifiedAfter && !modifiedBefore) return results;
217
392
  const after = modifiedAfter ? new Date(modifiedAfter).getTime() : -Infinity;
@@ -248,6 +423,10 @@ async function buildContext(notesPath, options = {}) {
248
423
  indexingMessage,
249
424
  enrichResult,
250
425
  applyPriorityBoost,
426
+ applyDecay,
427
+ applyPathClass,
428
+ applyUsageBoost,
429
+ refreshUsageBoost,
251
430
  applyDateFilter
252
431
  };
253
432
  }
@@ -4294,10 +4473,19 @@ var coerce = {
4294
4473
  var NEVER = INVALID;
4295
4474
 
4296
4475
  // src/mcp/tools/search.ts
4476
+ async function logSearch(ctx, tool, query, enriched) {
4477
+ const results = enriched.map((r) => ({
4478
+ path: r.path,
4479
+ score: r.score,
4480
+ ...r.decay ? { decay: r.decay.multiplier } : {}
4481
+ }));
4482
+ recordSearchForCorrelation(ctx.notesPath, results.map((r) => r.path));
4483
+ await appendEvent(ctx.notesPath, { kind: "search", tool, query, results });
4484
+ }
4297
4485
  function registerSearchTools(server, ctx) {
4298
4486
  server.tool(
4299
4487
  "search_semantic",
4300
- "Vector similarity search \u2014 find notes similar to a query by meaning. Scores are boosted by load_priority when present.",
4488
+ "Vector similarity search \u2014 find notes similar to a query by meaning. Scores are boosted by load_priority and down-weighted by confidence decay (time since last_verified); decayed results carry a `decay` block. Set decay.enabled:false in vault.schema.yml to disable.",
4301
4489
  {
4302
4490
  query: external_exports.string(),
4303
4491
  limit: external_exports.coerce.number().optional().default(10),
@@ -4315,8 +4503,9 @@ function registerSearchTools(server, ctx) {
4315
4503
  );
4316
4504
  }
4317
4505
  const queryEmbed = await ctx.embedder.embed(query);
4506
+ await ctx.refreshUsageBoost();
4318
4507
  let results = vectorIndex.search(queryEmbed, limit * 3);
4319
- results = results.map((r) => ({ ...r, score: ctx.applyPriorityBoost(r.score, r.path) }));
4508
+ results = results.map((r) => ctx.applyUsageBoost(ctx.applyPathClass(ctx.applyDecay({ ...r, score: ctx.applyPriorityBoost(r.score, r.path) }))));
4320
4509
  results.sort((a, b) => b.score - a.score);
4321
4510
  results = ctx.applyDateFilter(results, modifiedAfter, modifiedBefore);
4322
4511
  const docByPath = ctx.getDocByPath();
@@ -4324,6 +4513,7 @@ function registerSearchTools(server, ctx) {
4324
4513
  if (tier) results = results.filter((r) => docByPath.get(r.path)?.tier === tier);
4325
4514
  if (domain) results = results.filter((r) => docByPath.get(r.path)?.domains?.includes(domain));
4326
4515
  const enriched = results.slice(0, limit).map((r) => ctx.enrichResult(r));
4516
+ await logSearch(ctx, "search_semantic", query, enriched);
4327
4517
  return ctx.textResponse(JSON.stringify(enriched, null, 2));
4328
4518
  }
4329
4519
  );
@@ -4373,7 +4563,7 @@ function registerSearchTools(server, ctx) {
4373
4563
  );
4374
4564
  server.tool(
4375
4565
  "search_hybrid",
4376
- "Combined semantic + graph search \u2014 vector results re-ranked by graph proximity and load_priority",
4566
+ "Combined semantic + graph search \u2014 vector results re-ranked by graph proximity and load_priority, and down-weighted by confidence decay (time since last_verified); decayed results carry a `decay` block.",
4377
4567
  {
4378
4568
  query: external_exports.string(),
4379
4569
  limit: external_exports.coerce.number().optional().default(10),
@@ -4391,16 +4581,20 @@ function registerSearchTools(server, ctx) {
4391
4581
  );
4392
4582
  }
4393
4583
  const queryEmbed = await ctx.embedder.embed(query);
4584
+ await ctx.refreshUsageBoost();
4394
4585
  const semanticResults = vectorIndex.search(queryEmbed, limit * 3);
4395
4586
  const graphResults = ctx.graph.searchGraph(query, 2);
4396
4587
  const graphPaths = new Set(graphResults.map((r) => r.path));
4397
- let hybrid = semanticResults.map((r) => ({
4398
- ...r,
4399
- score: ctx.applyPriorityBoost(
4400
- graphPaths.has(r.path) ? r.score * 1.3 : r.score,
4401
- r.path
4588
+ let hybrid = semanticResults.map(
4589
+ (r) => ctx.applyUsageBoost(
4590
+ ctx.applyPathClass(
4591
+ ctx.applyDecay({
4592
+ ...r,
4593
+ score: ctx.applyPriorityBoost(graphPaths.has(r.path) ? r.score * 1.3 : r.score, r.path)
4594
+ })
4595
+ )
4402
4596
  )
4403
- }));
4597
+ );
4404
4598
  hybrid.sort((a, b) => b.score - a.score);
4405
4599
  hybrid = ctx.applyDateFilter(hybrid, modifiedAfter, modifiedBefore);
4406
4600
  const docByPath = ctx.getDocByPath();
@@ -4408,6 +4602,7 @@ function registerSearchTools(server, ctx) {
4408
4602
  if (tier) hybrid = hybrid.filter((r) => docByPath.get(r.path)?.tier === tier);
4409
4603
  if (domain) hybrid = hybrid.filter((r) => docByPath.get(r.path)?.domains?.includes(domain));
4410
4604
  const enriched = hybrid.slice(0, limit).map((r) => ctx.enrichResult(r));
4605
+ await logSearch(ctx, "search_hybrid", query, enriched);
4411
4606
  return ctx.textResponse(JSON.stringify(enriched, null, 2));
4412
4607
  }
4413
4608
  );
@@ -4417,10 +4612,20 @@ function registerSearchTools(server, ctx) {
4417
4612
  function registerReadTools(server, ctx) {
4418
4613
  server.tool(
4419
4614
  "read_note",
4420
- "Read the full content of a specific note by path",
4421
- { path: external_exports.string() },
4422
- async ({ path }) => {
4615
+ "Read a note by path. Pass `section` (a heading name) to read only that heading's section instead of the whole file \u2014 cheaper when you only need one part (e.g. a dossier's 'Knobs' section).",
4616
+ { path: external_exports.string(), section: external_exports.string().optional().describe("Heading name \u2014 return only that section (heading through the next same-or-higher heading).") },
4617
+ async ({ path, section }) => {
4423
4618
  const content = await ctx.crud.read(path);
4619
+ await appendEvent(ctx.notesPath, {
4620
+ kind: "selection",
4621
+ note_path: path,
4622
+ via: "read_note",
4623
+ correlated: wasRecentlySearched(ctx.notesPath, path)
4624
+ });
4625
+ if (section) {
4626
+ const extracted = extractSection(content, section);
4627
+ return ctx.textResponse(extracted ?? `Section "${section}" not found in ${path}. Read without \`section\` for the full note.`);
4628
+ }
4424
4629
  return ctx.textResponse(content);
4425
4630
  }
4426
4631
  );
@@ -4528,7 +4733,7 @@ function registerWriteTools(server, ctx) {
4528
4733
 
4529
4734
  // src/mcp/tools/patch.ts
4530
4735
  import { readFile as readFile2 } from "fs/promises";
4531
- import { join as join2 } from "path";
4736
+ import { join as join4 } from "path";
4532
4737
  import matter from "gray-matter";
4533
4738
  function registerPatchTools(server, ctx) {
4534
4739
  if (ctx.options.readOnly) return;
@@ -4601,6 +4806,7 @@ function registerPatchTools(server, ctx) {
4601
4806
  decision_maker: external_exports.string().optional(),
4602
4807
  decided_on: external_exports.string().optional(),
4603
4808
  severity: external_exports.string().optional(),
4809
+ symptoms: external_exports.array(external_exports.string()).optional().describe("Verbatim symptom phrases in the user's own words (e.g. 'it hangs after the second reindex'). Stored as `symptoms:` frontmatter and symptom-keyed at index time so terse symptom queries match this note \u2014 the retrieval key for next time."),
4604
4810
  dry_run: external_exports.boolean().optional().default(false),
4605
4811
  proposal: external_exports.boolean().optional().default(false).describe("Write to proposals/<date>-<slug>.md with status:proposal instead of the canonical destination. Use for review-first workflows."),
4606
4812
  proposal_subdir: external_exports.string().optional().describe("Override the proposal subdirectory (default 'proposals'). Useful for source segregation, e.g. 'proposals/research'."),
@@ -4644,6 +4850,7 @@ ${verifLines.join("\n")}
4644
4850
  decision_maker: args.decision_maker,
4645
4851
  decided_on: args.decided_on,
4646
4852
  severity: args.severity,
4853
+ symptoms: args.symptoms,
4647
4854
  proposal: args.proposal,
4648
4855
  proposal_subdir: args.proposal_subdir
4649
4856
  });
@@ -4698,7 +4905,7 @@ ${verifLines.join("\n")}
4698
4905
  dry_run: external_exports.boolean().optional().default(false)
4699
4906
  },
4700
4907
  async (args) => {
4701
- const absoluteProposalPath = join2(ctx.notesPath, args.proposal_path);
4908
+ const absoluteProposalPath = join4(ctx.notesPath, args.proposal_path);
4702
4909
  let raw;
4703
4910
  try {
4704
4911
  raw = await readFile2(absoluteProposalPath, "utf-8");
@@ -4825,6 +5032,160 @@ ${verifLines.join("\n")}
4825
5032
  );
4826
5033
  }
4827
5034
  );
5035
+ server.tool(
5036
+ "verify_note",
5037
+ "Re-verify a note: stamp `last_verified` to today WITHOUT changing content or any other frontmatter field, and log a `verify` event. Resets the confidence-decay clock so the note ranks fresh again. Returns the note's new decay multiplier (typically 1.0).",
5038
+ { path: external_exports.string() },
5039
+ async ({ path }) => {
5040
+ let fm;
5041
+ try {
5042
+ fm = await ctx.frontmatterManager.get(path);
5043
+ } catch {
5044
+ return ctx.textResponse(`verify_note: note not found: ${path}`);
5045
+ }
5046
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5047
+ await ctx.frontmatterManager.update(path, { last_verified: today });
5048
+ await logEvent(ctx.notesPath, {
5049
+ kind: "verify",
5050
+ summary: `verified ${path}`,
5051
+ payload: { path, last_verified: today }
5052
+ });
5053
+ const d = computeDecay({
5054
+ type: typeof fm.type === "string" ? fm.type : void 0,
5055
+ last_verified: today,
5056
+ evergreen: fm.evergreen === true,
5057
+ config: loadDecayConfig(ctx.notesPath)
5058
+ });
5059
+ return ctx.textResponse(
5060
+ JSON.stringify({ path, last_verified: today, decay_multiplier: d.multiplier }, null, 2)
5061
+ );
5062
+ }
5063
+ );
5064
+ server.tool(
5065
+ "manage_lexicon",
5066
+ "Manage the learned human\u2192artifact lexicon (v1.4). `action`: add (upsert a canonical target + the human phrases that refer to it; bumps evidence_count on repeats), lookup (expand a query via matching alias phrases), list (all aliases), remove (by canonical), compile (rebuild the derived cache). The lexicon maps how THIS human names things to concrete paths/symbols.",
5067
+ {
5068
+ action: external_exports.enum(["add", "lookup", "list", "remove", "compile"]),
5069
+ canonical: external_exports.string().optional().describe("For add/remove: the concrete target (note path or code symbol)."),
5070
+ phrases: external_exports.array(external_exports.string()).optional().describe("For add: the human's verbatim phrases for the canonical target."),
5071
+ query: external_exports.string().optional().describe("For lookup: the utterance to expand via alias phrases."),
5072
+ source: external_exports.enum(["learned", "authored"]).optional().describe("For add: provenance of the binding (default learned).")
5073
+ },
5074
+ async ({ action, canonical, phrases, query, source }) => {
5075
+ switch (action) {
5076
+ case "add": {
5077
+ if (!canonical || !phrases || phrases.length === 0) return ctx.textResponse("manage_lexicon add requires `canonical` and non-empty `phrases`");
5078
+ const r = await addAlias(ctx.notesPath, { canonical, phrases, source });
5079
+ return ctx.textResponse(JSON.stringify(r, null, 2));
5080
+ }
5081
+ case "lookup": {
5082
+ if (!query) return ctx.textResponse("manage_lexicon lookup requires `query`");
5083
+ const aliases = await loadLexiconCache(ctx.notesPath);
5084
+ const { expanded, matched } = expandQuery(aliases, query);
5085
+ return ctx.textResponse(JSON.stringify({ query, expanded, matched }, null, 2));
5086
+ }
5087
+ case "list": {
5088
+ return ctx.textResponse(JSON.stringify(await loadLexiconCache(ctx.notesPath), null, 2));
5089
+ }
5090
+ case "remove": {
5091
+ if (!canonical) return ctx.textResponse("manage_lexicon remove requires `canonical`");
5092
+ const removed = await removeAlias(ctx.notesPath, canonical);
5093
+ return ctx.textResponse(removed ? `removed alias: ${canonical}` : `no alias for canonical: ${canonical}`);
5094
+ }
5095
+ case "compile": {
5096
+ const entries = await compileLexicon(ctx.notesPath);
5097
+ return ctx.textResponse(`compiled ${entries.length} alias(es) to lexicon-cache.json`);
5098
+ }
5099
+ }
5100
+ }
5101
+ );
5102
+ server.tool(
5103
+ "manage_dossier",
5104
+ "Manage entity dossiers (v1.5) \u2014 the per-component living notes that organize memory by entity instead of by file. `action`: init (scaffold a dossier for an entity with the fixed Purpose/Failure modes/Knobs/Incident log/Current state sections), append_incident (append a dated line to the Incident log \u2014 accretion, never a new note), set_state (replace the Current state one-liner), get (read a dossier by entity/alias/path), list (all dossiers with their current state). Knowledge about a critical component accretes IN PLACE here.",
5105
+ {
5106
+ action: external_exports.enum(["init", "append_incident", "set_state", "get", "list"]),
5107
+ entity: external_exports.string().optional().describe("For init/append_incident/set_state/get: the entity name, one of its aliases, or a dossier path."),
5108
+ aliases: external_exports.array(external_exports.string()).optional().describe("For init: the human's phrases for this entity (fed to the lexicon compiler for query expansion)."),
5109
+ purpose: external_exports.string().optional().describe("For init: seed the Purpose section instead of the placeholder."),
5110
+ seeded_from: external_exports.string().optional().describe("For init: provenance of the scaffold (e.g. a babel-fish project-map entry)."),
5111
+ incident: external_exports.string().optional().describe("For append_incident: what happened \u2192 cause \u2192 fix, in one line. Dated automatically."),
5112
+ state: external_exports.string().optional().describe("For set_state: the single freshest sentence about where the entity stands.")
5113
+ },
5114
+ async ({ action, entity, aliases, purpose, seeded_from, incident, state }) => {
5115
+ switch (action) {
5116
+ case "init": {
5117
+ if (!entity) return ctx.textResponse("manage_dossier init requires `entity`");
5118
+ const r = await initDossier(ctx.notesPath, entity, { aliases, purpose, seeded_from });
5119
+ if (r.created) await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
5120
+ });
5121
+ return ctx.textResponse(JSON.stringify(r, null, 2));
5122
+ }
5123
+ case "append_incident": {
5124
+ if (!entity || !incident) return ctx.textResponse("manage_dossier append_incident requires `entity` and `incident`");
5125
+ const r = await appendIncident(ctx.notesPath, entity, incident);
5126
+ if (!r) return ctx.textResponse(`no dossier for entity: ${entity} (run action:init first)`);
5127
+ await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
5128
+ });
5129
+ await logEvent(ctx.notesPath, { kind: "synthesis", summary: `dossier incident: ${entity}`, payload: { path: r.path, incident } }).catch(() => {
5130
+ });
5131
+ return ctx.textResponse(JSON.stringify(r, null, 2));
5132
+ }
5133
+ case "set_state": {
5134
+ if (!entity || !state) return ctx.textResponse("manage_dossier set_state requires `entity` and `state`");
5135
+ const r = await setCurrentState(ctx.notesPath, entity, state);
5136
+ if (!r) return ctx.textResponse(`no dossier for entity: ${entity} (run action:init first)`);
5137
+ await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
5138
+ });
5139
+ return ctx.textResponse(JSON.stringify(r, null, 2));
5140
+ }
5141
+ case "get": {
5142
+ if (!entity) return ctx.textResponse("manage_dossier get requires `entity`");
5143
+ const rel = await resolveDossierPath(ctx.notesPath, entity);
5144
+ if (!rel) return ctx.textResponse(`no dossier for entity: ${entity}`);
5145
+ const content = await readFile2(join4(ctx.notesPath, rel), "utf-8");
5146
+ return ctx.textResponse(JSON.stringify({ path: rel, content }, null, 2));
5147
+ }
5148
+ case "list": {
5149
+ await compileDossiers(ctx.notesPath);
5150
+ const entries = await listDossiers(ctx.notesPath);
5151
+ return ctx.textResponse(JSON.stringify(entries, null, 2));
5152
+ }
5153
+ }
5154
+ }
5155
+ );
5156
+ server.tool(
5157
+ "manage_profile",
5158
+ 'Manage the speaker profile (v1.5) \u2014 the model of how THIS human communicates (severity calibration, chronic omissions, verbosity preference, non-entity shorthand). `action`: init (scaffold profile/speaker.md with the fixed sections), get (read it), update_section (append a learned line to \u2014 or replace \u2014 one section; scaffolds first if absent). Update it when the user corrects an interpretation ("no, when I say X I mean Y"). One human, one profile.',
5159
+ {
5160
+ action: external_exports.enum(["init", "get", "update_section"]),
5161
+ section: external_exports.string().optional().describe("For update_section: the section heading (e.g. 'Severity calibration', 'Shorthand & terms')."),
5162
+ text: external_exports.string().optional().describe("For update_section: the learned line or replacement body."),
5163
+ mode: external_exports.enum(["append", "replace"]).optional().describe("For update_section: append a line (default) or replace the section body.")
5164
+ },
5165
+ async ({ action, section, text, mode }) => {
5166
+ switch (action) {
5167
+ case "init": {
5168
+ const r = await initSpeakerProfile(ctx.notesPath);
5169
+ if (r.created) await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
5170
+ });
5171
+ return ctx.textResponse(JSON.stringify(r, null, 2));
5172
+ }
5173
+ case "get": {
5174
+ const p = await readSpeakerProfile(ctx.notesPath);
5175
+ if (!p) return ctx.textResponse(`no speaker profile yet (run action:init or manage_profile update_section)`);
5176
+ const content = await readFile2(join4(ctx.notesPath, SPEAKER_PROFILE_PATH), "utf-8");
5177
+ return ctx.textResponse(JSON.stringify({ path: p.path, head: p.head, content }, null, 2));
5178
+ }
5179
+ case "update_section": {
5180
+ if (!section || !text) return ctx.textResponse("manage_profile update_section requires `section` and `text`");
5181
+ const r = await updateProfileSection(ctx.notesPath, section, text, mode ?? "append");
5182
+ await ctx.sessions.recordNotesTouched([r.path]).catch(() => {
5183
+ });
5184
+ return ctx.textResponse(JSON.stringify(r, null, 2));
5185
+ }
5186
+ }
5187
+ }
5188
+ );
4828
5189
  }
4829
5190
 
4830
5191
  // src/mcp/tools/lint.ts
@@ -4833,7 +5194,11 @@ var RULE_KEY_FOR_CHECK = {
4833
5194
  schema: "schema_violations",
4834
5195
  provenance: "missing_provenance",
4835
5196
  stale: "stale",
4836
- broken_links: "broken_links"
5197
+ broken_links: "broken_links",
5198
+ code_symbols: "code_symbols",
5199
+ decay_candidates: "decay_candidates",
5200
+ alias_conflicts: "alias_conflicts",
5201
+ decoys: "decoys"
4837
5202
  };
4838
5203
  function registerLintTools(server, ctx) {
4839
5204
  server.tool(
@@ -4874,13 +5239,19 @@ function registerLintTools(server, ctx) {
4874
5239
  );
4875
5240
  server.tool(
4876
5241
  "lint_vault",
4877
- "Run lint rules across the vault. Default returns the full LintReport with all findings grouped by rule. Pass `checks` to filter to specific rules (schema, provenance, stale, broken_links) \u2014 when filtered, byRule contains only the requested rules.",
5242
+ "Run lint rules across the vault. Default returns the full LintReport with all findings grouped by rule. Pass `checks` to filter to specific rules (schema, provenance, stale, broken_links, code_symbols, decay_candidates) \u2014 when filtered, byRule contains only the requested rules. Two checks are opt-in only (never in the default report): `code_symbols` flags inline-code file-path references that no longer exist under the project root (no-op outside a code repo); `decay_candidates` cross-references the selection log against current decay to surface frequently-retrieved-but-decayed notes (no-op with no selection log). `decoys` flags notes retrieved 3+ times but never cited - surfaced for review, never auto-down-ranked (v1.5 Q5).",
4878
5243
  {
4879
5244
  pathGlob: external_exports.string().optional(),
4880
- checks: external_exports.array(external_exports.enum(["schema", "provenance", "stale", "broken_links"])).optional().describe("Filter to specific lint rules. Omit for full report.")
5245
+ checks: external_exports.array(external_exports.enum(["schema", "provenance", "stale", "broken_links", "code_symbols", "decay_candidates", "alias_conflicts", "decoys"])).optional().describe("Filter to specific lint rules. Omit for full report (excludes the opt-in code_symbols / decay_candidates / alias_conflicts / decoys).")
4881
5246
  },
4882
5247
  async ({ pathGlob, checks }) => {
4883
- const report = await lintVault(ctx.notesPath, { pathGlob });
5248
+ const report = await lintVault(ctx.notesPath, {
5249
+ pathGlob,
5250
+ checkCodeSymbols: !!checks?.includes("code_symbols"),
5251
+ checkDecayCandidates: !!checks?.includes("decay_candidates"),
5252
+ checkAliasConflicts: !!checks?.includes("alias_conflicts"),
5253
+ checkDecoys: !!checks?.includes("decoys")
5254
+ });
4884
5255
  if (!checks || checks.length === 0) {
4885
5256
  return ctx.textResponse(JSON.stringify(report, null, 2));
4886
5257
  }
@@ -5138,6 +5509,10 @@ var CURRENT_TOOL_INVENTORY = [
5138
5509
  { name: "synthesize_promote", description: "Move a reviewed proposal note to its canonical destination, stripping proposal markers.", group: "patch" },
5139
5510
  { name: "ingest_source", description: "Build + apply a ChangeSet that ingests a source and the notes extracted from it.", group: "patch" },
5140
5511
  { name: "install_schema", description: "Bootstrap vault.schema.yml in the vault root with the default schema.", group: "patch" },
5512
+ { name: "verify_note", description: "Stamp last_verified to today (content unchanged) to reset the confidence-decay clock; logs a verify event.", group: "patch" },
5513
+ { name: "manage_lexicon", description: "Manage the learned human\u2192artifact lexicon (add/lookup/list/remove/compile aliases).", group: "patch" },
5514
+ { name: "manage_dossier", description: "Manage entity dossiers \u2014 per-component living notes (init/append_incident/set_state/get/list). Knowledge accretes in place.", group: "patch" },
5515
+ { name: "manage_profile", description: "Manage the speaker profile \u2014 how this human communicates (init/get/update_section).", group: "patch" },
5141
5516
  // lint
5142
5517
  { name: "find_schema_violations", description: "Scan for schema violations.", group: "lint", deprecated: true },
5143
5518
  { name: "find_missing_provenance", description: "Scan for notes lacking sources/derived_from.", group: "lint", deprecated: true },
@@ -5172,6 +5547,29 @@ var CURRENT_TOOL_INVENTORY = [
5172
5547
  ];
5173
5548
 
5174
5549
  // src/mcp/tools/contract.ts
5550
+ import { fileURLToPath } from "url";
5551
+ import { dirname, join as join5 } from "path";
5552
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
5553
+ function pluginVersion() {
5554
+ try {
5555
+ let dir = dirname(fileURLToPath(import.meta.url));
5556
+ for (let i = 0; i < 6; i++) {
5557
+ const pkgPath = join5(dir, "package.json");
5558
+ if (existsSync4(pkgPath)) {
5559
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
5560
+ if (pkg?.name === "@theglitchking/semantic-memory" && typeof pkg.version === "string") {
5561
+ return pkg.version;
5562
+ }
5563
+ }
5564
+ const parent = dirname(dir);
5565
+ if (parent === dir) break;
5566
+ dir = parent;
5567
+ }
5568
+ return "0.0.0";
5569
+ } catch {
5570
+ return "0.0.0";
5571
+ }
5572
+ }
5175
5573
  function registerContractTools(server, ctx) {
5176
5574
  if (ctx.options.readOnly) return;
5177
5575
  server.tool(
@@ -5195,7 +5593,7 @@ function registerContractTools(server, ctx) {
5195
5593
  ];
5196
5594
  const result = await regenerateAgentsContract({
5197
5595
  projectRoot: root,
5198
- pluginVersion: "1.1.0",
5596
+ pluginVersion: pluginVersion(),
5199
5597
  tools,
5200
5598
  modes,
5201
5599
  force
@@ -5310,6 +5708,43 @@ function registerSessionTools(server, ctx) {
5310
5708
  );
5311
5709
  }
5312
5710
 
5711
+ // src/core/tools-config.ts
5712
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
5713
+ import { join as join6 } from "path";
5714
+ import { parse as parseYaml3 } from "yaml";
5715
+ function loadToolsConfig(vaultPath) {
5716
+ try {
5717
+ const schemaPath = join6(vaultPath, "vault.schema.yml");
5718
+ if (!existsSync5(schemaPath)) return { conditional: false };
5719
+ const parsed = parseYaml3(readFileSync4(schemaPath, "utf-8"));
5720
+ return { conditional: parsed?.tools?.conditional === true };
5721
+ } catch {
5722
+ return { conditional: false };
5723
+ }
5724
+ }
5725
+ function readActiveMode(vaultPath) {
5726
+ const root = deriveProjectRoot(vaultPath);
5727
+ const candidates = [
5728
+ join6(root, ".claude", ".semantic-memory", "mode"),
5729
+ join6(root, ".claude", ".sidekick-mode")
5730
+ ];
5731
+ for (const p of candidates) {
5732
+ try {
5733
+ if (existsSync5(p)) {
5734
+ const v = readFileSync4(p, "utf-8").trim();
5735
+ if (v) return v;
5736
+ }
5737
+ } catch {
5738
+ }
5739
+ }
5740
+ return "vault-first";
5741
+ }
5742
+ function useMinimalToolSurface(vaultPath) {
5743
+ const cfg = loadToolsConfig(vaultPath);
5744
+ if (!cfg.conditional) return false;
5745
+ return readActiveMode(vaultPath) === "outage-silence";
5746
+ }
5747
+
5313
5748
  // src/mcp/server.ts
5314
5749
  async function createServer(notesPath, options = {}) {
5315
5750
  const ctx = await buildContext(notesPath, options);
@@ -5317,17 +5752,20 @@ async function createServer(notesPath, options = {}) {
5317
5752
  name: "semantic-memory",
5318
5753
  version: "1.0.0"
5319
5754
  });
5755
+ const minimal = useMinimalToolSurface(notesPath);
5320
5756
  registerSearchTools(server, ctx);
5321
5757
  registerReadTools(server, ctx);
5322
- registerWriteTools(server, ctx);
5323
- registerPatchTools(server, ctx);
5324
- registerLintTools(server, ctx);
5325
- registerLogTools(server, ctx);
5326
- registerMetadataTools(server, ctx);
5327
- registerGraphTools(server, ctx);
5328
5758
  registerSystemTools(server, ctx);
5329
- registerContractTools(server, ctx);
5330
- registerSessionTools(server, ctx);
5759
+ if (!minimal) {
5760
+ registerWriteTools(server, ctx);
5761
+ registerPatchTools(server, ctx);
5762
+ registerLintTools(server, ctx);
5763
+ registerLogTools(server, ctx);
5764
+ registerMetadataTools(server, ctx);
5765
+ registerGraphTools(server, ctx);
5766
+ registerContractTools(server, ctx);
5767
+ registerSessionTools(server, ctx);
5768
+ }
5331
5769
  if (options.waitForReady) {
5332
5770
  await ctx.tryLoadCachedIndex();
5333
5771
  await ctx.fullIndex();