Refactor issue management commands and introduce lifecycle requirements

- Updated lifecycle requirements in issue creation to include new fields for testing, regression, acceptance, and commit strategies.
- Enhanced the planning command to generate structured output and handle multi-solution scenarios.
- Improved queue formation logic to ensure valid DAG and conflict resolution.
- Introduced a new interactive issue management skill for CRUD operations, allowing users to manage issues through a menu-driven interface.
- Updated documentation across commands to reflect changes in task structure and output requirements.
This commit is contained in:
catlog22
2025-12-27 22:44:49 +08:00
parent b58589ddad
commit 726151bfea
7 changed files with 746 additions and 2627 deletions

View File

@@ -20,17 +20,23 @@ Interactive menu-driven interface for issue management using `ccw issue` CLI end
```bash
# Core endpoints (ccw issue)
ccw issue list # List all issues
ccw issue list <id> --json # Get issue details
ccw issue status <id> # Detailed status
ccw issue init <id> --title "..." # Create issue
ccw issue task <id> --title "..." # Add task
ccw issue list # List all issues
ccw issue list <id> --json # Get issue details
ccw issue status <id> # Detailed status
ccw issue init <id> --title "..." # Create issue
ccw issue task <id> --title "..." # Add task
ccw issue bind <id> <solution-id> # Bind solution
# Queue management
ccw issue queue # List queue
ccw issue queue add <id> # Add to queue
ccw issue next # Get next task
ccw issue complete <item-id> # Complete task
ccw issue queue # List current queue
ccw issue queue add <id> # Add to queue
ccw issue queue list # Queue history
ccw issue queue switch <queue-id> # Switch queue
ccw issue queue archive # Archive queue
ccw issue queue delete <queue-id> # Delete queue
ccw issue next # Get next task
ccw issue done <queue-id> # Mark completed
ccw issue complete <item-id> # (legacy alias for done)
```
## Usage
@@ -49,7 +55,9 @@ ccw issue complete <item-id> # Complete task
## Implementation
### Phase 1: Entry Point
This command delegates to the `issue-manage` skill for detailed implementation.
### Entry Point
```javascript
const issueId = parseIssueId(userInput);
@@ -63,787 +71,30 @@ if (!action) {
}
```
### Phase 2: Main Menu
### Main Menu Flow
```javascript
async function showMainMenu(preselectedIssue = null) {
// Fetch current issues summary
const issuesResult = Bash('ccw issue list --json 2>/dev/null || echo "[]"');
const issues = JSON.parse(issuesResult) || [];
const queueResult = Bash('ccw issue status --json 2>/dev/null');
const queueStatus = JSON.parse(queueResult || '{}');
console.log(`
## Issue Management Dashboard
1. **Dashboard**: Fetch issues summary via `ccw issue list --json`
2. **Menu**: Present action options via AskUserQuestion
3. **Route**: Execute selected action (List/View/Edit/Delete/Bulk)
4. **Loop**: Return to menu after each action
**Total Issues**: ${issues.length}
**Queue Status**: ${queueStatus.queue?.total_tasks || 0} tasks (${queueStatus.queue?.pending_count || 0} pending)
### Available Actions
### Quick Stats
- Registered: ${issues.filter(i => i.status === 'registered').length}
- Planned: ${issues.filter(i => i.status === 'planned').length}
- Executing: ${issues.filter(i => i.status === 'executing').length}
- Completed: ${issues.filter(i => i.status === 'completed').length}
`);
| Action | Description | CLI Command |
|--------|-------------|-------------|
| List | Browse with filters | `ccw issue list --json` |
| View | Detail view | `ccw issue status <id> --json` |
| Edit | Modify fields | Update `issues.jsonl` |
| Delete | Remove issue | Clean up all related files |
| Bulk | Batch operations | Multi-select + batch update |
const answer = AskUserQuestion({
questions: [{
question: 'What would you like to do?',
header: 'Action',
multiSelect: false,
options: [
{ label: 'List Issues', description: 'Browse all issues with filters' },
{ label: 'View Issue', description: 'Detailed view of specific issue' },
{ label: 'Create Issue', description: 'Add new issue from text or GitHub' },
{ label: 'Edit Issue', description: 'Modify issue fields' },
{ label: 'Delete Issue', description: 'Remove issue(s)' },
{ label: 'Bulk Operations', description: 'Batch actions on multiple issues' }
]
}]
});
const selected = parseAnswer(answer);
switch (selected) {
case 'List Issues':
await listIssuesInteractive();
break;
case 'View Issue':
await viewIssueInteractive(preselectedIssue);
break;
case 'Create Issue':
await createIssueInteractive();
break;
case 'Edit Issue':
await editIssueInteractive(preselectedIssue);
break;
case 'Delete Issue':
await deleteIssueInteractive(preselectedIssue);
break;
case 'Bulk Operations':
await bulkOperationsInteractive();
break;
}
}
```
## Data Files
### Phase 3: List Issues
```javascript
async function listIssuesInteractive() {
// Ask for filter
const filterAnswer = AskUserQuestion({
questions: [{
question: 'Filter issues by status?',
header: 'Filter',
multiSelect: true,
options: [
{ label: 'All', description: 'Show all issues' },
{ label: 'Registered', description: 'New, unplanned issues' },
{ label: 'Planned', description: 'Issues with bound solutions' },
{ label: 'Queued', description: 'In execution queue' },
{ label: 'Executing', description: 'Currently being worked on' },
{ label: 'Completed', description: 'Finished issues' },
{ label: 'Failed', description: 'Failed issues' }
]
}]
});
const filters = parseMultiAnswer(filterAnswer);
// Fetch and filter issues
const result = Bash('ccw issue list --json');
let issues = JSON.parse(result) || [];
if (!filters.includes('All')) {
const statusMap = {
'Registered': 'registered',
'Planned': 'planned',
'Queued': 'queued',
'Executing': 'executing',
'Completed': 'completed',
'Failed': 'failed'
};
const allowedStatuses = filters.map(f => statusMap[f]).filter(Boolean);
issues = issues.filter(i => allowedStatuses.includes(i.status));
}
if (issues.length === 0) {
console.log('No issues found matching filters.');
return showMainMenu();
}
// Display issues table
console.log(`
## Issues (${issues.length})
| ID | Status | Priority | Title |
|----|--------|----------|-------|
${issues.map(i => `| ${i.id} | ${i.status} | P${i.priority} | ${i.title.substring(0, 40)} |`).join('\n')}
`);
// Ask for action on issue
const actionAnswer = AskUserQuestion({
questions: [{
question: 'Select an issue to view/edit, or return to menu:',
header: 'Select',
multiSelect: false,
options: [
...issues.slice(0, 10).map(i => ({
label: i.id,
description: i.title.substring(0, 50)
})),
{ label: 'Back to Menu', description: 'Return to main menu' }
]
}]
});
const selected = parseAnswer(actionAnswer);
if (selected === 'Back to Menu') {
return showMainMenu();
}
// View selected issue
await viewIssueInteractive(selected);
}
```
### Phase 4: View Issue
```javascript
async function viewIssueInteractive(issueId) {
if (!issueId) {
// Ask for issue ID
const issues = JSON.parse(Bash('ccw issue list --json') || '[]');
const idAnswer = AskUserQuestion({
questions: [{
question: 'Select issue to view:',
header: 'Issue',
multiSelect: false,
options: issues.slice(0, 10).map(i => ({
label: i.id,
description: `${i.status} - ${i.title.substring(0, 40)}`
}))
}]
});
issueId = parseAnswer(idAnswer);
}
// Fetch detailed status
const result = Bash(`ccw issue status ${issueId} --json`);
const data = JSON.parse(result);
const issue = data.issue;
const solutions = data.solutions || [];
const bound = data.bound;
console.log(`
## Issue: ${issue.id}
**Title**: ${issue.title}
**Status**: ${issue.status}
**Priority**: P${issue.priority}
**Created**: ${issue.created_at}
**Updated**: ${issue.updated_at}
### Context
${issue.context || 'No context provided'}
### Solutions (${solutions.length})
${solutions.length === 0 ? 'No solutions registered' :
solutions.map(s => `- ${s.is_bound ? '◉' : '○'} ${s.id}: ${s.tasks?.length || 0} tasks`).join('\n')}
${bound ? `### Bound Solution: ${bound.id}\n**Tasks**: ${bound.tasks?.length || 0}` : ''}
`);
// Show tasks if bound solution exists
if (bound?.tasks?.length > 0) {
console.log(`
### Tasks
| ID | Action | Scope | Title |
|----|--------|-------|-------|
${bound.tasks.map(t => `| ${t.id} | ${t.action} | ${t.scope?.substring(0, 20) || '-'} | ${t.title.substring(0, 30)} |`).join('\n')}
`);
}
// Action menu
const actionAnswer = AskUserQuestion({
questions: [{
question: 'What would you like to do?',
header: 'Action',
multiSelect: false,
options: [
{ label: 'Edit Issue', description: 'Modify issue fields' },
{ label: 'Plan Issue', description: 'Generate solution (/issue:plan)' },
{ label: 'Add to Queue', description: 'Queue bound solution tasks' },
{ label: 'View Queue', description: 'See queue status' },
{ label: 'Delete Issue', description: 'Remove this issue' },
{ label: 'Back to Menu', description: 'Return to main menu' }
]
}]
});
const action = parseAnswer(actionAnswer);
switch (action) {
case 'Edit Issue':
await editIssueInteractive(issueId);
break;
case 'Plan Issue':
console.log(`Running: /issue:plan ${issueId}`);
// Invoke plan skill
break;
case 'Add to Queue':
Bash(`ccw issue queue add ${issueId}`);
console.log(`✓ Added ${issueId} tasks to queue`);
break;
case 'View Queue':
const queueOutput = Bash('ccw issue queue');
console.log(queueOutput);
break;
case 'Delete Issue':
await deleteIssueInteractive(issueId);
break;
default:
return showMainMenu();
}
}
```
### Phase 5: Edit Issue
```javascript
async function editIssueInteractive(issueId) {
if (!issueId) {
const issues = JSON.parse(Bash('ccw issue list --json') || '[]');
const idAnswer = AskUserQuestion({
questions: [{
question: 'Select issue to edit:',
header: 'Issue',
multiSelect: false,
options: issues.slice(0, 10).map(i => ({
label: i.id,
description: `${i.status} - ${i.title.substring(0, 40)}`
}))
}]
});
issueId = parseAnswer(idAnswer);
}
// Get current issue data
const result = Bash(`ccw issue list ${issueId} --json`);
const issueData = JSON.parse(result);
const issue = issueData.issue || issueData;
// Ask which field to edit
const fieldAnswer = AskUserQuestion({
questions: [{
question: 'Which field to edit?',
header: 'Field',
multiSelect: false,
options: [
{ label: 'Title', description: `Current: ${issue.title?.substring(0, 40)}` },
{ label: 'Priority', description: `Current: P${issue.priority}` },
{ label: 'Status', description: `Current: ${issue.status}` },
{ label: 'Context', description: 'Edit problem description' },
{ label: 'Labels', description: `Current: ${issue.labels?.join(', ') || 'none'}` },
{ label: 'Back', description: 'Return without changes' }
]
}]
});
const field = parseAnswer(fieldAnswer);
if (field === 'Back') {
return viewIssueInteractive(issueId);
}
let updatePayload = {};
switch (field) {
case 'Title':
const titleAnswer = AskUserQuestion({
questions: [{
question: 'Enter new title (or select current to keep):',
header: 'Title',
multiSelect: false,
options: [
{ label: issue.title.substring(0, 50), description: 'Keep current title' }
]
}]
});
const newTitle = parseAnswer(titleAnswer);
if (newTitle && newTitle !== issue.title.substring(0, 50)) {
updatePayload.title = newTitle;
}
break;
case 'Priority':
const priorityAnswer = AskUserQuestion({
questions: [{
question: 'Select priority:',
header: 'Priority',
multiSelect: false,
options: [
{ label: 'P1 - Critical', description: 'Production blocking' },
{ label: 'P2 - High', description: 'Major functionality' },
{ label: 'P3 - Medium', description: 'Normal priority (default)' },
{ label: 'P4 - Low', description: 'Minor issues' },
{ label: 'P5 - Trivial', description: 'Nice to have' }
]
}]
});
const priorityStr = parseAnswer(priorityAnswer);
updatePayload.priority = parseInt(priorityStr.charAt(1));
break;
case 'Status':
const statusAnswer = AskUserQuestion({
questions: [{
question: 'Select status:',
header: 'Status',
multiSelect: false,
options: [
{ label: 'registered', description: 'New issue, not yet planned' },
{ label: 'planning', description: 'Solution being generated' },
{ label: 'planned', description: 'Solution bound, ready for queue' },
{ label: 'queued', description: 'In execution queue' },
{ label: 'executing', description: 'Currently being worked on' },
{ label: 'completed', description: 'All tasks finished' },
{ label: 'failed', description: 'Execution failed' },
{ label: 'paused', description: 'Temporarily on hold' }
]
}]
});
updatePayload.status = parseAnswer(statusAnswer);
break;
case 'Context':
console.log(`Current context:\n${issue.context || '(empty)'}\n`);
const contextAnswer = AskUserQuestion({
questions: [{
question: 'Enter new context (problem description):',
header: 'Context',
multiSelect: false,
options: [
{ label: 'Keep current', description: 'No changes' }
]
}]
});
const newContext = parseAnswer(contextAnswer);
if (newContext && newContext !== 'Keep current') {
updatePayload.context = newContext;
}
break;
case 'Labels':
const labelsAnswer = AskUserQuestion({
questions: [{
question: 'Enter labels (comma-separated):',
header: 'Labels',
multiSelect: false,
options: [
{ label: issue.labels?.join(',') || '', description: 'Keep current labels' }
]
}]
});
const labelsStr = parseAnswer(labelsAnswer);
if (labelsStr) {
updatePayload.labels = labelsStr.split(',').map(l => l.trim());
}
break;
}
// Apply update if any
if (Object.keys(updatePayload).length > 0) {
// Read, update, write issues.jsonl
const issuesPath = '.workflow/issues/issues.jsonl';
const allIssues = Bash(`cat "${issuesPath}"`)
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
const idx = allIssues.findIndex(i => i.id === issueId);
if (idx !== -1) {
allIssues[idx] = {
...allIssues[idx],
...updatePayload,
updated_at: new Date().toISOString()
};
Write(issuesPath, allIssues.map(i => JSON.stringify(i)).join('\n'));
console.log(`✓ Updated ${issueId}: ${Object.keys(updatePayload).join(', ')}`);
}
}
// Continue editing or return
const continueAnswer = AskUserQuestion({
questions: [{
question: 'Continue editing?',
header: 'Continue',
multiSelect: false,
options: [
{ label: 'Edit Another Field', description: 'Continue editing this issue' },
{ label: 'View Issue', description: 'See updated issue' },
{ label: 'Back to Menu', description: 'Return to main menu' }
]
}]
});
const cont = parseAnswer(continueAnswer);
if (cont === 'Edit Another Field') {
await editIssueInteractive(issueId);
} else if (cont === 'View Issue') {
await viewIssueInteractive(issueId);
} else {
return showMainMenu();
}
}
```
### Phase 6: Delete Issue
```javascript
async function deleteIssueInteractive(issueId) {
if (!issueId) {
const issues = JSON.parse(Bash('ccw issue list --json') || '[]');
const idAnswer = AskUserQuestion({
questions: [{
question: 'Select issue to delete:',
header: 'Delete',
multiSelect: false,
options: issues.slice(0, 10).map(i => ({
label: i.id,
description: `${i.status} - ${i.title.substring(0, 40)}`
}))
}]
});
issueId = parseAnswer(idAnswer);
}
// Confirm deletion
const confirmAnswer = AskUserQuestion({
questions: [{
question: `Delete issue ${issueId}? This will also remove associated solutions.`,
header: 'Confirm',
multiSelect: false,
options: [
{ label: 'Delete', description: 'Permanently remove issue and solutions' },
{ label: 'Cancel', description: 'Keep issue' }
]
}]
});
if (parseAnswer(confirmAnswer) !== 'Delete') {
console.log('Deletion cancelled.');
return showMainMenu();
}
// Remove from issues.jsonl
const issuesPath = '.workflow/issues/issues.jsonl';
const allIssues = Bash(`cat "${issuesPath}"`)
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
const filtered = allIssues.filter(i => i.id !== issueId);
Write(issuesPath, filtered.map(i => JSON.stringify(i)).join('\n'));
// Remove solutions file if exists
const solPath = `.workflow/issues/solutions/${issueId}.jsonl`;
Bash(`rm -f "${solPath}" 2>/dev/null || true`);
// Remove from queue if present
const queuePath = '.workflow/issues/queue.json';
if (Bash(`test -f "${queuePath}" && echo exists`) === 'exists') {
const queue = JSON.parse(Bash(`cat "${queuePath}"`));
queue.tasks = queue.tasks.filter(q => q.issue_id !== issueId);
Write(queuePath, JSON.stringify(queue, null, 2));
}
console.log(`✓ Deleted issue ${issueId}`);
return showMainMenu();
}
```
### Phase 7: Bulk Operations
```javascript
async function bulkOperationsInteractive() {
const bulkAnswer = AskUserQuestion({
questions: [{
question: 'Select bulk operation:',
header: 'Bulk',
multiSelect: false,
options: [
{ label: 'Update Status', description: 'Change status of multiple issues' },
{ label: 'Update Priority', description: 'Change priority of multiple issues' },
{ label: 'Add Labels', description: 'Add labels to multiple issues' },
{ label: 'Delete Multiple', description: 'Remove multiple issues' },
{ label: 'Queue All Planned', description: 'Add all planned issues to queue' },
{ label: 'Retry All Failed', description: 'Reset all failed tasks to pending' },
{ label: 'Back', description: 'Return to main menu' }
]
}]
});
const operation = parseAnswer(bulkAnswer);
if (operation === 'Back') {
return showMainMenu();
}
// Get issues for selection
const allIssues = JSON.parse(Bash('ccw issue list --json') || '[]');
if (operation === 'Queue All Planned') {
const planned = allIssues.filter(i => i.status === 'planned' && i.bound_solution_id);
for (const issue of planned) {
Bash(`ccw issue queue add ${issue.id}`);
console.log(`✓ Queued ${issue.id}`);
}
console.log(`\n✓ Queued ${planned.length} issues`);
return showMainMenu();
}
if (operation === 'Retry All Failed') {
Bash('ccw issue retry');
console.log('✓ Reset all failed tasks to pending');
return showMainMenu();
}
// Multi-select issues
const selectAnswer = AskUserQuestion({
questions: [{
question: 'Select issues (multi-select):',
header: 'Select',
multiSelect: true,
options: allIssues.slice(0, 15).map(i => ({
label: i.id,
description: `${i.status} - ${i.title.substring(0, 30)}`
}))
}]
});
const selectedIds = parseMultiAnswer(selectAnswer);
if (selectedIds.length === 0) {
console.log('No issues selected.');
return showMainMenu();
}
// Execute bulk operation
const issuesPath = '.workflow/issues/issues.jsonl';
let issues = Bash(`cat "${issuesPath}"`)
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
switch (operation) {
case 'Update Status':
const statusAnswer = AskUserQuestion({
questions: [{
question: 'Select new status:',
header: 'Status',
multiSelect: false,
options: [
{ label: 'registered', description: 'Reset to registered' },
{ label: 'paused', description: 'Pause issues' },
{ label: 'completed', description: 'Mark completed' }
]
}]
});
const newStatus = parseAnswer(statusAnswer);
issues = issues.map(i =>
selectedIds.includes(i.id)
? { ...i, status: newStatus, updated_at: new Date().toISOString() }
: i
);
break;
case 'Update Priority':
const prioAnswer = AskUserQuestion({
questions: [{
question: 'Select new priority:',
header: 'Priority',
multiSelect: false,
options: [
{ label: 'P1', description: 'Critical' },
{ label: 'P2', description: 'High' },
{ label: 'P3', description: 'Medium' },
{ label: 'P4', description: 'Low' },
{ label: 'P5', description: 'Trivial' }
]
}]
});
const newPrio = parseInt(parseAnswer(prioAnswer).charAt(1));
issues = issues.map(i =>
selectedIds.includes(i.id)
? { ...i, priority: newPrio, updated_at: new Date().toISOString() }
: i
);
break;
case 'Add Labels':
const labelAnswer = AskUserQuestion({
questions: [{
question: 'Enter labels to add (comma-separated):',
header: 'Labels',
multiSelect: false,
options: [
{ label: 'bug', description: 'Bug fix' },
{ label: 'feature', description: 'New feature' },
{ label: 'urgent', description: 'Urgent priority' }
]
}]
});
const newLabels = parseAnswer(labelAnswer).split(',').map(l => l.trim());
issues = issues.map(i =>
selectedIds.includes(i.id)
? {
...i,
labels: [...new Set([...(i.labels || []), ...newLabels])],
updated_at: new Date().toISOString()
}
: i
);
break;
case 'Delete Multiple':
const confirmDelete = AskUserQuestion({
questions: [{
question: `Delete ${selectedIds.length} issues permanently?`,
header: 'Confirm',
multiSelect: false,
options: [
{ label: 'Delete All', description: 'Remove selected issues' },
{ label: 'Cancel', description: 'Keep issues' }
]
}]
});
if (parseAnswer(confirmDelete) === 'Delete All') {
issues = issues.filter(i => !selectedIds.includes(i.id));
// Clean up solutions
for (const id of selectedIds) {
Bash(`rm -f ".workflow/issues/solutions/${id}.jsonl" 2>/dev/null || true`);
}
} else {
console.log('Deletion cancelled.');
return showMainMenu();
}
break;
}
Write(issuesPath, issues.map(i => JSON.stringify(i)).join('\n'));
console.log(`✓ Updated ${selectedIds.length} issues`);
return showMainMenu();
}
```
### Phase 8: Create Issue (Redirect)
```javascript
async function createIssueInteractive() {
const typeAnswer = AskUserQuestion({
questions: [{
question: 'Create issue from:',
header: 'Source',
multiSelect: false,
options: [
{ label: 'GitHub URL', description: 'Import from GitHub issue' },
{ label: 'Text Description', description: 'Enter problem description' },
{ label: 'Quick Create', description: 'Just title and priority' }
]
}]
});
const type = parseAnswer(typeAnswer);
if (type === 'GitHub URL' || type === 'Text Description') {
console.log('Use /issue:new for structured issue creation');
console.log('Example: /issue:new https://github.com/org/repo/issues/123');
return showMainMenu();
}
// Quick create
const titleAnswer = AskUserQuestion({
questions: [{
question: 'Enter issue title:',
header: 'Title',
multiSelect: false,
options: [
{ label: 'Authentication Bug', description: 'Example title' }
]
}]
});
const title = parseAnswer(titleAnswer);
const prioAnswer = AskUserQuestion({
questions: [{
question: 'Select priority:',
header: 'Priority',
multiSelect: false,
options: [
{ label: 'P3 - Medium (Recommended)', description: 'Normal priority' },
{ label: 'P1 - Critical', description: 'Production blocking' },
{ label: 'P2 - High', description: 'Major functionality' }
]
}]
});
const priority = parseInt(parseAnswer(prioAnswer).charAt(1));
// Generate ID and create
const id = `ISS-${Date.now()}`;
Bash(`ccw issue init ${id} --title "${title}" --priority ${priority}`);
console.log(`✓ Created issue ${id}`);
await viewIssueInteractive(id);
}
```
## Helper Functions
```javascript
function parseAnswer(answer) {
// Extract selected option from AskUserQuestion response
if (typeof answer === 'string') return answer;
if (answer.answers) {
const values = Object.values(answer.answers);
return values[0] || '';
}
return '';
}
function parseMultiAnswer(answer) {
// Extract multiple selections
if (typeof answer === 'string') return answer.split(',').map(s => s.trim());
if (answer.answers) {
const values = Object.values(answer.answers);
return values.flatMap(v => v.split(',').map(s => s.trim()));
}
return [];
}
function parseFlags(input) {
const flags = {};
const matches = input.matchAll(/--(\w+)\s+([^\s-]+)/g);
for (const match of matches) {
flags[match[1]] = match[2];
}
return flags;
}
function parseIssueId(input) {
const match = input.match(/^([A-Z]+-\d+|ISS-\d+|GH-\d+)/i);
return match ? match[1] : null;
}
```
| File | Purpose |
|------|---------|
| `.workflow/issues/issues.jsonl` | Issue records |
| `.workflow/issues/solutions/<id>.jsonl` | Solutions per issue |
| `.workflow/issues/queue.json` | Execution queue |
## Error Handling
@@ -853,7 +104,6 @@ function parseIssueId(input) {
| Issue not found | Show available issues, ask for correction |
| Invalid selection | Show error, re-prompt |
| Write failure | Check permissions, show error |
| Queue operation fails | Show ccw issue error, suggest fix |
## Related Commands
@@ -861,5 +111,3 @@ function parseIssueId(input) {
- `/issue:plan` - Plan solution for issue
- `/issue:queue` - Form execution queue
- `/issue:execute` - Execute queued tasks
- `ccw issue list` - CLI list command
- `ccw issue status` - CLI status command

View File

@@ -51,51 +51,18 @@ interface Issue {
}
```
## Task Lifecycle (Each Task is Closed-Loop)
## Lifecycle Requirements
When `/issue:plan` generates tasks, each task MUST include:
The `lifecycle_requirements` field guides downstream commands (`/issue:plan`, `/issue:execute`):
```typescript
interface SolutionTask {
id: string;
title: string;
scope: string;
action: string;
| Field | Options | Purpose |
|-------|---------|---------|
| `test_strategy` | `unit`, `integration`, `e2e`, `manual`, `auto` | Which test types to generate |
| `regression_scope` | `affected`, `related`, `full` | Which tests to run for regression |
| `acceptance_type` | `automated`, `manual`, `both` | How to verify completion |
| `commit_strategy` | `per-task`, `squash`, `atomic` | Commit granularity |
// Phase 1: Implementation
implementation: string[]; // Step-by-step implementation
modification_points: { file: string; target: string; change: string }[];
// Phase 2: Testing
test: {
unit?: string[]; // Unit test requirements
integration?: string[]; // Integration test requirements
commands?: string[]; // Test commands to run
coverage_target?: number; // Minimum coverage %
};
// Phase 3: Regression
regression: string[]; // Regression check commands/points
// Phase 4: Acceptance
acceptance: {
criteria: string[]; // Testable acceptance criteria
verification: string[]; // How to verify each criterion
manual_checks?: string[]; // Manual verification if needed
};
// Phase 5: Commit
commit: {
type: 'feat' | 'fix' | 'refactor' | 'test' | 'docs' | 'chore';
scope: string; // e.g., "auth", "api"
message_template: string; // Commit message template
breaking?: boolean;
};
depends_on: string[];
executor: 'codex' | 'gemini' | 'agent' | 'auto';
}
```
> **Note**: Task structure (SolutionTask) is defined in `/issue:plan` - see `.claude/commands/issue/plan.md`
## Usage

View File

@@ -9,13 +9,35 @@ allowed-tools: TodoWrite(*), Task(*), SlashCommand(*), AskUserQuestion(*), Bash(
## Overview
Unified planning command using **issue-plan-agent** that combines exploration and planning into a single closed-loop workflow. The agent handles ACE semantic search, solution generation, and task breakdown.
Unified planning command using **issue-plan-agent** that combines exploration and planning into a single closed-loop workflow.
## Output Requirements
**Generate Files:**
1. `.workflow/issues/solutions/{issue-id}.jsonl` - Solution with tasks for each issue
**Return Summary:**
```json
{
"bound": [{ "issue_id": "...", "solution_id": "...", "task_count": N }],
"pending_selection": [{ "issue_id": "...", "solutions": [...] }],
"conflicts": [{ "file": "...", "issues": [...] }]
}
```
**Completion Criteria:**
- [ ] Solution file generated for each issue
- [ ] Single solution → auto-bound via `ccw issue bind`
- [ ] Multiple solutions → returned for user selection
- [ ] Tasks conform to schema: `cat .claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json`
- [ ] Each task has quantified `delivery_criteria`
## Core Capabilities
**Core capabilities:**
- **Closed-loop agent**: issue-plan-agent combines explore + plan
- Batch processing: 1 agent processes 1-3 issues
- ACE semantic search integrated into planning
- Solution with executable tasks and acceptance criteria
- Solution with executable tasks and delivery criteria
- Automatic solution registration and binding
## Storage Structure (Flat JSONL)
@@ -123,96 +145,39 @@ TodoWrite({
### Phase 2: Unified Explore + Plan (issue-plan-agent)
```javascript
// Ensure solutions directory exists
Bash(`mkdir -p .workflow/issues/solutions`);
const pendingSelections = []; // Collect multi-solution issues for user selection
for (const [batchIndex, batch] of batches.entries()) {
updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress');
// Build issue prompt for agent - pass IDs only, agent fetches details
// Build minimal prompt - agent handles exploration, planning, and binding
const issuePrompt = `
## Issues to Plan (Closed-Loop Tasks Required)
## Plan Issues
**Issue IDs**: ${batch.join(', ')}
**Project Root**: ${process.cwd()}
### Step 1: Fetch Issue Details
For each issue ID, use CLI to get full details:
\`\`\`bash
ccw issue status <issue-id> --json
\`\`\`
### Steps
1. Fetch: \`ccw issue status <id> --json\`
2. Explore (ACE) → Plan solution
3. Register & bind: \`ccw issue bind <id> --solution <file>\`
Returns:
### Generate Files
\`.workflow/issues/solutions/{issue-id}.jsonl\` - Solution with tasks (schema: cat .claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json)
### Binding Rules
- **Single solution**: Auto-bind via \`ccw issue bind <id> --solution <file>\`
- **Multiple solutions**: Register only, return for user selection
### Return Summary
\`\`\`json
{
"issue": { "id", "title", "context", "affected_components", "lifecycle_requirements", ... },
"solutions": [...],
"bound": null
"bound": [{ "issue_id": "...", "solution_id": "...", "task_count": N }],
"pending_selection": [{ "issue_id": "...", "solutions": [{ "id": "...", "description": "...", "task_count": N }] }],
"conflicts": [{ "file": "...", "issues": [...] }]
}
\`\`\`
## Project Root
${process.cwd()}
## Output Requirements
**IMPORTANT**: Register solutions via CLI, do NOT write files directly.
### 1. Register Solutions via CLI
For each issue, save solution to temp file and register via CLI:
\`\`\`bash
# Write solution JSON to temp file
echo '<solution-json>' > /tmp/sol-{issue-id}.json
# Register solution via CLI (generates SOL-xxx ID automatically)
ccw issue bind {issue-id} --solution /tmp/sol-{issue-id}.json
\`\`\`
- Solution must include all closed-loop task fields (see Solution Format below)
### 2. Return Summary Only
After registering solutions, return ONLY a brief JSON summary:
\`\`\`json
{
"planned": [
{ "issue_id": "XXX", "solution_id": "SOL-xxx", "task_count": 3, "description": "Brief description" }
],
"conflicts": [
{ "file": "path/to/file", "issues": ["ID1", "ID2"], "suggested_order": ["ID1", "ID2"] }
]
}
\`\`\`
## Closed-Loop Task Requirements
Each task MUST include ALL lifecycle phases:
### 1. Implementation
- implementation: string[] (2-7 concrete steps)
- modification_points: { file, target, change }[]
### 2. Test
- test.unit: string[] (unit test requirements)
- test.integration: string[] (integration test requirements if needed)
- test.commands: string[] (actual test commands to run)
- test.coverage_target: number (minimum coverage %)
### 3. Regression
- regression: string[] (commands to run for regression check)
### 4. Acceptance
- acceptance.criteria: string[] (testable acceptance criteria)
- acceptance.verification: string[] (how to verify each criterion)
### 5. Commit
- commit.type: feat|fix|refactor|test|docs|chore
- commit.scope: string (module name)
- commit.message_template: string (full commit message)
- commit.breaking: boolean
## Additional Requirements
1. Use ACE semantic search (mcp__ace-tool__search_context) for exploration
2. Detect file conflicts if multiple issues
3. Generate executable test commands based on project's test framework
4. Infer commit scope from affected files
`;
// Launch issue-plan-agent - agent writes solutions directly
@@ -223,79 +188,52 @@ Each task MUST include ALL lifecycle phases:
prompt=issuePrompt
);
// Parse brief summary from agent
// Parse summary from agent
const summary = JSON.parse(result);
// Display planning results
for (const item of summary.planned || []) {
console.log(`${item.issue_id}: ${item.solution_id} (${item.task_count} tasks) - ${item.description}`);
// Display auto-bound solutions
for (const item of summary.bound || []) {
console.log(`${item.issue_id}: ${item.solution_id} (${item.task_count} tasks)`);
}
// Handle conflicts if any
// Collect pending selections for Phase 3
pendingSelections.push(...(summary.pending_selection || []));
// Show conflicts
if (summary.conflicts?.length > 0) {
console.log(`\n⚠ File conflicts detected:`);
summary.conflicts.forEach(c => {
console.log(` ${c.file}: ${c.issues.join(', ')} → suggested: ${c.suggested_order.join(' → ')}`);
});
console.log(`⚠ Conflicts: ${summary.conflicts.map(c => c.file).join(', ')}`);
}
updateTodo(`Plan batch ${batchIndex + 1}`, 'completed');
}
```
### Phase 3: Solution Binding
### Phase 3: Multi-Solution Selection
```javascript
// Collect issues needing user selection (multiple solutions)
const needSelection = [];
for (const issueId of issueIds) {
// Get solutions via CLI
const statusJson = Bash(`ccw issue status ${issueId} --json 2>/dev/null || echo '{}'`).trim();
const status = JSON.parse(statusJson);
const solutions = status.solutions || [];
if (solutions.length === 0) continue; // No solutions - skip silently (agent already reported)
if (solutions.length === 1) {
// Auto-bind single solution
bindSolution(issueId, solutions[0].id);
} else {
// Multiple solutions - collect for batch selection
const options = solutions.map(s => ({
id: s.id,
description: s.description,
task_count: (s.tasks || []).length
}));
needSelection.push({ issueId, options });
}
}
// Batch ask user for multiple-solution issues
if (needSelection.length > 0) {
// Only handle issues where agent generated multiple solutions
if (pendingSelections.length > 0) {
const answer = AskUserQuestion({
questions: needSelection.map(({ issueId, options }) => ({
question: `Select solution for ${issueId}:`,
header: issueId,
questions: pendingSelections.map(({ issue_id, solutions }) => ({
question: `Select solution for ${issue_id}:`,
header: issue_id,
multiSelect: false,
options: options.map(s => ({
options: solutions.map(s => ({
label: `${s.id} (${s.task_count} tasks)`,
description: s.description || 'Solution'
description: s.description
}))
}))
});
// Bind selected solutions
for (const { issueId } of needSelection) {
const selectedSolId = extractSelectedSolutionId(answer, issueId);
if (selectedSolId) bindSolution(issueId, selectedSolId);
// Bind user-selected solutions
for (const { issue_id } of pendingSelections) {
const selectedId = extractSelectedSolutionId(answer, issue_id);
if (selectedId) {
Bash(`ccw issue bind ${issue_id} ${selectedId}`);
console.log(`${issue_id}: ${selectedId} bound`);
}
}
}
// Helper: bind solution to issue (using CLI for safety)
function bindSolution(issueId, solutionId) {
Bash(`ccw issue bind ${issueId} ${solutionId}`);
}
```
### Phase 4: Summary
@@ -312,86 +250,6 @@ Next: \`/issue:queue\` → \`/issue:execute\`
`);
```
## Solution Format (Closed-Loop Tasks)
Each solution line in `solutions/{issue-id}.jsonl`:
```json
{
"id": "SOL-20251226-001",
"description": "Direct Implementation",
"tasks": [
{
"id": "T1",
"title": "Create auth middleware",
"scope": "src/middleware/",
"action": "Create",
"description": "Create JWT validation middleware",
"modification_points": [
{ "file": "src/middleware/auth.ts", "target": "new file", "change": "Create middleware" }
],
"implementation": [
"Create auth.ts file in src/middleware/",
"Implement JWT token validation using jsonwebtoken",
"Add error handling for invalid/expired tokens",
"Export middleware function"
],
"test": {
"unit": [
"Test valid token passes through",
"Test invalid token returns 401",
"Test expired token returns 401",
"Test missing token returns 401"
],
"commands": [
"npm test -- --grep 'auth middleware'",
"npm run test:coverage -- src/middleware/auth.ts"
],
"coverage_target": 80
},
"regression": [
"npm test -- --grep 'protected routes'",
"npm run test:integration -- auth"
],
"acceptance": {
"criteria": [
"Middleware validates JWT tokens successfully",
"Returns 401 for invalid or missing tokens",
"Passes decoded token to request context"
],
"verification": [
"curl -H 'Authorization: Bearer valid_token' /api/protected → 200",
"curl /api/protected → 401",
"curl -H 'Authorization: Bearer invalid' /api/protected → 401"
]
},
"commit": {
"type": "feat",
"scope": "auth",
"message_template": "feat(auth): add JWT validation middleware\n\n- Implement token validation\n- Add error handling for invalid tokens\n- Export for route protection",
"breaking": false
},
"depends_on": [],
"estimated_minutes": 30,
"executor": "codex"
}
],
"exploration_context": {
"relevant_files": ["src/config/auth.ts"],
"patterns": "Follow existing middleware pattern"
},
"is_bound": true,
"created_at": "2025-12-26T10:00:00Z",
"bound_at": "2025-12-26T10:05:00Z"
}
```
## Error Handling
| Error | Resolution |
@@ -402,17 +260,6 @@ Each solution line in `solutions/{issue-id}.jsonl`:
| User cancels selection | Skip issue, continue with others |
| File conflicts | Agent detects and suggests resolution order |
## Agent Integration
The command uses `issue-plan-agent` which:
1. Performs ACE semantic search per issue
2. Identifies modification points and patterns
3. Generates task breakdown with dependencies
4. Detects cross-issue file conflicts
5. Outputs solution JSON for registration
See `.claude/agents/issue-plan-agent.md` for agent specification.
## Related Commands
- `/issue:queue` - Form execution queue from bound solutions

View File

@@ -9,16 +9,39 @@ allowed-tools: TodoWrite(*), Task(*), Bash(*), Read(*), Write(*)
## Overview
Queue formation command using **issue-queue-agent** that analyzes all bound solutions, resolves conflicts, determines dependencies, and creates an ordered execution queue. The queue is global across all issues.
Queue formation command using **issue-queue-agent** that analyzes all bound solutions, resolves conflicts, and creates an ordered execution queue.
## Output Requirements
**Generate Files:**
1. `.workflow/issues/queues/{queue-id}.json` - Full queue with tasks, conflicts, groups
2. `.workflow/issues/queues/index.json` - Update with new queue entry
**Return Summary:**
```json
{
"queue_id": "QUE-20251227-143000",
"total_tasks": N,
"execution_groups": [{ "id": "P1", "type": "parallel", "count": N }],
"conflicts_resolved": N,
"issues_queued": ["GH-123", "GH-124"]
}
```
**Completion Criteria:**
- [ ] Queue JSON generated with valid DAG (no cycles)
- [ ] All file conflicts resolved with rationale
- [ ] Semantic priority calculated for all tasks
- [ ] Execution groups assigned (parallel P* / sequential S*)
- [ ] Issue statuses updated to `queued` via `ccw issue update`
## Core Capabilities
**Core capabilities:**
- **Agent-driven**: issue-queue-agent handles all ordering logic
- ACE semantic search for relationship discovery
- Dependency DAG construction and cycle detection
- File conflict detection and resolution
- Semantic priority calculation (0.0-1.0)
- Parallel/Sequential group assignment
- Output global queue.json
## Storage Structure (Queue History)
@@ -168,162 +191,93 @@ console.log(`Loaded ${allTasks.length} tasks from ${plannedIssues.length} issues
### Phase 2-4: Agent-Driven Queue Formation
```javascript
// Launch issue-queue-agent to handle all ordering logic
// Build minimal prompt - agent reads schema and handles ordering
const agentPrompt = `
## Tasks to Order
## Order Tasks
${JSON.stringify(allTasks, null, 2)}
**Tasks**: ${allTasks.length} from ${plannedIssues.length} issues
**Project Root**: ${process.cwd()}
## Project Root
${process.cwd()}
### Input
\`\`\`json
${JSON.stringify(allTasks.map(t => ({
key: \`\${t.issue_id}:\${t.task.id}\`,
type: t.task.type,
file_context: t.task.file_context,
depends_on: t.task.depends_on
})), null, 2)}
\`\`\`
## Requirements
1. Build dependency DAG from depends_on fields
2. Detect circular dependencies (abort if found)
3. Identify file modification conflicts
4. Resolve conflicts using ordering rules:
- Create before Update/Implement
- Foundation scopes (config/types) before implementation
- Core logic before tests
5. Calculate semantic priority (0.0-1.0) for each task
6. Assign execution groups (parallel P* / sequential S*)
7. Output queue JSON
### Steps
1. Parse tasks: Extract task keys, types, file contexts, dependencies
2. Build DAG: Construct dependency graph from depends_on references
3. Detect cycles: Verify no circular dependencies exist (abort if found)
4. Detect conflicts: Identify file modification conflicts across issues
5. Resolve conflicts: Apply ordering rules (Create→Update→Delete, config→src→tests)
6. Calculate priority: Compute semantic priority (0.0-1.0) for each task
7. Assign groups: Assign parallel (P*) or sequential (S*) execution groups
8. Generate queue: Write queue JSON with ordered tasks
9. Update index: Update queues/index.json with new queue entry
### Rules
- **DAG Validity**: Output must be valid DAG with no circular dependencies
- **Conflict Resolution**: All file conflicts must be resolved with rationale
- **Ordering Priority**:
1. Create before Update (files must exist before modification)
2. Foundation before integration (config/ → src/)
3. Types before implementation (types/ → components/)
4. Core before tests (src/ → __tests__/)
5. Delete last (preserve dependencies until no longer needed)
- **Parallel Safety**: Tasks in same parallel group must have no file conflicts
- **Queue ID Format**: \`QUE-YYYYMMDD-HHMMSS\` (UTC timestamp)
### Generate Files
1. \`.workflow/issues/queues/\${queueId}.json\` - Full queue (schema: cat .claude/workflows/cli-templates/schemas/queue-schema.json)
2. \`.workflow/issues/queues/index.json\` - Update with new entry
### Return Summary
\`\`\`json
{
"queue_id": "QUE-YYYYMMDD-HHMMSS",
"total_tasks": N,
"execution_groups": [{ "id": "P1", "type": "parallel", "count": N }],
"conflicts_resolved": N,
"issues_queued": ["GH-123"]
}
\`\`\`
`;
const result = Task(
subagent_type="issue-queue-agent",
run_in_background=false,
description=`Order ${allTasks.length} tasks from ${plannedIssues.length} issues`,
description=`Order ${allTasks.length} tasks`,
prompt=agentPrompt
);
// Parse agent output
const agentOutput = JSON.parse(result);
if (!agentOutput.success) {
console.error(`Queue formation failed: ${agentOutput.error}`);
if (agentOutput.cycles) {
console.error('Circular dependencies:', agentOutput.cycles.join(', '));
}
return;
}
const summary = JSON.parse(result);
```
### Phase 5: Queue Output & Summary
### Phase 5: Summary & Status Update
```javascript
const queueOutput = agentOutput.output;
// Write queue.json
Write('.workflow/issues/queue.json', JSON.stringify(queueOutput, null, 2));
// Update issue statuses in issues.jsonl
const updatedIssues = allIssues.map(issue => {
if (plannedIssues.find(p => p.id === issue.id)) {
return {
...issue,
status: 'queued',
queued_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
}
return issue;
});
Write(issuesPath, updatedIssues.map(i => JSON.stringify(i)).join('\n'));
// Display summary
// Agent already generated queue files, use summary
console.log(`
## Queue Formed
## Queue Formed: ${summary.queue_id}
**Total Tasks**: ${queueOutput.tasks.length}
**Issues**: ${plannedIssues.length}
**Conflicts**: ${queueOutput.conflicts?.length || 0} (${queueOutput._metadata?.resolved_conflicts || 0} resolved)
**Tasks**: ${summary.total_tasks}
**Issues**: ${summary.issues_queued.join(', ')}
**Groups**: ${summary.execution_groups.map(g => `${g.id}(${g.count})`).join(', ')}
**Conflicts Resolved**: ${summary.conflicts_resolved}
### Execution Groups
${(queueOutput.execution_groups || []).map(g => {
const type = g.type === 'parallel' ? 'Parallel' : 'Sequential';
return `- ${g.id} (${type}): ${g.task_count} tasks`;
}).join('\n')}
### Next Steps
1. Review queue: \`ccw issue queue list\`
2. Execute: \`/issue:execute\`
Next: \`/issue:execute\`
`);
```
## Queue Schema
Output `queues/{queue-id}.json`:
```json
{
"name": "Auth Feature Queue",
"status": "active",
"issue_ids": ["GH-123", "GH-124"],
"tasks": [
{
"item_id": "T-1",
"issue_id": "GH-123",
"solution_id": "SOL-001",
"task_id": "T1",
"status": "pending",
"execution_order": 1,
"execution_group": "P1",
"depends_on": [],
"semantic_priority": 0.7
}
],
"conflicts": [
{
"type": "file_conflict",
"file": "src/auth.ts",
"tasks": ["GH-123:T1", "GH-124:T2"],
"resolution": "sequential",
"resolution_order": ["GH-123:T1", "GH-124:T2"],
"rationale": "T1 creates file before T2 updates",
"resolved": true
}
],
"execution_groups": [
{ "id": "P1", "type": "parallel", "task_count": 3, "tasks": ["T-1", "T-2", "T-3"] },
{ "id": "S2", "type": "sequential", "task_count": 2, "tasks": ["T-4", "T-5"] }
],
"_metadata": {
"version": "2.1-optimized",
"total_tasks": 5,
"pending_count": 3,
"completed_count": 2,
"failed_count": 0,
"updated_at": "2025-12-26T11:00:00Z",
"source": "issue-queue-agent"
}
// Update issue statuses via CLI
for (const issueId of summary.issues_queued) {
Bash(`ccw issue update ${issueId} --status queued`);
}
```
### Queue ID Format
```
QUE-YYYYMMDD-HHMMSS
例如: QUE-20251227-143052
```
## Semantic Priority Rules
| Factor | Priority Boost |
|--------|---------------|
| Create action | +0.2 |
| Configure action | +0.15 |
| Implement action | +0.1 |
| Config/Types scope | +0.1 |
| Refactor action | -0.05 |
| Test action | -0.1 |
| Delete action | -0.15 |
## Error Handling
| Error | Resolution |
@@ -333,19 +287,6 @@ QUE-YYYYMMDD-HHMMSS
| Unresolved conflicts | Agent resolves using ordering rules |
| Invalid task reference | Skip and warn |
## Agent Integration
The command uses `issue-queue-agent` which:
1. Builds dependency DAG from task depends_on fields
2. Detects circular dependencies (aborts if found)
3. Identifies file modification conflicts across issues
4. Resolves conflicts using semantic ordering rules
5. Calculates priority (0.0-1.0) for each task
6. Assigns parallel/sequential execution groups
7. Outputs structured queue JSON
See `.claude/agents/issue-queue-agent.md` for agent specification.
## Related Commands
- `/issue:plan` - Plan issues and bind solutions