feat: Implement CCW Coordinator for interactive command orchestration

- Add action files for session management, command selection, building, execution, and completion.
- Introduce orchestrator logic to drive state transitions and action selection.
- Create state schema to define session state structure.
- Develop command registry and validation tools for command metadata extraction and chain validation.
- Establish skill configuration and specifications for command library and validation rules.
- Implement tools for command registry and chain validation with CLI support.
This commit is contained in:
catlog22
2026-01-23 23:39:16 +08:00
parent 8179472e56
commit e727a07fc5
15 changed files with 1728 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
# CCW Coordinator Tools
## command-registry.js
命令注册表工具:按需查找并提取命令 YAML 头元数据。
### 功能
- **按需提取**: 只提取用户任务链中的命令(不是全量扫描)
- **自动查找**: 从全局 `.claude/commands/workflow` 目录读取(项目相对路径 > 用户 home
- **解析 YAML 头**: 提取 name, description, argument-hint, allowed-tools
- **缓存机制**: 避免重复读取文件
### 编程接口
```javascript
const CommandRegistry = require('./tools/command-registry.js');
const registry = new CommandRegistry();
// 按需提取命令链中的命令元数据
const commandNames = ['/workflow:lite-plan', '/workflow:lite-execute'];
const commands = registry.getCommands(commandNames);
// 输出:
// {
// "/workflow:lite-plan": {
// name: 'lite-plan',
// command: '/workflow:lite-plan',
// description: '...',
// argumentHint: '[-e|--explore] "task description"',
// allowedTools: [...],
// filePath: '...'
// },
// "/workflow:lite-execute": { ... }
// }
```
### 命令行接口
```bash
# 提取指定命令
node .claude/skills/ccw-coordinator/tools/command-registry.js lite-plan lite-execute
# 输出 JSON
node .claude/skills/ccw-coordinator/tools/command-registry.js /workflow:lite-plan
```
### 集成用途
`action-command-execute` 中使用:
```javascript
// 1. 只提取任务链中的命令
const commandNames = command_chain.map(cmd => cmd.command);
const commandMeta = registry.getCommands(commandNames);
// 2. 生成提示词时使用
function generatePrompt(cmd, state, commandMeta) {
const cmdInfo = commandMeta[cmd.command];
let prompt = `任务: ${state.task_description}\n`;
if (cmdInfo?.argumentHint) {
prompt += `命令: ${cmd.command} ${cmdInfo.argumentHint}`;
}
return prompt;
}
```
确保 `ccw cli -p "..."` 提示词包含准确的命令参数提示。
### 目录查找逻辑
自动查找顺序:
1. `.claude/commands/workflow` (相对于当前工作目录)
2. `~/.claude/commands/workflow` (用户 home 目录)

View File

@@ -0,0 +1,281 @@
#!/usr/bin/env node
/**
* Chain Validation Tool
*
* Validates workflow command chains against defined rules.
*
* Usage:
* node chain-validate.js plan execute test-cycle-execute
* node chain-validate.js --json "plan,execute,test-cycle-execute"
* node chain-validate.js --file custom-chain.json
*/
const fs = require('fs');
const path = require('path');
// Load registry
const registryPath = path.join(__dirname, '..', 'specs', 'chain-registry.json');
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
class ChainValidator {
constructor(registry) {
this.registry = registry;
this.errors = [];
this.warnings = [];
}
validate(chain) {
this.errors = [];
this.warnings = [];
this.validateSinglePlanning(chain);
this.validateCompatiblePairs(chain);
this.validateTestingPosition(chain);
this.validateReviewPosition(chain);
this.validateBugfixStandalone(chain);
this.validateDependencies(chain);
this.validateNoRedundancy(chain);
this.validateCommandExistence(chain);
return {
valid: this.errors.length === 0,
errors: this.errors,
warnings: this.warnings
};
}
validateSinglePlanning(chain) {
const planningCommands = chain.filter(cmd =>
['plan', 'lite-plan', 'multi-cli-plan', 'tdd-plan'].includes(cmd)
);
if (planningCommands.length > 1) {
this.errors.push({
rule: 'Single Planning Command',
message: `Too many planning commands: ${planningCommands.join(', ')}`,
severity: 'error'
});
}
}
validateCompatiblePairs(chain) {
const compatibility = {
'lite-plan': ['lite-execute'],
'multi-cli-plan': ['lite-execute', 'execute'],
'plan': ['execute'],
'tdd-plan': ['execute']
};
const planningCmd = chain.find(cmd =>
['plan', 'lite-plan', 'multi-cli-plan', 'tdd-plan'].includes(cmd)
);
const executionCmd = chain.find(cmd =>
['execute', 'lite-execute'].includes(cmd)
);
if (planningCmd && executionCmd) {
const compatible = compatibility[planningCmd] || [];
if (!compatible.includes(executionCmd)) {
this.errors.push({
rule: 'Compatible Pairs',
message: `${planningCmd} incompatible with ${executionCmd}`,
fix: `Use ${planningCmd} with ${compatible.join(' or ')}`,
severity: 'error'
});
}
}
}
validateTestingPosition(chain) {
const executionIdx = chain.findIndex(cmd =>
['execute', 'lite-execute', 'develop-with-file'].includes(cmd)
);
const testingIdx = chain.findIndex(cmd =>
['test-cycle-execute', 'tdd-verify', 'test-gen', 'test-fix-gen'].includes(cmd)
);
if (testingIdx !== -1 && executionIdx !== -1 && executionIdx > testingIdx) {
this.errors.push({
rule: 'Testing After Execution',
message: 'Testing commands must come after execution',
severity: 'error'
});
}
if (testingIdx !== -1 && executionIdx === -1) {
const hasTestGen = chain.some(cmd => ['test-gen', 'test-fix-gen'].includes(cmd));
if (!hasTestGen) {
this.warnings.push({
rule: 'Testing After Execution',
message: 'test-cycle-execute without execution context - needs test-gen or execute first',
severity: 'warning'
});
}
}
}
validateReviewPosition(chain) {
const executionIdx = chain.findIndex(cmd =>
['execute', 'lite-execute'].includes(cmd)
);
const reviewIdx = chain.findIndex(cmd =>
cmd.includes('review')
);
if (reviewIdx !== -1 && executionIdx !== -1 && executionIdx > reviewIdx) {
this.errors.push({
rule: 'Review After Changes',
message: 'Review commands must come after execution',
severity: 'error'
});
}
if (reviewIdx !== -1 && executionIdx === -1) {
const isModuleReview = chain[reviewIdx] === 'review-module-cycle';
if (!isModuleReview) {
this.warnings.push({
rule: 'Review After Changes',
message: 'Review without execution - needs git changes to review',
severity: 'warning'
});
}
}
}
validateBugfixStandalone(chain) {
if (chain.includes('lite-fix')) {
const others = chain.filter(cmd => cmd !== 'lite-fix');
if (others.length > 0) {
this.errors.push({
rule: 'BugFix Standalone',
message: 'lite-fix must be standalone, cannot combine with other commands',
fix: 'Use lite-fix alone OR use plan + execute for larger changes',
severity: 'error'
});
}
}
}
validateDependencies(chain) {
for (let i = 0; i < chain.length; i++) {
const cmd = chain[i];
const cmdMeta = this.registry.commands[cmd];
if (!cmdMeta) continue;
const deps = cmdMeta.dependencies || [];
const depsOptional = cmdMeta.dependencies_optional || false;
if (deps.length > 0 && !depsOptional) {
const hasDependency = deps.some(dep => {
const depIdx = chain.indexOf(dep);
return depIdx !== -1 && depIdx < i;
});
if (!hasDependency) {
this.errors.push({
rule: 'Dependency Satisfaction',
message: `${cmd} requires ${deps.join(' or ')} before it`,
severity: 'error'
});
}
}
}
}
validateNoRedundancy(chain) {
const seen = new Set();
const duplicates = [];
for (const cmd of chain) {
if (seen.has(cmd)) {
duplicates.push(cmd);
}
seen.add(cmd);
}
if (duplicates.length > 0) {
this.errors.push({
rule: 'No Redundant Commands',
message: `Duplicate commands: ${duplicates.join(', ')}`,
severity: 'error'
});
}
}
validateCommandExistence(chain) {
for (const cmd of chain) {
if (!this.registry.commands[cmd]) {
this.errors.push({
rule: 'Command Existence',
message: `Unknown command: ${cmd}`,
severity: 'error'
});
}
}
}
}
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage:');
console.log(' chain-validate.js <command1> <command2> ...');
console.log(' chain-validate.js --json "cmd1,cmd2,cmd3"');
console.log(' chain-validate.js --file chain.json');
process.exit(1);
}
let chain;
if (args[0] === '--json') {
chain = args[1].split(',').map(s => s.trim());
} else if (args[0] === '--file') {
const filePath = args[1];
const fileContent = JSON.parse(fs.readFileSync(filePath, 'utf8'));
chain = fileContent.chain || fileContent.steps.map(s => s.command);
} else {
chain = args;
}
const validator = new ChainValidator(registry);
const result = validator.validate(chain);
console.log('\n=== Chain Validation Report ===\n');
console.log('Chain:', chain.join(' → '));
console.log('');
if (result.valid) {
console.log('✓ Chain is valid!\n');
} else {
console.log('✗ Chain has errors:\n');
result.errors.forEach(err => {
console.log(` [${err.rule}] ${err.message}`);
if (err.fix) {
console.log(` Fix: ${err.fix}`);
}
});
console.log('');
}
if (result.warnings.length > 0) {
console.log('⚠ Warnings:\n');
result.warnings.forEach(warn => {
console.log(` [${warn.rule}] ${warn.message}`);
});
console.log('');
}
process.exit(result.valid ? 0 : 1);
}
if (require.main === module) {
main();
}
module.exports = { ChainValidator };

View File

@@ -0,0 +1,181 @@
#!/usr/bin/env node
/**
* Command Registry Tool
*
* 功能:
* 1. 根据命令名称查找并提取 YAML 头
* 2. 从全局 .claude/commands/workflow 目录读取
* 3. 支持按需提取(不是全量扫描)
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
class CommandRegistry {
constructor(commandDir = null) {
// 优先使用传入的目录
if (commandDir) {
this.commandDir = commandDir;
} else {
// 自动查找 .claude/commands/workflow
this.commandDir = this.findCommandDir();
}
this.cache = {};
}
/**
* 自动查找 .claude/commands/workflow 目录
* 支持: 项目相对路径、用户 home 目录
*/
findCommandDir() {
// 1. 尝试相对于当前工作目录
const relativePath = path.join('.claude', 'commands', 'workflow');
if (fs.existsSync(relativePath)) {
return path.resolve(relativePath);
}
// 2. 尝试用户 home 目录
const homeDir = os.homedir();
const homeCommandDir = path.join(homeDir, '.claude', 'commands', 'workflow');
if (fs.existsSync(homeCommandDir)) {
return homeCommandDir;
}
// 未找到时返回 null后续操作会失败并提示
return null;
}
/**
* 解析 YAML 头
*/
parseYamlHeader(content) {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return null;
const yamlContent = match[1];
const result = {};
const lines = yamlContent.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
const colonIndex = line.indexOf(':');
if (colonIndex === -1) continue;
const key = line.substring(0, colonIndex).trim();
const value = line.substring(colonIndex + 1).trim();
let cleanValue = value.replace(/^["']|["']$/g, '');
if (key === 'allowed-tools') {
cleanValue = cleanValue.split(',').map(t => t.trim());
}
result[key] = cleanValue;
}
return result;
}
/**
* 获取单个命令的元数据
* @param {string} commandName 命令名称 (e.g., "lite-plan" 或 "/workflow:lite-plan")
* @returns {object|null} 命令信息或 null
*/
getCommand(commandName) {
if (!this.commandDir) {
console.error('ERROR: .claude/commands/workflow 目录未找到');
return null;
}
// 标准化命令名称
const normalized = commandName.startsWith('/workflow:')
? commandName.substring('/workflow:'.length)
: commandName;
// 检查缓存
if (this.cache[normalized]) {
return this.cache[normalized];
}
// 读取命令文件
const filePath = path.join(this.commandDir, `${normalized}.md`);
if (!fs.existsSync(filePath)) {
return null;
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
const header = this.parseYamlHeader(content);
if (header && header.name) {
const result = {
name: header.name,
command: `/workflow:${header.name}`,
description: header.description || '',
argumentHint: header['argument-hint'] || '',
allowedTools: Array.isArray(header['allowed-tools'])
? header['allowed-tools']
: (header['allowed-tools'] ? [header['allowed-tools']] : []),
filePath: filePath
};
// 缓存结果
this.cache[normalized] = result;
return result;
}
} catch (error) {
console.error(`读取命令失败 ${filePath}:`, error.message);
}
return null;
}
/**
* 批量获取多个命令的元数据
* @param {array} commandNames 命令名称数组
* @returns {object} 命令信息映射
*/
getCommands(commandNames) {
const result = {};
for (const name of commandNames) {
const cmd = this.getCommand(name);
if (cmd) {
result[cmd.command] = cmd;
}
}
return result;
}
/**
* 生成注册表 JSON
*/
toJSON(commands = null) {
const data = commands || this.cache;
return JSON.stringify(data, null, 2);
}
}
// CLI 模式
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('用法: node command-registry.js <command-name> [command-name2] ...');
console.error('示例: node command-registry.js lite-plan lite-execute');
console.error(' node command-registry.js /workflow:lite-plan');
process.exit(1);
}
const registry = new CommandRegistry();
const commands = registry.getCommands(args);
console.log(JSON.stringify(commands, null, 2));
}
module.exports = CommandRegistry;