README(红线速查/一页纸总则)已由 CLAUDE.md 用 @import 常驻每次会话, 按区注入里再读一遍是纯重复。readDirDocs 过滤掉 README.md,只注编号正文。 - server 首注 54,339 → 49,416 字符(省 ~4.9K) - client 首注(含 client+engineering 两个 README)省 ~5.5K - 注入文案标注「README 已随 CLAUDE.md 常驻,此处为编号正文」 - 零规范损失(内容未删,只是不重复注入);pipe-test 确认无 README 段、 正文与跨区去重逻辑不变 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
106 lines
4.3 KiB
JavaScript
106 lines
4.3 KiB
JavaScript
#!/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.md:它已被 CLAUDE.md 用 @import 常驻每次会话上下文,注入里再带就是重复。
|
||
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) && !/^README\.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('、') + ';各目录 README 的红线速查/一页纸总则已随 CLAUDE.md 常驻上下文,此处不再重复)。' +
|
||
'逐条落实,勿凭记忆越线;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);
|
||
});
|