mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
refactor: improve CCW orchestrator with serial blocking execution and hook-based continuation
- Rename file to lowercase: CCW-COORDINATOR.md → ccw-coordinator.md - Replace polling waitForTaskCompletion with stop-action blocking model - CLI commands execute in background with immediate stop (no polling) - Hook callbacks (handleCliCompletion) trigger continuation to next command - Add task_id and completed_at fields to execution_results - Maintain state checkpoint after each command launch - Add status flow documentation (running → waiting → completed) - Include CLI invocation example with hook configuration - Separate concerns: orchestrator launches, hooks handle callbacks - Support serial execution: one command at a time with break after launch
This commit is contained in:
@@ -177,8 +177,14 @@ const commandPorts = {
|
|||||||
},
|
},
|
||||||
'review-session-cycle': {
|
'review-session-cycle': {
|
||||||
name: 'review-session-cycle',
|
name: 'review-session-cycle',
|
||||||
input: ['code'],
|
input: ['code', 'session'], // 可接受代码或会话
|
||||||
output: ['review-verified'],
|
output: ['review-verified'], // 输出端口:审查通过
|
||||||
|
tags: ['review']
|
||||||
|
},
|
||||||
|
'review-module-cycle': {
|
||||||
|
name: 'review-module-cycle',
|
||||||
|
input: ['module-pattern'], // 输入端口:模块模式
|
||||||
|
output: ['review-verified'], // 输出端口:审查通过
|
||||||
tags: ['review']
|
tags: ['review']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -277,18 +283,27 @@ async function executeCommandChain(chain, analysis) {
|
|||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
status: 'running',
|
status: 'running',
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
analysis: analysis,
|
analysis: analysis,
|
||||||
command_chain: chain,
|
command_chain: chain.map((cmd, idx) => ({ ...cmd, index: idx, status: 'pending' })),
|
||||||
execution_results: [],
|
execution_results: [],
|
||||||
prompts_used: []
|
prompts_used: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save initial state immediately after confirmation
|
||||||
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
|
|
||||||
for (let i = 0; i < chain.length; i++) {
|
for (let i = 0; i < chain.length; i++) {
|
||||||
const cmd = chain[i];
|
const cmd = chain[i];
|
||||||
console.log(`[${i+1}/${chain.length}] ${cmd.command}`);
|
console.log(`[${i+1}/${chain.length}] ${cmd.command}`);
|
||||||
|
|
||||||
|
// Update command_chain status to running
|
||||||
|
state.command_chain[i].status = 'running';
|
||||||
|
state.updated_at = new Date().toISOString();
|
||||||
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
|
|
||||||
// Assemble prompt with previous results
|
// Assemble prompt with previous results
|
||||||
const prompt = `Task: ${analysis.goal}\n`;
|
let prompt = `Task: ${analysis.goal}\n`;
|
||||||
if (state.execution_results.length > 0) {
|
if (state.execution_results.length > 0) {
|
||||||
prompt += '\nPrevious results:\n';
|
prompt += '\nPrevious results:\n';
|
||||||
state.execution_results.forEach(r => {
|
state.execution_results.forEach(r => {
|
||||||
@@ -299,55 +314,89 @@ async function executeCommandChain(chain, analysis) {
|
|||||||
}
|
}
|
||||||
prompt += `\n${formatCommand(cmd, state.execution_results, analysis)}\n`;
|
prompt += `\n${formatCommand(cmd, state.execution_results, analysis)}\n`;
|
||||||
|
|
||||||
// Execute via ccw cli
|
// Record prompt used
|
||||||
|
state.prompts_used.push({
|
||||||
|
index: i,
|
||||||
|
command: cmd.command,
|
||||||
|
prompt: prompt
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute via ccw cli (background, serial blocking with stop action)
|
||||||
try {
|
try {
|
||||||
const result = Bash(
|
console.log(`Executing: ${cmd.command}`);
|
||||||
|
|
||||||
|
// Execute CLI command in background
|
||||||
|
// Result will arrive via hook callback (do NOT poll)
|
||||||
|
const taskId = Bash(
|
||||||
`ccw cli -p "${escapePrompt(prompt)}" --tool claude --mode write -y`,
|
`ccw cli -p "${escapePrompt(prompt)}" --tool claude --mode write -y`,
|
||||||
{ run_in_background: true }
|
{ run_in_background: true }
|
||||||
);
|
).task_id;
|
||||||
const parsed = parseOutput(result.stdout);
|
|
||||||
|
|
||||||
// Record result
|
// Save state with pending result (checkpoint)
|
||||||
state.execution_results.push({
|
state.execution_results.push({
|
||||||
index: i,
|
index: i,
|
||||||
command: cmd.command,
|
command: cmd.command,
|
||||||
status: 'completed',
|
status: 'in-progress',
|
||||||
session_id: parsed.sessionId,
|
task_id: taskId,
|
||||||
artifacts: parsed.artifacts,
|
session_id: null,
|
||||||
|
artifacts: [],
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`✓ ${parsed.sessionId}\n`);
|
state.command_chain[i].status = 'running';
|
||||||
|
state.updated_at = new Date().toISOString();
|
||||||
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
|
|
||||||
|
// STOP - CLI executes in background, break loop immediately
|
||||||
|
// Hook callback will call handleCliCompletion → resumeChainExecution
|
||||||
|
console.log(`[${i+1}/${chain.length}] Waiting for CLI result...\n`);
|
||||||
|
break; // Serial blocking: one command at a time
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
state.command_chain[i].status = 'failed';
|
||||||
|
state.updated_at = new Date().toISOString();
|
||||||
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
|
|
||||||
const action = await AskUserQuestion({
|
const action = await AskUserQuestion({
|
||||||
questions: [{
|
questions: [{
|
||||||
question: `${cmd.command} failed. What to do?`,
|
question: `${cmd.command} failed to start: ${error.message}. What to do?`,
|
||||||
header: 'Error',
|
header: 'Error',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Retry', description: 'Try again' },
|
{ label: 'Retry', description: 'Try again' },
|
||||||
{ label: 'Skip', description: 'Continue' },
|
{ label: 'Skip', description: 'Continue next command' },
|
||||||
{ label: 'Abort', description: 'Stop' }
|
{ label: 'Abort', description: 'Stop execution' }
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (action.error === 'retry') {
|
if (action.error === 'Retry') {
|
||||||
|
state.command_chain[i].status = 'pending';
|
||||||
|
state.execution_results.pop();
|
||||||
i--;
|
i--;
|
||||||
} else if (action.error === 'abort') {
|
} else if (action.error === 'Skip') {
|
||||||
|
state.execution_results[state.execution_results.length - 1].status = 'skipped';
|
||||||
|
} else if (action.error === 'Abort') {
|
||||||
state.status = 'failed';
|
state.status = 'failed';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save state after each command
|
// Save state checkpoint after each iteration
|
||||||
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
state.status = 'completed';
|
// If no error, orchestrator will continue via hook callbacks
|
||||||
|
// Do NOT set status to 'completed' here - hook callback handles final completion
|
||||||
|
if (state.status !== 'failed' && state.execution_results.length < state.command_chain.length) {
|
||||||
|
state.status = 'waiting'; // Waiting for hook callbacks
|
||||||
|
}
|
||||||
state.updated_at = new Date().toISOString();
|
state.updated_at = new Date().toISOString();
|
||||||
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
|
|
||||||
|
console.log(`\n📋 Orchestrator paused - waiting for CLI callbacks`);
|
||||||
|
console.log(`Session: ${state.session_id}`);
|
||||||
|
console.log(`State: ${stateDir}/state.json\n`);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,6 +466,54 @@ function formatCommand(cmd, previousResults, analysis) {
|
|||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hook callback handler for CLI completion
|
||||||
|
// IMPORTANT: This is called by user's hook when CLI finishes
|
||||||
|
async function handleCliCompletion(sessionId, taskId, output) {
|
||||||
|
// Load current state
|
||||||
|
const stateDir = `.workflow/.ccw-coordinator/${sessionId}`;
|
||||||
|
const state = JSON.parse(Read(`${stateDir}/state.json`));
|
||||||
|
|
||||||
|
// Find the pending result
|
||||||
|
const pendingIdx = state.execution_results.findIndex(r => r.task_id === taskId);
|
||||||
|
if (pendingIdx === -1) {
|
||||||
|
console.error(`Unknown task_id: ${taskId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse CLI output
|
||||||
|
const parsed = parseOutput(output);
|
||||||
|
|
||||||
|
// Update execution result
|
||||||
|
state.execution_results[pendingIdx] = {
|
||||||
|
...state.execution_results[pendingIdx],
|
||||||
|
status: parsed.sessionId ? 'completed' : 'failed',
|
||||||
|
session_id: parsed.sessionId,
|
||||||
|
artifacts: parsed.artifacts,
|
||||||
|
completed_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update command_chain status
|
||||||
|
const cmdIdx = state.execution_results[pendingIdx].index;
|
||||||
|
state.command_chain[cmdIdx].status = parsed.sessionId ? 'completed' : 'failed';
|
||||||
|
state.updated_at = new Date().toISOString();
|
||||||
|
|
||||||
|
// Save updated state
|
||||||
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
|
|
||||||
|
// Continue to next command in chain
|
||||||
|
const nextIdx = cmdIdx + 1;
|
||||||
|
if (nextIdx < state.command_chain.length) {
|
||||||
|
console.log(`\n✓ ${parsed.sessionId || 'Failed'} - Continuing to next command...\n`);
|
||||||
|
// Resume orchestrator from checkpoint (implementation-specific)
|
||||||
|
await resumeChainExecution(sessionId, nextIdx);
|
||||||
|
} else {
|
||||||
|
console.log(`\n✅ All commands completed! Session: ${sessionId}\n`);
|
||||||
|
state.status = 'completed';
|
||||||
|
state.updated_at = new Date().toISOString();
|
||||||
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parse command output
|
// Parse command output
|
||||||
function parseOutput(output) {
|
function parseOutput(output) {
|
||||||
const sessionMatch = output.match(/WFS-[\w-]+/);
|
const sessionMatch = output.match(/WFS-[\w-]+/);
|
||||||
@@ -433,7 +530,7 @@ function parseOutput(output) {
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"session_id": "ccw-coord-20250124-143025",
|
"session_id": "ccw-coord-20250124-143025",
|
||||||
"status": "running|completed|failed",
|
"status": "running|waiting|completed|failed",
|
||||||
"created_at": "2025-01-24T14:30:25Z",
|
"created_at": "2025-01-24T14:30:25Z",
|
||||||
"updated_at": "2025-01-24T14:35:45Z",
|
"updated_at": "2025-01-24T14:35:45Z",
|
||||||
"analysis": {
|
"analysis": {
|
||||||
@@ -471,17 +568,21 @@ function parseOutput(output) {
|
|||||||
"index": 0,
|
"index": 0,
|
||||||
"command": "/workflow:plan",
|
"command": "/workflow:plan",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
|
"task_id": "task-001",
|
||||||
"session_id": "WFS-plan-20250124",
|
"session_id": "WFS-plan-20250124",
|
||||||
"artifacts": ["IMPL_PLAN.md", "exploration-architecture.json"],
|
"artifacts": ["IMPL_PLAN.md", "exploration-architecture.json"],
|
||||||
"timestamp": "2025-01-24T14:30:45Z"
|
"timestamp": "2025-01-24T14:30:25Z",
|
||||||
|
"completed_at": "2025-01-24T14:30:45Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"index": 1,
|
"index": 1,
|
||||||
"command": "/workflow:execute",
|
"command": "/workflow:execute",
|
||||||
"status": "completed",
|
"status": "in-progress",
|
||||||
"session_id": "WFS-execute-20250124",
|
"task_id": "task-002",
|
||||||
"artifacts": ["src/features/auth/**", "src/db/migrations/**"],
|
"session_id": null,
|
||||||
"timestamp": "2025-01-24T14:32:15Z"
|
"artifacts": [],
|
||||||
|
"timestamp": "2025-01-24T14:32:00Z",
|
||||||
|
"completed_at": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"prompts_used": [
|
"prompts_used": [
|
||||||
@@ -499,6 +600,38 @@ function parseOutput(output) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Status Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
running → waiting → [hook callback] → waiting → [hook callback] → completed
|
||||||
|
↓ ↑
|
||||||
|
failed ←────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Values**:
|
||||||
|
- `running`: Orchestrator actively executing (launching CLI commands)
|
||||||
|
- `waiting`: Paused, waiting for hook callbacks to trigger continuation
|
||||||
|
- `completed`: All commands finished successfully
|
||||||
|
- `failed`: User aborted or unrecoverable error
|
||||||
|
|
||||||
|
### Field Descriptions
|
||||||
|
|
||||||
|
**execution_results[] fields**:
|
||||||
|
- `index`: Command position in chain (0-indexed)
|
||||||
|
- `command`: Full command string (e.g., `/workflow:plan`)
|
||||||
|
- `status`: `in-progress` | `completed` | `skipped` | `failed`
|
||||||
|
- `task_id`: Background task identifier (from Bash tool)
|
||||||
|
- `session_id`: Workflow session ID (e.g., `WFS-*`) or null if failed
|
||||||
|
- `artifacts`: Generated files/directories
|
||||||
|
- `timestamp`: Command start time (ISO 8601)
|
||||||
|
- `completed_at`: Command completion time or null if pending
|
||||||
|
|
||||||
|
**command_chain[] status values**:
|
||||||
|
- `pending`: Not started yet
|
||||||
|
- `running`: Currently executing
|
||||||
|
- `completed`: Successfully finished
|
||||||
|
- `failed`: Failed to execute
|
||||||
|
|
||||||
## CommandRegistry Integration
|
## CommandRegistry Integration
|
||||||
|
|
||||||
Sole CCW tool for command discovery:
|
Sole CCW tool for command discovery:
|
||||||
@@ -746,6 +879,119 @@ async function ccwCoordinator(taskDescription) {
|
|||||||
5. **User Control** - Confirmation + error handling with user choice
|
5. **User Control** - Confirmation + error handling with user choice
|
||||||
6. **Context Passing** - Each prompt includes previous results
|
6. **Context Passing** - Each prompt includes previous results
|
||||||
7. **Resumable** - Can load state.json to continue
|
7. **Resumable** - Can load state.json to continue
|
||||||
|
8. **Serial Blocking Execution** - Commands execute one-by-one with stop-action blocking (no polling)
|
||||||
|
|
||||||
|
## CLI Execution Mechanism
|
||||||
|
|
||||||
|
### Execution Model: Serial Blocking with Stop Action
|
||||||
|
|
||||||
|
```
|
||||||
|
Command 1 Start → Background Execution → STOP (save checkpoint)
|
||||||
|
↓
|
||||||
|
Hook Callback
|
||||||
|
↓
|
||||||
|
Command 2 Start → Background Execution → STOP (save checkpoint)
|
||||||
|
↓
|
||||||
|
Hook Callback
|
||||||
|
↓
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Principles**:
|
||||||
|
- ✅ **Serial**: Commands execute one-by-one (not parallel)
|
||||||
|
- ✅ **Blocking**: Each command waits for completion before next
|
||||||
|
- ✅ **Stop Action**: No polling - execution stops and waits for hook callback
|
||||||
|
- ✅ **Checkpoint**: State saved after each command launch
|
||||||
|
- ❌ **No Polling**: Never use `TaskOutput` to poll for results
|
||||||
|
|
||||||
|
### CLI Invocation Example (Claude Tool)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Step 1: Prepare prompt
|
||||||
|
const prompt = `Task: Implement user registration
|
||||||
|
|
||||||
|
Previous results:
|
||||||
|
- /workflow:plan: WFS-plan-20250124 (IMPL_PLAN.md)
|
||||||
|
|
||||||
|
/workflow:execute --yes --resume-session="WFS-plan-20250124"`;
|
||||||
|
|
||||||
|
// Step 2: Execute CLI in background
|
||||||
|
const taskId = Bash(
|
||||||
|
`ccw cli -p "${escapePrompt(prompt)}" --tool claude --mode write -y`,
|
||||||
|
{ run_in_background: true }
|
||||||
|
).task_id;
|
||||||
|
|
||||||
|
// Step 3: Save checkpoint
|
||||||
|
state.execution_results.push({
|
||||||
|
index: 1,
|
||||||
|
command: '/workflow:execute',
|
||||||
|
status: 'in-progress',
|
||||||
|
task_id: taskId,
|
||||||
|
timestamp: '2025-01-24T14:32:00Z'
|
||||||
|
});
|
||||||
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
|
|
||||||
|
// Step 4: STOP - Output immediately stops here
|
||||||
|
console.log(`[2/3] Waiting for CLI result (${taskId})...\n`);
|
||||||
|
|
||||||
|
// Step 5: Hook callback will call handleCliCompletion(sessionId, taskId, output)
|
||||||
|
// when CLI finishes, which will:
|
||||||
|
// - Parse output for WFS-* session ID
|
||||||
|
// - Update state.execution_results with result
|
||||||
|
// - Trigger next command in chain
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook Configuration
|
||||||
|
|
||||||
|
User should configure hook in `~/.claude/hooks/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ~/.claude/hooks/task-complete.sh
|
||||||
|
#!/bin/bash
|
||||||
|
# Called when background task completes
|
||||||
|
|
||||||
|
TASK_ID="$1"
|
||||||
|
OUTPUT="$2"
|
||||||
|
SESSION_ID=$(grep -o 'ccw-coord-[0-9]\+' .workflow/.ccw-coordinator/*/state.json | head -1 | cut -d: -f2)
|
||||||
|
|
||||||
|
# Trigger orchestrator callback
|
||||||
|
node <<EOF
|
||||||
|
const { handleCliCompletion } = require('./.claude/commands/ccw-coordinator');
|
||||||
|
handleCliCompletion('${SESSION_ID}', '${TASK_ID}', \`${OUTPUT}\`);
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling via Hook
|
||||||
|
|
||||||
|
If CLI fails, hook callback receives error:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In handleCliCompletion
|
||||||
|
if (!parsed.sessionId) {
|
||||||
|
// CLI failed - prompt user
|
||||||
|
const action = await AskUserQuestion({
|
||||||
|
questions: [{
|
||||||
|
question: `CLI command failed. What to do?`,
|
||||||
|
header: 'CLI Error',
|
||||||
|
options: [
|
||||||
|
{ label: 'Retry', description: 'Re-run command' },
|
||||||
|
{ label: 'Skip', description: 'Continue to next' },
|
||||||
|
{ label: 'Abort', description: 'Stop workflow' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle user choice
|
||||||
|
if (action.error === 'Retry') {
|
||||||
|
await resumeChainExecution(sessionId, cmdIdx); // Same command
|
||||||
|
} else if (action.error === 'Skip') {
|
||||||
|
await resumeChainExecution(sessionId, cmdIdx + 1); // Next command
|
||||||
|
} else {
|
||||||
|
state.status = 'failed';
|
||||||
|
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Available Commands
|
## Available Commands
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user