强制遵守升级:编辑区域时按会话注入完整规范全文(非仅红线)
从"红线常驻"升级为"全部规范时刻在场":真正开工某区域时,把该区域的 完整编号文档 + 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user