open-memex 0.7.3-alpha.1 → 0.7.3-alpha.2

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
@@ -9,6 +9,8 @@ the `@alpha` tag are development snapshots and are not listed individually.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ - Tool-definition quality pass (Glama TDQS 4.0/5 on v0.7.2): fixed `memory_pr_status` declaring `readOnlyHint` while `apply=true` mutates local files; added explicit MCP annotations (read-only / destructive / open-world) to all 11 tools; descriptions now route between sibling tools (propose vs submit vs promote, resolve vs pr_status, forget vs supersede), state error behavior and real defaults (search limit 8, list limit 20), and the search/list `type` and `limit` parameters are documented. D76 records the rule; `scripts/smoke-mcp.ts` guards the pr_status annotation.
13
+
12
14
  - Design doc maintenance (D75): pruned resolved Open Questions (decided in D56/D57/D61/D68, plus the shipped retrieval-robustness layers) and recorded four competitor-scan directions — retrieval evaluation gate, per-client trust levels, cold-start import, and a `doctor` mis-registration check.
13
15
 
14
16
  - Dropped the deprecated `user` value from the MCP tools' advertised `scope` enums (`personal` is the only global-scope name now; the storage layer still migrates v1 `user` data automatically).
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- 70cc561442c8566b0002af1bb7d40b5f9965232d0469be7f774576d328ebfac6
1
+ c5a82fa9f711dea3b1e8787d551424e978d606435b08f83d51ba5aaf0996e9ff
package/dist/mcp.js CHANGED
@@ -164,51 +164,57 @@ export async function runMcpServer() {
164
164
  server.registerTool("memory_add", {
165
165
  description: TOOL_DESCRIPTIONS.memory_add,
166
166
  inputSchema: z.object(memoryAddArgs),
167
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
167
168
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, addMemory(getScope, cfg, args), "call memory_status to review"))));
168
169
  server.registerTool("memory_search", {
169
170
  description: TOOL_DESCRIPTIONS.memory_search,
170
171
  inputSchema: z.object(memorySearchArgs),
171
- annotations: { readOnlyHint: true },
172
+ annotations: { readOnlyHint: true, openWorldHint: false },
172
173
  }, withSync((args) => toMcp(searchMemories(getScope, args))));
173
174
  server.registerTool("memory_list", {
174
175
  description: TOOL_DESCRIPTIONS.memory_list,
175
176
  inputSchema: z.object(memoryListArgs),
176
- annotations: { readOnlyHint: true },
177
+ annotations: { readOnlyHint: true, openWorldHint: false },
177
178
  }, withSync((args) => toMcp(listMemories(getScope, args))));
178
179
  server.registerTool("memory_supersede", {
179
180
  description: TOOL_DESCRIPTIONS.memory_supersede,
180
181
  inputSchema: z.object(memorySupersedeArgs),
182
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
181
183
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, supersedeMemory(cfg, args), "call memory_status to review"))));
182
184
  server.registerTool("memory_forget", {
183
185
  description: TOOL_DESCRIPTIONS.memory_forget,
184
186
  inputSchema: z.object(memoryForgetArgs),
185
- annotations: { destructiveHint: true },
187
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
186
188
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, forgetMemory(args), "call memory_status to review"))));
187
189
  server.registerTool("memory_status", {
188
190
  description: TOOL_DESCRIPTIONS.memory_status,
189
191
  inputSchema: z.object(memoryStatusArgs),
190
- annotations: { readOnlyHint: true },
192
+ annotations: { readOnlyHint: true, openWorldHint: false },
191
193
  }, withSync((_args) => toMcp(statusMemories())));
192
194
  server.registerTool("memory_submit", {
193
195
  description: TOOL_DESCRIPTIONS.memory_submit,
194
196
  inputSchema: z.object(memorySubmitArgs),
197
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
195
198
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, submitMemoriesOp(args), "call memory_status to review"))));
196
199
  server.registerTool("memory_propose", {
197
200
  description: TOOL_DESCRIPTIONS.memory_propose,
198
201
  inputSchema: z.object(memoryProposeArgs),
202
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
199
203
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, proposeMemoriesOp(args), "call memory_status to review"))));
200
204
  server.registerTool("memory_promote", {
201
205
  description: TOOL_DESCRIPTIONS.memory_promote,
202
206
  inputSchema: z.object(memoryPromoteArgs),
207
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
203
208
  }, withSync((args) => toMcp(promoteMemoryOp(args))));
204
209
  server.registerTool("memory_resolve", {
205
210
  description: TOOL_DESCRIPTIONS.memory_resolve,
206
211
  inputSchema: z.object(memoryResolveArgs),
212
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
207
213
  }, withSync((args) => toMcp(resolveMemoryOp(args))));
208
214
  server.registerTool("memory_pr_status", {
209
215
  description: TOOL_DESCRIPTIONS.memory_pr_status,
210
216
  inputSchema: z.object(memoryPrStatusArgs),
211
- annotations: { readOnlyHint: true },
217
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
212
218
  }, withSync((args) => toMcp(prStatusOp(args))));
213
219
  const transport = new StdioServerTransport();
214
220
  await server.connect(transport);
package/dist/tools/ops.js CHANGED
@@ -38,17 +38,17 @@ export async function withOutboxNote(scopeKey, p, reviewHint) {
38
38
  }
39
39
  /** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
40
40
  export const TOOL_DESCRIPTIONS = {
41
- memory_add: "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects.",
42
- memory_search: "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored.",
43
- memory_list: "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown).",
44
- memory_supersede: "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated.",
45
- memory_forget: "Delete a memory by id. Use when the user asks to forget something. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
46
- memory_status: "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
47
- memory_submit: "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.)",
48
- memory_propose: "Copy personal memories into the project outbox as review drafts. The personal originals stay put. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
49
- memory_promote: "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
50
- memory_resolve: "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
51
- memory_pr_status: "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default; apply=true performs the mapped transitions locally (no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
41
+ memory_add: "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects. Returns the new memory's id; an identical fact already stored returns that id instead of duplicating.",
42
+ memory_search: "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored. Results are ranked by BM25 within review tiers — reviewed (approved/published) project memories come first. `limit` caps how many come back (default 8) and `type` restricts to one memory type.",
43
+ memory_list: "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown). `type` filters to one memory type; `limit` caps how many entries come back.",
44
+ memory_supersede: "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated. Only active memories can be superseded; the new memory inherits type, tags, and aliases unless you pass replacements. An unknown id is reported back as a failure; nothing is written.",
45
+ memory_forget: "Delete a memory by id. Use when the user asks to forget something. To replace an outdated fact with a corrected one instead of deleting it, use memory_supersede. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
46
+ memory_status: "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. For the memories themselves use memory_list; for PR review state use memory_pr_status. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
47
+ memory_submit: "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.) Unknown ids or a failed local commit abort the submit with the reason; nothing is published automatically.",
48
+ memory_propose: "Copy personal memories into the project outbox as review drafts. The personal originals stay put. Use this when personal knowledge should enter a project's review flow; memory_submit then moves the drafts into the repo, and memory_promote advances drafts already there. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
49
+ memory_promote: "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. One step per call; an unknown id or a memory already published errors. Pass `by` to record the reviewer when it should differ from the default (e.g. approving on the strength of someone else's review). For brand-new outbox drafts, memory_submit is the entry point. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
50
+ memory_resolve: "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. Run it when a git pull or merge leaves memory files conflicted; the merged file is written in place (undo with git if the merge is wrong). For PR review state rather than file conflicts, use memory_pr_status. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
51
+ memory_pr_status: "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default — that mode changes nothing; apply=true writes the mapped transitions to the local files (still no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
52
52
  };
53
53
  /** Shared zod input shapes (raw shape, not z.object — hosts wrap as needed). */
54
54
  export const scopeArg = z
@@ -82,16 +82,16 @@ export const memorySearchArgs = {
82
82
  .enum(["project", "personal", "both"])
83
83
  .optional()
84
84
  .describe("Which scope(s) to search. Default: both."),
85
- type: z.string().optional().describe("Restrict to memories of this type."),
86
- limit: z.number().int().min(1).max(50).optional(),
85
+ type: z.string().optional().describe("Restrict to memories of this type (e.g. decision, preference, gotcha)."),
86
+ limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 8)."),
87
87
  };
88
88
  export const memoryListArgs = {
89
89
  scope: z
90
90
  .enum(["project", "personal", "both"])
91
91
  .optional()
92
92
  .describe("Which scope(s) to list. `project` = tied to this repo. `personal` = global across all your projects. `both` lists each scope's newest. Default: project."),
93
- type: z.string().optional(),
94
- limit: z.number().int().min(1).max(100).optional(),
93
+ type: z.string().optional().describe("Only list memories of this type (e.g. decision, preference, gotcha)."),
94
+ limit: z.number().int().min(1).max(100).optional().describe("Max entries to list, newest first (default 20)."),
95
95
  include: z
96
96
  .enum(["active", "all"])
97
97
  .optional()
package/docs/V2-DESIGN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenMemex — Design Document (protocol v0.2)
2
2
 
3
- **Status:** FROZEN — protocol v0.2 (2026-09-26). Decisions D1–D75 are settled; open questions are tracked at the end of this document.
3
+ **Status:** FROZEN — protocol v0.2 (2026-09-26). Decisions D1–D76 are settled; open questions are tracked at the end of this document.
4
4
  **Author:** Stone, with 小沐
5
5
  **Changelog vs v1:** incorporates round-3 review from Perplexity, Grok, Gemini, ChatGPT, DeepSeek.
6
6
  Key changes: Design Principles section; `role` separated from `type`; two iron rules;
@@ -1592,6 +1592,8 @@ requirement: personal data never touches third-party services). Benchmarks to tr
1592
1592
 
1593
1593
  - **D75 — closed questions leave the list; competitor-inspired directions enter it (2026-10-04).** The Open Questions section is a working list, not an archive: the entries decided in D56 (host parity), D57 (retrieval explainability / memory health), D61 (checkpoint proactivity, capture criteria, retrieval layers 2–3), and D68 (memory visibility), plus the retrieval-robustness entry whose three cheap layers all shipped, are removed; the decisions remain the record. A same-day scan of five Glama-listed memory servers (auxly-memory-cli, agent-memory, remnic, Agent-Memory-Bridge, memtomem, memryzed) confirmed the current wedge — proactive in-session capture + one memory shared across editors + PR-based team sharing — appears in none of them whole. Borrowable directions are recorded instead: four new open questions (retrieval evaluation gate, per-client trust levels, cold-start import, session working state) and one roadmap candidate (`doctor` mis-registration check). Nothing here commits implementation; each direction needs its own design pass.
1594
1594
 
1595
+ - **D76 — tool annotations must tell the truth (2026-10-05).** `memory_pr_status` declared `readOnlyHint: true` while `apply=true` writes local review states (caught by Glama's TDQS evaluation of v0.7.2, 4.0/5). Structured hints are part of the protocol surface: all 11 tools now declare read-only / destructive / open-world hints that match their behavior, descriptions route between sibling tools instead of leaving agents to guess, and `scripts/smoke-mcp.ts` guards the pr_status combination. Description honesty is held to the same standard as behavior: ranking claims, defaults, and error semantics in tool text must match the code.
1596
+
1595
1597
  ## Open Questions
1596
1598
 
1597
1599
  _Pruned 2026-10-04 (D75): entries decided in D56/D57/D61/D68 and the shipped layers of the retrieval-robustness question were removed — the decisions are the record. What remains below is genuinely open._
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-memex",
3
- "version": "0.7.3-alpha.1",
3
+ "version": "0.7.3-alpha.2",
4
4
  "mcpName": "io.github.stoneskin/open-memex",
5
5
  "description": "Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.",
6
6
  "keywords": [
@@ -85,6 +85,9 @@ try {
85
85
  JSON.stringify(["memory_add", "memory_forget", "memory_list", "memory_pr_status", "memory_promote", "memory_propose", "memory_resolve", "memory_search", "memory_status", "memory_submit", "memory_supersede"]),
86
86
  names.join(","),
87
87
  );
88
+ // D76: annotations must tell the truth — pr_status mutates with apply=true.
89
+ const prStatusTool = tools.result.tools.find((t: any) => t.name === "memory_pr_status");
90
+ check("pr_status is not read-only (D76)", prStatusTool?.annotations?.readOnlyHint === false, JSON.stringify(prStatusTool?.annotations));
88
91
  const addSchema = tools.result.tools.find((t: any) => t.name === "memory_add").inputSchema;
89
92
  check("memory_add schema has content+type+scope+tags", !!addSchema.properties?.content && !!addSchema.properties?.type, Object.keys(addSchema.properties ?? {}).join(","));
90
93
 
package/src/mcp.ts CHANGED
@@ -209,6 +209,7 @@ export async function runMcpServer() {
209
209
  {
210
210
  description: TOOL_DESCRIPTIONS.memory_add,
211
211
  inputSchema: z.object(memoryAddArgs),
212
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
212
213
  },
213
214
  withSync((args) => toMcp(withOutboxNote(getScope().key, addMemory(getScope, cfg, args), "call memory_status to review"))),
214
215
  );
@@ -218,7 +219,7 @@ export async function runMcpServer() {
218
219
  {
219
220
  description: TOOL_DESCRIPTIONS.memory_search,
220
221
  inputSchema: z.object(memorySearchArgs),
221
- annotations: { readOnlyHint: true },
222
+ annotations: { readOnlyHint: true, openWorldHint: false },
222
223
  },
223
224
  withSync((args) => toMcp(searchMemories(getScope, args))),
224
225
  );
@@ -228,7 +229,7 @@ export async function runMcpServer() {
228
229
  {
229
230
  description: TOOL_DESCRIPTIONS.memory_list,
230
231
  inputSchema: z.object(memoryListArgs),
231
- annotations: { readOnlyHint: true },
232
+ annotations: { readOnlyHint: true, openWorldHint: false },
232
233
  },
233
234
  withSync((args) => toMcp(listMemories(getScope, args))),
234
235
  );
@@ -238,6 +239,7 @@ export async function runMcpServer() {
238
239
  {
239
240
  description: TOOL_DESCRIPTIONS.memory_supersede,
240
241
  inputSchema: z.object(memorySupersedeArgs),
242
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
241
243
  },
242
244
  withSync((args) => toMcp(withOutboxNote(getScope().key, supersedeMemory(cfg, args), "call memory_status to review"))),
243
245
  );
@@ -247,7 +249,7 @@ export async function runMcpServer() {
247
249
  {
248
250
  description: TOOL_DESCRIPTIONS.memory_forget,
249
251
  inputSchema: z.object(memoryForgetArgs),
250
- annotations: { destructiveHint: true },
252
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
251
253
  },
252
254
  withSync((args) => toMcp(withOutboxNote(getScope().key, forgetMemory(args), "call memory_status to review"))),
253
255
  );
@@ -257,7 +259,7 @@ export async function runMcpServer() {
257
259
  {
258
260
  description: TOOL_DESCRIPTIONS.memory_status,
259
261
  inputSchema: z.object(memoryStatusArgs),
260
- annotations: { readOnlyHint: true },
262
+ annotations: { readOnlyHint: true, openWorldHint: false },
261
263
  },
262
264
  withSync((_args) => toMcp(statusMemories())),
263
265
  );
@@ -267,6 +269,7 @@ export async function runMcpServer() {
267
269
  {
268
270
  description: TOOL_DESCRIPTIONS.memory_submit,
269
271
  inputSchema: z.object(memorySubmitArgs),
272
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
270
273
  },
271
274
  withSync((args) => toMcp(withOutboxNote(getScope().key, submitMemoriesOp(args), "call memory_status to review"))),
272
275
  );
@@ -276,6 +279,7 @@ export async function runMcpServer() {
276
279
  {
277
280
  description: TOOL_DESCRIPTIONS.memory_propose,
278
281
  inputSchema: z.object(memoryProposeArgs),
282
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
279
283
  },
280
284
  withSync((args) => toMcp(withOutboxNote(getScope().key, proposeMemoriesOp(args), "call memory_status to review"))),
281
285
  );
@@ -285,6 +289,7 @@ export async function runMcpServer() {
285
289
  {
286
290
  description: TOOL_DESCRIPTIONS.memory_promote,
287
291
  inputSchema: z.object(memoryPromoteArgs),
292
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
288
293
  },
289
294
  withSync((args) => toMcp(promoteMemoryOp(args))),
290
295
  );
@@ -294,6 +299,7 @@ export async function runMcpServer() {
294
299
  {
295
300
  description: TOOL_DESCRIPTIONS.memory_resolve,
296
301
  inputSchema: z.object(memoryResolveArgs),
302
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
297
303
  },
298
304
  withSync((args) => toMcp(resolveMemoryOp(args))),
299
305
  );
@@ -303,7 +309,7 @@ export async function runMcpServer() {
303
309
  {
304
310
  description: TOOL_DESCRIPTIONS.memory_pr_status,
305
311
  inputSchema: z.object(memoryPrStatusArgs),
306
- annotations: { readOnlyHint: true },
312
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
307
313
  },
308
314
  withSync((args) => toMcp(prStatusOp(args))),
309
315
  );
package/src/tools/ops.ts CHANGED
@@ -66,27 +66,27 @@ export async function withOutboxNote(
66
66
  /** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
67
67
  export const TOOL_DESCRIPTIONS = {
68
68
  memory_add:
69
- "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects.",
69
+ "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects. Returns the new memory's id; an identical fact already stored returns that id instead of duplicating.",
70
70
  memory_search:
71
- "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored.",
71
+ "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored. Results are ranked by BM25 within review tiers — reviewed (approved/published) project memories come first. `limit` caps how many come back (default 8) and `type` restricts to one memory type.",
72
72
  memory_list:
73
- "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown).",
73
+ "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown). `type` filters to one memory type; `limit` caps how many entries come back.",
74
74
  memory_supersede:
75
- "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated.",
75
+ "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated. Only active memories can be superseded; the new memory inherits type, tags, and aliases unless you pass replacements. An unknown id is reported back as a failure; nothing is written.",
76
76
  memory_forget:
77
- "Delete a memory by id. Use when the user asks to forget something. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
77
+ "Delete a memory by id. Use when the user asks to forget something. To replace an outdated fact with a corrected one instead of deleting it, use memory_supersede. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
78
78
  memory_status:
79
- "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
79
+ "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. For the memories themselves use memory_list; for PR review state use memory_pr_status. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
80
80
  memory_submit:
81
- "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.)",
81
+ "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.) Unknown ids or a failed local commit abort the submit with the reason; nothing is published automatically.",
82
82
  memory_propose:
83
- "Copy personal memories into the project outbox as review drafts. The personal originals stay put. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
83
+ "Copy personal memories into the project outbox as review drafts. The personal originals stay put. Use this when personal knowledge should enter a project's review flow; memory_submit then moves the drafts into the repo, and memory_promote advances drafts already there. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
84
84
  memory_promote:
85
- "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
85
+ "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. One step per call; an unknown id or a memory already published errors. Pass `by` to record the reviewer when it should differ from the default (e.g. approving on the strength of someone else's review). For brand-new outbox drafts, memory_submit is the entry point. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
86
86
  memory_resolve:
87
- "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
87
+ "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. Run it when a git pull or merge leaves memory files conflicted; the merged file is written in place (undo with git if the merge is wrong). For PR review state rather than file conflicts, use memory_pr_status. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
88
88
  memory_pr_status:
89
- "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default; apply=true performs the mapped transitions locally (no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
89
+ "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default — that mode changes nothing; apply=true writes the mapped transitions to the local files (still no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
90
90
  } as const;
91
91
 
92
92
  /** Shared zod input shapes (raw shape, not z.object — hosts wrap as needed). */
@@ -131,8 +131,8 @@ export const memorySearchArgs = {
131
131
  .enum(["project", "personal", "both"])
132
132
  .optional()
133
133
  .describe("Which scope(s) to search. Default: both."),
134
- type: z.string().optional().describe("Restrict to memories of this type."),
135
- limit: z.number().int().min(1).max(50).optional(),
134
+ type: z.string().optional().describe("Restrict to memories of this type (e.g. decision, preference, gotcha)."),
135
+ limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 8)."),
136
136
  };
137
137
  export type MemorySearchArgs = z.infer<z.ZodObject<typeof memorySearchArgs>>;
138
138
 
@@ -143,8 +143,8 @@ export const memoryListArgs = {
143
143
  .describe(
144
144
  "Which scope(s) to list. `project` = tied to this repo. `personal` = global across all your projects. `both` lists each scope's newest. Default: project.",
145
145
  ),
146
- type: z.string().optional(),
147
- limit: z.number().int().min(1).max(100).optional(),
146
+ type: z.string().optional().describe("Only list memories of this type (e.g. decision, preference, gotcha)."),
147
+ limit: z.number().int().min(1).max(100).optional().describe("Max entries to list, newest first (default 20)."),
148
148
  include: z
149
149
  .enum(["active", "all"])
150
150
  .optional()