diff --git a/.claude/agents/issue-plan-agent.md b/.claude/agents/issue-plan-agent.md index 059ed248..580a74fd 100644 --- a/.claude/agents/issue-plan-agent.md +++ b/.claude/agents/issue-plan-agent.md @@ -182,17 +182,37 @@ function decomposeTasks(issue, exploration) { - Task validation (all 5 phases present) - Conflict detection (cross-issue file modifications) -**Solution Registration** (CRITICAL: check solution count first): +**Solution Registration** (TWO-STEP: Write first, then bind): ```javascript for (const issue of issues) { const solutions = generatedSolutions[issue.id]; + const solPath = `.workflow/issues/solutions/${issue.id}.jsonl`; + // Step 1: ALWAYS write ALL solutions to JSONL (append mode) + // Each solution on a new line, preserving existing solutions + for (const sol of solutions) { + // Ensure Solution ID format: SOL-{issue-id}-{seq} + const solutionJson = JSON.stringify({ + id: sol.id, // e.g., SOL-GH-123-1, SOL-GH-123-2 + description: sol.description, + approach: sol.approach, + tasks: sol.tasks, + exploration_context: sol.exploration_context, + analysis: sol.analysis, + score: sol.score, + is_bound: false, + created_at: new Date().toISOString() + }); + Bash(`echo '${solutionJson}' >> "${solPath}"`); + } + + // Step 2: Bind decision based on solution count if (solutions.length === 1) { - // Single solution → auto-bind - Bash(`ccw issue bind ${issue.id} --solution ${solutions[0].file}`); + // Single solution → auto-bind by ID (NOT --solution flag) + Bash(`ccw issue bind ${issue.id} ${solutions[0].id}`); bound.push({ issue_id: issue.id, solution_id: solutions[0].id, task_count: solutions[0].tasks.length }); } else { - // Multiple solutions → DO NOT BIND, return for user selection + // Multiple solutions → already written, return for user selection pending_selection.push({ issue_id: issue.id, solutions: solutions.map(s => ({ id: s.id, description: s.description, task_count: s.tasks.length })) @@ -332,8 +352,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. Single solution → auto-bind; Multiple → return `pending_selection` +7. **TWO-STEP registration**: Write JSONL first, then bind (see Phase 4) 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`) **NEVER**: 1. Execute implementation (return plan only) @@ -341,8 +362,9 @@ Each line is a solution JSON containing tasks. Schema: `cat .claude/workflows/cl 3. Create circular dependencies 4. Generate more than 10 tasks per issue 5. **Bind when multiple solutions exist** - MUST check `solutions.length === 1` before calling `ccw issue bind` +6. **Skip JSONL write** - ALL solutions must be written to disk before returning -**OUTPUT**: -1. Register solutions via `ccw issue bind --solution ` -2. Return JSON with `bound`, `pending_selection`, `conflicts` -3. Solutions written to `.workflow/issues/solutions/{issue-id}.jsonl` +**OUTPUT** (Two-Step): +1. **Step 1**: Write ALL solutions to `.workflow/issues/solutions/{issue-id}.jsonl` (one JSON per line) +2. **Step 2**: Single solution → `ccw issue bind `; Multiple → return only +3. Return JSON with `bound`, `pending_selection`, `conflicts` diff --git a/.claude/commands/issue/plan.md b/.claude/commands/issue/plan.md index 696a46e5..d6a6baf7 100644 --- a/.claude/commands/issue/plan.md +++ b/.claude/commands/issue/plan.md @@ -359,10 +359,27 @@ if (pendingSelections.length > 0) { })) }); - // Bind user-selected solutions - for (const { issue_id } of pendingSelections) { + // Bind user-selected solutions (with file validation) + for (const { issue_id, solutions } of pendingSelections) { const selectedId = extractSelectedSolutionId(answer, issue_id); if (selectedId) { + // Verify solution file exists and contains the selected ID + const solPath = `.workflow/issues/solutions/${issue_id}.jsonl`; + const fileExists = Bash(`test -f "${solPath}" && echo "yes" || echo "no"`).trim() === 'yes'; + + if (!fileExists) { + console.log(`⚠ ${issue_id}: Solution file missing, attempting recovery...`); + // Recovery: write solution from pending_selection payload + const selectedSol = solutions.find(s => s.id === selectedId); + if (selectedSol) { + Bash(`mkdir -p .workflow/issues/solutions`); + const solJson = JSON.stringify({ id: selectedId, ...selectedSol, is_bound: false, created_at: new Date().toISOString() }); + Bash(`echo '${solJson}' >> "${solPath}"`); + console.log(` Recovered ${selectedId} to ${solPath}`); + } + } + + // Now bind Bash(`ccw issue bind ${issue_id} ${selectedId}`); console.log(`✓ ${issue_id}: ${selectedId} bound`); } diff --git a/ccw/src/commands/issue.ts b/ccw/src/commands/issue.ts index c0658bfc..f5f3040e 100644 --- a/ccw/src/commands/issue.ts +++ b/ccw/src/commands/issue.ts @@ -271,6 +271,11 @@ function getBoundSolution(issueId: string): Solution | undefined { return readSolutions(issueId).find(s => s.is_bound); } +/** + * Generate fallback solution ID (timestamp-based). + * Note: Prefer agent-generated IDs in format `SOL-{issue-id}-{seq}` (e.g., SOL-GH-123-1). + * This function is only used when no ID is provided via CLI or file content. + */ function generateSolutionId(): string { const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14); return `SOL-${ts}`; @@ -802,8 +807,10 @@ async function bindAction(issueId: string | undefined, solutionId: string | unde try { const content = readFileSync(options.solution, 'utf-8'); const data = JSON.parse(content); + // Priority: CLI arg > file content ID > generate new + // This ensures agent-generated IDs (SOL-{issue-id}-{seq}) are preserved const newSol: Solution = { - id: solutionId || generateSolutionId(), + id: solutionId || data.id || generateSolutionId(), description: data.description || data.approach_name || 'Imported solution', tasks: data.tasks || [], exploration_context: data.exploration_context,