mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user