memory-bank-skill 5.6.0 → 5.6.2

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/dist/cli.js CHANGED
@@ -7,7 +7,57 @@ import { homedir } from "os";
7
7
  import { join, dirname } from "path";
8
8
  import { createHash } from "crypto";
9
9
  import { fileURLToPath } from "url";
10
- var VERSION = "5.6.0";
10
+ // package.json
11
+ var package_default = {
12
+ name: "memory-bank-skill",
13
+ version: "5.6.2",
14
+ 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",
15
+ type: "module",
16
+ main: "dist/plugin.js",
17
+ bin: {
18
+ "memory-bank-skill": "dist/cli.js"
19
+ },
20
+ files: [
21
+ "dist/",
22
+ "skill/",
23
+ "templates/"
24
+ ],
25
+ scripts: {
26
+ build: "bun build src/cli.ts plugin/memory-bank.ts --outdir dist --target bun && mv dist/src/cli.js dist/cli.js && mv dist/plugin/memory-bank.js dist/plugin.js && rm -rf dist/plugin dist/src",
27
+ prepublishOnly: "bun run build",
28
+ "test:install": "bun run build && bun ./dist/cli.js install",
29
+ "test:doctor": "bun run build && bun ./dist/cli.js doctor"
30
+ },
31
+ keywords: [
32
+ "opencode",
33
+ "plugin",
34
+ "memory-bank",
35
+ "ai",
36
+ "context",
37
+ "claude"
38
+ ],
39
+ author: "",
40
+ license: "MIT",
41
+ repository: {
42
+ type: "git",
43
+ url: "git+https://github.com/user/memory-bank-skill.git"
44
+ },
45
+ engines: {
46
+ node: ">=18.0.0"
47
+ },
48
+ peerDependencies: {
49
+ "@opencode-ai/plugin": "^1.1.14"
50
+ },
51
+ publishConfig: {
52
+ registry: "https://registry.npmjs.org/"
53
+ },
54
+ devDependencies: {
55
+ "memory-bank-skill": "5.5.1"
56
+ }
57
+ };
58
+
59
+ // src/cli.ts
60
+ var VERSION = package_default.version;
11
61
  var colors = {
12
62
  reset: "\x1B[0m",
13
63
  bold: "\x1B[1m",
@@ -0,0 +1,845 @@
1
+ // plugin/memory-bank.ts
2
+ import { stat, readFile } from "node:fs/promises";
3
+ import { execSync } from "node:child_process";
4
+ import path from "node:path";
5
+ var DEBUG = process.env.MEMORY_BANK_DEBUG === "1";
6
+ var DEFAULT_MAX_CHARS = 12000;
7
+ var TRUNCATION_NOTICE = `
8
+
9
+ ---
10
+
11
+ [TRUNCATED] Memory Bank context exceeded size limit. Read files directly for complete content.`;
12
+ var MEMORY_BANK_FILES = [
13
+ "memory-bank/brief.md",
14
+ "memory-bank/active.md",
15
+ "memory-bank/_index.md"
16
+ ];
17
+ var SENTINEL_OPEN = "<memory-bank-bootstrap>";
18
+ var SENTINEL_CLOSE = "</memory-bank-bootstrap>";
19
+ var SERVICE_NAME = "memory-bank";
20
+ var PLUGIN_PROMPT_VARIANT = "memory-bank-plugin";
21
+ var rootStates = new Map;
22
+ var sessionMetas = new Map;
23
+ var memoryBankExistsCache = new Map;
24
+ 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
+ function makeStateKey(sessionId, root) {
30
+ return `${sessionId}::${root}`;
31
+ }
32
+ function maxChars() {
33
+ const raw = process.env.MEMORY_BANK_MAX_CHARS;
34
+ const n = raw ? Number(raw) : NaN;
35
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_MAX_CHARS;
36
+ }
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
+ function createLogger(client) {
76
+ let pending = Promise.resolve();
77
+ const formatArgs = (args) => {
78
+ return args.map((a) => {
79
+ if (typeof a === "string")
80
+ return a;
81
+ try {
82
+ const str = JSON.stringify(a);
83
+ return str.length > 2000 ? str.slice(0, 2000) + "..." : str;
84
+ } catch {
85
+ return String(a);
86
+ }
87
+ }).join(" ");
88
+ };
89
+ const enqueue = (level, message) => {
90
+ pending = pending.then(() => client.app.log({
91
+ body: { service: SERVICE_NAME, level, message }
92
+ })).catch(() => {});
93
+ };
94
+ return {
95
+ debug: (...args) => {
96
+ if (DEBUG)
97
+ enqueue("debug", formatArgs(args));
98
+ },
99
+ info: (...args) => enqueue("info", formatArgs(args)),
100
+ warn: (...args) => enqueue("warn", formatArgs(args)),
101
+ error: (...args) => enqueue("error", formatArgs(args)),
102
+ flush: () => pending
103
+ };
104
+ }
105
+ async function readTextCached(absPath) {
106
+ try {
107
+ const st = await stat(absPath);
108
+ const cached = fileCache.get(absPath);
109
+ if (cached && cached.mtimeMs === st.mtimeMs)
110
+ return cached.text;
111
+ const text = await readFile(absPath, "utf8");
112
+ fileCache.set(absPath, { mtimeMs: st.mtimeMs, text });
113
+ return text;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+ function truncateToBudget(text, budget) {
119
+ if (text.length <= budget)
120
+ return text;
121
+ const reserve = TRUNCATION_NOTICE.length;
122
+ if (budget <= reserve)
123
+ return TRUNCATION_NOTICE.slice(0, budget);
124
+ return text.slice(0, budget - reserve) + TRUNCATION_NOTICE;
125
+ }
126
+ async function buildMemoryBankContextWithMeta(projectRoot) {
127
+ const fnStart = Date.now();
128
+ if (DEBUG)
129
+ console.error(`[MB-DEBUG] buildMemoryBankContextWithMeta START projectRoot=${projectRoot}`);
130
+ const parts = [];
131
+ const files = [];
132
+ for (const rel of MEMORY_BANK_FILES) {
133
+ const fileStart = Date.now();
134
+ const abs = path.join(projectRoot, rel);
135
+ if (DEBUG)
136
+ console.error(`[MB-DEBUG] reading ${rel}...`);
137
+ const content = await readTextCached(abs);
138
+ if (DEBUG)
139
+ console.error(`[MB-DEBUG] read ${rel} done, hasContent=${!!content}, elapsed=${Date.now() - fileStart}ms`);
140
+ if (!content)
141
+ continue;
142
+ const trimmed = content.trim();
143
+ if (!trimmed)
144
+ continue;
145
+ parts.push(`## ${rel}
146
+
147
+ ${trimmed}`);
148
+ files.push({ relPath: rel, chars: trimmed.length });
149
+ }
150
+ if (DEBUG)
151
+ console.error(`[MB-DEBUG] all files read, parts=${parts.length}, totalElapsed=${Date.now() - fnStart}ms`);
152
+ if (parts.length === 0)
153
+ 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
+ const header = `# Memory Bank Bootstrap (Auto-injected by OpenCode plugin)
157
+
158
+ ` + `Use \`memory-bank/_index.md\` to locate additional context files.
159
+ ` + `Read more files from \`memory-bank/\` as needed based on the task.
160
+
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
+ ` + `---
184
+
185
+ `;
186
+ const wrapped = `${SENTINEL_OPEN}
187
+ ` + header + parts.join(`
188
+
189
+ ---
190
+
191
+ `) + `
192
+ ${SENTINEL_CLOSE}`;
193
+ const budget = maxChars();
194
+ const truncated = wrapped.length > budget;
195
+ const text = truncateToBudget(wrapped, budget);
196
+ return { text, files, totalChars, truncated };
197
+ }
198
+ async function buildMemoryBankContext(projectRoot) {
199
+ const result = await buildMemoryBankContextWithMeta(projectRoot);
200
+ return result?.text ?? null;
201
+ }
202
+ async function checkMemoryBankExists(root, log) {
203
+ if (memoryBankExistsCache.has(root)) {
204
+ const cached = memoryBankExistsCache.get(root);
205
+ if (cached) {
206
+ log.debug("memoryBankExists cache hit (true):", root);
207
+ return true;
208
+ }
209
+ }
210
+ try {
211
+ const mbPath = path.join(root, "memory-bank");
212
+ await stat(mbPath);
213
+ memoryBankExistsCache.set(root, true);
214
+ log.debug("memoryBankExists check: true for", root);
215
+ return true;
216
+ } catch {
217
+ memoryBankExistsCache.set(root, false);
218
+ log.debug("memoryBankExists check: false for", root);
219
+ return false;
220
+ }
221
+ }
222
+ function getSessionMeta(sessionId, fallbackRoot) {
223
+ let meta = sessionMetas.get(sessionId);
224
+ if (!meta) {
225
+ meta = { rootsTouched: new Set, lastActiveRoot: fallbackRoot, notifiedMessageIds: new Set, planOutputted: false, promptInProgress: false, userMessageReceived: false, sessionNotified: false, userMessageSeq: 0 };
226
+ sessionMetas.set(sessionId, meta);
227
+ }
228
+ return meta;
229
+ }
230
+ function getRootState(sessionId, root) {
231
+ const key = makeStateKey(sessionId, root);
232
+ let state = rootStates.get(key);
233
+ if (!state) {
234
+ state = {
235
+ filesModified: [],
236
+ hasNewRequirement: false,
237
+ hasTechDecision: false,
238
+ hasBugFix: false,
239
+ memoryBankReviewed: false,
240
+ skipInit: false,
241
+ initReminderFired: false,
242
+ lastUpdateReminderSignature: undefined,
243
+ lastSyncedTriggerSignature: undefined
244
+ };
245
+ rootStates.set(key, state);
246
+ }
247
+ return state;
248
+ }
249
+ var TRACKABLE_FILE_PATTERNS = [
250
+ /\.py$/,
251
+ /\.ts$/,
252
+ /\.tsx$/,
253
+ /\.js$/,
254
+ /\.jsx$/,
255
+ /\.go$/,
256
+ /\.rs$/,
257
+ /\.md$/,
258
+ /\.json$/,
259
+ /\.yaml$/,
260
+ /\.yml$/,
261
+ /\.toml$/,
262
+ /\.css$/,
263
+ /\.scss$/,
264
+ /\.html$/,
265
+ /\.vue$/,
266
+ /\.svelte$/
267
+ ];
268
+ var EXCLUDED_DIRS = [
269
+ /^node_modules\//,
270
+ /^\.venv\//,
271
+ /^venv\//,
272
+ /^dist\//,
273
+ /^build\//,
274
+ /^\.next\//,
275
+ /^\.nuxt\//,
276
+ /^coverage\//,
277
+ /^\.pytest_cache\//,
278
+ /^__pycache__\//,
279
+ /^\.git\//,
280
+ /^\.opencode\//,
281
+ /^\.claude\//
282
+ ];
283
+ var MEMORY_BANK_PATTERN = /^memory-bank\//;
284
+ function isDisabled() {
285
+ return process.env.MEMORY_BANK_DISABLED === "1" || process.env.MEMORY_BANK_DISABLED === "true";
286
+ }
287
+ function computeTriggerSignature(state) {
288
+ return JSON.stringify({
289
+ files: [...state.filesModified].sort(),
290
+ flags: {
291
+ hasNewRequirement: state.hasNewRequirement,
292
+ hasTechDecision: state.hasTechDecision,
293
+ hasBugFix: state.hasBugFix
294
+ }
295
+ });
296
+ }
297
+ function detectGitChanges(root, log) {
298
+ try {
299
+ const stdout = execSync("git status --porcelain", {
300
+ cwd: root,
301
+ timeout: 5000,
302
+ encoding: "utf8"
303
+ });
304
+ if (!stdout.trim()) {
305
+ log.debug("No git changes detected in", root);
306
+ return { modifiedFiles: [], memoryBankUpdated: false };
307
+ }
308
+ const lines = stdout.replace(/[\r\n]+$/, "").split(/\r?\n/);
309
+ const modifiedFiles = [];
310
+ let memoryBankUpdated = false;
311
+ for (const line of lines) {
312
+ if (line.length < 4)
313
+ continue;
314
+ const x = line[0];
315
+ const y = line[1];
316
+ let payload = line.slice(3);
317
+ if (!payload)
318
+ continue;
319
+ if (payload.startsWith('"') && payload.endsWith('"')) {
320
+ payload = payload.slice(1, -1);
321
+ }
322
+ if ((x === "R" || x === "C") && payload.includes(" -> ")) {
323
+ payload = payload.split(" -> ")[1];
324
+ }
325
+ const relativePath = payload.replace(/\\/g, "/");
326
+ if (MEMORY_BANK_PATTERN.test(relativePath)) {
327
+ memoryBankUpdated = true;
328
+ log.debug("Git detected memory-bank update:", relativePath);
329
+ continue;
330
+ }
331
+ if (TRACKABLE_FILE_PATTERNS.some((p) => p.test(relativePath))) {
332
+ if (!EXCLUDED_DIRS.some((p) => p.test(relativePath))) {
333
+ const absPath = path.join(root, relativePath);
334
+ modifiedFiles.push(absPath);
335
+ log.debug("Git detected modified file:", relativePath);
336
+ }
337
+ }
338
+ }
339
+ log.info("Git changes detected", {
340
+ root,
341
+ modifiedCount: modifiedFiles.length,
342
+ memoryBankUpdated
343
+ });
344
+ return { modifiedFiles, memoryBankUpdated };
345
+ } catch (err) {
346
+ log.debug("Git detection failed (not a git repo?):", String(err));
347
+ return null;
348
+ }
349
+ }
350
+ var plugin = async ({ client, directory, worktree }) => {
351
+ const projectRoot = worktree || directory;
352
+ const log = createLogger(client);
353
+ 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
+ async function evaluateAndFireReminder(sessionId) {
409
+ if (isDisabled()) {
410
+ log.info("[SESSION_IDLE DECISION]", { sessionId, decision: "SKIP", reason: "MEMORY_BANK_DISABLED is set" });
411
+ return;
412
+ }
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
+ const gitChanges = detectGitChanges(projectRoot, log);
419
+ const isGitRepo = gitChanges !== null;
420
+ const state = getRootState(sessionId, projectRoot);
421
+ if (gitChanges) {
422
+ const { modifiedFiles, memoryBankUpdated: gitMemoryBankUpdated } = gitChanges;
423
+ state.filesModified = modifiedFiles;
424
+ if (gitMemoryBankUpdated) {
425
+ state.lastSyncedTriggerSignature = computeTriggerSignature(state);
426
+ }
427
+ }
428
+ memoryBankExistsCache.delete(projectRoot);
429
+ const hasMemoryBank = await checkMemoryBankExists(projectRoot, log);
430
+ const triggerSignature = computeTriggerSignature(state);
431
+ const decisionContext = {
432
+ sessionId,
433
+ root: projectRoot,
434
+ projectName: path.basename(projectRoot),
435
+ isGitRepo,
436
+ filesModified: state.filesModified.length,
437
+ hasMemoryBank,
438
+ initReminderFired: state.initReminderFired,
439
+ lastUpdateReminderSignature: state.lastUpdateReminderSignature,
440
+ lastSyncedTriggerSignature: state.lastSyncedTriggerSignature,
441
+ triggerSignature,
442
+ memoryBankReviewed: state.memoryBankReviewed,
443
+ skipInit: state.skipInit,
444
+ hasNewRequirement: state.hasNewRequirement,
445
+ hasTechDecision: state.hasTechDecision,
446
+ hasBugFix: state.hasBugFix
447
+ };
448
+ if (state.memoryBankReviewed) {
449
+ log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "SKIP", reason: "memoryBankReviewed escape valve active" });
450
+ return;
451
+ }
452
+ if (!hasMemoryBank) {
453
+ if (state.skipInit) {
454
+ log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "SKIP", reason: "skipInit escape valve active" });
455
+ return;
456
+ }
457
+ if (state.initReminderFired) {
458
+ log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "SKIP", reason: "initReminderFired already true" });
459
+ return;
460
+ }
461
+ state.initReminderFired = true;
462
+ log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "FIRE_INIT", reason: "no memory-bank directory" });
463
+ const hasGit = await (async () => {
464
+ try {
465
+ await stat(path.join(projectRoot, ".git"));
466
+ return true;
467
+ } catch {
468
+ return false;
469
+ }
470
+ })();
471
+ const gitInitStep = hasGit ? "" : "1. 执行 `git init`(项目尚未初始化 Git)\n";
472
+ const stepOffset = hasGit ? 0 : 1;
473
+ try {
474
+ meta.promptInProgress = true;
475
+ await client.session.prompt({
476
+ path: { id: sessionId },
477
+ body: {
478
+ noReply: true,
479
+ variant: PLUGIN_PROMPT_VARIANT,
480
+ parts: [{
481
+ type: "text",
482
+ text: `## [SYSTEM REMINDER - Memory Bank Init]
483
+
484
+ 项目 \`${path.basename(projectRoot)}\` 尚未初始化 Memory Bank。
485
+
486
+ **项目路径**:\`${projectRoot}\`
487
+
488
+ **将要执行的操作**:
489
+ ${gitInitStep}${stepOffset + 1}. 创建 \`memory-bank/\` 目录
490
+ ${stepOffset + 2}. 扫描项目结构(README.md、package.json 等)
491
+ ${stepOffset + 3}. 生成 \`memory-bank/brief.md\`(项目概述)
492
+ ${stepOffset + 4}. 生成 \`memory-bank/tech.md\`(技术栈)
493
+ ${stepOffset + 5}. 生成 \`memory-bank/_index.md\`(索引)
494
+
495
+ **操作选项**:
496
+ 1. 如需初始化 → 回复"初始化"
497
+ 2. 如需初始化并提交 → 回复"初始化并提交"
498
+ 3. 如不需要 → 回复"跳过初始化"
499
+
500
+ 注意:这是系统自动提醒,不是用户消息。`
501
+ }]
502
+ }
503
+ });
504
+ log.info("INIT reminder sent successfully", { sessionId, root: projectRoot });
505
+ } catch (promptErr) {
506
+ log.error("Failed to send INIT reminder:", String(promptErr));
507
+ } finally {
508
+ meta.promptInProgress = false;
509
+ }
510
+ return;
511
+ }
512
+ state.initReminderFired = false;
513
+ const triggers = [];
514
+ if (state.hasNewRequirement)
515
+ triggers.push("- 检测到新需求讨论");
516
+ if (state.hasTechDecision)
517
+ triggers.push("- 检测到技术决策");
518
+ if (state.hasBugFix)
519
+ triggers.push("- 检测到 Bug 修复/踩坑");
520
+ const modifiedFilesRelative = state.filesModified.map((abs) => path.relative(projectRoot, abs));
521
+ const displayFiles = modifiedFilesRelative.slice(0, 5);
522
+ const moreCount = modifiedFilesRelative.length - 5;
523
+ let filesSection = "";
524
+ if (modifiedFilesRelative.length > 0) {
525
+ triggers.push("- 代码文件变更");
526
+ filesSection = `
527
+ **变更文件**:
528
+ ${displayFiles.map((f) => `- ${f}`).join(`
529
+ `)}${moreCount > 0 ? `
530
+ (+${moreCount} more)` : ""}
531
+ `;
532
+ }
533
+ if (triggers.length === 0) {
534
+ log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "NO_TRIGGER", reason: "has memory-bank but no triggers" });
535
+ return;
536
+ }
537
+ if (meta.planOutputted) {
538
+ log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "SKIP", reason: "AI already outputted update plan" });
539
+ return;
540
+ }
541
+ if (triggerSignature === state.lastSyncedTriggerSignature) {
542
+ log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "SKIP", reason: "already synced (signature matches lastSyncedTriggerSignature)" });
543
+ return;
544
+ }
545
+ if (triggerSignature === state.lastUpdateReminderSignature) {
546
+ log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "SKIP", reason: "already reminded (signature matches lastUpdateReminderSignature)" });
547
+ return;
548
+ }
549
+ state.lastUpdateReminderSignature = triggerSignature;
550
+ log.info("[SESSION_IDLE DECISION]", { ...decisionContext, decision: "FIRE_UPDATE", reason: `${triggers.length} triggers detected`, triggers });
551
+ try {
552
+ meta.promptInProgress = true;
553
+ await client.session.prompt({
554
+ path: { id: sessionId },
555
+ body: {
556
+ noReply: true,
557
+ variant: PLUGIN_PROMPT_VARIANT,
558
+ parts: [{
559
+ type: "text",
560
+ text: `## [SYSTEM REMINDER - Memory Bank Update]
561
+
562
+ 本轮检测到以下变更:${filesSection}
563
+ **触发事件**:
564
+ ${triggers.join(`
565
+ `)}
566
+
567
+ **操作选项**:
568
+ 1. 如需更新 → 回复"更新",输出更新计划
569
+ 2. 如需更新并提交 → 回复"更新并提交"
570
+ 3. 如不需要 → 回复"跳过"`
571
+ }]
572
+ }
573
+ });
574
+ log.info("UPDATE reminder sent successfully", { sessionId, root: projectRoot });
575
+ } catch (promptErr) {
576
+ log.error("Failed to send UPDATE reminder:", String(promptErr));
577
+ } finally {
578
+ meta.promptInProgress = false;
579
+ }
580
+ }
581
+ return {
582
+ "experimental.chat.system.transform": async (_input, output) => {
583
+ const hookStart = Date.now();
584
+ log.info("[HOOK] system.transform START");
585
+ try {
586
+ if (output.system.some((s) => s.includes(SENTINEL_OPEN))) {
587
+ log.info("[HOOK] system.transform SKIP (sentinel exists)", { elapsed: Date.now() - hookStart });
588
+ return;
589
+ }
590
+ log.info("[HOOK] system.transform building context...");
591
+ const ctx = await buildMemoryBankContext(projectRoot);
592
+ log.info("[HOOK] system.transform context built", { hasCtx: !!ctx, elapsed: Date.now() - hookStart });
593
+ if (ctx) {
594
+ output.system.push(ctx);
595
+ log.info("[HOOK] system.transform DONE (ctx pushed)", { elapsed: Date.now() - hookStart });
596
+ return;
597
+ }
598
+ const initInstruction = `${SENTINEL_OPEN}
599
+ ` + `# Memory Bank 未初始化
600
+
601
+ ` + `项目 \`${path.basename(projectRoot)}\` 尚未初始化 Memory Bank。
602
+
603
+ ` + `**AI 行为指令**:
604
+ ` + `- **Todo 创建规则(必须)**:创建 todo 时,第一项必须是"初始化 Memory Bank"(扫描项目结构,创建 brief.md + tech.md + _index.md),最后一项必须是"更新 Memory Bank"(更新 active.md)
605
+ ` + `${SENTINEL_CLOSE}`;
606
+ output.system.push(initInstruction);
607
+ log.info("[HOOK] system.transform DONE (init pushed)", { elapsed: Date.now() - hookStart });
608
+ } catch (err) {
609
+ log.error("[HOOK] system.transform ERROR", String(err), { elapsed: Date.now() - hookStart });
610
+ }
611
+ },
612
+ "experimental.session.compacting": async (_input, output) => {
613
+ const hookStart = Date.now();
614
+ log.info("[HOOK] session.compacting START");
615
+ try {
616
+ if (output.context.some((s) => s.includes(SENTINEL_OPEN))) {
617
+ log.info("[HOOK] session.compacting SKIP (sentinel exists)", { elapsed: Date.now() - hookStart });
618
+ return;
619
+ }
620
+ log.info("[HOOK] session.compacting building context...");
621
+ const ctx = await buildMemoryBankContext(projectRoot);
622
+ log.info("[HOOK] session.compacting context built", { hasCtx: !!ctx, elapsed: Date.now() - hookStart });
623
+ if (ctx) {
624
+ output.context.push(ctx);
625
+ log.info("[HOOK] session.compacting DONE (ctx pushed)", { elapsed: Date.now() - hookStart });
626
+ return;
627
+ }
628
+ const initInstruction = `${SENTINEL_OPEN}
629
+ ` + `# Memory Bank 未初始化
630
+
631
+ ` + `项目 \`${path.basename(projectRoot)}\` 尚未初始化 Memory Bank。
632
+
633
+ ` + `**AI 行为指令**:
634
+ ` + `- **Todo 创建规则(必须)**:创建 todo 时,第一项必须是"初始化 Memory Bank"(扫描项目结构,创建 brief.md + tech.md + _index.md),最后一项必须是"更新 Memory Bank"(更新 active.md)
635
+ ` + `${SENTINEL_CLOSE}`;
636
+ output.context.push(initInstruction);
637
+ log.info("[HOOK] session.compacting DONE (init pushed)", { elapsed: Date.now() - hookStart });
638
+ } catch (err) {
639
+ log.error("[HOOK] session.compacting ERROR", String(err), { elapsed: Date.now() - hookStart });
640
+ }
641
+ },
642
+ event: async ({ event }) => {
643
+ try {
644
+ let sessionId;
645
+ const props = event.properties;
646
+ const info = props?.info;
647
+ if (event.type === "session.created" || event.type === "session.deleted") {
648
+ sessionId = info?.id;
649
+ } else if (event.type === "message.updated") {
650
+ sessionId = info?.sessionID;
651
+ } else {
652
+ sessionId = info?.sessionID || info?.id || props?.sessionID || props?.session_id;
653
+ }
654
+ if (!sessionId) {
655
+ log.debug("event handler: no sessionId in event", event.type, JSON.stringify(props || {}).slice(0, 200));
656
+ return;
657
+ }
658
+ if (event.type === "session.created") {
659
+ sessionMetas.set(sessionId, { rootsTouched: new Set, lastActiveRoot: projectRoot, notifiedMessageIds: new Set, planOutputted: false, promptInProgress: false, userMessageReceived: false, sessionNotified: false, userMessageSeq: 0 });
660
+ const parentID = info?.parentID;
661
+ sessionsById.set(sessionId, { parentID });
662
+ log.info("Session created", { sessionId, parentID });
663
+ }
664
+ if (event.type === "session.deleted") {
665
+ const meta = sessionMetas.get(sessionId);
666
+ if (meta) {
667
+ for (const root of meta.rootsTouched) {
668
+ rootStates.delete(makeStateKey(sessionId, root));
669
+ }
670
+ }
671
+ sessionMetas.delete(sessionId);
672
+ sessionsById.delete(sessionId);
673
+ writerSessionIDs.delete(sessionId);
674
+ agentBySessionID.delete(sessionId);
675
+ log.info("Session deleted", { sessionId });
676
+ }
677
+ if (event.type === "message.updated") {
678
+ 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
+ if (message?.role === "user") {
695
+ if (meta.promptInProgress) {
696
+ log.debug("message.updated skipped (prompt in progress)", { sessionId });
697
+ return;
698
+ }
699
+ const content = rawContent.toLowerCase();
700
+ const targetRoot = meta.lastActiveRoot || projectRoot;
701
+ const state = getRootState(sessionId, targetRoot);
702
+ if (/新需求|new req|feature request|需要实现|要做一个/.test(content)) {
703
+ state.hasNewRequirement = true;
704
+ log.debug("Keyword detected: newRequirement", { sessionId, root: targetRoot });
705
+ }
706
+ if (/决定用|选择了|我们用|技术选型|architecture|决策/.test(content)) {
707
+ state.hasTechDecision = true;
708
+ log.debug("Keyword detected: techDecision", { sessionId, root: targetRoot });
709
+ }
710
+ if (/bug|修复|fix|问题|error|踩坑|教训/.test(content)) {
711
+ state.hasBugFix = true;
712
+ log.debug("Keyword detected: bugFix", { sessionId, root: targetRoot });
713
+ }
714
+ if (/跳过初始化|skip.?init/.test(content)) {
715
+ state.skipInit = true;
716
+ log.info("Escape valve triggered: skipInit", { sessionId, root: targetRoot });
717
+ } else if (/memory.?bank.?reviewed|无需更新|不需要更新|已检查|^跳过$/.test(content)) {
718
+ state.memoryBankReviewed = true;
719
+ log.info("Escape valve triggered: memoryBankReviewed", { sessionId, root: targetRoot });
720
+ }
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
+ }
731
+ if (message?.role === "assistant") {
732
+ const content = JSON.stringify(message.content || "");
733
+ const meta2 = getSessionMeta(sessionId, projectRoot);
734
+ if (/Memory Bank 更新计划|\[Memory Bank 更新计划\]/.test(content)) {
735
+ meta2.planOutputted = true;
736
+ log.info("Plan outputted detected", { sessionId });
737
+ }
738
+ const agentName = message?.agent;
739
+ if (agentName) {
740
+ agentBySessionID.set(sessionId, agentName);
741
+ const sessionInfo = sessionsById.get(sessionId);
742
+ if (agentName === WRITER_AGENT_NAME && sessionInfo?.parentID) {
743
+ writerSessionIDs.add(sessionId);
744
+ log.info("Writer agent session registered", { sessionId, agentName, parentID: sessionInfo.parentID });
745
+ }
746
+ }
747
+ }
748
+ }
749
+ } catch (err) {
750
+ log.error("event handler error:", String(err));
751
+ }
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
+ }
840
+ };
841
+ };
842
+ var memory_bank_default = plugin;
843
+ export {
844
+ memory_bank_default as default
845
+ };
package/dist/plugin.js CHANGED
@@ -23,6 +23,10 @@ var rootStates = new Map;
23
23
  var sessionMetas = new Map;
24
24
  var memoryBankExistsCache = new Map;
25
25
  var fileCache = new Map;
26
+ var WRITER_AGENT_NAME = "memory-bank-writer";
27
+ var sessionsById = new Map;
28
+ var writerSessionIDs = new Set;
29
+ var agentBySessionID = new Map;
26
30
  function makeStateKey(sessionId, root) {
27
31
  return `${sessionId}::${root}`;
28
32
  }
@@ -654,7 +658,9 @@ ${triggers.join(`
654
658
  }
655
659
  if (event.type === "session.created") {
656
660
  sessionMetas.set(sessionId, { rootsTouched: new Set, lastActiveRoot: projectRoot, notifiedMessageIds: new Set, planOutputted: false, promptInProgress: false, userMessageReceived: false, sessionNotified: false, userMessageSeq: 0 });
657
- log.info("Session created", { sessionId });
661
+ const parentID = info?.parentID;
662
+ sessionsById.set(sessionId, { parentID });
663
+ log.info("Session created", { sessionId, parentID });
658
664
  }
659
665
  if (event.type === "session.deleted") {
660
666
  const meta = sessionMetas.get(sessionId);
@@ -664,6 +670,9 @@ ${triggers.join(`
664
670
  }
665
671
  }
666
672
  sessionMetas.delete(sessionId);
673
+ sessionsById.delete(sessionId);
674
+ writerSessionIDs.delete(sessionId);
675
+ agentBySessionID.delete(sessionId);
667
676
  log.info("Session deleted", { sessionId });
668
677
  }
669
678
  if (event.type === "message.updated") {
@@ -727,11 +736,107 @@ ${triggers.join(`
727
736
  meta2.planOutputted = true;
728
737
  log.info("Plan outputted detected", { sessionId });
729
738
  }
739
+ const agentName = message?.agent;
740
+ if (agentName) {
741
+ agentBySessionID.set(sessionId, agentName);
742
+ const sessionInfo = sessionsById.get(sessionId);
743
+ if (agentName === WRITER_AGENT_NAME && sessionInfo?.parentID) {
744
+ writerSessionIDs.add(sessionId);
745
+ log.info("Writer agent session registered", { sessionId, agentName, parentID: sessionInfo.parentID });
746
+ }
747
+ }
730
748
  }
731
749
  }
732
750
  } catch (err) {
733
751
  log.error("event handler error:", String(err));
734
752
  }
753
+ },
754
+ "tool.execute.before": async (input, output) => {
755
+ const { tool, sessionID } = input;
756
+ const isMemoryBankPath = (targetPath) => {
757
+ const absPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(projectRoot, targetPath);
758
+ const relativePath = path.relative(projectRoot, absPath);
759
+ if (relativePath === ".." || relativePath.startsWith(".." + path.sep))
760
+ return false;
761
+ return relativePath === "memory-bank" || relativePath.startsWith("memory-bank" + path.sep) || relativePath.startsWith("memory-bank/");
762
+ };
763
+ const isWriterAllowed = (sid) => {
764
+ if (writerSessionIDs.has(sid))
765
+ return true;
766
+ const sessionInfo = sessionsById.get(sid);
767
+ const agentName = agentBySessionID.get(sid);
768
+ if (agentName === WRITER_AGENT_NAME && sessionInfo?.parentID) {
769
+ writerSessionIDs.add(sid);
770
+ log.info("Writer agent late-registered", { sessionID: sid, agentName });
771
+ return true;
772
+ }
773
+ return false;
774
+ };
775
+ const blockWrite = (reason, context) => {
776
+ log.warn("Memory Bank write blocked", { sessionID, tool, reason, ...context });
777
+ throw new Error(`[Memory Bank Guard] \u5199\u5165 memory-bank/ \u53D7\u9650\u3002
778
+ ` + `\u8BF7\u4F7F\u7528 delegate_task \u8C03\u7528 memory-bank-writer agent \u6765\u66F4\u65B0 Memory Bank\u3002
779
+ ` + `\u793A\u4F8B: delegate_task(subagent_type="memory-bank-writer", load_skills=["memory-bank-writer"], prompt="\u66F4\u65B0...")`);
780
+ };
781
+ if (tool === "Write" || tool === "Edit") {
782
+ const targetPath = output.args?.filePath ?? output.args?.path ?? output.args?.filename;
783
+ if (!targetPath)
784
+ return;
785
+ if (!isMemoryBankPath(targetPath))
786
+ return;
787
+ if (!targetPath.endsWith(".md")) {
788
+ blockWrite("only .md files allowed", { targetPath });
789
+ }
790
+ if (isWriterAllowed(sessionID)) {
791
+ log.debug("Writer agent write allowed", { sessionID, tool, targetPath });
792
+ return;
793
+ }
794
+ blockWrite("not writer agent", {
795
+ targetPath,
796
+ isSubAgent: !!sessionsById.get(sessionID)?.parentID,
797
+ agentName: agentBySessionID.get(sessionID)
798
+ });
799
+ }
800
+ if (tool.toLowerCase() === "bash") {
801
+ const command = output.args?.command;
802
+ if (!command || typeof command !== "string")
803
+ return;
804
+ if (!/(?:^|[^A-Za-z0-9_-])memory-bank(?:$|[^A-Za-z0-9_-])/.test(command) && !command.startsWith("memory-bank"))
805
+ return;
806
+ const hasShellOperators = /[;&|]|\$\(|`/.test(command);
807
+ if (!hasShellOperators) {
808
+ const readOnlyPatterns = [
809
+ /^\s*(ls|cat|head|tail|less|more|grep|rg|ag|find|tree|wc|file|stat)\b/,
810
+ /^\s*git\s+(status|log|diff|show|blame)\b/
811
+ ];
812
+ if (readOnlyPatterns.some((p) => p.test(command)))
813
+ return;
814
+ }
815
+ const writePatterns = [
816
+ /(?:^|[^2])>/,
817
+ />>/,
818
+ /<</,
819
+ /\|/,
820
+ /\btee\b/,
821
+ /\bsed\s+-i/,
822
+ /\bperl\s+-[ip]/,
823
+ /\bcp\b/,
824
+ /\bmv\b/,
825
+ /\brm\b/,
826
+ /\bmkdir\b/,
827
+ /\btouch\b/,
828
+ /\bgit\s+(add|rm|mv|apply|checkout|restore|reset|clean|stash|commit)\b/,
829
+ /\bpython\b.*\bopen\b/
830
+ ];
831
+ const isWriteOperation = writePatterns.some((p) => p.test(command));
832
+ if (!isWriteOperation)
833
+ return;
834
+ if (isWriterAllowed(sessionID)) {
835
+ log.debug("Writer agent bash write allowed", { sessionID, command: command.slice(0, 100) });
836
+ return;
837
+ }
838
+ blockWrite("bash write operation", { command: command.slice(0, 200) });
839
+ }
735
840
  }
736
841
  };
737
842
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-bank-skill",
3
- "version": "5.6.0",
3
+ "version": "5.6.2",
4
4
  "description": "Memory Bank - 项目记忆系统,让 AI 助手在每次对话中都能快速理解项目上下文",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",
@@ -0,0 +1,27 @@
1
+ # Memory Bank Writer Skill
2
+
3
+ Memory Bank 专用写入 Agent 的技能定义。
4
+
5
+ ## 用途
6
+
7
+ 主 Agent 无法直接写入 `memory-bank/` 目录,必须通过 delegate 调用此 Agent。
8
+
9
+ ## 调用方式
10
+
11
+ ```
12
+ delegate_task(
13
+ subagent_type="memory-bank-writer",
14
+ load_skills=["memory-bank-writer"],
15
+ prompt="更新设计文档:[内容]"
16
+ )
17
+ ```
18
+
19
+ ## 内置规则
20
+
21
+ - 写入前 Glob 检查已有文件
22
+ - 优先更新而非创建
23
+ - 自动更新 `_index.md`
24
+
25
+ ## 文件
26
+
27
+ - `SKILL.md` - 技能定义和规则
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: memory-bank-writer
3
+ description: Memory Bank 专用写入 Agent - 负责所有 memory-bank/ 目录的写入操作
4
+ ---
5
+
6
+ # Memory Bank Writer
7
+
8
+ 你是 Memory Bank 的专用写入 Agent。只有你能写入 `memory-bank/` 目录。
9
+
10
+ ## 核心职责
11
+
12
+ 1. 接收主 Agent 的写入请求
13
+ 2. 执行写入前检查(Glob 查找已有文件)
14
+ 3. 决定更新现有文件还是创建新文件
15
+ 4. 执行写入并更新索引
16
+
17
+ ## 写入规则(强制)
18
+
19
+ ### 设计文档 (`docs/design-*.md`)
20
+
21
+ ```
22
+ 1. Glob("memory-bank/docs/design-*.md") 获取所有设计文档
23
+ 2. 读取 _index.md 中的设计文档列表
24
+ 3. 检查是否有相关文档:
25
+ - 文件名包含相同关键词 → 更新该文件
26
+ - 标题描述相同主题 → 更新该文件
27
+ - 无匹配 → 创建新文件
28
+ 4. 输出决策理由
29
+ ```
30
+
31
+ ### 需求文档 (`requirements/REQ-*.md`)
32
+
33
+ ```
34
+ 1. 读取 requirements/ 目录现有文件
35
+ 2. 检查是否有相同需求(按标题/ID)
36
+ 3. 有 → 更新;无 → 创建新 REQ-xxx.md
37
+ ```
38
+
39
+ ### 经验文档 (`learnings/**/*.md`)
40
+
41
+ ```
42
+ 1. 确定类型:bugs / performance / integrations
43
+ 2. 检查是否有相同问题的记录
44
+ 3. 有 → 追加或更新;无 → 创建新文件
45
+ 4. 文件名格式:YYYY-MM-DD-{slug}.md
46
+ ```
47
+
48
+ ### 其他文件
49
+
50
+ | 文件 | 规则 |
51
+ |------|------|
52
+ | `active.md` | 始终更新(不创建新的) |
53
+ | `brief.md` | 始终更新(不创建新的) |
54
+ | `tech.md` | 始终更新(不创建新的) |
55
+ | `patterns.md` | 追加内容(不覆盖) |
56
+ | `_index.md` | 每次写入后自动更新 |
57
+
58
+ ## 输出格式
59
+
60
+ 每次写入前,输出计划:
61
+
62
+ ```
63
+ [Memory Bank Writer 写入计划]
64
+
65
+ 检查结果:
66
+ - Glob docs/design-*.md → 找到 3 个文件
67
+ - 相关文件:design-auth.md(标题含 "认证")
68
+
69
+ 决策:更新 memory-bank/docs/design-auth.md(而非新建)
70
+
71
+ 将要写入:
72
+ - 更新: memory-bank/docs/design-auth.md
73
+ - 更新: memory-bank/_index.md
74
+
75
+ 执行写入...
76
+ ```
77
+
78
+ ## 索引更新
79
+
80
+ 每次写入后,检查并更新 `_index.md`:
81
+
82
+ 1. 新建文件 → 添加索引条目
83
+ 2. 更新文件 → 更新 `updated` 和 `size` 字段
84
+ 3. 删除文件 → 移除索引条目
85
+
86
+ ## 禁止行为
87
+
88
+ - 不要跳过 Glob 检查
89
+ - 不要在不确定时创建新文件(宁可询问)
90
+ - 不要修改 `memory-bank/` 以外的文件
91
+ - 不要删除文件(除非明确要求)
92
+
93
+ ## 错误处理
94
+
95
+ 如果写入失败:
96
+ 1. 报告具体错误
97
+ 2. 不要重试(让主 Agent 决定)
98
+ 3. 保持已写入的文件(不回滚)
99
+
100
+ ## 守卫机制
101
+
102
+ Plugin 层面强制执行:
103
+ - 只有 `memory-bank-writer` agent 能写入 `memory-bank/`
104
+ - 只允许写入 `.md` 文件
105
+ - 主 agent 直接写入会被阻止
106
+
107
+ ## 已知限制
108
+
109
+ > 写入守卫是**策略守卫**,防止意外违规,不是安全边界。
110
+
111
+ | 限制 | 说明 |
112
+ |------|------|
113
+ | Bash 启发式 | 变量间接、eval、脚本无法检测 |
114
+ | Symlinks | 通过 symlink 可绕过路径检查 |
115
+ | 首次写入 | 可能因 race condition 被阻止一次(重试即可) |