memory-bank-skill 7.0.1 → 7.2.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
@@ -14,9 +14,9 @@ Memory Bank 是一个 **OpenCode 技能(Skill)**,用于解决 AI 对话的
14
14
  - 不记得当前在做什么任务
15
15
 
16
16
  Memory Bank 通过结构化 Markdown 文件持久化项目上下文,实现:
17
- - **零初始化**:不需要手动 init,随项目推进自动创建
17
+ - **按需初始化**:首次使用运行 `/memory-bank-refresh`,之后自动维护
18
18
  - **智能检索**:基于 AI 语义理解,自动加载相关上下文
19
- - **自动写入**:工作过程中自动记录重要发现和决策
19
+ - **引导式写入**:AI 检测写入时机并提议,用户确认后执行
20
20
 
21
21
  ---
22
22
 
@@ -48,7 +48,7 @@ bunx memory-bank-skill doctor
48
48
 
49
49
  | 操作 | 目标路径 |
50
50
  |------|----------|
51
- | 复制 Skill 文件 | `~/.config/opencode/skills/memory-bank/` `memory-bank-writer/` |
51
+ | 复制 Skill 文件 | `~/.config/opencode/skills/memory-bank/`(含 `references/writer.md`) |
52
52
  | 配置 opencode.json | 添加 `permission.skill=allow`,注册插件和 agent |
53
53
  | 注册 Agent | 添加 `memory-bank-writer` agent(用于写入守卫) |
54
54
  | 写入 manifest | `~/.config/opencode/skills/memory-bank/.manifest.json` |
@@ -57,13 +57,13 @@ bunx memory-bank-skill doctor
57
57
 
58
58
  ## 快速开始
59
59
 
60
- **不需要手动初始化。** Memory Bank 会在你开始工作时自动检测和创建:
60
+ 首次使用需运行初始化命令,之后 Memory Bank 会自动维护:
61
61
 
62
- | 场景 | AI 行为 |
63
- |------|---------|
64
- | **已有代码库** | 扫描 package.json/README 等,自动生成 MEMORY.md + details/tech.md |
65
- | **新项目** | 不创建任何文件,等你开始工作后按需创建 |
66
- | **已有 Memory Bank** | 直接读取 MEMORY.md,恢复上下文 |
62
+ | 场景 | 行为 |
63
+ |------|------|
64
+ | **首次使用** | 运行 `/memory-bank-refresh` 初始化,扫描项目生成 MEMORY.md |
65
+ | **已有 Memory Bank** | 自动注入 MEMORY.md 内容到 AI 上下文 |
66
+ | **需要更新** | AI 检测到变更时会提议更新,用户确认后执行 |
67
67
 
68
68
  ---
69
69
 
@@ -180,5 +180,5 @@ service=memory-bank Plugin initialized (unified) {"projectRoot":"..."}
180
180
 
181
181
  ## 版本
182
182
 
183
- - **版本**: 6.1.0
184
- - **主要更新**: v6.1.0 统一 Task Tool 架构(同步执行 + Writer 自动触发)
183
+ - **版本**: 7.1.0
184
+ - **主要更新**: v7.1 Index-First + Direct-First 架构(意图驱动路由 + Gating 收紧 + 两层读取协议 + 模板升级路径)
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.0.1",
30
+ version: "7.2.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",
@@ -358,7 +358,7 @@ async function installCommands(undoStack, manifestFiles) {
358
358
  const commandsDir = join(homedir(), ".config", "opencode", "commands");
359
359
  const commandPath = join(commandsDir, "memory-bank-refresh.md");
360
360
  const commandContent = `---
361
- description: \u521D\u59CB\u5316\u3001\u8FC1\u79FB\u6216\u5237\u65B0 Memory Bank
361
+ description: \u521D\u59CB\u5316\u3001\u5347\u7EA7\u3001\u8FC1\u79FB\u6216\u5237\u65B0 Memory Bank
362
362
  agent: memory-bank-writer
363
363
  ---
364
364
 
@@ -367,12 +367,14 @@ agent: memory-bank-writer
367
367
  ## \u68C0\u6D4B\u5F53\u524D\u7ED3\u6784
368
368
 
369
369
  1. \u68C0\u67E5 \`memory-bank/\` \u76EE\u5F55\u662F\u5426\u5B58\u5728
370
- 2. \u68C0\u67E5\u662F\u65B0\u7ED3\u6784\uFF08MEMORY.md\uFF09\u8FD8\u662F\u65E7\u7ED3\u6784\uFF08_index.md, brief.md, active.md\uFF09
370
+ 2. \u5982\u5B58\u5728 MEMORY.md\uFF0C\u68C0\u6D4B\u7248\u672C\u6807\u8BB0 \`<!-- MEMORY_BANK_TEMPLATE:v7.x -->\`
371
+ 3. \u68C0\u67E5\u662F\u5426\u5B58\u5728\u65E7\u7ED3\u6784\uFF08_index.md, brief.md, active.md\uFF09
371
372
 
372
373
  ## \u6839\u636E\u68C0\u6D4B\u7ED3\u679C\u6267\u884C
373
374
 
374
375
  - **\u4E0D\u5B58\u5728 memory-bank/**\uFF1A\u6267\u884C\u521D\u59CB\u5316\u6D41\u7A0B
375
- - **\u5B58\u5728 MEMORY.md**\uFF1A\u6267\u884C\u5237\u65B0\u6D41\u7A0B
376
+ - **\u5B58\u5728 MEMORY.md\uFF0C\u7248\u672C >= v7.1**\uFF1A\u6267\u884C\u5237\u65B0\u6D41\u7A0B
377
+ - **\u5B58\u5728 MEMORY.md\uFF0C\u7248\u672C < v7.1 \u6216\u6807\u8BB0\u7F3A\u5931**\uFF1A\u6267\u884C\u5347\u7EA7\u6D41\u7A0B\uFF08v7.0 \u2192 v7.1\uFF09
376
378
  - **\u5B58\u5728\u65E7\u7ED3\u6784**\uFF1A\u6267\u884C\u8FC1\u79FB\u6D41\u7A0B
377
379
 
378
380
  ## \u6D41\u7A0B\u8BE6\u60C5
package/dist/plugin.js CHANGED
@@ -18,7 +18,7 @@ var __toESM = (mod, isNodeMode, target) => {
18
18
  var __require = import.meta.require;
19
19
 
20
20
  // plugin/memory-bank.ts
21
- import { stat, readFile, realpath } from "fs/promises";
21
+ import { stat, readFile, access, realpath } from "fs/promises";
22
22
  import { execSync } from "child_process";
23
23
  import path from "path";
24
24
  var DEBUG = process.env.MEMORY_BANK_DEBUG === "1";
@@ -49,6 +49,7 @@ var sessionsById = new Map;
49
49
  var writerSessionIDs = new Set;
50
50
  var agentBySessionID = new Map;
51
51
  var messageGatingStates = new Map;
52
+ var sessionAnchorStates = new Map;
52
53
  function makeStateKey(sessionId, root) {
53
54
  return `${sessionId}::${root}`;
54
55
  }
@@ -203,26 +204,30 @@ async function buildMemoryBankContextWithMeta(projectRoot) {
203
204
  behaviorProtocol = `
204
205
  ## Memory Bank Protocol
205
206
  protocol_version: memory-bank/v1
207
+ template_version: v7.1
206
208
  fingerprint: MEMORY.md | ${totalChars.toLocaleString()} chars | mtime ${mtimeISO} | hash ${contentHash}${truncated ? " | TRUNCATED" : ""}
209
+
207
210
  trigger: (handled by Sisyphus keyTrigger)
208
- skip: \u901A\u7528\u95EE\u9898 / \u7B80\u5355\u8FFD\u95EE / \u7528\u6237\u8BF4"\u4E0D\u9700\u8981\u4E0A\u4E0B\u6587"
209
- invoke: proxy_task(subagent_type="memory-reader", prompt="\u7528\u6237\u95EE\u9898:{q}\\n\u6309\u8DEF\u7531\u8BFBdetails/,\u8F93\u51FAYAML")
210
- output: \u5355\u4E2A YAML \u5757\uFF0C\u542B evidence + conflicts\uFF1B\u7981\u8BFB .env/*secret*/*.pem/*.key\uFF1B\u6700\u591A 10 \u6587\u4EF6
211
- conflict: \u53D1\u73B0\u51B2\u7A81 \u2192 \u62A5\u544A\u7528\u6237\u5E76\u7B49\u5F85\u786E\u8BA4\uFF0C\u786E\u8BA4\u540E\u518D\u8C03\u7528 write
212
- write: proxy_task(subagent_type="memory-bank-writer", prompt="Target:...\\nDraft:...")\uFF1B\u7981\u6B62\u76F4\u63A5\u5199 memory-bank/
211
+ drill_down: Step1 direct-read 1-3 details/*; Step2 \u9700\u8981\u8BC1\u636E/\u51B2\u7A81/\u8DE8\u6587\u4EF6 \u2192 memory-reader
212
+ output: \u56DE\u7B54\u5FC5\u987B\u7ED9\u5F15\u7528\u6307\u9488
213
+ gating: \u9AD8\u98CE\u9669\u5199\u524D\u9700\u5DF2\u8BFB patterns.md \u6216\u8C03\u7528\u8FC7 memory-reader
214
+
215
+ write: proxy_task(subagent_type="memory-bank-writer", prompt="Target:...\\nDraft:...")
213
216
  more: \u5B8C\u6574\u89C4\u8303\u89C1 /memory-bank skill
214
217
  `;
215
218
  } else {
216
219
  behaviorProtocol = `
217
220
  ## Memory Bank Protocol
218
221
  protocol_version: memory-bank/v1
222
+ template_version: v7.1
219
223
  fingerprint: MEMORY.md | ${totalChars.toLocaleString()} chars | mtime ${mtimeISO} | hash ${contentHash}${truncated ? " | TRUNCATED" : ""}
220
- trigger: \u6D89\u53CA\u9879\u76EE\u80CC\u666F / \u95EE"\u4E3A\u4EC0\u4E48\u8FD9\u6837\u505A" / \u7279\u5B9A\u6A21\u5757\u5B9E\u73B0
221
- skip: \u901A\u7528\u95EE\u9898 / \u7B80\u5355\u8FFD\u95EE / \u7528\u6237\u8BF4"\u4E0D\u9700\u8981\u4E0A\u4E0B\u6587"
222
- invoke: proxy_task(subagent_type="memory-reader", prompt="\u7528\u6237\u95EE\u9898:{q}\\n\u6309\u8DEF\u7531\u8BFBdetails/,\u8F93\u51FAYAML")
223
- output: \u5355\u4E2A YAML \u5757\uFF0C\u542B evidence + conflicts\uFF1B\u7981\u8BFB .env/*secret*/*.pem/*.key\uFF1B\u6700\u591A 10 \u6587\u4EF6
224
- conflict: \u53D1\u73B0\u51B2\u7A81 \u2192 \u62A5\u544A\u7528\u6237\u5E76\u7B49\u5F85\u786E\u8BA4\uFF0C\u786E\u8BA4\u540E\u518D\u8C03\u7528 write
225
- write: proxy_task(subagent_type="memory-bank-writer", prompt="Target:...\\nDraft:...")\uFF1B\u7981\u6B62\u76F4\u63A5\u5199 memory-bank/
224
+
225
+ trigger: \u6D89\u53CA\u9879\u76EE\u5B9E\u73B0/\u8BBE\u8BA1/\u5386\u53F2\u539F\u56E0
226
+ drill_down: Step1 direct-read 1-3 details/*; Step2 \u9700\u8981\u8BC1\u636E/\u51B2\u7A81/\u8DE8\u6587\u4EF6 \u2192 memory-reader
227
+ output: \u56DE\u7B54\u5FC5\u987B\u7ED9\u5F15\u7528\u6307\u9488
228
+ gating: \u9AD8\u98CE\u9669\u5199\u524D\u9700\u5DF2\u8BFB patterns.md \u6216\u8C03\u7528\u8FC7 memory-reader
229
+
230
+ write: proxy_task(subagent_type="memory-bank-writer", prompt="Target:...\\nDraft:...")
226
231
  more: \u5B8C\u6574\u89C4\u8303\u89C1 /memory-bank skill
227
232
  `;
228
233
  }
@@ -362,6 +367,97 @@ function getMessageGatingState(gatingKey) {
362
367
  }
363
368
  return state;
364
369
  }
370
+ var ANCHOR_PATH_PATTERNS = [
371
+ /^memory-bank\/details\/requirements\//,
372
+ /^memory-bank\/details\/design\//,
373
+ /^memory-bank\/details\/progress\.md$/
374
+ ];
375
+ var MAX_ANCHORS = 5;
376
+ var ANCHOR_SENTINEL = "<memory-bank-anchors>";
377
+ var ANCHOR_SENTINEL_CLOSE = "</memory-bank-anchors>";
378
+ var FALLBACK_ANCHORS = [
379
+ "memory-bank/MEMORY.md",
380
+ "memory-bank/details/patterns.md"
381
+ ];
382
+ function canonicalizeRelPath(rawPath, projectRoot) {
383
+ const abs = path.isAbsolute(rawPath) ? rawPath : path.resolve(projectRoot, rawPath);
384
+ const rel = path.relative(projectRoot, abs);
385
+ if (rel.startsWith(".."))
386
+ return "";
387
+ const posix = rel.replace(/\\/g, "/");
388
+ return process.platform === "darwin" || process.platform === "win32" ? posix.toLowerCase() : posix;
389
+ }
390
+ function isAnchorPath(canonicalPath) {
391
+ return ANCHOR_PATH_PATTERNS.some((p) => p.test(canonicalPath));
392
+ }
393
+ function updateAnchorLRU(lru, canonicalPath) {
394
+ const idx = lru.indexOf(canonicalPath);
395
+ if (idx !== -1)
396
+ lru.splice(idx, 1);
397
+ lru.push(canonicalPath);
398
+ while (lru.length > MAX_ANCHORS)
399
+ lru.shift();
400
+ }
401
+ function getSessionAnchorState(sessionID) {
402
+ let state = sessionAnchorStates.get(sessionID);
403
+ if (!state) {
404
+ state = { anchorsLRU: [], recovery: null, compactionCount: 0 };
405
+ sessionAnchorStates.set(sessionID, state);
406
+ }
407
+ return state;
408
+ }
409
+ async function validateAnchorPaths(paths, projectRoot) {
410
+ const validated = [];
411
+ for (const p of paths) {
412
+ try {
413
+ await access(path.join(projectRoot, p));
414
+ validated.push(p);
415
+ } catch {}
416
+ }
417
+ return validated;
418
+ }
419
+ function buildRequiredAnchors(anchorsLRU) {
420
+ const required = new Set(FALLBACK_ANCHORS);
421
+ for (const p of anchorsLRU) {
422
+ required.add(p);
423
+ if (required.size >= MAX_ANCHORS)
424
+ break;
425
+ }
426
+ return [...required];
427
+ }
428
+ async function buildAnchorBlock(sessionID, projectRoot) {
429
+ const state = getSessionAnchorState(sessionID);
430
+ const tracked = buildRequiredAnchors(state.anchorsLRU);
431
+ const validated = await validateAnchorPaths(tracked, projectRoot);
432
+ if (validated.length === 0)
433
+ return null;
434
+ let miniSCR = "";
435
+ try {
436
+ const entryContent = await readTextCached(path.join(projectRoot, MEMORY_BANK_ENTRY));
437
+ if (entryContent) {
438
+ const focusMatch = entryContent.match(/## Current Focus\n([\s\S]*?)(?=\n## |$)/);
439
+ if (focusMatch) {
440
+ const lines = focusMatch[1].trim().split(`
441
+ `).slice(0, 6);
442
+ miniSCR = `
443
+ Session state (from MEMORY.md):
444
+ ${lines.join(`
445
+ `)}
446
+ `;
447
+ }
448
+ }
449
+ } catch {}
450
+ const count = state.compactionCount + 1;
451
+ return `${ANCHOR_SENTINEL}
452
+ ## POST-COMPACTION RECOVERY (Compaction #${count})
453
+
454
+ Compaction occurred. Before medium/high-risk writes, you MUST read:
455
+ ` + validated.map((p) => `- ${p}`).join(`
456
+ `) + `
457
+ ` + miniSCR + `
458
+ Recovery Gate blocks medium/high-risk writes until anchor files are read.
459
+ ${ANCHOR_SENTINEL_CLOSE}`;
460
+ }
365
461
  function extractWritePaths(toolName, args) {
366
462
  const paths = [];
367
463
  const pathArgs = ["filePath", "path", "filename", "file", "dest", "destination", "target"];
@@ -388,7 +484,7 @@ function extractWritePaths(toolName, args) {
388
484
  if (m[1])
389
485
  paths.push(m[1]);
390
486
  }
391
- for (const m of patchText.matchAll(/^\*\*\*\s+(?:Add|Update|Delete|Move to)\s+(?:File:\s*)?(.+)$/gm)) {
487
+ for (const m of patchText.matchAll(/^\*\*\*\s+(?:Add|Update|Delete|Move to:?)\s+(?:File:\s*)?(.+)$/gm)) {
392
488
  if (m[1])
393
489
  paths.push(m[1].trim());
394
490
  }
@@ -397,6 +493,12 @@ function extractWritePaths(toolName, args) {
397
493
  return [...new Set(paths)];
398
494
  }
399
495
  function assessWriteRisk(toolName, args, projectRoot) {
496
+ const safePatterns = [
497
+ /^memory-bank\/details\/progress\.md$/i,
498
+ /^memory-bank\/details\/learnings\//i,
499
+ /^memory-bank\/details\/requirements\//i,
500
+ /^memory-bank\/details\/.*\/index\.md$/i
501
+ ];
400
502
  const sensitivePatterns = [
401
503
  /^src\/auth\//i,
402
504
  /^src\/security\//i,
@@ -432,29 +534,23 @@ function assessWriteRisk(toolName, args, projectRoot) {
432
534
  /nginx\.conf$/i,
433
535
  /oauth/i,
434
536
  /sso/i,
435
- /rbac/i
537
+ /rbac/i,
538
+ /plugin\/.*\.ts$/i
436
539
  ];
437
- if (toolName === "multiedit")
438
- return "high";
439
- if (toolName === "apply_patch" || toolName === "patch") {
440
- const patchText = args.patchText ?? args.patch ?? args.diff;
441
- if (patchText) {
442
- const stdFileMatches = patchText.match(/^(?:\+\+\+|---)\s+[ab]\/(.+)$/gm) || [];
443
- const customFileMatches = patchText.match(/^\*\*\*\s+(?:Add|Update|Delete)\s+File:/gm) || [];
444
- const totalFiles = stdFileMatches.length / 2 + customFileMatches.length;
445
- if (totalFiles > 1)
446
- return "high";
447
- }
540
+ const allPaths = extractWritePaths(toolName, args);
541
+ const relativePaths = allPaths.map((p) => {
542
+ const rel = p.startsWith(projectRoot) ? p.slice(projectRoot.length).replace(/^\//, "") : p;
543
+ return rel.replace(/\\/g, "/");
544
+ });
545
+ if (relativePaths.length > 0 && relativePaths.every((p) => safePatterns.some((sp) => sp.test(p)))) {
546
+ return "low";
448
547
  }
449
- const pathArgs = ["filePath", "path", "filename", "file", "dest", "destination", "target"];
450
- for (const arg of pathArgs) {
451
- const val = args[arg];
452
- if (typeof val === "string") {
453
- const relativePath = val.startsWith(projectRoot) ? val.slice(projectRoot.length).replace(/^\//, "").replace(/\\/g, "/") : val.replace(/\\/g, "/");
454
- if (sensitivePatterns.some((p) => p.test(relativePath)))
455
- return "high";
456
- }
548
+ if (relativePaths.some((p) => sensitivePatterns.some((sp) => sp.test(p)))) {
549
+ return "high";
457
550
  }
551
+ const isMultiFile = toolName === "multiedit" || (toolName === "apply_patch" || toolName === "patch") && relativePaths.length > 1;
552
+ if (isMultiFile)
553
+ return "medium";
458
554
  return "low";
459
555
  }
460
556
  function computeTriggerSignature(state) {
@@ -761,31 +857,56 @@ ${triggers.join(`
761
857
  log.error("[HOOK] system.transform ERROR", String(err), { elapsed: Date.now() - hookStart });
762
858
  }
763
859
  },
764
- "experimental.session.compacting": async (_input, output) => {
860
+ "experimental.session.compacting": async (input, output) => {
765
861
  const hookStart = Date.now();
766
- log.info("[HOOK] session.compacting START");
862
+ const { sessionID } = input;
863
+ log.info("[HOOK] session.compacting START", { sessionID });
767
864
  try {
768
- if (output.context.some((s) => s.includes(SENTINEL_OPEN))) {
769
- log.info("[HOOK] session.compacting SKIP (sentinel exists)", { elapsed: Date.now() - hookStart });
770
- return;
771
- }
772
- log.info("[HOOK] session.compacting building context...");
773
- const ctx = await buildMemoryBankContext(projectRoot);
774
- log.info("[HOOK] session.compacting context built", { hasCtx: !!ctx, elapsed: Date.now() - hookStart });
775
- if (ctx) {
776
- output.context.push(ctx);
777
- log.info("[HOOK] session.compacting DONE (ctx pushed)", { elapsed: Date.now() - hookStart });
778
- return;
779
- }
780
- const initInstruction = `${SENTINEL_OPEN}
865
+ if (!output.context.some((s) => s.includes(SENTINEL_OPEN))) {
866
+ log.info("[HOOK] session.compacting building context...");
867
+ const ctx = await buildMemoryBankContext(projectRoot);
868
+ log.info("[HOOK] session.compacting context built", { hasCtx: !!ctx, elapsed: Date.now() - hookStart });
869
+ if (ctx) {
870
+ output.context.push(ctx);
871
+ } else {
872
+ const initInstruction = `${SENTINEL_OPEN}
781
873
  ` + `# Memory Bank \u672A\u542F\u7528
782
874
 
783
875
  ` + `\u9879\u76EE \`${path.basename(projectRoot)}\` \u5C1A\u672A\u542F\u7528 Memory Bank\u3002
784
876
 
785
877
  ` + `\u53EF\u9009\uFF1A\u5982\u9700\u542F\u7528\u9879\u76EE\u8BB0\u5FC6\uFF0C\u8FD0\u884C \`/memory-bank-refresh\`\u3002
786
878
  ` + `${SENTINEL_CLOSE}`;
787
- output.context.push(initInstruction);
788
- log.info("[HOOK] session.compacting DONE (init pushed)", { elapsed: Date.now() - hookStart });
879
+ output.context.push(initInstruction);
880
+ log.info("[HOOK] session.compacting DONE (init pushed, no anchors)", { elapsed: Date.now() - hookStart });
881
+ return;
882
+ }
883
+ }
884
+ if (!output.context.some((s) => s.includes(ANCHOR_SENTINEL))) {
885
+ const anchorBlock = await buildAnchorBlock(sessionID, projectRoot);
886
+ if (anchorBlock) {
887
+ output.context.push(anchorBlock);
888
+ log.info("[HOOK] session.compacting anchor block injected", { sessionID, elapsed: Date.now() - hookStart });
889
+ }
890
+ }
891
+ const anchorState = getSessionAnchorState(sessionID);
892
+ const tracked = buildRequiredAnchors(anchorState.anchorsLRU);
893
+ const validPaths = await validateAnchorPaths(tracked, projectRoot);
894
+ if (validPaths.length > 0) {
895
+ anchorState.recovery = {
896
+ required: true,
897
+ anchorPaths: validPaths,
898
+ readFiles: new Set,
899
+ activatedAt: Date.now()
900
+ };
901
+ anchorState.compactionCount++;
902
+ log.info("[HOOK] session.compacting recovery set", {
903
+ sessionID,
904
+ anchorPaths: validPaths,
905
+ compactionCount: anchorState.compactionCount,
906
+ elapsed: Date.now() - hookStart
907
+ });
908
+ }
909
+ log.info("[HOOK] session.compacting DONE", { elapsed: Date.now() - hookStart });
789
910
  } catch (err) {
790
911
  log.error("[HOOK] session.compacting ERROR", String(err), { elapsed: Date.now() - hookStart });
791
912
  }
@@ -823,6 +944,7 @@ ${triggers.join(`
823
944
  sessionsById.delete(sessionId);
824
945
  writerSessionIDs.delete(sessionId);
825
946
  agentBySessionID.delete(sessionId);
947
+ sessionAnchorStates.delete(sessionId);
826
948
  log.info("Session deleted", { sessionId });
827
949
  }
828
950
  if (event.type === "message.updated") {
@@ -903,6 +1025,90 @@ ${triggers.join(`
903
1025
  },
904
1026
  "tool.execute.before": async (input, output) => {
905
1027
  const { tool, sessionID } = input;
1028
+ const toolLowerForRecovery = tool.toLowerCase();
1029
+ const anchorState = getSessionAnchorState(sessionID);
1030
+ const recovery = anchorState.recovery;
1031
+ if (recovery?.required) {
1032
+ const readToolsForRecovery = ["read"];
1033
+ if (readToolsForRecovery.includes(toolLowerForRecovery)) {
1034
+ const targetPath = output.args?.filePath || output.args?.path;
1035
+ if (targetPath) {
1036
+ const canonical = canonicalizeRelPath(targetPath, projectRoot);
1037
+ if (canonical && recovery.anchorPaths.includes(canonical)) {
1038
+ recovery.readFiles.add(canonical);
1039
+ const allRead = recovery.anchorPaths.every((p) => recovery.readFiles.has(p));
1040
+ if (allRead) {
1041
+ anchorState.recovery = null;
1042
+ log.info("Recovery Gate: cleared (all anchors read)", { sessionID, readFiles: [...recovery.readFiles] });
1043
+ }
1044
+ }
1045
+ }
1046
+ }
1047
+ if (toolLowerForRecovery === "proxy_task") {
1048
+ const subagentType = output.args?.subagent_type;
1049
+ if (subagentType === "memory-reader") {
1050
+ anchorState.recovery = null;
1051
+ log.info("Recovery Gate: cleared (memory-reader called)", { sessionID });
1052
+ }
1053
+ }
1054
+ if (anchorState.recovery?.required) {
1055
+ const writeToolsForRecovery = ["write", "edit", "multiedit", "apply_patch", "patch"];
1056
+ const isWriteTool = writeToolsForRecovery.includes(toolLowerForRecovery);
1057
+ const isBashWrite = toolLowerForRecovery === "bash" && (() => {
1058
+ const cmd = output.args?.command || "";
1059
+ const bashWritePatterns = [
1060
+ /(?<![0-9&])>(?![&>])\s*\S/,
1061
+ /(?<![0-9&])>>\s*\S/,
1062
+ /\|\s*tee\s/,
1063
+ /\bsed\s+(-[^-]*)?-i/,
1064
+ /\bperl\s+(-[^-]*)?-[pi]/
1065
+ ];
1066
+ return bashWritePatterns.some((p) => p.test(cmd));
1067
+ })();
1068
+ if (isWriteTool || isBashWrite) {
1069
+ const riskLevel = isWriteTool ? assessWriteRisk(toolLowerForRecovery, output.args || {}, projectRoot) : "medium";
1070
+ if (riskLevel !== "low") {
1071
+ const validAnchors = await validateAnchorPaths(recovery.anchorPaths, projectRoot);
1072
+ if (validAnchors.length === 0) {
1073
+ anchorState.recovery = null;
1074
+ log.info("Recovery Gate: cleared (all anchor files removed)", { sessionID });
1075
+ } else if (validAnchors.length !== recovery.anchorPaths.length) {
1076
+ recovery.anchorPaths = validAnchors;
1077
+ const allRead = validAnchors.every((p) => recovery.readFiles.has(p));
1078
+ if (allRead) {
1079
+ anchorState.recovery = null;
1080
+ log.info("Recovery Gate: cleared (remaining anchors all read)", { sessionID });
1081
+ }
1082
+ }
1083
+ if (anchorState.recovery?.required) {
1084
+ log.warn("Recovery Gate: write blocked", {
1085
+ sessionID,
1086
+ tool,
1087
+ riskLevel,
1088
+ anchorPaths: recovery.anchorPaths
1089
+ });
1090
+ throw new Error(`[Recovery Gate] Compaction detected. Before proceeding, read these anchor files:
1091
+ ` + recovery.anchorPaths.map((p) => ` read({ filePath: "${p}" })`).join(`
1092
+ `) + `
1093
+ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
1094
+ }
1095
+ }
1096
+ }
1097
+ }
1098
+ }
1099
+ if (!recovery?.required) {
1100
+ const readToolsForAnchors = ["read"];
1101
+ if (readToolsForAnchors.includes(toolLowerForRecovery)) {
1102
+ const targetPath = output.args?.filePath || output.args?.path;
1103
+ if (targetPath) {
1104
+ const canonical = canonicalizeRelPath(targetPath, projectRoot);
1105
+ if (canonical && isAnchorPath(canonical)) {
1106
+ updateAnchorLRU(anchorState.anchorsLRU, canonical);
1107
+ log.debug("Anchor tracked", { sessionID, path: canonical, lru: anchorState.anchorsLRU });
1108
+ }
1109
+ }
1110
+ }
1111
+ }
906
1112
  const isCaseInsensitiveFS = process.platform === "darwin" || process.platform === "win32";
907
1113
  const normalize = (p) => isCaseInsensitiveFS ? p.toLowerCase() : p;
908
1114
  let realProjectRoot = null;
@@ -948,15 +1154,17 @@ ${triggers.join(`
948
1154
  const gatingKey = `${sessionID}::${messageKey}`;
949
1155
  const gatingState = getMessageGatingState(gatingKey);
950
1156
  const toolLower2 = tool.toLowerCase();
951
- const readTools = ["read", "glob", "grep"];
1157
+ const readTools = ["read"];
952
1158
  if (readTools.includes(toolLower2)) {
953
- const targetPath = output.args?.filePath || output.args?.path || output.args?.pattern;
1159
+ const targetPath = output.args?.filePath || output.args?.path;
954
1160
  if (targetPath && await isMemoryBankPath(targetPath)) {
955
1161
  gatingState.readFiles.add(targetPath);
956
1162
  const relativePath = targetPath.replace(projectRoot, "").replace(/^\//, "");
957
- if (relativePath.includes("MEMORY.md") || relativePath.includes("patterns.md") || relativePath.includes("details/")) {
1163
+ const normalizedPath = relativePath.replace(/\\/g, "/").replace(/^\//, "").toLowerCase();
1164
+ const isPatterns = normalizedPath === "memory-bank/details/patterns.md";
1165
+ if (isPatterns) {
958
1166
  gatingState.contextSatisfied = true;
959
- log.debug("Gating: context satisfied via read", { sessionID, gatingKey, targetPath });
1167
+ log.debug("Gating: context satisfied via patterns.md read", { sessionID, gatingKey, targetPath });
960
1168
  }
961
1169
  }
962
1170
  }
@@ -982,36 +1190,33 @@ ${triggers.join(`
982
1190
  targetPaths
983
1191
  });
984
1192
  throw new Error(`[Memory Bank Gating] \u68C0\u6D4B\u5230\u9AD8\u98CE\u9669\u5199\u64CD\u4F5C\uFF0C\u4F46\u672C\u8F6E\u672A\u8BFB\u53D6\u9879\u76EE\u4E0A\u4E0B\u6587\u3002
985
- ` + `\u8BF7\u5148\u6267\u884C: read({ filePath: "memory-bank/details/patterns.md" })
986
- ` + `\u6216\u6267\u884C: read({ filePath: "memory-bank/MEMORY.md" })`);
987
- } else if (riskLevel === "high") {
988
- log.warn("Gating: high-risk write warning (context not read)", {
1193
+ ` + `\u8BF7\u5148\u6267\u884C: read({ filePath: "memory-bank/details/patterns.md" })`);
1194
+ } else if ((riskLevel === "high" || riskLevel === "medium") && !gatingState.warnedThisMessage) {
1195
+ gatingState.warnedThisMessage = true;
1196
+ log.warn("Gating: write warning (context not read)", {
989
1197
  sessionID,
990
1198
  gatingKey,
991
1199
  tool,
992
1200
  riskLevel,
993
1201
  targetPaths
994
1202
  });
995
- if (!gatingState.warnedThisMessage) {
996
- gatingState.warnedThisMessage = true;
997
- client.session.prompt({
998
- path: { id: sessionID },
999
- body: {
1000
- noReply: true,
1001
- variant: PLUGIN_PROMPT_VARIANT,
1002
- parts: [{
1003
- type: "text",
1004
- text: `## [Memory Bank Gating Warning]
1203
+ client.session.prompt({
1204
+ path: { id: sessionID },
1205
+ body: {
1206
+ noReply: true,
1207
+ variant: PLUGIN_PROMPT_VARIANT,
1208
+ parts: [{
1209
+ type: "text",
1210
+ text: `## [Memory Bank Gating Warning]
1005
1211
 
1006
- \u68C0\u6D4B\u5230\u9AD8\u98CE\u9669\u5199\u64CD\u4F5C\uFF0C\u4F46\u672C\u8F6E\u672A\u8BFB\u53D6\u9879\u76EE\u4E0A\u4E0B\u6587\u3002
1212
+ \u68C0\u6D4B\u5230${riskLevel === "high" ? "\u9AD8" : "\u4E2D"}\u98CE\u9669\u5199\u64CD\u4F5C\uFF0C\u4F46\u672C\u8F6E\u672A\u8BFB\u53D6\u9879\u76EE\u4E0A\u4E0B\u6587\u3002
1007
1213
 
1008
1214
  \u5EFA\u8BAE\u5148\u6267\u884C: \`read({ filePath: "memory-bank/details/patterns.md" })\`
1009
1215
 
1010
- \u5982\u9700\u8DF3\u8FC7\u68C0\u67E5\uFF0C\u7528\u6237\u53EF\u56DE\u590D"\u8DF3\u8FC7\u4E0A\u4E0B\u6587"\u3002`
1011
- }]
1012
- }
1013
- }).catch((err) => log.error("Failed to send gating warning:", String(err)));
1014
- }
1216
+ \u6216\u8C03\u7528: \`proxy_task({ subagent_type: "memory-reader", ... })\``
1217
+ }]
1218
+ }
1219
+ }).catch((err) => log.error("Failed to send gating warning:", String(err)));
1015
1220
  }
1016
1221
  }
1017
1222
  }
@@ -1024,16 +1229,45 @@ ${triggers.join(`
1024
1229
  /\bsed\s+(-[^-]*)?-i/,
1025
1230
  /\bperl\s+(-[^-]*)?-[pi]/
1026
1231
  ];
1232
+ const bashSensitivePatterns = [
1233
+ /package\.json/i,
1234
+ /\.env/i,
1235
+ /tsconfig\.json/i,
1236
+ /src\/auth\//i,
1237
+ /src\/security\//i,
1238
+ /plugin\//i,
1239
+ /docker/i,
1240
+ /\.github\/workflows/i,
1241
+ /infra\//i
1242
+ ];
1027
1243
  const isLikelyWrite = bashWritePatterns.some((p) => p.test(command));
1244
+ const isSensitiveTarget = bashSensitivePatterns.some((p) => p.test(command));
1028
1245
  if (isLikelyWrite) {
1029
- const riskLevel = assessWriteRisk("bash", { command }, projectRoot);
1246
+ const riskLevel = isSensitiveTarget ? "high" : "medium";
1030
1247
  if (riskLevel === "high" && GATING_MODE === "block") {
1031
1248
  log.warn("Gating: bash write blocked (context not read)", { sessionID, gatingKey, command: command.slice(0, 100) });
1032
1249
  throw new Error(`[Memory Bank Gating] \u68C0\u6D4B\u5230 bash \u5199\u5165\u64CD\u4F5C\uFF0C\u4F46\u672C\u8F6E\u672A\u8BFB\u53D6\u9879\u76EE\u4E0A\u4E0B\u6587\u3002
1033
1250
  ` + `\u8BF7\u5148\u6267\u884C: read({ filePath: "memory-bank/details/patterns.md" })`);
1034
- } else if (GATING_MODE === "warn" && !gatingState.warnedThisMessage) {
1251
+ } else if (!gatingState.warnedThisMessage) {
1035
1252
  gatingState.warnedThisMessage = true;
1036
- log.warn("Gating: bash write warning (context not read)", { sessionID, gatingKey, command: command.slice(0, 100) });
1253
+ log.warn("Gating: bash write warning (context not read)", { sessionID, gatingKey, riskLevel, command: command.slice(0, 100) });
1254
+ client.session.prompt({
1255
+ path: { id: sessionID },
1256
+ body: {
1257
+ noReply: true,
1258
+ variant: PLUGIN_PROMPT_VARIANT,
1259
+ parts: [{
1260
+ type: "text",
1261
+ text: `## [Memory Bank Gating Warning]
1262
+
1263
+ \u68C0\u6D4B\u5230 bash ${riskLevel === "high" ? "\u9AD8" : "\u4E2D"}\u98CE\u9669\u5199\u5165\u64CD\u4F5C\uFF0C\u4F46\u672C\u8F6E\u672A\u8BFB\u53D6\u9879\u76EE\u4E0A\u4E0B\u6587\u3002
1264
+
1265
+ \u5EFA\u8BAE\u5148\u6267\u884C: \`read({ filePath: "memory-bank/details/patterns.md" })\`
1266
+
1267
+ \u6216\u8C03\u7528: \`proxy_task({ subagent_type: "memory-reader", ... })\``
1268
+ }]
1269
+ }
1270
+ }).catch((err) => log.error("Failed to send bash gating warning:", String(err)));
1037
1271
  }
1038
1272
  }
1039
1273
  }
@@ -1082,7 +1316,7 @@ ${triggers.join(`
1082
1316
  if (m[1])
1083
1317
  paths.push(m[1]);
1084
1318
  }
1085
- for (const m of patchText.matchAll(/^\*\*\*\s+(?:Add|Update|Delete|Move to)\s+(?:File:\s*)?(.+)$/gm)) {
1319
+ for (const m of patchText.matchAll(/^\*\*\*\s+(?:Add|Update|Delete|Move to:?)\s+(?:File:\s*)?(.+)$/gm)) {
1086
1320
  if (m[1])
1087
1321
  paths.push(m[1].trim());
1088
1322
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-bank-skill",
3
- "version": "7.0.1",
3
+ "version": "7.2.0",
4
4
  "description": "Memory Bank - 项目记忆系统,让 AI 助手在每次对话中都能快速理解项目上下文",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",