mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat(issue): implement JSONL task generation and management for issue resolution
- Added `/issue:plan` command to generate a structured task plan from GitHub issues or descriptions, including delivery and pause criteria, and a dependency graph. - Introduced JSONL schema for task entries to enforce structure and validation. - Developed comprehensive issue command with functionalities for initializing, listing, adding, updating, and exporting tasks. - Implemented error handling and user feedback for various operations within the issue management workflow. - Enhanced task management with priority levels, phase results, and executor preferences.
This commit is contained in:
591
.claude/commands/issue/execute.md
Normal file
591
.claude/commands/issue/execute.md
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
---
|
||||||
|
name: execute
|
||||||
|
description: Execute issue tasks with closed-loop methodology (analyze→implement→test→optimize→commit)
|
||||||
|
argument-hint: "<issue-id> [--task <task-id>] [--batch <n>]"
|
||||||
|
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 <ISSUE_ID> [FLAGS]
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
<issue-id> Issue ID (e.g., GH-123, TEXT-1735200000)
|
||||||
|
|
||||||
|
# Flags
|
||||||
|
--task <id> Execute specific task only
|
||||||
|
--batch <n> 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 <noreply@anthropic.com>`
|
||||||
|
|
||||||
|
// 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
|
||||||
361
.claude/commands/issue/plan.md
Normal file
361
.claude/commands/issue/plan.md
Normal file
@@ -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>
|
||||||
|
|
||||||
|
# Input Formats
|
||||||
|
<issue-url> GitHub issue URL (e.g., https://github.com/owner/repo/issues/123)
|
||||||
|
<description> Text description of the issue
|
||||||
|
<file.md> Markdown file with issue details
|
||||||
|
|
||||||
|
# Flags
|
||||||
|
-e, --explore Force code exploration phase
|
||||||
|
--executor <type> 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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { cliCommand } from './commands/cli.js';
|
|||||||
import { memoryCommand } from './commands/memory.js';
|
import { memoryCommand } from './commands/memory.js';
|
||||||
import { coreMemoryCommand } from './commands/core-memory.js';
|
import { coreMemoryCommand } from './commands/core-memory.js';
|
||||||
import { hookCommand } from './commands/hook.js';
|
import { hookCommand } from './commands/hook.js';
|
||||||
|
import { issueCommand } from './commands/issue.js';
|
||||||
import { readFileSync, existsSync } from 'fs';
|
import { readFileSync, existsSync } from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
@@ -260,5 +261,24 @@ export function run(argv: string[]): void {
|
|||||||
.option('--type <type>', 'Context type: session-start, context')
|
.option('--type <type>', 'Context type: session-start, context')
|
||||||
.action((subcommand, args, options) => hookCommand(subcommand, args, options));
|
.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 <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);
|
program.parse(argv);
|
||||||
}
|
}
|
||||||
|
|||||||
722
ccw/src/commands/issue.ts
Normal file
722
ccw/src/commands/issue.ts
Normal file
@@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user