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 +11 -11
- package/dist/cli.js +6 -4
- package/dist/plugin.js +312 -78
- package/package.json +1 -1
- package/skills/memory-bank/README.md +14 -33
- package/skills/memory-bank/SKILL.md +25 -17
- package/skills/memory-bank/references/advanced-rules.md +14 -1
- package/skills/memory-bank/references/memory-reader-prompt.md +12 -22
- package/skills/memory-bank/references/reader.md +62 -74
- package/skills/memory-bank/references/schema.md +8 -1
- package/skills/memory-bank/references/templates.md +58 -16
- package/skills/memory-bank/references/writer.md +114 -29
package/README.md
CHANGED
|
@@ -14,9 +14,9 @@ Memory Bank 是一个 **OpenCode 技能(Skill)**,用于解决 AI 对话的
|
|
|
14
14
|
- 不记得当前在做什么任务
|
|
15
15
|
|
|
16
16
|
Memory Bank 通过结构化 Markdown 文件持久化项目上下文,实现:
|
|
17
|
-
-
|
|
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
|
|
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
|
-
|
|
60
|
+
首次使用需运行初始化命令,之后 Memory Bank 会自动维护:
|
|
61
61
|
|
|
62
|
-
| 场景 |
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
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
|
-
- **版本**:
|
|
184
|
-
- **主要更新**:
|
|
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
|
|
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. \
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
write: proxy_task(subagent_type="memory-bank-writer", prompt="Target:...\\nDraft:...")
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
output: \
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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 (
|
|
860
|
+
"experimental.session.compacting": async (input, output) => {
|
|
765
861
|
const hookStart = Date.now();
|
|
766
|
-
|
|
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
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
788
|
-
|
|
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"
|
|
1157
|
+
const readTools = ["read"];
|
|
952
1158
|
if (readTools.includes(toolLower2)) {
|
|
953
|
-
const targetPath = output.args?.filePath || output.args?.path
|
|
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
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
log.warn("Gating:
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
\
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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 =
|
|
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 (
|
|
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
|
}
|