mirror of
https://gitee.com/nicefold/seewis-soybean-plus.git
synced 2026-10-14 10:02:13 +08:00
docs(other): 更新 Vibe Coding 配置
This commit is contained in:
Executable
+65
@@ -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);
|
||||
});
|
||||
Executable
+63
@@ -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);
|
||||
});
|
||||
@@ -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
@@ -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
|
||||
});
|
||||
|
||||
Executable
+175
@@ -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) {}
|
||||
}
|
||||
Executable
+136
@@ -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;
|
||||
}
|
||||
Executable
+59
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
Executable
+54
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
Executable
+122
@@ -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);
|
||||
});
|
||||
Executable
+94
@@ -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);
|
||||
});
|
||||
@@ -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 {}
|
||||
Executable
+70
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user