mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
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:
77
.claude/skills/ccw-coordinator/tools/README.md
Normal file
77
.claude/skills/ccw-coordinator/tools/README.md
Normal 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 目录)
|
||||
|
||||
281
.claude/skills/ccw-coordinator/tools/chain-validate.js
Normal file
281
.claude/skills/ccw-coordinator/tools/chain-validate.js
Normal 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 };
|
||||
181
.claude/skills/ccw-coordinator/tools/command-registry.js
Normal file
181
.claude/skills/ccw-coordinator/tools/command-registry.js
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user