diff --git a/.claude/commands/issue/execute.md b/.claude/commands/issue/execute.md index 1b0fd6d9..bda1d960 100644 --- a/.claude/commands/issue/execute.md +++ b/.claude/commands/issue/execute.md @@ -1,7 +1,7 @@ --- name: execute -description: Execute queue with codex using endpoint-driven task fetching (single task per codex instance) -argument-hint: "[--parallel ] [--executor codex|gemini]" +description: Execute queue with codex using DAG-based parallel orchestration (delegates task lookup to executors) +argument-hint: "[--parallel ] [--executor codex|gemini|agent]" allowed-tools: TodoWrite(*), Bash(*), Read(*), AskUserQuestion(*) --- @@ -9,26 +9,13 @@ allowed-tools: TodoWrite(*), Bash(*), Read(*), AskUserQuestion(*) ## Overview -Execution orchestrator that coordinates codex instances. Each task is executed by an independent codex instance that fetches its task via CLI endpoint. **Codex does NOT read task files** - it calls `ccw issue next` to get task data dynamically. +Minimal orchestrator that dispatches task IDs to executors. **Does NOT read task details** - delegates all task lookup to the executor via `ccw issue next `. -**Core design:** -- Single task per codex instance (not loop mode) -- Endpoint-driven: `ccw issue next` → execute → `ccw issue complete` -- No file reading in codex -- Orchestrator manages parallelism - -## Storage Structure (Queue History) - -``` -.workflow/issues/ -├── issues.jsonl # All issues (one per line) -├── queues/ # Queue history directory -│ ├── index.json # Queue index (active + history) -│ └── {queue-id}.json # Individual queue files -└── solutions/ - ├── {issue-id}.jsonl # Solutions for issue - └── ... -``` +**Design Principles:** +- **DAG-driven**: Uses `ccw issue queue dag` to get parallel execution plan +- **ID-only dispatch**: Only passes `item_id` to executors +- **Executor responsibility**: Codex/Agent fetches task details via `ccw issue next ` +- **Parallel execution**: Launches multiple executors concurrently based on DAG batches ## Usage @@ -36,427 +23,250 @@ Execution orchestrator that coordinates codex instances. Each task is executed b /issue:execute [FLAGS] # Examples -/issue:execute # Execute all ready tasks -/issue:execute --parallel 3 # Execute up to 3 tasks in parallel -/issue:execute --executor codex # Force codex executor +/issue:execute # Execute with default parallelism +/issue:execute --parallel 4 # Execute up to 4 tasks in parallel +/issue:execute --executor agent # Use agent instead of codex # Flags ---parallel Max parallel codex instances (default: 1) ---executor Force executor: codex|gemini|agent ---dry-run Show what would execute without running +--parallel Max parallel executors (default: 3) +--executor Force executor: codex|gemini|agent (default: codex) +--dry-run Show DAG and batches without executing ``` -## Execution Process +## Execution Flow ``` -Phase 1: Queue Loading - ├─ Load queue.json - ├─ Count pending/ready tasks - └─ Initialize TodoWrite tracking +Phase 1: Get DAG + └─ ccw issue queue dag → { parallel_batches, nodes, ready_count } -Phase 2: Ready Task Detection - ├─ Find tasks with satisfied dependencies - ├─ Group by execution_group (parallel batches) - └─ Determine execution order +Phase 2: Dispatch Batches + ├─ For each batch in parallel_batches: + │ ├─ Launch N executors (up to --parallel limit) + │ ├─ Each executor receives: item_id only + │ ├─ Executor calls: ccw issue next + │ ├─ Executor gets full task definition + │ ├─ Executor implements + tests + commits + │ └─ Executor calls: ccw issue done + └─ Wait for batch completion before next batch -Phase 3: Codex Coordination - ├─ For each ready task: - │ ├─ Launch independent codex instance - │ ├─ Codex calls: ccw issue next - │ ├─ Codex receives task data (NOT file) - │ ├─ Codex executes task - │ ├─ Codex calls: ccw issue complete - │ └─ Update TodoWrite - └─ Parallel execution based on --parallel flag - -Phase 4: Completion - ├─ Generate execution summary - ├─ Update issue statuses in issues.jsonl - └─ Display results +Phase 3: Summary + └─ ccw issue queue dag → updated status ``` ## Implementation -### Phase 1: Queue Loading +### Phase 1: Get DAG ```javascript -// Load active queue via CLI endpoint -const queueJson = Bash(`ccw issue status --json 2>/dev/null || echo '{}'`); -const queue = JSON.parse(queueJson); +// Get dependency graph and parallel batches +const dagJson = Bash(`ccw issue queue dag`).trim(); +const dag = JSON.parse(dagJson); -if (!queue.id || queue.tasks?.length === 0) { - console.log('No active queue found. Run /issue:queue first.'); +if (dag.error || dag.ready_count === 0) { + console.log(dag.error || 'No tasks ready for execution'); + console.log('Use /issue:queue to form a queue first'); return; } -// Count by status -const pending = queue.tasks.filter(q => q.status === 'pending'); -const executing = queue.tasks.filter(q => q.status === 'executing'); -const completed = queue.tasks.filter(q => q.status === 'completed'); - console.log(` -## Execution Queue Status +## Queue DAG -- Pending: ${pending.length} -- Executing: ${executing.length} -- Completed: ${completed.length} -- Total: ${queue.tasks.length} +- Total: ${dag.total} +- Ready: ${dag.ready_count} +- Completed: ${dag.completed_count} +- Batches: ${dag.parallel_batches.length} +- Max parallel: ${dag._summary.can_parallel} `); -if (pending.length === 0 && executing.length === 0) { - console.log('All tasks completed!'); +// Dry run mode +if (flags.dryRun) { + console.log('### Parallel Batches (would execute):\n'); + dag.parallel_batches.forEach((batch, i) => { + console.log(`Batch ${i + 1}: ${batch.join(', ')}`); + }); return; } ``` -### Phase 2: Ready Task Detection +### Phase 2: Dispatch Batches ```javascript -// Find ready tasks (dependencies satisfied) -function getReadyTasks() { - const completedIds = new Set( - queue.tasks.filter(q => q.status === 'completed').map(q => q.item_id) - ); +const parallelLimit = flags.parallel || 3; +const executor = flags.executor || 'codex'; - return queue.tasks.filter(item => { - if (item.status !== 'pending') return false; - return item.depends_on.every(depId => completedIds.has(depId)); - }); -} - -const readyTasks = getReadyTasks(); - -if (readyTasks.length === 0) { - if (executing.length > 0) { - console.log('Tasks are currently executing. Wait for completion.'); - } else { - console.log('No ready tasks. Check for blocked dependencies.'); - } - return; -} - -console.log(`Found ${readyTasks.length} ready tasks`); - -// Sort by execution order -readyTasks.sort((a, b) => a.execution_order - b.execution_order); - -// Initialize TodoWrite +// Initialize TodoWrite for tracking +const allTasks = dag.parallel_batches.flat(); TodoWrite({ - todos: readyTasks.slice(0, parallelLimit).map(t => ({ - content: `[${t.item_id}] ${t.issue_id}:${t.task_id}`, + todos: allTasks.map(id => ({ + content: `Execute ${id}`, status: 'pending', - activeForm: `Executing ${t.item_id}` + activeForm: `Executing ${id}` })) }); + +// Process each batch +for (const [batchIndex, batch] of dag.parallel_batches.entries()) { + console.log(`\n### Batch ${batchIndex + 1}/${dag.parallel_batches.length}`); + console.log(`Tasks: ${batch.join(', ')}`); + + // Dispatch batch with parallelism limit + const chunks = []; + for (let i = 0; i < batch.length; i += parallelLimit) { + chunks.push(batch.slice(i, i + parallelLimit)); + } + + for (const chunk of chunks) { + // Launch executors in parallel + const executions = chunk.map(itemId => { + updateTodo(itemId, 'in_progress'); + return dispatchExecutor(itemId, executor); + }); + + await Promise.all(executions); + chunk.forEach(id => updateTodo(id, 'completed')); + } + + // Refresh DAG for next batch (dependencies may now be satisfied) + const refreshedDag = JSON.parse(Bash(`ccw issue queue dag`).trim()); + if (refreshedDag.ready_count === 0) break; +} ``` -### Phase 3: Codex Coordination (Single Task Mode - Full Lifecycle) +### Executor Dispatch (Minimal Prompt) ```javascript -// Execute tasks - single codex instance per task with full lifecycle -async function executeTask(queueItem) { - const codexPrompt = ` -## Single Task Execution - CLOSED-LOOP LIFECYCLE - -You are executing ONE task from the issue queue. Each task has 5 phases that MUST ALL complete successfully. +function dispatchExecutor(itemId, executorType) { + // Minimal prompt - executor fetches its own task + const prompt = ` +## Execute Task ${itemId} ### Step 1: Fetch Task -Run this command to get your task: \`\`\`bash -ccw issue next +ccw issue next ${itemId} \`\`\` -This returns JSON with full lifecycle definition: -- task.implementation: Implementation steps -- task.test: Test requirements and commands -- task.regression: Regression check commands -- task.acceptance: Acceptance criteria and verification -- task.commit: Commit specification +### Step 2: Execute +Follow the task definition returned by the command above. +The JSON includes: implementation steps, test commands, acceptance criteria, commit spec. -### Step 2: Execute Full Lifecycle - -**Phase 1: IMPLEMENT** -1. Follow task.implementation steps in order -2. Modify files specified in modification_points -3. Use context.relevant_files for reference -4. Use context.patterns for code style - -**Phase 2: TEST** -1. Run test commands from task.test.commands -2. Ensure all unit tests pass (task.test.unit) -3. Run integration tests if specified (task.test.integration) -4. Verify coverage meets task.test.coverage_target if specified -5. If tests fail → fix code and re-run, do NOT proceed until tests pass - -**Phase 3: REGRESSION** -1. Run all commands in task.regression -2. Ensure no existing tests are broken -3. If regression fails → fix and re-run - -**Phase 4: ACCEPTANCE** -1. Verify each criterion in task.acceptance.criteria -2. Execute verification steps in task.acceptance.verification -3. Complete any manual_checks if specified -4. All criteria MUST pass before proceeding - -**Phase 5: COMMIT** -1. Stage all modified files -2. Use task.commit.message_template as commit message -3. Commit with: git commit -m "$(cat <<'EOF'\n\nEOF\n)" -4. If commit_strategy is 'per-task', commit now -5. If commit_strategy is 'atomic' or 'squash', stage but don't commit - -### Step 3: Report Completion -When ALL phases complete successfully: +### Step 3: Report +When done: \`\`\`bash -ccw issue complete --result '{ - "files_modified": ["path1", "path2"], - "tests_passed": true, - "regression_passed": true, - "acceptance_passed": true, - "committed": true, - "commit_hash": "", - "summary": "What was done" -}' +ccw issue done ${itemId} --result '{"summary": "..."}' \`\`\` -If any phase fails and cannot be fixed: +If failed: \`\`\`bash -ccw issue fail --reason "Phase X failed:
" +ccw issue done ${itemId} --fail --reason "..." \`\`\` - -### Rules -- NEVER skip any lifecycle phase -- Tests MUST pass before proceeding to acceptance -- Regression MUST pass before commit -- ALL acceptance criteria MUST be verified -- Report accurate lifecycle status in result - -### Start Now -Begin by running: ccw issue next `; - // Execute codex - const executor = queueItem.assigned_executor || flags.executor || 'codex'; - - if (executor === 'codex') { - Bash( - `ccw cli -p "${escapePrompt(codexPrompt)}" --tool codex --mode write --id exec-${queueItem.item_id}`, - timeout=3600000 // 1 hour timeout + if (executorType === 'codex') { + return Bash( + `ccw cli -p "${escapePrompt(prompt)}" --tool codex --mode write --id exec-${itemId}`, + { timeout: 3600000, run_in_background: true } ); - } else if (executor === 'gemini') { - Bash( - `ccw cli -p "${escapePrompt(codexPrompt)}" --tool gemini --mode write --id exec-${queueItem.item_id}`, - timeout=1800000 // 30 min timeout + } else if (executorType === 'gemini') { + return Bash( + `ccw cli -p "${escapePrompt(prompt)}" --tool gemini --mode write --id exec-${itemId}`, + { timeout: 1800000, run_in_background: true } ); } else { - // Agent execution - Task( - subagent_type="code-developer", - run_in_background=false, - description=`Execute ${queueItem.item_id}`, - prompt=codexPrompt - ); - } -} - -// Execute with parallelism -const parallelLimit = flags.parallel || 1; - -for (let i = 0; i < readyTasks.length; i += parallelLimit) { - const batch = readyTasks.slice(i, i + parallelLimit); - - console.log(`\n### Executing Batch ${Math.floor(i / parallelLimit) + 1}`); - console.log(batch.map(t => `- ${t.item_id}: ${t.issue_id}:${t.task_id}`).join('\n')); - - if (parallelLimit === 1) { - // Sequential execution - for (const task of batch) { - updateTodo(task.item_id, 'in_progress'); - await executeTask(task); - updateTodo(task.item_id, 'completed'); - } - } else { - // Parallel execution - launch all at once - const executions = batch.map(task => { - updateTodo(task.item_id, 'in_progress'); - return executeTask(task); + return Task({ + subagent_type: 'code-developer', + run_in_background: false, + description: `Execute ${itemId}`, + prompt: prompt }); - await Promise.all(executions); - batch.forEach(task => updateTodo(task.item_id, 'completed')); - } - - // Refresh ready tasks after batch - const newReady = getReadyTasks(); - if (newReady.length > 0) { - console.log(`${newReady.length} more tasks now ready`); } } ``` -### Codex Task Fetch Response - -When codex calls `ccw issue next`, it receives: - -```json -{ - "item_id": "T-1", - "issue_id": "GH-123", - "solution_id": "SOL-001", - "task": { - "id": "T1", - "title": "Create auth middleware", - "scope": "src/middleware/", - "action": "Create", - "description": "Create JWT validation middleware", - "modification_points": [ - { "file": "src/middleware/auth.ts", "target": "new file", "change": "Create middleware" } - ], - "implementation": [ - "Create auth.ts file in src/middleware/", - "Implement JWT token validation using jsonwebtoken", - "Add error handling for invalid/expired tokens", - "Export middleware function" - ], - "acceptance": [ - "Middleware validates JWT tokens successfully", - "Returns 401 for invalid or missing tokens", - "Passes token payload to request context" - ] - }, - "context": { - "relevant_files": ["src/config/auth.ts", "src/types/auth.d.ts"], - "patterns": "Follow existing middleware pattern in src/middleware/logger.ts" - }, - "execution_hints": { - "executor": "codex", - "estimated_minutes": 30 - } -} -``` - -### Phase 4: Completion Summary +### Phase 3: Summary ```javascript -// Reload queue for final status via CLI -const finalQueueJson = Bash(`ccw issue status --json 2>/dev/null || echo '{}'`); -const finalQueue = JSON.parse(finalQueueJson); - -// Use queue._metadata for summary (already calculated by CLI) -const summary = finalQueue._metadata || { - completed_count: 0, - failed_count: 0, - pending_count: 0, - total_tasks: 0 -}; +// Get final status +const finalDag = JSON.parse(Bash(`ccw issue queue dag`).trim()); console.log(` ## Execution Complete -**Completed**: ${summary.completed_count}/${summary.total_tasks} -**Failed**: ${summary.failed_count} -**Pending**: ${summary.pending_count} +- Completed: ${finalDag.completed_count}/${finalDag.total} +- Remaining: ${finalDag.ready_count} -### Task Results -${(finalQueue.tasks || []).map(q => { - const icon = q.status === 'completed' ? '✓' : - q.status === 'failed' ? '✗' : - q.status === 'executing' ? '⟳' : '○'; - return `${icon} ${q.item_id} [${q.issue_id}:${q.task_id}] - ${q.status}`; +### Task Status +${finalDag.nodes.map(n => { + const icon = n.status === 'completed' ? '✓' : + n.status === 'failed' ? '✗' : + n.status === 'executing' ? '⟳' : '○'; + return `${icon} ${n.id} [${n.issue_id}:${n.task_id}] - ${n.status}`; }).join('\n')} `); -// Issue status updates are handled by ccw issue complete/fail endpoints -// No need to manually update issues.jsonl here - -if (summary.pending_count > 0) { - console.log(` -### Continue Execution -Run \`/issue:execute\` again to execute remaining tasks. -`); +if (finalDag.ready_count > 0) { + console.log('\nRun `/issue:execute` again for remaining tasks.'); } ``` -## Dry Run Mode +## CLI Endpoint Contract -```javascript -if (flags.dryRun) { - console.log(` -## Dry Run - Would Execute - -${readyTasks.map((t, i) => ` -${i + 1}. ${t.item_id} - Issue: ${t.issue_id} - Task: ${t.task_id} - Executor: ${t.assigned_executor} - Group: ${t.execution_group} -`).join('')} - -No changes made. Remove --dry-run to execute. -`); - return; +### `ccw issue queue dag` +Returns dependency graph with parallel batches: +```json +{ + "queue_id": "QUE-...", + "total": 10, + "ready_count": 3, + "completed_count": 2, + "nodes": [{ "id": "T-1", "status": "pending", "ready": true, ... }], + "edges": [{ "from": "T-1", "to": "T-2" }], + "parallel_batches": [["T-1", "T-3"], ["T-2"]], + "_summary": { "can_parallel": 2, "batches_needed": 2 } } ``` +### `ccw issue next ` +Returns full task definition for the specified item: +```json +{ + "item_id": "T-1", + "issue_id": "GH-123", + "task": { "id": "T1", "title": "...", "implementation": [...], ... }, + "context": { "relevant_files": [...] } +} +``` + +### `ccw issue done ` +Marks task completed/failed and updates queue state. + ## Error Handling | Error | Resolution | |-------|------------| -| Queue not found | Display message, suggest /issue:queue | -| No ready tasks | Check dependencies, show blocked tasks | -| Codex timeout | Mark as failed, allow retry | -| ccw issue next empty | All tasks done or blocked | -| Task execution failure | Marked via ccw issue fail, use `ccw issue retry` to reset | +| No queue | Run /issue:queue first | +| No ready tasks | Dependencies blocked, check DAG | +| Executor timeout | Marked as executing, can resume | +| Task failure | Use `ccw issue retry` to reset | ## Troubleshooting -### Interrupted Tasks - -If execution was interrupted (crashed/stopped), `ccw issue next` will automatically resume: - +### Check DAG Status ```bash -# Automatically returns the executing task for resumption -ccw issue next +ccw issue queue dag | jq '.parallel_batches' ``` -Tasks in `executing` status are prioritized and returned first, no manual reset needed. - -### Failed Tasks - -If a task failed and you want to retry: +### Resume Interrupted Execution +Executors in `executing` status will be resumed automatically when calling `ccw issue next `. +### Retry Failed Tasks ```bash -# Reset all failed tasks to pending -ccw issue retry - -# Reset failed tasks for specific issue -ccw issue retry +ccw issue retry # Reset all failed to pending +/issue:execute # Re-execute ``` -## Endpoint Contract - -### `ccw issue next` -- Returns next ready task as JSON -- Marks task as 'executing' -- Returns `{ status: 'empty' }` when no tasks - -### `ccw issue complete ` -- Marks task as 'completed' -- Updates queue.json -- Checks if issue is fully complete - -### `ccw issue fail ` -- Marks task as 'failed' -- Records failure reason -- Allows retry via /issue:execute - -### `ccw issue retry [issue-id]` -- Resets failed tasks to 'pending' -- Allows re-execution via `ccw issue next` - ## Related Commands - `/issue:plan` - Plan issues with solutions - `/issue:queue` - Form execution queue -- `ccw issue queue list` - View queue status -- `ccw issue retry` - Retry failed tasks +- `ccw issue queue dag` - View dependency graph +- `ccw issue retry` - Reset failed tasks diff --git a/ccw/src/commands/issue.ts b/ccw/src/commands/issue.ts index 4a93e3b7..3a29ea58 100644 --- a/ccw/src/commands/issue.ts +++ b/ccw/src/commands/issue.ts @@ -845,6 +845,101 @@ async function queueAction(subAction: string | undefined, issueId: string | unde return; } + // DAG - Return dependency graph for parallel execution planning + if (subAction === 'dag') { + const queue = readActiveQueue(); + + if (!queue.id || queue.tasks.length === 0) { + console.log(JSON.stringify({ error: 'No active queue', nodes: [], edges: [], groups: [] })); + return; + } + + // Build DAG nodes + const completedIds = new Set(queue.tasks.filter(t => t.status === 'completed').map(t => t.item_id)); + const failedIds = new Set(queue.tasks.filter(t => t.status === 'failed').map(t => t.item_id)); + + const nodes = queue.tasks.map(task => ({ + id: task.item_id, + issue_id: task.issue_id, + task_id: task.task_id, + status: task.status, + executor: task.assigned_executor, + priority: task.semantic_priority, + depends_on: task.depends_on, + // Calculate if ready (dependencies satisfied) + ready: task.status === 'pending' && task.depends_on.every(d => completedIds.has(d)), + blocked_by: task.depends_on.filter(d => !completedIds.has(d) && !failedIds.has(d)) + })); + + // Build edges for visualization + const edges = queue.tasks.flatMap(task => + task.depends_on.map(dep => ({ from: dep, to: task.item_id })) + ); + + // Group ready tasks by execution_group for parallel execution + const readyTasks = nodes.filter(n => n.ready || n.status === 'executing'); + const groups: Record = {}; + + for (const task of queue.tasks) { + if (readyTasks.some(r => r.id === task.item_id)) { + const group = task.execution_group || 'P1'; + if (!groups[group]) groups[group] = []; + groups[group].push(task.item_id); + } + } + + // Calculate parallel batches (tasks with no dependencies on each other) + const parallelBatches: string[][] = []; + const remainingReady = new Set(readyTasks.map(t => t.id)); + + while (remainingReady.size > 0) { + const batch: string[] = []; + const batchFiles = new Set(); + + for (const taskId of remainingReady) { + const task = queue.tasks.find(t => t.item_id === taskId); + if (!task) continue; + + // Check for file conflicts with already-batched tasks + const solution = findSolution(task.issue_id, task.solution_id); + const taskDef = solution?.tasks.find(t => t.id === task.task_id); + const taskFiles = taskDef?.modification_points?.map(mp => mp.file) || []; + + const hasConflict = taskFiles.some(f => batchFiles.has(f)); + + if (!hasConflict) { + batch.push(taskId); + taskFiles.forEach(f => batchFiles.add(f)); + } + } + + if (batch.length === 0) { + // Fallback: take one at a time if all conflict + const first = Array.from(remainingReady)[0]; + batch.push(first); + } + + parallelBatches.push(batch); + batch.forEach(id => remainingReady.delete(id)); + } + + console.log(JSON.stringify({ + queue_id: queue.id, + total: nodes.length, + ready_count: readyTasks.length, + completed_count: completedIds.size, + nodes, + edges, + groups: Object.entries(groups).map(([id, tasks]) => ({ id, tasks })), + parallel_batches: parallelBatches, + _summary: { + can_parallel: parallelBatches[0]?.length || 0, + batches_needed: parallelBatches.length + } + }, null, 2)); + return; + } + // Archive current queue if (subAction === 'archive') { const queue = readActiveQueue(); @@ -998,39 +1093,56 @@ async function queueAction(subAction: string | undefined, issueId: string | unde /** * next - Get next ready task for execution (JSON output) + * Accepts optional item_id to fetch a specific task directly */ -async function nextAction(options: IssueOptions): Promise { +async function nextAction(itemId: string | undefined, options: IssueOptions): Promise { const queue = readActiveQueue(); + let nextItem: typeof queue.tasks[0] | undefined; + let isResume = false; - // Priority 1: Resume executing tasks (interrupted/crashed) - const executingTasks = queue.tasks.filter(item => item.status === 'executing'); - - // Priority 2: Find pending tasks with satisfied dependencies - const pendingTasks = queue.tasks.filter(item => { - if (item.status !== 'pending') return false; - return item.depends_on.every(depId => { - const dep = queue.tasks.find(q => q.item_id === depId); - return !dep || dep.status === 'completed'; + // If specific item_id provided, fetch that task directly + if (itemId) { + nextItem = queue.tasks.find(t => t.item_id === itemId); + if (!nextItem) { + console.log(JSON.stringify({ status: 'error', message: `Task ${itemId} not found` })); + return; + } + if (nextItem.status === 'completed') { + console.log(JSON.stringify({ status: 'completed', message: `Task ${itemId} already completed` })); + return; + } + if (nextItem.status === 'failed') { + console.log(JSON.stringify({ status: 'failed', message: `Task ${itemId} failed, use retry to reset` })); + return; + } + isResume = nextItem.status === 'executing'; + } else { + // Auto-select: Priority 1 - executing, Priority 2 - ready pending + const executingTasks = queue.tasks.filter(item => item.status === 'executing'); + const pendingTasks = queue.tasks.filter(item => { + if (item.status !== 'pending') return false; + return item.depends_on.every(depId => { + const dep = queue.tasks.find(q => q.item_id === depId); + return !dep || dep.status === 'completed'; + }); }); - }); - // Combine: executing first, then pending - const readyTasks = [...executingTasks, ...pendingTasks]; + const readyTasks = [...executingTasks, ...pendingTasks]; - if (readyTasks.length === 0) { - console.log(JSON.stringify({ - status: 'empty', - message: 'No ready tasks', - queue_status: queue._metadata - }, null, 2)); - return; + if (readyTasks.length === 0) { + console.log(JSON.stringify({ + status: 'empty', + message: 'No ready tasks', + queue_status: queue._metadata + }, null, 2)); + return; + } + + readyTasks.sort((a, b) => a.execution_order - b.execution_order); + nextItem = readyTasks[0]; + isResume = nextItem.status === 'executing'; } - // Sort by execution order - readyTasks.sort((a, b) => a.execution_order - b.execution_order); - const nextItem = readyTasks[0]; - const isResume = nextItem.status === 'executing'; - // Load task definition const solution = findSolution(nextItem.issue_id, nextItem.solution_id); const taskDef = solution?.tasks.find(t => t.id === nextItem.task_id); @@ -1054,8 +1166,8 @@ async function nextAction(options: IssueOptions): Promise { total: queue.tasks.length, completed: queue.tasks.filter(q => q.status === 'completed').length, failed: queue.tasks.filter(q => q.status === 'failed').length, - executing: executingTasks.length, - pending: pendingTasks.length + executing: queue.tasks.filter(q => q.status === 'executing').length, + pending: queue.tasks.filter(q => q.status === 'pending').length }; const remaining = stats.pending + stats.executing; @@ -1219,7 +1331,7 @@ export async function issueCommand( await queueAction(argsArray[0], argsArray[1], options); break; case 'next': - await nextAction(options); + await nextAction(argsArray[0], options); break; case 'done': await doneAction(argsArray[0], options); @@ -1252,14 +1364,15 @@ export async function issueCommand( console.log(chalk.gray(' queue list List all queues (history)')); console.log(chalk.gray(' queue add Add issue to active queue (or create new)')); console.log(chalk.gray(' queue switch Switch active queue')); + console.log(chalk.gray(' queue dag Get dependency graph (JSON) for parallel execution')); console.log(chalk.gray(' queue archive Archive current queue')); console.log(chalk.gray(' queue delete Delete queue from history')); console.log(chalk.gray(' retry [issue-id] Retry failed tasks')); console.log(); console.log(chalk.bold('Execution Endpoints:')); - console.log(chalk.gray(' next Get next ready task (JSON)')); - console.log(chalk.gray(' done Mark task completed')); - console.log(chalk.gray(' done --fail Mark task failed')); + console.log(chalk.gray(' next [item-id] Get task by ID or next ready task (JSON)')); + console.log(chalk.gray(' done Mark task completed')); + console.log(chalk.gray(' done --fail Mark task failed')); console.log(); console.log(chalk.bold('Options:')); console.log(chalk.gray(' --title Issue/task title'));