From 7d71f603fe3218ad5d346f378f336c6301098afc Mon Sep 17 00:00:00 2001 From: catlog22 Date: Mon, 29 Dec 2025 20:24:31 +0800 Subject: [PATCH] feat(issue): add solution endpoint with auto-increment ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `ccw issue solution --data '{...}'` for solution creation - Add createSolution() with proper JSONL handling (trailing newline) - Fix writeSolutions() to always add trailing newline - Update plan.md to use CLI endpoint Supports multiple solutions per issue with sequential IDs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/commands/issue/plan.md | 17 +++++++-- ccw/src/commands/issue.ts | 69 +++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/.claude/commands/issue/plan.md b/.claude/commands/issue/plan.md index 6a90995e..88cc8c8f 100644 --- a/.claude/commands/issue/plan.md +++ b/.claude/commands/issue/plan.md @@ -176,13 +176,22 @@ ${issueList} 4. Plan solution with tasks (see issue-plan-agent.md for details) 5. Write solutions to JSONL, bind if single solution -### Generate Files -\`.workflow/issues/solutions/{issue-id}.jsonl\` - Solution with tasks (schema: cat .claude/workflows/cli-templates/schemas/solution-schema.json) +### Solution Creation (via CLI endpoint) +```bash +ccw issue solution --data '{"description":"...", "approach":"...", "tasks":[...]}' +``` -**Solution ID Format**: \`SOL-{issue-id}-{seq}\` (e.g., \`SOL-GH-123-1\`, \`SOL-ISS-20251229-1\`) +**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 --solution \` +- **Single solution**: Auto-bind via `ccw issue bind ` - **Multiple solutions**: Register only, return for user selection ### Return Summary diff --git a/ccw/src/commands/issue.ts b/ccw/src/commands/issue.ts index 56534be3..41748192 100644 --- a/ccw/src/commands/issue.ts +++ b/ccw/src/commands/issue.ts @@ -333,7 +333,9 @@ function readSolutions(issueId: string): Solution[] { function writeSolutions(issueId: string, solutions: Solution[]): void { const dir = join(getIssuesDir(), 'solutions'); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); - writeFileSync(getSolutionsPath(issueId), solutions.map(s => JSON.stringify(s)).join('\n'), 'utf-8'); + // Always add trailing newline for proper JSONL format + const content = solutions.map(s => JSON.stringify(s)).join('\n'); + writeFileSync(getSolutionsPath(issueId), content ? content + '\n' : '', 'utf-8'); } function findSolution(issueId: string, solutionId: string): Solution | undefined { @@ -363,6 +365,40 @@ function generateSolutionId(issueId: string, existingSolutions: Solution[] = []) return `SOL-${issueId}-${maxSeq + 1}`; } +/** + * Create a new solution with proper JSONL handling + * Auto-generates ID if not provided + */ +function createSolution(issueId: string, data: Partial): Solution { + const issue = findIssue(issueId); + if (!issue) { + throw new Error(`Issue "${issueId}" not found`); + } + + const solutions = readSolutions(issueId); + const solutionId = data.id || generateSolutionId(issueId, solutions); + + if (solutions.some(s => s.id === solutionId)) { + throw new Error(`Solution "${solutionId}" already exists`); + } + + const newSolution: Solution = { + id: solutionId, + description: data.description || '', + approach: data.approach || '', + tasks: data.tasks || [], + exploration_context: data.exploration_context, + analysis: data.analysis, + score: data.score, + is_bound: false, + created_at: new Date().toISOString() + }; + + solutions.push(newSolution); + writeSolutions(issueId, solutions); + return newSolution; +} + // ============ Queue Management (Multi-Queue) ============ function getQueuesDir(): string { @@ -542,6 +578,34 @@ async function createAction(options: IssueOptions): Promise { } } +/** + * solution - Create solution from JSON data + * Usage: ccw issue solution --data '{"tasks":[...]}' + * Output: JSON with created solution (includes auto-generated ID) + */ +async function solutionAction(issueId: string | undefined, options: IssueOptions): Promise { + if (!issueId) { + console.error(chalk.red('Issue ID required')); + console.error(chalk.gray('Usage: ccw issue solution --data \'{"tasks":[...]}\'')); + process.exit(1); + } + + if (!options.data) { + console.error(chalk.red('JSON data required')); + console.error(chalk.gray('Usage: ccw issue solution --data \'{"tasks":[...]}\'')); + process.exit(1); + } + + try { + const data = JSON.parse(options.data); + const solution = createSolution(issueId, data); + console.log(JSON.stringify(solution, null, 2)); + } catch (err) { + console.error(chalk.red((err as Error).message)); + process.exit(1); + } +} + /** * init - Initialize a new issue (manual ID) */ @@ -1639,6 +1703,9 @@ export async function issueCommand( case 'create': await createAction(options); break; + case 'solution': + await solutionAction(argsArray[0], options); + break; case 'init': await initAction(argsArray[0], options); break;