Files
youle_framework/.claude/hooks/remind-docs.js
T
joywayerandClaude Opus 4.8 ec58d0e584 强制遵守升级:编辑区域时按会话注入完整规范全文(非仅红线)
从"红线常驻"升级为"全部规范时刻在场":真正开工某区域时,把该区域的
完整编号文档 + 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>
2026-07-06 08:03:50 +08:00

104 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);
});