@danielmarbach/mnemonic-mcp 0.23.0 → 0.25.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,27 @@ 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
+ ## [Unreleased]
8
+
9
+ ## [0.25.0] - 2026-04-24
10
+
11
+ ### Added
12
+
13
+ - `recall` now supports `mode: "workflow"` for RPIR-oriented chain retrieval while keeping compatibility with existing note graphs.
14
+ - Relationship types now include `derives-from` and `follows` so workflows can express directional sequencing and derivation explicitly.
15
+ - The npm package now ships bundled skills and includes a `mnemonic-install-skills` command to install or update local skill directories for Claude, OpenCode, and custom targets.
16
+
17
+ ### Changed
18
+
19
+ - RPIR workflow guidance now uses `mnemonic-rpi-workflow` as the prompt name.
20
+ - The RPIR skill is now published under `skills/mnemonic-rpi-workflow`.
21
+
22
+ ## [0.24.0] - 2026-04-24
23
+
24
+ ### Added
25
+
26
+ - `update` now supports `semanticPatch` for token-efficient targeted edits (insert, replace, append, remove content under specific headings) without round-tripping the full note body.
27
+
7
28
  ## [0.23.0] - 2026-04-17
8
29
 
9
30
  ### Added
package/README.md CHANGED
@@ -51,8 +51,13 @@ No code changes required — set `EMBED_MODEL=qwen3-embedding:0.6b` in your envi
51
51
  npm install
52
52
  npm run build
53
53
  npm test
54
+
55
+ # release-confidence gate (build + full tests + isolated dogfooding)
56
+ npm run verify:release
54
57
  ```
55
58
 
59
+ The gate fails on required dogfood checks and reports advisory findings separately in the dogfood output.
60
+
56
61
  `npm run build` already runs `typecheck`, but running it explicitly first gives a faster failure loop when iterating on the codebase.
57
62
 
58
63
  For local dogfooding, start the built MCP server with:
@@ -94,6 +99,45 @@ npm install @danielmarbach/mnemonic-mcp
94
99
  npm install @danielmarbach/mnemonic-mcp@0.2.0
95
100
  ```
96
101
 
102
+ ### Install bundled skills (Claude/OpenCode)
103
+
104
+ The npm package now includes `skills/**` plus a helper binary to install them into local skill directories.
105
+
106
+ ```bash
107
+ # If mnemonic is installed in this project:
108
+ npx mnemonic-install-skills --target all --mode copy
109
+
110
+ # One-off install without adding dependency:
111
+ npx -y -p @danielmarbach/mnemonic-mcp mnemonic-install-skills --target all --mode copy
112
+ ```
113
+
114
+ Supported targets:
115
+
116
+ - `--target claude` -> `~/.claude/skills`
117
+ - `--target opencode` -> `~/.config/opencode/skills`
118
+ - `--target all` -> both (default)
119
+ - `--target custom` -> only use `--target-dir` destinations
120
+ - `--target-dir <path>` -> add any custom client skill directory
121
+
122
+ Update flow after upgrading `@danielmarbach/mnemonic-mcp`:
123
+
124
+ ```bash
125
+ npx mnemonic-install-skills --target all --mode copy --update
126
+ ```
127
+
128
+ If you prefer automatic propagation without copy refreshes, use symlink mode:
129
+
130
+ ```bash
131
+ npx mnemonic-install-skills --target all --mode symlink --update
132
+ ```
133
+
134
+ After install, load and use the skill by name:
135
+
136
+ - Skill name: `mnemonic-rpi-workflow`
137
+ - Prompt counterpart: `mnemonic-rpi-workflow`
138
+
139
+ In clients that support explicit skill loading (for example Claude Code or OpenCode), load `mnemonic-rpi-workflow` before running multi-step RPIR workflows.
140
+
97
141
  ### Homebrew
98
142
 
99
143
  The formula lives in this repository. Tap it with an explicit URL so no separate repository is needed:
@@ -312,7 +356,11 @@ Project identity derives from the **git remote URL**, normalized to a stable slu
312
356
 
313
357
  **Hybrid recall** enhances semantic search with lightweight lexical reranking over note projections. When semantic results are weak, a bounded lexical rescue path scans projections for additional candidates, improving exact-match and identifier-heavy recall without changing the storage model or adding new infrastructure. **Canonical explanation promotion** boosts notes that explain key decisions and concepts for "why"-style questions, using structural signals like role, connections, and format rather than keyword matching.
314
358
 
315
- Temporal recall is opt-in via `mode: "temporal"`. It keeps semantic selection first, then enriches only the top matches with compact git-backed history so agents can inspect how a note evolved without turning recall into raw log or diff output.
359
+ Recall modes:
360
+
361
+ - `mode: "default"` (default): semantic recall with optional lexical reranking and bounded relationship previews.
362
+ - `mode: "temporal"`: enrich top matches with compact git-backed history (no raw diffs by default).
363
+ - `mode: "workflow"`: prioritize RPIR-style chain reconstruction while remaining compatible with legacy `related-to` links.
316
364
 
317
365
  **What temporal mode shows:**
318
366
 
@@ -341,11 +389,21 @@ Each note carries a `lifecycle`:
341
389
 
342
390
  ### Roles and lifecycle
343
391
 
344
- Roles are optional prioritization hints, not required schema. mnemonic infers a `role` and `importance` from structural signals (heading count, bullet density, inbound references, relationship types) — inference is language-independent and never overwrites explicit frontmatter. Valid roles: `summary`, `decision`, `plan`, `log`, `reference`. Valid importance values: `high`, `normal`.
392
+ Roles are optional prioritization hints, not required schema. mnemonic infers a `role` and `importance` from structural signals (heading count, bullet density, inbound references, relationship types) — inference is language-independent and never overwrites explicit frontmatter. Valid roles: `summary`, `decision`, `plan`, `context`, `reference`, `research`, `review`. Valid importance values: `high`, `normal`, `low`.
345
393
 
346
394
  Set `alwaysLoad: true` in a note's frontmatter to mark it as an explicit session anchor; it receives the highest recall and relationship-expansion priority regardless of inferred role.
347
395
 
348
- mnemonic works without roles. Inferred roles stay internal-only, prioritization is language-independent by default, and lifecycle remains the separate durability axis. A note with `role: plan` can still be either `temporary` or `permanent`.
396
+ mnemonic works without roles. Inferred roles stay internal-only, prioritization is language-independent by default, and lifecycle remains the separate durability axis. When `lifecycle` is omitted, `remember` applies soft defaults based on role: `research`, `plan`, and `review` default to `temporary`; `decision`, `summary`, and `reference` default to `permanent`. Explicit `lifecycle` always overrides the role-based default.
397
+
398
+ ### RPIR workflow conventions
399
+
400
+ For structured workflows, use the RPIR stages: research -> plan -> implement -> review (iterate only when needed).
401
+
402
+ - Create one request root note per workflow: `role: context`, `lifecycle: temporary`, `tags: ["workflow", "request"]`.
403
+ - Keep one current plan note per request (`role: plan`) and update or supersede as the plan evolves.
404
+ - For apply/task notes, do not add a new role: use `role: plan` for executable steps and `role: context` for execution observations; tag both with `apply`.
405
+ - Keep relationships sparse and immediate-upstream only: research -> request, plan -> request/research, apply -> plan, review -> apply/plan, outcome -> plan (optionally request).
406
+ - Consolidate at workflow end: promote durable outcomes into permanent decision/summary/reference notes; let temporary scaffolding expire.
349
407
 
350
408
  ### Note format
351
409
 
@@ -444,6 +502,7 @@ Imported notes are written to the main vault with `lifecycle: permanent` and `sc
444
502
 
445
503
  | Prompt | Description |
446
504
  |--------|-------------|
505
+ | `mnemonic-rpi-workflow` | Optional. Returns RPIR stage protocol and conventions: request root note pattern, stage checklists, apply/task split, sparse relationships, subagent handoff contract, and commit discipline. |
447
506
  | `mnemonic-workflow-hint` | Optional. Returns a compact decision protocol: use `recall` or `list` first, inspect with `get`, update existing memories, remember only when nothing matches, then organize with `relate`, `consolidate`, or `move_memory`. It also reinforces summary-first orientation via `project_memory_summary`, temporary-note recovery only after orientation, and that roles are optional prioritization hints while lifecycle stays separate. |
448
507
 
449
508
  ## Tools
@@ -463,7 +522,7 @@ Imported notes are written to the main vault with `lifecycle: permanent` and `sc
463
522
  | `memory_graph` | Show compact adjacency list of relationships |
464
523
  | `move_memory` | Move note between vaults without changing id |
465
524
  | `project_memory_summary` | Session-start entrypoint: themed notes, anchors, and orientation for fast project orientation |
466
- | `recall` | Semantic search with optional project boost and opt-in temporal history |
525
+ | `recall` | Semantic search with optional project boost, temporal history mode, and workflow chain mode |
467
526
  | `recent_memories` | Show most recently updated notes for scope |
468
527
  | `remember` | Write note + embedding; `cwd` sets context, `scope` picks storage, `lifecycle` picks temporary vs permanent |
469
528
  | `relate` | Create typed relationship between notes (bidirectional) |
@@ -504,6 +563,10 @@ relatedTo:
504
563
  | `explains` | `fromId` explains `toId` |
505
564
  | `example-of` | `fromId` is a concrete example of `toId` |
506
565
  | `supersedes` | `fromId` is the newer version of `toId` |
566
+ | `derives-from` | `fromId` is derived from `toId` |
567
+ | `follows` | `fromId` follows `toId` in sequence |
568
+
569
+ `workflow` recall mode prefers directional and typed relationships first, then falls back to `related-to` for long-term compatibility with older vaults.
507
570
 
508
571
  `relate` is bidirectional by default. `forget` automatically removes any edges pointing at the deleted note.
509
572
 
package/build/index.js CHANGED
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
  import { randomUUID } from "crypto";
6
6
  import path from "path";
7
7
  import { promises as fs } from "fs";
8
- import { NOTE_LIFECYCLES } from "./storage.js";
8
+ import { NOTE_LIFECYCLES, NOTE_ROLES } from "./storage.js";
9
9
  import { embed, cosineSimilarity, embedModel } from "./embeddings.js";
10
10
  import { buildTemporalHistoryEntry, computeConfidence, getNoteProvenance } from "./provenance.js";
11
11
  import { enrichTemporalHistory } from "./temporal-interpretation.js";
@@ -14,10 +14,11 @@ import { invalidateActiveProjectCache, getOrBuildVaultEmbeddings, getOrBuildVaul
14
14
  import { performance } from "perf_hooks";
15
15
  import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
16
16
  import { suggestAutoRelationships } from "./auto-relate.js";
17
- import { computeRecallMetadataBoost, computeHybridScore, selectRecallResults, applyLexicalReranking, applyCanonicalExplanationPromotion, } from "./recall.js";
17
+ import { computeRecallMetadataBoost, computeHybridScore, selectRecallResults, selectWorkflowResults, applyLexicalReranking, applyCanonicalExplanationPromotion, } from "./recall.js";
18
18
  import { shouldTriggerLexicalRescue, rankDocumentsByTfIdf, LEXICAL_RESCUE_CANDIDATE_LIMIT, LEXICAL_RESCUE_THRESHOLD, LEXICAL_RESCUE_RESULT_LIMIT, } from "./lexical.js";
19
19
  import { getRelationshipPreview } from "./relationships.js";
20
20
  import { cleanMarkdown } from "./markdown.js";
21
+ import { applySemanticPatches } from "./semantic-patch.js";
21
22
  import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
22
23
  import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
23
24
  import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, daysSinceUpdate, withinThemeScore, anchorScore, computeConnectionDiversity, workingStateScore, extractNextAction, } from "./project-introspection.js";
@@ -406,12 +407,13 @@ function describeLifecycle(lifecycle) {
406
407
  function formatNote(note, score, showRawRelated = true) {
407
408
  const scoreStr = score !== undefined ? ` | similarity: ${score.toFixed(3)}` : "";
408
409
  const projectStr = note.project ? ` | project: ${note.projectName ?? note.project}` : " | global";
410
+ const roleStr = note.role ? ` | **role: ${note.role}**` : "";
409
411
  const relStr = showRawRelated && note.relatedTo && note.relatedTo.length > 0
410
412
  ? `\n**related:** ${note.relatedTo.map((r) => `\`${r.id}\` (${r.type})`).join(", ")}`
411
413
  : "";
412
414
  return (`## ${note.title}\n` +
413
415
  `**id:** \`${note.id}\`${projectStr}${scoreStr}\n` +
414
- `**tags:** ${note.tags.join(", ") || "none"} | **${describeLifecycle(note.lifecycle)}** | **updated:** ${note.updatedAt}${relStr}\n\n` +
416
+ `**tags:** ${note.tags.join(", ") || "none"} | **${describeLifecycle(note.lifecycle)}**${roleStr} | **updated:** ${note.updatedAt}${relStr}\n\n` +
415
417
  note.content);
416
418
  }
417
419
  function formatTemporalHistory(history) {
@@ -923,11 +925,13 @@ function formatListEntry(entry, options = {}) {
923
925
  const extras = [];
924
926
  if (note.tags.length > 0)
925
927
  extras.push(note.tags.join(", "));
926
- extras.push(`lifecycle=${note.lifecycle}`);
928
+ extras.push(`lifecycle: ${note.lifecycle}`);
929
+ if (note.role)
930
+ extras.push(`role: ${note.role}`);
927
931
  if (options.includeStorage)
928
- extras.push(`stored=${storageLabel(vault)}`);
932
+ extras.push(`stored: ${storageLabel(vault)}`);
929
933
  if (options.includeUpdated)
930
- extras.push(`updated=${note.updatedAt}`);
934
+ extras.push(`updated: ${note.updatedAt}`);
931
935
  const lines = [`- **${note.title}** \`${note.id}\` ${proj}${extras.length > 0 ? ` — ${extras.join(" | ")}` : ""}`];
932
936
  if (options.includeRelations && note.relatedTo && note.relatedTo.length > 0) {
933
937
  lines.push(` related: ${note.relatedTo.map((rel) => `${rel.id} (${rel.type})`).join(", ")}`);
@@ -1028,6 +1032,14 @@ function addVaultChange(vaultChanges, vault, file) {
1028
1032
  vaultChanges.set(vault, files);
1029
1033
  }
1030
1034
  }
1035
+ const ROLE_LIFECYCLE_DEFAULTS = {
1036
+ research: "temporary",
1037
+ plan: "temporary",
1038
+ review: "temporary",
1039
+ decision: "permanent",
1040
+ summary: "permanent",
1041
+ reference: "permanent",
1042
+ };
1031
1043
  // ── MCP Server ────────────────────────────────────────────────────────────────
1032
1044
  const server = new McpServer({
1033
1045
  name: "mnemonic",
@@ -1425,7 +1437,13 @@ server.registerTool("remember", {
1425
1437
  .enum(NOTE_LIFECYCLES)
1426
1438
  .optional()
1427
1439
  .describe("Memory lifetime. Use `temporary` for short-lived working context such as active investigations or transient status. " +
1428
- "Use `permanent` for durable knowledge such as decisions, fixes, patterns, and preferences."),
1440
+ "Use `permanent` for durable knowledge such as decisions, fixes, patterns, and preferences. " +
1441
+ "When omitted, defaults based on role: research/plan/review → temporary, decision/summary/reference → permanent."),
1442
+ role: z
1443
+ .enum(NOTE_ROLES)
1444
+ .optional()
1445
+ .describe("Optional prioritization hint for the note. Inferred automatically when omitted. " +
1446
+ "Set explicitly for workflow artifacts like research or review notes."),
1429
1447
  summary: z.string().optional().describe("Git commit summary only. Imperative mood, concise, and focused on why the change matters."),
1430
1448
  alwaysLoad: z
1431
1449
  .boolean()
@@ -1453,7 +1471,7 @@ server.registerTool("remember", {
1453
1471
  .describe("Optional agent hint indicating that `recall` or `list` was already used to check for an existing memory on this topic."),
1454
1472
  }),
1455
1473
  outputSchema: RememberResultSchema,
1456
- }, async ({ title, content, tags, lifecycle, summary, alwaysLoad, cwd, scope, allowProtectedBranch = false }) => {
1474
+ }, async ({ title, content, tags, lifecycle, role, summary, alwaysLoad, cwd, scope, allowProtectedBranch = false }) => {
1457
1475
  await ensureBranchSynced(cwd);
1458
1476
  const project = await resolveProject(cwd);
1459
1477
  const cleanedContent = await cleanMarkdown(content);
@@ -1482,7 +1500,8 @@ server.registerTool("remember", {
1482
1500
  const now = new Date().toISOString();
1483
1501
  const note = {
1484
1502
  id, title, content: cleanedContent, tags,
1485
- lifecycle: lifecycle ?? "permanent",
1503
+ lifecycle: lifecycle ?? (role ? ROLE_LIFECYCLE_DEFAULTS[role] : undefined) ?? "permanent",
1504
+ ...(role ? { role } : {}),
1486
1505
  alwaysLoad: alwaysLoad ?? false,
1487
1506
  project: project?.id,
1488
1507
  projectName: project?.name,
@@ -1840,6 +1859,7 @@ server.registerTool("recall", {
1840
1859
  title: "Recall",
1841
1860
  description: "Semantic search over stored memories using embeddings.\n\n" +
1842
1861
  "Supports opt-in temporal mode (`mode: \"temporal\"`) to enrich top semantic matches with compact git-backed history.\n\n" +
1862
+ "Supports workflow mode (`mode: \"workflow\"`) to prioritize RPIR-style chain reconstruction while retaining compatibility with legacy relationships.\n\n" +
1843
1863
  "Use this when:\n" +
1844
1864
  "- You know the topic but not the exact memory id\n" +
1845
1865
  "- You are starting a session and want relevant prior context\n" +
@@ -1866,7 +1886,7 @@ server.registerTool("recall", {
1866
1886
  cwd: projectParam,
1867
1887
  limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECALL_LIMIT),
1868
1888
  minSimilarity: z.number().min(0).max(1).optional().default(DEFAULT_MIN_SIMILARITY),
1869
- mode: z.enum(["default", "temporal"]).optional().default("default").describe("Temporal history is opt-in. Use `temporal` to enrich top semantic matches with compact git-backed history."),
1889
+ mode: z.enum(["default", "temporal", "workflow"]).optional().default("default").describe("Use `temporal` for compact git-backed history, or `workflow` for RPIR-oriented chain reconstruction."),
1870
1890
  verbose: z.boolean().optional().default(false).describe("Only meaningful with `mode: \"temporal\"`. Adds richer stats-based history context without returning raw diffs."),
1871
1891
  tags: z.array(z.string()).optional().describe("Filter results to notes with all of these tags."),
1872
1892
  scope: z
@@ -1995,7 +2015,9 @@ server.registerTool("recall", {
1995
2015
  promoted.push(...rescueCandidates);
1996
2016
  promoted = applyCanonicalExplanationPromotion(promoted);
1997
2017
  }
1998
- const top = selectRecallResults(promoted, limit, scope);
2018
+ const top = mode === "workflow"
2019
+ ? selectWorkflowResults(promoted, limit, scope)
2020
+ : selectRecallResults(promoted, limit, scope);
1999
2021
  if (top.length === 0) {
2000
2022
  const structuredContent = { action: "recalled", query, scope: scope || "all", results: [] };
2001
2023
  return { content: [{ type: "text", text: "No memories found matching that query." }], structuredContent };
@@ -2056,6 +2078,7 @@ server.registerTool("recall", {
2056
2078
  vault: storageLabel(vault),
2057
2079
  tags: note.tags,
2058
2080
  lifecycle: note.lifecycle,
2081
+ role: note.role,
2059
2082
  updatedAt: note.updatedAt,
2060
2083
  provenance,
2061
2084
  confidence,
@@ -2103,7 +2126,8 @@ server.registerTool("update", {
2103
2126
  "- The updated memory id, changed fields, and persistence status\n\n" +
2104
2127
  "Side effects: rewrites the note, refreshes embeddings, git commits, and may push.\n\n" +
2105
2128
  "Typical next step:\n" +
2106
- "- Use `relate` or `consolidate` if the update changes how this note connects to others.",
2129
+ "- Use `relate` or `consolidate` if the update changes how this note connects to others.\n\n" +
2130
+ "Use `semanticPatch` for targeted edits (more token-efficient). Use `content` only for complete rewrites.",
2107
2131
  annotations: {
2108
2132
  readOnlyHint: false,
2109
2133
  destructiveHint: false,
@@ -2112,13 +2136,41 @@ server.registerTool("update", {
2112
2136
  },
2113
2137
  inputSchema: z.object({
2114
2138
  id: z.string().describe("Exact memory id. Use an id returned by `recall`, `list`, `recent_memories`, or `where_is`."),
2115
- content: z.string().optional().describe("Markdown note body. Put the key fact, decision, or outcome in the opening lines, then supporting detail."),
2139
+ semanticPatch: z
2140
+ .array(z.object({
2141
+ selector: z.object({
2142
+ heading: z.string().optional(),
2143
+ headingStartsWith: z.string().optional(),
2144
+ nthChild: z.number().int().optional(),
2145
+ lastChild: z.literal(true).optional(),
2146
+ }).refine((sel) => {
2147
+ const keys = [sel.heading, sel.headingStartsWith, sel.nthChild, sel.lastChild].filter((v) => v !== undefined);
2148
+ return keys.length === 1;
2149
+ }, { message: "Selector must have exactly one of: heading, headingStartsWith, nthChild, lastChild" }),
2150
+ operation: z.discriminatedUnion("op", [
2151
+ z.object({ op: z.literal("appendChild"), value: z.string() }),
2152
+ z.object({ op: z.literal("prependChild"), value: z.string() }),
2153
+ z.object({ op: z.literal("replace"), value: z.string() }),
2154
+ z.object({ op: z.literal("replaceChildren"), value: z.string() }),
2155
+ z.object({ op: z.literal("insertAfter"), value: z.string() }),
2156
+ z.object({ op: z.literal("insertBefore"), value: z.string() }),
2157
+ z.object({ op: z.literal("remove") }),
2158
+ ]),
2159
+ }))
2160
+ .optional()
2161
+ .describe("Use for targeted edits when you know the structure. More token-efficient than passing full content. " +
2162
+ "Mutually exclusive with content."),
2163
+ content: z.string().optional().describe("Full note body replacement. Use only for complete rewrites or when the note is small. Mutually exclusive with semanticPatch."),
2116
2164
  title: z.string().optional().describe("Specific, retrieval-friendly title. Prefer the concrete topic or decision, not a vague label."),
2117
2165
  tags: z.array(z.string()).optional().describe("Optional tags for later filtering. Use a small number of stable, meaningful tags."),
2118
2166
  lifecycle: z
2119
2167
  .enum(NOTE_LIFECYCLES)
2120
2168
  .optional()
2121
2169
  .describe("Change lifecycle. Preserve the existing value unless you're intentionally switching it."),
2170
+ role: z
2171
+ .enum(NOTE_ROLES)
2172
+ .optional()
2173
+ .describe("Change role. Preserve the existing value unless you're intentionally switching it."),
2122
2174
  summary: z.string().optional().describe("Git commit summary only. Imperative mood, concise, and focused on why the change matters."),
2123
2175
  alwaysLoad: z
2124
2176
  .boolean()
@@ -2133,12 +2185,18 @@ server.registerTool("update", {
2133
2185
  "When true, update can commit on a protected branch without changing project policy."),
2134
2186
  }),
2135
2187
  outputSchema: UpdateResultSchema,
2136
- }, async ({ id, content, title, tags, lifecycle, summary, alwaysLoad, cwd, allowProtectedBranch = false }) => {
2188
+ }, async ({ id, content, semanticPatch, title, tags, lifecycle, role, summary, alwaysLoad, cwd, allowProtectedBranch = false }) => {
2137
2189
  await ensureBranchSynced(cwd);
2138
2190
  const found = await vaultManager.findNote(id, cwd);
2139
2191
  if (!found) {
2140
2192
  return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
2141
2193
  }
2194
+ // Validate: content and semanticPatch are mutually exclusive
2195
+ const hasContent = content !== undefined;
2196
+ const hasSemanticPatch = semanticPatch !== undefined && semanticPatch.length > 0;
2197
+ if (hasContent && hasSemanticPatch) {
2198
+ return { content: [{ type: "text", text: "Exactly one of content or semanticPatch must be provided, not both." }], isError: true };
2199
+ }
2142
2200
  const { note, vault } = found;
2143
2201
  if (vault.isProject) {
2144
2202
  const resolvedProject = await resolveProject(cwd);
@@ -2163,13 +2221,24 @@ server.registerTool("update", {
2163
2221
  }
2164
2222
  }
2165
2223
  const now = new Date().toISOString();
2224
+ let patchedContent;
2225
+ if (semanticPatch && semanticPatch.length > 0) {
2226
+ try {
2227
+ patchedContent = await applySemanticPatches(note.content, semanticPatch);
2228
+ }
2229
+ catch (err) {
2230
+ const message = err instanceof Error ? err.message : String(err);
2231
+ return { content: [{ type: "text", text: `Semantic patch failed: ${message}` }], isError: true };
2232
+ }
2233
+ }
2166
2234
  const cleanedContent = content === undefined ? undefined : await cleanMarkdown(content);
2167
2235
  const updated = {
2168
2236
  ...note,
2169
2237
  title: title ?? note.title,
2170
- content: cleanedContent ?? note.content,
2238
+ content: patchedContent ?? cleanedContent ?? note.content,
2171
2239
  tags: tags ?? note.tags,
2172
2240
  lifecycle: lifecycle ?? note.lifecycle,
2241
+ ...(role !== undefined ? { role } : (note.role ? { role: note.role } : {})),
2173
2242
  alwaysLoad: alwaysLoad !== undefined ? alwaysLoad : note.alwaysLoad,
2174
2243
  updatedAt: now,
2175
2244
  };
@@ -2211,10 +2280,14 @@ server.registerTool("update", {
2211
2280
  changes.push("title");
2212
2281
  if (content !== undefined)
2213
2282
  changes.push("content");
2283
+ if (semanticPatch !== undefined)
2284
+ changes.push("semanticPatch");
2214
2285
  if (tags !== undefined)
2215
2286
  changes.push("tags");
2216
2287
  if (lifecycle !== undefined && lifecycle !== note.lifecycle)
2217
2288
  changes.push("lifecycle");
2289
+ if (role !== undefined && role !== note.role)
2290
+ changes.push("role");
2218
2291
  if (alwaysLoad !== undefined && alwaysLoad !== note.alwaysLoad)
2219
2292
  changes.push("alwaysLoad");
2220
2293
  const changeDesc = changes.length > 0 ? `Updated ${changes.join(", ")}` : "No changes";
@@ -2259,10 +2332,12 @@ server.registerTool("update", {
2259
2332
  timestamp: now,
2260
2333
  project: noteProjectRef(updated),
2261
2334
  lifecycle: updated.lifecycle,
2335
+ role: updated.role,
2262
2336
  persistence,
2263
2337
  };
2264
2338
  invalidateActiveProjectCache();
2265
- return { content: [{ type: "text", text: `Updated memory '${id}'\n${formatPersistenceSummary(persistence)}` }], structuredContent };
2339
+ const fieldText = changes.length > 0 ? `\nfields modified: ${changes.join(", ")}` : "";
2340
+ return { content: [{ type: "text", text: `Updated memory '${id}'${fieldText}\n${formatPersistenceSummary(persistence)}` }], structuredContent };
2266
2341
  });
2267
2342
  // ── forget ────────────────────────────────────────────────────────────────────
2268
2343
  server.registerTool("forget", {
@@ -2442,6 +2517,7 @@ server.registerTool("get", {
2442
2517
  project: noteProjectRef(note),
2443
2518
  tags: note.tags,
2444
2519
  lifecycle: note.lifecycle,
2520
+ role: note.role,
2445
2521
  alwaysLoad: note.alwaysLoad,
2446
2522
  relatedTo: note.relatedTo,
2447
2523
  createdAt: note.createdAt,
@@ -2457,7 +2533,7 @@ server.registerTool("get", {
2457
2533
  const lines = [];
2458
2534
  for (const note of found) {
2459
2535
  lines.push(`## ${note.title} (${note.id})`);
2460
- lines.push(`project: ${note.project?.name ?? "global"} | stored: ${note.vault} | lifecycle: ${note.lifecycle}`);
2536
+ lines.push(`project: ${note.project?.name ?? "global"} | stored: ${note.vault} | lifecycle: ${note.lifecycle}${note.role ? ` | role: ${note.role}` : ""}`);
2461
2537
  if (note.tags.length > 0)
2462
2538
  lines.push(`tags: ${note.tags.join(", ")}`);
2463
2539
  lines.push("");
@@ -2603,6 +2679,7 @@ server.registerTool("list", {
2603
2679
  project: noteProjectRef(note),
2604
2680
  tags: note.tags,
2605
2681
  lifecycle: note.lifecycle,
2682
+ role: note.role,
2606
2683
  vault: storageLabel(vault),
2607
2684
  updatedAt: note.updatedAt,
2608
2685
  hasRelated: note.relatedTo && note.relatedTo.length > 0,
@@ -3740,6 +3817,8 @@ const RELATIONSHIP_TYPES = [
3740
3817
  "explains",
3741
3818
  "example-of",
3742
3819
  "supersedes",
3820
+ "derives-from",
3821
+ "follows",
3743
3822
  ];
3744
3823
  server.registerTool("relate", {
3745
3824
  title: "Relate Memories",
@@ -3765,7 +3844,7 @@ server.registerTool("relate", {
3765
3844
  inputSchema: z.object({
3766
3845
  fromId: z.string().describe("Source memory id"),
3767
3846
  toId: z.string().describe("Target memory id"),
3768
- type: z.enum(RELATIONSHIP_TYPES).default("related-to").describe("Relationship type: 'related-to' (same topic), 'explains' (clarifies why), 'example-of' (instance of pattern), 'supersedes' (replaces)"),
3847
+ type: z.enum(RELATIONSHIP_TYPES).default("related-to").describe("Relationship type: 'related-to' (same topic), 'explains' (clarifies why), 'example-of' (instance of pattern), 'supersedes' (replaces), 'derives-from' (derived artifact), 'follows' (sequence order)"),
3769
3848
  bidirectional: z.boolean().optional().default(true).describe("Add relationship in both directions (default: true)"),
3770
3849
  cwd: projectParam,
3771
3850
  }),
@@ -4937,7 +5016,7 @@ server.registerPrompt("mnemonic-workflow-hint", {
4937
5016
  "- When unsure, prefer `recall` over `remember`.\n" +
4938
5017
  "- For repo-related tasks, pass `cwd` so mnemonic can route project memories correctly.\n\n" +
4939
5018
  "Workflow: `recall`/`list` -> `get` -> `update` or `remember` -> `relate`/`consolidate`/`move_memory`. Use `discover_tags` only when tag choice is ambiguous.\n\n" +
4940
- "Roles are optional prioritization hints, not schema. Lifecycle still governs durability. `role: plan` does not imply `temporary`. Inferred roles are internal hints only. Prioritization is language-independent by default.\n\n" +
5019
+ "Roles are optional prioritization hints, not schema. Lifecycle still governs durability. When `lifecycle` is omitted, `remember` applies soft defaults based on role: `research`, `plan`, and `review` default to `temporary`; `decision`, `summary`, and `reference` default to `permanent`. Explicit `lifecycle` always overrides the role-based default. Inferred roles are internal hints only. Prioritization is language-independent by default.\n\n" +
4941
5020
  "### Working-state continuity\n\n" +
4942
5021
  "Preserve in-progress work as temporary notes when continuation value is high. Recovery happens after project orientation.\n\n" +
4943
5022
  "**Checkpoint note structure (temporary notes):**\n" +
@@ -4982,6 +5061,66 @@ server.registerPrompt("mnemonic-workflow-hint", {
4982
5061
  },
4983
5062
  ],
4984
5063
  }));
5064
+ // ── mnemonic-rpi-workflow prompt ───────────────────────────────────────────────
5065
+ const rpiWorkflowPrompt = async () => ({
5066
+ messages: [
5067
+ {
5068
+ role: "user",
5069
+ content: {
5070
+ type: "text",
5071
+ text: "## RPIR workflow: research → plan → implement → review\n\n" +
5072
+ "mnemonic is the artifact store, not the runtime. Store workflow artifacts with correct roles and lifecycle; do not build orchestration in core.\n\n" +
5073
+ "### Request root note\n\n" +
5074
+ "For each RPIR workflow, create one request root note: `role: context`, `lifecycle: temporary`, `tags: [\"workflow\", \"request\"]`. All artifacts relate to it.\n\n" +
5075
+ "### Stage 1 — Research\n\n" +
5076
+ "- Create or update request root note.\n" +
5077
+ "- Create research notes: `role: research`, `lifecycle: temporary`.\n" +
5078
+ "- Distill a short research summary when findings are scattered.\n" +
5079
+ "- Link research `related-to` request root.\n" +
5080
+ "- Before creating research notes, call `recall` to check whether existing notes already cover the topic.\n\n" +
5081
+ "### Stage 2 — Plan\n\n" +
5082
+ "- Create or update one plan note: `role: plan`, `lifecycle: temporary`.\n" +
5083
+ "- Link plan `related-to` request root + key research notes.\n" +
5084
+ "- Keep plan concise and executable.\n" +
5085
+ "- REQUIRES: One current plan per request. Update or supersede when plan evolves.\n" +
5086
+ "- Material changes (architecture, scope, ordering, validation, assumptions): update plan note first, then continue.\n" +
5087
+ "- Non-material changes (wording, phrasing, detail): update inline without branching.\n\n" +
5088
+ "### Stage 3 — Implement\n\n" +
5089
+ "- Create temporary apply/task notes, tagged with `apply`.\n" +
5090
+ "- Use `role: plan` for executable steps. Use `role: context` for observations and checkpoints.\n" +
5091
+ "- Link apply notes `related-to` plan.\n" +
5092
+ "- For non-trivial work, hand narrow context to subagent: request note, current plan or relevant slice, key research notes, narrow file/task scope.\n" +
5093
+ "- Subagent returns: updated apply note, optional review note, recommendation (continue / block / update plan).\n\n" +
5094
+ "### Stage 4 — Review\n\n" +
5095
+ "- Create review notes: `role: review`, `lifecycle: temporary`.\n" +
5096
+ "- Link review `related-to` apply or plan.\n" +
5097
+ "- Fix directly or mark blockers.\n" +
5098
+ "- If review changes the plan materially, update plan note first.\n\n" +
5099
+ "### Stage 5 — Consolidate\n\n" +
5100
+ "At workflow end:\n" +
5101
+ "- Create decision note for resolved approaches (`lifecycle: permanent`).\n" +
5102
+ "- Create summary note for outcome recaps (`lifecycle: permanent`).\n" +
5103
+ "- Promote reusable facts and patterns into permanent reference notes.\n" +
5104
+ "- Let pure scaffolding and redundant checkpoints expire as temporary notes.\n\n" +
5105
+ "### Relationship conventions\n\n" +
5106
+ "Minimal set. Link to immediate upstream artifacts only. No dense cross-linking.\n" +
5107
+ "- research → request root\n" +
5108
+ "- plan → request root + key research notes\n" +
5109
+ "- apply/task → plan\n" +
5110
+ "- review → apply or plan\n" +
5111
+ "- outcome → plan (optionally request root)\n\n" +
5112
+ "### Commit discipline\n\n" +
5113
+ "Three classes: memory (research/plan/review artifacts), work (code/test/docs), memory (consolidation/promotion). When plan changes materially: update notes, commit memory, then continue work.\n\n" +
5114
+ "### Iterate?\n\n" +
5115
+ "Only when review or checks warrant it. Not the default.",
5116
+ },
5117
+ },
5118
+ ],
5119
+ });
5120
+ server.registerPrompt("mnemonic-rpi-workflow", {
5121
+ title: "RPI Workflow: Research → Plan → Implement → Review",
5122
+ description: "Stage protocol and conventions for structured task workflows using mnemonic as artifact store.",
5123
+ }, rpiWorkflowPrompt);
4985
5124
  // ── start ─────────────────────────────────────────────────────────────────────
4986
5125
  await warnAboutPendingMigrationsOnStartup();
4987
5126
  const transport = new StdioServerTransport();