memory-bank-skill 7.3.2 → 7.4.1
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 -37
- package/dist/memory-bank.js +43 -378
- package/dist/plugin.js +58 -103
- 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.1
|
|
184
|
+
- **主要更新**: Writing guideline 改为 post-write advisory — 写入后建议参考规范检查,per-turn 去重
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
-
var __defProp = Object.defineProperty;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
-
for (let key of __getOwnPropNames(mod))
|
|
12
|
-
if (!__hasOwnProp.call(to, key))
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: () => mod[key],
|
|
15
|
-
enumerable: true
|
|
16
|
-
});
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
3
|
var __require = import.meta.require;
|
|
20
4
|
|
|
21
5
|
// src/cli.ts
|
|
@@ -27,7 +11,7 @@ import { fileURLToPath } from "url";
|
|
|
27
11
|
// package.json
|
|
28
12
|
var package_default = {
|
|
29
13
|
name: "memory-bank-skill",
|
|
30
|
-
version: "7.
|
|
14
|
+
version: "7.4.1",
|
|
31
15
|
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
16
|
type: "module",
|
|
33
17
|
main: "dist/plugin.js",
|
|
@@ -291,14 +275,8 @@ async function installPluginToConfig(undoStack, customModel) {
|
|
|
291
275
|
if (!config.agent) {
|
|
292
276
|
config.agent = {};
|
|
293
277
|
}
|
|
294
|
-
const defaultModel = "cliproxy/claude-opus-4-
|
|
278
|
+
const defaultModel = "cliproxy/claude-opus-4-6";
|
|
295
279
|
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
280
|
const readerDefaults = {
|
|
303
281
|
description: "Memory Bank \u5E76\u884C\u8BFB\u53D6\u4EE3\u7406\uFF0C\u8FD4\u56DE\u7ED3\u6784\u5316\u4E0A\u4E0B\u6587\u5305",
|
|
304
282
|
model: customModel || defaultModel,
|
|
@@ -337,7 +315,9 @@ async function installPluginToConfig(undoStack, customModel) {
|
|
|
337
315
|
}
|
|
338
316
|
return agentModified;
|
|
339
317
|
};
|
|
340
|
-
if (
|
|
318
|
+
if (config.agent["memory-bank-writer"]) {
|
|
319
|
+
delete config.agent["memory-bank-writer"];
|
|
320
|
+
changes.push("Removed agent: memory-bank-writer (no longer needed)");
|
|
341
321
|
modified = true;
|
|
342
322
|
}
|
|
343
323
|
if (mergeAgent(config.agent["memory-reader"], readerDefaults, "memory-reader")) {
|
|
@@ -359,7 +339,6 @@ async function installCommands(undoStack, manifestFiles) {
|
|
|
359
339
|
const commandPath = join(commandsDir, "memory-bank-refresh.md");
|
|
360
340
|
const commandContent = `---
|
|
361
341
|
description: \u521D\u59CB\u5316\u3001\u5347\u7EA7\u3001\u8FC1\u79FB\u6216\u5237\u65B0 Memory Bank
|
|
362
|
-
agent: memory-bank-writer
|
|
363
342
|
---
|
|
364
343
|
|
|
365
344
|
\u6267\u884C Memory Bank \u7684 refresh \u6D41\u7A0B\uFF1A
|
|
@@ -441,7 +420,7 @@ Replace <<<USER_REQUEST>>> with the user's original message verbatim.
|
|
|
441
420
|
|
|
442
421
|
---
|
|
443
422
|
|
|
444
|
-
AMENDMENT B \u2014 Final Step: Memory Bank
|
|
423
|
+
AMENDMENT B \u2014 Final Step: Memory Bank Write (Propose -> Confirm -> Execute)
|
|
445
424
|
|
|
446
425
|
Step W0 Timing:
|
|
447
426
|
- AFTER you finish main task output, RIGHT BEFORE final answer for this turn.
|
|
@@ -481,14 +460,7 @@ Ignore: User continues to next topic without addressing the prompt (treat as ski
|
|
|
481
460
|
|
|
482
461
|
**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
462
|
|
|
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
|
-
\`\`\`
|
|
463
|
+
On confirmation, directly use write/edit tools to update the target file(s). Plugin will auto-inject writing guidelines.
|
|
492
464
|
|
|
493
465
|
Step W5 After execution:
|
|
494
466
|
- Show which file(s) updated and brief preview.
|
|
@@ -808,8 +780,8 @@ Commands:
|
|
|
808
780
|
doctor Check installation status
|
|
809
781
|
|
|
810
782
|
Options:
|
|
811
|
-
--model <model> Specify model for memory-
|
|
812
|
-
Default: cliproxy/claude-opus-4-
|
|
783
|
+
--model <model> Specify model for memory-reader agent
|
|
784
|
+
Default: cliproxy/claude-opus-4-6
|
|
813
785
|
|
|
814
786
|
Examples:
|
|
815
787
|
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
|
@@ -1,20 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
-
for (let key of __getOwnPropNames(mod))
|
|
11
|
-
if (!__hasOwnProp.call(to, key))
|
|
12
|
-
__defProp(to, key, {
|
|
13
|
-
get: () => mod[key],
|
|
14
|
-
enumerable: true
|
|
15
|
-
});
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
2
|
var __require = import.meta.require;
|
|
19
3
|
|
|
20
4
|
// plugin/memory-bank.ts
|
|
@@ -45,10 +29,7 @@ var rootStates = new Map;
|
|
|
45
29
|
var sessionMetas = new Map;
|
|
46
30
|
var memoryBankExistsCache = new Map;
|
|
47
31
|
var fileCache = new Map;
|
|
48
|
-
var WRITER_AGENT_NAME = "memory-bank-writer";
|
|
49
32
|
var sessionsById = new Map;
|
|
50
|
-
var writerSessionIDs = new Set;
|
|
51
|
-
var agentBySessionID = new Map;
|
|
52
33
|
var messageGatingStates = new Map;
|
|
53
34
|
var sessionAnchorStates = new Map;
|
|
54
35
|
function makeStateKey(sessionId, root) {
|
|
@@ -213,7 +194,7 @@ drill_down: Step1 direct-read 1-3 details/*; Step2 \u9700\u8981\u8BC1\u636E/\u51
|
|
|
213
194
|
output: \u56DE\u7B54\u5FC5\u987B\u7ED9\u5F15\u7528\u6307\u9488
|
|
214
195
|
gating: \u9AD8\u98CE\u9669\u5199\u524D\u9700\u5DF2\u8BFB patterns.md \u6216\u8C03\u7528\u8FC7 memory-reader
|
|
215
196
|
|
|
216
|
-
write:
|
|
197
|
+
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
198
|
more: \u5B8C\u6574\u89C4\u8303\u89C1 /memory-bank skill
|
|
218
199
|
`;
|
|
219
200
|
} else {
|
|
@@ -228,7 +209,7 @@ drill_down: Step1 direct-read 1-3 details/*; Step2 \u9700\u8981\u8BC1\u636E/\u51
|
|
|
228
209
|
output: \u56DE\u7B54\u5FC5\u987B\u7ED9\u5F15\u7528\u6307\u9488
|
|
229
210
|
gating: \u9AD8\u98CE\u9669\u5199\u524D\u9700\u5DF2\u8BFB patterns.md \u6216\u8C03\u7528\u8FC7 memory-reader
|
|
230
211
|
|
|
231
|
-
write:
|
|
212
|
+
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
213
|
more: \u5B8C\u6574\u89C4\u8303\u89C1 /memory-bank skill
|
|
233
214
|
`;
|
|
234
215
|
}
|
|
@@ -377,7 +358,7 @@ function getMessageGatingState(gatingKey, sessionId, projectRoot) {
|
|
|
377
358
|
meta.pendingDocFirstSatisfied = false;
|
|
378
359
|
}
|
|
379
360
|
}
|
|
380
|
-
state = { readFiles: new Set, contextSatisfied: false, warnedThisMessage: false, docFirstSatisfied: inheritDocFirst, docFirstWarned: false };
|
|
361
|
+
state = { readFiles: new Set, contextSatisfied: false, warnedThisMessage: false, docFirstSatisfied: inheritDocFirst, docFirstWarned: false, writingGuideInjected: false };
|
|
381
362
|
messageGatingStates.set(gatingKey, state);
|
|
382
363
|
if (messageGatingStates.size > 100) {
|
|
383
364
|
const first = messageGatingStates.keys().next().value;
|
|
@@ -963,8 +944,6 @@ ${triggers.join(`
|
|
|
963
944
|
}
|
|
964
945
|
sessionMetas.delete(sessionId);
|
|
965
946
|
sessionsById.delete(sessionId);
|
|
966
|
-
writerSessionIDs.delete(sessionId);
|
|
967
|
-
agentBySessionID.delete(sessionId);
|
|
968
947
|
sessionAnchorStates.delete(sessionId);
|
|
969
948
|
log.info("Session deleted", { sessionId });
|
|
970
949
|
}
|
|
@@ -1029,15 +1008,6 @@ ${triggers.join(`
|
|
|
1029
1008
|
meta2.planOutputted = true;
|
|
1030
1009
|
log.info("Plan outputted detected", { sessionId });
|
|
1031
1010
|
}
|
|
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
1011
|
}
|
|
1042
1012
|
}
|
|
1043
1013
|
} catch (err) {
|
|
@@ -1351,7 +1321,7 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1351
1321
|
` + ` - \u91CD\u6784 / \u4F18\u5316 \u2192 design/design-xxx.md
|
|
1352
1322
|
` + ` - \u7B80\u5355\u53D8\u66F4 \u2192 \u8FFD\u52A0\u5230 progress.md
|
|
1353
1323
|
|
|
1354
|
-
` + `\
|
|
1324
|
+
` + `\u4F7F\u7528 write/edit \u5DE5\u5177\u76F4\u63A5\u5199\u5165 memory-bank/ \u4E0B\u5BF9\u5E94\u6587\u4EF6
|
|
1355
1325
|
` + `\u786E\u8BA4\u6587\u6863\u65E0\u8BEF\u540E\u518D\u6267\u884C\u4EE3\u7801\u4FEE\u6539\u3002`);
|
|
1356
1326
|
} else {
|
|
1357
1327
|
dfState.docFirstWarned = true;
|
|
@@ -1381,7 +1351,7 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1381
1351
|
` + ` - \u91CD\u6784 / \u4F18\u5316 \u2192 design/design-xxx.md
|
|
1382
1352
|
` + ` - \u7B80\u5355\u53D8\u66F4 \u2192 \u8FFD\u52A0\u5230 progress.md
|
|
1383
1353
|
|
|
1384
|
-
` + `\
|
|
1354
|
+
` + `\u4F7F\u7528 write/edit \u5DE5\u5177\u76F4\u63A5\u5199\u5165 memory-bank/ \u4E0B\u5BF9\u5E94\u6587\u4EF6`
|
|
1385
1355
|
}]
|
|
1386
1356
|
}
|
|
1387
1357
|
}).catch((err) => log.error("Failed to send doc-first warning:", String(err)));
|
|
@@ -1389,43 +1359,44 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1389
1359
|
}
|
|
1390
1360
|
}
|
|
1391
1361
|
}
|
|
1392
|
-
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1362
|
+
async function injectWritingGuideline(sid, writtenPaths) {
|
|
1363
|
+
const meta = getSessionMeta(sid, projectRoot);
|
|
1364
|
+
const messageKey = meta.lastUserMessageKey || "default";
|
|
1365
|
+
const gatingKey = `${sid}::${messageKey}`;
|
|
1366
|
+
const gatingState = getMessageGatingState(gatingKey, sid, projectRoot);
|
|
1367
|
+
if (gatingState.writingGuideInjected)
|
|
1395
1368
|
return;
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
` + `\u793A\u4F8B: proxy_task({ subagent_type: "memory-bank-writer", description: "Memory Bank write", prompt: "Target: memory-bank/details/patterns.md\\nDraft:\\n1) ..." })`);
|
|
1428
|
-
};
|
|
1369
|
+
gatingState.writingGuideInjected = true;
|
|
1370
|
+
const pathList = writtenPaths.map((p) => `- ${p}`).join(`
|
|
1371
|
+
`);
|
|
1372
|
+
client.session.prompt({
|
|
1373
|
+
path: { id: sid },
|
|
1374
|
+
body: {
|
|
1375
|
+
noReply: true,
|
|
1376
|
+
variant: PLUGIN_PROMPT_VARIANT,
|
|
1377
|
+
parts: [{
|
|
1378
|
+
type: "text",
|
|
1379
|
+
text: `## [Memory Bank Writing Guide]
|
|
1380
|
+
|
|
1381
|
+
` + `\u5DF2\u68C0\u6D4B\u5230 memory-bank/ \u5199\u5165\uFF1A
|
|
1382
|
+
${pathList}
|
|
1383
|
+
|
|
1384
|
+
` + `\u5EFA\u8BAE\u53C2\u8003\u5199\u5165\u89C4\u8303\u786E\u8BA4\u5185\u5BB9\u662F\u5426\u9700\u8981\u8C03\u6574\uFF1A
|
|
1385
|
+
` + `read({ filePath: "~/.config/opencode/skills/memory-bank/references/writer.md" })
|
|
1386
|
+
|
|
1387
|
+
` + `\u5982\u9700\u4FEE\u6B63\u8BF7\u76F4\u63A5\u4FEE\u6539\uFF0C\u7136\u540E\u7EE7\u7EED\u539F\u672C\u7684\u4EFB\u52A1\u3002`
|
|
1388
|
+
}]
|
|
1389
|
+
}
|
|
1390
|
+
}).catch((err) => log.error("Failed to send writing guideline:", String(err)));
|
|
1391
|
+
}
|
|
1392
|
+
function markDocFirstSatisfied(sid) {
|
|
1393
|
+
const meta = getSessionMeta(sid, projectRoot);
|
|
1394
|
+
const messageKey = meta.lastUserMessageKey || "default";
|
|
1395
|
+
const gatingKey = `${sid}::${messageKey}`;
|
|
1396
|
+
const gatingState = getMessageGatingState(gatingKey, sid, projectRoot);
|
|
1397
|
+
gatingState.docFirstSatisfied = true;
|
|
1398
|
+
log.debug("Doc-First: satisfied via direct memory-bank write", { sessionID: sid, gatingKey });
|
|
1399
|
+
}
|
|
1429
1400
|
const extractPaths = (toolName, args) => {
|
|
1430
1401
|
const paths = [];
|
|
1431
1402
|
const pathArgs = ["filePath", "path", "filename", "file", "dest", "destination", "target"];
|
|
@@ -1466,22 +1437,21 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1466
1437
|
const targetPaths = extractPaths(toolLower, output.args || {});
|
|
1467
1438
|
if (targetPaths.length === 0)
|
|
1468
1439
|
return;
|
|
1440
|
+
const mbWrittenPaths = [];
|
|
1469
1441
|
for (const targetPath of targetPaths) {
|
|
1470
1442
|
if (!await isMemoryBankPath(targetPath))
|
|
1471
1443
|
continue;
|
|
1472
1444
|
if (!targetPath.toLowerCase().endsWith(".md")) {
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
log.debug("Writer agent write allowed", { sessionID, tool, targetPath });
|
|
1477
|
-
markParentDocFirstSatisfied(sessionID);
|
|
1478
|
-
return;
|
|
1445
|
+
log.warn("Memory Bank write blocked (non-.md file)", { sessionID, tool, targetPath });
|
|
1446
|
+
throw new Error(`[Memory Bank Guard] memory-bank/ \u4E0B\u53EA\u5141\u8BB8\u5199\u5165 .md \u6587\u4EF6\u3002
|
|
1447
|
+
` + `\u76EE\u6807\u6587\u4EF6: ${targetPath}`);
|
|
1479
1448
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1449
|
+
mbWrittenPaths.push(targetPath);
|
|
1450
|
+
markDocFirstSatisfied(sessionID);
|
|
1451
|
+
log.debug("Memory Bank write allowed", { sessionID, tool, targetPath });
|
|
1452
|
+
}
|
|
1453
|
+
if (mbWrittenPaths.length > 0) {
|
|
1454
|
+
await injectWritingGuideline(sessionID, mbWrittenPaths);
|
|
1485
1455
|
}
|
|
1486
1456
|
}
|
|
1487
1457
|
if (tool.toLowerCase() === "bash") {
|
|
@@ -1623,13 +1593,8 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1623
1593
|
if (redirectTarget && redirectTarget.includes("memory-bank")) {
|
|
1624
1594
|
const resolvedTarget = path.resolve(projectRoot, redirectTarget);
|
|
1625
1595
|
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;
|
|
1596
|
+
log.warn("Memory Bank bash redirect blocked", { sessionID, command: command.slice(0, 200) });
|
|
1597
|
+
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
1598
|
}
|
|
1634
1599
|
}
|
|
1635
1600
|
if (readOnlyPatterns.some((p) => p.test(segment))) {
|
|
@@ -1639,13 +1604,8 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1639
1604
|
for (const pathArg of pathArgs2) {
|
|
1640
1605
|
const resolved = path.resolve(projectRoot, pathArg);
|
|
1641
1606
|
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;
|
|
1607
|
+
log.warn("Memory Bank bash find-write blocked", { sessionID, command: command.slice(0, 200) });
|
|
1608
|
+
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
1609
|
}
|
|
1650
1610
|
}
|
|
1651
1611
|
}
|
|
@@ -1656,13 +1616,8 @@ Or call: proxy_task({ subagent_type: "memory-reader", ... })`);
|
|
|
1656
1616
|
for (const pathArg of pathArgs) {
|
|
1657
1617
|
const resolved = path.resolve(projectRoot, pathArg);
|
|
1658
1618
|
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;
|
|
1619
|
+
log.warn("Memory Bank bash write blocked", { sessionID, command: command.slice(0, 200), pathArg });
|
|
1620
|
+
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
1621
|
}
|
|
1667
1622
|
}
|
|
1668
1623
|
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
|
|