diff --git a/.claude/commands/issue/execute.md b/.claude/commands/issue/execute.md new file mode 100644 index 00000000..e3eebb40 --- /dev/null +++ b/.claude/commands/issue/execute.md @@ -0,0 +1,591 @@ +--- +name: execute +description: Execute issue tasks with closed-loop methodology (analyze→implement→test→optimize→commit) +argument-hint: " [--task ] [--batch ]" +allowed-tools: TodoWrite(*), Task(*), Bash(*), Read(*), Write(*), Edit(*), AskUserQuestion(*) +--- + +# Issue Execute Command (/issue:execute) + +## Overview + +Execute tasks from a planned issue using closed-loop methodology. Each task goes through 5 phases: **Analyze → Implement → Test → Optimize → Commit**. Tasks are loaded progressively based on dependency satisfaction. + +**Core capabilities:** +- Progressive task loading (only load ready tasks) +- Closed-loop execution with 5 phases per task +- Automatic retry on test failures (up to 3 attempts) +- Pause on defined pause_criteria conditions +- Delivery criteria verification before completion +- Automatic git commit per task + +## Usage + +```bash +/issue:execute [FLAGS] + +# Arguments + Issue ID (e.g., GH-123, TEXT-1735200000) + +# Flags +--task Execute specific task only +--batch Max concurrent tasks (default: 1) +--skip-commit Skip git commit phase +--dry-run Simulate execution without changes +--continue Continue from paused/failed state +``` + +## Execution Process + +``` +Initialization: + ├─ Load state.json and tasks.jsonl + ├─ Build completed task index + └─ Identify ready tasks (dependencies satisfied) + +Task Loop: + └─ For each ready task: + ├─ Phase 1: ANALYZE + │ ├─ Verify task requirements + │ ├─ Check file existence + │ ├─ Validate preconditions + │ └─ Check pause_criteria (halt if triggered) + │ + ├─ Phase 2: IMPLEMENT + │ ├─ Execute code changes + │ ├─ Write/modify files + │ └─ Track modified files + │ + ├─ Phase 3: TEST + │ ├─ Run relevant tests + │ ├─ Verify functionality + │ └─ Retry loop (max 3) on failure → back to IMPLEMENT + │ + ├─ Phase 4: OPTIMIZE + │ ├─ Code quality check + │ ├─ Lint/format verification + │ └─ Apply minor improvements + │ + ├─ Phase 5: COMMIT + │ ├─ Stage modified files + │ ├─ Create commit with task reference + │ └─ Update task status to 'completed' + │ + └─ Update state.json + +Completion: + └─ Return execution summary +``` + +## Implementation + +### Initialization + +```javascript +// Load issue context +const issueDir = `.workflow/issues/${issueId}` +const state = JSON.parse(Read(`${issueDir}/state.json`)) +const tasks = readJsonl(`${issueDir}/tasks.jsonl`) + +// Build completed index +const completedIds = new Set( + tasks.filter(t => t.status === 'completed').map(t => t.id) +) + +// Get ready tasks (dependencies satisfied) +function getReadyTasks() { + return tasks.filter(task => + task.status === 'pending' && + task.depends_on.every(dep => completedIds.has(dep)) + ) +} + +let readyTasks = getReadyTasks() +if (readyTasks.length === 0) { + if (tasks.every(t => t.status === 'completed')) { + console.log('✓ All tasks completed!') + return + } + console.log('⚠ No ready tasks. Check dependencies or blocked tasks.') + return +} + +// Initialize TodoWrite for tracking +TodoWrite({ + todos: readyTasks.slice(0, batchSize).map(t => ({ + content: `[${t.id}] ${t.title}`, + status: 'pending', + activeForm: `Executing ${t.id}` + })) +}) +``` + +### Task Execution Loop + +```javascript +for (const task of readyTasks.slice(0, batchSize)) { + console.log(`\n## Executing: ${task.id} - ${task.title}`) + + // Update state + updateTaskStatus(task.id, 'in_progress', 'analyze') + + try { + // Phase 1: ANALYZE + const analyzeResult = await executePhase_Analyze(task) + if (analyzeResult.paused) { + console.log(`⏸ Task paused: ${analyzeResult.reason}`) + updateTaskStatus(task.id, 'paused', 'analyze') + continue + } + + // Phase 2-5: Closed Loop + let implementRetries = 0 + const maxRetries = 3 + + while (implementRetries < maxRetries) { + // Phase 2: IMPLEMENT + const implementResult = await executePhase_Implement(task, analyzeResult) + updateTaskStatus(task.id, 'in_progress', 'test') + + // Phase 3: TEST + const testResult = await executePhase_Test(task, implementResult) + + if (testResult.passed) { + // Phase 4: OPTIMIZE + await executePhase_Optimize(task, implementResult) + + // Phase 5: COMMIT + if (!flags.skipCommit) { + await executePhase_Commit(task, implementResult) + } + + // Mark completed + updateTaskStatus(task.id, 'completed', 'done') + completedIds.add(task.id) + break + } else { + implementRetries++ + console.log(`⚠ Test failed, retry ${implementRetries}/${maxRetries}`) + if (implementRetries >= maxRetries) { + updateTaskStatus(task.id, 'failed', 'test') + console.log(`✗ Task failed after ${maxRetries} retries`) + } + } + } + } catch (error) { + updateTaskStatus(task.id, 'failed', task.current_phase) + console.log(`✗ Task failed: ${error.message}`) + } +} +``` + +### Phase 1: ANALYZE + +```javascript +async function executePhase_Analyze(task) { + console.log('### Phase 1: ANALYZE') + + // Check pause criteria first + for (const criterion of task.pause_criteria || []) { + const shouldPause = await evaluatePauseCriterion(criterion, task) + if (shouldPause) { + return { paused: true, reason: criterion } + } + } + + // Execute analysis via CLI + const analysisResult = await Task( + subagent_type="cli-explore-agent", + run_in_background=false, + description=`Analyze: ${task.id}`, + prompt=` +## Analysis Task +ID: ${task.id} +Title: ${task.title} +Description: ${task.description} + +## File Context +${task.file_context.join('\n')} + +## Delivery Criteria (to be achieved) +${task.delivery_criteria.map((c, i) => `${i+1}. ${c}`).join('\n')} + +## Required Analysis +1. Verify all referenced files exist +2. Identify exact modification points +3. Check for potential conflicts +4. Validate approach feasibility + +## Output +Return JSON: +{ + "files_to_modify": ["path1", "path2"], + "integration_points": [...], + "potential_risks": [...], + "implementation_notes": "..." +} +` + ) + + // Parse and return + const analysis = JSON.parse(analysisResult) + + // Update phase results + updatePhaseResult(task.id, 'analyze', { + status: 'completed', + findings: analysis.potential_risks, + timestamp: new Date().toISOString() + }) + + return { paused: false, analysis } +} +``` + +### Phase 2: IMPLEMENT + +```javascript +async function executePhase_Implement(task, analyzeResult) { + console.log('### Phase 2: IMPLEMENT') + + updateTaskStatus(task.id, 'in_progress', 'implement') + + // Determine executor + const executor = task.executor === 'auto' + ? (task.type === 'test' ? 'agent' : 'codex') + : task.executor + + // Build implementation prompt + const prompt = ` +## Implementation Task +ID: ${task.id} +Title: ${task.title} +Type: ${task.type} + +## Description +${task.description} + +## Analysis Results +${JSON.stringify(analyzeResult.analysis, null, 2)} + +## Files to Modify +${analyzeResult.analysis.files_to_modify.join('\n')} + +## Delivery Criteria (MUST achieve all) +${task.delivery_criteria.map((c, i) => `- [ ] ${c}`).join('\n')} + +## Implementation Notes +${analyzeResult.analysis.implementation_notes} + +## Rules +- Follow existing code patterns +- Maintain backward compatibility +- Add appropriate error handling +- Document significant changes +` + + let result + if (executor === 'codex') { + result = Bash( + `ccw cli -p "${escapePrompt(prompt)}" --tool codex --mode write`, + timeout=3600000 + ) + } else if (executor === 'gemini') { + result = Bash( + `ccw cli -p "${escapePrompt(prompt)}" --tool gemini --mode write`, + timeout=1800000 + ) + } else { + result = await Task( + subagent_type="code-developer", + run_in_background=false, + description=`Implement: ${task.id}`, + prompt=prompt + ) + } + + // Track modified files + const modifiedFiles = extractModifiedFiles(result) + + updatePhaseResult(task.id, 'implement', { + status: 'completed', + files_modified: modifiedFiles, + timestamp: new Date().toISOString() + }) + + return { modifiedFiles, output: result } +} +``` + +### Phase 3: TEST + +```javascript +async function executePhase_Test(task, implementResult) { + console.log('### Phase 3: TEST') + + updateTaskStatus(task.id, 'in_progress', 'test') + + // Determine test command based on project + const testCommand = detectTestCommand(task.file_context) + // e.g., 'npm test', 'pytest', 'go test', etc. + + // Run tests + const testResult = Bash(testCommand, timeout=300000) + const passed = testResult.exitCode === 0 + + // Verify delivery criteria + let criteriaVerified = passed + if (passed) { + for (const criterion of task.delivery_criteria) { + const verified = await verifyCriterion(criterion, implementResult) + if (!verified) { + criteriaVerified = false + console.log(`⚠ Criterion not met: ${criterion}`) + } + } + } + + updatePhaseResult(task.id, 'test', { + status: passed && criteriaVerified ? 'passed' : 'failed', + test_results: testResult.output.substring(0, 1000), + retry_count: implementResult.retryCount || 0, + timestamp: new Date().toISOString() + }) + + return { passed: passed && criteriaVerified, output: testResult } +} +``` + +### Phase 4: OPTIMIZE + +```javascript +async function executePhase_Optimize(task, implementResult) { + console.log('### Phase 4: OPTIMIZE') + + updateTaskStatus(task.id, 'in_progress', 'optimize') + + // Run linting/formatting + const lintResult = Bash('npm run lint:fix || true', timeout=60000) + + // Quick code review + const reviewResult = await Task( + subagent_type="universal-executor", + run_in_background=false, + description=`Review: ${task.id}`, + prompt=` +Quick code review for task ${task.id} + +## Modified Files +${implementResult.modifiedFiles.join('\n')} + +## Check +1. Code follows project conventions +2. No obvious security issues +3. Error handling is appropriate +4. No dead code or console.logs + +## Output +If issues found, apply fixes directly. Otherwise confirm OK. +` + ) + + updatePhaseResult(task.id, 'optimize', { + status: 'completed', + improvements: extractImprovements(reviewResult), + timestamp: new Date().toISOString() + }) + + return { lintResult, reviewResult } +} +``` + +### Phase 5: COMMIT + +```javascript +async function executePhase_Commit(task, implementResult) { + console.log('### Phase 5: COMMIT') + + updateTaskStatus(task.id, 'in_progress', 'commit') + + // Stage modified files + for (const file of implementResult.modifiedFiles) { + Bash(`git add "${file}"`) + } + + // Create commit message + const typePrefix = { + 'feature': 'feat', + 'bug': 'fix', + 'refactor': 'refactor', + 'test': 'test', + 'chore': 'chore', + 'docs': 'docs' + }[task.type] || 'feat' + + const commitMessage = `${typePrefix}(${task.id}): ${task.title} + +${task.description.substring(0, 200)} + +Delivery Criteria: +${task.delivery_criteria.map(c => `- [x] ${c}`).join('\n')} + +🤖 Generated with [Claude Code](https://claude.com/claude-code) + +Co-Authored-By: Claude Opus 4.5 ` + + // Commit + const commitResult = Bash(`git commit -m "$(cat <<'EOF' +${commitMessage} +EOF +)"`) + + // Get commit hash + const commitHash = Bash('git rev-parse HEAD').trim() + + updatePhaseResult(task.id, 'commit', { + status: 'completed', + commit_hash: commitHash, + message: `${typePrefix}(${task.id}): ${task.title}`, + timestamp: new Date().toISOString() + }) + + console.log(`✓ Committed: ${commitHash.substring(0, 7)}`) + + return { commitHash } +} +``` + +### State Management + +```javascript +// Update task status in JSONL (append-style with compaction) +function updateTaskStatus(taskId, status, phase) { + const tasks = readJsonl(`${issueDir}/tasks.jsonl`) + const taskIndex = tasks.findIndex(t => t.id === taskId) + + if (taskIndex >= 0) { + tasks[taskIndex].status = status + tasks[taskIndex].current_phase = phase + tasks[taskIndex].updated_at = new Date().toISOString() + + // Rewrite JSONL (compact) + const jsonlContent = tasks.map(t => JSON.stringify(t)).join('\n') + Write(`${issueDir}/tasks.jsonl`, jsonlContent) + } + + // Update state.json + const state = JSON.parse(Read(`${issueDir}/state.json`)) + state.current_task = status === 'in_progress' ? taskId : null + state.completed_count = tasks.filter(t => t.status === 'completed').length + state.updated_at = new Date().toISOString() + Write(`${issueDir}/state.json`, JSON.stringify(state, null, 2)) +} + +// Update phase result +function updatePhaseResult(taskId, phase, result) { + const tasks = readJsonl(`${issueDir}/tasks.jsonl`) + const taskIndex = tasks.findIndex(t => t.id === taskId) + + if (taskIndex >= 0) { + tasks[taskIndex].phase_results = tasks[taskIndex].phase_results || {} + tasks[taskIndex].phase_results[phase] = result + + const jsonlContent = tasks.map(t => JSON.stringify(t)).join('\n') + Write(`${issueDir}/tasks.jsonl`, jsonlContent) + } +} +``` + +## Progressive Loading + +For memory efficiency with large task lists: + +```javascript +// Stream JSONL and only load ready tasks +function* getReadyTasksStream(issueDir, completedIds) { + const filePath = `${issueDir}/tasks.jsonl` + const lines = readFileLines(filePath) + + for (const line of lines) { + if (!line.trim()) continue + const task = JSON.parse(line) + + if (task.status === 'pending' && + task.depends_on.every(dep => completedIds.has(dep))) { + yield task + } + } +} + +// Usage: Only load what's needed +const iterator = getReadyTasksStream(issueDir, completedIds) +const batch = [] +for (let i = 0; i < batchSize; i++) { + const { value, done } = iterator.next() + if (done) break + batch.push(value) +} +``` + +## Pause Criteria Evaluation + +```javascript +async function evaluatePauseCriterion(criterion, task) { + // Pattern matching for common pause conditions + const patterns = [ + { match: /unclear|undefined|missing/i, action: 'ask_user' }, + { match: /security review/i, action: 'require_approval' }, + { match: /migration required/i, action: 'check_migration' }, + { match: /external (api|service)/i, action: 'verify_external' } + ] + + for (const pattern of patterns) { + if (pattern.match.test(criterion)) { + // Check if condition is resolved + const resolved = await checkCondition(pattern.action, criterion, task) + if (!resolved) return true // Pause + } + } + + return false // Don't pause +} +``` + +## Error Handling + +| Error | Resolution | +|-------|------------| +| Task not found | List available tasks, suggest correct ID | +| Dependencies unsatisfied | Show blocking tasks, suggest running those first | +| Test failure (3x) | Mark failed, save state, suggest manual intervention | +| Pause triggered | Save state, display pause reason, await user action | +| Commit conflict | Stash changes, report conflict, await resolution | + +## Output + +``` +## Execution Complete + +**Issue**: GH-123 +**Tasks Executed**: 3/5 +**Completed**: 3 +**Failed**: 0 +**Pending**: 2 (dependencies not met) + +### Task Status +| ID | Title | Status | Phase | Commit | +|----|-------|--------|-------|--------| +| TASK-001 | Setup auth middleware | ✓ | done | a1b2c3d | +| TASK-002 | Protect API routes | ✓ | done | e4f5g6h | +| TASK-003 | Add login endpoint | ✓ | done | i7j8k9l | +| TASK-004 | Add logout endpoint | ⏳ | pending | - | +| TASK-005 | Integration tests | ⏳ | pending | - | + +### Next Steps +Run `/issue:execute GH-123` to continue with remaining tasks. +``` + +## Related Commands + +- `/issue:plan` - Create issue plan with JSONL tasks +- `ccw issue status` - Check issue execution status +- `ccw issue retry` - Retry failed tasks diff --git a/.claude/commands/issue/plan.md b/.claude/commands/issue/plan.md new file mode 100644 index 00000000..3a7a3a96 --- /dev/null +++ b/.claude/commands/issue/plan.md @@ -0,0 +1,361 @@ +--- +name: plan +description: Plan issue resolution with JSONL task generation, delivery/pause criteria, and dependency graph +argument-hint: "\"issue description\"|github-url|file.md" +allowed-tools: TodoWrite(*), Task(*), SlashCommand(*), AskUserQuestion(*), Bash(*), Read(*), Write(*) +--- + +# Issue Plan Command (/issue:plan) + +## Overview + +Generate a JSONL-based task plan from a GitHub issue or description. Each task includes delivery criteria, pause criteria, and dependency relationships. The plan is designed for progressive execution with the `/issue:execute` command. + +**Core capabilities:** +- Parse issue from URL, text description, or markdown file +- Analyze codebase context for accurate task breakdown +- Generate JSONL task file with DAG (Directed Acyclic Graph) dependencies +- Define clear delivery criteria (what marks a task complete) +- Define pause criteria (conditions to halt execution) +- Interactive confirmation before finalizing + +## Usage + +```bash +/issue:plan [FLAGS] + +# Input Formats + GitHub issue URL (e.g., https://github.com/owner/repo/issues/123) + Text description of the issue + Markdown file with issue details + +# Flags +-e, --explore Force code exploration phase +--executor Default executor: agent|codex|gemini|auto (default: auto) +``` + +## Execution Process + +``` +Phase 1: Input Parsing & Context + ├─ Parse input (URL → fetch issue, text → use directly, file → read content) + ├─ Extract: title, description, labels, acceptance criteria + └─ Store as issueContext + +Phase 2: Exploration (if needed) + ├─ Complexity assessment (Low/Medium/High) + ├─ Launch cli-explore-agent for codebase understanding + └─ Identify: relevant files, patterns, integration points + +Phase 3: Task Breakdown + ├─ Agent generates JSONL task list + ├─ Each task includes: + │ ├─ delivery_criteria (completion checklist) + │ ├─ pause_criteria (halt conditions) + │ └─ depends_on (dependency graph) + └─ Validate DAG (no circular dependencies) + +Phase 4: User Confirmation + ├─ Display task summary table + ├─ Show dependency graph + └─ AskUserQuestion: Approve / Refine / Cancel + +Phase 5: Persistence + ├─ Write tasks.jsonl to .workflow/issues/{issue-id}/ + ├─ Initialize state.json for status tracking + └─ Return summary and next steps +``` + +## Implementation + +### Phase 1: Input Parsing + +```javascript +// Helper: Get UTC+8 ISO string +const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString() + +// Parse input type +function parseInput(input) { + if (input.startsWith('https://github.com/')) { + const match = input.match(/github\.com\/(.+?)\/(.+?)\/issues\/(\d+)/) + if (match) { + return { type: 'github', owner: match[1], repo: match[2], number: match[3] } + } + } + if (input.endsWith('.md') && fileExists(input)) { + return { type: 'file', path: input } + } + return { type: 'text', content: input } +} + +// Generate issue ID +const inputType = parseInput(userInput) +let issueId, issueTitle, issueContent + +if (inputType.type === 'github') { + // Fetch via gh CLI + const issueData = Bash(`gh issue view ${inputType.number} --repo ${inputType.owner}/${inputType.repo} --json title,body,labels`) + const parsed = JSON.parse(issueData) + issueId = `GH-${inputType.number}` + issueTitle = parsed.title + issueContent = parsed.body +} else if (inputType.type === 'file') { + issueContent = Read(inputType.path) + issueId = `FILE-${Date.now()}` + issueTitle = extractTitle(issueContent) // First # heading +} else { + issueContent = inputType.content + issueId = `TEXT-${Date.now()}` + issueTitle = issueContent.substring(0, 50) +} + +// Create issue directory +const issueDir = `.workflow/issues/${issueId}` +Bash(`mkdir -p ${issueDir}`) + +// Save issue context +Write(`${issueDir}/context.md`, `# ${issueTitle}\n\n${issueContent}`) +``` + +### Phase 2: Exploration + +```javascript +// Complexity assessment +const complexity = analyzeComplexity(issueContent) +// Low: Single file change, isolated +// Medium: Multiple files, some dependencies +// High: Cross-module, architectural + +const needsExploration = ( + flags.includes('--explore') || + complexity !== 'Low' || + issueContent.mentions_specific_files +) + +if (needsExploration) { + Task( + subagent_type="cli-explore-agent", + run_in_background=false, + description="Explore codebase for issue context", + prompt=` +## Task Objective +Analyze codebase to understand context for issue resolution. + +## Issue Context +Title: ${issueTitle} +Content: ${issueContent} + +## Required Analysis +1. Identify files that need modification +2. Find relevant patterns and conventions +3. Map dependencies and integration points +4. Identify potential risks or blockers + +## Output +Write exploration results to: ${issueDir}/exploration.json +` + ) +} +``` + +### Phase 3: Task Breakdown + +```javascript +// Load schema reference +const schema = Read('~/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json') + +// Generate tasks via CLI +Task( + subagent_type="cli-lite-planning-agent", + run_in_background=false, + description="Generate JSONL task breakdown", + prompt=` +## Objective +Break down the issue into executable tasks in JSONL format. + +## Issue Context +ID: ${issueId} +Title: ${issueTitle} +Content: ${issueContent} + +## Exploration Results +${explorationResults || 'No exploration performed'} + +## Task Schema +${schema} + +## Requirements +1. Generate 2-10 tasks depending on complexity +2. Each task MUST include: + - delivery_criteria: Specific, verifiable conditions for completion (2-5 items) + - pause_criteria: Conditions that should halt execution (0-3 items) + - depends_on: Task IDs that must complete first (ensure DAG) +3. Task execution phases: analyze → implement → test → optimize → commit +4. Assign executor based on task nature (analysis=gemini, implementation=codex) + +## Delivery Criteria Examples +Good: "User login endpoint returns JWT token with 24h expiry" +Bad: "Authentication works" (too vague) + +## Pause Criteria Examples +- "API spec for external service unclear" +- "Database schema migration required" +- "Security review needed before implementation" + +## Output Format +Write JSONL file (one JSON object per line): +${issueDir}/tasks.jsonl + +## Validation +- Ensure no circular dependencies +- Ensure all depends_on references exist +- Ensure at least one task has empty depends_on (entry point) +` +) + +// Validate DAG +const tasks = readJsonl(`${issueDir}/tasks.jsonl`) +validateDAG(tasks) // Throws if circular dependency detected +``` + +### Phase 4: User Confirmation + +```javascript +// Display task summary +const tasks = readJsonl(`${issueDir}/tasks.jsonl`) + +console.log(` +## Issue Plan: ${issueId} + +**Title**: ${issueTitle} +**Tasks**: ${tasks.length} +**Complexity**: ${complexity} + +### Task Breakdown + +| ID | Title | Type | Dependencies | Delivery Criteria | +|----|-------|------|--------------|-------------------| +${tasks.map(t => `| ${t.id} | ${t.title} | ${t.type} | ${t.depends_on.join(', ') || '-'} | ${t.delivery_criteria.length} items |`).join('\n')} + +### Dependency Graph +${generateDependencyGraph(tasks)} +`) + +// User confirmation +AskUserQuestion({ + questions: [ + { + question: `Approve issue plan? (${tasks.length} tasks)`, + header: "Confirm", + multiSelect: false, + options: [ + { label: "Approve", description: "Proceed with this plan" }, + { label: "Refine", description: "Modify tasks before proceeding" }, + { label: "Cancel", description: "Discard plan" } + ] + } + ] +}) + +if (answer === "Refine") { + // Allow editing specific tasks + AskUserQuestion({ + questions: [ + { + question: "What would you like to refine?", + header: "Refine", + multiSelect: true, + options: [ + { label: "Add Task", description: "Add a new task" }, + { label: "Remove Task", description: "Remove an existing task" }, + { label: "Modify Dependencies", description: "Change task dependencies" }, + { label: "Regenerate", description: "Regenerate entire plan" } + ] + } + ] + }) +} +``` + +### Phase 5: Persistence + +```javascript +// Initialize state.json for status tracking +const state = { + issue_id: issueId, + title: issueTitle, + status: 'planned', + created_at: getUtc8ISOString(), + updated_at: getUtc8ISOString(), + task_count: tasks.length, + completed_count: 0, + current_task: null, + executor_default: flags.executor || 'auto' +} + +Write(`${issueDir}/state.json`, JSON.stringify(state, null, 2)) + +console.log(` +## Plan Created + +**Issue**: ${issueId} +**Location**: ${issueDir}/ +**Tasks**: ${tasks.length} + +### Files Created +- tasks.jsonl (task definitions) +- state.json (execution state) +- context.md (issue context) +${explorationResults ? '- exploration.json (codebase analysis)' : ''} + +### Next Steps +1. Review: \`ccw issue list ${issueId}\` +2. Execute: \`/issue:execute ${issueId}\` +3. Monitor: \`ccw issue status ${issueId}\` +`) +``` + +## JSONL Task Format + +Each line in `tasks.jsonl` is a complete JSON object: + +```json +{"id":"TASK-001","title":"Setup auth middleware","type":"feature","description":"Implement JWT verification middleware","file_context":["src/middleware/","src/config/auth.ts"],"depends_on":[],"delivery_criteria":["Middleware validates JWT tokens","Returns 401 for invalid tokens","Passes existing auth tests"],"pause_criteria":["JWT secret configuration unclear"],"status":"pending","current_phase":"analyze","executor":"auto","priority":1,"created_at":"2025-12-26T10:00:00Z","updated_at":"2025-12-26T10:00:00Z"} +{"id":"TASK-002","title":"Protect API routes","type":"feature","description":"Apply auth middleware to /api/v1/* routes","file_context":["src/routes/api/"],"depends_on":["TASK-001"],"delivery_criteria":["All /api/v1/* routes require auth","Public routes excluded","Integration tests pass"],"pause_criteria":[],"status":"pending","current_phase":"analyze","executor":"auto","priority":2,"created_at":"2025-12-26T10:00:00Z","updated_at":"2025-12-26T10:00:00Z"} +``` + +## Progressive Loading Algorithm + +For large task lists, only load tasks with satisfied dependencies: + +```javascript +function getReadyTasks(tasks, completedIds) { + return tasks.filter(task => + task.status === 'pending' && + task.depends_on.every(dep => completedIds.has(dep)) + ) +} + +// Stream JSONL line-by-line for memory efficiency +function* streamJsonl(filePath) { + const lines = readLines(filePath) + for (const line of lines) { + if (line.trim()) yield JSON.parse(line) + } +} +``` + +## Error Handling + +| Error | Resolution | +|-------|------------| +| Invalid GitHub URL | Display correct format, ask for valid URL | +| Circular dependency | List cycle, ask user to resolve | +| No tasks generated | Suggest simpler breakdown or manual entry | +| Exploration timeout | Proceed without exploration, warn user | + +## Related Commands + +- `/issue:execute` - Execute planned tasks with closed-loop methodology +- `ccw issue list` - List all issues and their status +- `ccw issue status` - Show detailed issue status diff --git a/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json b/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json new file mode 100644 index 00000000..9f6e80ed --- /dev/null +++ b/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json @@ -0,0 +1,136 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Issue Task JSONL Schema", + "description": "Schema for individual task entries in tasks.jsonl file", + "type": "object", + "required": ["id", "title", "type", "description", "depends_on", "delivery_criteria", "status", "current_phase", "executor"], + "properties": { + "id": { + "type": "string", + "description": "Unique task identifier (e.g., TASK-001)", + "pattern": "^TASK-[0-9]+$" + }, + "title": { + "type": "string", + "description": "Short summary of the task", + "maxLength": 100 + }, + "type": { + "type": "string", + "enum": ["feature", "bug", "refactor", "test", "chore", "docs"], + "description": "Task category" + }, + "description": { + "type": "string", + "description": "Detailed instructions for the task" + }, + "file_context": { + "type": "array", + "items": { "type": "string" }, + "description": "List of relevant files/globs", + "default": [] + }, + "depends_on": { + "type": "array", + "items": { "type": "string" }, + "description": "Array of Task IDs that must complete first", + "default": [] + }, + "delivery_criteria": { + "type": "array", + "items": { "type": "string" }, + "description": "Checklist items that define task completion", + "minItems": 1 + }, + "pause_criteria": { + "type": "array", + "items": { "type": "string" }, + "description": "Conditions that should halt execution (e.g., 'API spec unclear')", + "default": [] + }, + "status": { + "type": "string", + "enum": ["pending", "ready", "in_progress", "completed", "failed", "paused", "skipped"], + "description": "Current task status", + "default": "pending" + }, + "current_phase": { + "type": "string", + "enum": ["analyze", "implement", "test", "optimize", "commit", "done"], + "description": "Current execution phase within the task lifecycle", + "default": "analyze" + }, + "executor": { + "type": "string", + "enum": ["agent", "codex", "gemini", "auto"], + "description": "Preferred executor for this task", + "default": "auto" + }, + "priority": { + "type": "integer", + "minimum": 1, + "maximum": 5, + "description": "Task priority (1=highest, 5=lowest)", + "default": 3 + }, + "phase_results": { + "type": "object", + "description": "Results from each execution phase", + "properties": { + "analyze": { + "type": "object", + "properties": { + "status": { "type": "string" }, + "findings": { "type": "array", "items": { "type": "string" } }, + "timestamp": { "type": "string", "format": "date-time" } + } + }, + "implement": { + "type": "object", + "properties": { + "status": { "type": "string" }, + "files_modified": { "type": "array", "items": { "type": "string" } }, + "timestamp": { "type": "string", "format": "date-time" } + } + }, + "test": { + "type": "object", + "properties": { + "status": { "type": "string" }, + "test_results": { "type": "string" }, + "retry_count": { "type": "integer" }, + "timestamp": { "type": "string", "format": "date-time" } + } + }, + "optimize": { + "type": "object", + "properties": { + "status": { "type": "string" }, + "improvements": { "type": "array", "items": { "type": "string" } }, + "timestamp": { "type": "string", "format": "date-time" } + } + }, + "commit": { + "type": "object", + "properties": { + "status": { "type": "string" }, + "commit_hash": { "type": "string" }, + "message": { "type": "string" }, + "timestamp": { "type": "string", "format": "date-time" } + } + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Task creation timestamp" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + } + }, + "additionalProperties": false +} diff --git a/ccw/src/cli.ts b/ccw/src/cli.ts index 16fe60cc..20439cd8 100644 --- a/ccw/src/cli.ts +++ b/ccw/src/cli.ts @@ -12,6 +12,7 @@ import { cliCommand } from './commands/cli.js'; import { memoryCommand } from './commands/memory.js'; import { coreMemoryCommand } from './commands/core-memory.js'; import { hookCommand } from './commands/hook.js'; +import { issueCommand } from './commands/issue.js'; import { readFileSync, existsSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; @@ -260,5 +261,24 @@ export function run(argv: string[]): void { .option('--type ', 'Context type: session-start, context') .action((subcommand, args, options) => hookCommand(subcommand, args, options)); + // Issue command - Issue lifecycle management with JSONL task tracking + program + .command('issue [subcommand] [args...]') + .description('Issue lifecycle management with JSONL task tracking') + .option('--title ', 'Task title') + .option('--type <type>', 'Task type: feature, bug, refactor, test, chore, docs') + .option('--status <status>', 'Task status') + .option('--phase <phase>', 'Execution phase') + .option('--description <desc>', 'Task description') + .option('--depends-on <ids>', 'Comma-separated dependency task IDs') + .option('--delivery-criteria <items>', 'Pipe-separated delivery criteria') + .option('--pause-criteria <items>', 'Pipe-separated pause criteria') + .option('--executor <type>', 'Executor: agent, codex, gemini, auto') + .option('--priority <n>', 'Task priority (1-5)') + .option('--format <fmt>', 'Output format: json, markdown') + .option('--json', 'Output as JSON') + .option('--force', 'Force operation') + .action((subcommand, args, options) => issueCommand(subcommand, args, options)); + program.parse(argv); } diff --git a/ccw/src/commands/issue.ts b/ccw/src/commands/issue.ts new file mode 100644 index 00000000..6e8d8c16 --- /dev/null +++ b/ccw/src/commands/issue.ts @@ -0,0 +1,722 @@ +/** + * Issue Command - Issue lifecycle management with JSONL task tracking + * Supports: init, list, add, update, status, export, retry, clean + */ + +import chalk from 'chalk'; +import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs'; +import { join, resolve } from 'path'; + +// Handle EPIPE errors gracefully +process.stdout.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EPIPE') { + process.exit(0); + } + throw err; +}); + +interface IssueTask { + id: string; + title: string; + type: 'feature' | 'bug' | 'refactor' | 'test' | 'chore' | 'docs'; + description: string; + file_context: string[]; + depends_on: string[]; + delivery_criteria: string[]; + pause_criteria: string[]; + status: 'pending' | 'ready' | 'in_progress' | 'completed' | 'failed' | 'paused' | 'skipped'; + current_phase: 'analyze' | 'implement' | 'test' | 'optimize' | 'commit' | 'done'; + executor: 'agent' | 'codex' | 'gemini' | 'auto'; + priority: number; + phase_results?: Record<string, any>; + created_at: string; + updated_at: string; +} + +interface IssueState { + issue_id: string; + title: string; + status: 'planned' | 'in_progress' | 'completed' | 'paused' | 'failed'; + created_at: string; + updated_at: string; + task_count: number; + completed_count: number; + current_task: string | null; + executor_default: string; +} + +interface IssueOptions { + status?: string; + phase?: string; + title?: string; + type?: string; + description?: string; + dependsOn?: string; + deliveryCriteria?: string; + pauseCriteria?: string; + executor?: string; + priority?: string; + format?: string; + force?: boolean; + json?: boolean; +} + +const ISSUES_DIR = '.workflow/issues'; + +/** + * Get project root (where .workflow exists or should be created) + */ +function getProjectRoot(): string { + let dir = process.cwd(); + while (dir !== resolve(dir, '..')) { + if (existsSync(join(dir, '.workflow')) || existsSync(join(dir, '.git'))) { + return dir; + } + dir = resolve(dir, '..'); + } + return process.cwd(); +} + +/** + * Get issues directory path + */ +function getIssuesDir(): string { + const projectRoot = getProjectRoot(); + return join(projectRoot, ISSUES_DIR); +} + +/** + * Get issue directory path + */ +function getIssueDir(issueId: string): string { + return join(getIssuesDir(), issueId); +} + +/** + * Read JSONL file into array of tasks + */ +function readJsonl(filePath: string): IssueTask[] { + if (!existsSync(filePath)) return []; + const content = readFileSync(filePath, 'utf-8'); + return content.split('\n') + .filter(line => line.trim()) + .map(line => JSON.parse(line)); +} + +/** + * Write tasks to JSONL file + */ +function writeJsonl(filePath: string, tasks: IssueTask[]): void { + const content = tasks.map(t => JSON.stringify(t)).join('\n'); + writeFileSync(filePath, content, 'utf-8'); +} + +/** + * Read issue state + */ +function readState(issueId: string): IssueState | null { + const statePath = join(getIssueDir(issueId), 'state.json'); + if (!existsSync(statePath)) return null; + return JSON.parse(readFileSync(statePath, 'utf-8')); +} + +/** + * Write issue state + */ +function writeState(issueId: string, state: IssueState): void { + const statePath = join(getIssueDir(issueId), 'state.json'); + writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf-8'); +} + +/** + * Generate next task ID + */ +function generateTaskId(tasks: IssueTask[]): string { + const maxNum = tasks.reduce((max, t) => { + const match = t.id.match(/^TASK-(\d+)$/); + return match ? Math.max(max, parseInt(match[1])) : max; + }, 0); + return `TASK-${String(maxNum + 1).padStart(3, '0')}`; +} + +/** + * Initialize a new issue + */ +async function initAction(issueId: string | undefined, options: IssueOptions): Promise<void> { + if (!issueId) { + console.error(chalk.red('Issue ID is required')); + console.error(chalk.gray('Usage: ccw issue init <issue-id> [--title "..."]')); + process.exit(1); + } + + const issueDir = getIssueDir(issueId); + + if (existsSync(issueDir) && !options.force) { + console.error(chalk.red(`Issue "${issueId}" already exists`)); + console.error(chalk.gray('Use --force to reinitialize')); + process.exit(1); + } + + // Create directory + mkdirSync(issueDir, { recursive: true }); + + // Initialize state + const state: IssueState = { + issue_id: issueId, + title: options.title || issueId, + status: 'planned', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + task_count: 0, + completed_count: 0, + current_task: null, + executor_default: options.executor || 'auto' + }; + + writeState(issueId, state); + + // Create empty tasks.jsonl + writeFileSync(join(issueDir, 'tasks.jsonl'), '', 'utf-8'); + + // Create context.md placeholder + writeFileSync(join(issueDir, 'context.md'), `# ${options.title || issueId}\n\n<!-- Issue context will be added here -->\n`, 'utf-8'); + + console.log(chalk.green(`✓ Issue "${issueId}" initialized`)); + console.log(chalk.gray(` Location: ${issueDir}`)); + console.log(chalk.gray(` Next: ccw issue add ${issueId} --title "Task title"`)); +} + +/** + * List issues or tasks within an issue + */ +async function listAction(issueId: string | undefined, options: IssueOptions): Promise<void> { + const issuesDir = getIssuesDir(); + + if (!issueId) { + // List all issues + if (!existsSync(issuesDir)) { + console.log(chalk.yellow('No issues found')); + console.log(chalk.gray('Create one with: ccw issue init <issue-id>')); + return; + } + + const issues = readdirSync(issuesDir, { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => { + const state = readState(d.name); + return state || { issue_id: d.name, status: 'unknown', task_count: 0, completed_count: 0 }; + }); + + if (options.json) { + console.log(JSON.stringify(issues, null, 2)); + return; + } + + console.log(chalk.bold.cyan('\nIssues\n')); + console.log(chalk.gray('ID'.padEnd(20) + 'Status'.padEnd(15) + 'Progress'.padEnd(15) + 'Title')); + console.log(chalk.gray('-'.repeat(70))); + + for (const issue of issues) { + const statusColor = { + 'planned': chalk.blue, + 'in_progress': chalk.yellow, + 'completed': chalk.green, + 'paused': chalk.magenta, + 'failed': chalk.red + }[issue.status as string] || chalk.gray; + + const progress = `${issue.completed_count}/${issue.task_count}`; + console.log( + issue.issue_id.padEnd(20) + + statusColor(issue.status.padEnd(15)) + + progress.padEnd(15) + + ((issue as IssueState).title || '') + ); + } + return; + } + + // List tasks within an issue + const issueDir = getIssueDir(issueId); + if (!existsSync(issueDir)) { + console.error(chalk.red(`Issue "${issueId}" not found`)); + process.exit(1); + } + + const tasks = readJsonl(join(issueDir, 'tasks.jsonl')); + const state = readState(issueId); + + if (options.json) { + console.log(JSON.stringify({ state, tasks }, null, 2)); + return; + } + + // Filter by status if specified + const filteredTasks = options.status + ? tasks.filter(t => t.status === options.status) + : tasks; + + console.log(chalk.bold.cyan(`\nIssue: ${issueId}\n`)); + if (state) { + console.log(chalk.gray(`Status: ${state.status} | Progress: ${state.completed_count}/${state.task_count}`)); + } + console.log(); + + if (filteredTasks.length === 0) { + console.log(chalk.yellow('No tasks found')); + return; + } + + console.log(chalk.gray('ID'.padEnd(12) + 'Status'.padEnd(12) + 'Phase'.padEnd(12) + 'Deps'.padEnd(10) + 'Title')); + console.log(chalk.gray('-'.repeat(80))); + + for (const task of filteredTasks) { + const statusColor = { + 'pending': chalk.gray, + 'ready': chalk.blue, + 'in_progress': chalk.yellow, + 'completed': chalk.green, + 'failed': chalk.red, + 'paused': chalk.magenta, + 'skipped': chalk.gray + }[task.status] || chalk.white; + + const deps = task.depends_on.length > 0 ? task.depends_on.join(',') : '-'; + console.log( + task.id.padEnd(12) + + statusColor(task.status.padEnd(12)) + + task.current_phase.padEnd(12) + + deps.padEnd(10) + + task.title.substring(0, 40) + ); + } +} + +/** + * Add a new task to an issue + */ +async function addAction(issueId: string | undefined, options: IssueOptions): Promise<void> { + if (!issueId) { + console.error(chalk.red('Issue ID is required')); + console.error(chalk.gray('Usage: ccw issue add <issue-id> --title "..." [--depends-on "TASK-001,TASK-002"]')); + process.exit(1); + } + + if (!options.title) { + console.error(chalk.red('Task title is required (--title)')); + process.exit(1); + } + + const issueDir = getIssueDir(issueId); + if (!existsSync(issueDir)) { + console.error(chalk.red(`Issue "${issueId}" not found. Run: ccw issue init ${issueId}`)); + process.exit(1); + } + + const tasksPath = join(issueDir, 'tasks.jsonl'); + const tasks = readJsonl(tasksPath); + + // Parse options + const dependsOn = options.dependsOn ? options.dependsOn.split(',').map(s => s.trim()) : []; + const deliveryCriteria = options.deliveryCriteria ? options.deliveryCriteria.split('|').map(s => s.trim()) : ['Task completed successfully']; + const pauseCriteria = options.pauseCriteria ? options.pauseCriteria.split('|').map(s => s.trim()) : []; + + // Validate dependencies + const taskIds = new Set(tasks.map(t => t.id)); + for (const dep of dependsOn) { + if (!taskIds.has(dep)) { + console.error(chalk.red(`Dependency "${dep}" not found`)); + process.exit(1); + } + } + + const newTask: IssueTask = { + id: generateTaskId(tasks), + title: options.title, + type: (options.type as IssueTask['type']) || 'feature', + description: options.description || options.title, + file_context: [], + depends_on: dependsOn, + delivery_criteria: deliveryCriteria, + pause_criteria: pauseCriteria, + status: 'pending', + current_phase: 'analyze', + executor: (options.executor as IssueTask['executor']) || 'auto', + priority: options.priority ? parseInt(options.priority) : 3, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + + tasks.push(newTask); + writeJsonl(tasksPath, tasks); + + // Update state + const state = readState(issueId); + if (state) { + state.task_count = tasks.length; + state.updated_at = new Date().toISOString(); + writeState(issueId, state); + } + + console.log(chalk.green(`✓ Task ${newTask.id} added to ${issueId}`)); + console.log(chalk.gray(` Title: ${newTask.title}`)); + if (dependsOn.length > 0) { + console.log(chalk.gray(` Depends on: ${dependsOn.join(', ')}`)); + } +} + +/** + * Update task status or properties + */ +async function updateAction(issueId: string | undefined, taskId: string | undefined, options: IssueOptions): Promise<void> { + if (!issueId || !taskId) { + console.error(chalk.red('Issue ID and Task ID are required')); + console.error(chalk.gray('Usage: ccw issue update <issue-id> <task-id> --status completed')); + process.exit(1); + } + + const tasksPath = join(getIssueDir(issueId), 'tasks.jsonl'); + if (!existsSync(tasksPath)) { + console.error(chalk.red(`Issue "${issueId}" not found`)); + process.exit(1); + } + + const tasks = readJsonl(tasksPath); + const taskIndex = tasks.findIndex(t => t.id === taskId); + + if (taskIndex === -1) { + console.error(chalk.red(`Task "${taskId}" not found in issue "${issueId}"`)); + process.exit(1); + } + + const task = tasks[taskIndex]; + const updates: string[] = []; + + if (options.status) { + task.status = options.status as IssueTask['status']; + updates.push(`status → ${options.status}`); + } + if (options.phase) { + task.current_phase = options.phase as IssueTask['current_phase']; + updates.push(`phase → ${options.phase}`); + } + if (options.title) { + task.title = options.title; + updates.push(`title → ${options.title}`); + } + if (options.executor) { + task.executor = options.executor as IssueTask['executor']; + updates.push(`executor → ${options.executor}`); + } + + task.updated_at = new Date().toISOString(); + tasks[taskIndex] = task; + writeJsonl(tasksPath, tasks); + + // Update state + const state = readState(issueId); + if (state) { + state.completed_count = tasks.filter(t => t.status === 'completed').length; + state.current_task = task.status === 'in_progress' ? taskId : state.current_task; + state.updated_at = new Date().toISOString(); + writeState(issueId, state); + } + + console.log(chalk.green(`✓ Task ${taskId} updated`)); + updates.forEach(u => console.log(chalk.gray(` ${u}`))); +} + +/** + * Show detailed issue/task status + */ +async function statusAction(issueId: string | undefined, taskId: string | undefined, options: IssueOptions): Promise<void> { + if (!issueId) { + console.error(chalk.red('Issue ID is required')); + console.error(chalk.gray('Usage: ccw issue status <issue-id> [task-id]')); + process.exit(1); + } + + const issueDir = getIssueDir(issueId); + if (!existsSync(issueDir)) { + console.error(chalk.red(`Issue "${issueId}" not found`)); + process.exit(1); + } + + const state = readState(issueId); + const tasks = readJsonl(join(issueDir, 'tasks.jsonl')); + + if (taskId) { + // Show specific task + const task = tasks.find(t => t.id === taskId); + if (!task) { + console.error(chalk.red(`Task "${taskId}" not found`)); + process.exit(1); + } + + if (options.json) { + console.log(JSON.stringify(task, null, 2)); + return; + } + + console.log(chalk.bold.cyan(`\nTask: ${task.id}\n`)); + console.log(`Title: ${task.title}`); + console.log(`Type: ${task.type}`); + console.log(`Status: ${task.status}`); + console.log(`Phase: ${task.current_phase}`); + console.log(`Executor: ${task.executor}`); + console.log(`Priority: ${task.priority}`); + console.log(); + console.log(chalk.bold('Description:')); + console.log(task.description); + console.log(); + console.log(chalk.bold('Delivery Criteria:')); + task.delivery_criteria.forEach((c, i) => console.log(` ${i + 1}. ${c}`)); + if (task.pause_criteria.length > 0) { + console.log(); + console.log(chalk.bold('Pause Criteria:')); + task.pause_criteria.forEach((c, i) => console.log(` ${i + 1}. ${c}`)); + } + if (task.depends_on.length > 0) { + console.log(); + console.log(chalk.bold('Dependencies:')); + task.depends_on.forEach(d => console.log(` - ${d}`)); + } + if (task.phase_results) { + console.log(); + console.log(chalk.bold('Phase Results:')); + console.log(JSON.stringify(task.phase_results, null, 2)); + } + return; + } + + // Show issue overview + if (options.json) { + console.log(JSON.stringify({ state, tasks }, null, 2)); + return; + } + + console.log(chalk.bold.cyan(`\nIssue: ${issueId}\n`)); + if (state) { + console.log(`Title: ${state.title}`); + console.log(`Status: ${state.status}`); + console.log(`Progress: ${state.completed_count}/${state.task_count} tasks`); + console.log(`Current: ${state.current_task || 'none'}`); + console.log(`Created: ${state.created_at}`); + console.log(`Updated: ${state.updated_at}`); + } + + // Task summary by status + const byStatus: Record<string, number> = {}; + tasks.forEach(t => { + byStatus[t.status] = (byStatus[t.status] || 0) + 1; + }); + + console.log(); + console.log(chalk.bold('Task Summary:')); + Object.entries(byStatus).forEach(([status, count]) => { + console.log(` ${status}: ${count}`); + }); + + // Dependency graph + const readyTasks = tasks.filter(t => + t.status === 'pending' && + t.depends_on.every(dep => tasks.find(tt => tt.id === dep)?.status === 'completed') + ); + + if (readyTasks.length > 0) { + console.log(); + console.log(chalk.bold('Ready to Execute:')); + readyTasks.forEach(t => console.log(` ${t.id}: ${t.title}`)); + } +} + +/** + * Export issue to markdown + */ +async function exportAction(issueId: string | undefined, options: IssueOptions): Promise<void> { + if (!issueId) { + console.error(chalk.red('Issue ID is required')); + console.error(chalk.gray('Usage: ccw issue export <issue-id>')); + process.exit(1); + } + + const issueDir = getIssueDir(issueId); + if (!existsSync(issueDir)) { + console.error(chalk.red(`Issue "${issueId}" not found`)); + process.exit(1); + } + + const state = readState(issueId); + const tasks = readJsonl(join(issueDir, 'tasks.jsonl')); + + const markdown = `# ${state?.title || issueId} + +## Progress: ${state?.completed_count || 0}/${state?.task_count || 0} + +## Tasks + +${tasks.map(t => { + const checkbox = t.status === 'completed' ? '[x]' : '[ ]'; + const deps = t.depends_on.length > 0 ? ` (after: ${t.depends_on.join(', ')})` : ''; + return `- ${checkbox} **${t.id}**: ${t.title}${deps} + - Criteria: ${t.delivery_criteria.join('; ')}`; +}).join('\n')} + +--- +*Generated by CCW Issue Tracker* +`; + + if (options.format === 'json') { + console.log(JSON.stringify({ state, tasks }, null, 2)); + } else { + console.log(markdown); + } +} + +/** + * Retry failed tasks + */ +async function retryAction(issueId: string | undefined, taskId: string | undefined, options: IssueOptions): Promise<void> { + if (!issueId) { + console.error(chalk.red('Issue ID is required')); + console.error(chalk.gray('Usage: ccw issue retry <issue-id> [task-id]')); + process.exit(1); + } + + const tasksPath = join(getIssueDir(issueId), 'tasks.jsonl'); + if (!existsSync(tasksPath)) { + console.error(chalk.red(`Issue "${issueId}" not found`)); + process.exit(1); + } + + const tasks = readJsonl(tasksPath); + let updated = 0; + + for (const task of tasks) { + if ((taskId && task.id === taskId) || (!taskId && task.status === 'failed')) { + task.status = 'pending'; + task.current_phase = 'analyze'; + task.updated_at = new Date().toISOString(); + updated++; + } + } + + if (updated === 0) { + console.log(chalk.yellow('No failed tasks to retry')); + return; + } + + writeJsonl(tasksPath, tasks); + + // Update state + const state = readState(issueId); + if (state) { + state.updated_at = new Date().toISOString(); + writeState(issueId, state); + } + + console.log(chalk.green(`✓ Reset ${updated} task(s) to pending`)); +} + +/** + * Clean completed issues + */ +async function cleanAction(options: IssueOptions): Promise<void> { + const issuesDir = getIssuesDir(); + if (!existsSync(issuesDir)) { + console.log(chalk.yellow('No issues to clean')); + return; + } + + const issues = readdirSync(issuesDir, { withFileTypes: true }) + .filter(d => d.isDirectory()); + + let cleaned = 0; + for (const issue of issues) { + const state = readState(issue.name); + if (state?.status === 'completed') { + if (!options.force) { + console.log(chalk.gray(`Would remove: ${issue.name}`)); + } else { + // Actually remove (implement if needed) + console.log(chalk.green(`✓ Cleaned: ${issue.name}`)); + } + cleaned++; + } + } + + if (cleaned === 0) { + console.log(chalk.yellow('No completed issues to clean')); + } else if (!options.force) { + console.log(chalk.gray(`\nUse --force to actually remove ${cleaned} issue(s)`)); + } +} + +/** + * Issue command entry point + */ +export async function issueCommand( + subcommand: string, + args: string | string[], + options: IssueOptions +): Promise<void> { + const argsArray = Array.isArray(args) ? args : (args ? [args] : []); + + switch (subcommand) { + case 'init': + await initAction(argsArray[0], options); + break; + case 'list': + await listAction(argsArray[0], options); + break; + case 'add': + await addAction(argsArray[0], options); + break; + case 'update': + await updateAction(argsArray[0], argsArray[1], options); + break; + case 'status': + await statusAction(argsArray[0], argsArray[1], options); + break; + case 'export': + await exportAction(argsArray[0], options); + break; + case 'retry': + await retryAction(argsArray[0], argsArray[1], options); + break; + case 'clean': + await cleanAction(options); + break; + default: + console.log(chalk.bold.cyan('\nCCW Issue Management\n')); + console.log('Commands:'); + console.log(chalk.gray(' init <issue-id> Initialize new issue')); + console.log(chalk.gray(' list [issue-id] List issues or tasks')); + console.log(chalk.gray(' add <issue-id> --title "..." Add task to issue')); + console.log(chalk.gray(' update <issue-id> <task-id> Update task properties')); + console.log(chalk.gray(' status <issue-id> [task-id] Show detailed status')); + console.log(chalk.gray(' export <issue-id> Export to markdown')); + console.log(chalk.gray(' retry <issue-id> [task-id] Retry failed tasks')); + console.log(chalk.gray(' clean Clean completed issues')); + console.log(); + console.log('Options:'); + console.log(chalk.gray(' --title <title> Task title')); + console.log(chalk.gray(' --type <type> Task type (feature|bug|refactor|test|chore|docs)')); + console.log(chalk.gray(' --status <status> Task status')); + console.log(chalk.gray(' --phase <phase> Execution phase')); + console.log(chalk.gray(' --depends-on <ids> Comma-separated dependency IDs')); + console.log(chalk.gray(' --delivery-criteria <items> Pipe-separated criteria')); + console.log(chalk.gray(' --pause-criteria <items> Pipe-separated pause conditions')); + console.log(chalk.gray(' --executor <type> Executor (agent|codex|gemini|auto)')); + console.log(chalk.gray(' --json Output as JSON')); + console.log(chalk.gray(' --force Force operation')); + console.log(); + console.log('Examples:'); + console.log(chalk.gray(' ccw issue init GH-123 --title "Add authentication"')); + console.log(chalk.gray(' ccw issue add GH-123 --title "Setup JWT middleware" --type feature')); + console.log(chalk.gray(' ccw issue add GH-123 --title "Protect routes" --depends-on TASK-001')); + console.log(chalk.gray(' ccw issue list GH-123')); + console.log(chalk.gray(' ccw issue status GH-123 TASK-001')); + console.log(chalk.gray(' ccw issue update GH-123 TASK-001 --status completed')); + } +}