From b58589ddadd42c4e65635f5fb857885bd04f17de Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 27 Dec 2025 22:04:15 +0800 Subject: [PATCH] refactor: Update issue queue structure and commands - Changed queue structure from 'queue' to 'tasks' in various files for clarity. - Updated CLI commands to reflect new task ID usage instead of queue ID. - Enhanced queue management with new delete functionality for historical queues. - Improved metadata handling and task execution tracking. - Updated dashboard and issue manager views to accommodate new task structure. - Bumped version to 6.3.8 in package.json and package-lock.json. --- .claude/agents/issue-plan-agent.md | 53 ++++- .claude/agents/issue-queue-agent.md | 22 +- .claude/commands/issue/execute.md | 149 +++++++------- .claude/commands/issue/manage.md | 4 +- .claude/commands/issue/plan.md | 175 +++++++--------- .claude/commands/issue/queue.md | 27 ++- .codex/prompts/issue-execute.md | 22 +- ccw/src/cli.ts | 1 + ccw/src/commands/issue.ts | 191 +++++++++++------- ccw/src/core/routes/issue-routes.ts | 52 +++-- .../dashboard-js/views/issue-manager.js | 28 +-- package-lock.json | 4 +- package.json | 2 +- 13 files changed, 394 insertions(+), 336 deletions(-) diff --git a/.claude/agents/issue-plan-agent.md b/.claude/agents/issue-plan-agent.md index cf7a1a1c..b0806790 100644 --- a/.claude/agents/issue-plan-agent.md +++ b/.claude/agents/issue-plan-agent.md @@ -20,14 +20,7 @@ You are a specialized issue planning agent that combines exploration and plannin ```javascript { // Required - issues: [ - { - id: string, // Issue ID (e.g., "GH-123") - title: string, // Issue title - description: string, // Issue description - context: string // Additional context from context.md - } - ], + issue_ids: string[], // Issue IDs only (e.g., ["GH-123", "GH-124"]) project_root: string, // Project root path for ACE search // Optional @@ -36,6 +29,8 @@ You are a specialized issue planning agent that combines exploration and plannin } ``` +**Note**: Agent receives IDs only. Use `ccw issue status --json` to fetch full details. + ## Schema-Driven Output **CRITICAL**: Read the solution schema first to determine output structure: @@ -65,6 +60,31 @@ Phase 4: Validation & Output (15%) ## Phase 1: Issue Understanding +### Step 1: Fetch Issue Details via CLI + +For each issue ID received, fetch full details: + +```bash +ccw issue status --json +``` + +Returns: +```json +{ + "issue": { + "id": "GH-123", + "title": "Add authentication", + "context": "...", + "affected_components": ["auth", "api"], + "lifecycle_requirements": { "test_strategy": "unit", "regression_scope": "affected" } + }, + "solutions": [], + "bound": null +} +``` + +### Step 2: Analyze Issue + **Extract from each issue**: - Title and description analysis - Key requirements and constraints @@ -661,6 +681,23 @@ function generateOutput(solutions, conflicts) { } ``` +### Solution Registration via CLI + +**IMPORTANT**: Register solutions using CLI instead of direct file writes: + +```bash +# 1. Write solution JSON to temp file +echo '' > /tmp/sol-{issue-id}.json + +# 2. Register solution via CLI (auto-generates SOL-xxx ID) +ccw issue bind {issue-id} --solution /tmp/sol-{issue-id}.json +``` + +**CLI Output**: Returns registered solution ID for summary: +``` +✓ Solution SOL-20251227-001 registered (5 tasks) +``` + ### Solution Schema (Closed-Loop Tasks) Each task MUST include ALL 5 lifecycle phases: diff --git a/.claude/agents/issue-queue-agent.md b/.claude/agents/issue-queue-agent.md index a5e91c2d..008b5db4 100644 --- a/.claude/agents/issue-queue-agent.md +++ b/.claude/agents/issue-queue-agent.md @@ -500,35 +500,35 @@ function canRunParallel(taskKey, groupTasks, taskGraph, conflicts) { ```javascript function generateQueueItems(orderedTasks, taskGraph, conflicts) { const queueItems = [] - let queueIdCounter = 1 + let itemIdCounter = 1 for (const key of orderedTasks) { const node = taskGraph.get(key) queueItems.push({ - queue_id: `Q-${String(queueIdCounter++).padStart(3, '0')}`, + item_id: `T-${itemIdCounter++}`, issue_id: node.issue_id, solution_id: node.solution_id, task_id: node.task.id, status: 'pending', execution_order: node.execution_order, execution_group: node.execution_group, - depends_on: mapDependenciesToQueueIds(node, queueItems), + depends_on: mapDependenciesToItemIds(node, queueItems), semantic_priority: node.semantic_priority, - queued_at: new Date().toISOString() + assigned_executor: node.task.executor || 'codex' }) } return queueItems } -function mapDependenciesToQueueIds(node, queueItems) { +function mapDependenciesToItemIds(node, queueItems) { return (node.task.depends_on || []).map(dep => { const depKey = `${node.issue_id}:${dep}` const queueItem = queueItems.find(q => q.issue_id === node.issue_id && q.task_id === dep ) - return queueItem?.queue_id || dep + return queueItem?.item_id || dep }) } ``` @@ -538,7 +538,7 @@ function mapDependenciesToQueueIds(node, queueItems) { ```javascript function generateOutput(queueItems, conflicts, groups) { return { - queue: queueItems, + tasks: queueItems, conflicts: conflicts.map(c => ({ type: c.type, file: c.file, @@ -652,10 +652,10 @@ function validateOrdering(queueItems, taskGraph) { const node = taskGraph.get(key) // Check dependencies come before - for (const depQueueId of item.depends_on) { - const depItem = queueItems.find(q => q.queue_id === depQueueId) + for (const depItemId of item.depends_on) { + const depItem = queueItems.find(q => q.item_id === depItemId) if (depItem && depItem.execution_order >= item.execution_order) { - errors.push(`${item.queue_id} ordered before dependency ${depQueueId}`) + errors.push(`${item.item_id} ordered before dependency ${depItemId}`) } } } @@ -690,7 +690,7 @@ function validateOrdering(queueItems, taskGraph) { 5. Calculate semantic priority for all tasks 6. Validate ordering before output 7. Include rationale for conflict resolutions -8. Map depends_on to queue_ids in output +8. Map depends_on to item_ids in output **NEVER**: 1. Execute tasks (ordering only) diff --git a/.claude/commands/issue/execute.md b/.claude/commands/issue/execute.md index c417a9cc..1b0fd6d9 100644 --- a/.claude/commands/issue/execute.md +++ b/.claude/commands/issue/execute.md @@ -17,12 +17,14 @@ Execution orchestrator that coordinates codex instances. Each task is executed b - No file reading in codex - Orchestrator manages parallelism -## Storage Structure (Flat JSONL) +## Storage Structure (Queue History) ``` .workflow/issues/ ├── issues.jsonl # All issues (one per line) -├── queue.json # Execution queue +├── queues/ # Queue history directory +│ ├── index.json # Queue index (active + history) +│ └── {queue-id}.json # Individual queue files └── solutions/ ├── {issue-id}.jsonl # Solutions for issue └── ... @@ -78,19 +80,19 @@ Phase 4: Completion ### Phase 1: Queue Loading ```javascript -// Load queue -const queuePath = '.workflow/issues/queue.json'; -if (!Bash(`test -f "${queuePath}" && echo exists`).includes('exists')) { - console.log('No queue found. Run /issue:queue first.'); +// Load active queue via CLI endpoint +const queueJson = Bash(`ccw issue status --json 2>/dev/null || echo '{}'`); +const queue = JSON.parse(queueJson); + +if (!queue.id || queue.tasks?.length === 0) { + console.log('No active queue found. Run /issue:queue first.'); return; } -const queue = JSON.parse(Read(queuePath)); - // Count by status -const pending = queue.queue.filter(q => q.status === 'pending'); -const executing = queue.queue.filter(q => q.status === 'executing'); -const completed = queue.queue.filter(q => q.status === 'completed'); +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 @@ -98,7 +100,7 @@ console.log(` - Pending: ${pending.length} - Executing: ${executing.length} - Completed: ${completed.length} -- Total: ${queue.queue.length} +- Total: ${queue.tasks.length} `); if (pending.length === 0 && executing.length === 0) { @@ -113,10 +115,10 @@ if (pending.length === 0 && executing.length === 0) { // Find ready tasks (dependencies satisfied) function getReadyTasks() { const completedIds = new Set( - queue.queue.filter(q => q.status === 'completed').map(q => q.queue_id) + queue.tasks.filter(q => q.status === 'completed').map(q => q.item_id) ); - return queue.queue.filter(item => { + return queue.tasks.filter(item => { if (item.status !== 'pending') return false; return item.depends_on.every(depId => completedIds.has(depId)); }); @@ -141,9 +143,9 @@ readyTasks.sort((a, b) => a.execution_order - b.execution_order); // Initialize TodoWrite TodoWrite({ todos: readyTasks.slice(0, parallelLimit).map(t => ({ - content: `[${t.queue_id}] ${t.issue_id}:${t.task_id}`, + content: `[${t.item_id}] ${t.issue_id}:${t.task_id}`, status: 'pending', - activeForm: `Executing ${t.queue_id}` + activeForm: `Executing ${t.item_id}` })) }); ``` @@ -207,7 +209,7 @@ This returns JSON with full lifecycle definition: ### Step 3: Report Completion When ALL phases complete successfully: \`\`\`bash -ccw issue complete --result '{ +ccw issue complete --result '{ "files_modified": ["path1", "path2"], "tests_passed": true, "regression_passed": true, @@ -220,7 +222,7 @@ ccw issue complete --result '{ If any phase fails and cannot be fixed: \`\`\`bash -ccw issue fail --reason "Phase X failed:
" +ccw issue fail --reason "Phase X failed:
" \`\`\` ### Rules @@ -239,12 +241,12 @@ Begin by running: ccw issue next if (executor === 'codex') { Bash( - `ccw cli -p "${escapePrompt(codexPrompt)}" --tool codex --mode write --id exec-${queueItem.queue_id}`, + `ccw cli -p "${escapePrompt(codexPrompt)}" --tool codex --mode write --id exec-${queueItem.item_id}`, timeout=3600000 // 1 hour timeout ); } else if (executor === 'gemini') { Bash( - `ccw cli -p "${escapePrompt(codexPrompt)}" --tool gemini --mode write --id exec-${queueItem.queue_id}`, + `ccw cli -p "${escapePrompt(codexPrompt)}" --tool gemini --mode write --id exec-${queueItem.item_id}`, timeout=1800000 // 30 min timeout ); } else { @@ -252,7 +254,7 @@ Begin by running: ccw issue next Task( subagent_type="code-developer", run_in_background=false, - description=`Execute ${queueItem.queue_id}`, + description=`Execute ${queueItem.item_id}`, prompt=codexPrompt ); } @@ -265,23 +267,23 @@ 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.queue_id}: ${t.issue_id}:${t.task_id}`).join('\n')); + 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.queue_id, 'in_progress'); + updateTodo(task.item_id, 'in_progress'); await executeTask(task); - updateTodo(task.queue_id, 'completed'); + updateTodo(task.item_id, 'completed'); } } else { // Parallel execution - launch all at once const executions = batch.map(task => { - updateTodo(task.queue_id, 'in_progress'); + updateTodo(task.item_id, 'in_progress'); return executeTask(task); }); await Promise.all(executions); - batch.forEach(task => updateTodo(task.queue_id, 'completed')); + batch.forEach(task => updateTodo(task.item_id, 'completed')); } // Refresh ready tasks after batch @@ -298,7 +300,7 @@ When codex calls `ccw issue next`, it receives: ```json { - "queue_id": "Q-001", + "item_id": "T-1", "issue_id": "GH-123", "solution_id": "SOL-001", "task": { @@ -336,60 +338,38 @@ When codex calls `ccw issue next`, it receives: ### Phase 4: Completion Summary ```javascript -// Reload queue for final status -const finalQueue = JSON.parse(Read(queuePath)); +// Reload queue for final status via CLI +const finalQueueJson = Bash(`ccw issue status --json 2>/dev/null || echo '{}'`); +const finalQueue = JSON.parse(finalQueueJson); -const summary = { - completed: finalQueue.queue.filter(q => q.status === 'completed').length, - failed: finalQueue.queue.filter(q => q.status === 'failed').length, - pending: finalQueue.queue.filter(q => q.status === 'pending').length, - total: finalQueue.queue.length +// 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 }; console.log(` ## Execution Complete -**Completed**: ${summary.completed}/${summary.total} -**Failed**: ${summary.failed} -**Pending**: ${summary.pending} +**Completed**: ${summary.completed_count}/${summary.total_tasks} +**Failed**: ${summary.failed_count} +**Pending**: ${summary.pending_count} ### Task Results -${finalQueue.queue.map(q => { +${(finalQueue.tasks || []).map(q => { const icon = q.status === 'completed' ? '✓' : q.status === 'failed' ? '✗' : q.status === 'executing' ? '⟳' : '○'; - return `${icon} ${q.queue_id} [${q.issue_id}:${q.task_id}] - ${q.status}`; + return `${icon} ${q.item_id} [${q.issue_id}:${q.task_id}] - ${q.status}`; }).join('\n')} `); -// Update issue statuses in issues.jsonl -const issuesPath = '.workflow/issues/issues.jsonl'; -const allIssues = Bash(`cat "${issuesPath}"`) - .split('\n') - .filter(line => line.trim()) - .map(line => JSON.parse(line)); +// Issue status updates are handled by ccw issue complete/fail endpoints +// No need to manually update issues.jsonl here -const issueIds = [...new Set(finalQueue.queue.map(q => q.issue_id))]; -for (const issueId of issueIds) { - const issueTasks = finalQueue.queue.filter(q => q.issue_id === issueId); - - if (issueTasks.every(q => q.status === 'completed')) { - console.log(`\n✓ Issue ${issueId} fully completed!`); - - // Update issue status - const issueIndex = allIssues.findIndex(i => i.id === issueId); - if (issueIndex !== -1) { - allIssues[issueIndex].status = 'completed'; - allIssues[issueIndex].completed_at = new Date().toISOString(); - allIssues[issueIndex].updated_at = new Date().toISOString(); - } - } -} - -// Write updated issues.jsonl -Write(issuesPath, allIssues.map(i => JSON.stringify(i)).join('\n')); - -if (summary.pending > 0) { +if (summary.pending_count > 0) { console.log(` ### Continue Execution Run \`/issue:execute\` again to execute remaining tasks. @@ -405,7 +385,7 @@ if (flags.dryRun) { ## Dry Run - Would Execute ${readyTasks.map((t, i) => ` -${i + 1}. ${t.queue_id} +${i + 1}. ${t.item_id} Issue: ${t.issue_id} Task: ${t.task_id} Executor: ${t.assigned_executor} @@ -426,7 +406,32 @@ No changes made. Remove --dry-run to execute. | 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 | +| Task execution failure | Marked via ccw issue fail, use `ccw issue retry` to reset | + +## Troubleshooting + +### Interrupted Tasks + +If execution was interrupted (crashed/stopped), `ccw issue next` will automatically resume: + +```bash +# Automatically returns the executing task for resumption +ccw issue next +``` + +Tasks in `executing` status are prioritized and returned first, no manual reset needed. + +### Failed Tasks + +If a task failed and you want to retry: + +```bash +# Reset all failed tasks to pending +ccw issue retry + +# Reset failed tasks for specific issue +ccw issue retry +``` ## Endpoint Contract @@ -435,16 +440,20 @@ No changes made. Remove --dry-run to execute. - Marks task as 'executing' - Returns `{ status: 'empty' }` when no tasks -### `ccw issue complete ` +### `ccw issue complete ` - Marks task as 'completed' - Updates queue.json - Checks if issue is fully complete -### `ccw issue fail ` +### `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 diff --git a/.claude/commands/issue/manage.md b/.claude/commands/issue/manage.md index eb5794e3..8ce26dbf 100644 --- a/.claude/commands/issue/manage.md +++ b/.claude/commands/issue/manage.md @@ -30,7 +30,7 @@ ccw issue task --title "..." # Add task ccw issue queue # List queue ccw issue queue add # Add to queue ccw issue next # Get next task -ccw issue done # Complete task +ccw issue complete # Complete task ``` ## Usage @@ -561,7 +561,7 @@ async function deleteIssueInteractive(issueId) { const queuePath = '.workflow/issues/queue.json'; if (Bash(`test -f "${queuePath}" && echo exists`) === 'exists') { const queue = JSON.parse(Bash(`cat "${queuePath}"`)); - queue.queue = queue.queue.filter(q => q.issue_id !== issueId); + queue.tasks = queue.tasks.filter(q => q.issue_id !== issueId); Write(queuePath, JSON.stringify(queue, null, 2)); } diff --git a/.claude/commands/issue/plan.md b/.claude/commands/issue/plan.md index d8e08650..efd8165e 100644 --- a/.claude/commands/issue/plan.md +++ b/.claude/commands/issue/plan.md @@ -75,26 +75,16 @@ Phase 4: Summary ## Implementation -### Phase 1: Issue Loading +### Phase 1: Issue Loading (IDs Only) ```javascript -// Parse input and flags -const issuesPath = '.workflow/issues/issues.jsonl'; const batchSize = flags.batchSize || 3; - -// Key fields for planning (avoid loading full issue data) -const PLAN_FIELDS = 'id,title,status,context,affected_components,lifecycle_requirements,priority,bound_solution_id'; - let issueIds = []; if (flags.allPending) { - // Use jq to filter pending/registered issues - extract only IDs - const pendingIds = Bash(` - cat "${issuesPath}" 2>/dev/null | \\ - jq -r 'select(.status == "pending" or .status == "registered") | .id' 2>/dev/null || echo '' - `).trim(); - - issueIds = pendingIds ? pendingIds.split('\n').filter(Boolean) : []; + // Get pending issue IDs directly via CLI + const ids = Bash(`ccw issue list --status pending,registered --ids`).trim(); + issueIds = ids ? ids.split('\n').filter(Boolean) : []; if (issueIds.length === 0) { console.log('No pending issues found.'); @@ -106,50 +96,27 @@ if (flags.allPending) { issueIds = userInput.includes(',') ? userInput.split(',').map(s => s.trim()) : [userInput.trim()]; -} -// Load issues using jq to extract only key fields -const issues = []; -for (const id of issueIds) { - // Use jq to find issue by ID and extract only needed fields - const issueJson = Bash(` - cat "${issuesPath}" 2>/dev/null | \\ - jq -c 'select(.id == "${id}") | {${PLAN_FIELDS}}' 2>/dev/null | head -1 - `).trim(); - - let issue; - if (issueJson) { - issue = JSON.parse(issueJson); - } else { - console.log(`Issue ${id} not found. Creating...`); - issue = { - id, - title: `Issue ${id}`, - status: 'registered', - priority: 3, - context: '', - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - }; - // Append to issues.jsonl - Bash(`echo '${JSON.stringify(issue)}' >> "${issuesPath}"`); + // Create if not exists + for (const id of issueIds) { + Bash(`ccw issue init ${id} --title "Issue ${id}" 2>/dev/null || true`); } - - issues.push(issue); } // Group into batches const batches = []; -for (let i = 0; i < issues.length; i += batchSize) { - batches.push(issues.slice(i, i + batchSize)); +for (let i = 0; i < issueIds.length; i += batchSize) { + batches.push(issueIds.slice(i, i + batchSize)); } -console.log(`Processing ${issues.length} issues in ${batches.length} batch(es)`); +console.log(`Processing ${issueIds.length} issues in ${batches.length} batch(es)`); TodoWrite({ - todos: batches.flatMap((batch, i) => [ - { content: `Plan batch ${i+1}`, status: 'pending', activeForm: `Planning batch ${i+1}` } - ]) + todos: batches.map((_, i) => ({ + content: `Plan batch ${i+1}`, + status: 'pending', + activeForm: `Planning batch ${i+1}` + })) }); ``` @@ -162,36 +129,47 @@ Bash(`mkdir -p .workflow/issues/solutions`); for (const [batchIndex, batch] of batches.entries()) { updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress'); - // Build issue prompt for agent - agent writes solutions directly + // Build issue prompt for agent - pass IDs only, agent fetches details const issuePrompt = ` ## Issues to Plan (Closed-Loop Tasks Required) -${batch.map((issue, i) => ` -### Issue ${i + 1}: ${issue.id} -**Title**: ${issue.title} -**Context**: ${issue.context || 'No context provided'} -**Affected Components**: ${issue.affected_components?.join(', ') || 'Not specified'} +**Issue IDs**: ${batch.join(', ')} -**Lifecycle Requirements**: -- Test Strategy: ${issue.lifecycle_requirements?.test_strategy || 'auto'} -- Regression Scope: ${issue.lifecycle_requirements?.regression_scope || 'affected'} -- Commit Strategy: ${issue.lifecycle_requirements?.commit_strategy || 'per-task'} -`).join('\n')} +### Step 1: Fetch Issue Details +For each issue ID, use CLI to get full details: +\`\`\`bash +ccw issue status --json +\`\`\` + +Returns: +\`\`\`json +{ + "issue": { "id", "title", "context", "affected_components", "lifecycle_requirements", ... }, + "solutions": [...], + "bound": null +} +\`\`\` ## Project Root ${process.cwd()} ## Output Requirements -**IMPORTANT**: Write solutions DIRECTLY to files, do NOT return full solution content. +**IMPORTANT**: Register solutions via CLI, do NOT write files directly. -### 1. Write Solution Files -For each issue, write solution to: \`.workflow/issues/solutions/{issue-id}.jsonl\` -- Append one JSON line per solution +### 1. Register Solutions via CLI +For each issue, save solution to temp file and register via CLI: +\`\`\`bash +# Write solution JSON to temp file +echo '' > /tmp/sol-{issue-id}.json + +# Register solution via CLI (generates SOL-xxx ID automatically) +ccw issue bind {issue-id} --solution /tmp/sol-{issue-id}.json +\`\`\` - Solution must include all closed-loop task fields (see Solution Format below) ### 2. Return Summary Only -After writing solutions, return ONLY a brief JSON summary: +After registering solutions, return ONLY a brief JSON summary: \`\`\`json { "planned": [ @@ -271,31 +249,34 @@ Each task MUST include ALL lifecycle phases: // Collect issues needing user selection (multiple solutions) const needSelection = []; -for (const issue of issues) { - const solPath = `.workflow/issues/solutions/${issue.id}.jsonl`; +for (const issueId of issueIds) { + // Get solutions via CLI + const statusJson = Bash(`ccw issue status ${issueId} --json 2>/dev/null || echo '{}'`).trim(); + const status = JSON.parse(statusJson); + const solutions = status.solutions || []; - // Use jq to count solutions - const count = parseInt(Bash(`cat "${solPath}" 2>/dev/null | jq -s 'length' 2>/dev/null || echo '0'`).trim()) || 0; + if (solutions.length === 0) continue; // No solutions - skip silently (agent already reported) - if (count === 0) continue; // No solutions - skip silently (agent already reported) - - if (count === 1) { + if (solutions.length === 1) { // Auto-bind single solution - const solId = Bash(`cat "${solPath}" | jq -r '.id' | head -1`).trim(); - bindSolution(issue.id, solId); + bindSolution(issueId, solutions[0].id); } else { // Multiple solutions - collect for batch selection - const options = Bash(`cat "${solPath}" | jq -c '{id, description, task_count: (.tasks | length)}'`).trim(); - needSelection.push({ issue, options: options.split('\n').map(s => JSON.parse(s)) }); + const options = solutions.map(s => ({ + id: s.id, + description: s.description, + task_count: (s.tasks || []).length + })); + needSelection.push({ issueId, options }); } } // Batch ask user for multiple-solution issues if (needSelection.length > 0) { const answer = AskUserQuestion({ - questions: needSelection.map(({ issue, options }) => ({ - question: `Select solution for ${issue.id}:`, - header: issue.id, + questions: needSelection.map(({ issueId, options }) => ({ + question: `Select solution for ${issueId}:`, + header: issueId, multiSelect: false, options: options.map(s => ({ label: `${s.id} (${s.task_count} tasks)`, @@ -305,47 +286,27 @@ if (needSelection.length > 0) { }); // Bind selected solutions - for (const { issue } of needSelection) { - const selectedSolId = extractSelectedSolutionId(answer, issue.id); - if (selectedSolId) bindSolution(issue.id, selectedSolId); + for (const { issueId } of needSelection) { + const selectedSolId = extractSelectedSolutionId(answer, issueId); + if (selectedSolId) bindSolution(issueId, selectedSolId); } } -// Helper: bind solution to issue +// Helper: bind solution to issue (using CLI for safety) function bindSolution(issueId, solutionId) { - const now = new Date().toISOString(); - const solPath = `.workflow/issues/solutions/${issueId}.jsonl`; - - // Update issue status - Bash(` - tmpfile=$(mktemp) && \\ - cat "${issuesPath}" | jq -c 'if .id == "${issueId}" then . + { - bound_solution_id: "${solutionId}", status: "planned", - planned_at: "${now}", updated_at: "${now}" - } else . end' > "$tmpfile" && mv "$tmpfile" "${issuesPath}" - `); - - // Mark solution as bound - Bash(` - tmpfile=$(mktemp) && \\ - cat "${solPath}" | jq -c 'if .id == "${solutionId}" then . + { - is_bound: true, bound_at: "${now}" - } else . + {is_bound: false} end' > "$tmpfile" && mv "$tmpfile" "${solPath}" - `); + Bash(`ccw issue bind ${issueId} ${solutionId}`); } ``` ### Phase 4: Summary ```javascript -// Brief summary using jq -const stats = Bash(` - cat "${issuesPath}" 2>/dev/null | \\ - jq -s '[.[] | select(.status == "planned")] | length' 2>/dev/null || echo '0' -`).trim(); +// Count planned issues via CLI +const plannedIds = Bash(`ccw issue list --status planned --ids`).trim(); +const plannedCount = plannedIds ? plannedIds.split('\n').length : 0; console.log(` -## Done: ${issues.length} issues → ${stats} planned +## Done: ${issueIds.length} issues → ${plannedCount} planned Next: \`/issue:queue\` → \`/issue:execute\` `); diff --git a/.claude/commands/issue/queue.md b/.claude/commands/issue/queue.md index cbdc9f1d..35037aa5 100644 --- a/.claude/commands/issue/queue.md +++ b/.claude/commands/issue/queue.md @@ -77,10 +77,12 @@ Queue formation command using **issue-queue-agent** that analyzes all bound solu # Flags --issue Form queue for specific issue only --append Append issue to active queue (don't create new) ---list List all queues with status ---switch Switch active queue ---archive Archive current queue (mark completed) ---clear Delete a queue from history + +# CLI subcommands (ccw issue queue ...) +ccw issue queue list List all queues with status +ccw issue queue switch Switch active queue +ccw issue queue archive Archive current queue +ccw issue queue delete Delete queue from history ``` ## Execution Process @@ -234,7 +236,7 @@ Write(issuesPath, updatedIssues.map(i => JSON.stringify(i)).join('\n')); console.log(` ## Queue Formed -**Total Tasks**: ${queueOutput.queue.length} +**Total Tasks**: ${queueOutput.tasks.length} **Issues**: ${plannedIssues.length} **Conflicts**: ${queueOutput.conflicts?.length || 0} (${queueOutput._metadata?.resolved_conflicts || 0} resolved) @@ -256,14 +258,13 @@ Output `queues/{queue-id}.json`: ```json { - "id": "QUE-20251227-143000", "name": "Auth Feature Queue", "status": "active", "issue_ids": ["GH-123", "GH-124"], - "queue": [ + "tasks": [ { - "queue_id": "Q-001", + "item_id": "T-1", "issue_id": "GH-123", "solution_id": "SOL-001", "task_id": "T1", @@ -271,8 +272,7 @@ Output `queues/{queue-id}.json`: "execution_order": 1, "execution_group": "P1", "depends_on": [], - "semantic_priority": 0.7, - "queued_at": "2025-12-26T10:00:00Z" + "semantic_priority": 0.7 } ], @@ -289,17 +289,16 @@ Output `queues/{queue-id}.json`: ], "execution_groups": [ - { "id": "P1", "type": "parallel", "task_count": 3, "tasks": ["GH-123:T1", "GH-124:T1", "GH-125:T1"] }, - { "id": "S2", "type": "sequential", "task_count": 2, "tasks": ["GH-123:T2", "GH-124:T2"] } + { "id": "P1", "type": "parallel", "task_count": 3, "tasks": ["T-1", "T-2", "T-3"] }, + { "id": "S2", "type": "sequential", "task_count": 2, "tasks": ["T-4", "T-5"] } ], "_metadata": { - "version": "2.0", + "version": "2.1-optimized", "total_tasks": 5, "pending_count": 3, "completed_count": 2, "failed_count": 0, - "created_at": "2025-12-26T10:00:00Z", "updated_at": "2025-12-26T11:00:00Z", "source": "issue-queue-agent" } diff --git a/.codex/prompts/issue-execute.md b/.codex/prompts/issue-execute.md index d988b248..d9f3b2ab 100644 --- a/.codex/prompts/issue-execute.md +++ b/.codex/prompts/issue-execute.md @@ -21,7 +21,7 @@ WHILE task exists: - TEST: Run task.test commands - VERIFY: Check task.acceptance criteria - COMMIT: Stage files, commit with task.commit.message_template - 3. Report completion via ccw issue complete + 3. Report completion via ccw issue complete 4. Fetch next task via ccw issue next WHEN queue empty: @@ -37,7 +37,7 @@ ccw issue next ``` This returns JSON with the full task definition: -- `queue_id`: Unique ID for queue tracking (e.g., "Q-001") +- `item_id`: Unique task identifier in queue (e.g., "T-1") - `issue_id`: Parent issue ID (e.g., "ISSUE-20251227-001") - `task`: Full task definition with implementation steps - `context`: Relevant files and patterns @@ -51,7 +51,7 @@ Expected task structure: ```json { - "queue_id": "Q-001", + "item_id": "T-1", "issue_id": "ISSUE-20251227-001", "solution_id": "SOL-001", "task": { @@ -159,7 +159,7 @@ git add path/to/file1.ts path/to/file2.ts ... git commit -m "$(cat <<'EOF' [task.commit.message_template] -Queue-ID: [queue_id] +Item-ID: [item_id] Issue-ID: [issue_id] Task-ID: [task.id] EOF @@ -180,7 +180,7 @@ EOF After commit succeeds, report to queue system: ```bash -ccw issue complete [queue_id] --result '{ +ccw issue complete [item_id] --result '{ "files_modified": ["path1", "path2"], "tests_passed": true, "acceptance_passed": true, @@ -193,7 +193,7 @@ ccw issue complete [queue_id] --result '{ **If task failed and cannot be fixed:** ```bash -ccw issue fail [queue_id] --reason "Phase [X] failed: [details]" +ccw issue fail [item_id] --reason "Phase [X] failed: [details]" ``` ## Step 5: Continue to Next Task @@ -206,7 +206,7 @@ ccw issue next **Output progress:** ``` -✓ [N/M] Completed: [queue_id] - [task.title] +✓ [N/M] Completed: [item_id] - [task.title] → Fetching next task... ``` @@ -221,10 +221,10 @@ When `ccw issue next` returns `{ "status": "empty" }`: **Total Tasks Executed**: N **All Commits**: -| # | Queue ID | Task | Commit | -|---|----------|------|--------| -| 1 | Q-001 | Task title | abc123 | -| 2 | Q-002 | Task title | def456 | +| # | Item ID | Task | Commit | +|---|---------|------|--------| +| 1 | T-1 | Task title | abc123 | +| 2 | T-2 | Task title | def456 | **Files Modified**: - path/to/file1.ts diff --git a/ccw/src/cli.ts b/ccw/src/cli.ts index 5514b667..0340b6c4 100644 --- a/ccw/src/cli.ts +++ b/ccw/src/cli.ts @@ -277,6 +277,7 @@ export function run(argv: string[]): void { .option('--priority ', 'Task priority (1-5)') .option('--format ', 'Output format: json, markdown') .option('--json', 'Output as JSON') + .option('--ids', 'List only IDs (one per line, for scripting)') .option('--force', 'Force operation') // New options for solution/queue management .option('--solution ', 'Solution JSON file path') diff --git a/ccw/src/commands/issue.ts b/ccw/src/commands/issue.ts index 5e2a11f9..59390701 100644 --- a/ccw/src/commands/issue.ts +++ b/ccw/src/commands/issue.ts @@ -5,7 +5,7 @@ */ import chalk from 'chalk'; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs'; import { join, resolve } from 'path'; // Handle EPIPE errors gracefully @@ -29,6 +29,18 @@ interface Issue { source?: string; source_url?: string; labels?: string[]; + // Agent workflow fields + affected_components?: string[]; + lifecycle_requirements?: { + test_strategy?: 'unit' | 'integration' | 'e2e' | 'auto'; + regression_scope?: 'full' | 'related' | 'affected'; + commit_strategy?: 'per-task' | 'atomic' | 'squash'; + }; + problem_statement?: string; + expected_behavior?: string; + actual_behavior?: string; + reproduction_steps?: string[]; + // Timestamps created_at: string; updated_at: string; planned_at?: string; @@ -100,17 +112,17 @@ interface Solution { } interface QueueItem { - queue_id: string; + item_id: string; // Task item ID in queue: T-1, T-2, ... (formerly queue_id) issue_id: string; solution_id: string; task_id: string; + title?: string; status: 'pending' | 'ready' | 'executing' | 'completed' | 'failed' | 'blocked'; execution_order: number; execution_group: string; depends_on: string[]; semantic_priority: number; assigned_executor: 'codex' | 'gemini' | 'agent'; - queued_at: string; started_at?: string; completed_at?: string; result?: Record; @@ -118,11 +130,11 @@ interface QueueItem { } interface Queue { - id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS + id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS (derived from filename) name?: string; // Optional queue name status: 'active' | 'completed' | 'archived' | 'failed'; issue_ids: string[]; // Issues in this queue - queue: QueueItem[]; + tasks: QueueItem[]; // Task items (formerly 'queue') conflicts: any[]; execution_groups?: any[]; _metadata: { @@ -132,13 +144,12 @@ interface Queue { executing_count: number; completed_count: number; failed_count: number; - created_at: string; updated_at: string; }; } interface QueueIndex { - active_queue_id: string | null; + active_item_id: string | null; queues: { id: string; status: string; @@ -162,6 +173,7 @@ interface IssueOptions { json?: boolean; force?: boolean; fail?: boolean; + ids?: boolean; // List only IDs (one per line) } const ISSUES_DIR = '.workflow/issues'; @@ -278,7 +290,7 @@ function ensureQueuesDir(): void { function readQueueIndex(): QueueIndex { const path = join(getQueuesDir(), 'index.json'); if (!existsSync(path)) { - return { active_queue_id: null, queues: [] }; + return { active_item_id: null, queues: [] }; } return JSON.parse(readFileSync(path, 'utf-8')); } @@ -319,16 +331,15 @@ function createEmptyQueue(): Queue { id: generateQueueFileId(), status: 'active', issue_ids: [], - queue: [], + tasks: [], conflicts: [], _metadata: { - version: '2.0', + version: '2.1', total_tasks: 0, pending_count: 0, executing_count: 0, completed_count: 0, failed_count: 0, - created_at: new Date().toISOString(), updated_at: new Date().toISOString() } }; @@ -338,11 +349,11 @@ function writeQueue(queue: Queue): void { ensureQueuesDir(); // Update metadata counts - queue._metadata.total_tasks = queue.queue.length; - queue._metadata.pending_count = queue.queue.filter(q => q.status === 'pending').length; - queue._metadata.executing_count = queue.queue.filter(q => q.status === 'executing').length; - queue._metadata.completed_count = queue.queue.filter(q => q.status === 'completed').length; - queue._metadata.failed_count = queue.queue.filter(q => q.status === 'failed').length; + queue._metadata.total_tasks = queue.tasks.length; + queue._metadata.pending_count = queue.tasks.filter(q => q.status === 'pending').length; + queue._metadata.executing_count = queue.tasks.filter(q => q.status === 'executing').length; + queue._metadata.completed_count = queue.tasks.filter(q => q.status === 'completed').length; + queue._metadata.failed_count = queue.tasks.filter(q => q.status === 'failed').length; queue._metadata.updated_at = new Date().toISOString(); // Write queue file @@ -359,7 +370,7 @@ function writeQueue(queue: Queue): void { issue_ids: queue.issue_ids, total_tasks: queue._metadata.total_tasks, completed_tasks: queue._metadata.completed_count, - created_at: queue._metadata.created_at, + created_at: queue.id.replace('QUE-', '').replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, '$1-$2-$3T$4:$5:$6Z'), // Derive from ID completed_at: queue.status === 'completed' ? new Date().toISOString() : undefined }; @@ -377,11 +388,11 @@ function writeQueue(queue: Queue): void { } function generateQueueItemId(queue: Queue): string { - const maxNum = queue.queue.reduce((max, q) => { - const match = q.queue_id.match(/^Q-(\d+)$/); + const maxNum = queue.tasks.reduce((max, q) => { + const match = q.item_id.match(/^T-(\d+)$/); return match ? Math.max(max, parseInt(match[1])) : max; }, 0); - return `Q-${String(maxNum + 1).padStart(3, '0')}`; + return `T-${maxNum + 1}`; } // ============ Commands ============ @@ -429,7 +440,19 @@ async function initAction(issueId: string | undefined, options: IssueOptions): P async function listAction(issueId: string | undefined, options: IssueOptions): Promise { if (!issueId) { // List all issues - const issues = readIssues(); + let issues = readIssues(); + + // Filter by status if specified + if (options.status) { + const statuses = options.status.split(',').map(s => s.trim()); + issues = issues.filter(i => statuses.includes(i.status)); + } + + // IDs only mode (one per line, for scripting) + if (options.ids) { + issues.forEach(i => console.log(i.id)); + return; + } if (options.json) { console.log(JSON.stringify(issues, null, 2)); @@ -519,7 +542,8 @@ async function statusAction(issueId: string | undefined, options: IssueOptions): const index = readQueueIndex(); if (options.json) { - console.log(JSON.stringify({ queue: queue._metadata, issues: issues.length, queues: index.queues.length }, null, 2)); + // Return full queue for programmatic access + console.log(JSON.stringify(queue, null, 2)); return; } @@ -806,7 +830,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde // Archive current queue if (subAction === 'archive') { const queue = readActiveQueue(); - if (!queue.id || queue.queue.length === 0) { + if (!queue.id || queue.tasks.length === 0) { console.log(chalk.yellow('No active queue to archive')); return; } @@ -822,6 +846,31 @@ async function queueAction(subAction: string | undefined, issueId: string | unde return; } + // Delete queue from history + if ((subAction === 'clear' || subAction === 'delete') && issueId) { + const queueId = issueId; // issueId is actually queue ID here + const queuePath = join(getQueuesDir(), `${queueId}.json`); + + if (!existsSync(queuePath)) { + console.error(chalk.red(`Queue "${queueId}" not found`)); + process.exit(1); + } + + // Remove from index + const index = readQueueIndex(); + index.queues = index.queues.filter(q => q.id !== queueId); + if (index.active_queue_id === queueId) { + index.active_queue_id = null; + } + writeQueueIndex(index); + + // Delete queue file + unlinkSync(queuePath); + + console.log(chalk.green(`✓ Deleted queue ${queueId}`)); + return; + } + // Add issue tasks to queue if (subAction === 'add' && issueId) { const issue = findIssue(issueId); @@ -839,7 +888,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde // Get or create active queue (create new if current is completed/archived) let queue = readActiveQueue(); - const isNewQueue = queue.queue.length === 0 || queue.status !== 'active'; + const isNewQueue = queue.tasks.length === 0 || queue.status !== 'active'; if (queue.status !== 'active') { // Create new queue if current is not active @@ -853,24 +902,23 @@ async function queueAction(subAction: string | undefined, issueId: string | unde let added = 0; for (const task of solution.tasks) { - const exists = queue.queue.some(q => q.issue_id === issueId && q.task_id === task.id); + const exists = queue.tasks.some(q => q.issue_id === issueId && q.task_id === task.id); if (exists) continue; - queue.queue.push({ - queue_id: generateQueueItemId(queue), + queue.tasks.push({ + item_id: generateQueueItemId(queue), issue_id: issueId, solution_id: solution.id, task_id: task.id, status: 'pending', - execution_order: queue.queue.length + 1, + execution_order: queue.tasks.length + 1, execution_group: 'P1', depends_on: task.depends_on.map(dep => { - const depItem = queue.queue.find(q => q.task_id === dep && q.issue_id === issueId); - return depItem?.queue_id || dep; + const depItem = queue.tasks.find(q => q.task_id === dep && q.issue_id === issueId); + return depItem?.item_id || dep; }), semantic_priority: 0.5, - assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any, - queued_at: new Date().toISOString() + assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any }); added++; } @@ -895,7 +943,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde console.log(chalk.bold.cyan('\nActive Queue\n')); - if (!queue.id || queue.queue.length === 0) { + if (!queue.id || queue.tasks.length === 0) { console.log(chalk.yellow('No active queue')); console.log(chalk.gray('Create one: ccw issue queue add ')); console.log(chalk.gray('Or list history: ccw issue queue list')); @@ -910,7 +958,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor')); console.log(chalk.gray('-'.repeat(60))); - for (const item of queue.queue) { + for (const item of queue.tasks) { const statusColor = { 'pending': chalk.gray, 'ready': chalk.cyan, @@ -921,7 +969,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde }[item.status] || chalk.white; console.log( - item.queue_id.padEnd(10) + + item.item_id.padEnd(10) + item.issue_id.substring(0, 13).padEnd(15) + item.task_id.padEnd(8) + statusColor(item.status.padEnd(12)) + @@ -937,13 +985,13 @@ async function nextAction(options: IssueOptions): Promise { const queue = readActiveQueue(); // Priority 1: Resume executing tasks (interrupted/crashed) - const executingTasks = queue.queue.filter(item => item.status === 'executing'); + const executingTasks = queue.tasks.filter(item => item.status === 'executing'); // Priority 2: Find pending tasks with satisfied dependencies - const pendingTasks = queue.queue.filter(item => { + const pendingTasks = queue.tasks.filter(item => { if (item.status !== 'pending') return false; return item.depends_on.every(depId => { - const dep = queue.queue.find(q => q.queue_id === depId); + const dep = queue.tasks.find(q => q.item_id === depId); return !dep || dep.status === 'completed'; }); }); @@ -976,25 +1024,25 @@ async function nextAction(options: IssueOptions): Promise { // Only update status if not already executing (new task) if (!isResume) { - const idx = queue.queue.findIndex(q => q.queue_id === nextItem.queue_id); - queue.queue[idx].status = 'executing'; - queue.queue[idx].started_at = new Date().toISOString(); + const idx = queue.tasks.findIndex(q => q.item_id === nextItem.item_id); + queue.tasks[idx].status = 'executing'; + queue.tasks[idx].started_at = new Date().toISOString(); writeQueue(queue); updateIssue(nextItem.issue_id, { status: 'executing' }); } // Calculate queue stats for context const stats = { - total: queue.queue.length, - completed: queue.queue.filter(q => q.status === 'completed').length, - failed: queue.queue.filter(q => q.status === 'failed').length, + 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 }; const remaining = stats.pending + stats.executing; console.log(JSON.stringify({ - queue_id: nextItem.queue_id, + item_id: nextItem.item_id, issue_id: nextItem.issue_id, solution_id: nextItem.solution_id, task: taskDef, @@ -1025,7 +1073,7 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P } const queue = readActiveQueue(); - const idx = queue.queue.findIndex(q => q.queue_id === queueId); + const idx = queue.tasks.findIndex(q => q.item_id === queueId); if (idx === -1) { console.error(chalk.red(`Queue item "${queueId}" not found`)); @@ -1033,22 +1081,22 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P } const isFail = options.fail; - queue.queue[idx].status = isFail ? 'failed' : 'completed'; - queue.queue[idx].completed_at = new Date().toISOString(); + queue.tasks[idx].status = isFail ? 'failed' : 'completed'; + queue.tasks[idx].completed_at = new Date().toISOString(); if (isFail) { - queue.queue[idx].failure_reason = options.reason || 'Unknown failure'; + queue.tasks[idx].failure_reason = options.reason || 'Unknown failure'; } else if (options.result) { try { - queue.queue[idx].result = JSON.parse(options.result); + queue.tasks[idx].result = JSON.parse(options.result); } catch { console.warn(chalk.yellow('Warning: Could not parse result JSON')); } } // Check if all issue tasks are complete - const issueId = queue.queue[idx].issue_id; - const issueTasks = queue.queue.filter(q => q.issue_id === issueId); + const issueId = queue.tasks[idx].issue_id; + const issueTasks = queue.tasks.filter(q => q.issue_id === issueId); const allIssueComplete = issueTasks.every(q => q.status === 'completed'); const anyIssueFailed = issueTasks.some(q => q.status === 'failed'); @@ -1064,13 +1112,13 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P } // Check if entire queue is complete - const allQueueComplete = queue.queue.every(q => q.status === 'completed'); - const anyQueueFailed = queue.queue.some(q => q.status === 'failed'); + const allQueueComplete = queue.tasks.every(q => q.status === 'completed'); + const anyQueueFailed = queue.tasks.some(q => q.status === 'failed'); if (allQueueComplete) { queue.status = 'completed'; console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all tasks done)`)); - } else if (anyQueueFailed && queue.queue.every(q => q.status === 'completed' || q.status === 'failed')) { + } else if (anyQueueFailed && queue.tasks.every(q => q.status === 'completed' || q.status === 'failed')) { queue.status = 'failed'; console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`)); } @@ -1079,24 +1127,20 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P } /** - * retry - Retry failed tasks, or reset stuck executing tasks (--force) + * retry - Reset failed tasks to pending for re-execution */ async function retryAction(issueId: string | undefined, options: IssueOptions): Promise { const queue = readActiveQueue(); - if (!queue.id || queue.queue.length === 0) { + if (!queue.id || queue.tasks.length === 0) { console.log(chalk.yellow('No active queue')); return; } let updated = 0; - // Check for stuck executing tasks (started > 30 min ago with no completion) - const stuckThreshold = 30 * 60 * 1000; // 30 minutes - const now = Date.now(); - - for (const item of queue.queue) { - // Retry failed tasks + for (const item of queue.tasks) { + // Retry failed tasks only if (item.status === 'failed') { if (!issueId || item.issue_id === issueId) { item.status = 'pending'; @@ -1106,23 +1150,11 @@ async function retryAction(issueId: string | undefined, options: IssueOptions): updated++; } } - // Reset stuck executing tasks (optional: use --force or --reset-stuck) - else if (item.status === 'executing' && options.force) { - const startedAt = item.started_at ? new Date(item.started_at).getTime() : 0; - if (now - startedAt > stuckThreshold) { - if (!issueId || item.issue_id === issueId) { - console.log(chalk.yellow(`Resetting stuck task: ${item.queue_id} (started ${Math.round((now - startedAt) / 60000)} min ago)`)); - item.status = 'pending'; - item.started_at = undefined; - updated++; - } - } - } } if (updated === 0) { - console.log(chalk.yellow('No failed/stuck tasks to retry')); - console.log(chalk.gray('Use --force to reset stuck executing tasks (>30 min)')); + console.log(chalk.yellow('No failed tasks to retry')); + console.log(chalk.gray('Note: Interrupted (executing) tasks are auto-resumed by "ccw issue next"')); return; } @@ -1203,7 +1235,8 @@ export async function issueCommand( 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 archive Archive current queue')); - console.log(chalk.gray(' retry [issue-id] [--force] Retry failed/stuck tasks')); + 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)')); @@ -1212,6 +1245,8 @@ export async function issueCommand( console.log(); console.log(chalk.bold('Options:')); console.log(chalk.gray(' --title Issue/task title')); + console.log(chalk.gray(' --status <status> Filter by status (comma-separated)')); + console.log(chalk.gray(' --ids List only IDs (one per line)')); console.log(chalk.gray(' --solution <path> Solution JSON file')); console.log(chalk.gray(' --result <json> Execution result')); console.log(chalk.gray(' --reason <text> Failure reason')); diff --git a/ccw/src/core/routes/issue-routes.ts b/ccw/src/core/routes/issue-routes.ts index b3ab0f78..1be89304 100644 --- a/ccw/src/core/routes/issue-routes.ts +++ b/ccw/src/core/routes/issue-routes.ts @@ -5,7 +5,9 @@ * Storage Structure: * .workflow/issues/ * ├── issues.jsonl # All issues (one per line) - * ├── queue.json # Execution queue + * ├── queues/ # Queue history directory + * │ ├── index.json # Queue index (active + history) + * │ └── {queue-id}.json # Individual queue files * └── solutions/ * ├── {issue-id}.jsonl # Solutions for issue (one per line) * └── ... @@ -102,12 +104,12 @@ function readQueue(issuesDir: string) { } } - return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } }; + return { tasks: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } }; } function writeQueue(issuesDir: string, queue: any) { if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true }); - queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.queue?.length || 0 }; + queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.tasks?.length || 0 }; // Check if using new multi-queue structure const queuesDir = join(issuesDir, 'queues'); @@ -123,8 +125,8 @@ function writeQueue(issuesDir: string, queue: any) { const index = JSON.parse(readFileSync(indexPath, 'utf8')); const queueEntry = index.queues?.find((q: any) => q.id === queue.id); if (queueEntry) { - queueEntry.total_tasks = queue.queue?.length || 0; - queueEntry.completed_tasks = queue.queue?.filter((i: any) => i.status === 'completed').length || 0; + queueEntry.total_tasks = queue.tasks?.length || 0; + queueEntry.completed_tasks = queue.tasks?.filter((i: any) => i.status === 'completed').length || 0; writeFileSync(indexPath, JSON.stringify(index, null, 2)); } } catch { @@ -151,15 +153,29 @@ function getIssueDetail(issuesDir: string, issueId: string) { } function enrichIssues(issues: any[], issuesDir: string) { - return issues.map(issue => ({ - ...issue, - solution_count: readSolutionsJsonl(issuesDir, issue.id).length - })); + return issues.map(issue => { + const solutions = readSolutionsJsonl(issuesDir, issue.id); + let taskCount = 0; + + // Get task count from bound solution + if (issue.bound_solution_id) { + const boundSol = solutions.find(s => s.id === issue.bound_solution_id); + if (boundSol?.tasks) { + taskCount = boundSol.tasks.length; + } + } + + return { + ...issue, + solution_count: solutions.length, + task_count: taskCount + }; + }); } function groupQueueByExecutionGroup(queue: any) { const groups: { [key: string]: any[] } = {}; - for (const item of queue.queue || []) { + for (const item of queue.tasks || []) { const groupId = item.execution_group || 'ungrouped'; if (!groups[groupId]) groups[groupId] = []; groups[groupId].push(item); @@ -171,7 +187,7 @@ function groupQueueByExecutionGroup(queue: any) { id, type: id.startsWith('P') ? 'parallel' : id.startsWith('S') ? 'sequential' : 'unknown', task_count: items.length, - tasks: items.map(i => i.queue_id) + tasks: items.map(i => i.item_id) })).sort((a, b) => { const aFirst = groups[a.id]?.[0]?.execution_order || 0; const bFirst = groups[b.id]?.[0]?.execution_order || 0; @@ -229,20 +245,20 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> { } const queue = readQueue(issuesDir); - const groupItems = queue.queue.filter((item: any) => item.execution_group === groupId); - const otherItems = queue.queue.filter((item: any) => item.execution_group !== groupId); + const groupItems = queue.tasks.filter((item: any) => item.execution_group === groupId); + const otherItems = queue.tasks.filter((item: any) => item.execution_group !== groupId); if (groupItems.length === 0) return { error: `No items in group ${groupId}` }; - const groupQueueIds = new Set(groupItems.map((i: any) => i.queue_id)); - if (groupQueueIds.size !== new Set(newOrder).size) { + const groupItemIds = new Set(groupItems.map((i: any) => i.item_id)); + if (groupItemIds.size !== new Set(newOrder).size) { return { error: 'newOrder must contain all group items' }; } for (const id of newOrder) { - if (!groupQueueIds.has(id)) return { error: `Invalid queue_id: ${id}` }; + if (!groupItemIds.has(id)) return { error: `Invalid item_id: ${id}` }; } - const itemMap = new Map(groupItems.map((i: any) => [i.queue_id, i])); + const itemMap = new Map(groupItems.map((i: any) => [i.item_id, i])); const reorderedItems = newOrder.map((qid: string, idx: number) => ({ ...itemMap.get(qid), _idx: idx })); const newQueue = [...otherItems, ...reorderedItems].sort((a, b) => { const aGroup = parseInt(a.execution_group?.match(/\d+/)?.[0] || '999'); @@ -255,7 +271,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> { }); newQueue.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; }); - queue.queue = newQueue; + queue.tasks = newQueue; writeQueue(issuesDir, queue); return { success: true, groupId, reordered: newOrder.length }; diff --git a/ccw/src/templates/dashboard-js/views/issue-manager.js b/ccw/src/templates/dashboard-js/views/issue-manager.js index d9c43f5d..ef5be1e5 100644 --- a/ccw/src/templates/dashboard-js/views/issue-manager.js +++ b/ccw/src/templates/dashboard-js/views/issue-manager.js @@ -6,7 +6,7 @@ // ========== Issue State ========== var issueData = { issues: [], - queue: { queue: [], conflicts: [], execution_groups: [], grouped_items: {} }, + queue: { tasks: [], conflicts: [], execution_groups: [], grouped_items: {} }, selectedIssue: null, selectedSolution: null, selectedSolutionIssueId: null, @@ -65,7 +65,7 @@ async function loadQueueData() { issueData.queue = await response.json(); } catch (err) { console.error('Failed to load queue:', err); - issueData.queue = { queue: [], conflicts: [], execution_groups: [], grouped_items: {} }; + issueData.queue = { tasks: [], conflicts: [], execution_groups: [], grouped_items: {} }; } } @@ -360,7 +360,7 @@ function filterIssuesByStatus(status) { // ========== Queue Section ========== function renderQueueSection() { const queue = issueData.queue; - const queueItems = queue.queue || []; + const queueItems = queue.tasks || []; const metadata = queue._metadata || {}; // Check if queue is empty @@ -530,10 +530,10 @@ function renderQueueItem(item, index, total) { return ` <div class="queue-item ${statusColors[item.status] || ''}" draggable="true" - data-queue-id="${item.queue_id}" + data-item-id="${item.item_id}" data-group-id="${item.execution_group}" - onclick="openQueueItemDetail('${item.queue_id}')"> - <span class="queue-item-id font-mono text-xs">${item.queue_id}</span> + onclick="openQueueItemDetail('${item.item_id}')"> + <span class="queue-item-id font-mono text-xs">${item.item_id}</span> <span class="queue-item-issue text-xs text-muted-foreground">${item.issue_id}</span> <span class="queue-item-task text-sm">${item.task_id}</span> <span class="queue-item-priority" style="opacity: ${item.semantic_priority || 0.5}"> @@ -586,12 +586,12 @@ function handleIssueDragStart(e) { const item = e.target.closest('.queue-item'); if (!item) return; - issueDragState.dragging = item.dataset.queueId; + issueDragState.dragging = item.dataset.itemId; issueDragState.groupId = item.dataset.groupId; item.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', item.dataset.queueId); + e.dataTransfer.setData('text/plain', item.dataset.itemId); } function handleIssueDragEnd(e) { @@ -610,7 +610,7 @@ function handleIssueDragOver(e) { e.preventDefault(); const target = e.target.closest('.queue-item'); - if (!target || target.dataset.queueId === issueDragState.dragging) return; + if (!target || target.dataset.itemId === issueDragState.dragging) return; // Only allow drag within same group if (target.dataset.groupId !== issueDragState.groupId) { @@ -635,7 +635,7 @@ function handleIssueDrop(e) { // Get new order const items = Array.from(container.querySelectorAll('.queue-item')); - const draggedItem = items.find(i => i.dataset.queueId === issueDragState.dragging); + const draggedItem = items.find(i => i.dataset.itemId === issueDragState.dragging); const targetIndex = items.indexOf(target); const draggedIndex = items.indexOf(draggedItem); @@ -649,7 +649,7 @@ function handleIssueDrop(e) { } // Get new order and save - const newOrder = Array.from(container.querySelectorAll('.queue-item')).map(i => i.dataset.queueId); + const newOrder = Array.from(container.querySelectorAll('.queue-item')).map(i => i.dataset.itemId); saveQueueOrder(issueDragState.groupId, newOrder); } @@ -767,7 +767,7 @@ function renderIssueDetailPanel(issue) { <div class="flex items-center justify-between"> <span class="font-mono text-sm">${task.id}</span> <select class="task-status-select" onchange="updateTaskStatus('${issue.id}', '${task.id}', this.value)"> - ${['pending', 'ready', 'in_progress', 'completed', 'failed', 'paused', 'skipped'].map(s => + ${['pending', 'ready', 'executing', 'completed', 'failed', 'blocked', 'paused', 'skipped'].map(s => `<option value="${s}" ${task.status === s ? 'selected' : ''}>${s}</option>` ).join('')} </select> @@ -1145,8 +1145,8 @@ function escapeHtml(text) { return div.innerHTML; } -function openQueueItemDetail(queueId) { - const item = issueData.queue.queue?.find(q => q.queue_id === queueId); +function openQueueItemDetail(itemId) { + const item = issueData.queue.tasks?.find(q => q.item_id === itemId); if (item) { openIssueDetail(item.issue_id); } diff --git a/package-lock.json b/package-lock.json index cf2e30d0..817b6a42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-workflow", - "version": "6.2.9", + "version": "6.3.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-workflow", - "version": "6.2.9", + "version": "6.3.8", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", diff --git a/package.json b/package.json index 439393ba..8b345c70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-code-workflow", - "version": "6.3.6", + "version": "6.3.8", "description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution", "type": "module", "main": "ccw/src/index.js",