memory-bank-skill 7.3.1 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,7 +32,7 @@ bunx memory-bank-skill install
32
32
 
33
33
  ### 自定义模型
34
34
 
35
- 默认使用 `cliproxy/claude-opus-4-5-20251101`,可通过 `--model` 指定:
35
+ 默认使用 `cliproxy/claude-opus-4-6`,可通过 `--model` 指定:
36
36
 
37
37
  ```bash
38
38
  bunx memory-bank-skill install --model anthropic/claude-sonnet-4-5
@@ -50,7 +50,7 @@ bunx memory-bank-skill doctor
50
50
  |------|----------|
51
51
  | 复制 Skill 文件 | `~/.config/opencode/skills/memory-bank/`(含 `references/writer.md`) |
52
52
  | 配置 opencode.json | 添加 `permission.skill=allow`,注册插件和 agent |
53
- | 注册 Agent | 添加 `memory-bank-writer` agent(用于写入守卫) |
53
+ | 注册 Agent | 添加 `memory-reader` agent(用于上下文读取) |
54
54
  | 写入 manifest | `~/.config/opencode/skills/memory-bank/.manifest.json` |
55
55
 
56
56
  ---
@@ -180,5 +180,5 @@ service=memory-bank Plugin initialized (unified) {"projectRoot":"..."}
180
180
 
181
181
  ## 版本
182
182
 
183
- - **版本**: 7.3.0
184
- - **主要更新**: Doc-First Gate 默认启用(warn),无 Memory Bank 项目自动提醒初始化
183
+ - **版本**: 7.4.0
184
+ - **主要更新**: Writer 轻量化 去掉 writer subagent,主 agent 直接写入 + Plugin 注入 writing guideline
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import { fileURLToPath } from "url";
27
27
  // package.json
28
28
  var package_default = {
29
29
  name: "memory-bank-skill",
30
- version: "7.3.1",
30
+ version: "7.4.0",
31
31
  description: "Memory Bank - \u9879\u76EE\u8BB0\u5FC6\u7CFB\u7EDF\uFF0C\u8BA9 AI \u52A9\u624B\u5728\u6BCF\u6B21\u5BF9\u8BDD\u4E2D\u90FD\u80FD\u5FEB\u901F\u7406\u89E3\u9879\u76EE\u4E0A\u4E0B\u6587",
32
32
  type: "module",
33
33
  main: "dist/plugin.js",
@@ -291,14 +291,8 @@ async function installPluginToConfig(undoStack, customModel) {
291
291
  if (!config.agent) {
292
292
  config.agent = {};
293
293
  }
294
- const defaultModel = "cliproxy/claude-opus-4-5-20251101";
294
+ const defaultModel = "cliproxy/claude-opus-4-6";
295
295
  const installedBy = `memory-bank-skill@${VERSION}`;
296
- const writerDefaults = {
297
- description: "Memory Bank \u4E13\u7528\u5199\u5165\u4EE3\u7406",
298
- model: customModel || defaultModel,
299
- tools: { write: true, edit: true, bash: true },
300
- "x-installed-by": installedBy
301
- };
302
296
  const readerDefaults = {
303
297
  description: "Memory Bank \u5E76\u884C\u8BFB\u53D6\u4EE3\u7406\uFF0C\u8FD4\u56DE\u7ED3\u6784\u5316\u4E0A\u4E0B\u6587\u5305",
304
298
  model: customModel || defaultModel,
@@ -337,7 +331,9 @@ async function installPluginToConfig(undoStack, customModel) {
337
331
  }
338
332
  return agentModified;
339
333
  };
340
- if (mergeAgent(config.agent["memory-bank-writer"], writerDefaults, "memory-bank-writer")) {
334
+ if (config.agent["memory-bank-writer"]) {
335
+ delete config.agent["memory-bank-writer"];
336
+ changes.push("Removed agent: memory-bank-writer (no longer needed)");
341
337
  modified = true;
342
338
  }
343
339
  if (mergeAgent(config.agent["memory-reader"], readerDefaults, "memory-reader")) {
@@ -359,7 +355,6 @@ async function installCommands(undoStack, manifestFiles) {
359
355
  const commandPath = join(commandsDir, "memory-bank-refresh.md");
360
356
  const commandContent = `---
361
357
  description: \u521D\u59CB\u5316\u3001\u5347\u7EA7\u3001\u8FC1\u79FB\u6216\u5237\u65B0 Memory Bank
362
- agent: memory-bank-writer
363
358
  ---
364
359
 
365
360
  \u6267\u884C Memory Bank \u7684 refresh \u6D41\u7A0B\uFF1A
@@ -441,7 +436,7 @@ Replace <<<USER_REQUEST>>> with the user's original message verbatim.
441
436
 
442
437
  ---
443
438
 
444
- AMENDMENT B \u2014 Final Step: Memory Bank Writer (Propose -> Confirm -> Execute)
439
+ AMENDMENT B \u2014 Final Step: Memory Bank Write (Propose -> Confirm -> Execute)
445
440
 
446
441
  Step W0 Timing:
447
442
  - AFTER you finish main task output, RIGHT BEFORE final answer for this turn.
@@ -481,14 +476,7 @@ Ignore: User continues to next topic without addressing the prompt (treat as ski
481
476
 
482
477
  **Mixed intent**: If user confirms AND asks another question (e.g., "\u5199\u5427\uFF0C\u987A\u4FBF\u95EE\u4E00\u4E0B..."), execute the write first, then answer their question in the same response.
483
478
 
484
- On confirmation, execute writer synchronously:
485
- \`\`\`
486
- proxy_task({
487
- subagent_type: "memory-bank-writer",
488
- description: "Memory Bank write (confirmed)",
489
- prompt: "You are updating Memory Bank.\\nConstraints:\\n- Edit ONLY the target file.\\n- Keep changes minimal and consistent with existing format.\\n- Do NOT invent facts.\\nInput:\\nTarget: <PASTE TARGET>\\nDraft:\\n1) <PASTE>\\n2) <PASTE>\\nOutput: Show what file changed + brief preview of changes."
490
- })
491
- \`\`\`
479
+ On confirmation, directly use write/edit tools to update the target file(s). Plugin will auto-inject writing guidelines.
492
480
 
493
481
  Step W5 After execution:
494
482
  - Show which file(s) updated and brief preview.
@@ -808,8 +796,8 @@ Commands:
808
796
  doctor Check installation status
809
797
 
810
798
  Options:
811
- --model <model> Specify model for memory-bank-writer agent
812
- Default: cliproxy/claude-opus-4-5-20251101
799
+ --model <model> Specify model for memory-reader agent
800
+ Default: cliproxy/claude-opus-4-6
813
801
 
814
802
  Examples:
815
803
  bunx memory-bank-skill install
@@ -17,15 +17,10 @@ var MEMORY_BANK_FILES = [
17
17
  var SENTINEL_OPEN = "<memory-bank-bootstrap>";
18
18
  var SENTINEL_CLOSE = "</memory-bank-bootstrap>";
19
19
  var SERVICE_NAME = "memory-bank";
20
- var PLUGIN_PROMPT_VARIANT = "memory-bank-plugin";
21
20
  var rootStates = new Map;
22
21
  var sessionMetas = new Map;
23
22
  var memoryBankExistsCache = new Map;
24
23
  var fileCache = new Map;
25
- var WRITER_AGENT_NAME = "memory-bank-writer";
26
- var sessionsById = new Map;
27
- var writerSessionIDs = new Set;
28
- var agentBySessionID = new Map;
29
24
  function makeStateKey(sessionId, root) {
30
25
  return `${sessionId}::${root}`;
31
26
  }
@@ -34,44 +29,6 @@ function maxChars() {
34
29
  const n = raw ? Number(raw) : NaN;
35
30
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_MAX_CHARS;
36
31
  }
37
- function isPluginGeneratedPrompt(message, content) {
38
- if (message?.variant === PLUGIN_PROMPT_VARIANT)
39
- return true;
40
- return content.includes("## [Memory Bank]") || content.includes("## [SYSTEM REMINDER - Memory Bank");
41
- }
42
- function getMessageKey(message, rawContent) {
43
- const id = message?.id || message?.messageID;
44
- if (id)
45
- return String(id);
46
- const created = message?.time?.created;
47
- if (typeof created === "number")
48
- return `ts:${created}`;
49
- const trimmed = rawContent.trim();
50
- if (trimmed)
51
- return `content:${trimmed.slice(0, 200)}`;
52
- return null;
53
- }
54
- function getOrCreateMessageKey(meta, message, rawContent) {
55
- const directKey = getMessageKey(message, rawContent);
56
- if (directKey && !directKey.startsWith("content:"))
57
- return directKey;
58
- const trimmed = rawContent.trim();
59
- if (!trimmed)
60
- return directKey ?? null;
61
- const now = Date.now();
62
- const digest = trimmed.slice(0, 200);
63
- const sameAsLast = meta.lastUserMessageDigest === digest;
64
- const withinWindow = typeof meta.lastUserMessageAt === "number" && now - meta.lastUserMessageAt < 2000;
65
- if (sameAsLast && withinWindow && meta.lastUserMessageKey) {
66
- return meta.lastUserMessageKey;
67
- }
68
- meta.userMessageSeq += 1;
69
- const key = `seq:${meta.userMessageSeq}`;
70
- meta.lastUserMessageDigest = digest;
71
- meta.lastUserMessageAt = now;
72
- meta.lastUserMessageKey = key;
73
- return key;
74
- }
75
32
  function createLogger(client) {
76
33
  let pending = Promise.resolve();
77
34
  const formatArgs = (args) => {
@@ -123,20 +80,11 @@ function truncateToBudget(text, budget) {
123
80
  return TRUNCATION_NOTICE.slice(0, budget);
124
81
  return text.slice(0, budget - reserve) + TRUNCATION_NOTICE;
125
82
  }
126
- async function buildMemoryBankContextWithMeta(projectRoot) {
127
- const fnStart = Date.now();
128
- if (DEBUG)
129
- console.error(`[MB-DEBUG] buildMemoryBankContextWithMeta START projectRoot=${projectRoot}`);
83
+ async function buildMemoryBankContext(projectRoot) {
130
84
  const parts = [];
131
- const files = [];
132
85
  for (const rel of MEMORY_BANK_FILES) {
133
- const fileStart = Date.now();
134
86
  const abs = path.join(projectRoot, rel);
135
- if (DEBUG)
136
- console.error(`[MB-DEBUG] reading ${rel}...`);
137
87
  const content = await readTextCached(abs);
138
- if (DEBUG)
139
- console.error(`[MB-DEBUG] read ${rel} done, hasContent=${!!content}, elapsed=${Date.now() - fileStart}ms`);
140
88
  if (!content)
141
89
  continue;
142
90
  const trimmed = content.trim();
@@ -145,41 +93,14 @@ async function buildMemoryBankContextWithMeta(projectRoot) {
145
93
  parts.push(`## ${rel}
146
94
 
147
95
  ${trimmed}`);
148
- files.push({ relPath: rel, chars: trimmed.length });
149
96
  }
150
- if (DEBUG)
151
- console.error(`[MB-DEBUG] all files read, parts=${parts.length}, totalElapsed=${Date.now() - fnStart}ms`);
152
97
  if (parts.length === 0)
153
98
  return null;
154
- const fileList = files.map((f) => f.relPath.replace("memory-bank/", "")).join(", ");
155
- const totalChars = files.reduce((sum, f) => sum + f.chars, 0);
156
99
  const header = `# Memory Bank Bootstrap (Auto-injected by OpenCode plugin)
157
100
 
158
101
  ` + `Use \`memory-bank/_index.md\` to locate additional context files.
159
102
  ` + `Read more files from \`memory-bank/\` as needed based on the task.
160
103
 
161
- ` + `**AI 行为指令**:
162
- ` + `- 每次回复末尾加一行确认:\`| \uD83D\uDCDA Memory Bank | ${fileList} (${totalChars.toLocaleString()} chars) |\`
163
- ` + `- **Memory-first(核心原则)**:
164
- ` + ` - 任何问题先查 _index.md 并打开相关文档(active.md/brief.md/tech.md/docs/requirements/learnings/patterns.md)
165
- ` + ` - 找到答案 → 引用文件路径直接回答;_index.md 与对应目录检索无果或确认已过时 → 才读代码
166
- ` + ` - 文档与代码不一致时以代码为准,但必须提议更新文档
167
- ` + ` - 若因文档缺失而读了代码 → 这本身就是写入触发点,需点名要补的文件路径
168
- ` + `- **文档驱动开发**:
169
- ` + ` - 方案讨论完成后,**先写设计文档到 memory-bank/docs/,再写代码**
170
- ` + ` - 设计文档是契约,代码实现要符合文档
171
- ` + ` - 实现完成后回顾:如有偏差,决定是改文档还是改实现
172
- ` + `- **写入触发场景**(语义判断,非关键词匹配):
173
- ` + ` - 方案讨论确定 / 设计变更("重新设计"、"改一下设计"、"那就这样吧")→ 检查 docs/design-*.md 是否已存在,存在则更新,否则创建
174
- ` + ` - 用户描述新功能/需求("我需要..."、"能不能加..."、"帮我做..."、"要实现...")→ requirements/
175
- ` + ` - 用户做出技术选型("我们用 X 吧"、"决定采用..."、"选择...")→ patterns.md
176
- ` + ` - 修复了 bug 或踩坑经验("原来问题是..."、"这个坑是..."、"发现...")→ learnings/
177
- ` + ` - AI 修改了代码/配置文件 → active.md(如涉及 bug 修复则同时 learnings/)
178
- ` + ` - 当前任务完成,焦点切换 → active.md
179
- ` + `- **Todo 创建规则(必须)**:
180
- ` + ` - 方案讨论完成后开始落地:第一项必须是"写入设计文档到 memory-bank/docs/"
181
- ` + ` - 最后一项必须是"更新 Memory Bank"(检查触发场景并更新相应文件)
182
-
183
104
  ` + `---
184
105
 
185
106
  `;
@@ -190,14 +111,7 @@ ${trimmed}`);
190
111
 
191
112
  `) + `
192
113
  ${SENTINEL_CLOSE}`;
193
- const budget = maxChars();
194
- const truncated = wrapped.length > budget;
195
- const text = truncateToBudget(wrapped, budget);
196
- return { text, files, totalChars, truncated };
197
- }
198
- async function buildMemoryBankContext(projectRoot) {
199
- const result = await buildMemoryBankContextWithMeta(projectRoot);
200
- return result?.text ?? null;
114
+ return truncateToBudget(wrapped, maxChars());
201
115
  }
202
116
  async function checkMemoryBankExists(root, log) {
203
117
  if (memoryBankExistsCache.has(root)) {
@@ -222,7 +136,7 @@ async function checkMemoryBankExists(root, log) {
222
136
  function getSessionMeta(sessionId, fallbackRoot) {
223
137
  let meta = sessionMetas.get(sessionId);
224
138
  if (!meta) {
225
- meta = { rootsTouched: new Set, lastActiveRoot: fallbackRoot, notifiedMessageIds: new Set, planOutputted: false, promptInProgress: false, userMessageReceived: false, sessionNotified: false, userMessageSeq: 0 };
139
+ meta = { rootsTouched: new Set, lastActiveRoot: fallbackRoot };
226
140
  sessionMetas.set(sessionId, meta);
227
141
  }
228
142
  return meta;
@@ -351,70 +265,11 @@ var plugin = async ({ client, directory, worktree }) => {
351
265
  const projectRoot = worktree || directory;
352
266
  const log = createLogger(client);
353
267
  log.info("Plugin initialized (unified)", { projectRoot });
354
- async function sendContextNotification(sessionId, messageKey, messageId) {
355
- if (isDisabled())
356
- return;
357
- const meta = getSessionMeta(sessionId, projectRoot);
358
- if (meta.promptInProgress) {
359
- log.debug("Context notification skipped (prompt in progress)", { sessionId, messageId });
360
- return;
361
- }
362
- if (meta.notifiedMessageIds.has(messageKey)) {
363
- log.debug("Context notification skipped (already notified for this message)", { sessionId, messageKey, messageId });
364
- return;
365
- }
366
- const result = await buildMemoryBankContextWithMeta(projectRoot);
367
- if (!result) {
368
- log.debug("Context notification skipped (no memory-bank)", { sessionId });
369
- return;
370
- }
371
- const fileList = result.files.map((f) => f.relPath.replace("memory-bank/", "")).join(", ");
372
- const truncatedNote = result.truncated ? " (truncated)" : "";
373
- const text = `## [Memory Bank]
374
-
375
- **已读取 Memory Bank 文件**: ${fileList} (${result.totalChars.toLocaleString()} chars)${truncatedNote}
376
-
377
- **写入提醒**:如果本轮涉及以下事件,工作完成后输出更新计划:
378
- - 新需求 → requirements/
379
- - 技术决策 → patterns.md
380
- - Bug修复/踩坑 → learnings/
381
- - 焦点变更 → active.md
382
-
383
- 操作:请加载 memory-bank skill,按规范输出更新计划或更新内容(无需 slash command)。`;
384
- try {
385
- meta.promptInProgress = true;
386
- await client.session.prompt({
387
- path: { id: sessionId },
388
- body: {
389
- noReply: false,
390
- variant: PLUGIN_PROMPT_VARIANT,
391
- parts: [{ type: "text", text }]
392
- }
393
- });
394
- meta.notifiedMessageIds.add(messageKey);
395
- meta.sessionNotified = true;
396
- if (meta.notifiedMessageIds.size > 100) {
397
- const first = meta.notifiedMessageIds.values().next().value;
398
- if (first)
399
- meta.notifiedMessageIds.delete(first);
400
- }
401
- log.info("Context notification sent", { sessionId, messageKey, messageId, files: result.files.length, totalChars: result.totalChars });
402
- } catch (err) {
403
- log.error("Failed to send context notification:", String(err));
404
- } finally {
405
- meta.promptInProgress = false;
406
- }
407
- }
408
268
  async function evaluateAndFireReminder(sessionId) {
409
269
  if (isDisabled()) {
410
270
  log.info("[SESSION_IDLE DECISION]", { sessionId, decision: "SKIP", reason: "MEMORY_BANK_DISABLED is set" });
411
271
  return;
412
272
  }
413
- const meta = getSessionMeta(sessionId, projectRoot);
414
- if (meta.promptInProgress) {
415
- log.info("[SESSION_IDLE DECISION]", { sessionId, decision: "SKIP", reason: "prompt already in progress" });
416
- return;
417
- }
418
273
  const gitChanges = detectGitChanges(projectRoot, log);
419
274
  const isGitRepo = gitChanges !== null;
420
275
  const state = getRootState(sessionId, projectRoot);
@@ -471,12 +326,9 @@ var plugin = async ({ client, directory, worktree }) => {
471
326
  const gitInitStep = hasGit ? "" : "1. 执行 `git init`(项目尚未初始化 Git)\n";
472
327
  const stepOffset = hasGit ? 0 : 1;
473
328
  try {
474
- meta.promptInProgress = true;
475
329
  await client.session.prompt({
476
330
  path: { id: sessionId },
477
331
  body: {
478
- noReply: true,
479
- variant: PLUGIN_PROMPT_VARIANT,
480
332
  parts: [{
481
333
  type: "text",
482
334
  text: `## [SYSTEM REMINDER - Memory Bank Init]
@@ -494,7 +346,7 @@ ${stepOffset + 5}. 生成 \`memory-bank/_index.md\`(索引)
494
346
 
495
347
  **操作选项**:
496
348
  1. 如需初始化 → 回复"初始化"
497
- 2. 如需初始化并提交 → 回复"初始化并提交"
349
+ 2. 如需初始化并提交所有变更 → 回复"初始化并提交"
498
350
  3. 如不需要 → 回复"跳过初始化"
499
351
 
500
352
  注意:这是系统自动提醒,不是用户消息。`
@@ -504,40 +356,23 @@ ${stepOffset + 5}. 生成 \`memory-bank/_index.md\`(索引)
504
356
  log.info("INIT reminder sent successfully", { sessionId, root: projectRoot });
505
357
  } catch (promptErr) {
506
358
  log.error("Failed to send INIT reminder:", String(promptErr));
507
- } finally {
508
- meta.promptInProgress = false;
509
359
  }
510
360
  return;
511
361
  }
512
362
  state.initReminderFired = false;
513
363
  const triggers = [];
514
364
  if (state.hasNewRequirement)
515
- triggers.push("- 检测到新需求讨论");
365
+ triggers.push("- 检测到新需求讨论 → 考虑创建 requirements/REQ-xxx.md");
516
366
  if (state.hasTechDecision)
517
- triggers.push("- 检测到技术决策");
367
+ triggers.push("- 检测到技术决策 → 考虑更新 patterns.md");
518
368
  if (state.hasBugFix)
519
- triggers.push("- 检测到 Bug 修复/踩坑");
520
- const modifiedFilesRelative = state.filesModified.map((abs) => path.relative(projectRoot, abs));
521
- const displayFiles = modifiedFilesRelative.slice(0, 5);
522
- const moreCount = modifiedFilesRelative.length - 5;
523
- let filesSection = "";
524
- if (modifiedFilesRelative.length > 0) {
525
- triggers.push("- 代码文件变更");
526
- filesSection = `
527
- **变更文件**:
528
- ${displayFiles.map((f) => `- ${f}`).join(`
529
- `)}${moreCount > 0 ? `
530
- (+${moreCount} more)` : ""}
531
- `;
532
- }
369
+ triggers.push("- 检测到 Bug 修复/踩坑 → 考虑记录到 learnings/");
370
+ if (state.filesModified.length >= 1)
371
+ triggers.push(`- 本轮修改了 ${state.filesModified.length} 个文件 → 考虑更新 active.md`);
533
372
  if (triggers.length === 0) {
534
373
  log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "NO_TRIGGER", reason: "has memory-bank but no triggers" });
535
374
  return;
536
375
  }
537
- if (meta.planOutputted) {
538
- log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "SKIP", reason: "AI already outputted update plan" });
539
- return;
540
- }
541
376
  if (triggerSignature === state.lastSyncedTriggerSignature) {
542
377
  log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "SKIP", reason: "already synced (signature matches lastSyncedTriggerSignature)" });
543
378
  return;
@@ -549,95 +384,50 @@ ${displayFiles.map((f) => `- ${f}`).join(`
549
384
  state.lastUpdateReminderSignature = triggerSignature;
550
385
  log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "FIRE_UPDATE", reason: `${triggers.length} triggers detected`, triggers });
551
386
  try {
552
- meta.promptInProgress = true;
553
387
  await client.session.prompt({
554
388
  path: { id: sessionId },
555
389
  body: {
556
- noReply: true,
557
- variant: PLUGIN_PROMPT_VARIANT,
558
390
  parts: [{
559
391
  type: "text",
560
392
  text: `## [SYSTEM REMINDER - Memory Bank Update]
561
393
 
562
- 本轮检测到以下变更:${filesSection}
563
- **触发事件**:
394
+ 项目 \`${path.basename(projectRoot)}\` 本轮检测到以下事件,请确认是否需要更新 Memory Bank:
395
+
396
+ **项目路径**:\`${projectRoot}\`
397
+
564
398
  ${triggers.join(`
565
399
  `)}
566
400
 
567
401
  **操作选项**:
568
402
  1. 如需更新 → 回复"更新",输出更新计划
569
- 2. 如需更新并提交 → 回复"更新并提交"
570
- 3. 如不需要 → 回复"跳过"`
403
+ 2. 如需更新并提交所有变更 → 回复"更新并提交"
404
+ 3. 如不需要 → 回复"跳过"
405
+
406
+ 注意:这是系统自动提醒,不是用户消息。`
571
407
  }]
572
408
  }
573
409
  });
574
410
  log.info("UPDATE reminder sent successfully", { sessionId, root: projectRoot });
575
411
  } catch (promptErr) {
576
412
  log.error("Failed to send UPDATE reminder:", String(promptErr));
577
- } finally {
578
- meta.promptInProgress = false;
579
413
  }
580
414
  }
581
415
  return {
582
416
  "experimental.chat.system.transform": async (_input, output) => {
583
- const hookStart = Date.now();
584
- log.info("[HOOK] system.transform START");
585
- try {
586
- if (output.system.some((s) => s.includes(SENTINEL_OPEN))) {
587
- log.info("[HOOK] system.transform SKIP (sentinel exists)", { elapsed: Date.now() - hookStart });
588
- return;
589
- }
590
- log.info("[HOOK] system.transform building context...");
591
- const ctx = await buildMemoryBankContext(projectRoot);
592
- log.info("[HOOK] system.transform context built", { hasCtx: !!ctx, elapsed: Date.now() - hookStart });
593
- if (ctx) {
594
- output.system.push(ctx);
595
- log.info("[HOOK] system.transform DONE (ctx pushed)", { elapsed: Date.now() - hookStart });
596
- return;
597
- }
598
- const initInstruction = `${SENTINEL_OPEN}
599
- ` + `# Memory Bank 未初始化
600
-
601
- ` + `项目 \`${path.basename(projectRoot)}\` 尚未初始化 Memory Bank。
602
-
603
- ` + `**AI 行为指令**:
604
- ` + `- **Todo 创建规则(必须)**:创建 todo 时,第一项必须是"初始化 Memory Bank"(扫描项目结构,创建 brief.md + tech.md + _index.md),最后一项必须是"更新 Memory Bank"(更新 active.md)
605
- ` + `${SENTINEL_CLOSE}`;
606
- output.system.push(initInstruction);
607
- log.info("[HOOK] system.transform DONE (init pushed)", { elapsed: Date.now() - hookStart });
608
- } catch (err) {
609
- log.error("[HOOK] system.transform ERROR", String(err), { elapsed: Date.now() - hookStart });
610
- }
417
+ if (output.system.some((s) => s.includes(SENTINEL_OPEN)))
418
+ return;
419
+ const ctx = await buildMemoryBankContext(projectRoot);
420
+ if (!ctx)
421
+ return;
422
+ output.system.push(ctx);
611
423
  },
612
424
  "experimental.session.compacting": async (_input, output) => {
613
- const hookStart = Date.now();
614
- log.info("[HOOK] session.compacting START");
615
- try {
616
- if (output.context.some((s) => s.includes(SENTINEL_OPEN))) {
617
- log.info("[HOOK] session.compacting SKIP (sentinel exists)", { elapsed: Date.now() - hookStart });
618
- return;
619
- }
620
- log.info("[HOOK] session.compacting building context...");
621
- const ctx = await buildMemoryBankContext(projectRoot);
622
- log.info("[HOOK] session.compacting context built", { hasCtx: !!ctx, elapsed: Date.now() - hookStart });
623
- if (ctx) {
624
- output.context.push(ctx);
625
- log.info("[HOOK] session.compacting DONE (ctx pushed)", { elapsed: Date.now() - hookStart });
626
- return;
627
- }
628
- const initInstruction = `${SENTINEL_OPEN}
629
- ` + `# Memory Bank 未初始化
630
-
631
- ` + `项目 \`${path.basename(projectRoot)}\` 尚未初始化 Memory Bank。
632
-
633
- ` + `**AI 行为指令**:
634
- ` + `- **Todo 创建规则(必须)**:创建 todo 时,第一项必须是"初始化 Memory Bank"(扫描项目结构,创建 brief.md + tech.md + _index.md),最后一项必须是"更新 Memory Bank"(更新 active.md)
635
- ` + `${SENTINEL_CLOSE}`;
636
- output.context.push(initInstruction);
637
- log.info("[HOOK] session.compacting DONE (init pushed)", { elapsed: Date.now() - hookStart });
638
- } catch (err) {
639
- log.error("[HOOK] session.compacting ERROR", String(err), { elapsed: Date.now() - hookStart });
640
- }
425
+ if (output.context.some((s) => s.includes(SENTINEL_OPEN)))
426
+ return;
427
+ const ctx = await buildMemoryBankContext(projectRoot);
428
+ if (!ctx)
429
+ return;
430
+ output.context.push(ctx);
641
431
  },
642
432
  event: async ({ event }) => {
643
433
  try {
@@ -656,10 +446,8 @@ ${triggers.join(`
656
446
  return;
657
447
  }
658
448
  if (event.type === "session.created") {
659
- sessionMetas.set(sessionId, { rootsTouched: new Set, lastActiveRoot: projectRoot, notifiedMessageIds: new Set, planOutputted: false, promptInProgress: false, userMessageReceived: false, sessionNotified: false, userMessageSeq: 0 });
660
- const parentID = info?.parentID;
661
- sessionsById.set(sessionId, { parentID });
662
- log.info("Session created", { sessionId, parentID });
449
+ sessionMetas.set(sessionId, { rootsTouched: new Set, lastActiveRoot: projectRoot });
450
+ log.info("Session created", { sessionId });
663
451
  }
664
452
  if (event.type === "session.deleted") {
665
453
  const meta = sessionMetas.get(sessionId);
@@ -669,34 +457,13 @@ ${triggers.join(`
669
457
  }
670
458
  }
671
459
  sessionMetas.delete(sessionId);
672
- sessionsById.delete(sessionId);
673
- writerSessionIDs.delete(sessionId);
674
- agentBySessionID.delete(sessionId);
675
460
  log.info("Session deleted", { sessionId });
676
461
  }
677
462
  if (event.type === "message.updated") {
678
463
  const message = info;
679
- const meta = getSessionMeta(sessionId, projectRoot);
680
- const rawContent = JSON.stringify(message?.content || "");
681
- if (DEBUG) {
682
- log.debug("message.updated received", {
683
- sessionId,
684
- role: message?.role,
685
- agent: message?.agent,
686
- variant: message?.variant,
687
- messageId: message?.id || message?.messageID
688
- });
689
- }
690
- if (isPluginGeneratedPrompt(message, rawContent)) {
691
- log.debug("message.updated skipped (plugin prompt)", { sessionId });
692
- return;
693
- }
694
464
  if (message?.role === "user") {
695
- if (meta.promptInProgress) {
696
- log.debug("message.updated skipped (prompt in progress)", { sessionId });
697
- return;
698
- }
699
- const content = rawContent.toLowerCase();
465
+ const content = JSON.stringify(message.content || "").toLowerCase();
466
+ const meta = getSessionMeta(sessionId, projectRoot);
700
467
  const targetRoot = meta.lastActiveRoot || projectRoot;
701
468
  const state = getRootState(sessionId, targetRoot);
702
469
  if (/新需求|new req|feature request|需要实现|要做一个/.test(content)) {
@@ -718,124 +485,22 @@ ${triggers.join(`
718
485
  state.memoryBankReviewed = true;
719
486
  log.info("Escape valve triggered: memoryBankReviewed", { sessionId, root: targetRoot });
720
487
  }
721
- const messageId = message.id || message.messageID;
722
- const messageKey = getOrCreateMessageKey(meta, message, rawContent);
723
- if (!messageKey) {
724
- log.debug("Context notification skipped (no message key)", { sessionId, messageId });
725
- return;
726
- }
727
- log.debug("Context notification disabled (using system prompt instruction instead)", { sessionId, messageKey, messageId });
728
- meta.userMessageReceived = true;
729
- meta.planOutputted = false;
730
488
  }
731
- if (message?.role === "assistant") {
732
- const content = JSON.stringify(message.content || "");
733
- const meta2 = getSessionMeta(sessionId, projectRoot);
734
- if (/Memory Bank 更新计划|\[Memory Bank 更新计划\]/.test(content)) {
735
- meta2.planOutputted = true;
736
- log.info("Plan outputted detected", { sessionId });
737
- }
738
- const agentName = message?.agent;
739
- if (agentName) {
740
- agentBySessionID.set(sessionId, agentName);
741
- const sessionInfo = sessionsById.get(sessionId);
742
- if (agentName === WRITER_AGENT_NAME && sessionInfo?.parentID) {
743
- writerSessionIDs.add(sessionId);
744
- log.info("Writer agent session registered", { sessionId, agentName, parentID: sessionInfo.parentID });
745
- }
746
- }
489
+ }
490
+ if (event.type === "session.idle") {
491
+ log.info("Session idle event received", { sessionId });
492
+ await evaluateAndFireReminder(sessionId);
493
+ }
494
+ if (event.type === "session.status") {
495
+ const status = event.properties?.status;
496
+ if (status?.type === "idle") {
497
+ log.info("Session status idle received", { sessionId });
498
+ await evaluateAndFireReminder(sessionId);
747
499
  }
748
500
  }
749
501
  } catch (err) {
750
502
  log.error("event handler error:", String(err));
751
503
  }
752
- },
753
- "tool.execute.before": async (input, output) => {
754
- const { tool, sessionID } = input;
755
- const isMemoryBankPath = (targetPath) => {
756
- const absPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(projectRoot, targetPath);
757
- const relativePath = path.relative(projectRoot, absPath);
758
- if (relativePath === ".." || relativePath.startsWith(".." + path.sep))
759
- return false;
760
- return relativePath === "memory-bank" || relativePath.startsWith("memory-bank" + path.sep) || relativePath.startsWith("memory-bank/");
761
- };
762
- const isWriterAllowed = (sid) => {
763
- if (writerSessionIDs.has(sid))
764
- return true;
765
- const sessionInfo = sessionsById.get(sid);
766
- const agentName = agentBySessionID.get(sid);
767
- if (agentName === WRITER_AGENT_NAME && sessionInfo?.parentID) {
768
- writerSessionIDs.add(sid);
769
- log.info("Writer agent late-registered", { sessionID: sid, agentName });
770
- return true;
771
- }
772
- return false;
773
- };
774
- const blockWrite = (reason, context) => {
775
- log.warn("Memory Bank write blocked", { sessionID, tool, reason, ...context });
776
- throw new Error(`[Memory Bank Guard] 写入 memory-bank/ 受限。
777
- ` + `请使用 delegate_task 调用 memory-bank-writer agent 来更新 Memory Bank。
778
- ` + `示例: delegate_task(subagent_type="memory-bank-writer", load_skills=["memory-bank-writer"], prompt="更新...")`);
779
- };
780
- if (tool === "Write" || tool === "Edit") {
781
- const targetPath = output.args?.filePath ?? output.args?.path ?? output.args?.filename;
782
- if (!targetPath)
783
- return;
784
- if (!isMemoryBankPath(targetPath))
785
- return;
786
- if (!targetPath.endsWith(".md")) {
787
- blockWrite("only .md files allowed", { targetPath });
788
- }
789
- if (isWriterAllowed(sessionID)) {
790
- log.debug("Writer agent write allowed", { sessionID, tool, targetPath });
791
- return;
792
- }
793
- blockWrite("not writer agent", {
794
- targetPath,
795
- isSubAgent: !!sessionsById.get(sessionID)?.parentID,
796
- agentName: agentBySessionID.get(sessionID)
797
- });
798
- }
799
- if (tool.toLowerCase() === "bash") {
800
- const command = output.args?.command;
801
- if (!command || typeof command !== "string")
802
- return;
803
- if (!/(?:^|[^A-Za-z0-9_-])memory-bank(?:$|[^A-Za-z0-9_-])/.test(command) && !command.startsWith("memory-bank"))
804
- return;
805
- const hasShellOperators = /[;&|]|\$\(|`/.test(command);
806
- if (!hasShellOperators) {
807
- const readOnlyPatterns = [
808
- /^\s*(ls|cat|head|tail|less|more|grep|rg|ag|find|tree|wc|file|stat)\b/,
809
- /^\s*git\s+(status|log|diff|show|blame)\b/
810
- ];
811
- if (readOnlyPatterns.some((p) => p.test(command)))
812
- return;
813
- }
814
- const writePatterns = [
815
- /(?:^|[^2])>/,
816
- />>/,
817
- /<</,
818
- /\|/,
819
- /\btee\b/,
820
- /\bsed\s+-i/,
821
- /\bperl\s+-[ip]/,
822
- /\bcp\b/,
823
- /\bmv\b/,
824
- /\brm\b/,
825
- /\bmkdir\b/,
826
- /\btouch\b/,
827
- /\bgit\s+(add|rm|mv|apply|checkout|restore|reset|clean|stash|commit)\b/,
828
- /\bpython\b.*\bopen\b/
829
- ];
830
- const isWriteOperation = writePatterns.some((p) => p.test(command));
831
- if (!isWriteOperation)
832
- return;
833
- if (isWriterAllowed(sessionID)) {
834
- log.debug("Writer agent bash write allowed", { sessionID, command: command.slice(0, 100) });
835
- return;
836
- }
837
- blockWrite("bash write operation", { command: command.slice(0, 200) });
838
- }
839
504
  }
840
505
  };
841
506
  };
package/dist/plugin.js CHANGED
@@ -45,10 +45,7 @@ var rootStates = new Map;
45
45
  var sessionMetas = new Map;
46
46
  var memoryBankExistsCache = new Map;
47
47
  var fileCache = new Map;
48
- var WRITER_AGENT_NAME = "memory-bank-writer";
49
48
  var sessionsById = new Map;
50
- var writerSessionIDs = new Set;
51
- var agentBySessionID = new Map;
52
49
  var messageGatingStates = new Map;
53
50
  var sessionAnchorStates = new Map;
54
51
  function makeStateKey(sessionId, root) {
@@ -213,7 +210,7 @@ drill_down: Step1 direct-read 1-3 details/*; Step2 \u9700\u8981\u8BC1\u636E/\u51
213
210
  output: \u56DE\u7B54\u5FC5\u987B\u7ED9\u5F15\u7528\u6307\u9488
214
211
  gating: \u9AD8\u98CE\u9669\u5199\u524D\u9700\u5DF2\u8BFB patterns.md \u6216\u8C03\u7528\u8FC7 memory-reader
215
212
 
216
- write: proxy_task(subagent_type="memory-bank-writer", prompt="Target:...\\nDraft:...")
213
+ write: \u4E3B agent \u76F4\u63A5 write/edit \u5199\u5165 memory-bank/\u3002\u5199\u5165\u524D Proposal \u2192 \u7528\u6237\u786E\u8BA4\u3002Plugin \u6CE8\u5165 writing guide\uFF08advisory\uFF09\u3002
217
214
  more: \u5B8C\u6574\u89C4\u8303\u89C1 /memory-bank skill
218
215
  `;
219
216
  } else {
@@ -228,7 +225,7 @@ drill_down: Step1 direct-read 1-3 details/*; Step2 \u9700\u8981\u8BC1\u636E/\u51
228
225
  output: \u56DE\u7B54\u5FC5\u987B\u7ED9\u5F15\u7528\u6307\u9488
229
226
  gating: \u9AD8\u98CE\u9669\u5199\u524D\u9700\u5DF2\u8BFB patterns.md \u6216\u8C03\u7528\u8FC7 memory-reader
230
227
 
231
- write: proxy_task(subagent_type="memory-bank-writer", prompt="Target:...\\nDraft:...")
228
+ write: \u4E3B agent \u76F4\u63A5 write/edit \u5199\u5165 memory-bank/\u3002\u5199\u5165\u524D Proposal \u2192 \u7528\u6237\u786E\u8BA4\u3002Plugin \u6CE8\u5165 writing guide\uFF08advisory\uFF09\u3002
232
229
  more: \u5B8C\u6574\u89C4\u8303\u89C1 /memory-bank skill
233
230
  `;
234
231
  }
@@ -963,8 +960,6 @@ ${triggers.join(`
963
960
  }
964
961
  sessionMetas.delete(sessionId);
965
962
  sessionsById.delete(sessionId);
966
- writerSessionIDs.delete(sessionId);
967
- agentBySessionID.delete(sessionId);
968
963
  sessionAnchorStates.delete(sessionId);
969
964
  log.info("Session deleted", { sessionId });
970
965
  }
@@ -1029,15 +1024,6 @@ ${triggers.join(`
1029
1024
  meta2.planOutputted = true;
1030
1025
  log.info("Plan outputted detected", { sessionId });
1031
1026
  }
1032
- const agentName = message?.agent;
1033
- if (agentName) {
1034
- agentBySessionID.set(sessionId, agentName);
1035
- const sessionInfo = sessionsById.get(sessionId);
1036
- if (agentName === WRITER_AGENT_NAME && sessionInfo?.parentID) {
1037
- writerSessionIDs.add(sessionId);
1038
- log.info("Writer agent session registered", { sessionId, agentName, parentID: sessionInfo.parentID });
1039
- }
1040
- }
1041
1027
  }
1042
1028
  }
1043
1029
  } catch (err) {
@@ -1338,16 +1324,21 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
1338
1324
  tool,
1339
1325
  targetPaths: dfTargetPaths
1340
1326
  });
1341
- throw new Error(`[Doc-First Gate] \u8BF7\u5148\u6C89\u6DC0\u5DE5\u4F5C\u6587\u6863\u518D\u5199\u4EE3\u7801\u3002
1327
+ throw new Error(`[Doc-First Gate] \u8BF7\u5148\u68C0\u67E5\u76F8\u5173\u6587\u6863\u518D\u5199\u4EE3\u7801\u3002
1342
1328
 
1343
- ` + `\u8BF7\u7528 MemoryWriter \u5148\u8BB0\u5F55\u4F60\u8981\u505A\u4EC0\u4E48\uFF1A
1344
- ` + `\u2022 \u4FEE Bug / \u8E29\u5751 \u2192 learnings/YYYY-MM-DD-xxx.md
1345
- ` + `\u2022 \u65B0\u529F\u80FD / \u9700\u6C42 \u2192 requirements/REQ-xxx.md
1346
- ` + `\u2022 \u91CD\u6784 / \u4F18\u5316 \u2192 design/design-xxx.md
1347
- ` + `\u2022 \u7B80\u5355\u53D8\u66F4 \u2192 \u8FFD\u52A0\u5230 progress.md
1329
+ ` + `**\u7B2C\u4E00\u6B65\uFF1A\u68C0\u67E5\u5DF2\u6709\u6587\u6863**
1330
+ ` + `\u5148\u5728 memory-bank/details/ \u4E2D\u641C\u7D22\u662F\u5426\u5DF2\u6709\u76F8\u5173\u7684\u9700\u6C42/\u8BBE\u8BA1\u6587\u6863\u3002
1348
1331
 
1349
- ` + `\u8C03\u7528\u65B9\u5F0F: proxy_task({ subagent_type: "memory-bank-writer", ... })
1350
- ` + `\u5199\u5B8C\u6587\u6863\u540E\u518D\u6267\u884C\u4EE3\u7801\u4FEE\u6539\u3002`);
1332
+ ` + `**\u7B2C\u4E8C\u6B65\uFF1A\u6839\u636E\u7ED3\u679C\u884C\u52A8**
1333
+ ` + `\u2022 **\u5DF2\u6709\u76F8\u5173\u6587\u6863** \u2192 \u5BF9\u7167\u68C0\u67E5\u539F\u59CB\u63CF\u8FF0\u662F\u5426\u51C6\u786E\uFF0C\u5982\u6709\u504F\u5DEE\u5148\u4FEE\u6B63\u6587\u6863\u518D\u6539\u4EE3\u7801
1334
+ ` + `\u2022 **\u65E0\u76F8\u5173\u6587\u6863** \u2192 \u7528 MemoryWriter \u5148\u8BB0\u5F55\u518D\u52A8\u624B\uFF1A
1335
+ ` + ` - \u4FEE Bug / \u8E29\u5751 \u2192 learnings/YYYY-MM-DD-xxx.md
1336
+ ` + ` - \u65B0\u529F\u80FD / \u9700\u6C42 \u2192 requirements/REQ-xxx.md
1337
+ ` + ` - \u91CD\u6784 / \u4F18\u5316 \u2192 design/design-xxx.md
1338
+ ` + ` - \u7B80\u5355\u53D8\u66F4 \u2192 \u8FFD\u52A0\u5230 progress.md
1339
+
1340
+ ` + `\u4F7F\u7528 write/edit \u5DE5\u5177\u76F4\u63A5\u5199\u5165 memory-bank/ \u4E0B\u5BF9\u5E94\u6587\u4EF6
1341
+ ` + `\u786E\u8BA4\u6587\u6863\u65E0\u8BEF\u540E\u518D\u6267\u884C\u4EE3\u7801\u4FEE\u6539\u3002`);
1351
1342
  } else {
1352
1343
  dfState.docFirstWarned = true;
1353
1344
  log.info("Doc-First Gate: warning issued", {
@@ -1363,15 +1354,20 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
1363
1354
  variant: PLUGIN_PROMPT_VARIANT,
1364
1355
  parts: [{
1365
1356
  type: "text",
1366
- text: `## \u26A0\uFE0F [Doc-First] \u5EFA\u8BAE\u5148\u6C89\u6DC0\u5DE5\u4F5C\u6587\u6863\u518D\u5199\u4EE3\u7801
1357
+ text: `## \u26A0\uFE0F [Doc-First] \u5EFA\u8BAE\u5148\u68C0\u67E5\u76F8\u5173\u6587\u6863\u518D\u5199\u4EE3\u7801
1358
+
1359
+ ` + `**\u7B2C\u4E00\u6B65\uFF1A\u68C0\u67E5\u5DF2\u6709\u6587\u6863**
1360
+ ` + `\u5148\u5728 memory-bank/details/ \u4E2D\u641C\u7D22\u662F\u5426\u5DF2\u6709\u76F8\u5173\u7684\u9700\u6C42/\u8BBE\u8BA1\u6587\u6863\u3002
1367
1361
 
1368
- ` + `\u8BF7\u7528 MemoryWriter \u5148\u8BB0\u5F55\u4F60\u8981\u505A\u4EC0\u4E48\uFF0C\u5E76\u4F5C\u4E3A\u7B2C\u4E00\u4F18\u5148\u7EA7 todo\uFF1A
1369
- ` + `\u2022 \u4FEE Bug / \u8E29\u5751 \u2192 learnings/YYYY-MM-DD-xxx.md
1370
- ` + `\u2022 \u65B0\u529F\u80FD / \u9700\u6C42 \u2192 requirements/REQ-xxx.md
1371
- ` + `\u2022 \u91CD\u6784 / \u4F18\u5316 \u2192 design/design-xxx.md
1372
- ` + `\u2022 \u7B80\u5355\u53D8\u66F4 \u2192 \u8FFD\u52A0\u5230 progress.md
1362
+ ` + `**\u7B2C\u4E8C\u6B65\uFF1A\u6839\u636E\u7ED3\u679C\u884C\u52A8**
1363
+ ` + `\u2022 **\u5DF2\u6709\u76F8\u5173\u6587\u6863** \u2192 \u5BF9\u7167\u68C0\u67E5\u539F\u59CB\u63CF\u8FF0\u662F\u5426\u51C6\u786E\uFF0C\u5982\u6709\u504F\u5DEE\u5148\u4FEE\u6B63\u6587\u6863\u518D\u6539\u4EE3\u7801
1364
+ ` + `\u2022 **\u65E0\u76F8\u5173\u6587\u6863** \u2192 \u7528 MemoryWriter \u5148\u8BB0\u5F55\u518D\u52A8\u624B\uFF1A
1365
+ ` + ` - \u4FEE Bug / \u8E29\u5751 \u2192 learnings/YYYY-MM-DD-xxx.md
1366
+ ` + ` - \u65B0\u529F\u80FD / \u9700\u6C42 \u2192 requirements/REQ-xxx.md
1367
+ ` + ` - \u91CD\u6784 / \u4F18\u5316 \u2192 design/design-xxx.md
1368
+ ` + ` - \u7B80\u5355\u53D8\u66F4 \u2192 \u8FFD\u52A0\u5230 progress.md
1373
1369
 
1374
- ` + `\u8C03\u7528\u65B9\u5F0F: \`proxy_task({ subagent_type: "memory-bank-writer", ... })\``
1370
+ ` + `\u4F7F\u7528 write/edit \u5DE5\u5177\u76F4\u63A5\u5199\u5165 memory-bank/ \u4E0B\u5BF9\u5E94\u6587\u4EF6`
1375
1371
  }]
1376
1372
  }
1377
1373
  }).catch((err) => log.error("Failed to send doc-first warning:", String(err)));
@@ -1379,43 +1375,30 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
1379
1375
  }
1380
1376
  }
1381
1377
  }
1382
- const markParentDocFirstSatisfied = (writerSessionID) => {
1383
- const parentID = sessionsById.get(writerSessionID)?.parentID;
1384
- if (!parentID)
1385
- return;
1386
- const parentMeta = getSessionMeta(parentID, projectRoot);
1387
- const parentMsgKey = parentMeta.lastUserMessageKey;
1388
- if (parentMsgKey) {
1389
- const parentGatingKey = `${parentID}::${parentMsgKey}`;
1390
- const parentGating = getMessageGatingState(parentGatingKey, parentID, projectRoot);
1391
- parentGating.docFirstSatisfied = true;
1392
- log.debug("Doc-First: parent satisfied via writer write", { writerSessionID, parentID, parentGatingKey });
1393
- }
1394
- const defaultGatingKey = `${parentID}::default`;
1395
- const defaultState = messageGatingStates.get(defaultGatingKey);
1396
- if (defaultState) {
1397
- defaultState.docFirstSatisfied = true;
1398
- }
1399
- parentMeta.pendingDocFirstSatisfied = true;
1400
- };
1401
- const isWriterAllowed = (sid) => {
1402
- if (writerSessionIDs.has(sid))
1403
- return true;
1404
- const sessionInfo = sessionsById.get(sid);
1405
- const agentName = agentBySessionID.get(sid);
1406
- if (agentName === WRITER_AGENT_NAME && sessionInfo?.parentID) {
1407
- writerSessionIDs.add(sid);
1408
- log.info("Writer agent late-registered", { sessionID: sid, agentName });
1409
- return true;
1410
- }
1411
- return false;
1412
- };
1413
- const blockWrite = (reason, context) => {
1414
- log.warn("Memory Bank write blocked", { sessionID, tool, reason, ...context });
1415
- throw new Error(`[Memory Bank Guard] \u5199\u5165 memory-bank/ \u53D7\u9650\u3002
1416
- ` + `\u8BF7\u4F7F\u7528 proxy_task \u8C03\u7528 memory-bank-writer agent \u6765\u66F4\u65B0 Memory Bank\u3002
1417
- ` + `\u793A\u4F8B: proxy_task({ subagent_type: "memory-bank-writer", description: "Memory Bank write", prompt: "Target: memory-bank/details/patterns.md\\nDraft:\\n1) ..." })`);
1418
- };
1378
+ async function injectWritingGuideline(sid) {
1379
+ client.session.prompt({
1380
+ path: { id: sid },
1381
+ body: {
1382
+ noReply: true,
1383
+ variant: PLUGIN_PROMPT_VARIANT,
1384
+ parts: [{
1385
+ type: "text",
1386
+ text: `## [Memory Bank Writing Guide]
1387
+
1388
+ ` + `\u6B63\u5728\u5199\u5165 memory-bank/\uFF0C\u8BF7\u5148\u52A0\u8F7D\u5199\u5165\u89C4\u8303\uFF1A
1389
+ ` + `read({ filePath: "~/.config/opencode/skills/memory-bank/references/writer.md" })`
1390
+ }]
1391
+ }
1392
+ }).catch((err) => log.error("Failed to send writing guideline:", String(err)));
1393
+ }
1394
+ function markDocFirstSatisfied(sid) {
1395
+ const meta = getSessionMeta(sid, projectRoot);
1396
+ const messageKey = meta.lastUserMessageKey || "default";
1397
+ const gatingKey = `${sid}::${messageKey}`;
1398
+ const gatingState = getMessageGatingState(gatingKey, sid, projectRoot);
1399
+ gatingState.docFirstSatisfied = true;
1400
+ log.debug("Doc-First: satisfied via direct memory-bank write", { sessionID: sid, gatingKey });
1401
+ }
1419
1402
  const extractPaths = (toolName, args) => {
1420
1403
  const paths = [];
1421
1404
  const pathArgs = ["filePath", "path", "filename", "file", "dest", "destination", "target"];
@@ -1460,18 +1443,13 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
1460
1443
  if (!await isMemoryBankPath(targetPath))
1461
1444
  continue;
1462
1445
  if (!targetPath.toLowerCase().endsWith(".md")) {
1463
- blockWrite("only .md files allowed", { targetPath });
1446
+ log.warn("Memory Bank write blocked (non-.md file)", { sessionID, tool, targetPath });
1447
+ throw new Error(`[Memory Bank Guard] memory-bank/ \u4E0B\u53EA\u5141\u8BB8\u5199\u5165 .md \u6587\u4EF6\u3002
1448
+ ` + `\u76EE\u6807\u6587\u4EF6: ${targetPath}`);
1464
1449
  }
1465
- if (isWriterAllowed(sessionID)) {
1466
- log.debug("Writer agent write allowed", { sessionID, tool, targetPath });
1467
- markParentDocFirstSatisfied(sessionID);
1468
- return;
1469
- }
1470
- blockWrite("not writer agent", {
1471
- targetPath,
1472
- isSubAgent: !!sessionsById.get(sessionID)?.parentID,
1473
- agentName: agentBySessionID.get(sessionID)
1474
- });
1450
+ await injectWritingGuideline(sessionID);
1451
+ markDocFirstSatisfied(sessionID);
1452
+ log.debug("Memory Bank write allowed", { sessionID, tool, targetPath });
1475
1453
  }
1476
1454
  }
1477
1455
  if (tool.toLowerCase() === "bash") {
@@ -1613,13 +1591,8 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
1613
1591
  if (redirectTarget && redirectTarget.includes("memory-bank")) {
1614
1592
  const resolvedTarget = path.resolve(projectRoot, redirectTarget);
1615
1593
  if (await isMemoryBankPath(resolvedTarget)) {
1616
- if (isWriterAllowed(sessionID)) {
1617
- log.debug("Writer agent bash redirect allowed", { sessionID, command: command.slice(0, 100) });
1618
- markParentDocFirstSatisfied(sessionID);
1619
- return;
1620
- }
1621
- blockWrite("bash redirect to memory-bank", { command: command.slice(0, 200), segment: segment.slice(0, 100) });
1622
- return;
1594
+ log.warn("Memory Bank bash redirect blocked", { sessionID, command: command.slice(0, 200) });
1595
+ throw new Error("[Memory Bank Guard] \u8BF7\u4F7F\u7528 write/edit \u5DE5\u5177\u5199\u5165 memory-bank/\uFF0C\u4E0D\u652F\u6301 bash \u5199\u5165\u3002");
1623
1596
  }
1624
1597
  }
1625
1598
  if (readOnlyPatterns.some((p) => p.test(segment))) {
@@ -1629,13 +1602,8 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
1629
1602
  for (const pathArg of pathArgs2) {
1630
1603
  const resolved = path.resolve(projectRoot, pathArg);
1631
1604
  if (await isMemoryBankPath(resolved)) {
1632
- if (isWriterAllowed(sessionID)) {
1633
- log.debug("Writer agent find with dangerous flags allowed", { sessionID });
1634
- markParentDocFirstSatisfied(sessionID);
1635
- return;
1636
- }
1637
- blockWrite("find with dangerous flags on memory-bank", { command: command.slice(0, 200), segment: segment.slice(0, 100) });
1638
- return;
1605
+ log.warn("Memory Bank bash find-write blocked", { sessionID, command: command.slice(0, 200) });
1606
+ throw new Error("[Memory Bank Guard] \u8BF7\u4F7F\u7528 write/edit \u5DE5\u5177\u5199\u5165 memory-bank/\uFF0C\u4E0D\u652F\u6301 bash \u5199\u5165\u3002");
1639
1607
  }
1640
1608
  }
1641
1609
  }
@@ -1646,13 +1614,8 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
1646
1614
  for (const pathArg of pathArgs) {
1647
1615
  const resolved = path.resolve(projectRoot, pathArg);
1648
1616
  if (await isMemoryBankPath(resolved)) {
1649
- if (isWriterAllowed(sessionID)) {
1650
- log.debug("Writer agent bash write allowed", { sessionID, command: command.slice(0, 100) });
1651
- markParentDocFirstSatisfied(sessionID);
1652
- return;
1653
- }
1654
- blockWrite("bash write to memory-bank", { command: command.slice(0, 200), pathArg, resolved });
1655
- return;
1617
+ log.warn("Memory Bank bash write blocked", { sessionID, command: command.slice(0, 200), pathArg });
1618
+ throw new Error("[Memory Bank Guard] \u8BF7\u4F7F\u7528 write/edit \u5DE5\u5177\u5199\u5165 memory-bank/\uFF0C\u4E0D\u652F\u6301 bash \u5199\u5165\u3002");
1656
1619
  }
1657
1620
  }
1658
1621
  log.debug("Bash command allowed (path not under root memory-bank/)", { segment: segment.slice(0, 100) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-bank-skill",
3
- "version": "7.3.1",
3
+ "version": "7.4.0",
4
4
  "description": "Memory Bank - 项目记忆系统,让 AI 助手在每次对话中都能快速理解项目上下文",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",
@@ -121,12 +121,16 @@ proxy_task({
121
121
 
122
122
  ### 写入阶段
123
123
 
124
- **核心约束**:主 Agent **禁止直接写入** `memory-bank/`,必须 delegate `memory-bank-writer`。
124
+ **写入方式**:主 Agent 直接使用 `write`/`edit` 工具写入 `memory-bank/`。Plugin 会自动注入 writing guideline(advisory)。
125
125
 
126
126
  流程(跨 turn):
127
127
  1. 主 Agent 检测到写入时机,用自然语言询问是否写入(含目标文件 + 要点)
128
128
  2. 用户自然语言确认("好"/"写"/"确认")或跳过("不用"/"跳过"/继续下一话题)
129
- 3. 下一 turn 调用:`proxy_task({ subagent_type: "memory-bank-writer", description: "Memory Bank write", prompt: "Target: ...\nDraft: ..." })`
129
+ 3. Agent 直接使用 `write`/`edit` 工具写入目标文件
130
+
131
+ **硬限制**:
132
+ - 只允许写入 `.md` 文件(Plugin 强制)
133
+ - 不允许通过 bash 写入(必须使用 write/edit 等结构化工具)
130
134
 
131
135
  详见 [writer.md](references/writer.md)
132
136
 
@@ -1,20 +1,32 @@
1
- # Memory Bank Writer 规则
1
+ # Memory Bank 写入规则
2
2
 
3
- > 此文档定义 Memory Bank 的写入规则,由 Writer Agent 执行。
3
+ > 此文档定义 Memory Bank 的写入规则。主 Agent 直接执行写入,Plugin 注入 writing guideline(advisory)。
4
4
 
5
- ## 调用方式
5
+ ## 写入方式
6
6
 
7
- 使用 `proxy_task`(Task tool)同步调用 memory-bank-writer:
7
+ Agent 直接使用 `write`/`edit` 工具写入 `memory-bank/` 下的 `.md` 文件:
8
8
 
9
9
  ```typescript
10
- proxy_task({
11
- subagent_type: "memory-bank-writer",
12
- description: "Memory Bank write (confirmed)",
13
- prompt: "You are updating Memory Bank.\nConstraints:\n- Edit ONLY the target file.\n- Keep changes minimal and consistent with existing format.\n- Do NOT invent facts.\nInput:\nTarget: {target_file}\nDraft:\n1) {bullet_1}\n2) {bullet_2}\nOutput: Show what file changed + brief preview of changes."
10
+ // 示例:更新 patterns.md
11
+ edit({
12
+ filePath: "memory-bank/details/patterns.md",
13
+ oldString: "...",
14
+ newString: "..."
15
+ })
16
+
17
+ // 示例:创建新需求文档
18
+ write({
19
+ filePath: "memory-bank/details/requirements/REQ-007-xxx.md",
20
+ content: "# REQ-007: ...\n\n..."
14
21
  })
15
22
  ```
16
23
 
17
- ## Writer 自动触发流程(跨 turn)
24
+ **Plugin 保护**:
25
+ - 只允许 `.md` 文件写入(非 `.md` 会被阻止)
26
+ - 不允许通过 bash 写入(必须使用 write/edit 等结构化工具)
27
+ - 写入时 Plugin 自动注入 writing guideline 提示
28
+
29
+ ## 写入触发流程(跨 turn)
18
30
 
19
31
  ### 触发时机
20
32
 
@@ -58,9 +70,9 @@ proxy_task({
58
70
 
59
71
  **混合意图**:如果用户确认同时问了其他问题(如"写吧,顺便问一下..."),先执行写入,再回答问题。
60
72
 
61
- **Step 3: 执行(下一 turn)**
73
+ **Step 3: 执行(本 turn 或下一 turn)**
62
74
 
63
- 收到确认后,调用 memory-bank-writer 执行写入,然后展示变更预览。
75
+ 收到确认后,直接使用 `write`/`edit` 工具写入,然后展示变更预览。
64
76
 
65
77
  ## Refresh 流程(/memory-bank-refresh)
66
78
 
@@ -297,34 +309,22 @@ USER_BLOCK 将保持不变。
297
309
 
298
310
  ---
299
311
 
300
- ## 职责分离(Auto-Trigger 模式)
312
+ ## 写入流程
301
313
 
302
- **Proposal 流程**:主 Agent 提供 Target + Draft,用户确认后 Writer 执行。
314
+ **Proposal 流程**:主 Agent 提议 用户确认 Agent 直接执行写入。
303
315
 
304
316
  | 步骤 | 负责方 | 动作 |
305
317
  |------|--------|------|
306
318
  | 1 | 主 Agent | 检测写入时机,自然语言询问是否写入 |
307
319
  | 2 | 用户 | 自然语言确认("好"/"写")或拒绝("不用"/"跳过") |
308
- | 3 | 主 Agent | 调用 `proxy_task({ subagent_type: "memory-bank-writer", ... })` |
309
- | 4 | **Writer** | 执行写入(可顺带更新 index.md / MEMORY.md) |
310
-
311
- ### 主 Agent 的 prompt 格式(调用 Writer 时)
312
-
313
- ```
314
- Target: memory-bank/details/patterns.md
315
- Draft:
316
- 1) {bullet 1}
317
- 2) {bullet 2}
318
- ```
319
-
320
- **说明**:Auto-Trigger 模式下,主 Agent 在 Proposal 中明确指定 Target 文件,用户确认后 Writer 按指定目标执行。
320
+ | 3 | 主 Agent | 直接使用 `write`/`edit` 工具写入目标文件 |
321
321
 
322
322
  ---
323
323
 
324
324
  ## 执行输出格式
325
325
 
326
326
  ```
327
- [Memory Bank Writer 执行完成]
327
+ [Memory Bank 写入完成]
328
328
 
329
329
  已执行:
330
330
  - 创建: memory-bank/details/design/xxx.md
@@ -339,9 +339,10 @@ Draft:
339
339
  ## 守卫机制
340
340
 
341
341
  Plugin 层面强制执行:
342
- - 只有 `memory-bank-writer` agent 能写入 `memory-bank/`
343
- - 只允许写入 `.md` 文件
344
- - agent 直接写入会被阻止
342
+ - Agent 直接使用 `write`/`edit` 写入 `memory-bank/`
343
+ - 只允许写入 `.md` 文件(非 `.md` 会被阻止)
344
+ - 不允许通过 bash 写入(必须使用结构化工具)
345
+ - 写入时 Plugin 自动注入 writing guideline 提示
345
346
 
346
347
  ---
347
348
 
@@ -364,10 +365,8 @@ Plugin 层面强制执行:
364
365
  ## 禁止行为
365
366
 
366
367
  - 不要跳过 Glob 检查
367
- - 不要等待用户确认(确认已由主 Agent 前置完成)
368
368
  - 不要修改 `memory-bank/` 以外的文件
369
369
  - 不要删除文件(除非迁移流程明确要求)
370
- - 不要自行决定写入内容(内容由主 Agent 提供)
371
370
 
372
371
  ---
373
372