#!/usr/bin/env node // 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 context = ''; try { var input = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); var p = String((input.tool_input && input.tool_input.file_path) || '').replace(/\\/g, '/'); var dirs = null, area = ''; if (/\/docs\//.test(p)) { dirs = null; // 改文档本身不提醒 } else if (/\/server\//.test(p)) { 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) { context = ''; } if (context) { process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: context } })); } process.exit(0); });