@danielmarbach/mnemonic-mcp 0.17.0 → 0.18.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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to `mnemonic` will be documented in this file.
4
4
 
5
5
  The format is loosely based on Keep a Changelog and uses semver-style version headings.
6
6
 
7
+ ## [0.18.0] - 2026-03-27
8
+
9
+ ### Added
10
+
11
+ - Theme graduation from "other": keywords appearing across multiple notes become named themes automatically.
12
+ - Keyword extraction with synonym normalization for theme classification.
13
+ - `analyzeThemeQuality()` utility for checking theme distribution metrics.
14
+
15
+ ### Changed
16
+
17
+ - All 23 MCP tools now include complete SDK tool hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). Previously, read-only tools were missing `destructiveHint: false`.
18
+ - Mutating tool retry results now return explicit recovery guidance instead of vague `Retry: safe` text, including recovery kind and no-inference instructions for partial-persistence failures.
19
+ - Recovery precedence is now encoded in retry metadata: deterministic tool reconciliation is preferred where available, and same-vault retries must be replayed serially.
20
+ - Retry metadata now uses `attemptedCommit.subject` instead of `attemptedCommit.message`.
21
+ - `classifyTheme` now uses keyword-based graduation in addition to tag matching.
22
+ - `project_memory_summary` includes both fixed themes and dynamically promoted keywords.
23
+
7
24
  ## [0.17.0] - 2026-03-25
8
25
 
9
26
  ### Added
package/README.md CHANGED
@@ -452,6 +452,18 @@ Imported notes are written to the main vault with `lifecycle: permanent` and `sc
452
452
  | `update` | Update note content/title/tags/lifecycle, re-embeds always |
453
453
  | `where_is_memory` | Show note's project association and storage location |
454
454
 
455
+ ### Theme emergence
456
+
457
+ `project_memory_summary` categorizes notes by theme. Themes **emerge automatically** from your notes:
458
+
459
+ - **Tag-based classification** — notes with matching tags (e.g., `["decisions"]`, `["bugs"]`) are grouped immediately
460
+ - **Keyword graduation** — keywords that appear across multiple notes become named themes over time
461
+ - **"other" bucket** — notes that don't match any theme are grouped here; this shrinks as themes emerge
462
+
463
+ No predefined schema required. The system adapts to your project's vocabulary.
464
+
465
+ **Language handling:** The system degrades gracefully for non-English notes. Stopwords and synonyms are optional English enhancements; keywords that don't match pass through unchanged, allowing non-English keywords to graduate if they meet frequency thresholds.
466
+
455
467
  ### Relationships
456
468
 
457
469
  Notes can be linked with typed edges stored in frontmatter:
package/build/index.js CHANGED
@@ -17,7 +17,7 @@ import { getRelationshipPreview } from "./relationships.js";
17
17
  import { cleanMarkdown } from "./markdown.js";
18
18
  import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
19
19
  import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
20
- import { classifyTheme, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, buildThemeCache, computeConnectionDiversity, } from "./project-introspection.js";
20
+ import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, buildThemeCache, computeConnectionDiversity, } from "./project-introspection.js";
21
21
  import { detectProject, getCurrentGitBranch, resolveProjectIdentity } from "./project.js";
22
22
  import { VaultManager } from "./vault.js";
23
23
  import { checkBranchChange } from "./branch-tracker.js";
@@ -693,9 +693,22 @@ function buildMutationRetryContract(args) {
693
693
  if (args.commit.status !== "failed") {
694
694
  return undefined;
695
695
  }
696
+ const recoveryKind = args.preferredRecovery ?? (args.mutationApplied
697
+ ? "manual-exact-git-recovery"
698
+ : "no-manual-recovery");
699
+ const recoveryReason = recoveryKind === "rerun-tool-call-serial"
700
+ ? "Tool-level reconciliation exists for this mutation; rerun the same tool call serially for the affected vault."
701
+ : recoveryKind === "manual-exact-git-recovery"
702
+ ? "Mutation is already persisted on disk; manual git recovery is allowed only with the exact attemptedCommit values."
703
+ : "Mutation was not applied deterministically; manual git recovery is not authorized.";
696
704
  return {
705
+ recovery: {
706
+ kind: recoveryKind,
707
+ allowed: recoveryKind !== "no-manual-recovery",
708
+ reason: recoveryReason,
709
+ },
697
710
  attemptedCommit: {
698
- message: args.commitMessage,
711
+ subject: args.commitMessage,
699
712
  body: args.commitBody,
700
713
  files: args.files,
701
714
  cwd: args.cwd,
@@ -708,19 +721,69 @@ function buildMutationRetryContract(args) {
708
721
  rationale: args.mutationApplied
709
722
  ? "Mutation is already persisted on disk; commit can be retried deterministically."
710
723
  : "Mutation was not applied; retry may require re-running the operation.",
724
+ instructions: {
725
+ sourceOfTruth: recoveryKind === "manual-exact-git-recovery" ? "attemptedCommit" : "tool-response",
726
+ useExactSubject: recoveryKind === "manual-exact-git-recovery",
727
+ useExactBody: recoveryKind === "manual-exact-git-recovery",
728
+ useExactFiles: recoveryKind === "manual-exact-git-recovery",
729
+ forbidInferenceFromHistory: true,
730
+ forbidInferenceFromTitleOrSummary: true,
731
+ forbidParallelSameVaultRetries: true,
732
+ preferToolReconciliation: recoveryKind === "rerun-tool-call-serial",
733
+ rerunSameToolCallSerially: recoveryKind === "rerun-tool-call-serial",
734
+ },
711
735
  };
712
736
  }
713
737
  function formatRetrySummary(retry) {
714
738
  if (!retry) {
715
739
  return undefined;
716
740
  }
717
- const safety = retry.retrySafe ? "safe" : "requires review";
718
741
  const opLabel = retry.attemptedCommit.operation === "add" ? "add" : "commit";
719
742
  const error = retry.attemptedCommit.error;
720
- return [
721
- `Retry: ${safety} | vault=${retry.attemptedCommit.vault} | files=${retry.attemptedCommit.files.length}`,
722
- `Git ${opLabel} error: ${error}`,
723
- ].join("\n");
743
+ const lines = [];
744
+ switch (retry.recovery.kind) {
745
+ case "rerun-tool-call-serial":
746
+ lines.push("Recovery: rerun same tool call serially");
747
+ lines.push(retry.recovery.reason);
748
+ lines.push("Rerun the same mnemonic tool call one time for the affected vault.");
749
+ lines.push("Do not replay same-vault mutations in parallel.");
750
+ lines.push("Manual git recovery is not authorized for this failure.");
751
+ lines.push("Git failure:");
752
+ lines.push(`${opLabel}: ${error}`);
753
+ break;
754
+ case "manual-exact-git-recovery":
755
+ lines.push("Recovery: manual exact git recovery allowed");
756
+ lines.push(retry.recovery.reason);
757
+ lines.push("Use only the exact values below. Do not infer from git history, note title, summary, or repo state.");
758
+ lines.push("");
759
+ lines.push("Commit subject:");
760
+ lines.push(retry.attemptedCommit.subject);
761
+ if (retry.attemptedCommit.body) {
762
+ lines.push("");
763
+ lines.push("Commit body:");
764
+ lines.push(retry.attemptedCommit.body);
765
+ }
766
+ lines.push("");
767
+ lines.push("Files:");
768
+ for (const file of retry.attemptedCommit.files) {
769
+ lines.push(`- ${file}`);
770
+ }
771
+ lines.push("");
772
+ lines.push("Git failure:");
773
+ lines.push(`${opLabel}: ${error}`);
774
+ break;
775
+ case "no-manual-recovery":
776
+ lines.push("Recovery: no manual recovery authorized");
777
+ lines.push(retry.recovery.reason);
778
+ lines.push("Git failure:");
779
+ lines.push(`${opLabel}: ${error}`);
780
+ break;
781
+ default: {
782
+ const _exhaustive = retry.recovery.kind;
783
+ throw new Error(`Unknown recovery kind: ${_exhaustive}`);
784
+ }
785
+ }
786
+ return lines.join("\n");
724
787
  }
725
788
  function formatPersistenceSummary(persistence) {
726
789
  const parts = [
@@ -731,14 +794,14 @@ function formatPersistenceSummary(persistence) {
731
794
  if (persistence.embedding.reason) {
732
795
  lines[0] += ` | embedding reason=${persistence.embedding.reason}`;
733
796
  }
734
- if (persistence.git.commit === "failed" && persistence.git.commitError) {
797
+ const retrySummary = formatRetrySummary(persistence.retry);
798
+ if (!retrySummary && persistence.git.commit === "failed" && persistence.git.commitError) {
735
799
  const opLabel = persistence.git.commitOperation === "add" ? "add" : "commit";
736
800
  lines.push(`Git ${opLabel} error: ${persistence.git.commitError}`);
737
801
  }
738
802
  if (persistence.git.push === "failed" && persistence.git.pushError) {
739
803
  lines.push(`Git push error: ${persistence.git.pushError}`);
740
804
  }
741
- const retrySummary = formatRetrySummary(persistence.retry);
742
805
  if (retrySummary) {
743
806
  lines.push(retrySummary);
744
807
  }
@@ -969,6 +1032,7 @@ server.registerTool("detect_project", {
969
1032
  "- Use `recall` or `project_memory_summary` to orient on existing memory.",
970
1033
  annotations: {
971
1034
  readOnlyHint: true,
1035
+ destructiveHint: false,
972
1036
  idempotentHint: true,
973
1037
  openWorldHint: false,
974
1038
  },
@@ -1023,6 +1087,7 @@ server.registerTool("get_project_identity", {
1023
1087
  "- Use `set_project_identity` only if the wrong remote is defining identity.",
1024
1088
  annotations: {
1025
1089
  readOnlyHint: true,
1090
+ destructiveHint: false,
1026
1091
  idempotentHint: true,
1027
1092
  openWorldHint: false,
1028
1093
  },
@@ -1179,6 +1244,7 @@ server.registerTool("list_migrations", {
1179
1244
  "- Run `execute_migration` with `dryRun: true` first.",
1180
1245
  annotations: {
1181
1246
  readOnlyHint: true,
1247
+ destructiveHint: false,
1182
1248
  idempotentHint: true,
1183
1249
  openWorldHint: false,
1184
1250
  },
@@ -1600,6 +1666,7 @@ server.registerTool("get_project_memory_policy", {
1600
1666
  "- Call `remember` with explicit `scope` for a one-off override, or `set_project_memory_policy` to change defaults.",
1601
1667
  annotations: {
1602
1668
  readOnlyHint: true,
1669
+ destructiveHint: false,
1603
1670
  idempotentHint: true,
1604
1671
  openWorldHint: false,
1605
1672
  },
@@ -1673,6 +1740,7 @@ server.registerTool("recall", {
1673
1740
  "- Use `get`, `update`, `relate`, or `consolidate` based on the results.",
1674
1741
  annotations: {
1675
1742
  readOnlyHint: true,
1743
+ destructiveHint: false,
1676
1744
  idempotentHint: true,
1677
1745
  openWorldHint: true,
1678
1746
  },
@@ -2107,6 +2175,7 @@ server.registerTool("get", {
2107
2175
  "- Use `update`, `forget`, `move_memory`, or `relate` after inspection.",
2108
2176
  annotations: {
2109
2177
  readOnlyHint: true,
2178
+ destructiveHint: false,
2110
2179
  idempotentHint: true,
2111
2180
  openWorldHint: false,
2112
2181
  },
@@ -2204,6 +2273,7 @@ server.registerTool("where_is_memory", {
2204
2273
  "- Use `move_memory` if the storage location is wrong, or `get` for full inspection.",
2205
2274
  annotations: {
2206
2275
  readOnlyHint: true,
2276
+ destructiveHint: false,
2207
2277
  idempotentHint: true,
2208
2278
  openWorldHint: false,
2209
2279
  },
@@ -2260,6 +2330,7 @@ server.registerTool("list", {
2260
2330
  "- Use `get` for exact inspection or `update` / `consolidate` for cleanup.",
2261
2331
  annotations: {
2262
2332
  readOnlyHint: true,
2333
+ destructiveHint: false,
2263
2334
  idempotentHint: true,
2264
2335
  openWorldHint: false,
2265
2336
  },
@@ -2370,6 +2441,7 @@ server.registerTool("discover_tags", {
2370
2441
  "Read-only.",
2371
2442
  annotations: {
2372
2443
  readOnlyHint: true,
2444
+ destructiveHint: false,
2373
2445
  idempotentHint: true,
2374
2446
  openWorldHint: false,
2375
2447
  },
@@ -2576,6 +2648,7 @@ server.registerTool("recent_memories", {
2576
2648
  "- Use `get` for exact inspection or `update` to continue refining a recent note.",
2577
2649
  annotations: {
2578
2650
  readOnlyHint: true,
2651
+ destructiveHint: false,
2579
2652
  idempotentHint: true,
2580
2653
  openWorldHint: false,
2581
2654
  },
@@ -2645,6 +2718,7 @@ server.registerTool("memory_graph", {
2645
2718
  "- Use `get`, `relate`, `unrelate`, or `consolidate` based on what the graph reveals.",
2646
2719
  annotations: {
2647
2720
  readOnlyHint: true,
2721
+ destructiveHint: false,
2648
2722
  idempotentHint: true,
2649
2723
  openWorldHint: false,
2650
2724
  },
@@ -2724,6 +2798,7 @@ server.registerTool("project_memory_summary", {
2724
2798
  "- Use `recall` or `list` to drill down into specific areas.",
2725
2799
  annotations: {
2726
2800
  readOnlyHint: true,
2801
+ destructiveHint: false,
2727
2802
  idempotentHint: true,
2728
2803
  openWorldHint: false,
2729
2804
  },
@@ -2765,17 +2840,23 @@ server.registerTool("project_memory_summary", {
2765
2840
  }
2766
2841
  const policyLine = await formatProjectPolicyLine(project.id);
2767
2842
  // Build theme cache for connection diversity scoring (project-scoped only)
2843
+ // This uses simple classifyTheme for consistent diversity calculations
2768
2844
  const themeCache = buildThemeCache(projectEntries.map(e => e.note));
2769
- // Categorize by theme (project-scoped only)
2845
+ // Compute promoted themes from keywords (graduation system)
2846
+ const graduationResult = computeThemesWithGraduation(projectEntries.map(e => e.note));
2847
+ const promotedThemes = new Set(graduationResult.promotedThemes);
2848
+ // Categorize by theme with graduation (project-scoped only)
2770
2849
  const themed = new Map();
2771
2850
  for (const entry of projectEntries) {
2772
- const theme = classifyTheme(entry.note);
2851
+ const theme = classifyThemeWithGraduation(entry.note, promotedThemes);
2773
2852
  const bucket = themed.get(theme) ?? [];
2774
2853
  bucket.push(entry);
2775
2854
  themed.set(theme, bucket);
2776
2855
  }
2777
- // Theme order for display
2778
- const themeOrder = ["overview", "decisions", "tooling", "bugs", "architecture", "quality", "other"];
2856
+ // Theme order: fixed themes first, then promoted themes alphabetically, then "other"
2857
+ const fixedThemes = ["overview", "decisions", "tooling", "bugs", "architecture", "quality"];
2858
+ const dynamicThemes = graduationResult.promotedThemes.filter(t => !fixedThemes.includes(t));
2859
+ const themeOrder = [...fixedThemes, ...dynamicThemes.sort(), "other"];
2779
2860
  // Calculate notes distribution (project-scoped only)
2780
2861
  const projectVaultCount = projectEntries.filter(e => e.vault.isProject).length;
2781
2862
  const mainVaultProjectEntries = projectEntries.filter(e => !e.vault.isProject);
@@ -3051,7 +3132,7 @@ server.registerTool("project_memory_summary", {
3051
3132
  id: e.note.id,
3052
3133
  title: e.note.title,
3053
3134
  updatedAt: e.note.updatedAt,
3054
- theme: classifyTheme(e.note),
3135
+ theme: classifyThemeWithGraduation(e.note, promotedThemes),
3055
3136
  })),
3056
3137
  anchors,
3057
3138
  orientation,
@@ -3403,6 +3484,7 @@ server.registerTool("relate", {
3403
3484
  cwd,
3404
3485
  vault,
3405
3486
  mutationApplied: true,
3487
+ preferredRecovery: "rerun-tool-call-serial",
3406
3488
  });
3407
3489
  const structuredContent = {
3408
3490
  action: "related",
@@ -3452,6 +3534,7 @@ server.registerTool("relate", {
3452
3534
  cwd,
3453
3535
  vault,
3454
3536
  mutationApplied: true,
3537
+ preferredRecovery: "rerun-tool-call-serial",
3455
3538
  });
3456
3539
  }
3457
3540
  if (commitStatus.status === "committed") {
@@ -3574,6 +3657,7 @@ server.registerTool("unrelate", {
3574
3657
  cwd,
3575
3658
  vault,
3576
3659
  mutationApplied: true,
3660
+ preferredRecovery: "rerun-tool-call-serial",
3577
3661
  });
3578
3662
  const structuredContent = {
3579
3663
  action: "unrelated",
@@ -3617,6 +3701,7 @@ server.registerTool("unrelate", {
3617
3701
  cwd,
3618
3702
  vault,
3619
3703
  mutationApplied: true,
3704
+ preferredRecovery: "rerun-tool-call-serial",
3620
3705
  });
3621
3706
  }
3622
3707
  if (commitStatus.status === "committed") {