From ae76926d5a622eae0467c3761a4015af95ac96a6 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Mon, 29 Dec 2025 22:14:06 +0800 Subject: [PATCH] feat(queue): support solution-based queues and update metadata handling --- .claude/agents/issue-plan-agent.md | 55 +++++++++++++++++------ ccw/src/core/routes/issue-routes.ts | 70 +++++++++++++++++++++++------ 2 files changed, 98 insertions(+), 27 deletions(-) diff --git a/.claude/agents/issue-plan-agent.md b/.claude/agents/issue-plan-agent.md index c7707bb8..9d8196b4 100644 --- a/.claude/agents/issue-plan-agent.md +++ b/.claude/agents/issue-plan-agent.md @@ -172,20 +172,47 @@ function decomposeTasks(issue, exploration) { - Task validation (all 5 phases present) - File isolation check (ensure minimal overlap across issues in batch) -**Solution Registration** (via CLI endpoint): +**Solution Registration** (via file write): -**Step 1: Create solutions** -```bash -ccw issue solution --data '{"description":"...", "approach":"...", "tasks":[...]}' -# Output: {"id":"SOL-{issue-id}-1", ...} +**Step 1: Create solution files** + +Write solution JSON to JSONL file (one line per solution): + +``` +.workflow/issues/solutions/{issue-id}.jsonl ``` -**CLI 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 | -| Trailing newline | Proper JSONL format, no corruption | +**File Format** (JSONL - each line is a complete solution): +``` +{"id":"SOL-GH-123-1","description":"...","approach":"...","analysis":{...},"score":0.85,"tasks":[...]} +{"id":"SOL-GH-123-2","description":"...","approach":"...","analysis":{...},"score":0.75,"tasks":[...]} +``` + +**Solution Schema** (must match CLI `Solution` interface): +```typescript +{ + id: string; // Format: SOL-{issue-id}-{N} + description?: string; + approach?: string; + tasks: SolutionTask[]; + analysis?: { risk, impact, complexity }; + score?: number; + // Note: is_bound, created_at are added by CLI on read +} +``` + +**Write Operation**: +```javascript +// Append solution to JSONL file (one line per solution) +const solutionId = `SOL-${issueId}-${seq}`; +const solutionLine = JSON.stringify({ id: solutionId, ...solution }); + +// Read existing, append new line, write back +const filePath = `.workflow/issues/solutions/${issueId}.jsonl`; +const existing = existsSync(filePath) ? readFileSync(filePath) : ''; +const newContent = existing.trimEnd() + (existing ? '\n' : '') + solutionLine + '\n'; +Write({ file_path: filePath, content: newContent }) +``` **Step 2: Bind decision** - **Single solution** → Auto-bind: `ccw issue bind ` @@ -251,9 +278,9 @@ Each line is a solution JSON containing tasks. Schema: `cat .claude/workflows/cl 4. Quantify acceptance.criteria with testable conditions 5. Validate DAG before output 6. Evaluate each solution with `analysis` and `score` -7. Use CLI endpoint: `ccw issue solution --data '{...}'` +7. Write solutions to `.workflow/issues/solutions/{issue-id}.jsonl` (append mode) 8. For HIGH complexity: generate 2-3 candidate solutions -9. **Solution ID format**: `SOL-{issue-id}-{seq}` (e.g., `SOL-GH-123-1`, `SOL-GH-123-2`) +9. **Solution ID format**: `SOL-{issue-id}-{N}` (e.g., `SOL-GH-123-1`, `SOL-GH-123-2`) **CONFLICT AVOIDANCE** (for batch processing of similar issues): 1. **File isolation**: Each issue's solution should target distinct files when possible @@ -270,6 +297,6 @@ Each line is a solution JSON containing tasks. Schema: `cat .claude/workflows/cl 5. **Bind when multiple solutions exist** - MUST check `solutions.length === 1` before calling `ccw issue bind` **OUTPUT**: -1. Create solutions via CLI: `ccw issue solution --data '{...}'` +1. Write solutions to `.workflow/issues/solutions/{issue-id}.jsonl` (JSONL format) 2. Single solution → `ccw issue bind `; Multiple → return only 3. Return JSON with `bound`, `pending_selection` diff --git a/ccw/src/core/routes/issue-routes.ts b/ccw/src/core/routes/issue-routes.ts index aefbef5c..83a736dd 100644 --- a/ccw/src/core/routes/issue-routes.ts +++ b/ccw/src/core/routes/issue-routes.ts @@ -120,7 +120,18 @@ function readQueue(issuesDir: string) { function writeQueue(issuesDir: string, queue: any) { if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true }); - queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.tasks?.length || 0 }; + + // Support both solution-based and task-based queues + const items = queue.solutions || queue.tasks || []; + const isSolutionBased = Array.isArray(queue.solutions) && queue.solutions.length > 0; + + queue._metadata = { + ...queue._metadata, + updated_at: new Date().toISOString(), + ...(isSolutionBased + ? { total_solutions: items.length } + : { total_tasks: items.length }) + }; // Check if using new multi-queue structure const queuesDir = join(issuesDir, 'queues'); @@ -136,8 +147,13 @@ function writeQueue(issuesDir: string, queue: any) { const index = JSON.parse(readFileSync(indexPath, 'utf8')); const queueEntry = index.queues?.find((q: any) => q.id === queue.id); if (queueEntry) { - queueEntry.total_tasks = queue.tasks?.length || 0; - queueEntry.completed_tasks = queue.tasks?.filter((i: any) => i.status === 'completed').length || 0; + if (isSolutionBased) { + queueEntry.total_solutions = items.length; + queueEntry.completed_solutions = items.filter((i: any) => i.status === 'completed').length; + } else { + queueEntry.total_tasks = items.length; + queueEntry.completed_tasks = items.filter((i: any) => i.status === 'completed').length; + } writeFileSync(indexPath, JSON.stringify(index, null, 2)); } } catch { @@ -184,9 +200,26 @@ function enrichIssues(issues: any[], issuesDir: string) { }); } +/** + * Get queue items (supports both solution-based and task-based queues) + */ +function getQueueItems(queue: any): any[] { + return queue.solutions || queue.tasks || []; +} + +/** + * Check if queue is solution-based + */ +function isSolutionBasedQueue(queue: any): boolean { + return Array.isArray(queue.solutions) && queue.solutions.length > 0; +} + function groupQueueByExecutionGroup(queue: any) { const groups: { [key: string]: any[] } = {}; - for (const item of queue.tasks || []) { + const items = getQueueItems(queue); + const isSolutionBased = isSolutionBasedQueue(queue); + + for (const item of items) { const groupId = item.execution_group || 'ungrouped'; if (!groups[groupId]) groups[groupId] = []; groups[groupId].push(item); @@ -194,11 +227,13 @@ function groupQueueByExecutionGroup(queue: any) { for (const groupId of Object.keys(groups)) { groups[groupId].sort((a, b) => (a.execution_order || 0) - (b.execution_order || 0)); } - const executionGroups = Object.entries(groups).map(([id, items]) => ({ + const executionGroups = Object.entries(groups).map(([id, groupItems]) => ({ id, type: id.startsWith('P') ? 'parallel' : id.startsWith('S') ? 'sequential' : 'unknown', - task_count: items.length, - tasks: items.map(i => i.item_id) + // Use appropriate count field based on queue type + ...(isSolutionBased + ? { solution_count: groupItems.length, solutions: groupItems.map(i => i.item_id) } + : { task_count: groupItems.length, tasks: groupItems.map(i => i.item_id) }) })).sort((a, b) => { const aFirst = groups[a.id]?.[0]?.execution_order || 0; const bFirst = groups[b.id]?.[0]?.execution_order || 0; @@ -323,7 +358,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise { return true; } - // POST /api/queue/reorder - Reorder queue items + // POST /api/queue/reorder - Reorder queue items (supports both solutions and tasks) if (pathname === '/api/queue/reorder' && req.method === 'POST') { handlePostRequest(req, res, async (body: any) => { const { groupId, newOrder } = body; @@ -332,8 +367,11 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise { } const queue = readQueue(issuesDir); - const groupItems = queue.tasks.filter((item: any) => item.execution_group === groupId); - const otherItems = queue.tasks.filter((item: any) => item.execution_group !== groupId); + const items = getQueueItems(queue); + const isSolutionBased = isSolutionBasedQueue(queue); + + const groupItems = items.filter((item: any) => item.execution_group === groupId); + const otherItems = items.filter((item: any) => item.execution_group !== groupId); if (groupItems.length === 0) return { error: `No items in group ${groupId}` }; @@ -347,7 +385,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise { const itemMap = new Map(groupItems.map((i: any) => [i.item_id, i])); const reorderedItems = newOrder.map((qid: string, idx: number) => ({ ...itemMap.get(qid), _idx: idx })); - const newQueue = [...otherItems, ...reorderedItems].sort((a, b) => { + const newQueueItems = [...otherItems, ...reorderedItems].sort((a, b) => { const aGroup = parseInt(a.execution_group?.match(/\d+/)?.[0] || '999'); const bGroup = parseInt(b.execution_group?.match(/\d+/)?.[0] || '999'); if (aGroup !== bGroup) return aGroup - bGroup; @@ -357,8 +395,14 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise { return (a.execution_order || 0) - (b.execution_order || 0); }); - newQueue.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; }); - queue.tasks = newQueue; + newQueueItems.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; }); + + // Write back to appropriate array based on queue type + if (isSolutionBased) { + queue.solutions = newQueueItems; + } else { + queue.tasks = newQueueItems; + } writeQueue(issuesDir, queue); return { success: true, groupId, reordered: newOrder.length };