open-memex 0.4.0-alpha.1 → 0.4.0-alpha.5

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/AGENTS.md CHANGED
@@ -29,8 +29,9 @@ Consequences:
29
29
  npm install # once
30
30
  npm run typecheck # tsc --noEmit — the only lint/type gate
31
31
  npm run cli -- where | list | search "q" | add ... | forget <id> | reindex
32
+ npm run cli -- <command> --help # per-command help (AI assistants discover flags this way)
32
33
  npm run cli -- sync-status # last sync time/kind + outbox drafts + repo review states + uncommitted files
33
- npm run cli -- submit <id...> [--onto <branch>] [--base <branch>] # drafts → .ai/open-memex/ (local branch+commit)
34
+ npm run cli -- submit <id...> [--branch <name>] [--base <branch>] # drafts → .ai/open-memex/ (current branch + local commit; never auto-branches)
34
35
  npm run cli -- propose <id...> --to project [--local-approve] # copy personal → project outbox (batch OK)
35
36
  npm run cli -- promote <id> [--reject] [--resubmit] [--note "..."] # proposed → approved → published (audit trail appended)
36
37
  npm run cli -- pr-status [--apply] # map branch PR's GitHub state onto review_state (report; apply = local only)
package/README.md CHANGED
@@ -262,6 +262,7 @@ open-memex doctor # environment health check
262
262
  open-memex capture --dry-run "记住我喜欢简洁的回答" # preview keyword capture
263
263
  open-memex mcp --print-config vscode|cursor|claude|opencode|visualstudio
264
264
  open-memex --help # this reference
265
+ open-memex <command> --help # help for one command
265
266
  open-memex --version # installed version
266
267
  ```
267
268
 
@@ -282,6 +283,11 @@ Project drafts live in the **appdata outbox** (git-invisible, branch-independent
282
283
  only user-approved drafts move into `<repo>/.ai/open-memex/`, where they follow
283
284
  branches and PRs. Nothing moves without you naming it.
284
285
 
286
+ In an AI chat with the MCP server connected, just say **"sync memory"**
287
+ (or "同步记忆") — the agent runs the status check, summarizes the outbox drafts,
288
+ and asks which ones to sync. The agent also proposes this on its own at session
289
+ start and at work checkpoints.
290
+
285
291
  ```sh
286
292
  open-memex sync-status
287
293
  # show when the index was last synced (and what triggered it), the outbox
@@ -289,14 +295,16 @@ open-memex sync-status
289
295
  # (draft / proposed / approved / published / rejected),
290
296
  # and any uncommitted repo memory files.
291
297
 
292
- open-memex submit <id...> [--onto <branch>] [--base <branch>]
298
+ open-memex submit <id...> [--branch <name>] [--base <branch>]
293
299
  # move your named drafts into .ai/open-memex/ as "proposed":
294
- # creates mem/sync-<timestamp> (or stays on --onto for a code+memory PR),
295
- # copies, flips review_state, local git commit. All-or-nothing; conflicts
296
- # (same id, different content) abort cleanly. Prints the push + gh pr
297
- # commands; an agent holding your Yes carries through push/PR itself.
298
- # Default PR base is the current branch; --base redirects to main or your
299
- # integration branch.
300
+ # copies, flips review_state, local git commit ON THE CURRENT BRANCH.
301
+ # Never creates a branch on its own — branch creation is your call
302
+ # (or the agent's, only with your explicit approval for the full chain).
303
+ # All-or-nothing; conflicts (same id, different content) abort cleanly.
304
+ # Prints the push + gh pr commands; an agent holding your Yes carries
305
+ # through push/PR itself. --branch <name> creates the branch first
306
+ # (agent full-chain path). Default PR base is the current branch; --base
307
+ # redirects to main or your integration branch.
300
308
 
301
309
  open-memex pr-status [--apply]
302
310
  # read the branch's GitHub PR and map its state onto each in-repo memory:
@@ -352,8 +360,11 @@ server with cwd set to your project root (`init` handles this for you).
352
360
 
353
361
  > **Note:** MCP is request/response — it gives the agent tools, not the opencode
354
362
  > plugin's automatic keyword capture or first-turn context injection. Proactive
355
- > memory use depends on the agent's instructions (the Copilot instructions
356
- > that `init` writes).
363
+ > memory use depends on the agent's instructions: the server sends session-start
364
+ > guidance (call `memory_status` at session start and at checkpoints) in the MCP
365
+ > handshake `instructions`, and `init` writes the fuller version into the
366
+ > editor's instruction files. Both are advisory — no MCP consumer offers a hard
367
+ > session-start hook.
357
368
 
358
369
  ## Roadmap
359
370
 
package/README.zh-CN.md CHANGED
@@ -260,6 +260,7 @@ open-memex doctor # 环境健康检查
260
260
  open-memex capture --dry-run "记住我喜欢简洁的回答" # 预览关键词捕获
261
261
  open-memex mcp --print-config vscode|cursor|claude|opencode|visualstudio
262
262
  open-memex --help # 本帮助
263
+ open-memex <command> --help # 单个命令的帮助
263
264
  open-memex --version # 已安装版本
264
265
  ```
265
266
 
@@ -280,17 +281,23 @@ project 草稿先住在 **appdata outbox**(git 看不见、跟分支无关)
280
281
  点名批准的草稿,才会被移入 `<repo>/.ai/open-memex/`,之后随分支和 PR 走。
281
282
  没经过你点名,什么都不会动。
282
283
 
284
+ 在接了 MCP 服务器的 AI 对话里,直接说 **"同步记忆"**(或 "sync memory")——
285
+ agent 会查状态、把 outbox 草稿逐条摘要、问你同步哪几条。agent 也会在新对话
286
+ 开始和任务检查点主动提这件事。
287
+
283
288
  ```sh
284
289
  open-memex sync-status
285
290
  # 看索引上次同步的时间和触发方、outbox(待同步)、repo 里的评审状态
286
291
  # (draft / proposed / approved / published / rejected),
287
292
  # 以及 repo 里还没 commit 的记忆文件。
288
293
 
289
- open-memex submit <id...> [--onto <branch>] [--base <branch>]
294
+ open-memex submit <id...> [--branch <name>] [--base <branch>]
290
295
  # 把你点名的草稿移入 .ai/open-memex/,状态变为 proposed:
291
- # 建 mem/sync-<timestamp> 分支(或 --onto 当前分支,跟代码走同一个 PR),
292
- # 复制、改 review_state、本地 git commit。全有或全无;冲突(同 id 不同内容)
293
- # 干净回滚。打印 push + gh pr 命令;Agent 拿到你的 Yes 后会自己走完 push/PR。
296
+ # 复制、改 review_state、在当前分支本地 git commit。
297
+ # 永不自动建分支——建分支是你说了算(或 Agent 拿到你明确批准走全链时)。
298
+ # 全有或全无;冲突(同 id 不同内容)干净回滚。
299
+ # 打印 push + gh pr 命令;Agent 拿到你的 Yes 后会自己走完 push/PR。
300
+ # --branch <name> 先建分支再提交(Agent 全链路径)。
294
301
  # PR 默认 base 是当前分支;--base 可改到 main 或集成支。
295
302
 
296
303
  open-memex pr-status [--apply]
@@ -344,7 +351,10 @@ project scope 从进程工作目录解析,所以配置 server 时 cwd 要指
344
351
 
345
352
  > **注意:** MCP 是请求/响应式的——它给 agent 提供 tools,但没有 opencode
346
353
  > 插件的关键词自动捕获和首轮上下文注入。想让 agent 主动用记忆,
347
- > 靠的是 agent 的 instructions(`init` 写的 Copilot instructions)。
354
+ > 靠的是 agent 的 instructions:服务器在 MCP 握手的 `instructions` 里自带
355
+ > session-start 指引(开场调 `memory_status`、检查点再调),`init` 则把更完整
356
+ > 的版本写进编辑器的 instruction 文件。两者都是建议性的——MCP 客户端没有
357
+ > 强制的 session-start hook。
348
358
 
349
359
  ## 路线图(Roadmap)
350
360
 
package/dist/cli.js CHANGED
@@ -18,6 +18,168 @@ import { resolveMcpCommand } from "./init.js";
18
18
  import fs from "node:fs";
19
19
  import path from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
+ /** Per-command help, printed by `open-memex <command> --help`.
22
+ AI assistants discover the CLI through --help, so every command needs one. */
23
+ const COMMAND_HELP = {
24
+ where: `Show which project scope the current directory resolves to, and where its data lives.
25
+
26
+ Usage: open-memex where`,
27
+ list: `List memories in a scope, newest first.
28
+
29
+ Usage: open-memex list [--scope project|personal] [--type T] [--limit N]
30
+
31
+ Flags:
32
+ --scope project (default) or personal
33
+ --type filter by memory type
34
+ --limit max results
35
+
36
+ Examples:
37
+ open-memex list
38
+ open-memex list --scope personal --limit 20`,
39
+ search: `Search memories by keyword (BM25 full-text), best matches first.
40
+
41
+ Usage: open-memex search "query" [--scope project|personal|both] [--type T] [--limit N]
42
+
43
+ Flags:
44
+ --scope project (default), personal, or both
45
+ --type filter by memory type
46
+ --limit max results
47
+
48
+ Example:
49
+ open-memex search "deploy checklist" --scope both`,
50
+ add: `Save a fact, preference, decision, or note to local memory.
51
+
52
+ Usage: open-memex add "content" [--scope project|personal] [--type T] [--tag t1,t2]
53
+
54
+ Flags:
55
+ --scope project (default) or personal (personal never leaves this machine)
56
+ --type memory type (default: fact)
57
+ --tag comma-separated tags
58
+
59
+ Example:
60
+ open-memex add "We deploy on Fridays" --scope project --tag process`,
61
+ supersede: `Replace a memory with a newer version. The old one is kept as history.
62
+
63
+ Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]`,
64
+ status: `Change a memory's lifecycle status.
65
+
66
+ Usage: open-memex status <id> active|deprecated|retracted|archived`,
67
+ forget: `Delete a memory by id.
68
+
69
+ Usage: open-memex forget <id>`,
70
+ propose: `Copy personal memories into the project outbox as review drafts.
71
+ The personal originals stay put. Nothing enters git at this step.
72
+
73
+ Usage: open-memex propose <id...> --to project [--local-approve]
74
+
75
+ Flags:
76
+ --to project (required)
77
+ --local-approve mark the copies approved right away (solo-dev shortcut)
78
+
79
+ Example:
80
+ open-memex propose 01ABC 01DEF --to project`,
81
+ promote: `Advance a project memory one step up the review ladder
82
+ (proposed → approved → published), or reject it with a note.
83
+ Every transition is appended to the memory's review_history.
84
+ Rejected memories are never deleted — they can be revised and resubmitted.
85
+
86
+ Usage: open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
87
+
88
+ Flags:
89
+ --reject move back to rejected (requires --note)
90
+ --resubmit move a rejected memory back to proposed
91
+ --note reason for the transition (recorded in review_history)
92
+ --by reviewer name (defaults to the git user)
93
+
94
+ Examples:
95
+ open-memex promote 01ABC --note "verified against the runbook"
96
+ open-memex promote 01ABC --reject --note "outdated after the migration"`,
97
+ resolve: `List conflicted memories, or 3-way-merge one.
98
+
99
+ Usage: open-memex resolve [id-or-path]
100
+
101
+ With no argument, lists conflicts. With an id or file path, shows the
102
+ 3-way merge (base / outbox / repo) so you can resolve it by hand.
103
+ Conflicts are never auto-resolved.`,
104
+ "sync-status": `Show the project memory sync pipeline: when the index last synced
105
+ and what triggered it, drafts waiting in the outbox (appdata), memories in the
106
+ repo awaiting review or published, and repo files not yet committed.
107
+
108
+ Usage: open-memex sync-status`,
109
+ submit: `Move outbox drafts into the repo for review: copies the drafts into
110
+ the repo memory dir as proposed (a local-approved copy keeps its approval),
111
+ commits locally on the CURRENT branch, and moves the outbox originals out.
112
+ Never creates a branch on its own — branch creation is your call.
113
+
114
+ Usage: open-memex submit <id...> [--branch <name>] [--base <branch>]
115
+
116
+ Flags:
117
+ --branch create this branch and submit onto it (only with your explicit
118
+ approval for the full chain); default: stay on current branch
119
+ --base PR base override (default: the branch you're on)
120
+
121
+ Example:
122
+ open-memex submit 01ABC 01DEF`,
123
+ "pr-status": `Map the current branch's GitHub PR state back onto review_state:
124
+ merged → published, approval → approved (approved_by = the reviewer),
125
+ changes-requested → suggestion only (never auto-rejects).
126
+ Each memory in the PR is mapped independently; a human rejection is never
127
+ overwritten. Report-only by default.
128
+
129
+ Usage: open-memex pr-status [--apply]
130
+
131
+ Flags:
132
+ --apply write the transitions locally (still never pushes)`,
133
+ reindex: `Rebuild the SQLite index from the markdown files.
134
+
135
+ Usage: open-memex reindex`,
136
+ scopes: `List the known scopes (personal + project).
137
+
138
+ Usage: open-memex scopes`,
139
+ migrate: `Move memories between scopes, or convert a legacy my-o-memory data dir.
140
+
141
+ Usage: open-memex migrate [--from <key>] [--to <key>] [--dry-run] [--on-conflict newer|overwrite|skip]
142
+ open-memex migrate --to-v2 [--dry-run]
143
+
144
+ Flags:
145
+ --from / --to scope keys (default: current project → personal)
146
+ --dry-run preview without moving anything
147
+ --on-conflict newer (default), overwrite, or skip
148
+ --to-v2 convert a legacy my-o-memory data dir to the v2 layout
149
+
150
+ Always preview with --dry-run first; nothing moves without confirmation.`,
151
+ mcp: `Start the stdio MCP server (the same server editors connect to).
152
+
153
+ Usage: open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
154
+
155
+ Flags:
156
+ --print-config print the MCP client config instead of starting the server`,
157
+ init: `One-command project setup: writes the MCP config for your editor and the
158
+ agent memory instructions. Existing files are merged, never clobbered.
159
+
160
+ Usage: open-memex init [--client vscode|cursor|opencode|visualstudio]
161
+ [--instructions personal|project] [--force] [--yes]
162
+
163
+ Flags:
164
+ --client editor to configure (default: auto-detect)
165
+ --instructions personal (default, ~/.copilot/copilot-instructions.md) or project
166
+ --force overwrite existing config
167
+ --yes accept all defaults, never prompt`,
168
+ config: `Show config, or set a key.
169
+
170
+ Usage: open-memex config [set <key> <value>]
171
+
172
+ Example:
173
+ open-memex config set sync.autoPull false`,
174
+ capture: `Preview what the keyword-capture watcher would extract from text.
175
+
176
+ Usage: open-memex capture --dry-run "text"`,
177
+ doctor: `Environment health check: Node version, config source, scope resolution,
178
+ storage writability, then boots a real MCP server and runs initialize +
179
+ tools/list against it — all eleven tools must show up.
180
+
181
+ Usage: open-memex doctor`,
182
+ };
21
183
  function usage(exitCode = 1) {
22
184
  console.log(`open-memex CLI
23
185
 
@@ -33,7 +195,7 @@ Usage:
33
195
  open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
34
196
  open-memex resolve [id-or-path]
35
197
  open-memex sync-status
36
- open-memex submit <id...> [--onto <branch>] [--base <branch>]
198
+ open-memex submit <id...> [--branch <name>] [--base <branch>]
37
199
  open-memex pr-status [--apply]
38
200
  open-memex reindex
39
201
  open-memex scopes
@@ -72,7 +234,9 @@ the \`--from\` key when migrating.
72
234
  git remote after memories were already stored under the cwd-based key.
73
235
  \`migrate --to-v2\` converts v1 memory files to the v2 format (§19):
74
236
  user→personal scope rename, epoch→RFC 3339 times, priority→importance,
75
- type: instruction→role split. Always preview with --dry-run first.`);
237
+ type: instruction→role split. Always preview with --dry-run first.
238
+
239
+ Run \`open-memex <command> --help\` for details on a single command.`);
76
240
  process.exit(exitCode);
77
241
  }
78
242
  function parseFlags(argv) {
@@ -172,6 +336,16 @@ async function main() {
172
336
  console.log(`open-memex ${pkg.version}`);
173
337
  return;
174
338
  }
339
+ // Per-command help: `open-memex <command> --help`. Checked before loadConfig()
340
+ // so it works even when the environment is broken.
341
+ if (rest.includes("--help") || rest.includes("-h")) {
342
+ const h = COMMAND_HELP[cmd];
343
+ if (h) {
344
+ console.log(`open-memex ${cmd}\n\n${h}`);
345
+ return;
346
+ }
347
+ usage(0);
348
+ }
175
349
  const cfg = loadConfig();
176
350
  const project = resolveProjectScope(process.cwd());
177
351
  // `migrate --to-v2` is a pure file operation (V2-DESIGN §19) — it runs
@@ -637,7 +811,7 @@ async function main() {
637
811
  usage();
638
812
  syncScope(project.key, "submit");
639
813
  try {
640
- const r = submitMemories(ids, { onto: flags.onto, base: flags.base });
814
+ const r = submitMemories(ids, { branch: flags.branch, base: flags.base });
641
815
  for (const s of r.submitted) {
642
816
  console.log(`submitted ${s.id} → ${path.relative(process.cwd(), s.filePath)} [${s.reviewState}]`);
643
817
  }
package/dist/init.js CHANGED
@@ -59,17 +59,22 @@ You have a local memory MCP server (\`open-memex\`) with eleven tools:
59
59
  Project memories you save land in a local outbox first — they are NOT in git yet.
60
60
  Syncing them into the repo for review is an explicit, user-approved step:
61
61
 
62
- - At session start, and when you finish a meaningful chunk of work, call
62
+ - At session start, when you finish a meaningful chunk of work, after the user
63
+ commits (git commit), and after any memory_* action completes, call
63
64
  \`memory_status\`. If the outbox has drafts, summarize them (one line each) and ask
64
65
  the user which ones to sync. Sync NOTHING the user did not name.
65
- - When the user approves, ask ONE follow-up: a separate memory-only branch/PR
66
- (recommended), or fold the memories into the current branch alongside code?
67
- A "yes, you do it" answer covers the whole chain — branch, local commit,
68
- push, PR creation — do NOT re-ask at each step. If the user says they will
69
- do it themselves, hand them the printed push/PR commands and do nothing.
70
- Then call \`memory_submit\` with the approved ids (\`onto\` = current branch name
71
- only for the fold-into-code path). It creates the branch, copies the drafts in
72
- as \`proposed\`, commits locally, and prints the push + PR commands.
66
+ - When the user says "sync memory" (or "同步记忆"), treat it as a request to run
67
+ the sync flow above: call \`memory_status\`, summarize the outbox drafts, and ask
68
+ which ones to sync.
69
+ - When the user approves, call \`memory_submit\` with the approved ids. It copies
70
+ the drafts into the repo as \`proposed\`, commits locally on the CURRENT branch,
71
+ and prints the push + PR commands. It NEVER creates a branch on its own.
72
+ - After the submit, ask ONE follow-up: "want me to create a branch + push +
73
+ open the PR, or will you handle it yourself?" A "yes, you do it" answer covers
74
+ the whole chain — branch creation, push, PR creation — do NOT re-ask at each
75
+ step. If the user says they will do it themselves, hand them the printed
76
+ push/PR commands and do nothing. NEVER create branches, push, or open PRs
77
+ without their explicit approval.
73
78
  - Base branch for the memory PR defaults to the branch you are on; the user may
74
79
  redirect it to the integration branch (main) for branch-independent knowledge.
75
80
  - If anything conflicts (same id with different content, push rejected), STOP and
package/dist/mcp.js CHANGED
@@ -26,6 +26,38 @@ import { db } from "./store/db.js";
26
26
  import { syncScope } from "./store/sync.js";
27
27
  import { addMemory, searchMemories, listMemories, supersedeMemory, forgetMemory, statusMemories, submitMemoriesOp, proposeMemoriesOp, promoteMemoryOp, resolveMemoryOp, prStatusOp, memoryAddArgs, memorySearchArgs, memoryListArgs, memorySupersedeArgs, memoryForgetArgs, memoryStatusArgs, memorySubmitArgs, memoryProposeArgs, memoryPromoteArgs, memoryResolveArgs, memoryPrStatusArgs, TOOL_DESCRIPTIONS, } from "./tools/ops.js";
28
28
  const SERVER_VERSION = "0.2.0-alpha";
29
+ /**
30
+ * D26: session-start guidance delivered through the MCP handshake itself.
31
+ * The init-written instruction files only exist if the user ran
32
+ * `open-memex init --client`; the initialize `instructions` reach every MCP
33
+ * client at connect time. Still advisory — no MCP consumer offers a hard
34
+ * session-start hook — but it is the strongest signal available.
35
+ */
36
+ const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server
37
+ (eleven memory_* tools: add, search, list, supersede, forget, status, submit,
38
+ propose, promote, resolve, pr_status).
39
+
40
+ - At the START of this session, when you finish a meaningful chunk of work,
41
+ after the user commits (git commit), and after any memory_* action completes,
42
+ call memory_status. If the project outbox has drafts waiting for review,
43
+ summarize them (one line each) and ask the user which ones to sync into the
44
+ repo. Sync NOTHING the user did not name.
45
+ - When the user says "sync memory" (or "同步记忆"), treat it as a request to run
46
+ the sync flow: call memory_status, summarize the outbox drafts, and ask which
47
+ ones to sync. ALWAYS use the memory_status tool for this — never browse the
48
+ appdata directory directly.
49
+ - After memory_submit, ask ONE follow-up: "want me to create a branch + push +
50
+ open the PR, or will you handle it yourself?" NEVER create branches, push, or
51
+ open PRs without the user's explicit approval. A "yes, you do it" covers the
52
+ whole chain — do NOT re-ask at each step.
53
+ - BE PROACTIVE: when the user shares something worth remembering across sessions
54
+ (a decision, a preference, a project convention, a fix and its cause), call
55
+ memory_add without being asked. Keep each memory to one self-contained statement.
56
+ - Before asking the user about past decisions, conventions, or preferences they
57
+ may have told you before, call memory_search first.
58
+ - Memories default to this project's scope; use the personal scope for facts
59
+ about the user that hold across all projects.
60
+ - personal scope memories NEVER leave this machine.`;
29
61
  /** Adapt a framework-agnostic op result to an MCP tool response. */
30
62
  function toMcp(p) {
31
63
  return p.then((r) => ({ content: [{ type: "text", text: r.output }] }), (e) => ({
@@ -57,7 +89,7 @@ export async function runMcpServer() {
57
89
  return fn(args);
58
90
  };
59
91
  };
60
- const server = new McpServer({ name: "open-memex", version: SERVER_VERSION });
92
+ const server = new McpServer({ name: "open-memex", version: SERVER_VERSION }, { instructions: SERVER_INSTRUCTIONS });
61
93
  server.registerTool("memory_add", {
62
94
  description: TOOL_DESCRIPTIONS.memory_add,
63
95
  inputSchema: z.object(memoryAddArgs),
package/dist/submit.js CHANGED
@@ -150,7 +150,8 @@ export function submitMemories(ids, opts = {}) {
150
150
  if (ids.length === 0)
151
151
  fail("submit needs at least one memory id");
152
152
  const root = projectRoot();
153
- // submit needs a git repo — the whole point is branch + commit.
153
+ // submit needs a git repo — the memories land in .ai/open-memex/ and are
154
+ // committed locally. Branch creation is never automatic (D36).
154
155
  git(root, ["rev-parse", "--git-dir"]);
155
156
  const scope = resolveProjectScope(root);
156
157
  const cfg = loadConfig();
@@ -197,26 +198,26 @@ export function submitMemories(ids, opts = {}) {
197
198
  fail(`submit aborted — nothing was written:\n ${problems.join("\n ")}`);
198
199
  }
199
200
  // 2. Resolve the target branch.
201
+ // D36: submit never creates a branch on its own — it works on the branch
202
+ // you're already on. Pass --branch <name> (explicitly) only when the user
203
+ // approved the full chain (branch + push + PR).
200
204
  const startBranch = git(root, ["branch", "--show-current"]) || git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
201
- let branch;
202
- if (opts.onto) {
203
- if (opts.onto !== startBranch) {
204
- fail(`--onto ${opts.onto} is not the current branch (${startBranch || "(detached)"}). submit only targets the branch you're on.`);
205
- }
206
- branch = opts.onto;
207
- }
208
- else {
209
- const stamp = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15);
210
- branch = `mem/sync-${stamp}`;
205
+ let branch = startBranch;
206
+ let createdBranch = false;
207
+ if (opts.branch) {
208
+ branch = opts.branch;
211
209
  let n = 0;
210
+ let candidate = branch;
212
211
  while (true) {
213
- const exists = git(root, ["branch", "--list", branch]);
212
+ const exists = git(root, ["branch", "--list", candidate]);
214
213
  if (!exists)
215
214
  break;
216
215
  n++;
217
- branch = `mem/sync-${stamp}-${n}`;
216
+ candidate = `${branch}-${n}`;
218
217
  }
218
+ branch = candidate;
219
219
  git(root, ["checkout", "-b", branch]);
220
+ createdBranch = true;
220
221
  }
221
222
  const base = opts.base ?? startBranch;
222
223
  const author = currentAuthor();
@@ -278,7 +279,7 @@ export function submitMemories(ids, opts = {}) {
278
279
  }
279
280
  // If we created the branch and never committed, remove it too — an
280
281
  // aborted submit leaves no trace.
281
- if (!opts.onto) {
282
+ if (createdBranch) {
282
283
  try {
283
284
  git(root, ["checkout", "--quiet", startBranch]);
284
285
  git(root, ["branch", "--quiet", "-D", branch]);
@@ -327,13 +328,20 @@ export function submitMemories(ids, opts = {}) {
327
328
  removed: 0,
328
329
  scanned: validated.length,
329
330
  });
331
+ // D36: no auto-branch — the commit sits on the branch you were already on.
332
+ // Next steps are printed, not run. For a separate memory PR, create a branch
333
+ // first (the commit comes along), then push + open the PR.
334
+ const branchCmd = `git checkout -b mem/sync-YYYYMMDD`;
330
335
  return {
331
336
  branch,
332
337
  base,
333
338
  submitted,
334
339
  skippedIdentical,
335
340
  committed,
341
+ createdBranch,
336
342
  pushCommand: `git push -u origin ${branch}`,
337
- prCommand: `gh pr create --base ${base} --title "mem: review ${validated.length} ${validated.length === 1 ? "memory" : "memories"}" --body "Submitted from the open-memex outbox: ${ids8}."`,
343
+ prCommand: createdBranch
344
+ ? `gh pr create --base ${base} --title "mem: review ${validated.length} ${validated.length === 1 ? "memory" : "memories"}" --body "Submitted from the open-memex outbox: ${ids8}."`
345
+ : `${branchCmd} # if you want a separate memory PR (else push ${branch} directly)\n git push -u origin <new-branch> && gh pr create --base ${base} --title "mem: review ${validated.length}" --body "Submitted from the open-memex outbox: ${ids8}."`,
338
346
  };
339
347
  }
package/dist/tools/ops.js CHANGED
@@ -25,8 +25,8 @@ export const TOOL_DESCRIPTIONS = {
25
25
  memory_list: "List memories in a scope, newest first. Useful for browsing what is remembered, or verifying that a save landed.",
26
26
  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.",
27
27
  memory_forget: "Delete a memory by id. Use when the user asks to forget something.",
28
- 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 and at task checkpoints, then ask the user which drafts to sync.",
29
- memory_submit: "Move outbox drafts into a git branch for review: creates a branch (default mem/sync-*), copies the drafts into the repo memory dir as proposed, commits locally, and moves the outbox originals out. Prints the push and PR commands — those need the user's explicit approval and are never run automatically.",
28
+ 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 and at task checkpoints, then ask the user which drafts to sync. The user may also trigger this flow by saying 'sync memory' (or '同步记忆').",
29
+ 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.",
30
30
  memory_propose: "Copy personal memories into the project outbox as review drafts. The personal originals stay put.",
31
31
  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.",
32
32
  memory_resolve: "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved.",
@@ -78,14 +78,14 @@ export const memoryForgetArgs = {
78
78
  export const memoryStatusArgs = {};
79
79
  export const memorySubmitArgs = {
80
80
  ids: z.array(z.string().min(1)).min(1).describe("Outbox draft ids to submit."),
81
- onto: z
81
+ branch: z
82
82
  .string()
83
83
  .optional()
84
- .describe("Submit onto this branch instead of creating mem/sync-*. Must be the current branch (for folding memories into a code PR)."),
84
+ .describe("Create this branch and submit onto it. If omitted, submit stays on the current branch — branches are never auto-created. Only pass this when the user explicitly approved the full chain (branch + push + PR)."),
85
85
  base: z
86
86
  .string()
87
87
  .optional()
88
- .describe("PR base branch override. Default: the branch the submit branched from."),
88
+ .describe("PR base branch override. Default: the branch the submit ran on."),
89
89
  };
90
90
  export const memoryProposeArgs = {
91
91
  ids: z.array(z.string().min(1)).min(1).describe("Personal memory ids to copy into the project outbox."),
@@ -260,16 +260,18 @@ export async function statusMemories() {
260
260
  };
261
261
  }
262
262
  export async function submitMemoriesOp(args) {
263
- const r = submitMemories(args.ids, { onto: args.onto, base: args.base });
263
+ const r = submitMemories(args.ids, { branch: args.branch, base: args.base });
264
264
  const lines = [];
265
265
  for (const s of r.submitted)
266
266
  lines.push(`submitted ${s.id} [${s.reviewState}]`);
267
267
  for (const id of r.skippedIdentical)
268
268
  lines.push(`already on branch: ${id} (outbox copy removed)`);
269
- lines.push(r.committed ? `committed on ${r.branch}.` : `nothing new to commit on ${r.branch}.`);
270
- lines.push(`Next (needs the user's explicit approval — never run automatically):`);
269
+ lines.push(r.committed ? `committed on ${r.branch} (you are still on this branch).` : `nothing new to commit on ${r.branch}.`);
270
+ lines.push(`Next — ask the user: "want me to create a branch + push + open the PR, or will you handle it yourself?"`);
271
+ lines.push(`Never create branches, push, or open PRs without their explicit approval. If they handle it themselves, hand them these:`);
271
272
  lines.push(` ${r.pushCommand}`);
272
- lines.push(` ${r.prCommand}`);
273
+ for (const l of r.prCommand.split("\n"))
274
+ lines.push(` ${l}`);
273
275
  return { title: `memory: submitted ${r.submitted.length}`, output: lines.join("\n") };
274
276
  }
275
277
  export async function proposeMemoriesOp(args) {
@@ -99,13 +99,15 @@ personal idea ──propose──▶ outbox draft ──submit──▶ proposed
99
99
  what triggered it — session start, a request, a CLI run, a submit), the
100
100
  outbox (pending sync), the repo review states
101
101
  (`draft / proposed / approved / published / rejected`), and any uncommitted
102
- repo memory files. Your agent calls this at session start and at meaningful
103
- checkpoints, then asks which drafts (if any) you want synced.
102
+ repo memory files. Your agent calls this at session start, at meaningful
103
+ checkpoints, after you commit, and after memory actions — then asks which
104
+ drafts (if any) you want synced. You can also just say "sync memory".
104
105
  - `open-memex submit <id...>`: moves **your named drafts** into
105
- `<repo>/.ai/open-memex/` as `proposed`. It creates `mem/sync-<timestamp>`
106
- (or stays on the current branch with `--onto` for a code+memory PR), copies
107
- the files, flips `review_state`, and makes a **local** git commit —
108
- all-or-nothing, idempotent, crash-safe. It prints the `git push` +
106
+ `<repo>/.ai/open-memex/` as `proposed`. It copies the files, flips
107
+ `review_state`, and makes a **local** git commit **on your current branch** —
108
+ it never creates a branch on its own (branch creation is your call, or your
109
+ agent's with your explicit approval via `--branch <name>`).
110
+ All-or-nothing, idempotent, crash-safe. It prints the `git push` +
109
111
  `gh pr create` commands; if your agent already has your Yes for this sync,
110
112
  it carries through push and PR itself. The PR base defaults to the current
111
113
  branch; `--base` redirects to `main` or your integration branch.
@@ -120,8 +122,8 @@ personal idea ──propose──▶ outbox draft ──submit──▶ proposed
120
122
  (push, PR, merge).
121
123
  - A standalone memory PR contains **only memory files, no code**, reviewed and
122
124
  audited separately from code PRs. Reviewers check "is this true? is it safe
123
- to share? any secrets?" — things a code PR's CI never checks. You can also
124
- ride along in a code PR (`submit --onto <branch>`).
125
+ to share? any secrets?" — things a code PR's CI never checks. You create the
126
+ branch yourself when you want one (or your agent does, with your approval).
125
127
  - "Request changes" needs no command: while the PR is open, the author edits
126
128
  the same file (directly, or by asking their agent in chat), commits, and
127
129
  pushes. The state stays `proposed`; the PR is the review mechanism.
@@ -87,12 +87,13 @@ server 不能主动推送,调不调 `memory_search` 全看 model 的判断。
87
87
  - `open-memex sync-status`:看索引**上次同步的时间和触发方**(会话开始、
88
88
  请求、CLI、submit)、草稿箱(待同步)、repo 里的评审状态
89
89
  (`draft / proposed / approved / published / rejected`),以及 repo 里还没
90
- commit 的记忆文件。你的 agent 会在会话开始和关键节点跑这个,然后问你
91
- 哪些草稿(如果有)要同步。
90
+ commit 的记忆文件。你的 agent 会在会话开始、关键节点、你 commit 之后、
91
+ 记忆动作之后跑这个,然后问你哪些草稿(如果有)要同步。你也可以直接说"同步记忆"。
92
92
  - `open-memex submit <id...>`:把**你点名的草稿**移入 `<repo>/.ai/open-memex/`,
93
- 状态变为 `proposed`。它会建 `mem/sync-<timestamp>` 分支(或用 `--onto`
94
- 留在当前分支,跟代码走同一个 PR),复制文件、改 `review_state`、做一次
95
- **本地** git commit——全有或全无、幂等、crash-safe。它打印 `git push` +
93
+ 状态变为 `proposed`。它复制文件、改 `review_state`、在**当前分支**做一次
94
+ **本地** git commit——永不自动建分支(建分支你说了算,或 agent 拿到你明确
95
+ 批准走全链时用 `--branch <name>`)。
96
+ 全有或全无、幂等、crash-safe。它打印 `git push` +
96
97
  `gh pr create` 命令;如果你的 agent 已经拿到你这次的 Yes,它会自己走完
97
98
  push 和 PR。PR 默认 base 是当前分支;`--base` 可改到 `main` 或集成支。
98
99
  分支上同 id 但内容不同——**直接中止**,等人裁决,绝不覆盖。
@@ -104,7 +105,7 @@ server 不能主动推送,调不调 `memory_search` 全看 model 的判断。
104
105
  **git 负责运输**(push、PR、合并)。
105
106
  - 独立的记忆 PR **只含记忆文件,不含代码**,跟代码 PR 分开评审、分开审计。
106
107
  审的是"这条是真的吗?能给全团队看吗?有没有 secret?"——代码 PR 的 CI 不会查这些。
107
- 也可以搭代码 PR 的车(`submit --onto <branch>`)。
108
+ 想开分支时你自己建(或 agent 拿到批准后建)。
108
109
  - "要求修改"不需要命令:PR 开着的时候,作者直接改同一个文件(自己改,
109
110
  或在 chat 里让 agent 改),commit、push。状态一直是 `proposed`,
110
111
  PR 本身就是评审机制。
package/docs/V2-DESIGN.md CHANGED
@@ -699,6 +699,39 @@ requirement: personal data never touches third-party services). Benchmarks to tr
699
699
  is never overridden by a PR signal. *Rationale: the PR is where the team
700
700
  actually reviews — the mapping closes the loop without inventing new
701
701
  review UI. Approved 2026-09-28.*
702
+ - **D34** — The MCP server sends session-start guidance in the handshake
703
+ `instructions`: call `memory_status` at session start (and at work
704
+ checkpoints); if the outbox has drafts, summarize and ask the user which to
705
+ sync; proactive `memory_add`; `memory_search` before asking about the past;
706
+ personal never leaves the machine. *Rationale: the init-written instruction
707
+ files only exist if the user ran `init --client` — the handshake reaches
708
+ every MCP client at connect time. Still advisory: no MCP consumer offers a
709
+ hard session-start hook, and we do not claim otherwise. Approved 2026-09-28.*
710
+ - **D35** — "sync memory" (or "同步记忆") is a natural-language trigger for the
711
+ sync flow: the agent calls `memory_status`, summarizes the outbox drafts, and
712
+ asks the user which ones to sync — same flow as the session-start proposal,
713
+ but user-initiated. Taught in the MCP handshake instructions, the
714
+ init-written instruction files, and the `memory_status` tool description.
715
+ *Rationale: the user should not have to remember command names to sync;
716
+ saying it in words must work. Approved 2026-09-28.*
717
+ - **D36** — `submit` never creates a branch on its own (revises D26/D27): it
718
+ copies the named drafts into `.ai/open-memex/`, commits locally on the
719
+ CURRENT branch, and prints next-step commands. Branch creation is the human's
720
+ call — or the agent's, only with explicit approval for the full chain, via
721
+ `submit --branch <name>` / `memory_submit(branch=...)`. *Rationale: an
722
+ auto-created branch strands the user on it — they forget to switch back.
723
+ After a submit the agent asks ONE follow-up ("want me to create a branch +
724
+ push + open the PR, or will you handle it yourself?") instead of branching
725
+ silently. Checkpoints that trigger `memory_status`: session start, end of a
726
+ work chunk, after the user commits, and after any memory_* action.
727
+ The "sync memory" trigger ALWAYS goes through the `memory_status` tool —
728
+ never by browsing the appdata directory directly. Approved 2026-09-28.*
729
+ - **D33** — Every CLI command answers `open-memex <command> --help` (and `-h`)
730
+ with its own usage, flags, and examples; checked before config/DB load so
731
+ help works even in a broken environment. Unknown commands with `--help`
732
+ fall back to the global usage. *Rationale: AI assistants discover the CLI
733
+ through --help first — a command that silently swallows --help as a flag
734
+ teaches the agent nothing. Approved 2026-09-28.*
702
735
 
703
736
  ## Open Questions
704
737
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-memex",
3
- "version": "0.4.0-alpha.1",
3
+ "version": "0.4.0-alpha.5",
4
4
  "description": "Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/cli.ts CHANGED
@@ -25,6 +25,189 @@ import fs from "node:fs";
25
25
  import path from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
27
 
28
+ /** Per-command help, printed by `open-memex <command> --help`.
29
+ AI assistants discover the CLI through --help, so every command needs one. */
30
+ const COMMAND_HELP: Record<string, string> = {
31
+ where: `Show which project scope the current directory resolves to, and where its data lives.
32
+
33
+ Usage: open-memex where`,
34
+
35
+ list: `List memories in a scope, newest first.
36
+
37
+ Usage: open-memex list [--scope project|personal] [--type T] [--limit N]
38
+
39
+ Flags:
40
+ --scope project (default) or personal
41
+ --type filter by memory type
42
+ --limit max results
43
+
44
+ Examples:
45
+ open-memex list
46
+ open-memex list --scope personal --limit 20`,
47
+
48
+ search: `Search memories by keyword (BM25 full-text), best matches first.
49
+
50
+ Usage: open-memex search "query" [--scope project|personal|both] [--type T] [--limit N]
51
+
52
+ Flags:
53
+ --scope project (default), personal, or both
54
+ --type filter by memory type
55
+ --limit max results
56
+
57
+ Example:
58
+ open-memex search "deploy checklist" --scope both`,
59
+
60
+ add: `Save a fact, preference, decision, or note to local memory.
61
+
62
+ Usage: open-memex add "content" [--scope project|personal] [--type T] [--tag t1,t2]
63
+
64
+ Flags:
65
+ --scope project (default) or personal (personal never leaves this machine)
66
+ --type memory type (default: fact)
67
+ --tag comma-separated tags
68
+
69
+ Example:
70
+ open-memex add "We deploy on Fridays" --scope project --tag process`,
71
+
72
+ supersede: `Replace a memory with a newer version. The old one is kept as history.
73
+
74
+ Usage: open-memex supersede <id> "new content" [--type T] [--tag t1,t2]`,
75
+
76
+ status: `Change a memory's lifecycle status.
77
+
78
+ Usage: open-memex status <id> active|deprecated|retracted|archived`,
79
+
80
+ forget: `Delete a memory by id.
81
+
82
+ Usage: open-memex forget <id>`,
83
+
84
+ propose: `Copy personal memories into the project outbox as review drafts.
85
+ The personal originals stay put. Nothing enters git at this step.
86
+
87
+ Usage: open-memex propose <id...> --to project [--local-approve]
88
+
89
+ Flags:
90
+ --to project (required)
91
+ --local-approve mark the copies approved right away (solo-dev shortcut)
92
+
93
+ Example:
94
+ open-memex propose 01ABC 01DEF --to project`,
95
+
96
+ promote: `Advance a project memory one step up the review ladder
97
+ (proposed → approved → published), or reject it with a note.
98
+ Every transition is appended to the memory's review_history.
99
+ Rejected memories are never deleted — they can be revised and resubmitted.
100
+
101
+ Usage: open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
102
+
103
+ Flags:
104
+ --reject move back to rejected (requires --note)
105
+ --resubmit move a rejected memory back to proposed
106
+ --note reason for the transition (recorded in review_history)
107
+ --by reviewer name (defaults to the git user)
108
+
109
+ Examples:
110
+ open-memex promote 01ABC --note "verified against the runbook"
111
+ open-memex promote 01ABC --reject --note "outdated after the migration"`,
112
+
113
+ resolve: `List conflicted memories, or 3-way-merge one.
114
+
115
+ Usage: open-memex resolve [id-or-path]
116
+
117
+ With no argument, lists conflicts. With an id or file path, shows the
118
+ 3-way merge (base / outbox / repo) so you can resolve it by hand.
119
+ Conflicts are never auto-resolved.`,
120
+
121
+ "sync-status": `Show the project memory sync pipeline: when the index last synced
122
+ and what triggered it, drafts waiting in the outbox (appdata), memories in the
123
+ repo awaiting review or published, and repo files not yet committed.
124
+
125
+ Usage: open-memex sync-status`,
126
+
127
+ submit: `Move outbox drafts into the repo for review: copies the drafts into
128
+ the repo memory dir as proposed (a local-approved copy keeps its approval),
129
+ commits locally on the CURRENT branch, and moves the outbox originals out.
130
+ Never creates a branch on its own — branch creation is your call.
131
+
132
+ Usage: open-memex submit <id...> [--branch <name>] [--base <branch>]
133
+
134
+ Flags:
135
+ --branch create this branch and submit onto it (only with your explicit
136
+ approval for the full chain); default: stay on current branch
137
+ --base PR base override (default: the branch you're on)
138
+
139
+ Example:
140
+ open-memex submit 01ABC 01DEF`,
141
+
142
+ "pr-status": `Map the current branch's GitHub PR state back onto review_state:
143
+ merged → published, approval → approved (approved_by = the reviewer),
144
+ changes-requested → suggestion only (never auto-rejects).
145
+ Each memory in the PR is mapped independently; a human rejection is never
146
+ overwritten. Report-only by default.
147
+
148
+ Usage: open-memex pr-status [--apply]
149
+
150
+ Flags:
151
+ --apply write the transitions locally (still never pushes)`,
152
+
153
+ reindex: `Rebuild the SQLite index from the markdown files.
154
+
155
+ Usage: open-memex reindex`,
156
+
157
+ scopes: `List the known scopes (personal + project).
158
+
159
+ Usage: open-memex scopes`,
160
+
161
+ migrate: `Move memories between scopes, or convert a legacy my-o-memory data dir.
162
+
163
+ Usage: open-memex migrate [--from <key>] [--to <key>] [--dry-run] [--on-conflict newer|overwrite|skip]
164
+ open-memex migrate --to-v2 [--dry-run]
165
+
166
+ Flags:
167
+ --from / --to scope keys (default: current project → personal)
168
+ --dry-run preview without moving anything
169
+ --on-conflict newer (default), overwrite, or skip
170
+ --to-v2 convert a legacy my-o-memory data dir to the v2 layout
171
+
172
+ Always preview with --dry-run first; nothing moves without confirmation.`,
173
+
174
+ mcp: `Start the stdio MCP server (the same server editors connect to).
175
+
176
+ Usage: open-memex mcp [--print-config vscode|cursor|claude|opencode|visualstudio]
177
+
178
+ Flags:
179
+ --print-config print the MCP client config instead of starting the server`,
180
+
181
+ init: `One-command project setup: writes the MCP config for your editor and the
182
+ agent memory instructions. Existing files are merged, never clobbered.
183
+
184
+ Usage: open-memex init [--client vscode|cursor|opencode|visualstudio]
185
+ [--instructions personal|project] [--force] [--yes]
186
+
187
+ Flags:
188
+ --client editor to configure (default: auto-detect)
189
+ --instructions personal (default, ~/.copilot/copilot-instructions.md) or project
190
+ --force overwrite existing config
191
+ --yes accept all defaults, never prompt`,
192
+
193
+ config: `Show config, or set a key.
194
+
195
+ Usage: open-memex config [set <key> <value>]
196
+
197
+ Example:
198
+ open-memex config set sync.autoPull false`,
199
+
200
+ capture: `Preview what the keyword-capture watcher would extract from text.
201
+
202
+ Usage: open-memex capture --dry-run "text"`,
203
+
204
+ doctor: `Environment health check: Node version, config source, scope resolution,
205
+ storage writability, then boots a real MCP server and runs initialize +
206
+ tools/list against it — all eleven tools must show up.
207
+
208
+ Usage: open-memex doctor`,
209
+ };
210
+
28
211
  function usage(exitCode = 1): never {
29
212
  console.log(`open-memex CLI
30
213
 
@@ -40,7 +223,7 @@ Usage:
40
223
  open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
41
224
  open-memex resolve [id-or-path]
42
225
  open-memex sync-status
43
- open-memex submit <id...> [--onto <branch>] [--base <branch>]
226
+ open-memex submit <id...> [--branch <name>] [--base <branch>]
44
227
  open-memex pr-status [--apply]
45
228
  open-memex reindex
46
229
  open-memex scopes
@@ -79,7 +262,9 @@ the \`--from\` key when migrating.
79
262
  git remote after memories were already stored under the cwd-based key.
80
263
  \`migrate --to-v2\` converts v1 memory files to the v2 format (§19):
81
264
  user→personal scope rename, epoch→RFC 3339 times, priority→importance,
82
- type: instruction→role split. Always preview with --dry-run first.`);
265
+ type: instruction→role split. Always preview with --dry-run first.
266
+
267
+ Run \`open-memex <command> --help\` for details on a single command.`);
83
268
  process.exit(exitCode);
84
269
  }
85
270
 
@@ -204,6 +389,17 @@ async function main() {
204
389
  return;
205
390
  }
206
391
 
392
+ // Per-command help: `open-memex <command> --help`. Checked before loadConfig()
393
+ // so it works even when the environment is broken.
394
+ if (rest.includes("--help") || rest.includes("-h")) {
395
+ const h = COMMAND_HELP[cmd];
396
+ if (h) {
397
+ console.log(`open-memex ${cmd}\n\n${h}`);
398
+ return;
399
+ }
400
+ usage(0);
401
+ }
402
+
207
403
  const cfg = loadConfig();
208
404
  const project = resolveProjectScope(process.cwd());
209
405
 
@@ -687,7 +883,7 @@ function positionalArgs(argv: string[]): string[] {
687
883
  if (ids.length === 0) usage();
688
884
  syncScope(project.key, "submit");
689
885
  try {
690
- const r = submitMemories(ids, { onto: flags.onto, base: flags.base });
886
+ const r = submitMemories(ids, { branch: flags.branch, base: flags.base });
691
887
  for (const s of r.submitted) {
692
888
  console.log(`submitted ${s.id} → ${path.relative(process.cwd(), s.filePath)} [${s.reviewState}]`);
693
889
  }
package/src/init.ts CHANGED
@@ -70,17 +70,22 @@ You have a local memory MCP server (\`open-memex\`) with eleven tools:
70
70
  Project memories you save land in a local outbox first — they are NOT in git yet.
71
71
  Syncing them into the repo for review is an explicit, user-approved step:
72
72
 
73
- - At session start, and when you finish a meaningful chunk of work, call
73
+ - At session start, when you finish a meaningful chunk of work, after the user
74
+ commits (git commit), and after any memory_* action completes, call
74
75
  \`memory_status\`. If the outbox has drafts, summarize them (one line each) and ask
75
76
  the user which ones to sync. Sync NOTHING the user did not name.
76
- - When the user approves, ask ONE follow-up: a separate memory-only branch/PR
77
- (recommended), or fold the memories into the current branch alongside code?
78
- A "yes, you do it" answer covers the whole chain — branch, local commit,
79
- push, PR creation — do NOT re-ask at each step. If the user says they will
80
- do it themselves, hand them the printed push/PR commands and do nothing.
81
- Then call \`memory_submit\` with the approved ids (\`onto\` = current branch name
82
- only for the fold-into-code path). It creates the branch, copies the drafts in
83
- as \`proposed\`, commits locally, and prints the push + PR commands.
77
+ - When the user says "sync memory" (or "同步记忆"), treat it as a request to run
78
+ the sync flow above: call \`memory_status\`, summarize the outbox drafts, and ask
79
+ which ones to sync.
80
+ - When the user approves, call \`memory_submit\` with the approved ids. It copies
81
+ the drafts into the repo as \`proposed\`, commits locally on the CURRENT branch,
82
+ and prints the push + PR commands. It NEVER creates a branch on its own.
83
+ - After the submit, ask ONE follow-up: "want me to create a branch + push +
84
+ open the PR, or will you handle it yourself?" A "yes, you do it" answer covers
85
+ the whole chain — branch creation, push, PR creation — do NOT re-ask at each
86
+ step. If the user says they will do it themselves, hand them the printed
87
+ push/PR commands and do nothing. NEVER create branches, push, or open PRs
88
+ without their explicit approval.
84
89
  - Base branch for the memory PR defaults to the branch you are on; the user may
85
90
  redirect it to the integration branch (main) for branch-independent knowledge.
86
91
  - If anything conflicts (same id with different content, push rejected), STOP and
package/src/mcp.ts CHANGED
@@ -53,6 +53,39 @@ import {
53
53
 
54
54
  const SERVER_VERSION = "0.2.0-alpha";
55
55
 
56
+ /**
57
+ * D26: session-start guidance delivered through the MCP handshake itself.
58
+ * The init-written instruction files only exist if the user ran
59
+ * `open-memex init --client`; the initialize `instructions` reach every MCP
60
+ * client at connect time. Still advisory — no MCP consumer offers a hard
61
+ * session-start hook — but it is the strongest signal available.
62
+ */
63
+ const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server
64
+ (eleven memory_* tools: add, search, list, supersede, forget, status, submit,
65
+ propose, promote, resolve, pr_status).
66
+
67
+ - At the START of this session, when you finish a meaningful chunk of work,
68
+ after the user commits (git commit), and after any memory_* action completes,
69
+ call memory_status. If the project outbox has drafts waiting for review,
70
+ summarize them (one line each) and ask the user which ones to sync into the
71
+ repo. Sync NOTHING the user did not name.
72
+ - When the user says "sync memory" (or "同步记忆"), treat it as a request to run
73
+ the sync flow: call memory_status, summarize the outbox drafts, and ask which
74
+ ones to sync. ALWAYS use the memory_status tool for this — never browse the
75
+ appdata directory directly.
76
+ - After memory_submit, ask ONE follow-up: "want me to create a branch + push +
77
+ open the PR, or will you handle it yourself?" NEVER create branches, push, or
78
+ open PRs without the user's explicit approval. A "yes, you do it" covers the
79
+ whole chain — do NOT re-ask at each step.
80
+ - BE PROACTIVE: when the user shares something worth remembering across sessions
81
+ (a decision, a preference, a project convention, a fix and its cause), call
82
+ memory_add without being asked. Keep each memory to one self-contained statement.
83
+ - Before asking the user about past decisions, conventions, or preferences they
84
+ may have told you before, call memory_search first.
85
+ - Memories default to this project's scope; use the personal scope for facts
86
+ about the user that hold across all projects.
87
+ - personal scope memories NEVER leave this machine.`;
88
+
56
89
  /** Adapt a framework-agnostic op result to an MCP tool response. */
57
90
  function toMcp(p: Promise<ToolResult>) {
58
91
  return p.then(
@@ -91,7 +124,10 @@ export async function runMcpServer() {
91
124
  };
92
125
  };
93
126
 
94
- const server = new McpServer({ name: "open-memex", version: SERVER_VERSION });
127
+ const server = new McpServer(
128
+ { name: "open-memex", version: SERVER_VERSION },
129
+ { instructions: SERVER_INSTRUCTIONS },
130
+ );
95
131
 
96
132
  server.registerTool(
97
133
  "memory_add",
package/src/submit.ts CHANGED
@@ -191,15 +191,19 @@ export function formatSyncStatus(st: SyncStatus): string {
191
191
  // ---------------------------------------------------------------------------
192
192
 
193
193
  export interface SubmitOptions {
194
- /** Submit onto this branch instead of creating mem/sync-*. Must be the current branch. */
195
- onto?: string;
196
- /** PR base override (default: the branch we branched from). */
194
+ /** Create this branch and submit onto it. If omitted, submit stays on the
195
+ current branch — D36: no auto-created branches; branch creation is the
196
+ human's call (or the agent's, only with explicit approval). */
197
+ branch?: string;
198
+ /** PR base override (default: the branch the submit ran on). */
197
199
  base?: string;
198
200
  }
199
201
 
200
202
  export interface SubmitResult {
201
203
  branch: string;
202
204
  base: string;
205
+ /** true when --branch created a new branch for this submit */
206
+ createdBranch: boolean;
203
207
  submitted: Array<{ id: string; filePath: string; reviewState: ReviewState }>;
204
208
  /** already on the branch with identical content — outbox move completed */
205
209
  skippedIdentical: string[];
@@ -219,7 +223,8 @@ interface Validated {
219
223
  export function submitMemories(ids: string[], opts: SubmitOptions = {}): SubmitResult {
220
224
  if (ids.length === 0) fail("submit needs at least one memory id");
221
225
  const root = projectRoot();
222
- // submit needs a git repo — the whole point is branch + commit.
226
+ // submit needs a git repo — the memories land in .ai/open-memex/ and are
227
+ // committed locally. Branch creation is never automatic (D36).
223
228
  git(root, ["rev-parse", "--git-dir"]);
224
229
 
225
230
  const scope = resolveProjectScope(root);
@@ -269,24 +274,25 @@ export function submitMemories(ids: string[], opts: SubmitOptions = {}): SubmitR
269
274
  }
270
275
 
271
276
  // 2. Resolve the target branch.
277
+ // D36: submit never creates a branch on its own — it works on the branch
278
+ // you're already on. Pass --branch <name> (explicitly) only when the user
279
+ // approved the full chain (branch + push + PR).
272
280
  const startBranch = git(root, ["branch", "--show-current"]) || git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
273
- let branch: string;
274
- if (opts.onto) {
275
- if (opts.onto !== startBranch) {
276
- fail(`--onto ${opts.onto} is not the current branch (${startBranch || "(detached)"}). submit only targets the branch you're on.`);
277
- }
278
- branch = opts.onto;
279
- } else {
280
- const stamp = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15);
281
- branch = `mem/sync-${stamp}`;
281
+ let branch: string = startBranch;
282
+ let createdBranch = false;
283
+ if (opts.branch) {
284
+ branch = opts.branch;
282
285
  let n = 0;
286
+ let candidate = branch;
283
287
  while (true) {
284
- const exists = git(root, ["branch", "--list", branch]);
288
+ const exists = git(root, ["branch", "--list", candidate]);
285
289
  if (!exists) break;
286
290
  n++;
287
- branch = `mem/sync-${stamp}-${n}`;
291
+ candidate = `${branch}-${n}`;
288
292
  }
293
+ branch = candidate;
289
294
  git(root, ["checkout", "-b", branch]);
295
+ createdBranch = true;
290
296
  }
291
297
  const base = opts.base ?? startBranch;
292
298
  const author = currentAuthor();
@@ -348,7 +354,7 @@ export function submitMemories(ids: string[], opts: SubmitOptions = {}): SubmitR
348
354
  }
349
355
  // If we created the branch and never committed, remove it too — an
350
356
  // aborted submit leaves no trace.
351
- if (!opts.onto) {
357
+ if (createdBranch) {
352
358
  try {
353
359
  git(root, ["checkout", "--quiet", startBranch]);
354
360
  git(root, ["branch", "--quiet", "-D", branch]);
@@ -399,13 +405,20 @@ export function submitMemories(ids: string[], opts: SubmitOptions = {}): SubmitR
399
405
  removed: 0,
400
406
  scanned: validated.length,
401
407
  });
408
+ // D36: no auto-branch — the commit sits on the branch you were already on.
409
+ // Next steps are printed, not run. For a separate memory PR, create a branch
410
+ // first (the commit comes along), then push + open the PR.
411
+ const branchCmd = `git checkout -b mem/sync-YYYYMMDD`;
402
412
  return {
403
413
  branch,
404
414
  base,
405
415
  submitted,
406
416
  skippedIdentical,
407
417
  committed,
418
+ createdBranch,
408
419
  pushCommand: `git push -u origin ${branch}`,
409
- prCommand: `gh pr create --base ${base} --title "mem: review ${validated.length} ${validated.length === 1 ? "memory" : "memories"}" --body "Submitted from the open-memex outbox: ${ids8}."`,
420
+ prCommand: createdBranch
421
+ ? `gh pr create --base ${base} --title "mem: review ${validated.length} ${validated.length === 1 ? "memory" : "memories"}" --body "Submitted from the open-memex outbox: ${ids8}."`
422
+ : `${branchCmd} # if you want a separate memory PR (else push ${branch} directly)\n git push -u origin <new-branch> && gh pr create --base ${base} --title "mem: review ${validated.length}" --body "Submitted from the open-memex outbox: ${ids8}."`,
410
423
  };
411
424
  }
package/src/tools/ops.ts CHANGED
@@ -51,9 +51,9 @@ export const TOOL_DESCRIPTIONS = {
51
51
  "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.",
52
52
  memory_forget: "Delete a memory by id. Use when the user asks to forget something.",
53
53
  memory_status:
54
- "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 and at task checkpoints, then ask the user which drafts to sync.",
54
+ "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 and at task checkpoints, then ask the user which drafts to sync. The user may also trigger this flow by saying 'sync memory' (or '同步记忆').",
55
55
  memory_submit:
56
- "Move outbox drafts into a git branch for review: creates a branch (default mem/sync-*), copies the drafts into the repo memory dir as proposed, commits locally, and moves the outbox originals out. Prints the push and PR commands — those need the user's explicit approval and are never run automatically.",
56
+ "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.",
57
57
  memory_propose:
58
58
  "Copy personal memories into the project outbox as review drafts. The personal originals stay put.",
59
59
  memory_promote:
@@ -125,14 +125,16 @@ export type MemoryStatusArgs = z.infer<z.ZodObject<typeof memoryStatusArgs>>;
125
125
 
126
126
  export const memorySubmitArgs = {
127
127
  ids: z.array(z.string().min(1)).min(1).describe("Outbox draft ids to submit."),
128
- onto: z
128
+ branch: z
129
129
  .string()
130
130
  .optional()
131
- .describe("Submit onto this branch instead of creating mem/sync-*. Must be the current branch (for folding memories into a code PR)."),
131
+ .describe(
132
+ "Create this branch and submit onto it. If omitted, submit stays on the current branch — branches are never auto-created. Only pass this when the user explicitly approved the full chain (branch + push + PR).",
133
+ ),
132
134
  base: z
133
135
  .string()
134
136
  .optional()
135
- .describe("PR base branch override. Default: the branch the submit branched from."),
137
+ .describe("PR base branch override. Default: the branch the submit ran on."),
136
138
  };
137
139
  export type MemorySubmitArgs = z.infer<z.ZodObject<typeof memorySubmitArgs>>;
138
140
 
@@ -359,14 +361,15 @@ export async function statusMemories(): Promise<ToolResult> {
359
361
  }
360
362
 
361
363
  export async function submitMemoriesOp(args: MemorySubmitArgs): Promise<ToolResult> {
362
- const r = submitMemories(args.ids, { onto: args.onto, base: args.base });
364
+ const r = submitMemories(args.ids, { branch: args.branch, base: args.base });
363
365
  const lines: string[] = [];
364
366
  for (const s of r.submitted) lines.push(`submitted ${s.id} [${s.reviewState}]`);
365
367
  for (const id of r.skippedIdentical) lines.push(`already on branch: ${id} (outbox copy removed)`);
366
- lines.push(r.committed ? `committed on ${r.branch}.` : `nothing new to commit on ${r.branch}.`);
367
- lines.push(`Next (needs the user's explicit approval — never run automatically):`);
368
+ lines.push(r.committed ? `committed on ${r.branch} (you are still on this branch).` : `nothing new to commit on ${r.branch}.`);
369
+ lines.push(`Next — ask the user: "want me to create a branch + push + open the PR, or will you handle it yourself?"`);
370
+ lines.push(`Never create branches, push, or open PRs without their explicit approval. If they handle it themselves, hand them these:`);
368
371
  lines.push(` ${r.pushCommand}`);
369
- lines.push(` ${r.prCommand}`);
372
+ for (const l of r.prCommand.split("\n")) lines.push(` ${l}`);
370
373
  return { title: `memory: submitted ${r.submitted.length}`, output: lines.join("\n") };
371
374
  }
372
375