memory-bank-skill 7.3.2 → 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 +4 -4
- package/dist/cli.js +9 -21
- package/dist/memory-bank.js +43 -378
- package/dist/plugin.js +40 -87
- package/package.json +1 -1
- package/skills/memory-bank/SKILL.md +6 -2
- package/skills/memory-bank/references/writer.md +31 -32
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ bunx memory-bank-skill install
|
|
|
32
32
|
|
|
33
33
|
### 自定义模型
|
|
34
34
|
|
|
35
|
-
默认使用 `cliproxy/claude-opus-4-
|
|
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-
|
|
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.
|
|
184
|
-
- **主要更新**:
|
|
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.
|
|
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-
|
|
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 (
|
|
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
|
|
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,
|
|
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-
|
|
812
|
-
Default: cliproxy/claude-opus-4-
|
|
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
|
package/dist/memory-bank.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
|
660
|
-
|
|
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
|
-
|
|
696
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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:
|
|
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:
|
|
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) {
|
|
@@ -1351,7 +1337,7 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1351
1337
|
` + ` - \u91CD\u6784 / \u4F18\u5316 \u2192 design/design-xxx.md
|
|
1352
1338
|
` + ` - \u7B80\u5355\u53D8\u66F4 \u2192 \u8FFD\u52A0\u5230 progress.md
|
|
1353
1339
|
|
|
1354
|
-
` + `\
|
|
1340
|
+
` + `\u4F7F\u7528 write/edit \u5DE5\u5177\u76F4\u63A5\u5199\u5165 memory-bank/ \u4E0B\u5BF9\u5E94\u6587\u4EF6
|
|
1355
1341
|
` + `\u786E\u8BA4\u6587\u6863\u65E0\u8BEF\u540E\u518D\u6267\u884C\u4EE3\u7801\u4FEE\u6539\u3002`);
|
|
1356
1342
|
} else {
|
|
1357
1343
|
dfState.docFirstWarned = true;
|
|
@@ -1381,7 +1367,7 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1381
1367
|
` + ` - \u91CD\u6784 / \u4F18\u5316 \u2192 design/design-xxx.md
|
|
1382
1368
|
` + ` - \u7B80\u5355\u53D8\u66F4 \u2192 \u8FFD\u52A0\u5230 progress.md
|
|
1383
1369
|
|
|
1384
|
-
` + `\
|
|
1370
|
+
` + `\u4F7F\u7528 write/edit \u5DE5\u5177\u76F4\u63A5\u5199\u5165 memory-bank/ \u4E0B\u5BF9\u5E94\u6587\u4EF6`
|
|
1385
1371
|
}]
|
|
1386
1372
|
}
|
|
1387
1373
|
}).catch((err) => log.error("Failed to send doc-first warning:", String(err)));
|
|
@@ -1389,43 +1375,30 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1389
1375
|
}
|
|
1390
1376
|
}
|
|
1391
1377
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
if (agentName === WRITER_AGENT_NAME && sessionInfo?.parentID) {
|
|
1417
|
-
writerSessionIDs.add(sid);
|
|
1418
|
-
log.info("Writer agent late-registered", { sessionID: sid, agentName });
|
|
1419
|
-
return true;
|
|
1420
|
-
}
|
|
1421
|
-
return false;
|
|
1422
|
-
};
|
|
1423
|
-
const blockWrite = (reason, context) => {
|
|
1424
|
-
log.warn("Memory Bank write blocked", { sessionID, tool, reason, ...context });
|
|
1425
|
-
throw new Error(`[Memory Bank Guard] \u5199\u5165 memory-bank/ \u53D7\u9650\u3002
|
|
1426
|
-
` + `\u8BF7\u4F7F\u7528 proxy_task \u8C03\u7528 memory-bank-writer agent \u6765\u66F4\u65B0 Memory Bank\u3002
|
|
1427
|
-
` + `\u793A\u4F8B: proxy_task({ subagent_type: "memory-bank-writer", description: "Memory Bank write", prompt: "Target: memory-bank/details/patterns.md\\nDraft:\\n1) ..." })`);
|
|
1428
|
-
};
|
|
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
|
+
}
|
|
1429
1402
|
const extractPaths = (toolName, args) => {
|
|
1430
1403
|
const paths = [];
|
|
1431
1404
|
const pathArgs = ["filePath", "path", "filename", "file", "dest", "destination", "target"];
|
|
@@ -1470,18 +1443,13 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1470
1443
|
if (!await isMemoryBankPath(targetPath))
|
|
1471
1444
|
continue;
|
|
1472
1445
|
if (!targetPath.toLowerCase().endsWith(".md")) {
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
log.debug("Writer agent write allowed", { sessionID, tool, targetPath });
|
|
1477
|
-
markParentDocFirstSatisfied(sessionID);
|
|
1478
|
-
return;
|
|
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}`);
|
|
1479
1449
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
agentName: agentBySessionID.get(sessionID)
|
|
1484
|
-
});
|
|
1450
|
+
await injectWritingGuideline(sessionID);
|
|
1451
|
+
markDocFirstSatisfied(sessionID);
|
|
1452
|
+
log.debug("Memory Bank write allowed", { sessionID, tool, targetPath });
|
|
1485
1453
|
}
|
|
1486
1454
|
}
|
|
1487
1455
|
if (tool.toLowerCase() === "bash") {
|
|
@@ -1623,13 +1591,8 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1623
1591
|
if (redirectTarget && redirectTarget.includes("memory-bank")) {
|
|
1624
1592
|
const resolvedTarget = path.resolve(projectRoot, redirectTarget);
|
|
1625
1593
|
if (await isMemoryBankPath(resolvedTarget)) {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
markParentDocFirstSatisfied(sessionID);
|
|
1629
|
-
return;
|
|
1630
|
-
}
|
|
1631
|
-
blockWrite("bash redirect to memory-bank", { command: command.slice(0, 200), segment: segment.slice(0, 100) });
|
|
1632
|
-
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");
|
|
1633
1596
|
}
|
|
1634
1597
|
}
|
|
1635
1598
|
if (readOnlyPatterns.some((p) => p.test(segment))) {
|
|
@@ -1639,13 +1602,8 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1639
1602
|
for (const pathArg of pathArgs2) {
|
|
1640
1603
|
const resolved = path.resolve(projectRoot, pathArg);
|
|
1641
1604
|
if (await isMemoryBankPath(resolved)) {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
markParentDocFirstSatisfied(sessionID);
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
blockWrite("find with dangerous flags on memory-bank", { command: command.slice(0, 200), segment: segment.slice(0, 100) });
|
|
1648
|
-
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");
|
|
1649
1607
|
}
|
|
1650
1608
|
}
|
|
1651
1609
|
}
|
|
@@ -1656,13 +1614,8 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1656
1614
|
for (const pathArg of pathArgs) {
|
|
1657
1615
|
const resolved = path.resolve(projectRoot, pathArg);
|
|
1658
1616
|
if (await isMemoryBankPath(resolved)) {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
markParentDocFirstSatisfied(sessionID);
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
blockWrite("bash write to memory-bank", { command: command.slice(0, 200), pathArg, resolved });
|
|
1665
|
-
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");
|
|
1666
1619
|
}
|
|
1667
1620
|
}
|
|
1668
1621
|
log.debug("Bash command allowed (path not under root memory-bank/)", { segment: segment.slice(0, 100) });
|
package/package.json
CHANGED
|
@@ -121,12 +121,16 @@ proxy_task({
|
|
|
121
121
|
|
|
122
122
|
### 写入阶段
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
**写入方式**:主 Agent 直接使用 `write`/`edit` 工具写入 `memory-bank/`。Plugin 会自动注入 writing guideline(advisory)。
|
|
125
125
|
|
|
126
126
|
流程(跨 turn):
|
|
127
127
|
1. 主 Agent 检测到写入时机,用自然语言询问是否写入(含目标文件 + 要点)
|
|
128
128
|
2. 用户自然语言确认("好"/"写"/"确认")或跳过("不用"/"跳过"/继续下一话题)
|
|
129
|
-
3.
|
|
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
|
|
1
|
+
# Memory Bank 写入规则
|
|
2
2
|
|
|
3
|
-
> 此文档定义 Memory Bank
|
|
3
|
+
> 此文档定义 Memory Bank 的写入规则。主 Agent 直接执行写入,Plugin 注入 writing guideline(advisory)。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 写入方式
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
主 Agent 直接使用 `write`/`edit` 工具写入 `memory-bank/` 下的 `.md` 文件:
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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:
|
|
73
|
+
**Step 3: 执行(本 turn 或下一 turn)**
|
|
62
74
|
|
|
63
|
-
|
|
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
|
-
##
|
|
312
|
+
## 写入流程
|
|
301
313
|
|
|
302
|
-
**Proposal 流程**:主 Agent
|
|
314
|
+
**Proposal 流程**:主 Agent 提议 → 用户确认 → 主 Agent 直接执行写入。
|
|
303
315
|
|
|
304
316
|
| 步骤 | 负责方 | 动作 |
|
|
305
317
|
|------|--------|------|
|
|
306
318
|
| 1 | 主 Agent | 检测写入时机,自然语言询问是否写入 |
|
|
307
319
|
| 2 | 用户 | 自然语言确认("好"/"写")或拒绝("不用"/"跳过") |
|
|
308
|
-
| 3 | 主 Agent |
|
|
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
|
|
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
|
-
-
|
|
343
|
-
- 只允许写入 `.md`
|
|
344
|
-
-
|
|
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
|
|