config(claude): 角色路由 hook (dev/test/pm) + role-router 单测

- claude-config/hooks/role-router.mjs
  按 user prompt 关键词检测当前角色,注入推荐 agent hint
  - dev: 开发/实现/重构/修(复|bug)/dev/implement/refactor/fix/feature → executor/debugger
  - test: 测试/单测/回归/QA/test/regression/spec → qa-tester/test-engineer/verifier
  - pm: 产品/需求/排期/规划/PM/product/PRD/roadmap → planner/analyst/scientist
  支持多角色同时命中
- claude-config/hooks/role-router.test.mjs
  10 fixture 单测 (含 subprocess pipe integration), 10/10 PASS
- claude-config/settings.json.snapshot.20260426
  全局 settings.json 快照, hooks.UserPromptSubmit 接 keyword-detector + role-router
- claude-config/README.md
  设计/安装/测试 文档

来源: ralph US-3, a+b 融合方案 (211 中文 agents + 关键词 hook)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
pp
2026-04-26 21:36:34 +08:00
parent 45409c60d2
commit 8915fa2898
4 changed files with 245 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* role-router unit tests — 6 fixture (每角色 2 条)
* 跑法: node role-router.test.mjs
*/
import { detectRoles, buildContext, DEV_RE, TEST_RE, PM_RE } from './role-router.mjs';
import { execFileSync } from 'child_process';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const HOOK = join(__dirname, 'role-router.mjs');
const fixtures = [
{ prompt: '帮我实现一下登录页面', expect: ['dev'] },
{ prompt: 'refactor the auth middleware', expect: ['dev'] },
{ prompt: '跑一下回归测试看看', expect: ['test'] },
{ prompt: 'write unit tests for the parser', expect: ['test'] },
{ prompt: '排期一下下周的需求', expect: ['pm'] },
{ prompt: 'draft the PRD for new chat module', expect: ['pm'] },
{ prompt: '今天天气怎么样', expect: [] },
{ prompt: 'PM 要我先调研后排期再让 dev 实现 + 写测试', expect: ['dev', 'test', 'pm'] },
];
let pass = 0, fail = 0;
const failures = [];
for (const f of fixtures) {
const got = detectRoles(f.prompt);
const ok = JSON.stringify(got.sort()) === JSON.stringify(f.expect.sort());
if (ok) {
pass++;
console.log(`PASS "${f.prompt}" → [${got.join(',')}]`);
} else {
fail++;
failures.push({ prompt: f.prompt, expected: f.expect, got });
console.log(`FAIL "${f.prompt}" expected=[${f.expect.join(',')}] got=[${got.join(',')}]`);
}
}
// integration: pipe JSON to hook subprocess
function runHook(promptObj) {
const input = JSON.stringify(promptObj);
const out = execFileSync('node', [HOOK], { input, encoding: 'utf8' });
return JSON.parse(out);
}
console.log('\n--- integration (subprocess pipe) ---');
const t1 = runHook({ prompt: '帮我修个 bug' });
const hasContextDev = t1?.hookSpecificOutput?.additionalContext?.includes('role="dev"');
if (hasContextDev) { pass++; console.log('PASS subprocess injects dev role'); }
else { fail++; failures.push({prompt:'pipe-dev', got:t1}); console.log('FAIL subprocess dev:', JSON.stringify(t1)); }
const t2 = runHook({ prompt: 'hello world' });
const continued = t2?.continue === true;
if (continued) { pass++; console.log('PASS no-match passes through'); }
else { fail++; failures.push({prompt:'pipe-noop', got:t2}); console.log('FAIL no-match:', JSON.stringify(t2)); }
console.log(`\nResult: ${pass} pass, ${fail} fail`);
if (fail > 0) {
console.error('FAILURES:', JSON.stringify(failures, null, 2));
process.exit(1);
}
process.exit(0);