mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
719 lines
23 KiB
Markdown
719 lines
23 KiB
Markdown
---
|
|
name: convert-to-plan
|
|
description: Convert planning artifacts (lite-plan, workflow session, markdown) to issue solutions
|
|
argument-hint: "[-y|--yes] [--issue <id>] [--supplement] <SOURCE>"
|
|
allowed-tools: TodoWrite(*), Bash(*), Read(*), Write(*), Glob(*), AskUserQuestion(*)
|
|
---
|
|
|
|
## Auto Mode
|
|
|
|
When `--yes` or `-y`: Skip confirmation, auto-create issue and bind solution.
|
|
|
|
# Issue Convert-to-Plan Command (/issue:convert-to-plan)
|
|
|
|
## Overview
|
|
|
|
Converts various planning artifact formats into issue workflow solutions with intelligent detection and automatic binding.
|
|
|
|
**Supported Sources** (auto-detected):
|
|
- **lite-plan**: `.workflow/.lite-plan/{slug}/plan.json`
|
|
- **workflow-session**: `WFS-xxx` ID or `.workflow/active/{session}/` folder
|
|
- **markdown**: Any `.md` file with implementation/task content
|
|
- **json**: Direct JSON files matching plan-json-schema
|
|
|
|
## Quick Reference
|
|
|
|
```bash
|
|
# Convert lite-plan to new issue (auto-creates issue)
|
|
/issue:convert-to-plan ".workflow/.lite-plan/implement-auth-2026-01-25"
|
|
|
|
# Convert workflow session to existing issue
|
|
/issue:convert-to-plan WFS-auth-impl --issue GH-123
|
|
|
|
# Supplement existing solution with additional tasks
|
|
/issue:convert-to-plan "./docs/additional-tasks.md" --issue ISS-001 --supplement
|
|
|
|
# Auto mode - skip confirmations
|
|
/issue:convert-to-plan ".workflow/.lite-plan/my-plan" -y
|
|
```
|
|
|
|
## Command Options
|
|
|
|
| Option | Description | Default |
|
|
|--------|-------------|---------|
|
|
| `<SOURCE>` | Planning artifact path or WFS-xxx ID | Required |
|
|
| `--issue <id>` | Bind to existing issue instead of creating new | Auto-create |
|
|
| `--supplement` | Add tasks to existing solution (requires --issue) | false |
|
|
| `-y, --yes` | Skip all confirmations | false |
|
|
|
|
## Core Data Access Principle
|
|
|
|
**⚠️ Important**: Use CLI commands for all issue/solution operations.
|
|
|
|
| Operation | Correct | Incorrect |
|
|
|-----------|---------|-----------|
|
|
| Get issue | `ccw issue status <id> --json` | Read issues.jsonl directly |
|
|
| Create issue | `ccw issue init <id> --title "..."` | Write to issues.jsonl |
|
|
| Bind solution | `ccw issue bind <id> <sol-id>` | Edit issues.jsonl |
|
|
| List solutions | `ccw issue solutions --issue <id> --brief` | Read solutions/*.jsonl |
|
|
|
|
## Solution Schema Reference
|
|
|
|
Target format for all extracted data (from solution-schema.json):
|
|
|
|
```typescript
|
|
interface Solution {
|
|
id: string; // SOL-{issue-id}-{4-char-uid}
|
|
description?: string; // High-level summary
|
|
approach?: string; // Technical strategy
|
|
tasks: Task[]; // Required: at least 1 task
|
|
exploration_context?: object; // Optional: source context
|
|
analysis?: { risk, impact, complexity };
|
|
score?: number; // 0.0-1.0
|
|
is_bound: boolean;
|
|
created_at: string;
|
|
bound_at?: string;
|
|
}
|
|
|
|
interface Task {
|
|
id: string; // T1, T2, T3... (pattern: ^T[0-9]+$)
|
|
title: string; // Required: action verb + target
|
|
scope: string; // Required: module path or feature area
|
|
action: Action; // Required: Create|Update|Implement|...
|
|
description?: string;
|
|
modification_points?: Array<{file, target, change}>;
|
|
implementation: string[]; // Required: step-by-step guide
|
|
test?: { unit?, integration?, commands?, coverage_target? };
|
|
acceptance: { criteria: string[], verification: string[] }; // Required
|
|
commit?: { type, scope, message_template, breaking? };
|
|
depends_on?: string[];
|
|
priority?: number; // 1-5 (default: 3)
|
|
}
|
|
|
|
type Action = 'Create' | 'Update' | 'Implement' | 'Refactor' | 'Add' | 'Delete' | 'Configure' | 'Test' | 'Fix';
|
|
```
|
|
|
|
## Implementation
|
|
|
|
### Phase 1: Parse Arguments & Detect Source Type
|
|
|
|
```javascript
|
|
const input = userInput.trim();
|
|
const flags = parseFlags(userInput); // --issue, --supplement, -y/--yes
|
|
|
|
// Extract source path (first non-flag argument)
|
|
const source = extractSourceArg(input);
|
|
|
|
// Detect source type
|
|
function detectSourceType(source) {
|
|
// Check for WFS-xxx pattern (workflow session ID)
|
|
if (source.match(/^WFS-[\w-]+$/)) {
|
|
return { type: 'workflow-session-id', path: `.workflow/active/${source}` };
|
|
}
|
|
|
|
// Check if directory
|
|
const isDir = Bash(`test -d "${source}" && echo "dir" || echo "file"`).trim() === 'dir';
|
|
|
|
if (isDir) {
|
|
// Check for lite-plan indicator
|
|
const hasPlanJson = Bash(`test -f "${source}/plan.json" && echo "yes" || echo "no"`).trim() === 'yes';
|
|
if (hasPlanJson) {
|
|
return { type: 'lite-plan', path: source };
|
|
}
|
|
|
|
// Check for workflow session indicator
|
|
const hasSession = Bash(`test -f "${source}/workflow-session.json" && echo "yes" || echo "no"`).trim() === 'yes';
|
|
if (hasSession) {
|
|
return { type: 'workflow-session', path: source };
|
|
}
|
|
}
|
|
|
|
// Check file extensions
|
|
if (source.endsWith('.json')) {
|
|
return { type: 'json-file', path: source };
|
|
}
|
|
if (source.endsWith('.md')) {
|
|
return { type: 'markdown-file', path: source };
|
|
}
|
|
|
|
// Check if path exists at all
|
|
const exists = Bash(`test -e "${source}" && echo "yes" || echo "no"`).trim() === 'yes';
|
|
if (!exists) {
|
|
throw new Error(`E001: Source not found: ${source}`);
|
|
}
|
|
|
|
return { type: 'unknown', path: source };
|
|
}
|
|
|
|
const sourceInfo = detectSourceType(source);
|
|
if (sourceInfo.type === 'unknown') {
|
|
throw new Error(`E002: Unable to detect source format for: ${source}`);
|
|
}
|
|
|
|
console.log(`Detected source type: ${sourceInfo.type}`);
|
|
```
|
|
|
|
### Phase 2: Extract Data Using Format-Specific Extractor
|
|
|
|
```javascript
|
|
let extracted = { title: '', approach: '', tasks: [], metadata: {} };
|
|
|
|
switch (sourceInfo.type) {
|
|
case 'lite-plan':
|
|
extracted = extractFromLitePlan(sourceInfo.path);
|
|
break;
|
|
case 'workflow-session':
|
|
case 'workflow-session-id':
|
|
extracted = extractFromWorkflowSession(sourceInfo.path);
|
|
break;
|
|
case 'markdown-file':
|
|
extracted = await extractFromMarkdownAI(sourceInfo.path);
|
|
break;
|
|
case 'json-file':
|
|
extracted = extractFromJsonFile(sourceInfo.path);
|
|
break;
|
|
}
|
|
|
|
// Validate extraction
|
|
if (!extracted.tasks || extracted.tasks.length === 0) {
|
|
throw new Error('E006: No tasks extracted from source');
|
|
}
|
|
|
|
// Ensure task IDs are normalized to T1, T2, T3...
|
|
extracted.tasks = normalizeTaskIds(extracted.tasks);
|
|
|
|
console.log(`Extracted: ${extracted.tasks.length} tasks`);
|
|
```
|
|
|
|
#### Extractor: Lite-Plan
|
|
|
|
```javascript
|
|
function extractFromLitePlan(folderPath) {
|
|
const planJson = Read(`${folderPath}/plan.json`);
|
|
const plan = JSON.parse(planJson);
|
|
|
|
return {
|
|
title: plan.summary?.split('.')[0]?.trim() || 'Untitled Plan',
|
|
description: plan.summary,
|
|
approach: plan.approach,
|
|
tasks: plan.tasks.map(t => ({
|
|
id: t.id,
|
|
title: t.title,
|
|
scope: t.scope || '',
|
|
action: t.action || 'Implement',
|
|
description: t.description || t.title,
|
|
modification_points: t.modification_points || [],
|
|
implementation: Array.isArray(t.implementation) ? t.implementation : [t.implementation || ''],
|
|
test: t.verification ? {
|
|
unit: t.verification.unit_tests,
|
|
integration: t.verification.integration_tests,
|
|
commands: t.verification.manual_checks
|
|
} : {},
|
|
acceptance: {
|
|
criteria: Array.isArray(t.acceptance) ? t.acceptance : [t.acceptance || ''],
|
|
verification: t.verification?.manual_checks || []
|
|
},
|
|
depends_on: t.depends_on || [],
|
|
priority: 3
|
|
})),
|
|
metadata: {
|
|
source_type: 'lite-plan',
|
|
source_path: folderPath,
|
|
complexity: plan.complexity,
|
|
estimated_time: plan.estimated_time,
|
|
exploration_angles: plan._metadata?.exploration_angles || [],
|
|
original_timestamp: plan._metadata?.timestamp
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
#### Extractor: Workflow Session
|
|
|
|
```javascript
|
|
function extractFromWorkflowSession(sessionPath) {
|
|
// Load session metadata
|
|
const sessionJson = Read(`${sessionPath}/workflow-session.json`);
|
|
const session = JSON.parse(sessionJson);
|
|
|
|
// Load IMPL_PLAN.md for approach (if exists)
|
|
let approach = '';
|
|
const implPlanPath = `${sessionPath}/IMPL_PLAN.md`;
|
|
const hasImplPlan = Bash(`test -f "${implPlanPath}" && echo "yes" || echo "no"`).trim() === 'yes';
|
|
if (hasImplPlan) {
|
|
const implPlan = Read(implPlanPath);
|
|
// Extract overview/approach section
|
|
const overviewMatch = implPlan.match(/##\s*(?:Overview|Approach|Strategy)\s*\n([\s\S]*?)(?=\n##|$)/i);
|
|
approach = overviewMatch?.[1]?.trim() || implPlan.split('\n').slice(0, 10).join('\n');
|
|
}
|
|
|
|
// Load all task JSONs from .task folder
|
|
const taskFiles = Glob({ pattern: `${sessionPath}/.task/IMPL-*.json` });
|
|
const tasks = taskFiles.map(f => {
|
|
const taskJson = Read(f);
|
|
const task = JSON.parse(taskJson);
|
|
return {
|
|
id: task.id?.replace(/^IMPL-0*/, 'T') || 'T1', // IMPL-001 → T1
|
|
title: task.title,
|
|
scope: task.scope || inferScopeFromTask(task),
|
|
action: capitalizeAction(task.type) || 'Implement',
|
|
description: task.description,
|
|
modification_points: task.implementation?.modification_points || [],
|
|
implementation: task.implementation?.steps || [],
|
|
test: task.implementation?.test || {},
|
|
acceptance: {
|
|
criteria: task.acceptance_criteria || [],
|
|
verification: task.verification_steps || []
|
|
},
|
|
commit: task.commit,
|
|
depends_on: (task.depends_on || []).map(d => d.replace(/^IMPL-0*/, 'T')),
|
|
priority: task.priority || 3
|
|
};
|
|
});
|
|
|
|
return {
|
|
title: session.name || session.description?.split('.')[0] || 'Workflow Session',
|
|
description: session.description || session.name,
|
|
approach: approach || session.description,
|
|
tasks: tasks,
|
|
metadata: {
|
|
source_type: 'workflow-session',
|
|
source_path: sessionPath,
|
|
session_id: session.id,
|
|
created_at: session.created_at
|
|
}
|
|
};
|
|
}
|
|
|
|
function inferScopeFromTask(task) {
|
|
if (task.implementation?.modification_points?.length) {
|
|
const files = task.implementation.modification_points.map(m => m.file);
|
|
// Find common directory prefix
|
|
const dirs = files.map(f => f.split('/').slice(0, -1).join('/'));
|
|
return [...new Set(dirs)][0] || '';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function capitalizeAction(type) {
|
|
if (!type) return 'Implement';
|
|
const map = { feature: 'Implement', bugfix: 'Fix', refactor: 'Refactor', test: 'Test', docs: 'Update' };
|
|
return map[type.toLowerCase()] || type.charAt(0).toUpperCase() + type.slice(1);
|
|
}
|
|
```
|
|
|
|
#### Extractor: Markdown (AI-Assisted via Gemini)
|
|
|
|
```javascript
|
|
async function extractFromMarkdownAI(filePath) {
|
|
const fileContent = Read(filePath);
|
|
|
|
// Use Gemini CLI for intelligent extraction
|
|
const cliPrompt = `PURPOSE: Extract implementation plan from markdown document for issue solution conversion. Must output ONLY valid JSON.
|
|
TASK: • Analyze document structure • Identify title/summary • Extract approach/strategy section • Parse tasks from any format (lists, tables, sections, code blocks) • Normalize each task to solution schema
|
|
MODE: analysis
|
|
CONTEXT: Document content provided below
|
|
EXPECTED: Valid JSON object with format:
|
|
{
|
|
"title": "extracted title",
|
|
"approach": "extracted approach/strategy",
|
|
"tasks": [
|
|
{
|
|
"id": "T1",
|
|
"title": "task title",
|
|
"scope": "module or feature area",
|
|
"action": "Implement|Update|Create|Fix|Refactor|Add|Delete|Configure|Test",
|
|
"description": "what to do",
|
|
"implementation": ["step 1", "step 2"],
|
|
"acceptance": ["criteria 1", "criteria 2"]
|
|
}
|
|
]
|
|
}
|
|
CONSTRAINTS: Output ONLY valid JSON - no markdown, no explanation | Action must be one of: Create, Update, Implement, Refactor, Add, Delete, Configure, Test, Fix | Tasks must have id, title, scope, action, implementation (array), acceptance (array)
|
|
|
|
DOCUMENT CONTENT:
|
|
${fileContent}`;
|
|
|
|
// Execute Gemini CLI
|
|
const result = Bash(`ccw cli -p '${cliPrompt.replace(/'/g, "'\\''")}' --tool gemini --mode analysis`, { timeout: 120000 });
|
|
|
|
// Parse JSON from result (may be wrapped in markdown code block)
|
|
let jsonText = result.trim();
|
|
const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
if (jsonMatch) {
|
|
jsonText = jsonMatch[1].trim();
|
|
}
|
|
|
|
try {
|
|
const extracted = JSON.parse(jsonText);
|
|
|
|
// Normalize tasks
|
|
const tasks = (extracted.tasks || []).map((t, i) => ({
|
|
id: t.id || `T${i + 1}`,
|
|
title: t.title || 'Untitled task',
|
|
scope: t.scope || '',
|
|
action: validateAction(t.action) || 'Implement',
|
|
description: t.description || t.title,
|
|
modification_points: t.modification_points || [],
|
|
implementation: Array.isArray(t.implementation) ? t.implementation : [t.implementation || ''],
|
|
test: t.test || {},
|
|
acceptance: {
|
|
criteria: Array.isArray(t.acceptance) ? t.acceptance : [t.acceptance || ''],
|
|
verification: t.verification || []
|
|
},
|
|
depends_on: t.depends_on || [],
|
|
priority: t.priority || 3
|
|
}));
|
|
|
|
return {
|
|
title: extracted.title || 'Extracted Plan',
|
|
description: extracted.summary || extracted.title,
|
|
approach: extracted.approach || '',
|
|
tasks: tasks,
|
|
metadata: {
|
|
source_type: 'markdown',
|
|
source_path: filePath,
|
|
extraction_method: 'gemini-ai'
|
|
}
|
|
};
|
|
} catch (e) {
|
|
// Provide more context for debugging
|
|
throw new Error(`E005: Failed to extract tasks from markdown. Gemini response was not valid JSON. Error: ${e.message}. Response preview: ${jsonText.substring(0, 200)}...`);
|
|
}
|
|
}
|
|
|
|
function validateAction(action) {
|
|
const validActions = ['Create', 'Update', 'Implement', 'Refactor', 'Add', 'Delete', 'Configure', 'Test', 'Fix'];
|
|
if (!action) return null;
|
|
const normalized = action.charAt(0).toUpperCase() + action.slice(1).toLowerCase();
|
|
return validActions.includes(normalized) ? normalized : null;
|
|
}
|
|
```
|
|
|
|
#### Extractor: JSON File
|
|
|
|
```javascript
|
|
function extractFromJsonFile(filePath) {
|
|
const content = Read(filePath);
|
|
const plan = JSON.parse(content);
|
|
|
|
// Detect if it's already solution format or plan format
|
|
if (plan.tasks && Array.isArray(plan.tasks)) {
|
|
// Map tasks to normalized format
|
|
const tasks = plan.tasks.map((t, i) => ({
|
|
id: t.id || `T${i + 1}`,
|
|
title: t.title,
|
|
scope: t.scope || '',
|
|
action: t.action || 'Implement',
|
|
description: t.description || t.title,
|
|
modification_points: t.modification_points || [],
|
|
implementation: Array.isArray(t.implementation) ? t.implementation : [t.implementation || ''],
|
|
test: t.test || t.verification || {},
|
|
acceptance: normalizeAcceptance(t.acceptance),
|
|
depends_on: t.depends_on || [],
|
|
priority: t.priority || 3
|
|
}));
|
|
|
|
return {
|
|
title: plan.summary?.split('.')[0] || plan.title || 'JSON Plan',
|
|
description: plan.summary || plan.description,
|
|
approach: plan.approach,
|
|
tasks: tasks,
|
|
metadata: {
|
|
source_type: 'json',
|
|
source_path: filePath,
|
|
complexity: plan.complexity,
|
|
original_metadata: plan._metadata
|
|
}
|
|
};
|
|
}
|
|
|
|
throw new Error('E002: JSON file does not contain valid plan structure (missing tasks array)');
|
|
}
|
|
|
|
function normalizeAcceptance(acceptance) {
|
|
if (!acceptance) return { criteria: [], verification: [] };
|
|
if (typeof acceptance === 'object' && acceptance.criteria) return acceptance;
|
|
if (Array.isArray(acceptance)) return { criteria: acceptance, verification: [] };
|
|
return { criteria: [String(acceptance)], verification: [] };
|
|
}
|
|
```
|
|
|
|
### Phase 3: Normalize Task IDs
|
|
|
|
```javascript
|
|
function normalizeTaskIds(tasks) {
|
|
return tasks.map((t, i) => ({
|
|
...t,
|
|
id: `T${i + 1}`,
|
|
// Also normalize depends_on references
|
|
depends_on: (t.depends_on || []).map(d => {
|
|
// Handle various ID formats: IMPL-001, T1, 1, etc.
|
|
const num = d.match(/\d+/)?.[0];
|
|
return num ? `T${parseInt(num)}` : d;
|
|
})
|
|
}));
|
|
}
|
|
```
|
|
|
|
### Phase 4: Resolve Issue (Create or Find)
|
|
|
|
```javascript
|
|
let issueId = flags.issue;
|
|
let existingSolution = null;
|
|
|
|
if (issueId) {
|
|
// Validate issue exists
|
|
let issueCheck;
|
|
try {
|
|
issueCheck = Bash(`ccw issue status ${issueId} --json 2>/dev/null`).trim();
|
|
if (!issueCheck || issueCheck === '') {
|
|
throw new Error('empty response');
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`E003: Issue not found: ${issueId}`);
|
|
}
|
|
|
|
const issue = JSON.parse(issueCheck);
|
|
|
|
// Check if issue already has bound solution
|
|
if (issue.bound_solution_id && !flags.supplement) {
|
|
throw new Error(`E004: Issue ${issueId} already has bound solution (${issue.bound_solution_id}). Use --supplement to add tasks.`);
|
|
}
|
|
|
|
// Load existing solution for supplement mode
|
|
if (flags.supplement && issue.bound_solution_id) {
|
|
try {
|
|
const solResult = Bash(`ccw issue solution ${issue.bound_solution_id} --json`).trim();
|
|
existingSolution = JSON.parse(solResult);
|
|
console.log(`Loaded existing solution with ${existingSolution.tasks.length} tasks`);
|
|
} catch (e) {
|
|
throw new Error(`Failed to load existing solution: ${e.message}`);
|
|
}
|
|
}
|
|
} else {
|
|
// Create new issue via ccw issue create (auto-generates correct ID)
|
|
// Smart extraction: title from content, priority from complexity
|
|
const title = extracted.title || 'Converted Plan';
|
|
const context = extracted.description || extracted.approach || title;
|
|
|
|
// Auto-determine priority based on complexity
|
|
const complexityMap = { high: 2, medium: 3, low: 4 };
|
|
const priority = complexityMap[extracted.metadata.complexity?.toLowerCase()] || 3;
|
|
|
|
try {
|
|
// Use heredoc to avoid shell escaping issues
|
|
const createResult = Bash(`ccw issue create << 'EOF'
|
|
{
|
|
"title": ${JSON.stringify(title)},
|
|
"context": ${JSON.stringify(context)},
|
|
"priority": ${priority},
|
|
"source": "converted"
|
|
}
|
|
EOF`).trim();
|
|
|
|
// Parse result to get created issue ID
|
|
const created = JSON.parse(createResult);
|
|
issueId = created.id;
|
|
console.log(`Created issue: ${issueId} (priority: ${priority})`);
|
|
} catch (e) {
|
|
throw new Error(`Failed to create issue: ${e.message}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Phase 5: Generate Solution
|
|
|
|
```javascript
|
|
// Generate solution ID
|
|
function generateSolutionId(issueId) {
|
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
let uid = '';
|
|
for (let i = 0; i < 4; i++) {
|
|
uid += chars[Math.floor(Math.random() * chars.length)];
|
|
}
|
|
return `SOL-${issueId}-${uid}`;
|
|
}
|
|
|
|
let solution;
|
|
const solutionId = generateSolutionId(issueId);
|
|
|
|
if (flags.supplement && existingSolution) {
|
|
// Supplement mode: merge with existing solution
|
|
const maxTaskId = Math.max(...existingSolution.tasks.map(t => parseInt(t.id.slice(1))));
|
|
|
|
const newTasks = extracted.tasks.map((t, i) => ({
|
|
...t,
|
|
id: `T${maxTaskId + i + 1}`
|
|
}));
|
|
|
|
solution = {
|
|
...existingSolution,
|
|
tasks: [...existingSolution.tasks, ...newTasks],
|
|
approach: existingSolution.approach + '\n\n[Supplementary] ' + (extracted.approach || ''),
|
|
updated_at: new Date().toISOString()
|
|
};
|
|
|
|
console.log(`Supplementing: ${existingSolution.tasks.length} existing + ${newTasks.length} new = ${solution.tasks.length} total tasks`);
|
|
} else {
|
|
// New solution
|
|
solution = {
|
|
id: solutionId,
|
|
description: extracted.description || extracted.title,
|
|
approach: extracted.approach,
|
|
tasks: extracted.tasks,
|
|
exploration_context: extracted.metadata.exploration_angles ? {
|
|
exploration_angles: extracted.metadata.exploration_angles
|
|
} : undefined,
|
|
analysis: {
|
|
risk: 'medium',
|
|
impact: 'medium',
|
|
complexity: extracted.metadata.complexity?.toLowerCase() || 'medium'
|
|
},
|
|
is_bound: false,
|
|
created_at: new Date().toISOString(),
|
|
_conversion_metadata: {
|
|
source_type: extracted.metadata.source_type,
|
|
source_path: extracted.metadata.source_path,
|
|
converted_at: new Date().toISOString()
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
### Phase 6: Confirm & Persist
|
|
|
|
```javascript
|
|
// Display preview
|
|
console.log(`
|
|
## Conversion Summary
|
|
|
|
**Issue**: ${issueId}
|
|
**Solution**: ${flags.supplement ? existingSolution.id : solutionId}
|
|
**Tasks**: ${solution.tasks.length}
|
|
**Mode**: ${flags.supplement ? 'Supplement' : 'New'}
|
|
|
|
### Tasks:
|
|
${solution.tasks.map(t => `- ${t.id}: ${t.title} [${t.action}]`).join('\n')}
|
|
`);
|
|
|
|
// Confirm if not auto mode
|
|
if (!flags.yes && !flags.y) {
|
|
const confirm = AskUserQuestion({
|
|
questions: [{
|
|
question: `Create solution for issue ${issueId} with ${solution.tasks.length} tasks?`,
|
|
header: 'Confirm',
|
|
multiSelect: false,
|
|
options: [
|
|
{ label: 'Yes, create solution', description: 'Create and bind solution' },
|
|
{ label: 'Cancel', description: 'Abort without changes' }
|
|
]
|
|
}]
|
|
});
|
|
|
|
if (!confirm.answers?.['Confirm']?.includes('Yes')) {
|
|
console.log('Cancelled.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Persist solution (following issue-plan-agent pattern)
|
|
Bash(`mkdir -p .workflow/issues/solutions`);
|
|
|
|
const solutionFile = `.workflow/issues/solutions/${issueId}.jsonl`;
|
|
|
|
if (flags.supplement) {
|
|
// Supplement mode: update existing solution line atomically
|
|
try {
|
|
const existingContent = Read(solutionFile);
|
|
const lines = existingContent.trim().split('\n').filter(l => l);
|
|
const updatedLines = lines.map(line => {
|
|
const sol = JSON.parse(line);
|
|
if (sol.id === existingSolution.id) {
|
|
return JSON.stringify(solution);
|
|
}
|
|
return line;
|
|
});
|
|
// Atomic write: write entire content at once
|
|
Write({ file_path: solutionFile, content: updatedLines.join('\n') + '\n' });
|
|
console.log(`✓ Updated solution: ${existingSolution.id}`);
|
|
} catch (e) {
|
|
throw new Error(`Failed to update solution: ${e.message}`);
|
|
}
|
|
|
|
// Note: No need to rebind - solution is already bound to issue
|
|
} else {
|
|
// New solution: append to JSONL file (following issue-plan-agent pattern)
|
|
try {
|
|
const solutionLine = JSON.stringify(solution);
|
|
|
|
// Read existing content, append new line, write atomically
|
|
const existing = Bash(`test -f "${solutionFile}" && cat "${solutionFile}" || echo ""`).trim();
|
|
const newContent = existing ? existing + '\n' + solutionLine + '\n' : solutionLine + '\n';
|
|
Write({ file_path: solutionFile, content: newContent });
|
|
|
|
console.log(`✓ Created solution: ${solutionId}`);
|
|
} catch (e) {
|
|
throw new Error(`Failed to write solution: ${e.message}`);
|
|
}
|
|
|
|
// Bind solution to issue
|
|
try {
|
|
Bash(`ccw issue bind ${issueId} ${solutionId}`);
|
|
console.log(`✓ Bound solution to issue`);
|
|
} catch (e) {
|
|
// Cleanup: remove solution file on bind failure
|
|
try {
|
|
Bash(`rm -f "${solutionFile}"`);
|
|
} catch (cleanupError) {
|
|
// Ignore cleanup errors
|
|
}
|
|
throw new Error(`Failed to bind solution: ${e.message}`);
|
|
}
|
|
|
|
// Update issue status to planned
|
|
try {
|
|
Bash(`ccw issue update ${issueId} --status planned`);
|
|
} catch (e) {
|
|
throw new Error(`Failed to update issue status: ${e.message}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Phase 7: Summary
|
|
|
|
```javascript
|
|
console.log(`
|
|
## Done
|
|
|
|
**Issue**: ${issueId}
|
|
**Solution**: ${flags.supplement ? existingSolution.id : solutionId}
|
|
**Tasks**: ${solution.tasks.length}
|
|
**Status**: planned
|
|
|
|
### Next Steps:
|
|
- \`/issue:queue\` → Form execution queue
|
|
- \`ccw issue status ${issueId}\` → View issue details
|
|
- \`ccw issue solution ${flags.supplement ? existingSolution.id : solutionId}\` → View solution
|
|
`);
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
| Error | Code | Resolution |
|
|
|-------|------|------------|
|
|
| Source not found | E001 | Check path exists |
|
|
| Invalid source format | E002 | Verify file contains valid plan structure |
|
|
| Issue not found | E003 | Check issue ID or omit --issue to create new |
|
|
| Solution already bound | E004 | Use --supplement to add tasks |
|
|
| AI extraction failed | E005 | Check markdown structure, try simpler format |
|
|
| No tasks extracted | E006 | Source must contain at least 1 task |
|
|
|
|
## Related Commands
|
|
|
|
- `/issue:plan` - Generate solutions from issue exploration
|
|
- `/issue:queue` - Form execution queue from bound solutions
|
|
- `/issue:execute` - Execute queue with DAG parallelism
|
|
- `ccw issue status <id>` - View issue details
|
|
- `ccw issue solution <id>` - View solution details
|