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:
94
claude-config/hooks/role-router.mjs
Executable file
94
claude-config/hooks/role-router.mjs
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Role Router Hook (UserPromptSubmit)
|
||||
* 按 user prompt 关键词检测当前角色(dev/test/pm),注入推荐 agent hint。
|
||||
*
|
||||
* 三种角色:
|
||||
* dev → executor / debugger / code-simplifier + engineering-* 中文 agent
|
||||
* test → qa-tester / test-engineer / verifier + testing-* 中文 agent
|
||||
* pm → planner / analyst / scientist + product-* / project-management-* 中文 agent
|
||||
*
|
||||
* 输出: stdout JSON { hookSpecificOutput: { additionalContext, hookEventName } }
|
||||
* 不阻塞、不退出非零码(保证主流程继续)。
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
export const DEV_RE = /(?:\b(?:dev|develop|develops?|implement|implements?|refactor|refactors?|fix|fixes|bug|bugs|hotfix|patch|patches|impl)\b|开发|实现|重构|修(?:复|bug)|写代码|编码|添加功能|新功能|改(?:bug|代码))/i;
|
||||
export const TEST_RE = /(?:\b(?:tests?|qa|unit\s?tests?|regression|e2e|integration\s?tests?|specs?)\b|测试|单测|单元测试|回归(?:测试)?|集成测试|端到端|质量保证|QA)/i;
|
||||
export const PM_RE = /(?:\b(?:pm|product|spec|requirement|roadmap|planning|backlog|user\s?story|prd)\b|产品(?:经理)?|需求|排期|规划|计划|调研|路线图|用户故事)/i;
|
||||
|
||||
const ROLE_HINTS = {
|
||||
dev: `<role-routing role="dev">
|
||||
检测到**开发角色**关键词。本回合优先路由:
|
||||
- 主 agent: executor (sonnet/opus by 复杂度) / debugger / code-simplifier
|
||||
- 中文角色补强: engineering-backend-architect / engineering-frontend-architect / engineering-ai-engineer
|
||||
- 工作流: 默认开 TDD(先写失败测试再实现),用 git-master 处理分支
|
||||
</role-routing>`,
|
||||
test: `<role-routing role="test">
|
||||
检测到**测试角色**关键词。本回合优先路由:
|
||||
- 主 agent: qa-tester / test-engineer / verifier
|
||||
- 中文角色补强: testing-* 系列(中文 agent 库)
|
||||
- 工作流: 测试先行;产出 PRD acceptance criteria 跟测试断言一一对应;要 evidence-based 验收
|
||||
</role-routing>`,
|
||||
pm: `<role-routing role="pm">
|
||||
检测到**PM 角色**关键词。本回合优先路由:
|
||||
- 主 agent: planner / analyst / scientist
|
||||
- 中文角色补强: product-* / project-management-* 系列(中文 agent 库)
|
||||
- 工作流: 先 deep-interview 拆需求,再 ralplan 起 PRD,明确 user stories + acceptance criteria 后才落代码
|
||||
</role-routing>`,
|
||||
};
|
||||
|
||||
export function detectRoles(prompt) {
|
||||
const roles = [];
|
||||
if (DEV_RE.test(prompt)) roles.push('dev');
|
||||
if (TEST_RE.test(prompt)) roles.push('test');
|
||||
if (PM_RE.test(prompt)) roles.push('pm');
|
||||
return roles;
|
||||
}
|
||||
|
||||
export function buildContext(roles) {
|
||||
if (roles.length === 0) return null;
|
||||
return roles.map(r => ROLE_HINTS[r]).join('\n\n');
|
||||
}
|
||||
|
||||
function readStdinSync() {
|
||||
try {
|
||||
return readFileSync(0, 'utf8');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
const raw = readStdinSync();
|
||||
if (!raw) {
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(raw);
|
||||
const prompt = data.prompt || data.user_prompt || '';
|
||||
if (!prompt) {
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
return;
|
||||
}
|
||||
const roles = detectRoles(prompt);
|
||||
const ctx = buildContext(roles);
|
||||
if (!ctx) {
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
return;
|
||||
}
|
||||
console.log(JSON.stringify({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'UserPromptSubmit',
|
||||
additionalContext: ctx,
|
||||
},
|
||||
}));
|
||||
} catch {
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
}
|
||||
}
|
||||
|
||||
const isMain = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMain) main();
|
||||
66
claude-config/hooks/role-router.test.mjs
Executable file
66
claude-config/hooks/role-router.test.mjs
Executable 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);
|
||||
Reference in New Issue
Block a user