Refactor issue management and history tracking

- Removed the interactive issue management command and its associated documentation.
- Enhanced the issue planning command to streamline project context reading and solution creation.
- Improved queue management with conflict clarification and status syncing from queues.
- Added functionality to track completed issues, moving them to a history file upon completion.
- Updated CLI options to support syncing issue statuses from queues.
- Introduced new API endpoint for retrieving completed issues from history.
- Enhanced error handling and validation for issue updates and queue management.
This commit is contained in:
catlog22
2025-12-29 21:42:06 +08:00
parent 6ec6643448
commit fd48045fe3
8 changed files with 499 additions and 455 deletions

View File

@@ -1,26 +1,18 @@
---
name: issue-queue-agent
description: |
Solution ordering agent for queue formation with dependency analysis and conflict resolution.
Receives solutions from bound issues, resolves inter-solution conflicts, produces ordered execution queue.
Examples:
- Context: Single issue queue
user: "Order solutions for GH-123"
assistant: "I'll analyze dependencies and generate execution queue"
- Context: Multi-issue queue with conflicts
user: "Order solutions for GH-123, GH-124"
assistant: "I'll detect file conflicts between solutions, resolve ordering, and assign groups"
Solution ordering agent for queue formation with Gemini CLI conflict analysis.
Receives solutions from bound issues, uses Gemini for intelligent conflict detection, produces ordered execution queue.
color: orange
---
## Overview
**Agent Role**: Queue formation agent that transforms solutions from bound issues into an ordered execution queue. Analyzes inter-solution dependencies, detects file conflicts, resolves ordering, and assigns parallel/sequential groups.
**Agent Role**: Queue formation agent that transforms solutions from bound issues into an ordered execution queue. Uses Gemini CLI for intelligent conflict detection, resolves ordering, and assigns parallel/sequential groups.
**Core Capabilities**:
- Inter-solution dependency DAG construction
- File conflict detection between solutions (based on files_touched intersection)
- Gemini CLI conflict analysis (5 types: file, API, data, dependency, architecture)
- Conflict resolution with semantic ordering rules
- Priority calculation (0.0-1.0) per solution
- Parallel/Sequential group assignment for solutions
@@ -70,84 +62,50 @@ Phase 4: Ordering & Grouping (25%)
### 2.1 Dependency Graph
```javascript
function buildDependencyGraph(solutions) {
const graph = new Map()
const fileModifications = new Map()
**Build DAG from solutions**:
1. Create node for each solution with `inDegree: 0` and `outEdges: []`
2. Build file→solutions mapping from `files_touched`
3. For files touched by multiple solutions → potential conflict edges
for (const sol of solutions) {
graph.set(sol.solution_id, { ...sol, inDegree: 0, outEdges: [] })
**Graph Structure**:
- Nodes: Solutions (keyed by `solution_id`)
- Edges: Dependency relationships (added during conflict resolution)
- Properties: `inDegree` (incoming edges), `outEdges` (outgoing dependencies)
for (const file of sol.files_touched || []) {
if (!fileModifications.has(file)) fileModifications.set(file, [])
fileModifications.get(file).push(sol.solution_id)
}
}
### 2.2 Conflict Detection (Gemini CLI)
return { graph, fileModifications }
}
Use Gemini CLI for intelligent conflict analysis across all solutions:
```bash
ccw cli -p "
PURPOSE: Analyze solutions for conflicts across 5 dimensions
TASK: • Detect file conflicts (same file modified by multiple solutions)
• Detect API conflicts (breaking interface changes)
• Detect data conflicts (schema changes to same model)
• Detect dependency conflicts (package version mismatches)
• Detect architecture conflicts (pattern violations)
MODE: analysis
CONTEXT: @.workflow/issues/solutions/**/*.jsonl | Solution data: \${SOLUTIONS_JSON}
EXPECTED: JSON array of conflicts with type, severity, solutions, recommended_order
RULES: $(cat ~/.claude/workflows/cli-templates/protocols/analysis-protocol.md) | Severity: high (API/data) > medium (file/dependency) > low (architecture)
" --tool gemini --mode analysis --cd .workflow/issues
```
### 2.2 Conflict Detection (5 Types)
**Placeholder**: `${SOLUTIONS_JSON}` = serialized solutions array from bound issues
Detect all conflict types between solutions:
```javascript
function detectConflicts(solutions, graph) {
const conflicts = [];
const fileModifications = buildFileModificationMap(solutions);
**Conflict Types & Severity**:
// 1. File conflicts (multiple solutions modify same file)
for (const [file, solIds] of fileModifications.entries()) {
if (solIds.length > 1) {
conflicts.push({
type: 'file_conflict', severity: 'medium',
file, solutions: solIds, resolved: false
});
}
}
| Type | Severity | Trigger |
|------|----------|---------|
| `file_conflict` | medium | Multiple solutions modify same file |
| `api_conflict` | high | Breaking interface changes |
| `data_conflict` | high | Schema changes to same model |
| `dependency_conflict` | medium | Package version mismatches |
| `architecture_conflict` | low | Pattern violations |
// 2. API conflicts (breaking interface changes)
const apiChanges = extractApiChangesFromAllSolutions(solutions);
for (const [api, changes] of apiChanges.entries()) {
if (changes.some(c => c.breaking)) {
conflicts.push({
type: 'api_conflict', severity: 'high',
api, solutions: changes.map(c => c.solution_id), resolved: false
});
}
}
// 3. Data model conflicts (schema changes to same model)
const dataChanges = extractDataChangesFromAllSolutions(solutions);
for (const [model, changes] of dataChanges.entries()) {
if (changes.length > 1) {
conflicts.push({
type: 'data_conflict', severity: 'high',
model, solutions: changes.map(c => c.solution_id), resolved: false
});
}
}
// 4. Dependency conflicts (package version conflicts)
const depChanges = extractDependencyChanges(solutions);
for (const [pkg, versions] of depChanges.entries()) {
if (versions.length > 1 && !versionsCompatible(versions)) {
conflicts.push({
type: 'dependency_conflict', severity: 'medium',
package: pkg, solutions: versions.map(v => v.solution_id), resolved: false
});
}
}
// 5. Architecture conflicts (pattern violations)
const archIssues = detectArchitectureViolations(solutions);
conflicts.push(...archIssues.map(issue => ({
type: 'architecture_conflict', severity: 'low',
pattern: issue.pattern, solutions: issue.solutions, resolved: false
})));
return conflicts;
}
**Output per conflict**:
```json
{ "type": "...", "severity": "...", "solutions": [...], "recommended_order": [...], "rationale": "..." }
```
### 2.2.5 Clarification (BLOCKING)
@@ -155,37 +113,22 @@ function detectConflicts(solutions, graph) {
**Purpose**: Surface ambiguous dependencies for user/system clarification
**Trigger Conditions**:
- High severity conflicts with no clear resolution order
- High severity conflicts without `recommended_order` from Gemini analysis
- Circular dependencies detected
- Multiple valid resolution strategies
**Clarification Logic**:
```javascript
function generateClarifications(conflicts, solutions) {
const clarifications = [];
**Clarification Generation**:
for (const conflict of conflicts) {
if (conflict.severity === 'high' && !conflict.recommended_order) {
clarifications.push({
conflict_id: `CFT-${clarifications.length + 1}`,
question: `${conflict.type}: Which solution should execute first?`,
options: conflict.solutions.map(solId => ({
value: solId,
label: getSolutionSummary(solId, solutions)
})),
requires_user_input: true
});
}
}
For each unresolved high-severity conflict:
1. Generate conflict ID: `CFT-{N}`
2. Build question: `"{type}: Which solution should execute first?"`
3. List options with solution summaries (issue title + task count)
4. Mark `requires_user_input: true`
return clarifications;
}
```
**Blocking Behavior**: Agent BLOCKS execution until clarifications are resolved
**Blocking Behavior**:
- Return `clarifications` array in output
- Main agent presents to user via AskUserQuestion
- Agent waits for response before proceeding to Phase 3
- Agent BLOCKS until all clarifications resolved
- No best-guess fallback - explicit user decision required
### 2.3 Resolution Rules
@@ -253,7 +196,6 @@ Queue Item ID format: `S-N` (S-1, S-2, S-3, ...)
"execution_group": "P1",
"depends_on": [],
"semantic_priority": 0.8,
"assigned_executor": "codex",
"files_touched": ["src/auth.ts", "src/utils.ts"],
"task_count": 3
}
@@ -345,14 +287,21 @@ Return brief summaries; full conflict details in separate files:
5. Merge conflicting solutions in parallel group
6. Split tasks from their solution
**OUTPUT** (STRICT - only these 2 files):
**WRITE** (exactly 2 files):
- `.workflow/issues/queues/{Queue ID}.json` - Full queue with solutions, groups
- `.workflow/issues/queues/index.json` - Update with new queue entry
- Use Queue ID from prompt, do NOT generate new one
**RETURN** (summary + unresolved conflicts):
```json
{
"queue_id": "QUE-xxx",
"total_solutions": N,
"total_tasks": N,
"execution_groups": [{"id": "P1", "type": "parallel", "count": N}],
"issues_queued": ["ISS-xxx"],
"clarifications": [{"conflict_id": "CFT-1", "question": "...", "options": [...]}]
}
```
.workflow/issues/queues/{Queue ID}.json # Use Queue ID from prompt
.workflow/issues/queues/index.json # Update existing index
```
- Use the Queue ID provided in prompt, do NOT generate new one
- Write ONLY the 2 files listed above, NO other files
- Final return: PURE JSON summary (no markdown, no prose):
```json
{"queue_id":"QUE-xxx","total_solutions":N,"total_tasks":N,"execution_groups":[...],"conflicts_resolved":N,"issues_queued":["ISS-xxx"]}
```
- `clarifications`: Only present if unresolved high-severity conflicts exist
- No markdown, no prose - PURE JSON only

View File

@@ -1,113 +0,0 @@
---
name: manage
description: Interactive issue management (CRUD) via ccw cli endpoints with menu-driven interface
argument-hint: "[issue-id] [--action list|view|edit|delete|bulk]"
allowed-tools: TodoWrite(*), Bash(*), Read(*), Write(*), AskUserQuestion(*), Task(*)
---
# Issue Manage Command (/issue:manage)
## Overview
Interactive menu-driven interface for issue management using `ccw issue` CLI endpoints:
- **List**: Browse and filter issues
- **View**: Detailed issue inspection
- **Edit**: Modify issue fields
- **Delete**: Remove issues
- **Bulk**: Batch operations on multiple issues
## CLI Endpoints Reference
```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 bind <id> <solution-id> # Bind solution
# Queue management
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
```bash
# Interactive mode (menu-driven)
/issue:manage
# Direct to specific issue
/issue:manage GH-123
# Direct action
/issue:manage --action list
/issue:manage GH-123 --action edit
```
## Implementation
This command delegates to the `issue-manage` skill for detailed implementation.
### Entry Point
```javascript
const issueId = parseIssueId(userInput);
const action = flags.action;
// Show main menu if no action specified
if (!action) {
await showMainMenu(issueId);
} else {
await executeAction(action, issueId);
}
```
### Main Menu Flow
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
### Available Actions
| 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 |
## Data Files
| File | Purpose |
|------|---------|
| `.workflow/issues/issues.jsonl` | Issue records |
| `.workflow/issues/solutions/<id>.jsonl` | Solutions per issue |
| `.workflow/issues/queue.json` | Execution queue |
## Error Handling
| Error | Resolution |
|-------|------------|
| No issues found | Suggest creating with /issue:new |
| Issue not found | Show available issues, ask for correction |
| Invalid selection | Show error, re-prompt |
| Write failure | Check permissions, show error |
## Related Commands
- `/issue:new` - Create structured issue
- `/issue:plan` - Plan solution for issue
- `/issue:queue` - Form execution queue
- `/issue:execute` - Execute queued tasks

View File

@@ -163,51 +163,26 @@ ${issueList}
**Project Root**: ${process.cwd()}
### Project Context (MANDATORY - Read Both Files First)
1. Read: .workflow/project-tech.json (technology stack, architecture, key components)
2. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
### Project Context (MANDATORY)
1. Read: .workflow/project-tech.json (technology stack, architecture)
2. Read: .workflow/project-guidelines.json (constraints and conventions)
**CRITICAL**: All solution tasks MUST comply with constraints in project-guidelines.json
### Steps
1. Fetch issue: \`ccw issue status <id> --json\`
2. Load project context (project-tech.json + project-guidelines.json)
### Workflow
1. Fetch issue details: ccw issue status <id> --json
2. Load project context files
3. Explore codebase (ACE semantic search)
4. Plan solution with tasks (see issue-plan-agent.md for details)
5. Write solutions to JSONL, bind if single solution
4. Plan solution with tasks (schema: solution-schema.json)
5. Create solution: ccw issue solution <issue-id> --data '{...}'
6. Single solution → auto-bind; Multiple → return for selection
### Solution Creation (via CLI endpoint)
```bash
ccw issue solution <issue-id> --data '{"description":"...", "approach":"...", "tasks":[...]}'
```
**CLI Endpoint Features:**
| Feature | Description |
|---------|-------------|
| Auto-increment ID | `SOL-{issue-id}-{seq}` (e.g., `SOL-GH-123-1`) |
| Multi-solution | Appends to existing JSONL, supports multiple per issue |
| JSON output | Returns created solution with ID |
**Schema Reference:** `cat .claude/workflows/cli-templates/schemas/solution-schema.json`
### Binding Rules
- **Single solution**: Auto-bind via `ccw issue bind <issue-id> <solution-id>`
- **Multiple solutions**: Register only, return for user selection
### Rules
- Solution ID format: SOL-{issue-id}-{seq}
- Single solution per issue → auto-bind via ccw issue bind
- Multiple solutions → register only, return pending_selection
- Tasks must have quantified acceptance.criteria
### Return Summary
\`\`\`json
{
"bound": [{ "issue_id": "...", "solution_id": "...", "task_count": N }],
"pending_selection": [{ "issue_id": "...", "solutions": [{ "id": "...", "description": "...", "task_count": N }] }],
"conflicts": [{
"type": "file_conflict|api_conflict|data_conflict|dependency_conflict|architecture_conflict",
"severity": "high|medium|low",
"summary": "brief description",
"recommended_resolution": "auto-resolution for low/medium",
"resolution_options": [{ "strategy": "...", "rationale": "..." }]
}]
}
\`\`\`
{"bound":[{"issue_id":"...","solution_id":"...","task_count":N}],"pending_selection":[{"issue_id":"...","solutions":[{"id":"...","description":"...","task_count":N}]}]}
`;
return { batchIndex, batchIds, issuePrompt, batch };

View File

@@ -17,8 +17,7 @@ Queue formation command using **issue-queue-agent** that analyzes all bound solu
- **Agent-driven**: issue-queue-agent handles all ordering logic
- **Solution-level granularity**: Queue items are solutions, not tasks
- Inter-solution dependency DAG (based on file conflicts)
- File conflict detection between solutions
- **Conflict clarification**: High-severity conflicts prompt user decision
- Semantic priority calculation per solution (0.0-1.0)
- Parallel/Sequential group assignment for solutions
@@ -52,28 +51,29 @@ ccw issue queue delete <queue-id> Delete queue from history
```
Phase 1: Solution Loading
├─ Load issues.jsonl
├─ Filter issues with bound_solution_id
├─ Read solutions/{issue-id}.jsonl for each issue
Find bound solution by ID
├─ Collect files_touched from all tasks in solution
└─ Build solution objects (NOT individual tasks)
├─ Load issues.jsonl, filter by status='planned' + bound_solution_id
├─ Read solutions/{issue-id}.jsonl, find bound solution
├─ Extract files_touched from task modification_points
Build solution objects array
Phase 2-4: Agent-Driven Queue Formation (issue-queue-agent)
├─ Launch issue-queue-agent with all solutions
├─ Launch issue-queue-agent with solutions array
├─ Agent performs:
│ ├─ Detect file overlaps between solutions
│ ├─ Build dependency DAG from file conflicts
│ ├─ Detect circular dependencies
│ ├─ Resolve conflicts using priority rules
│ ├─ Conflict analysis (5 types via Gemini CLI)
│ ├─ Build dependency DAG from conflicts
│ ├─ Calculate semantic priority per solution
│ └─ Assign execution groups (parallel/sequential)
└─ Output: queue JSON with ordered solutions (S-1, S-2, ...)
└─ Agent writes: queue JSON + index update
Phase 5: Queue Output
├─ Write queue.json with solutions array
├─ Update issue statuses in issues.jsonl
Display queue summary
Phase 5: Conflict Clarification (if needed)
├─ Check agent return for `clarifications` array
├─ If clarifications exist → AskUserQuestion
Pass user decisions back to agent (resume)
└─ Agent updates queue with resolved conflicts
Phase 6: Status Update & Summary
├─ Update issue statuses to 'queued'
└─ Display queue summary, next step: /issue:execute
```
## Implementation
@@ -105,87 +105,133 @@ Phase 5: Queue Output
### Phase 2-4: Agent-Driven Queue Formation
**Generate Queue ID** (command layer, pass to agent):
```javascript
// Generate queue-id ONCE here, pass to agent
const now = new Date();
const queueId = `QUE-${now.toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
// Build minimal prompt - agent orders SOLUTIONS, not tasks
const agentPrompt = `
## Order Solutions
**Queue ID**: ${queueId}
**Solutions**: ${allSolutions.length} from ${plannedIssues.length} issues
**Project Root**: ${process.cwd()}
### Input (Solution-Level)
\`\`\`json
${JSON.stringify(allSolutions, null, 2)}
\`\`\`
### Steps
1. Parse solutions: Extract solution IDs, files_touched, task_count, priority
2. Detect conflicts: Find file overlaps between solutions (files_touched intersection)
3. Build DAG: Create dependency edges where solutions share files
4. Detect cycles: Verify no circular dependencies (abort if found)
5. Resolve conflicts: Apply ordering rules based on action types
6. Calculate priority: Compute semantic priority (0.0-1.0) per solution
7. Assign groups: Parallel (P*) for no-conflict, Sequential (S*) for conflicts
8. Generate queue: Write queue JSON with ordered solutions
9. Update index: Update queues/index.json with new queue entry
### Rules
- **Solution Granularity**: Queue items are solutions, NOT individual tasks
- **DAG Validity**: Output must be valid DAG with no circular dependencies
- **Conflict Detection**: Two solutions conflict if files_touched intersect
- **Ordering Priority**:
1. Higher issue priority first (critical > high > medium > low)
2. Fewer dependencies first (foundation solutions)
3. More tasks = higher priority (larger impact)
- **Parallel Safety**: Solutions in same parallel group must have NO file overlaps
- **Queue Item ID Format**: \`S-N\` (S-1, S-2, S-3, ...)
- **Queue ID**: Use the provided Queue ID (passed above), do NOT generate new one
### Generate Files (STRICT - only these 2)
1. \`.workflow/issues/queues/{Queue ID}.json\` - Use Queue ID from above
2. \`.workflow/issues/queues/index.json\` - Update existing index
Write ONLY these 2 files, using the provided Queue ID.
### Return Summary
\`\`\`json
{
"queue_id": "QUE-YYYYMMDD-HHMMSS",
"total_solutions": N,
"total_tasks": N,
"execution_groups": [{ "id": "P1", "type": "parallel", "count": N }],
"conflicts_resolved": N,
"issues_queued": ["ISS-xxx"]
}
\`\`\`
`;
const result = Task(
subagent_type="issue-queue-agent",
run_in_background=false,
description=`Order ${allSolutions.length} solutions`,
prompt=agentPrompt
);
const summary = JSON.parse(result);
const queueId = `QUE-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
```
### Phase 5: Validation & Status Update
**Agent Prompt**:
```
## Order Solutions into Execution Queue
**Validation:**
- Verify queue file exists at `queues/{queue-id}.json`
- Check `solutions` array is non-empty → abort if empty
**Queue ID**: ${queueId}
**Solutions**: ${solutions.length} from ${issues.length} issues
**Project Root**: ${cwd}
**Status Update:**
- Update each queued issue status to `queued` via `ccw issue update <id> --status queued`
### Input
${JSON.stringify(solutions)}
### Workflow
Step 1: Build dependency graph from solutions (nodes=solutions, edges=file conflicts)
Step 2: Use Gemini CLI for conflict analysis (5 types: file, API, data, dependency, architecture)
Step 3: For high-severity conflicts without clear resolution → add to `clarifications`
Step 4: Calculate semantic priority (base from issue priority + task_count boost)
Step 5: Assign execution groups: P* (parallel, no overlaps) / S* (sequential, shared files)
Step 6: Write queue JSON + update index
### Output Requirements
**Write files** (exactly 2):
- `.workflow/issues/queues/${queueId}.json` - Full queue with solutions, conflicts, groups
- `.workflow/issues/queues/index.json` - Update with new queue entry
**Return JSON**:
\`\`\`json
{
"queue_id": "${queueId}",
"total_solutions": N,
"total_tasks": N,
"execution_groups": [{"id": "P1", "type": "parallel", "count": N}],
"issues_queued": ["ISS-xxx"],
"clarifications": [{"conflict_id": "CFT-1", "question": "...", "options": [...]}]
}
\`\`\`
### Rules
- Solution granularity (NOT individual tasks)
- Queue Item ID format: S-1, S-2, S-3, ...
- Use provided Queue ID (do NOT generate new)
- `clarifications` only present if high-severity unresolved conflicts exist
### Done Criteria
- [ ] Queue JSON written with all solutions ordered
- [ ] Index updated with active_queue_id
- [ ] No circular dependencies
- [ ] Parallel groups have no file overlaps
- [ ] Return JSON matches required shape
```
**Launch Agent**:
```javascript
const result = Task(
subagent_type="issue-queue-agent",
prompt=agentPrompt,
description=`Order ${solutions.length} solutions`
);
```
### Phase 5: Conflict Clarification
**Check Agent Return:**
- Parse agent result JSON
- If `clarifications` array exists and non-empty → user decision required
**Clarification Flow:**
```javascript
if (result.clarifications?.length > 0) {
for (const clarification of result.clarifications) {
// Present to user via AskUserQuestion
const answer = AskUserQuestion({
questions: [{
question: clarification.question,
header: clarification.conflict_id,
options: clarification.options,
multiSelect: false
}]
});
// Resume agent with user decision
Task(
subagent_type="issue-queue-agent",
resume=agentId,
prompt=`Conflict ${clarification.conflict_id} resolved: ${answer.selected}`
);
}
}
```
### Phase 6: Status Update & Summary
**Status Update** (single command):
```bash
ccw issue update --from-queue [queue-id] --json
# Examples
ccw issue update --from-queue --json # Use active queue
ccw issue update --from-queue QUE-xxx --json # Use specific queue
```
**Output** (JSON):
```json
{
"success": true,
"queue_id": "QUE-xxx",
"queued": ["ISS-001", "ISS-002"],
"queued_count": 2,
"unplanned": ["ISS-003"],
"unplanned_count": 1
}
```
**Behavior:**
- Updates issues in queue to `status: 'queued'` (skips already queued/executing/completed)
- Identifies planned issues with `bound_solution_id` NOT in queue → `unplanned` array
- Optional `queue-id`: defaults to active queue if omitted
**Summary Output:**
- Display queue ID, solution count, task count, issue IDs
- Display queue ID, solution count, task count
- Show unplanned issues (planned but NOT in queue)
- Show next step: `/issue:execute`
@@ -221,77 +267,16 @@ const summary = JSON.parse(result);
}
```
### Queue File Schema (Solution-Level)
```json
{
"id": "QUE-20251227-143000",
"status": "active",
"solutions": [
{
"item_id": "S-1",
"issue_id": "ISS-20251227-003",
"solution_id": "SOL-ISS-20251227-003-1",
"status": "pending",
"execution_order": 1,
"execution_group": "P1",
"depends_on": [],
"semantic_priority": 0.8,
"files_touched": ["src/auth.ts", "src/utils.ts"],
"task_count": 3
},
{
"item_id": "S-2",
"issue_id": "ISS-20251227-001",
"solution_id": "SOL-ISS-20251227-001-1",
"status": "pending",
"execution_order": 2,
"execution_group": "P1",
"depends_on": [],
"semantic_priority": 0.7,
"files_touched": ["src/api.ts"],
"task_count": 2
},
{
"item_id": "S-3",
"issue_id": "ISS-20251227-002",
"solution_id": "SOL-ISS-20251227-002-1",
"status": "pending",
"execution_order": 3,
"execution_group": "S2",
"depends_on": ["S-1"],
"semantic_priority": 0.5,
"files_touched": ["src/auth.ts"],
"task_count": 4
}
],
"conflicts": [
{
"type": "file_conflict",
"file": "src/auth.ts",
"solutions": ["S-1", "S-3"],
"resolution": "sequential",
"resolution_order": ["S-1", "S-3"],
"rationale": "S-1 creates auth module, S-3 extends it"
}
],
"execution_groups": [
{ "id": "P1", "type": "parallel", "solutions": ["S-1", "S-2"] },
{ "id": "S2", "type": "sequential", "solutions": ["S-3"] }
]
}
```
**Note**: Queue file schema is produced by `issue-queue-agent`. See agent documentation for details.
## Error Handling
| Error | Resolution |
|-------|------------|
| No bound solutions | Display message, suggest /issue:plan |
| Circular dependency | List cycles, abort queue formation |
| Unresolved conflicts | Agent resolves using ordering rules |
| Invalid task reference | Skip and warn |
| High-severity conflict | Return `clarifications`, prompt user decision |
| User cancels clarification | Abort queue formation |
| **index.json not updated** | Auto-fix: Set active_queue_id to new queue |
| **Wrong status value** | Auto-fix: Convert non-pending status to "pending" |
| **No entry points (all have deps)** | Auto-fix: Clear depends_on for first item |
| **Queue file missing solutions** | Abort with error, agent must regenerate |
## Quality Checklist
@@ -302,7 +287,7 @@ Before completing, verify:
- [ ] Queue JSON written to `queues/{queue-id}.json`
- [ ] Index updated in `queues/index.json` with `active_queue_id`
- [ ] No circular dependencies in solution DAG
- [ ] File conflicts resolved with rationale
- [ ] All conflicts resolved (auto or via user clarification)
- [ ] Parallel groups have no file overlaps
- [ ] Issue statuses updated to `queued`
@@ -311,3 +296,4 @@ Before completing, verify:
- `/issue:plan` - Plan issues and bind solutions
- `/issue:execute` - Execute queue with codex
- `ccw issue queue list` - View current queue
- `ccw issue update --from-queue [queue-id]` - Sync issue statuses from queue

View File

@@ -1,6 +1,6 @@
---
name: issue-manage
description: Interactive issue management with menu-driven CRUD operations. Use when managing issues, viewing issue status, editing issue fields, or performing bulk operations on issues. Triggers on "manage issue", "list issues", "edit issue", "delete issue", "bulk update", "issue dashboard".
description: Interactive issue management with menu-driven CRUD operations. Use when managing issues, viewing issue status, editing issue fields, performing bulk operations, or viewing issue history. Triggers on "manage issue", "list issues", "edit issue", "delete issue", "bulk update", "issue dashboard", "issue history", "completed issues".
allowed-tools: Bash, Read, Write, AskUserQuestion, Task, Glob
---
@@ -16,17 +16,22 @@ Ask me:
- "Edit issue priority" → Modify fields
- "Delete old issues" → Remove with confirmation
- "Bulk update status" → Batch operations
- "Show completed issues" → View issue history
- "Archive old issues" → Move to history
## CLI Endpoints
```bash
# Core operations
ccw issue list # List all issues
ccw issue list # List active issues
ccw issue list <id> --json # Get issue details
ccw issue history # List completed issues (from history)
ccw issue history --json # Completed issues as JSON
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
ccw issue update <id> --status completed # Complete & auto-archive
# Queue management
ccw issue queue # List current queue
@@ -37,6 +42,7 @@ 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 update --from-queue # Sync statuses from queue
```
## Operations
@@ -113,7 +119,29 @@ This will also remove:
3. Clean up `solutions/<id>.jsonl`
4. Remove from `queue.json`
### 5. BULK 📦
### 5. HISTORY 📚
View and manage completed issues:
```
┌─ Issue History ─────────────────────┐
│ ID Completed Title │
│ ISS-001 2025-12-28 12:00 Fix bug │
│ ISS-002 2025-12-27 15:30 Feature │
└──────────────────────────────────────┘
```
**Flow**:
1. Fetch `ccw issue history --json`
2. Display table: ID | Completed At | Title
3. Optional: Filter by date range
**Auto-Archive**: When issue status → `completed`:
- Issue moves from `issues.jsonl``issue-history.jsonl`
- Solutions remain in `solutions/<id>.jsonl`
- Queue items marked completed
### 6. BULK 📦
Batch operations:
@@ -125,25 +153,34 @@ Batch operations:
| Delete Multiple | Bulk removal |
| Queue All Planned | Add all planned to queue |
| Retry All Failed | Reset failed tasks |
| Sync from Queue | Update statuses from active queue |
## Workflow
```
┌──────────────────────────────────────┐
│ Main Menu │
│ ┌────┐ ┌────┐ ┌────┐ ┌────
│ │List│ │View│ │Edit│ │Bulk│ │
│ └──┬─┘ └──┬─┘ └──┬─┘ └──┬─
└─────┼──────┼──────┼──────┼──────────┘
│ │ │ │
▼ ▼ ▼ ▼
Filter Detail Fields Multi
Select Actions Update Select
│ │ │ │
└──────┴──────┴──────┘
Back to Menu
┌────────────────────────────────────────────────
Main Menu
│ ┌────┐ ┌────┐ ┌────┐ ┌─────┐ ┌────┐
│ │List│ │View│ │Edit│ │Hist.│ │Bulk│
│ └──┬─┘ └──┬─┘ └──┬─┘ └──┬──┘ └──┬─┘
└─────┼──────┼──────┼──────┼───────┼─────────────┘
│ │ │ │
▼ ▼ ▼ ▼
Filter Detail Fields History Multi
Select Actions Update Browse Select
│ │ │ │
└──────┴──────┴──────┴───────
Back to Menu
```
**Issue Lifecycle**:
```
registered → planned → queued → executing → completed
issue-history.jsonl
```
## Implementation Guide
@@ -163,10 +200,11 @@ await showMainMenu(issueId);
```javascript
// 1. Fetch dashboard data
const issues = JSON.parse(Bash('ccw issue list --json') || '[]');
const history = JSON.parse(Bash('ccw issue history --json 2>/dev/null') || '[]');
const queue = JSON.parse(Bash('ccw issue queue --json 2>/dev/null') || '{}');
// 2. Display summary
console.log(`Issues: ${issues.length} | Queue: ${queue.pending_count || 0} pending`);
console.log(`Active: ${issues.length} | Completed: ${history.length} | Queue: ${queue.pending_count || 0} pending`);
// 3. Ask action via AskUserQuestion
const action = AskUserQuestion({
@@ -174,9 +212,10 @@ const action = AskUserQuestion({
question: 'What would you like to do?',
header: 'Action',
options: [
{ label: 'List Issues', description: 'Browse with filters' },
{ label: 'List Issues', description: 'Browse active issues' },
{ label: 'View Issue', description: 'Detail view' },
{ label: 'Edit Issue', description: 'Modify fields' },
{ label: 'View History', description: 'Completed issues' },
{ label: 'Bulk Operations', description: 'Batch actions' }
]
}]
@@ -223,9 +262,11 @@ const issuesPath = '.workflow/issues/issues.jsonl';
| File | Purpose |
|------|---------|
| `.workflow/issues/issues.jsonl` | Issue records |
| `.workflow/issues/issues.jsonl` | Active issue records |
| `.workflow/issues/issue-history.jsonl` | Completed issues (archived) |
| `.workflow/issues/solutions/<id>.jsonl` | Solutions per issue |
| `.workflow/issues/queue.json` | Execution queue |
| `.workflow/issues/queues/index.json` | Queue index (multi-queue) |
| `.workflow/issues/queues/<queue-id>.json` | Individual queue files |
## Error Handling

View File

@@ -282,9 +282,11 @@ export function run(argv: string[]): void {
// New options for solution/queue management
.option('--solution <path>', 'Solution JSON file path')
.option('--solution-id <id>', 'Solution ID')
.option('--data <json>', 'JSON data for create/solution')
.option('--result <json>', 'Execution result JSON')
.option('--reason <text>', 'Failure reason')
.option('--fail', 'Mark task as failed')
.option('--from-queue [queue-id]', 'Sync issue statuses from queue (default: active queue)')
.action((subcommand, args, options) => issueCommand(subcommand, args, options));
program.parse(argv);

View File

@@ -196,6 +196,7 @@ interface IssueOptions {
fail?: boolean;
ids?: boolean; // List only IDs (one per line)
data?: string; // JSON data for create
fromQueue?: boolean | string; // Sync statuses from queue (true=active, string=specific queue ID)
}
const ISSUES_DIR = '.workflow/issues';
@@ -251,12 +252,69 @@ function findIssue(issueId: string): Issue | undefined {
return readIssues().find(i => i.id === issueId);
}
// ============ Issue History JSONL ============
function readIssueHistory(): Issue[] {
const path = join(getIssuesDir(), 'issue-history.jsonl');
if (!existsSync(path)) return [];
try {
return readFileSync(path, 'utf-8')
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
} catch {
return [];
}
}
function appendIssueHistory(issue: Issue): void {
ensureIssuesDir();
const path = join(getIssuesDir(), 'issue-history.jsonl');
const line = JSON.stringify(issue) + '\n';
// Append to history file
if (existsSync(path)) {
const content = readFileSync(path, 'utf-8');
// Ensure proper newline before appending
const needsNewline = content.length > 0 && !content.endsWith('\n');
writeFileSync(path, (needsNewline ? '\n' : '') + line, { flag: 'a' });
} else {
writeFileSync(path, line, 'utf-8');
}
}
/**
* Move completed issue from issues.jsonl to issue-history.jsonl
*/
function moveIssueToHistory(issueId: string): boolean {
const issues = readIssues();
const idx = issues.findIndex(i => i.id === issueId);
if (idx === -1) return false;
const issue = issues[idx];
if (issue.status !== 'completed') return false;
// Append to history
appendIssueHistory(issue);
// Remove from active issues
issues.splice(idx, 1);
writeIssues(issues);
return true;
}
function updateIssue(issueId: string, updates: Partial<Issue>): boolean {
const issues = readIssues();
const idx = issues.findIndex(i => i.id === issueId);
if (idx === -1) return false;
issues[idx] = { ...issues[idx], ...updates, updated_at: new Date().toISOString() };
writeIssues(issues);
// Auto-move to history when completed
if (updates.status === 'completed') {
moveIssueToHistory(issueId);
}
return true;
}
@@ -740,6 +798,46 @@ async function listAction(issueId: string | undefined, options: IssueOptions): P
}
}
/**
* history - List completed issues from history
*/
async function historyAction(options: IssueOptions): Promise<void> {
const history = readIssueHistory();
// IDs only mode
if (options.ids) {
history.forEach(i => console.log(i.id));
return;
}
if (options.json) {
console.log(JSON.stringify(history, null, 2));
return;
}
if (history.length === 0) {
console.log(chalk.yellow('No completed issues in history'));
return;
}
console.log(chalk.bold.cyan('\nIssue History (Completed)\n'));
console.log(chalk.gray('ID'.padEnd(25) + 'Completed At'.padEnd(22) + 'Title'));
console.log(chalk.gray('-'.repeat(80)));
for (const issue of history) {
const completedAt = issue.completed_at
? new Date(issue.completed_at).toLocaleString()
: 'N/A';
console.log(
chalk.green(issue.id.padEnd(25)) +
completedAt.padEnd(22) +
(issue.title || '').substring(0, 35)
);
}
console.log(chalk.gray(`\nTotal: ${history.length} completed issues`));
}
/**
* status - Show detailed status
*/
@@ -891,11 +989,88 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine
/**
* update - Update issue fields (status, priority, title, etc.)
* --from-queue: Sync statuses from active queue (auto-update queued issues)
*/
async function updateAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
// Handle --from-queue: Sync statuses from queue
if (options.fromQueue) {
// Determine queue ID: string value = specific queue, true = active queue
const queueId = typeof options.fromQueue === 'string' ? options.fromQueue : undefined;
const queue = queueId ? readQueue(queueId) : readActiveQueue();
if (!queue) {
if (options.json) {
console.log(JSON.stringify({ success: false, message: `Queue not found: ${queueId}`, queued: [], unplanned: [] }));
} else {
console.log(chalk.red(`Queue not found: ${queueId}`));
}
return;
}
const items = queue.solutions || queue.tasks || [];
const allIssues = readIssues();
if (!queue.id || items.length === 0) {
if (options.json) {
console.log(JSON.stringify({ success: false, message: 'No active queue', queued: [], unplanned: [] }));
} else {
console.log(chalk.yellow('No active queue to sync from'));
}
return;
}
// Get issue IDs from queue
const queuedIssueIds = new Set(items.map(item => item.issue_id));
const now = new Date().toISOString();
// Track updates
const updated: string[] = [];
const unplanned: string[] = [];
// Update queued issues
for (const issueId of queuedIssueIds) {
const issue = allIssues.find(i => i.id === issueId);
if (issue && issue.status !== 'queued' && issue.status !== 'executing' && issue.status !== 'completed') {
updateIssue(issueId, { status: 'queued', queued_at: now });
updated.push(issueId);
}
}
// Find planned issues NOT in queue
for (const issue of allIssues) {
if (issue.status === 'planned' && issue.bound_solution_id && !queuedIssueIds.has(issue.id)) {
unplanned.push(issue.id);
}
}
if (options.json) {
console.log(JSON.stringify({
success: true,
queue_id: queue.id,
queued: updated,
queued_count: updated.length,
unplanned: unplanned,
unplanned_count: unplanned.length
}, null, 2));
} else {
console.log(chalk.green(`✓ Synced from queue ${queue.id}`));
console.log(chalk.gray(` Updated to 'queued': ${updated.length} issues`));
if (updated.length > 0) {
updated.forEach(id => console.log(chalk.gray(` - ${id}`)));
}
if (unplanned.length > 0) {
console.log(chalk.yellow(` Planned but NOT in queue: ${unplanned.length} issues`));
unplanned.forEach(id => console.log(chalk.yellow(` - ${id}`)));
}
}
return;
}
// Standard single-issue update
if (!issueId) {
console.error(chalk.red('Issue ID is required'));
console.error(chalk.gray('Usage: ccw issue update <issue-id> --status <status> [--priority <n>] [--title "..."]'));
console.error(chalk.gray('Usage: ccw issue update <issue-id> --status <status>'));
console.error(chalk.gray(' ccw issue update --from-queue [queue-id] (sync from queue)'));
process.exit(1);
}
@@ -1712,6 +1887,9 @@ export async function issueCommand(
case 'list':
await listAction(argsArray[0], options);
break;
case 'history':
await historyAction(options);
break;
case 'status':
await statusAction(argsArray[0], options);
break;
@@ -1753,12 +1931,15 @@ export async function issueCommand(
default:
console.log(chalk.bold.cyan('\nCCW Issue Management (v3.0 - Multi-Queue + Lifecycle)\n'));
console.log(chalk.bold('Core Commands:'));
console.log(chalk.gray(' init <issue-id> Initialize new issue'));
console.log(chalk.gray(' create --data \'{"title":"..."}\' Create issue (auto-generates ID)'));
console.log(chalk.gray(' init <issue-id> Initialize new issue (manual ID)'));
console.log(chalk.gray(' list [issue-id] List issues or tasks'));
console.log(chalk.gray(' history List completed issues (from history)'));
console.log(chalk.gray(' status [issue-id] Show detailed status'));
console.log(chalk.gray(' task <issue-id> [task-id] Add or update task'));
console.log(chalk.gray(' bind <issue-id> [sol-id] Bind solution (--solution <path> to register)'));
console.log(chalk.gray(' update <issue-id> Update issue (--status, --priority, --title)'));
console.log(chalk.gray(' solution <id> --data \'{...}\' Create solution (auto-generates ID)'));
console.log(chalk.gray(' bind <issue-id> [sol-id] Bind solution'));
console.log(chalk.gray(' update <issue-id> --status <s> Update issue status'));
console.log(chalk.gray(' update --from-queue [queue-id] Sync statuses from queue (default: active)'));
console.log();
console.log(chalk.bold('Queue Commands:'));
console.log(chalk.gray(' queue Show active queue'));
@@ -1787,7 +1968,8 @@ export async function issueCommand(
console.log(chalk.gray(' --force Force operation'));
console.log();
console.log(chalk.bold('Storage:'));
console.log(chalk.gray(' .workflow/issues/issues.jsonl All issues'));
console.log(chalk.gray(' .workflow/issues/issues.jsonl Active issues'));
console.log(chalk.gray(' .workflow/issues/issue-history.jsonl Completed issues'));
console.log(chalk.gray(' .workflow/issues/solutions/*.jsonl Solutions per issue'));
console.log(chalk.gray(' .workflow/issues/queues/ Queue files (multi-queue)'));
console.log(chalk.gray(' .workflow/issues/queues/index.json Queue index'));

View File

@@ -67,6 +67,17 @@ function readSolutionsJsonl(issuesDir: string, issueId: string): any[] {
}
}
function readIssueHistoryJsonl(issuesDir: string): any[] {
const historyPath = join(issuesDir, 'issue-history.jsonl');
if (!existsSync(historyPath)) return [];
try {
const content = readFileSync(historyPath, 'utf8');
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
} catch {
return [];
}
}
function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[]) {
const solutionsDir = join(issuesDir, 'solutions');
if (!existsSync(solutionsDir)) mkdirSync(solutionsDir, { recursive: true });
@@ -376,6 +387,17 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// GET /api/issues/history - List completed issues from history
if (pathname === '/api/issues/history' && req.method === 'GET') {
const history = readIssueHistoryJsonl(issuesDir);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
issues: history,
_metadata: { version: '1.0', storage: 'jsonl', total_issues: history.length, last_updated: new Date().toISOString() }
}));
return true;
}
// POST /api/issues - Create issue
if (pathname === '/api/issues' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {