docs(other): 更新 Vibe Coding 配置

This commit is contained in:
xlsea
2026-05-23 10:25:03 +08:00
parent 7eeff925cb
commit 7fec02ff77
132 changed files with 4833 additions and 1519 deletions
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env node
'use strict';
// VibeCoding Athena PostCompact hook v9.6
// 协议: hookSpecificOutput.{hookEventName: "PostCompact", additionalContext}
// 参考: https://code.claude.com/docs/en/hooks#postcompact
//
// 设计:
// 读 compact-snapshot.md, 注入 _index.md frontmatter 摘要
// 提示 model: 详细内容按 _index.md.pointers lazy load, 不要 glob 全目录 (铁律 8)
const fs = require('fs');
const path = require('path');
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const STATE_DIR = path.join(PROJECT_DIR, '.ai_state');
const SNAPSHOT_PATH = path.join(STATE_DIR, 'compact-snapshot.md');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
if (!fs.existsSync(SNAPSHOT_PATH)) {
process.exit(0); return;
}
try {
const snapshot = fs.readFileSync(SNAPSHOT_PATH, 'utf8');
const fmMatch = snapshot.match(/---\n([\s\S]*?)\n---/);
if (!fmMatch) { process.exit(0); return; }
const additionalContext = `[Athena PostCompact 恢复]
上次 compact 前的项目状态 (_index.md frontmatter 快照):
\`\`\`yaml
${fmMatch[1].slice(0, 1500)}
\`\`\`
详细内容**按需** lazy load:
- progress 详情 → 按 pointers.latest_progress 读 progress.md 尾部
- lessons → 命中主题再按 pointers.latest_lesson 跳段
- 完整 _index.md → \`.ai_state/_index.md\`
⚠️ 禁止 glob 整个 .ai_state/ (铁律 8 索引先行)`;
process.stderr.write(`[compact-restore] injected ${additionalContext.length} chars\n`);
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PostCompact',
additionalContext
}
}));
const trace = JSON.stringify({
ts: new Date().toISOString(),
hook: 'compact-restore',
chars: additionalContext.length
}) + '\n';
fs.appendFileSync(path.join(STATE_DIR, 'hook-trace.jsonl'), trace);
} catch (e) {
process.stderr.write(`[compact-restore] failed: ${e.message}\n`);
}
process.exit(0);
});
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env node
'use strict';
// VibeCoding Athena PreCompact hook v9.6
// 协议: 不阻断, 写文件做副作用
// 参考: https://code.claude.com/docs/en/hooks#precompact
//
// v9.6 减薄设计 (L2.C):
// 旧版 v9.5 快照整个 .ai_state/{tasks,design,progress,lessons,handoff}.md 多个文件
// v9.6 仅快照 _index.md frontmatter (≤30 行), context 利用率提升数倍
// detail/* 内容在 PostCompact 时按需 lazy load (按 pointers 跳转)
const fs = require('fs');
const path = require('path');
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const STATE_DIR = path.join(PROJECT_DIR, '.ai_state');
const INDEX_PATH = path.join(STATE_DIR, '_index.md');
const SNAPSHOT_PATH = path.join(STATE_DIR, 'compact-snapshot.md');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
let event = {};
try { event = JSON.parse(input); } catch (e) {}
const trigger = event.trigger || 'unknown'; // "manual" | "auto"
if (!fs.existsSync(INDEX_PATH)) {
process.stderr.write('[compact-snapshot] _index.md not found, skip\n');
process.exit(0); return;
}
try {
const content = fs.readFileSync(INDEX_PATH, 'utf8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!fmMatch) {
process.stderr.write('[compact-snapshot] _index.md frontmatter missing, skip\n');
process.exit(0); return;
}
const ts = new Date().toISOString();
const snapshot = `# Athena Compact Snapshot (${trigger})
# Saved at: ${ts}
# 仅快照 _index.md frontmatter (≤30 行). 详细内容按 pointers 在 PostCompact 后 lazy load.
---
${fmMatch[1]}
---
`;
fs.writeFileSync(SNAPSHOT_PATH, snapshot);
process.stderr.write(`[compact-snapshot] saved (${trigger}, ${snapshot.length} chars)\n`);
const trace = JSON.stringify({
ts, hook: 'compact-snapshot', trigger,
chars: snapshot.length
}) + '\n';
fs.appendFileSync(path.join(STATE_DIR, 'hook-trace.jsonl'), trace);
} catch (e) {
process.stderr.write(`[compact-snapshot] failed: ${e.message}\n`);
}
process.exit(0);
});
-37
View File
@@ -1,37 +0,0 @@
// VibeCoding v9.2.0 — SessionStart: 检测状态 + 恢复 + 提醒
'use strict';
const fs = require('fs');
const path = require('path');
const STATE_DIR = path.join(process.cwd(), '.ai_state');
try {
if (fs.existsSync(STATE_DIR)) {
// 恢复: 读取关键状态文件
const files = ['session.md', 'doing.md', 'plan.md', 'design.md'];
const found = files.filter(f => fs.existsSync(path.join(STATE_DIR, f)));
if (found.length > 0) {
console.log(`[VibeCoding] 恢复会话: ${found.join(', ')} 已加载`);
// 检查未完成任务
const planFile = path.join(STATE_DIR, 'plan.md');
if (fs.existsSync(planFile)) {
const plan = fs.readFileSync(planFile, 'utf8');
const unchecked = (plan.match(/^- \[ \]/gm) || []).length;
if (unchecked > 0) console.log(`[VibeCoding] ⚠ plan.md 有 ${unchecked} 个未完成任务`);
}
}
// Agent 易犯错误提醒
const convFile = path.join(STATE_DIR, 'conventions.md');
if (fs.existsSync(convFile)) {
const conv = fs.readFileSync(convFile, 'utf8');
const errSection = conv.match(/## Agent 易犯错误\n([\s\S]*?)(?=\n##|$)/);
if (errSection && errSection[1].trim()) {
console.log('[VibeCoding] 🔔 Agent 易犯错误提醒:');
console.log(errSection[1].trim().split('\n').slice(0, 5).join('\n'));
}
}
} else {
console.log('[VibeCoding] 新会话: .ai_state/ 不存在, 首次任务时自动初始化');
}
} catch (e) {
console.log('[VibeCoding] context-loader 警告: ' + e.message);
}
Regular → Executable
+175 -73
View File
@@ -1,86 +1,188 @@
// VibeCoding v9.2.0 — Stop: 4级 Quality Gate
#!/usr/bin/env node
'use strict';
// VibeCoding Athena Stop hook (delivery-gate) v9.6
// 协议:
// - block: stdout JSON { decision: "block", reason: "..." }
// - pass: exit 0 无输出
// 参考: https://code.claude.com/docs/en/hooks#stop
//
// v9.6 设计:
// 1. 只读 _index.md 不读单独文件 (铁律 8)
// 2. 适配 CC v2.1.x 的 Stop hook block-cap=8 (CLAUDE_CODE_STOP_HOOK_BLOCK_CAP)
// hook 自身计数 block 次数, 接近 8 时 warn 而非 block
// 3. 当平台支持 /goal (CC v2.1.139+) 时, 让位给 /goal evaluator, 仅作 fallback
// 4. CONCERNS 时仅 systemMessage 不阻断
//
// stop_hook_active 防递归: CC 已经在嵌套 Stop hook 时设置该字段为 true
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
process.on('uncaughtException', function(e) {
process.stderr.write('[delivery-gate] ERROR: ' + e.message + '\n');
process.exit(0); // 脚本错误不阻断
});
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const STATE_DIR = path.join(PROJECT_DIR, '.ai_state');
const INDEX_PATH = path.join(STATE_DIR, '_index.md');
const HOME = process.env.HOME || require('os').homedir();
const COUNTER_PATH = path.join(HOME, '.claude', 'state', 'athena-block-counter.json');
if (process.env.VIBECODING_HOOKS_DISABLED === '1') { process.exit(0); }
const BLOCK_CAP_SAFE = 6; // 留 2 次冗余给 CC 的硬 cap 8
const STATE_DIR = path.join(process.cwd(), '.ai_state');
const severity = { fail: [], rework: [], concerns: [] };
try {
if (!fs.existsSync(STATE_DIR)) { process.exit(0); } // 无状态 = Path A, 放行
const planFile = path.join(STATE_DIR, 'plan.md');
const qualityFile = path.join(STATE_DIR, 'quality.md');
// ── FAIL 级: 硬约束 ──
// 未完成任务
if (fs.existsSync(planFile)) {
const plan = fs.readFileSync(planFile, 'utf8');
const unchecked = (plan.match(/^- \[ \]/gm) || []).length;
if (unchecked > 0) severity.fail.push(`plan.md 有 ${unchecked} 个未完成任务`);
}
// 缺 quality.md (有 plan 但没 quality = T 阶段未执行)
if (fs.existsSync(planFile) && !fs.existsSync(qualityFile)) {
severity.fail.push('缺少 quality.md — T 阶段验证未执行');
}
// 硬编码密钥
function trace(event) {
try {
const secrets = execSync(
"git diff --cached -U0 2>/dev/null | grep -iE '(password|secret|api_key|token)\\s*[:=]\\s*.' || true",
{ encoding: 'utf8' }
);
if (secrets.trim()) severity.fail.push('疑似硬编码密钥');
} catch {}
const line = JSON.stringify({ ts: new Date().toISOString(), hook: 'delivery-gate', ...event }) + '\n';
fs.appendFileSync(path.join(STATE_DIR, 'hook-trace.jsonl'), line);
} catch (e) {}
}
// ── REWORK 级: 源码无测试 ──
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
let event = {};
try { event = JSON.parse(input); } catch (e) {}
// 防递归 (官方字段)
if (event.stop_hook_active) { process.exit(0); return; }
// 跳过 subagent
if (event.agent_type && event.agent_type !== 'main') { process.exit(0); return; }
if (!fs.existsSync(INDEX_PATH)) { process.exit(0); return; }
let fm = '';
try {
const diff = execSync('git diff --cached --name-only 2>/dev/null || git diff --name-only 2>/dev/null', { encoding: 'utf8' });
const codeExts = /\.(ts|tsx|js|jsx|py|go|rs|java|rb)$/;
const testPattern = /\.test\.|\.spec\.|__test__|__spec__|_test\.|tests\//;
const configPattern = /config|\.env|\.json$|\.yaml$|\.yml$|\.toml$|\.md$|\.txt$/i;
const srcFiles = diff.split('\n').filter(f =>
f.match(codeExts) && !f.match(testPattern) && !f.match(configPattern)
);
const testFiles = diff.split('\n').filter(f => f.match(testPattern));
if (srcFiles.length > 0 && testFiles.length === 0) {
if (srcFiles.length > 2) {
severity.rework.push(`${srcFiles.length} 个源码文件修改但无对应测试`);
} else {
severity.concerns.push(`${srcFiles.length} 个源码文件修改但无测试 (数量少, 标注)`);
const content = fs.readFileSync(INDEX_PATH, 'utf8');
const m = content.match(/^---\n([\s\S]*?)\n---/);
if (!m) { process.exit(0); return; }
fm = m[1];
} catch (e) { process.exit(0); return; }
const getStr = k => {
const m = fm.match(new RegExp('^ ' + k + ': (.*)$', 'm'));
return m ? m[1].replace(/^["']|["']$/g, '').trim() : '';
};
const getNum = k => {
const m = fm.match(new RegExp('^ ' + k + ': (\\d+)$', 'm'));
return m ? parseInt(m[1], 10) : 0;
};
const getBool = k => {
const m = fm.match(new RegExp('^ ' + k + ': (true|false)$', 'm'));
return m ? m[1] === 'true' : false;
};
const pathName = getStr('path');
const stage = getStr('stage');
const sprint = getNum('sprint');
const tasks_pending = getNum('tasks_pending');
const goal_supported = getBool('goal_supported');
const active_goal = getStr('active_goal');
// 仅在 review 阶段 + 非 Hotfix 时介入
if (!sprint || !stage || pathName === 'Hotfix' || pathName === 'Bugfix' || stage !== 'review') {
process.exit(0); return;
}
// 平台支持 /goal 且用户已设置 active_goal → 让位给 /goal evaluator
if (goal_supported && active_goal) {
process.stderr.write('[delivery-gate] defer to /goal evaluator (active goal detected)\n');
trace({ action: 'defer', reason: 'goal-active', sprint });
process.exit(0); return;
}
const needsExtReview = ['Feature', 'Refactor', 'System'].includes(pathName);
const issues = [];
// 1. tasks 全完成
if (tasks_pending > 0) {
issues.push(`${tasks_pending} Task 未完成`);
}
// 2. review report 存在
const reviewFile = path.join(STATE_DIR, 'details', 'reviews', `sprint-${sprint}.md`);
let rc = '';
try {
rc = fs.readFileSync(reviewFile, 'utf8');
} catch (e) {
issues.push('审查报告不存在');
}
// 3. Feature+ 外部审查
if (needsExtReview && rc &&
!/\/codex:review|\/codex:adversarial|reviewer|ecc-agentshield|codex unavailable/i.test(rc)) {
issues.push('无外部审查记录');
}
// 4. 测试通过记录
if (rc && !/test|测试|pass|通过|✅/i.test(rc)) {
issues.push('无测试通过记录');
}
// 5. VERDICT
let verdict = '';
if (rc) {
const m = rc.match(/VERDICT:\s*(PASS|CONCERNS|REWORK|FAIL)/i);
if (m) {
verdict = m[1].toUpperCase();
if (verdict === 'REWORK') issues.push('VERDICT=REWORK');
else if (verdict === 'FAIL') issues.push('VERDICT=FAIL');
else if (verdict === 'CONCERNS') {
process.stderr.write('[delivery-gate] CONCERNS: 建议修复后重新评分\n');
}
}
} catch {}
}
} catch (e) {
if (issues.length > 0) {
// 检查 block 次数, 接近 cap 时降级 systemMessage
fs.mkdirSync(path.dirname(COUNTER_PATH), { recursive: true });
let counter = {};
try { counter = JSON.parse(fs.readFileSync(COUNTER_PATH, 'utf8')); } catch (e) {}
const sessionId = event.session_id || 'unknown';
counter[sessionId] = (counter[sessionId] || 0) + 1;
try { fs.writeFileSync(COUNTER_PATH, JSON.stringify(counter)); } catch (e) {}
const count = counter[sessionId];
process.stderr.write(`[delivery-gate] block #${count}/${BLOCK_CAP_SAFE} ${pathName}/${stage}: ${issues.join(', ')}\n`);
trace({ action: 'block', count, pathName, stage, sprint, issues });
if (count >= BLOCK_CAP_SAFE) {
// 接近 cap, 改为 systemMessage 避免触发 CC 的 force-kill
process.stdout.write(JSON.stringify({
systemMessage: `[Athena] 已连续 block ${count} 次, 建议手动处理: ${issues.join('; ')} (剩余尝试: ${8 - count})`
}));
} else {
process.stdout.write(JSON.stringify({
decision: 'block',
reason: `[Athena delivery-gate] 阻断:\n${issues.map(i => '• ' + i).join('\n')}\n\n修复后再交付 (block ${count}/${BLOCK_CAP_SAFE}).`
}));
}
process.exit(0); return;
}
// 通过, 重置计数
try {
let counter = JSON.parse(fs.readFileSync(COUNTER_PATH, 'utf8'));
delete counter[event.session_id];
fs.writeFileSync(COUNTER_PATH, JSON.stringify(counter));
} catch (e) {}
if (verdict === 'PASS') {
// 检查是否做了 compound
const lessonsText = (() => {
try { return fs.readFileSync(path.join(STATE_DIR, 'details', 'lessons.md'), 'utf8'); }
catch (e) { return ''; }
})();
const compounded = lessonsText.includes(`Sprint ${sprint}`);
if (!compounded) {
const msg = `Sprint ${sprint} 通过但 details/lessons.md 无条目, 建议 /compound 沉淀经验`;
process.stderr.write(`[delivery-gate] PASS ${pathName}/${stage} · ${msg}\n`);
trace({ action: 'soft-warn', pathName, stage, sprint, reason: 'no compound' });
process.stdout.write(JSON.stringify({ systemMessage: `[Athena] ${msg}` }));
} else {
process.stderr.write(`[delivery-gate] PASS ${pathName}/${stage} · lessons ✓\n`);
trace({ action: 'pass', pathName, stage, sprint });
}
} else {
process.stderr.write(`[delivery-gate] 放行 ${pathName}/${stage}\n`);
trace({ action: 'pass', pathName, stage, sprint, verdict });
}
process.exit(0);
}
// ── 输出结果 ──
// Stop hook 语义: exit 0 = 放行停止, exit 2 = 阻断停止(强制继续)
// stdout 输出会作为 context 注入 Claude
const allIssues = [...severity.fail, ...severity.rework, ...severity.concerns];
if (severity.fail.length > 0) {
process.stderr.write(`[delivery-gate] ❌ FAIL — 严重问题, 必须修复:\n${severity.fail.map(i => ` ✗ ${i}`).join('\n')}\n`);
if (severity.rework.length) process.stderr.write(` + REWORK: ${severity.rework.join(', ')}\n`);
if (severity.concerns.length) process.stderr.write(` + CONCERNS: ${severity.concerns.join(', ')}\n`);
process.exit(2); // 阻断: 不让停
}
if (severity.rework.length > 0) {
process.stderr.write(`[delivery-gate] 🔧 REWORK — 需要修复:\n${severity.rework.map(i => ` ✗ ${i}`).join('\n')}\n`);
if (severity.concerns.length) process.stderr.write(` + CONCERNS: ${severity.concerns.join(', ')}\n`);
process.exit(2); // 阻断: 不让停
}
if (severity.concerns.length > 0) {
// CONCERNS: 放行但输出警告作为 context
process.stdout.write(`[delivery-gate] ⚠ CONCERNS (已放行, 建议修复):\n${severity.concerns.map(i => ` △ ${i}`).join('\n')}\n`);
process.exit(0); // 放行: 但警告已注入
}
process.exit(0); // PASS
});
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env node
'use strict';
// VibeCoding Athena PostToolUse index-updater v9.6 ★新增
// 协议: 无输出即放行 (PostToolUse 不阻断)
// 参考: https://code.claude.com/docs/en/hooks#posttooluse
//
// 设计 (铁律 5 文档即真相 + 铁律 8 索引先行):
// 1. 检测 tool_input.file_path 是否在 .ai_state/details/
// 2. 若是 → 重新扫描 details/ 计算 counts/fingerprints/pointers
// 3. 原子写 _index.md (写 .tmp 后 rename)
// 4. 避免热路径: 同一秒内对同一文件多次触发只更新一次 (debounce by mtime)
const fs = require('fs');
const path = require('path');
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const STATE_DIR = path.join(PROJECT_DIR, '.ai_state');
const INDEX_PATH = path.join(STATE_DIR, '_index.md');
const DETAILS_DIR = path.join(STATE_DIR, 'details');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
let event = {};
try { event = JSON.parse(input); } catch (e) { process.exit(0); return; }
// 检测 file_path 是否在 details/
const filePath = (event.tool_input && (event.tool_input.file_path || event.tool_input.notebook_path)) || '';
if (!filePath || !filePath.includes('.ai_state/details/')) {
process.exit(0); return;
}
if (!fs.existsSync(INDEX_PATH)) { process.exit(0); return; }
try {
updateIndex();
} catch (e) {
process.stderr.write(`[index-updater] failed: ${e.message}\n`);
}
process.exit(0);
});
function readFileSafe(p) {
try { return fs.readFileSync(p, 'utf8'); } catch (e) { return null; }
}
function mtime(p) {
try { return Math.floor(fs.statSync(p).mtimeMs); } catch (e) { return 0; }
}
function countMatches(text, regex) {
if (!text) return 0;
return (text.match(regex) || []).length;
}
function lastSectionLine(text, headPrefix) {
if (!text) return 0;
const lines = text.split('\n');
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith(headPrefix)) return i + 1;
}
return 0;
}
function lastNonEmptyLine(text) {
if (!text) return 0;
const lines = text.split('\n');
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].trim() && !lines[i].startsWith('<!--') && !lines[i].startsWith('-->')) return i + 1;
}
return 0;
}
function updateIndex() {
// 读 _index.md
const content = fs.readFileSync(INDEX_PATH, 'utf8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!fmMatch) {
process.stderr.write('[index-updater] _index.md frontmatter parse failed\n');
return;
}
let fm = fmMatch[1];
const body = fmMatch[2];
// 扫描 details/
const tasksText = readFileSafe(path.join(DETAILS_DIR, 'tasks-current.md'));
const progressText = readFileSafe(path.join(DETAILS_DIR, 'progress.md'));
const lessonsText = readFileSafe(path.join(DETAILS_DIR, 'lessons.md'));
const designText = readFileSafe(path.join(DETAILS_DIR, 'design.md'));
const proposalsText = readFileSafe(path.join(DETAILS_DIR, 'proposals.md'));
// counts
const tasks_pending = countMatches(tasksText, /^- \[ \]/gm);
const tasks_done = countMatches(tasksText, /^- \[x\]/gm);
const tasks_blocked = countMatches(tasksText, /^- \[!\]/gm);
const lessons_lines = countMatches(lessonsText, /^## /gm);
// progress entries: 非空非注释非标题行
let progress_entries = 0;
if (progressText) {
progress_entries = progressText.split('\n')
.filter(l => l.trim() && !l.startsWith('<!--') && !l.startsWith('-->') && !l.startsWith('#'))
.length;
}
// reviews_count
let reviews_count = 0;
let latest_review = '';
try {
const files = fs.readdirSync(path.join(DETAILS_DIR, 'reviews'))
.filter(f => f.match(/^sprint-\d+\.md$/));
reviews_count = files.length;
if (files.length > 0) {
const sorted = files.sort((a, b) => {
const na = parseInt(a.match(/\d+/)[0], 10);
const nb = parseInt(b.match(/\d+/)[0], 10);
return na - nb;
});
latest_review = `details/reviews/${sorted[sorted.length - 1]}`;
}
} catch (e) {}
// pointers
const latest_progress = progressText ? `details/progress.md#L${lastNonEmptyLine(progressText)}` : '';
const latest_lesson = lessonsText ? `details/lessons.md#L${lastSectionLine(lessonsText, '## ')}` : '';
const latest_proposal = proposalsText ? `details/proposals.md#L${lastSectionLine(proposalsText, '## ')}` : '';
// fingerprints
const tasks_mtime = mtime(path.join(DETAILS_DIR, 'tasks-current.md'));
const design_mtime = mtime(path.join(DETAILS_DIR, 'design.md'));
const progress_mtime = mtime(path.join(DETAILS_DIR, 'progress.md'));
const lessons_mtime = mtime(path.join(DETAILS_DIR, 'lessons.md'));
// 替换 frontmatter 字段 (保留其他字段不动)
function replaceField(yaml, key, value, isNumeric) {
const re = new RegExp(`(^ ${key}: ).*$`, 'm');
const val = isNumeric ? value : `"${value}"`;
if (re.test(yaml)) return yaml.replace(re, `$1${val}`);
return yaml; // 不存在不创建, 等用户/init 加
}
fm = replaceField(fm, 'tasks_pending', tasks_pending, true);
fm = replaceField(fm, 'tasks_done', tasks_done, true);
fm = replaceField(fm, 'tasks_blocked', tasks_blocked, true);
fm = replaceField(fm, 'lessons_lines', lessons_lines, true);
fm = replaceField(fm, 'progress_entries', progress_entries, true);
fm = replaceField(fm, 'reviews_count', reviews_count, true);
fm = replaceField(fm, 'latest_progress', latest_progress, false);
fm = replaceField(fm, 'latest_review', latest_review, false);
fm = replaceField(fm, 'latest_lesson', latest_lesson, false);
fm = replaceField(fm, 'latest_proposal', latest_proposal, false);
fm = replaceField(fm, 'tasks_mtime', tasks_mtime, true);
fm = replaceField(fm, 'design_mtime', design_mtime, true);
fm = replaceField(fm, 'progress_mtime', progress_mtime, true);
fm = replaceField(fm, 'lessons_mtime', lessons_mtime, true);
const newContent = `---\n${fm}\n---\n${body}`;
// 原子写入
const tmpPath = INDEX_PATH + '.tmp';
fs.writeFileSync(tmpPath, newContent);
fs.renameSync(tmpPath, INDEX_PATH);
process.stderr.write(`[index-updater] _index.md updated (pending=${tasks_pending} done=${tasks_done})\n`);
// hook-trace
try {
const trace = JSON.stringify({
ts: new Date().toISOString(),
hook: 'index-updater',
tasks_pending, tasks_done, lessons_lines, reviews_count
}) + '\n';
fs.appendFileSync(path.join(STATE_DIR, 'hook-trace.jsonl'), trace);
} catch (e) {}
}
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env node
'use strict';
// VibeCoding Athena Stop hook (pace-continuator) v9.6 ★新增
// 协议: 不阻断 (放在 delivery-gate 之后串联运行)
// - 写入 details/next.md (PACE-aware 下一动作建议)
// - 在检测到"反复试错/失败模式"时, append details/proposals.md (铁律 9)
// 参考: https://code.claude.com/docs/en/hooks#stop
//
// 不发明 "自动 PACE", 只是写"下一步可以做的"备忘, 等用户自己读或下次 SessionStart 注入
const fs = require('fs');
const path = require('path');
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const STATE_DIR = path.join(PROJECT_DIR, '.ai_state');
const INDEX_PATH = path.join(STATE_DIR, '_index.md');
const NEXT_PATH = path.join(STATE_DIR, 'details', 'next.md');
const PROPOSALS_PATH = path.join(STATE_DIR, 'details', 'proposals.md');
const HOOK_TRACE = path.join(STATE_DIR, 'hook-trace.jsonl');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
let event = {};
try { event = JSON.parse(input); } catch (e) {}
if (event.stop_hook_active) { process.exit(0); return; }
if (event.agent_type && event.agent_type !== 'main') { process.exit(0); return; }
if (!fs.existsSync(INDEX_PATH)) { process.exit(0); return; }
let fm = '';
try {
const content = fs.readFileSync(INDEX_PATH, 'utf8');
const m = content.match(/^---\n([\s\S]*?)\n---/);
if (!m) { process.exit(0); return; }
fm = m[1];
} catch (e) { process.exit(0); return; }
const getStr = k => {
const m = fm.match(new RegExp('^ ' + k + ': (.*)$', 'm'));
return m ? m[1].replace(/^["']|["']$/g, '').trim() : '';
};
const getNum = k => {
const m = fm.match(new RegExp('^ ' + k + ': (\\d+)$', 'm'));
return m ? parseInt(m[1], 10) : 0;
};
const pathName = getStr('path');
const stage = getStr('stage');
const sprint = getNum('sprint');
const tasks_pending = getNum('tasks_pending');
const tasks_done = getNum('tasks_done');
if (!pathName || !stage || pathName === 'Hotfix' || pathName === 'Bugfix') {
process.exit(0); return;
}
// 推断下一动作
const suggestions = nextActionSuggestions(pathName, stage, tasks_pending, tasks_done);
if (suggestions.length === 0) { process.exit(0); return; }
// 写 next.md (覆盖, 仅保留最新一条 + 历史移到顶部)
const ts = new Date().toISOString().replace('T', ' ').slice(0, 16);
const entry = `## ${ts}\n当前: ${pathName}/${stage}/sprint ${sprint}\n建议:\n${suggestions.map(s => '- ' + s).join('\n')}\n`;
let existing = '';
try { existing = fs.readFileSync(NEXT_PATH, 'utf8'); } catch (e) {}
// 去掉模板注释, 在顶部 append
existing = existing.replace(/<!--[\s\S]*?-->\s*/g, '').trim();
const newContent = `# 下一动作建议\n\n${entry}\n${existing ? '\n---\n\n' + existing : ''}`.trim() + '\n';
fs.writeFileSync(NEXT_PATH, newContent);
// 检测反复试错: 读 hook-trace 最近 20 条, 看 subagent-retry / delivery-gate block 是否高频
try {
const trace = fs.readFileSync(HOOK_TRACE, 'utf8').split('\n').filter(l => l).slice(-20);
const retryCount = trace.filter(l => l.includes('"subagent-retry"')).length;
const blockCount = trace.filter(l => l.includes('"delivery-gate"') && l.includes('"block"')).length;
if (retryCount >= 3 || blockCount >= 2) {
// append proposals.md (铁律 9 Hook 反思)
const proposalEntry = `\n## ${ts} (sprint ${sprint})\n- 观察: 最近 20 hook 事件含 subagent-retry × ${retryCount}, delivery-gate block × ${blockCount}\n- 假设: ${pathName}/${stage} 阶段流程可能不顺畅, 可能是 design 不足 / task 过大 / 验收标准模糊\n- 提议: 复盘当前 sprint 设计粒度, 考虑拆 task 或回 plan 阶段\n`;
fs.appendFileSync(PROPOSALS_PATH, proposalEntry);
process.stderr.write(`[pace-continuator] proposals.md +1 (retry=${retryCount}, block=${blockCount})\n`);
}
} catch (e) {}
process.stderr.write(`[pace-continuator] next.md updated (${pathName}/${stage})\n`);
// hook-trace
try {
const trace = JSON.stringify({
ts: new Date().toISOString(),
hook: 'pace-continuator',
path: pathName, stage, sprint,
suggestions_count: suggestions.length
}) + '\n';
fs.appendFileSync(HOOK_TRACE, trace);
} catch (e) {}
process.exit(0);
});
function nextActionSuggestions(path, stage, pending, done) {
const s = [];
if (stage === 'plan') {
s.push('完成 design.md 的 File Structure Plan 段');
s.push('和用户确认设计后 → 进入 stage=impl');
s.push('可选: /goal "design.md 含 File Structure Plan + tasks ≥ 1 + 用户确认"');
} else if (stage === 'design') {
s.push('锁定架构, 完成 details/tasks-current.md (每 Task 含 Boundary/Depends)');
s.push('用户确认 → 进入 stage=impl');
} else if (stage === 'impl') {
if (pending > 0) {
s.push(`完成剩余 ${pending} 个 Task (按 Boundary 拆分)`);
if (path === 'Refactor' || path === 'System') {
s.push('可选: /batch "<重构描述>" 并行加速 (CC v2.1.121+ 原生)');
}
s.push('每完成 Task → tasks-current.md 勾选 + progress.md 追加');
} else if (done > 0) {
s.push('所有 task 完成, 跑测试和 lint → 切 stage=review');
s.push('/simplify 清理冗余');
}
} else if (stage === 'review') {
s.push('运行 /review → 写 reviews/sprint-N.md');
if (path !== 'Quick') {
s.push('运行 /codex:review (跨模型审查)');
}
s.push('@evaluator 评分 → VERDICT');
s.push('PASS → /compound 沉淀 lessons → stage=ship');
} else if (stage === 'ship') {
s.push('superpowers /ship (Feature+) 或 git commit (Quick)');
s.push('归档 details/tasks-archive/sprint-N.md');
s.push('_index.md sprint += 1, stage="" ');
}
return s;
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env node
'use strict';
// VibeCoding Athena PermissionDenied hook v9.6
// 协议: stdout JSON { retry: true } (CC v2.1.89+ 官方标准)
// 参考: https://code.claude.com/docs/en/hooks#permissiondenied
//
// 设计:
// 当 auto mode classifier 拒绝某个工具调用 → 提示 model 可以重试 (但只针对一次)
// 并 systemMessage 建议用户加入全局 deny (如果是反复出现的 pattern)
const fs = require('fs');
const path = require('path');
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const STATE_DIR = path.join(PROJECT_DIR, '.ai_state');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
let event = {};
try { event = JSON.parse(input); } catch (e) { process.exit(0); return; }
const tool = event.tool_name || '';
const cmd = (event.tool_input && event.tool_input.command) || '';
const reason = event.reason || '';
// 软提示: 如果是 Bash + 看起来是项目本地命令 → 提示用户考虑加入项目 allow
let suggestion = '';
if (tool === 'Bash' && cmd) {
// 抽取命令名 (第一个 token)
const cmdName = cmd.trim().split(/\s+/)[0];
if (['npm', 'pnpm', 'yarn', 'pytest', 'cargo', 'go', 'make'].includes(cmdName)) {
suggestion = `如果这是项目常用命令, 可: /athena:remember-allow "Bash(${cmdName}:*)" 加入全局 allow`;
}
}
if (suggestion) {
process.stderr.write(`[permission-retry] suggest: ${suggestion}\n`);
process.stdout.write(JSON.stringify({
retry: true,
systemMessage: `[Athena] permission denied. ${suggestion}`
}));
} else {
// 默认让 model 知道可以一次性重试
process.stdout.write(JSON.stringify({ retry: true }));
}
try {
const trace = JSON.stringify({
ts: new Date().toISOString(),
hook: 'permission-retry',
tool, cmd_head: cmd.slice(0, 60), reason: reason.slice(0, 100)
}) + '\n';
fs.appendFileSync(path.join(STATE_DIR, 'hook-trace.jsonl'), trace);
} catch (e) {}
process.exit(0);
});
-19
View File
@@ -1,19 +0,0 @@
// VibeCoding v9.2.0 — PostToolUse(Write|Edit): 格式化
'use strict';
const { execSync } = require('child_process');
const fs = require('fs');
const input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
const file = input?.tool_input?.file_path || input?.tool_input?.path || '';
try {
if (file.match(/\.(ts|tsx|js|jsx)$/)) {
// 先检查 prettier 存在
if (fs.existsSync('node_modules/.bin/prettier')) {
execSync(`npx prettier --write "${file}" 2>/dev/null`, { timeout: 5000 });
}
} else if (file.match(/\.py$/)) {
try { execSync('which black', { stdio: 'ignore', timeout: 2000 }); } catch { process.exit(0); }
execSync(`python3 -m black "${file}" --quiet 2>/dev/null`, { timeout: 5000 });
}
} catch {}
process.exit(0);
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env node
'use strict';
// VibeCoding Athena PreToolUse Bash guard v9.6
// 协议: hookSpecificOutput.{hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason}
// 参考: https://code.claude.com/docs/en/hooks#pretooluse
//
// v9.6 设计:
// 配合 settings.json 的 if 字段做精细化拦截 (官方 if syntax: https://code.claude.com/docs/en/hooks#common-fields)
// settings.json 已用 if: "Bash(rm *)" / "Bash(curl *|wget *)" / "Bash(mkfs*|dd *)" 预过滤
// hook 内部做二次确认 + 提供具体的拒绝原因
//
// permissions.deny 已含 "Bash(rm -rf *)" 灾难规则, 这里是双重保险
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
let cmd = '';
try {
const d = JSON.parse(input);
cmd = (d.tool_input && d.tool_input.command) || '';
} catch (e) { process.exit(0); return; }
if (!cmd) { process.exit(0); return; }
const deny = (reason) => {
process.stderr.write(`[bash-guard] deny: ${reason} (${cmd.slice(0, 60)})\n`);
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: `Athena bash-guard 阻断: ${reason}`
}
}));
process.exit(0);
};
// 灾难级 (3 条):
// 1. rm -rf 系统/用户根
if (/rm\s+-[rRf]+\s+(\/\s|\/\*|~\/?\s|~\s|\/$|~$)/.test(cmd) ||
/rm\s+-[rRf]+\s+\/\s*[;&|]/.test(cmd)) {
return deny('禁止删除系统/用户根目录');
}
// 2. 管道远程脚本
if (/(curl|wget)\s+[^|]*\|\s*(bash|sh|zsh|fish|ksh)/.test(cmd)) {
return deny('禁止管道执行远程脚本 (curl|wget ... | sh)');
}
// 3. 格式化磁盘 / 写设备
if (/\bmkfs\.|dd\s+if=.*of=\/dev\/sd|>\s*\/dev\/sd[a-z]/.test(cmd)) {
return deny('禁止格式化磁盘/写设备');
}
process.exit(0);
});
-26
View File
@@ -1,26 +0,0 @@
// VibeCoding v9.2.0 — PreToolUse(Bash): 安全守卫 (JSON decision output)
'use strict';
const input = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
const cmd = input?.tool_input?.command || '';
const BLOCKED = [
/rm\s+-rf\s+[\/~]/, /chmod\s+777/, /curl\s+.*\|\s*(?:bash|sh)/,
/wget\s+.*\|\s*(?:bash|sh)/, />\s*\/etc\//, /mkfs\./, /dd\s+if=/,
/:(){ :\|:& };:/, /fork\s*bomb/i
];
for (const pattern of BLOCKED) {
if (pattern.test(cmd)) {
// 使用 JSON decision output (官方推荐方式)
const output = JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: `[pre-bash] 危险命令被阻断: ${cmd.slice(0, 80)}`
}
});
process.stdout.write(output);
process.exit(0);
}
}
process.exit(0);
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env node
'use strict';
// VibeCoding Athena SessionStart hook v9.6
// 协议: hookSpecificOutput.{hookEventName: "SessionStart", additionalContext}
// 参考: https://code.claude.com/docs/en/hooks#sessionstart
//
// 设计 (铁律 8 索引先行):
// 1. 唯一 IO: read .ai_state/_index.md (frontmatter)
// 2. 注入 context-essentials.md (静态文本)
// 3. 注入 _index.md 关键字段摘要 (path/stage/sprint/counts/active_goal)
// 4. effort=max 时跳过 PACE 状态注入 (高 effort 模型自己探索)
// 5. 不预加载 tasks/design/progress 内容
const fs = require('fs');
const path = require('path');
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const STATE_DIR = path.join(PROJECT_DIR, '.ai_state');
const INDEX_PATH = path.join(STATE_DIR, '_index.md');
const PROJECT_ESSENTIALS = path.join(PROJECT_DIR, '.claude', 'skills', 'pace', 'context-essentials.md');
const HOME = process.env.HOME || require('os').homedir();
const GLOBAL_ESSENTIALS = path.join(HOME, '.claude', 'skills', 'pace', 'context-essentials.md');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
let event = {};
try { event = JSON.parse(input); } catch (e) {}
const source = event.source || 'unknown';
// CC v2.1.133+ 注入 effort.level
const effort = (event.effort && event.effort.level) || process.env.CLAUDE_EFFORT || '';
const isMax = effort === 'max';
const lines = [];
// 1. 注入 context-essentials.md (优先项目级, 回落全局)
let essentialsText = '';
for (const p of [PROJECT_ESSENTIALS, GLOBAL_ESSENTIALS]) {
try {
const t = fs.readFileSync(p, 'utf8').trim();
if (t) { essentialsText = t; break; }
} catch (e) {}
}
if (essentialsText) {
lines.push(essentialsText);
} else {
lines.push('[VibeCoding Athena v9.6] TDD · Review · Sisyphus · 索引先行 · 完成度证据 · 出处优先\n[Get-bearings] read .ai_state/_index.md 然后按 pointers 跳转明细');
}
// 2. effort=max 时不注入 PACE 状态摘要 (高 effort 自己探索)
if (!isMax && fs.existsSync(INDEX_PATH)) {
try {
const content = fs.readFileSync(INDEX_PATH, 'utf8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (fmMatch) {
const fm = fmMatch[1];
const getStr = k => {
const m = fm.match(new RegExp('^ ' + k + ': (.*)$', 'm'));
return m ? m[1].replace(/^["']|["']$/g, '').trim() : '';
};
const getNum = k => {
const m = fm.match(new RegExp('^ ' + k + ': (\\d+)$', 'm'));
return m ? parseInt(m[1], 10) : 0;
};
const path_ = getStr('path');
const stage = getStr('stage');
const sprint = getNum('sprint');
const goal = getStr('active_goal');
const pending = getNum('tasks_pending');
const done = getNum('tasks_done');
const blocked = getNum('tasks_blocked');
if (path_ && stage) {
lines.push(`\n[PACE] Path:${path_} Stage:${stage} Sprint:${sprint}`);
}
if (goal) {
lines.push(`[/goal active] ${goal.slice(0, 120)}${goal.length > 120 ? '...' : ''}`);
}
if (pending + done + blocked > 0) {
lines.push(`[任务] ${done} 完成 ${pending} 待办 ${blocked} 阻塞 (详见 details/tasks-current.md)`);
}
// gotchas (按 YAML array 简单解析: 单行 [..] 或多行 - 形式)
const gotchasMatch = fm.match(/^gotchas:\s*(.+)$/m);
if (gotchasMatch) {
const g = gotchasMatch[1].trim();
if (g.startsWith('[') && !g.match(/^\[\s*\]$/)) {
lines.push(`[Gotchas] ${g.slice(1, -1).trim()}`);
}
}
}
} catch (e) {
process.stderr.write(`[session-start] _index.md parse failed: ${e.message}\n`);
}
}
if (lines.length === 0) { process.exit(0); return; }
const additionalContext = lines.join('\n');
process.stderr.write(`[session-start/${source}${isMax ? '/max' : ''}] injected ${additionalContext.length} chars\n`);
// hook-trace
try {
const trace = JSON.stringify({
ts: new Date().toISOString(),
hook: 'session-start',
source, effort, chars: additionalContext.length
}) + '\n';
fs.appendFileSync(path.join(STATE_DIR, 'hook-trace.jsonl'), trace);
} catch (e) {}
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext
}
}));
process.exit(0);
});
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
'use strict';
// VibeCoding Athena ConfigChange hook v9.6 ★新增 (L2.G)
// 协议: 不阻断, additionalContext 注入 warning
// 参考: https://code.claude.com/docs/en/hooks#configchange
//
// 设计:
// ConfigChange 事件触发时 (settings.json/CLAUDE.md/MCP 等变更):
// 1. 重新读 _index.md, 验证 schema_version 仍是 "9.6"
// 2. 比对 fingerprints 段, 检测 details/* 是否被外部修改 (mtime 异常跳变)
// 3. 异常时注入 systemMessage 提醒 (而非 block, ConfigChange 不该 block)
//
// 用途场景: 用户用编辑器手改了 _index.md 导致 schema 损坏 / git checkout 切换分支后状态错位
const fs = require('fs');
const path = require('path');
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const STATE_DIR = path.join(PROJECT_DIR, '.ai_state');
const INDEX_PATH = path.join(STATE_DIR, '_index.md');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
let event = {};
try { event = JSON.parse(input); } catch (e) {}
const configSource = event.matcher || 'unknown';
if (!fs.existsSync(INDEX_PATH)) { process.exit(0); return; }
const warnings = [];
try {
const content = fs.readFileSync(INDEX_PATH, 'utf8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!fmMatch) {
warnings.push('_index.md frontmatter 已损坏, 无法解析');
} else {
const fm = fmMatch[1];
// 1. schema_version 检查
const schemaMatch = fm.match(/^schema_version: "([^"]+)"$/m);
if (!schemaMatch || schemaMatch[1] !== '9.6') {
warnings.push(`schema_version 异常: ${schemaMatch ? schemaMatch[1] : 'missing'} (期望 "9.6", 可能需要 /athena-migrate)`);
}
// 2. fingerprints vs 实际 mtime 比对
const getFp = k => {
const m = fm.match(new RegExp('^ ' + k + ': (\\d+)$', 'm'));
return m ? parseInt(m[1], 10) : 0;
};
const fpTasks = getFp('tasks_mtime');
const fpDesign = getFp('design_mtime');
const fpProgress = getFp('progress_mtime');
const fpLessons = getFp('lessons_mtime');
const checkFile = (name, fp) => {
try {
const actual = Math.floor(fs.statSync(path.join(STATE_DIR, 'details', name)).mtimeMs);
// 允许 fingerprint 落后于 actual (PostToolUse 还没跑), 但反过来不正常
if (fp > actual + 1000) {
warnings.push(`${name} fingerprint 比实际 mtime 新 (${fp - actual}ms), 可能被外部修改`);
}
} catch (e) {}
};
checkFile('tasks-current.md', fpTasks);
checkFile('design.md', fpDesign);
checkFile('progress.md', fpProgress);
checkFile('lessons.md', fpLessons);
}
} catch (e) {
warnings.push(`审计失败: ${e.message}`);
}
if (warnings.length === 0) { process.exit(0); return; }
const msg = `[Athena state-audit] ConfigChange (${configSource}) 检测到 ${warnings.length} 个潜在问题:\n${warnings.map(w => '• ' + w).join('\n')}`;
process.stderr.write(msg + '\n');
process.stdout.write(JSON.stringify({ systemMessage: msg }));
try {
const trace = JSON.stringify({
ts: new Date().toISOString(),
hook: 'state-audit',
source: configSource,
warnings_count: warnings.length
}) + '\n';
fs.appendFileSync(path.join(STATE_DIR, 'hook-trace.jsonl'), trace);
} catch (e) {}
process.exit(0);
});
-18
View File
@@ -1,18 +0,0 @@
// VibeCoding v9.2.0 — StopFailure: API错误时保存状态
'use strict';
const fs = require('fs');
const path = require('path');
const STATE_DIR = path.join(process.cwd(), '.ai_state');
try {
if (fs.existsSync(STATE_DIR)) {
const sessionFile = path.join(STATE_DIR, 'session.md');
if (fs.existsSync(sessionFile)) {
let content = fs.readFileSync(sessionFile, 'utf8');
const ts = new Date().toISOString();
content += `\n\n## 异常中断\n${ts} — API错误导致会话中断。下次 SessionStart 自动恢复。\n`;
fs.writeFileSync(sessionFile, content);
}
console.log('[VibeCoding] API错误,状态已保存。下次启动自动恢复。');
}
} catch {}
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env node
'use strict';
// VibeCoding Athena PostToolUse subagent-retry v9.6
// 协议: 不阻断 (additionalContext 注入 hint)
// 参考: https://code.claude.com/docs/en/hooks#posttooluse
//
// 设计:
// Task tool 调用 subagent. 若返回的 tool_response 里含"无法"/"无能力"等放弃语
// → 注入 systemMessage 提示 main agent 重试或亲自接管
// 防止 subagent 偷懒 (v9.5 实测问题)
const fs = require('fs');
const path = require('path');
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const STATE_DIR = path.join(PROJECT_DIR, '.ai_state');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
let event = {};
try { event = JSON.parse(input); } catch (e) { process.exit(0); return; }
if (event.tool_name !== 'Task') { process.exit(0); return; }
const response = event.tool_response;
if (!response || typeof response !== 'object') { process.exit(0); return; }
// 提取 subagent 的完整响应内容
let text = '';
if (typeof response.content === 'string') text = response.content;
else if (Array.isArray(response.content)) text = response.content.map(c => c.text || '').join('\n');
else text = JSON.stringify(response);
// 放弃信号词
const giveupPatterns = [
/我(无法|不能|没法|无能力|不具备能力)/,
/I (cannot|can't|am not able to|don't have access)/i,
/no (capability|access|permission) to/i,
/unable to (read|write|execute|access)/i,
/tool not available/i,
/(请|你)(自己|手动|帮我)?(运行|执行|跑)/,
/please run (this|the command) (yourself|manually)/i
];
const found = giveupPatterns.find(p => p.test(text));
if (!found) { process.exit(0); return; }
process.stderr.write(`[subagent-retry] giveup detected: ${found}\n`);
const subagentType = (event.tool_input && event.tool_input.subagent_type) || 'subagent';
process.stdout.write(JSON.stringify({
systemMessage: `[Athena subagent-retry] ${subagentType} 返回了放弃信号. 协议: 1) 再尝试一次, 用不同参数或拆分任务 2) 仍失败则主 agent 接管, 不要让用户代执行 (铁律 6 完成度证据)`
}));
// hook-trace
try {
const trace = JSON.stringify({
ts: new Date().toISOString(),
hook: 'subagent-retry',
subagent_type: subagentType,
pattern: String(found)
}) + '\n';
fs.appendFileSync(path.join(STATE_DIR, 'hook-trace.jsonl'), trace);
} catch (e) {}
process.exit(0);
});
-21
View File
@@ -1,21 +0,0 @@
// VibeCoding v9.2.0 — Stop: TDD 检查 (git diff 对比)
'use strict';
const { execSync } = require('child_process');
try {
const diff = execSync('git diff --name-only 2>/dev/null', { encoding: 'utf8' });
const codeExts = /\.(ts|tsx|js|jsx|py|go|rs)$/;
const testPattern = /\.test\.|\.spec\.|__test__|_test\./;
const configPattern = /config|\.json$|\.yaml$|\.yml$|\.md$|\.lock$/i;
const srcFiles = diff.split('\n').filter(f =>
f.match(codeExts) && !f.match(testPattern) && !f.match(configPattern)
);
const testFiles = diff.split('\n').filter(f => f.match(testPattern));
if (srcFiles.length > 0 && testFiles.length === 0) {
process.stderr.write(`[tdd-check] ⚠ ${srcFiles.length} 个源码修改但无测试。TDD: 先写测试!\n`);
// 不阻断, 只提醒 (阻断在 delivery-gate)
}
} catch {}
process.exit(0);