From ec58d0e5845e0497aadedf1d88a7e22eab1d136c Mon Sep 17 00:00:00 2001 From: Joywayer Date: Mon, 6 Jul 2026 08:03:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=BA=E5=88=B6=E9=81=B5=E5=AE=88=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=EF=BC=9A=E7=BC=96=E8=BE=91=E5=8C=BA=E5=9F=9F=E6=97=B6?= =?UTF-8?q?=E6=8C=89=E4=BC=9A=E8=AF=9D=E6=B3=A8=E5=85=A5=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=85=A8=E6=96=87=EF=BC=88=E9=9D=9E=E4=BB=85?= =?UTF-8?q?=E7=BA=A2=E7=BA=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从"红线常驻"升级为"全部规范时刻在场":真正开工某区域时,把该区域的 完整编号文档 + engineering 通则全文自动注入上下文,新会话同样触发。 - remind-docs.js:PreToolUse 时按路径判定区域(server / client), 每会话「首次」编辑相关区域即全量注入对应 docs 目录所有 .md 全文; 同会话再改同区域只给简短「已注入、继续遵守」提醒;跨区域共享的 engineering 通则每会话只注一次。会话级去重用 .spec-injected 哨兵。 - settings.json:新增 SessionStart 钩子,每个新会话清空哨兵目录, 保证新会话必然重新全量注入。 - .gitignore:忽略 .claude/.spec-injected/(会话级临时状态)。 - 已 pipe-test:首注 55K/44K 字符全文、二次简短、跨区去重、docs 免打扰、 SessionStart 清空、settings 结构校验,全部通过。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/hooks/remind-docs.js | 120 ++++++++++++++++++++++++----------- .claude/settings.json | 14 +++- .gitignore | 2 + 3 files changed, 98 insertions(+), 38 deletions(-) diff --git a/.claude/hooks/remind-docs.js b/.claude/hooks/remind-docs.js index c89cd07..a2de447 100644 --- a/.claude/hooks/remind-docs.js +++ b/.claude/hooks/remind-docs.js @@ -1,54 +1,102 @@ #!/usr/bin/env node -// PreToolUse hook:编辑 client/** 或 server/** 前,按路径把「必须遵守的文档」 -// 注入到上下文,作为硬提醒。非阻塞——只追加 additionalContext,不拦截操作。 -// 权威说明见根目录 CLAUDE.md「文档」一节与各 README 红线速查。 +// PreToolUse 钩子:编辑 client/** 或 server/** 时,把「必须严格遵守的完整开发规范」注入上下文。 +// +// 策略(对应用户要求:三套文档全部规范时刻遵守、新会话也一样): +// - 每个文档目录,每会话「首次」编辑相关区域时全量注入其所有 .md 全文; +// - 同会话之后再改同区域,只给简短「已注入、继续遵守」提醒,避免重复刷屏; +// - 会话级去重用 .claude/.spec-injected/ 下的哨兵文件;SessionStart 钩子会在 +// 每个新会话开始时清空该目录 → 新会话必然重新注入。 +// - 改 docs/ 本身不打扰。 +// +// 本脚本是「工具代码」,只在 Node(Claude Code 宿主)里跑,不受项目 ES5 红线约束。 + +var fs = require('fs'); +var path = require('path'); + +var HOOK_DIR = __dirname; // .../.claude/hooks +var PROJECT_DIR = path.resolve(HOOK_DIR, '..', '..'); // 项目根 +var SENTINEL_DIR = path.join(PROJECT_DIR, '.claude', '.spec-injected'); + +function slug(relDir) { return relDir.replace(/[^A-Za-z0-9]+/g, '_'); } + +function alreadyInjected(relDir) { + try { return fs.existsSync(path.join(SENTINEL_DIR, slug(relDir))); } catch (e) { return false; } +} + +function markInjected(relDir) { + try { + fs.mkdirSync(SENTINEL_DIR, { recursive: true }); + fs.writeFileSync(path.join(SENTINEL_DIR, slug(relDir)), '1'); + } catch (e) {} +} + +// 读取某目录下全部 .md(按文件名排序,README 先于编号靠 sort 自然处理),拼成一段带分隔标题的全文。 +function readDirDocs(relDir) { + var abs = path.join(PROJECT_DIR, relDir); + var files; + try { files = fs.readdirSync(abs); } catch (e) { return ''; } + var out = []; + files.filter(function (f) { return /\.md$/i.test(f); }) + .sort() + .forEach(function (f) { + try { + out.push('===== ' + relDir + '/' + f + ' =====\n' + + fs.readFileSync(path.join(abs, f), 'utf8')); + } catch (e) {} + }); + return out.join('\n\n'); +} var chunks = []; process.stdin.on('data', function (c) { chunks.push(c); }); process.stdin.on('end', function () { - var reminder = ''; + var context = ''; try { var input = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); - var p = (input.tool_input && input.tool_input.file_path) || ''; - p = String(p).replace(/\\/g, '/'); + var p = String((input.tool_input && input.tool_input.file_path) || '').replace(/\\/g, '/'); + var dirs = null, area = ''; if (/\/docs\//.test(p)) { - // 改文档本身不提醒 - reminder = ''; - } else if (/\/codes\/shared\//.test(p)) { - reminder = - '你在改 codes/shared/(服务端 shared/ 的只读同步副本)。红线:前端侧不改,' + - '只改服务端权威源再跑同步脚本。详见 client 05 §「shared 只读」/ server 04 §8。'; - } else if (/\/client\/js\/gameabc-framework\//.test(p)) { - reminder = - '你在改 gameabc-framework/(可复用框架)。必须遵守:框架保持游戏中立,' + - '禁渗入任何具体玩法逻辑/常量;依赖严格单向。权威见 client 01、client 05 红线速查,' + - '设计通则见 games/engineering。动手前请先读对应编号文档。'; - } else if (/\/client\/js\/01_subgame\//i.test(p) || /\/client\/js\//.test(p)) { - reminder = - '你在改客户端子游戏/前端代码。必须遵守 client development-guide:' + - '架构与加载顺序(01)、渲染与组件(02)、事件/动画/音频/Spine(03)、网络对接(04)、' + - '红线速查(05)、接入模式(06);工程通则见 games/engineering。' + - '红线:严格 ES5、精灵只走 SpriteManager、常量集中禁硬编码、成败只认 data.success、' + - '服务端权威前端只展示、组件走 BaseComponent、数据优先表现延后。动手前先读对应编号文档。'; + dirs = null; // 改文档本身不提醒 } else if (/\/server\//.test(p)) { - reminder = - '你在改服务端代码。必须遵守 server development-guide:环境与框架(01)、' + - '子游戏接入(02)、通信协议(03)、红线速查(04);工程通则见 games/engineering。' + - '红线:严格 ES5、require 只在文件头守卫块、成败标志 data.success(推送包自带)、' + - '数据权威缺失显式报错、模块职责单一、房间隔离(房间+seat 为 key)、' + - '自动操作复用真人链路、下发包携带前端所需全部核心数据。动手前先读对应编号文档。'; + area = '服务端'; + dirs = ['docs/server/development-guide', 'docs/games/engineering']; + } else if (/\/client\/js\//.test(p) || /\/codes\/shared\//.test(p)) { + area = '客户端'; + dirs = ['docs/client/development-guide', 'docs/games/engineering']; + } + + if (dirs) { + var pending = []; + for (var i = 0; i < dirs.length; i++) { + if (!alreadyInjected(dirs[i])) { pending.push(dirs[i]); } + } + + if (pending.length) { + var full = []; + pending.forEach(function (d) { + var txt = readDirDocs(d); + if (txt) { full.push(txt); } + markInjected(d); + }); + context = + '【必须严格遵守 · ' + area + '开发规范(本会话首次注入完整文档全文)】\n' + + '你正在修改' + area + '代码。下面是本次及后续所有改动都必须时刻遵守的完整规范' + + '(' + dirs.join('、') + ')。逐条落实,勿凭记忆越线;dev-guide 与 engineering 冲突时以 dev-guide 硬红线为准。\n\n' + + full.join('\n\n'); + } else { + context = + '【必须严格遵守 · ' + area + '开发规范】本会话已注入完整规范(' + dirs.join('、') + + ')。继续严格遵守;细节记不清就回看已注入全文或对应编号文档,不要绕过任何红线与规范。'; + } } } catch (e) { - reminder = ''; + context = ''; } - if (reminder) { + if (context) { process.stdout.write(JSON.stringify({ - hookSpecificOutput: { - hookEventName: 'PreToolUse', - additionalContext: reminder - } + hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: context } })); } process.exit(0); diff --git a/.claude/settings.json b/.claude/settings.json index 42ba05e..89e4e31 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,5 +1,15 @@ { "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "rm -rf \"$CLAUDE_PROJECT_DIR/.claude/.spec-injected\"" + } + ] + } + ], "PreToolUse": [ { "matcher": "Edit|Write|MultiEdit", @@ -7,8 +17,8 @@ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/remind-docs.js\"", - "timeout": 10, - "statusMessage": "校验开发文档红线…" + "timeout": 15, + "statusMessage": "注入/校验开发规范…" } ] } diff --git a/.gitignore b/.gitignore index 55ec011..9d8d489 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ yarn-error.log* # Claude Code 本地个人配置(含本机专属的权限豁免,不应共享) .claude/settings.local.json +# 会话级哨兵:记录本会话已注入过哪些规范目录,SessionStart 会清空,勿提交 +.claude/.spec-injected/