mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: Add cost-aware parallel execution with execution_group support
- Schema: Add execution_group and task-level complexity fields - Executor: Hybrid dependency analysis (explicit + file conflicts) - Executor: Cost-based batching (MAX_BATCH_COST=8, Low=1/Medium=2/High=4) - Executor: execution_group priority for explicit parallel grouping - Planner: Add guidance for execution_group and complexity fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -149,14 +149,14 @@ Input Parsing:
|
||||
|
||||
Execution:
|
||||
├─ Step 1: Initialize result tracking (previousExecutionResults = [])
|
||||
├─ Step 2: Task grouping & batch creation
|
||||
│ ├─ Extract explicit depends_on (no file/keyword inference)
|
||||
│ ├─ Group: independent tasks → single parallel batch (maximize utilization)
|
||||
│ ├─ Group: dependent tasks → sequential phases (respect dependencies)
|
||||
├─ Step 2: Task grouping & batch creation (cost-aware)
|
||||
│ ├─ Build hybrid dependencies (explicit depends_on + file conflicts via modification_points)
|
||||
│ ├─ Phase 1: Group by execution_group (explicit parallel groups)
|
||||
│ ├─ Phase 2: Cost-aware batching (MAX_BATCH_COST=8, Low=1/Medium=2/High=4)
|
||||
│ └─ Create TodoWrite list for batches
|
||||
├─ Step 3: Launch execution
|
||||
│ ├─ Phase 1: All independent tasks (⚡ single batch, concurrent)
|
||||
│ └─ Phase 2+: Dependent tasks by dependency order
|
||||
│ ├─ Parallel batches: ⚡ concurrent via multiple tool calls
|
||||
│ └─ Sequential batches: → one by one
|
||||
├─ Step 4: Track progress (TodoWrite updates per batch)
|
||||
└─ Step 5: Code review (if codeReviewTool ≠ "Skip")
|
||||
|
||||
@@ -181,67 +181,96 @@ previousExecutionResults = []
|
||||
|
||||
**Dependency Analysis & Grouping Algorithm**:
|
||||
```javascript
|
||||
// Use explicit depends_on from plan.json (no inference from file/keywords)
|
||||
function extractDependencies(tasks) {
|
||||
const MAX_BATCH_COST = 8 // Workload limit per batch (tunable)
|
||||
|
||||
// Task cost based on complexity (Low=1, Medium=2, High=4)
|
||||
function calculateTaskCost(task) {
|
||||
const costs = { Low: 1, Medium: 2, High: 4 }
|
||||
return costs[task.complexity] || 1
|
||||
}
|
||||
|
||||
// Build hybrid dependencies: explicit depends_on + implicit file conflicts
|
||||
function buildDependencies(tasks) {
|
||||
const taskIdToIndex = {}
|
||||
tasks.forEach((t, i) => { taskIdToIndex[t.id] = i })
|
||||
const fileOwner = {} // Track which task last modified each file
|
||||
|
||||
return tasks.map((task, i) => {
|
||||
// Only use explicit depends_on from plan.json
|
||||
const deps = (task.depends_on || [])
|
||||
.map(depId => taskIdToIndex[depId])
|
||||
.filter(idx => idx !== undefined && idx < i)
|
||||
return { ...task, taskIndex: i, dependencies: deps }
|
||||
const deps = new Set()
|
||||
|
||||
// 1. Explicit depends_on
|
||||
;(task.depends_on || []).forEach(depId => {
|
||||
const idx = taskIdToIndex[depId]
|
||||
if (idx !== undefined && idx < i) deps.add(idx)
|
||||
})
|
||||
|
||||
// 2. Implicit file conflicts via modification_points
|
||||
;(task.modification_points || []).forEach(mp => {
|
||||
if (fileOwner[mp.file] !== undefined && fileOwner[mp.file] !== i) {
|
||||
deps.add(fileOwner[mp.file])
|
||||
}
|
||||
fileOwner[mp.file] = i
|
||||
})
|
||||
|
||||
return { ...task, taskIndex: i, dependencies: [...deps], cost: calculateTaskCost(task) }
|
||||
})
|
||||
}
|
||||
|
||||
// Group into batches: maximize parallel execution
|
||||
// Group into cost-aware batches with execution_group priority
|
||||
function createExecutionCalls(tasks, executionMethod) {
|
||||
const tasksWithDeps = extractDependencies(tasks)
|
||||
const tasksWithDeps = buildDependencies(tasks)
|
||||
const processed = new Set()
|
||||
const calls = []
|
||||
let parallelIdx = 1, sequentialIdx = 1
|
||||
|
||||
// Phase 1: All independent tasks → single parallel batch (maximize utilization)
|
||||
const independentTasks = tasksWithDeps.filter(t => t.dependencies.length === 0)
|
||||
if (independentTasks.length > 0) {
|
||||
independentTasks.forEach(t => processed.add(t.taskIndex))
|
||||
// Phase 1: Group by execution_group (explicit parallel groups)
|
||||
const groups = {}
|
||||
tasksWithDeps.forEach(t => {
|
||||
if (t.execution_group) {
|
||||
groups[t.execution_group] = groups[t.execution_group] || []
|
||||
groups[t.execution_group].push(t)
|
||||
}
|
||||
})
|
||||
Object.entries(groups).forEach(([groupId, groupTasks]) => {
|
||||
groupTasks.forEach(t => processed.add(t.taskIndex))
|
||||
calls.push({
|
||||
method: executionMethod,
|
||||
executionType: "parallel",
|
||||
groupId: "P1",
|
||||
taskSummary: independentTasks.map(t => t.title).join(' | '),
|
||||
tasks: independentTasks
|
||||
groupId: `P${parallelIdx++}`,
|
||||
taskSummary: groupTasks.map(t => t.title).join(' | '),
|
||||
tasks: groupTasks
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Phase 2: Dependent tasks → sequential batches (respect dependencies)
|
||||
let sequentialIndex = 1
|
||||
// Phase 2: Process remaining by dependency order with cost-aware batching
|
||||
let remaining = tasksWithDeps.filter(t => !processed.has(t.taskIndex))
|
||||
|
||||
while (remaining.length > 0) {
|
||||
// Find tasks whose dependencies are all satisfied
|
||||
const ready = remaining.filter(t =>
|
||||
t.dependencies.every(d => processed.has(d))
|
||||
)
|
||||
const ready = remaining.filter(t => t.dependencies.every(d => processed.has(d)))
|
||||
if (ready.length === 0) { ready.push(...remaining) } // Break circular deps
|
||||
|
||||
if (ready.length === 0) {
|
||||
console.warn('Circular dependency detected, forcing remaining tasks')
|
||||
ready.push(...remaining)
|
||||
// Cost-aware batch creation
|
||||
let batch = [], batchCost = 0
|
||||
const stillReady = [...ready]
|
||||
while (stillReady.length > 0) {
|
||||
const idx = stillReady.findIndex(t => batchCost + t.cost <= MAX_BATCH_COST)
|
||||
if (idx === -1) break
|
||||
const task = stillReady.splice(idx, 1)[0]
|
||||
batch.push(task)
|
||||
batchCost += task.cost
|
||||
}
|
||||
if (batch.length === 0) batch = [ready[0]] // At least one task
|
||||
|
||||
// Group ready tasks (can run in parallel within this phase)
|
||||
ready.forEach(t => processed.add(t.taskIndex))
|
||||
batch.forEach(t => processed.add(t.taskIndex))
|
||||
calls.push({
|
||||
method: executionMethod,
|
||||
executionType: ready.length > 1 ? "parallel" : "sequential",
|
||||
groupId: ready.length > 1 ? `P${calls.length + 1}` : `S${sequentialIndex++}`,
|
||||
taskSummary: ready.map(t => t.title).join(ready.length > 1 ? ' | ' : ' → '),
|
||||
tasks: ready
|
||||
executionType: batch.length > 1 ? "parallel" : "sequential",
|
||||
groupId: batch.length > 1 ? `P${parallelIdx++}` : `S${sequentialIdx++}`,
|
||||
taskSummary: batch.map(t => t.title).join(batch.length > 1 ? ' | ' : ' → '),
|
||||
tasks: batch
|
||||
})
|
||||
|
||||
remaining = remaining.filter(t => !processed.has(t.taskIndex))
|
||||
}
|
||||
|
||||
return calls
|
||||
}
|
||||
|
||||
@@ -539,8 +568,8 @@ codex --full-auto exec "[Verify plan acceptance criteria at ${plan.json}]" --ski
|
||||
## Best Practices
|
||||
|
||||
**Input Modes**: In-memory (lite-plan), prompt (standalone), file (JSON/text)
|
||||
**Task Grouping**: Based on explicit depends_on only; independent tasks run in single parallel batch
|
||||
**Execution**: All independent tasks launch concurrently via single Claude message with multiple tool calls
|
||||
**Task Grouping**: Hybrid dependencies (explicit depends_on + file conflicts) + execution_group priority + cost-aware batching (MAX_BATCH_COST=8)
|
||||
**Execution**: Parallel batches via single Claude message with multiple tool calls; cost balances workload (Low=1, Medium=2, High=4)
|
||||
|
||||
## Error Handling
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ const schema = Bash(`cat ~/.claude/workflows/cli-templates/schemas/plan-json-sch
|
||||
const plan = {
|
||||
summary: "...",
|
||||
approach: "...",
|
||||
tasks: [...], // Follow Task Grouping Rules below
|
||||
tasks: [...], // Each task: { id, title, scope, ..., depends_on, execution_group, complexity }
|
||||
estimated_time: "...",
|
||||
recommended_execution: "Agent",
|
||||
complexity: "Low",
|
||||
@@ -412,6 +412,8 @@ Generate plan.json with:
|
||||
3. **Substantial tasks**: Each task should represent 15-60 minutes of work
|
||||
4. **True dependencies only**: Only use depends_on when Task B cannot start without Task A's output
|
||||
5. **Prefer parallel**: Most tasks should be independent (no depends_on)
|
||||
6. **Explicit parallel groups**: Assign same `execution_group` ID (e.g., "group-1") to tasks that can run concurrently; null for others
|
||||
7. **Task complexity**: Set `complexity` (Low/Medium/High) for workload balancing - executor uses cost units (1/2/4)
|
||||
|
||||
## Execution
|
||||
1. Read ALL exploration files for comprehensive context
|
||||
|
||||
@@ -118,6 +118,15 @@
|
||||
"pattern": "^T[0-9]+$"
|
||||
},
|
||||
"description": "Task IDs this task depends on (e.g., ['T1', 'T2'])"
|
||||
},
|
||||
"execution_group": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Parallel execution group ID. Tasks with same group ID run concurrently. null = use depends_on logic"
|
||||
},
|
||||
"complexity": {
|
||||
"type": "string",
|
||||
"enum": ["Low", "Medium", "High"],
|
||||
"description": "Task complexity for workload balancing (Low=1, Medium=2, High=4 cost units)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user