From 9b1655be9ba3dcf8131a5d2edcf703dd349c97fa Mon Sep 17 00:00:00 2001 From: catlog22 Date: Fri, 6 Feb 2026 14:23:13 +0800 Subject: [PATCH] Add phases for issue resolution: From Brainstorm and Form Execution Queue - Implement Phase 3: From Brainstorm to convert brainstorm session output into executable issues and solutions. - Implement Phase 4: Form Execution Queue to analyze bound solutions, resolve conflicts, and create an ordered execution queue. - Introduce new data structures for Issue and Solution schemas. - Enhance CLI commands for issue creation and queue management. - Add error handling and quality checklist for queue formation. --- .claude/skills/issue-resolve/SKILL.md | 277 +++++++ .../issue-resolve/phases/01-issue-plan.md | 292 +++++++ .../phases/02-convert-to-plan.md | 703 ++++++++++++++++ .../phases/03-from-brainstorm.md | 393 +++++++++ .../issue-resolve/phases/04-issue-queue.md | 389 +++++++++ ccw/docs-site/src/css/variables.css | 98 +-- .../coordinator/CoordinatorEmptyState.tsx | 192 ----- .../CoordinatorInputModal.test.tsx | 136 --- .../coordinator/CoordinatorInputModal.tsx | 441 ---------- .../coordinator/CoordinatorLogStream.tsx | 196 ----- .../coordinator/CoordinatorQuestionModal.tsx | 289 ------- .../coordinator/CoordinatorTaskCard.tsx | 137 ---- .../coordinator/CoordinatorTaskList.tsx | 140 ---- .../coordinator/CoordinatorTimeline.tsx | 116 --- .../components/coordinator/NodeConnector.tsx | 49 -- .../coordinator/NodeDetailsPanel.tsx | 254 ------ .../src/components/coordinator/README.md | 279 ------- .../components/coordinator/TimelineNode.tsx | 213 ----- .../src/components/coordinator/index.ts | 32 - .../widgets/RecentSessionsWidget.tsx | 36 +- .../src/components/layout/Sidebar.tsx | 1 - ccw/frontend/src/hooks/useWebSocket.ts | 59 -- ccw/frontend/src/locales/en/coordinator.json | 141 ---- ccw/frontend/src/locales/en/index.ts | 2 - ccw/frontend/src/locales/zh/coordinator.json | 157 ---- ccw/frontend/src/locales/zh/index.ts | 2 - .../src/pages/coordinator/CoordinatorPage.tsx | 354 -------- ccw/frontend/src/pages/coordinator/index.ts | 6 - ccw/frontend/src/pages/index.ts | 1 - .../pages/orchestrator/ExecutionMonitor.tsx | 394 ++++----- .../src/pages/orchestrator/FlowCanvas.tsx | 23 +- .../src/pages/orchestrator/FlowToolbar.tsx | 63 +- .../src/pages/orchestrator/NodeLibrary.tsx | 251 ++++-- .../pages/orchestrator/OrchestratorPage.tsx | 14 +- .../src/pages/orchestrator/PropertyPanel.tsx | 289 +++++-- ccw/frontend/src/router.tsx | 6 - ccw/frontend/src/stores/coordinatorStore.ts | 772 ------------------ ccw/frontend/src/stores/executionStore.ts | 8 +- ccw/frontend/src/stores/flowStore.ts | 79 +- ccw/frontend/src/stores/index.ts | 26 +- ccw/frontend/src/types/execution.ts | 4 +- ccw/frontend/src/types/flow.ts | 175 +--- 42 files changed, 2845 insertions(+), 4644 deletions(-) create mode 100644 .claude/skills/issue-resolve/SKILL.md create mode 100644 .claude/skills/issue-resolve/phases/01-issue-plan.md create mode 100644 .claude/skills/issue-resolve/phases/02-convert-to-plan.md create mode 100644 .claude/skills/issue-resolve/phases/03-from-brainstorm.md create mode 100644 .claude/skills/issue-resolve/phases/04-issue-queue.md delete mode 100644 ccw/frontend/src/components/coordinator/CoordinatorEmptyState.tsx delete mode 100644 ccw/frontend/src/components/coordinator/CoordinatorInputModal.test.tsx delete mode 100644 ccw/frontend/src/components/coordinator/CoordinatorInputModal.tsx delete mode 100644 ccw/frontend/src/components/coordinator/CoordinatorLogStream.tsx delete mode 100644 ccw/frontend/src/components/coordinator/CoordinatorQuestionModal.tsx delete mode 100644 ccw/frontend/src/components/coordinator/CoordinatorTaskCard.tsx delete mode 100644 ccw/frontend/src/components/coordinator/CoordinatorTaskList.tsx delete mode 100644 ccw/frontend/src/components/coordinator/CoordinatorTimeline.tsx delete mode 100644 ccw/frontend/src/components/coordinator/NodeConnector.tsx delete mode 100644 ccw/frontend/src/components/coordinator/NodeDetailsPanel.tsx delete mode 100644 ccw/frontend/src/components/coordinator/README.md delete mode 100644 ccw/frontend/src/components/coordinator/TimelineNode.tsx delete mode 100644 ccw/frontend/src/components/coordinator/index.ts delete mode 100644 ccw/frontend/src/locales/en/coordinator.json delete mode 100644 ccw/frontend/src/locales/zh/coordinator.json delete mode 100644 ccw/frontend/src/pages/coordinator/CoordinatorPage.tsx delete mode 100644 ccw/frontend/src/pages/coordinator/index.ts delete mode 100644 ccw/frontend/src/stores/coordinatorStore.ts diff --git a/.claude/skills/issue-resolve/SKILL.md b/.claude/skills/issue-resolve/SKILL.md new file mode 100644 index 00000000..2bf7f98d --- /dev/null +++ b/.claude/skills/issue-resolve/SKILL.md @@ -0,0 +1,277 @@ +--- +name: issue-resolve +description: Unified issue resolution pipeline with source selection. Plan issues via AI exploration, convert from artifacts, import from brainstorm sessions, or form execution queues. Triggers on "issue:plan", "issue:queue", "issue:convert-to-plan", "issue:from-brainstorm", "resolve issue", "plan issue", "queue issues", "convert plan to issue". +allowed-tools: Task, AskUserQuestion, TodoWrite, Read, Write, Edit, Bash, Glob, Grep, Skill +--- + +# Issue Resolve + +Unified issue resolution pipeline that orchestrates solution creation from multiple sources and queue formation for execution. + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Issue Resolve Orchestrator (SKILL.md) │ +│ → Source selection → Route to phase → Execute → Summary │ +└───────────────┬─────────────────────────────────────────────────┘ + │ + ├─ AskUserQuestion: Select issue source + │ + ┌───────────┼───────────┬───────────┬───────────┐ + ↓ ↓ ↓ ↓ │ +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ Phase 1 │ │ Phase 2 │ │ Phase 3 │ │ Phase 4 │ │ +│ Explore │ │ Convert │ │ From │ │ Form │ │ +│ & Plan │ │Artifact │ │Brainstorm│ │ Queue │ │ +└─────────┘ └─────────┘ └─────────┘ └─────────┘ │ + ↓ ↓ ↓ ↓ │ + Solutions Solutions Issue+Sol Exec Queue │ + (bound) (bound) (bound) (ordered) │ + │ + ┌────────────────────────────────┘ + ↓ + /issue:execute +``` + +## Key Design Principles + +1. **Source-Driven Routing**: AskUserQuestion selects workflow, then load single phase +2. **Progressive Phase Loading**: Only read the selected phase document +3. **CLI-First Data Access**: All issue/solution CRUD via `ccw issue` CLI commands +4. **Auto Mode Support**: `-y` flag skips source selection (defaults to Explore & Plan) + +## Auto Mode + +When `--yes` or `-y`: Skip source selection, use Explore & Plan for issue IDs, or auto-detect source type for paths. + +## Usage + +``` +Skill(skill="issue-resolve", args="") +Skill(skill="issue-resolve", args="[FLAGS] \"\"") + +# Flags +-y, --yes Skip all confirmations (auto mode) +--source Pre-select source: plan|convert|brainstorm|queue +--batch-size Max issues per agent batch (plan mode, default: 3) +--issue Bind to existing issue (convert mode) +--supplement Add tasks to existing solution (convert mode) +--queues Number of parallel queues (queue mode, default: 1) + +# Examples +Skill(skill="issue-resolve", args="GH-123,GH-124") # Explore & plan issues +Skill(skill="issue-resolve", args="--source plan --all-pending") # Plan all pending issues +Skill(skill="issue-resolve", args="--source convert \".workflow/.lite-plan/my-plan\"") # Convert artifact +Skill(skill="issue-resolve", args="--source brainstorm SESSION=\"BS-rate-limiting\"") # From brainstorm +Skill(skill="issue-resolve", args="--source queue") # Form execution queue +Skill(skill="issue-resolve", args="-y GH-123") # Auto mode, plan single issue +``` + +## Execution Flow + +``` +Input Parsing: + └─ Parse flags (--source, -y, --issue, etc.) and positional args + +Source Selection: + ├─ --source flag provided → Route directly + ├─ Auto-detect from input: + │ ├─ Issue IDs (GH-xxx, ISS-xxx) → Explore & Plan + │ ├─ SESSION="..." → From Brainstorm + │ ├─ File/folder path → Convert from Artifact + │ └─ No input or --all-pending → Explore & Plan (all pending) + └─ Otherwise → AskUserQuestion to select source + +Phase Execution (load one phase): + ├─ Phase 1: Explore & Plan → phases/01-issue-plan.md + ├─ Phase 2: Convert Artifact → phases/02-convert-to-plan.md + ├─ Phase 3: From Brainstorm → phases/03-from-brainstorm.md + └─ Phase 4: Form Queue → phases/04-issue-queue.md + +Post-Phase: + └─ Summary + Next steps recommendation +``` + +### Phase Reference Documents + +| Phase | Document | Load When | Purpose | +|-------|----------|-----------|---------| +| Phase 1 | [phases/01-issue-plan.md](phases/01-issue-plan.md) | Source = Explore & Plan | Batch plan issues via issue-plan-agent | +| Phase 2 | [phases/02-convert-to-plan.md](phases/02-convert-to-plan.md) | Source = Convert Artifact | Convert lite-plan/session/markdown to solutions | +| Phase 3 | [phases/03-from-brainstorm.md](phases/03-from-brainstorm.md) | Source = From Brainstorm | Convert brainstorm ideas to issue + solution | +| Phase 4 | [phases/04-issue-queue.md](phases/04-issue-queue.md) | Source = Form Queue | Order bound solutions into execution queue | + +## Core Rules + +1. **Source Selection First**: Always determine source before loading any phase +2. **Single Phase Load**: Only read the selected phase document, never load all phases +3. **CLI Data Access**: Use `ccw issue` CLI for all issue/solution operations, NEVER read files directly +4. **Content Preservation**: Each phase contains complete execution logic from original commands +5. **Auto-Detect Input**: Smart input parsing reduces need for explicit --source flag + +## Input Processing + +### Auto-Detection Logic + +```javascript +function detectSource(input, flags) { + // 1. Explicit --source flag + if (flags.source) return flags.source; + + // 2. Auto-detect from input content + const trimmed = input.trim(); + + // Issue IDs pattern (GH-xxx, ISS-xxx, comma-separated) + if (trimmed.match(/^[A-Z]+-\d+/i) || trimmed.includes(',')) { + return 'plan'; + } + + // --all-pending or empty input → plan all pending + if (flags.allPending || trimmed === '') { + return 'plan'; + } + + // SESSION="..." pattern → brainstorm + if (trimmed.includes('SESSION=')) { + return 'brainstorm'; + } + + // File/folder path → convert + if (trimmed.match(/\.(md|json)$/) || trimmed.includes('.workflow/')) { + return 'convert'; + } + + // Cannot auto-detect → ask user + return null; +} +``` + +### Source Selection (AskUserQuestion) + +```javascript +// When source cannot be auto-detected +const answer = AskUserQuestion({ + questions: [{ + question: "How would you like to create/manage issue solutions?", + header: "Source", + multiSelect: false, + options: [ + { + label: "Explore & Plan (Recommended)", + description: "AI explores codebase and generates solutions for issues" + }, + { + label: "Convert from Artifact", + description: "Convert existing lite-plan, workflow session, or markdown to solution" + }, + { + label: "From Brainstorm", + description: "Convert brainstorm session ideas into issue with solution" + }, + { + label: "Form Execution Queue", + description: "Order bound solutions into execution queue for /issue:execute" + } + ] + }] +}); + +// Route based on selection +const sourceMap = { + "Explore & Plan": "plan", + "Convert from Artifact": "convert", + "From Brainstorm": "brainstorm", + "Form Execution Queue": "queue" +}; +``` + +## Data Flow + +``` +User Input (issue IDs / artifact path / session ID / flags) + ↓ +[Parse Flags + Auto-Detect Source] + ↓ +[Source Selection] ← AskUserQuestion (if needed) + ↓ +[Read Selected Phase Document] + ↓ +[Execute Phase Logic] + ↓ +[Summary + Next Steps] + ├─ After Plan/Convert/Brainstorm → Suggest /issue:queue or /issue:execute + └─ After Queue → Suggest /issue:execute +``` + +## TodoWrite Pattern + +```json +[ + {"content": "Select issue source", "status": "completed"}, + {"content": "Execute: [selected phase name]", "status": "in_progress"}, + {"content": "Summary & next steps", "status": "pending"} +] +``` + +Phase-specific sub-tasks are attached when the phase executes (see individual phase docs for details). + +## Core Guidelines + +**Data Access Principle**: Issues and solutions files can grow very large. To avoid context overflow: + +| Operation | Correct | Incorrect | +|-----------|---------|-----------| +| List issues (brief) | `ccw issue list --status pending --brief` | `Read('issues.jsonl')` | +| Read issue details | `ccw issue status --json` | `Read('issues.jsonl')` | +| Update status | `ccw issue update --status ...` | Direct file edit | +| Bind solution | `ccw issue bind ` | Direct file edit | +| Batch solutions | `ccw issue solutions --status planned --brief` | Loop individual queries | + +**Output Options**: +- `--brief`: JSON with minimal fields (orchestrator use) +- `--json`: Full JSON (agent use only) + +**ALWAYS** use CLI commands for CRUD operations. **NEVER** read entire `issues.jsonl` or `solutions/*.jsonl` directly. + +## Error Handling + +| Error | Resolution | +|-------|------------| +| No source detected | Show AskUserQuestion with all 4 options | +| Invalid source type | Show available sources, re-prompt | +| Phase execution fails | Report error, suggest manual intervention | +| No pending issues (plan) | Suggest `/issue:new` to create issues first | +| No bound solutions (queue) | Suggest running plan/convert/brainstorm first | + +## Post-Phase Next Steps + +After successful phase execution, recommend next action: + +```javascript +// After Plan/Convert/Brainstorm (solutions created) +AskUserQuestion({ + questions: [{ + question: "Solutions created. What next?", + header: "Next", + multiSelect: false, + options: [ + { label: "Form Queue", description: "Order solutions for execution (/issue:queue)" }, + { label: "Plan More Issues", description: "Continue creating solutions" }, + { label: "View Issues", description: "Review issue details" }, + { label: "Done", description: "Exit workflow" } + ] + }] +}); + +// After Queue (queue formed) +// → Suggest /issue:execute directly +``` + +## Related Skills & Commands + +- `issue-manage` - Interactive issue CRUD operations +- `/issue:new` - Create structured issue from GitHub or text +- `/issue:execute` - Execute queue with DAG-based parallel orchestration +- `ccw issue list` - List all issues +- `ccw issue status ` - View issue details diff --git a/.claude/skills/issue-resolve/phases/01-issue-plan.md b/.claude/skills/issue-resolve/phases/01-issue-plan.md new file mode 100644 index 00000000..b7e21a1d --- /dev/null +++ b/.claude/skills/issue-resolve/phases/01-issue-plan.md @@ -0,0 +1,292 @@ +# Phase 1: Explore & Plan + +> 来源: `commands/issue/plan.md` + +## Overview + +Batch plan issue resolution using **issue-plan-agent** that combines exploration and planning into a single closed-loop workflow. + +**Behavior:** +- Single solution per issue → auto-bind +- Multiple solutions → return for user selection +- Agent handles file generation + +## Prerequisites + +- Issue IDs provided (comma-separated) or `--all-pending` flag +- `ccw issue` CLI available +- `.workflow/issues/` directory exists or will be created + +## Auto Mode + +When `--yes` or `-y`: Auto-bind solutions without confirmation, use recommended settings. + +## Core Guidelines + +**⚠️ Data Access Principle**: Issues and solutions files can grow very large. To avoid context overflow: + +| Operation | Correct | Incorrect | +|-----------|---------|-----------| +| List issues (brief) | `ccw issue list --status pending --brief` | `Read('issues.jsonl')` | +| Read issue details | `ccw issue status --json` | `Read('issues.jsonl')` | +| Update status | `ccw issue update --status ...` | Direct file edit | +| Bind solution | `ccw issue bind ` | Direct file edit | + +**Output Options**: +- `--brief`: JSON with minimal fields (id, title, status, priority, tags) +- `--json`: Full JSON (agent use only) + +**Orchestration vs Execution**: +- **Command (orchestrator)**: Use `--brief` for minimal context +- **Agent (executor)**: Fetch full details → `ccw issue status --json` + +**ALWAYS** use CLI commands for CRUD operations. **NEVER** read entire `issues.jsonl` or `solutions/*.jsonl` directly. + +## Execution Steps + +### Step 1.1: Issue Loading (Brief Info Only) + +```javascript +const batchSize = flags.batchSize || 3; +let issues = []; // {id, title, tags} - brief info for grouping only + +// Default to --all-pending if no input provided +const useAllPending = flags.allPending || !userInput || userInput.trim() === ''; + +if (useAllPending) { + // Get pending issues with brief metadata via CLI + const result = Bash(`ccw issue list --status pending,registered --json`).trim(); + const parsed = result ? JSON.parse(result) : []; + issues = parsed.map(i => ({ id: i.id, title: i.title || '', tags: i.tags || [] })); + + if (issues.length === 0) { + console.log('No pending issues found.'); + return; + } + console.log(`Found ${issues.length} pending issues`); +} else { + // Parse comma-separated issue IDs, fetch brief metadata + const ids = userInput.includes(',') + ? userInput.split(',').map(s => s.trim()) + : [userInput.trim()]; + + for (const id of ids) { + Bash(`ccw issue init ${id} --title "Issue ${id}" 2>/dev/null || true`); + const info = Bash(`ccw issue status ${id} --json`).trim(); + const parsed = info ? JSON.parse(info) : {}; + issues.push({ id, title: parsed.title || '', tags: parsed.tags || [] }); + } +} +// Note: Agent fetches full issue content via `ccw issue status --json` + +// Intelligent grouping: Analyze issues by title/tags, group semantically similar ones +// Strategy: Same module/component, related bugs, feature clusters +// Constraint: Max ${batchSize} issues per batch + +console.log(`Processing ${issues.length} issues in ${batches.length} batch(es)`); + +TodoWrite({ + todos: batches.map((_, i) => ({ + content: `Plan batch ${i+1}`, + status: 'pending', + activeForm: `Planning batch ${i+1}` + })) +}); +``` + +### Step 1.2: Unified Explore + Plan (issue-plan-agent) - PARALLEL + +```javascript +Bash(`mkdir -p .workflow/issues/solutions`); +const pendingSelections = []; // Collect multi-solution issues for user selection +const agentResults = []; // Collect all agent results for conflict aggregation + +// Build prompts for all batches +const agentTasks = batches.map((batch, batchIndex) => { + const issueList = batch.map(i => `- ${i.id}: ${i.title}${i.tags.length ? ` [${i.tags.join(', ')}]` : ''}`).join('\n'); + const batchIds = batch.map(i => i.id); + + const issuePrompt = ` +## Plan Issues + +**Issues** (grouped by similarity): +${issueList} + +**Project Root**: ${process.cwd()} + +### Project Context (MANDATORY) +1. Read: .workflow/project-tech.json (technology stack, architecture) +2. Read: .workflow/project-guidelines.json (constraints and conventions) + +### Workflow +1. Fetch issue details: ccw issue status --json +2. **Analyze failure history** (if issue.feedback exists): + - Extract failure details from issue.feedback (type='failure', stage='execute') + - Parse error_type, message, task_id, solution_id from content JSON + - Identify failure patterns: repeated errors, root causes, blockers + - **Constraint**: Avoid repeating failed approaches +3. Load project context files +4. Explore codebase (ACE semantic search) +5. Plan solution with tasks (schema: solution-schema.json) + - **If previous solution failed**: Reference failure analysis in solution.approach + - Add explicit verification steps to prevent same failure mode +6. **If github_url exists**: Add final task to comment on GitHub issue +7. Write solution to: .workflow/issues/solutions/{issue-id}.jsonl +8. **CRITICAL - Binding Decision**: + - Single solution → **MUST execute**: ccw issue bind + - Multiple solutions → Return pending_selection only (no bind) + +### Failure-Aware Planning Rules +- **Extract failure patterns**: Parse issue.feedback where type='failure' and stage='execute' +- **Identify root causes**: Analyze error_type (test_failure, compilation, timeout, etc.) +- **Design alternative approach**: Create solution that addresses root cause +- **Add prevention steps**: Include explicit verification to catch same error earlier +- **Document lessons**: Reference previous failures in solution.approach + +### Rules +- Solution ID format: SOL-{issue-id}-{uid} (uid: 4 random alphanumeric chars, e.g., a7x9) +- 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 +{"bound":[{"issue_id":"...","solution_id":"...","task_count":N}],"pending_selection":[{"issue_id":"...","solutions":[{"id":"...","description":"...","task_count":N}]}]} +`; + + return { batchIndex, batchIds, issuePrompt, batch }; +}); + +// Launch agents in parallel (max 10 concurrent) +const MAX_PARALLEL = 10; +for (let i = 0; i < agentTasks.length; i += MAX_PARALLEL) { + const chunk = agentTasks.slice(i, i + MAX_PARALLEL); + const taskIds = []; + + // Launch chunk in parallel + for (const { batchIndex, batchIds, issuePrompt, batch } of chunk) { + updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress'); + const taskId = Task( + subagent_type="issue-plan-agent", + run_in_background=true, + description=`Explore & plan ${batch.length} issues: ${batchIds.join(', ')}`, + prompt=issuePrompt + ); + taskIds.push({ taskId, batchIndex }); + } + + console.log(`Launched ${taskIds.length} agents (batch ${i/MAX_PARALLEL + 1}/${Math.ceil(agentTasks.length/MAX_PARALLEL)})...`); + + // Collect results from this chunk + for (const { taskId, batchIndex } of taskIds) { + const result = TaskOutput(task_id=taskId, block=true); + + // Extract JSON from potential markdown code blocks (agent may wrap in ```json...```) + const jsonText = extractJsonFromMarkdown(result); + let summary; + try { + summary = JSON.parse(jsonText); + } catch (e) { + console.log(`⚠ Batch ${batchIndex + 1}: Failed to parse agent result, skipping`); + updateTodo(`Plan batch ${batchIndex + 1}`, 'completed'); + continue; + } + agentResults.push(summary); // Store for Phase 3 conflict aggregation + + // Verify binding for bound issues (agent should have executed bind) + for (const item of summary.bound || []) { + const status = JSON.parse(Bash(`ccw issue status ${item.issue_id} --json`).trim()); + if (status.bound_solution_id === item.solution_id) { + console.log(`✓ ${item.issue_id}: ${item.solution_id} (${item.task_count} tasks)`); + } else { + // Fallback: agent failed to bind, execute here + Bash(`ccw issue bind ${item.issue_id} ${item.solution_id}`); + console.log(`✓ ${item.issue_id}: ${item.solution_id} (${item.task_count} tasks) [recovered]`); + } + } + // Collect pending selections for Phase 3 + for (const pending of summary.pending_selection || []) { + pendingSelections.push(pending); + } + updateTodo(`Plan batch ${batchIndex + 1}`, 'completed'); + } +} +``` + +### Step 1.3: Solution Selection (if pending) + +```javascript +// Handle multi-solution issues +for (const pending of pendingSelections) { + if (pending.solutions.length === 0) continue; + + const options = pending.solutions.slice(0, 4).map(sol => ({ + label: `${sol.id} (${sol.task_count} tasks)`, + description: sol.description || sol.approach || 'No description' + })); + + const answer = AskUserQuestion({ + questions: [{ + question: `Issue ${pending.issue_id}: which solution to bind?`, + header: pending.issue_id, + options: options, + multiSelect: false + }] + }); + + const selected = answer[Object.keys(answer)[0]]; + if (!selected || selected === 'Other') continue; + + const solId = selected.split(' ')[0]; + Bash(`ccw issue bind ${pending.issue_id} ${solId}`); + console.log(`✓ ${pending.issue_id}: ${solId} bound`); +} +``` + +### Step 1.4: Summary + +```javascript +// Count planned issues via CLI +const planned = JSON.parse(Bash(`ccw issue list --status planned --brief`) || '[]'); +const plannedCount = planned.length; + +console.log(` +## Done: ${issues.length} issues → ${plannedCount} planned + +Next: \`/issue:queue\` → \`/issue:execute\` +`); +``` + +## Error Handling + +| Error | Resolution | +|-------|------------| +| Issue not found | Auto-create in issues.jsonl | +| ACE search fails | Agent falls back to ripgrep | +| No solutions generated | Display error, suggest manual planning | +| User cancels selection | Skip issue, continue with others | +| File conflicts | Agent detects and suggests resolution order | + +## Bash Compatibility + +**Avoid**: `$(cmd)`, `$var`, `for` loops — will be escaped incorrectly + +**Use**: Simple commands + `&&` chains, quote comma params `"pending,registered"` + +## Quality Checklist + +Before completing, verify: + +- [ ] All input issues have solutions in `solutions/{issue-id}.jsonl` +- [ ] Single solution issues are auto-bound (`bound_solution_id` set) +- [ ] Multi-solution issues returned in `pending_selection` for user choice +- [ ] Each solution has executable tasks with `modification_points` +- [ ] Task acceptance criteria are quantified (not vague) +- [ ] Conflicts detected and reported (if multiple issues touch same files) +- [ ] Issue status updated to `planned` after binding + +## Post-Phase Update + +After plan completion: +- All processed issues should have `status: planned` and `bound_solution_id` set +- Report: total issues processed, solutions bound, pending selections resolved +- Recommend next step: Form execution queue via Phase 4 or `Skill(skill="issue-resolve", args="--source queue")` diff --git a/.claude/skills/issue-resolve/phases/02-convert-to-plan.md b/.claude/skills/issue-resolve/phases/02-convert-to-plan.md new file mode 100644 index 00000000..50c10727 --- /dev/null +++ b/.claude/skills/issue-resolve/phases/02-convert-to-plan.md @@ -0,0 +1,703 @@ +# Phase 2: Convert from Artifact + +> 来源: `commands/issue/convert-to-plan.md` + +## 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 + +## Prerequisites + +- Source artifact path or WFS-xxx ID provided +- `ccw issue` CLI available +- `.workflow/issues/` directory exists or will be created + +## Auto Mode + +When `--yes` or `-y`: Skip confirmation, auto-create issue and bind solution. + +## Command Options + +| Option | Description | Default | +|--------|-------------|---------| +| `` | Planning artifact path or WFS-xxx ID | Required | +| `--issue ` | 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 --json` | Read issues.jsonl directly | +| Create issue | `ccw issue init --title "..."` | Write to issues.jsonl | +| Bind solution | `ccw issue bind ` | Edit issues.jsonl | +| List solutions | `ccw issue solutions --issue --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'; +``` + +## Execution Steps + +### Step 2.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}`); +``` + +### Step 2.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: [] }; +} +``` + +### Step 2.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; + }) + })); +} +``` + +### Step 2.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}`); + } +} +``` + +### Step 2.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() + } + }; +} +``` + +### Step 2.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}`); + } +} +``` + +### Step 2.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 | + +## Post-Phase Update + +After conversion completion: +- Issue created/updated with `status: planned` and `bound_solution_id` set +- Solution persisted in `.workflow/issues/solutions/{issue-id}.jsonl` +- Report: issue ID, solution ID, task count, mode (new/supplement) +- Recommend next step: Form execution queue via Phase 4 or `Skill(skill="issue-resolve", args="--source queue")` diff --git a/.claude/skills/issue-resolve/phases/03-from-brainstorm.md b/.claude/skills/issue-resolve/phases/03-from-brainstorm.md new file mode 100644 index 00000000..2e6c02f0 --- /dev/null +++ b/.claude/skills/issue-resolve/phases/03-from-brainstorm.md @@ -0,0 +1,393 @@ +# Phase 3: From Brainstorm + +> 来源: `commands/issue/from-brainstorm.md` + +## Overview + +Bridge command that converts **brainstorm-with-file** session output into executable **issue + solution** for parallel-dev-cycle consumption. + +**Core workflow**: Load Session → Select Idea → Convert to Issue → Generate Solution → Bind & Ready + +**Input sources**: +- **synthesis.json** - Main brainstorm results with top_ideas +- **perspectives.json** - Multi-CLI perspectives (creative/pragmatic/systematic) +- **.brainstorming/** - Synthesis artifacts (clarifications, enhancements from role analyses) + +**Output**: +- **Issue** (ISS-YYYYMMDD-NNN) - Full context with clarifications +- **Solution** (SOL-{issue-id}-{uid}) - Structured tasks for parallel-dev-cycle + +## Prerequisites + +- Brainstorm session ID or path (e.g., `SESSION="BS-rate-limiting-2025-01-28"`) +- `synthesis.json` must exist in session directory +- `ccw issue` CLI available + +## Auto Mode + +When `--yes` or `-y`: Auto-select highest-scored idea, skip confirmations, create issue directly. + +## Arguments + +| Argument | Required | Type | Default | Description | +|----------|----------|------|---------|-------------| +| SESSION | Yes | String | - | Session ID or path to `.workflow/.brainstorm/BS-xxx` | +| --idea | No | Integer | - | Pre-select idea by index (0-based) | +| --auto | No | Flag | false | Auto-select highest-scored idea | +| -y, --yes | No | Flag | false | Skip all confirmations | + +## Data Structures + +### Issue Schema (Output) + +```typescript +interface Issue { + id: string; // ISS-YYYYMMDD-NNN + title: string; // From idea.title + status: 'planned'; // Auto-set after solution binding + priority: number; // 1-5 (derived from idea.score) + context: string; // Full description with clarifications + source: 'brainstorm'; + labels: string[]; // ['brainstorm', perspective, feasibility] + + // Structured fields + expected_behavior: string; // From key_strengths + actual_behavior: string; // From main_challenges + affected_components: string[]; // Extracted from description + + _brainstorm_metadata: { + session_id: string; + idea_score: number; + novelty: number; + feasibility: string; + clarifications_count: number; + }; +} +``` + +### Solution Schema (Output) + +```typescript +interface Solution { + id: string; // SOL-{issue-id}-{4-char-uid} + description: string; // idea.title + approach: string; // idea.description + tasks: Task[]; // Generated from idea.next_steps + + analysis: { + risk: 'low' | 'medium' | 'high'; + impact: 'low' | 'medium' | 'high'; + complexity: 'low' | 'medium' | 'high'; + }; + + is_bound: boolean; // true + created_at: string; + bound_at: string; +} + +interface Task { + id: string; // T1, T2, T3... + title: string; // Actionable task name + scope: string; // design|implementation|testing|documentation + action: string; // Implement|Design|Research|Test|Document + description: string; + + implementation: string[]; // Step-by-step guide + acceptance: { + criteria: string[]; // What defines success + verification: string[]; // How to verify + }; + + priority: number; // 1-5 + depends_on: string[]; // Task dependencies +} +``` + +## Execution Steps + +### Step 3.1: Session Loading + +``` +Phase 1: Session Loading + ├─ Validate session path + ├─ Load synthesis.json (required) + ├─ Load perspectives.json (optional - multi-CLI insights) + ├─ Load .brainstorming/** (optional - synthesis artifacts) + └─ Validate top_ideas array exists +``` + +### Step 3.2: Idea Selection + +``` +Phase 2: Idea Selection + ├─ Auto mode: Select highest scored idea + ├─ Pre-selected: Use --idea=N index + └─ Interactive: Display table, ask user to select +``` + +### Step 3.3: Enrich Issue Context + +``` +Phase 3: Enrich Issue Context + ├─ Base: idea.description + key_strengths + main_challenges + ├─ Add: Relevant clarifications (Requirements/Architecture/Feasibility) + ├─ Add: Multi-perspective insights (creative/pragmatic/systematic) + └─ Add: Session metadata (session_id, completion date, clarification count) +``` + +### Step 3.4: Create Issue + +``` +Phase 4: Create Issue + ├─ Generate issue data with enriched context + ├─ Calculate priority from idea.score (0-10 → 1-5) + ├─ Create via: ccw issue create (heredoc for JSON) + └─ Returns: ISS-YYYYMMDD-NNN +``` + +### Step 3.5: Generate Solution Tasks + +``` +Phase 5: Generate Solution Tasks + ├─ T1: Research & Validate (if main_challenges exist) + ├─ T2: Design & Specification (if key_strengths exist) + ├─ T3+: Implementation tasks (from idea.next_steps) + └─ Each task includes: implementation steps + acceptance criteria +``` + +### Step 3.6: Bind Solution + +``` +Phase 6: Bind Solution + ├─ Write solution to .workflow/issues/solutions/{issue-id}.jsonl + ├─ Bind via: ccw issue bind {issue-id} {solution-id} + ├─ Update issue status to 'planned' + └─ Returns: SOL-{issue-id}-{uid} +``` + +### Step 3.7: Next Steps + +``` +Phase 7: Next Steps + └─ Offer: Form queue | Convert another idea | View details | Done +``` + +## Context Enrichment Logic + +### Base Context (Always Included) + +- **Description**: `idea.description` +- **Why This Idea**: `idea.key_strengths[]` +- **Challenges to Address**: `idea.main_challenges[]` +- **Implementation Steps**: `idea.next_steps[]` + +### Enhanced Context (If Available) + +**From Synthesis Artifacts** (`.brainstorming/*/analysis*.md`): +- Extract clarifications matching categories: Requirements, Architecture, Feasibility +- Format: `**{Category}** ({role}): {question} → {answer}` +- Limit: Top 3 most relevant + +**From Perspectives** (`perspectives.json`): +- **Creative**: First insight from `perspectives.creative.insights[0]` +- **Pragmatic**: First blocker from `perspectives.pragmatic.blockers[0]` +- **Systematic**: First pattern from `perspectives.systematic.patterns[0]` + +**Session Metadata**: +- Session ID, Topic, Completion Date +- Clarifications count (if synthesis artifacts loaded) + +## Task Generation Strategy + +### Task 1: Research & Validation +**Trigger**: `idea.main_challenges.length > 0` +- **Title**: "Research & Validate Approach" +- **Scope**: design +- **Action**: Research +- **Implementation**: Investigate blockers, review similar implementations, validate with team +- **Acceptance**: Blockers documented, feasibility assessed, approach validated + +### Task 2: Design & Specification +**Trigger**: `idea.key_strengths.length > 0` +- **Title**: "Design & Create Specification" +- **Scope**: design +- **Action**: Design +- **Implementation**: Create design doc, define success criteria, plan phases +- **Acceptance**: Design complete, metrics defined, plan outlined + +### Task 3+: Implementation Tasks +**Trigger**: `idea.next_steps[]` +- **Title**: From `next_steps[i]` (max 60 chars) +- **Scope**: Inferred from keywords (test→testing, api→backend, ui→frontend) +- **Action**: Detected from verbs (implement, create, update, fix, test, document) +- **Implementation**: Execute step + follow design + write tests +- **Acceptance**: Step implemented + tests passing + code reviewed + +### Fallback Task +**Trigger**: No tasks generated from above +- **Title**: `idea.title` +- **Scope**: implementation +- **Action**: Implement +- **Generic implementation + acceptance criteria** + +## Priority Calculation + +### Issue Priority (1-5) +``` +idea.score: 0-10 +priority = max(1, min(5, ceil((10 - score) / 2))) + +Examples: +score 9-10 → priority 1 (critical) +score 7-8 → priority 2 (high) +score 5-6 → priority 3 (medium) +score 3-4 → priority 4 (low) +score 0-2 → priority 5 (lowest) +``` + +### Task Priority (1-5) +- Research task: 1 (highest) +- Design task: 2 +- Implementation tasks: 3 by default, decrement for later tasks +- Testing/documentation: 4-5 + +### Complexity Analysis +``` +risk: main_challenges.length > 2 ? 'high' : 'medium' +impact: score >= 8 ? 'high' : score >= 6 ? 'medium' : 'low' +complexity: main_challenges > 3 OR tasks > 5 ? 'high' + tasks > 3 ? 'medium' : 'low' +``` + +## CLI Integration + +### Issue Creation +```bash +# Uses heredoc to avoid shell escaping +ccw issue create << 'EOF' +{ + "title": "...", + "context": "...", + "priority": 3, + "source": "brainstorm", + "labels": ["brainstorm", "creative", "feasibility-high"], + ... +} +EOF +``` + +### Solution Binding +```bash +# Append solution to JSONL file +echo '{"id":"SOL-xxx","tasks":[...]}' >> .workflow/issues/solutions/{issue-id}.jsonl + +# Bind to issue +ccw issue bind {issue-id} {solution-id} + +# Update status +ccw issue update {issue-id} --status planned +``` + +## Error Handling + +| Error | Message | Resolution | +|-------|---------|------------| +| Session not found | synthesis.json missing | Check session ID, list available sessions | +| No ideas | top_ideas array empty | Complete brainstorm workflow first | +| Invalid idea index | Index out of range | Check valid range 0 to N-1 | +| Issue creation failed | ccw issue create error | Verify CLI endpoint working | +| Solution binding failed | Bind error | Check issue exists, retry | + +## Examples + +### Interactive Mode + +```bash +Skill(skill="issue-resolve", args="--source brainstorm SESSION=\"BS-rate-limiting-2025-01-28\"") + +# Output: +# | # | Title | Score | Feasibility | +# |---|-------|-------|-------------| +# | 0 | Token Bucket Algorithm | 8.5 | High | +# | 1 | Sliding Window Counter | 7.2 | Medium | +# | 2 | Fixed Window | 6.1 | High | + +# User selects: #0 + +# Result: +# ✓ Created issue: ISS-20250128-001 +# ✓ Created solution: SOL-ISS-20250128-001-ab3d +# ✓ Bound solution to issue +# → Next: /issue:queue +``` + +### Auto Mode + +```bash +Skill(skill="issue-resolve", args="--source brainstorm SESSION=\"BS-caching-2025-01-28\" --auto") + +# Result: +# Auto-selected: Redis Cache Layer (Score: 9.2/10) +# ✓ Created issue: ISS-20250128-002 +# ✓ Solution with 4 tasks +# → Status: planned +``` + +## Integration Flow + +``` +brainstorm-with-file + │ + ├─ synthesis.json + ├─ perspectives.json + └─ .brainstorming/** (optional) + │ + ▼ + Phase 3: From Brainstorm ◄─── This phase + │ + ├─ ISS-YYYYMMDD-NNN (enriched issue) + └─ SOL-{issue-id}-{uid} (structured solution) + │ + ▼ + Phase 4: Form Queue (or Skill(skill="issue-resolve", args="--source queue")) + │ + ▼ + /issue:execute + │ + ▼ + RA → EP → CD → VAS +``` + +## Session Files Reference + +### Input Files + +``` +.workflow/.brainstorm/BS-{slug}-{date}/ +├── synthesis.json # REQUIRED - Top ideas with scores +├── perspectives.json # OPTIONAL - Multi-CLI insights +├── brainstorm.md # Reference only +└── .brainstorming/ # OPTIONAL - Synthesis artifacts + ├── system-architect/ + │ └── analysis.md # Contains clarifications + enhancements + ├── api-designer/ + │ └── analysis.md + └── ... +``` + +### Output Files + +``` +.workflow/issues/ +├── solutions/ +│ └── ISS-YYYYMMDD-001.jsonl # Created solution (JSONL) +└── (managed by ccw issue CLI) +``` + +## Post-Phase Update + +After brainstorm conversion: +- Issue created with `status: planned`, enriched context from brainstorm session +- Solution bound with structured tasks derived from idea.next_steps +- Report: issue ID, solution ID, task count, idea score +- Recommend next step: Form execution queue via Phase 4 or `Skill(skill="issue-resolve", args="--source queue")` diff --git a/.claude/skills/issue-resolve/phases/04-issue-queue.md b/.claude/skills/issue-resolve/phases/04-issue-queue.md new file mode 100644 index 00000000..aff62704 --- /dev/null +++ b/.claude/skills/issue-resolve/phases/04-issue-queue.md @@ -0,0 +1,389 @@ +# Phase 4: Form Execution Queue + +> 来源: `commands/issue/queue.md` + +## Overview + +Queue formation command using **issue-queue-agent** that analyzes all bound solutions, resolves **inter-solution** conflicts, and creates an ordered execution queue at **solution level**. + +**Design Principle**: Queue items are **solutions**, not individual tasks. Each executor receives a complete solution with all its tasks. + +## Prerequisites + +- Issues with `status: planned` and `bound_solution_id` exist +- Solutions written in `.workflow/issues/solutions/{issue-id}.jsonl` +- `ccw issue` CLI available + +## Auto Mode + +When `--yes` or `-y`: Auto-confirm queue formation, use recommended conflict resolutions. + +## Core Capabilities + +- **Agent-driven**: issue-queue-agent handles all ordering logic +- **Solution-level granularity**: Queue items are solutions, not tasks +- **Conflict clarification**: High-severity conflicts prompt user decision +- Semantic priority calculation per solution (0.0-1.0) +- Parallel/Sequential group assignment for solutions + +## Core Guidelines + +**⚠️ Data Access Principle**: Issues and queue files can grow very large. To avoid context overflow: + +| Operation | Correct | Incorrect | +|-----------|---------|-----------| +| List issues (brief) | `ccw issue list --status planned --brief` | `Read('issues.jsonl')` | +| **Batch solutions (NEW)** | `ccw issue solutions --status planned --brief` | Loop `ccw issue solution ` | +| List queue (brief) | `ccw issue queue --brief` | `Read('queues/*.json')` | +| Read issue details | `ccw issue status --json` | `Read('issues.jsonl')` | +| Get next item | `ccw issue next --json` | `Read('queues/*.json')` | +| Update status | `ccw issue update --status ...` | Direct file edit | +| Sync from queue | `ccw issue update --from-queue` | Direct file edit | +| Read solution (single) | `ccw issue solution --brief` | `Read('solutions/*.jsonl')` | + +**Output Options**: +- `--brief`: JSON with minimal fields (id, status, counts) +- `--json`: Full JSON (agent use only) + +**Orchestration vs Execution**: +- **Command (orchestrator)**: Use `--brief` for minimal context +- **Agent (executor)**: Fetch full details → `ccw issue status --json` + +**ALWAYS** use CLI commands for CRUD operations. **NEVER** read entire `issues.jsonl` or `queues/*.json` directly. + +## Flags + +| Flag | Description | Default | +|------|-------------|---------| +| `--queues ` | Number of parallel queues | 1 | +| `--issue ` | Form queue for specific issue only | All planned | +| `--append ` | Append issue to active queue (don't create new) | - | +| `--force` | Skip active queue check, always create new queue | false | + +## CLI Subcommands Reference + +```bash +ccw issue queue list List all queues with status +ccw issue queue add Add issue to queue (interactive if active queue exists) +ccw issue queue add -f Add to new queue without prompt (force) +ccw issue queue merge --queue Merge source queue into target queue +ccw issue queue switch Switch active queue +ccw issue queue archive Archive current queue +ccw issue queue delete Delete queue from history +``` + +## Execution Steps + +### Step 4.1: Solution Loading & Distribution + +**Data Loading:** +- Use `ccw issue solutions --status planned --brief` to get all planned issues with solutions in **one call** +- Returns: Array of `{ issue_id, solution_id, is_bound, task_count, files_touched[], priority }` +- If no bound solutions found → display message, suggest running plan/convert/brainstorm first + +**Build Solution Objects:** +```javascript +// Single CLI call replaces N individual queries +const result = Bash(`ccw issue solutions --status planned --brief`).trim(); +const solutions = result ? JSON.parse(result) : []; + +if (solutions.length === 0) { + console.log('No bound solutions found. Run /issue:plan first.'); + return; +} + +// solutions already in correct format: +// { issue_id, solution_id, is_bound, task_count, files_touched[], priority } +``` + +**Multi-Queue Distribution** (if `--queues > 1`): +- Use `files_touched` from brief output for partitioning +- Group solutions with overlapping files into same queue + +**Output:** Array of solution objects (or N arrays if multi-queue) + +### Step 4.2: Agent-Driven Queue Formation + +**Generate Queue IDs** (command layer, pass to agent): +```javascript +const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14); +const numQueues = args.queues || 1; +const queueIds = numQueues === 1 + ? [`QUE-${timestamp}`] + : Array.from({length: numQueues}, (_, i) => `QUE-${timestamp}-${i + 1}`); +``` + +**Agent Prompt** (same for each queue, with assigned solutions): +``` +## Order Solutions into Execution Queue + +**Queue ID**: ${queueId} +**Solutions**: ${solutions.length} from ${issues.length} issues +**Project Root**: ${cwd} +**Queue Index**: ${queueIndex} of ${numQueues} + +### Input +${JSON.stringify(solutions)} +// Each object: { issue_id, solution_id, task_count, files_touched[], priority } + +### Workflow + +Step 1: Build dependency graph from solutions (nodes=solutions, edges=file conflicts via files_touched) +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 +- Use `files_touched` from input (already extracted by orchestrator) + +### 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 Agents** (parallel if multi-queue): +```javascript +const numQueues = args.queues || 1; + +if (numQueues === 1) { + // Single queue: single agent call + const result = Task( + subagent_type="issue-queue-agent", + prompt=buildPrompt(queueIds[0], solutions), + description=`Order ${solutions.length} solutions` + ); +} else { + // Multi-queue: parallel agent calls (single message with N Task calls) + const agentPromises = solutionGroups.map((group, i) => + Task( + subagent_type="issue-queue-agent", + prompt=buildPrompt(queueIds[i], group, i + 1, numQueues), + description=`Queue ${i + 1}/${numQueues}: ${group.length} solutions` + ) + ); + // All agents launched in parallel via single message with multiple Task tool calls +} +``` + +**Multi-Queue Index Update:** +- First queue sets `active_queue_id` +- All queues added to `queues` array with `queue_group` field linking them + +### Step 4.3: Conflict Clarification + +**Collect Agent Results** (multi-queue): +```javascript +// Collect clarifications from all agents +const allClarifications = results.flatMap((r, i) => + (r.clarifications || []).map(c => ({ ...c, queue_id: queueIds[i], agent_id: agentIds[i] })) +); +``` + +**Check Agent Return:** +- Parse agent result JSON (or all results if multi-queue) +- If any `clarifications` array exists and non-empty → user decision required + +**Clarification Flow:** +```javascript +if (allClarifications.length > 0) { + for (const clarification of allClarifications) { + // Present to user via AskUserQuestion + const answer = AskUserQuestion({ + questions: [{ + question: `[${clarification.queue_id}] ${clarification.question}`, + header: clarification.conflict_id, + options: clarification.options, + multiSelect: false + }] + }); + + // Resume respective agent with user decision + Task( + subagent_type="issue-queue-agent", + resume=clarification.agent_id, + prompt=`Conflict ${clarification.conflict_id} resolved: ${answer.selected}` + ); + } +} +``` + +### Step 4.4: Status Update & Summary + +**Status Update** (MUST use CLI command, NOT direct file operations): + +```bash +# Option 1: Batch update from queue (recommended) +ccw issue update --from-queue [queue-id] --json +ccw issue update --from-queue --json # Use active queue +ccw issue update --from-queue QUE-xxx --json # Use specific queue + +# Option 2: Individual issue update +ccw issue update --status queued +``` + +**⚠️ IMPORTANT**: Do NOT directly modify `issues.jsonl`. Always use CLI command to ensure proper validation and history tracking. + +**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 +- Show unplanned issues (planned but NOT in queue) +- Show next step: `/issue:execute` + +### Step 4.5: Active Queue Check & Decision + +**After agent completes, check for active queue:** + +```bash +ccw issue queue list --brief +``` + +**Decision:** +- If `active_queue_id` is null → `ccw issue queue switch ` (activate new queue) +- If active queue exists → Use **AskUserQuestion** to prompt user + +**AskUserQuestion:** +```javascript +AskUserQuestion({ + questions: [{ + question: "Active queue exists. How would you like to proceed?", + header: "Queue Action", + options: [ + { label: "Merge into existing queue", description: "Add new items to active queue, delete new queue" }, + { label: "Use new queue", description: "Switch to new queue, keep existing in history" }, + { label: "Cancel", description: "Delete new queue, keep existing active" } + ], + multiSelect: false + }] +}) +``` + +**Action Commands:** + +| User Choice | Commands | +|-------------|----------| +| **Merge into existing** | `ccw issue queue merge --queue ` then `ccw issue queue delete ` | +| **Use new queue** | `ccw issue queue switch ` | +| **Cancel** | `ccw issue queue delete ` | + +## Storage Structure (Queue History) + +``` +.workflow/issues/ +├── issues.jsonl # All issues (one per line) +├── queues/ # Queue history directory +│ ├── index.json # Queue index (active + history) +│ ├── {queue-id}.json # Individual queue files +│ └── ... +└── solutions/ + ├── {issue-id}.jsonl # Solutions for issue + └── ... +``` + +### Queue Index Schema + +```json +{ + "active_queue_id": "QUE-20251227-143000", + "active_queue_group": "QGR-20251227-143000", + "queues": [ + { + "id": "QUE-20251227-143000-1", + "queue_group": "QGR-20251227-143000", + "queue_index": 1, + "total_queues": 3, + "status": "active", + "issue_ids": ["ISS-xxx", "ISS-yyy"], + "total_solutions": 3, + "completed_solutions": 1, + "created_at": "2025-12-27T14:30:00Z" + } + ] +} +``` + +**Multi-Queue Fields:** +- `queue_group`: Links queues created in same batch (format: `QGR-{timestamp}`) +- `queue_index`: Position in group (1-based) +- `total_queues`: Total queues in group +- `active_queue_group`: Current active group (for multi-queue execution) + +**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 phases 1-3 (plan/convert/brainstorm) | +| Circular dependency | List cycles, abort queue formation | +| 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 | +| **Queue file missing solutions** | Abort with error, agent must regenerate | +| **User cancels queue add** | Display message, return without changes | +| **Merge with empty source** | Skip merge, display warning | +| **All items duplicate** | Skip merge, display "All items already exist" | + +## Quality Checklist + +Before completing, verify: + +- [ ] All planned issues with `bound_solution_id` are included +- [ ] Queue JSON written to `queues/{queue-id}.json` (N files if multi-queue) +- [ ] Index updated in `queues/index.json` with `active_queue_id` +- [ ] Multi-queue: All queues share same `queue_group` +- [ ] No circular dependencies in solution DAG +- [ ] All conflicts resolved (auto or via user clarification) +- [ ] Parallel groups have no file overlaps +- [ ] Cross-queue: No file overlaps between queues +- [ ] Issue statuses updated to `queued` + +## Post-Phase Update + +After queue formation: +- All planned issues updated to `status: queued` +- Queue files written and index updated +- Report: queue ID(s), solution count, task count, execution groups +- Recommend next step: `/issue:execute` to begin execution diff --git a/ccw/docs-site/src/css/variables.css b/ccw/docs-site/src/css/variables.css index cfbb4c50..8a460d76 100644 --- a/ccw/docs-site/src/css/variables.css +++ b/ccw/docs-site/src/css/variables.css @@ -8,15 +8,15 @@ ============================================================================= */ :root { - /* Primary Colors */ - --ccw-primary-h: 221; - --ccw-primary-s: 83%; - --ccw-primary-l: 53%; + /* Primary Colors - 匹配 Frontend light-blue 主题 */ + --ccw-primary-h: 220; + --ccw-primary-s: 60%; + --ccw-primary-l: 65%; --ccw-primary: hsl(var(--ccw-primary-h), var(--ccw-primary-s), var(--ccw-primary-l)); - --ccw-primary-hover: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 43%); - --ccw-primary-active: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 38%); + --ccw-primary-hover: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 55%); + --ccw-primary-active: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 50%); --ccw-primary-light: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 95%); - --ccw-primary-dark: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 23%); + --ccw-primary-dark: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 30%); /* Secondary Colors */ --ccw-secondary-h: 210; @@ -26,38 +26,38 @@ --ccw-secondary-hover: hsl(var(--ccw-secondary-h), var(--ccw-secondary-s), 92%); --ccw-secondary-border: hsl(var(--ccw-secondary-h), var(--ccw-secondary-s), 88%); - /* Accent Colors */ - --ccw-accent-h: 142; - --ccw-accent-s: 76%; - --ccw-accent-l: 36%; + /* Accent Colors - 匹配 Frontend 蓝色系 */ + --ccw-accent-h: 220; + --ccw-accent-s: 60%; + --ccw-accent-l: 65%; --ccw-accent: hsl(var(--ccw-accent-h), var(--ccw-accent-s), var(--ccw-accent-l)); - --ccw-accent-hover: hsl(var(--ccw-accent-h), var(--ccw-accent-s), 26%); - --ccw-accent-light: hsl(var(--ccw-accent-h), var(--ccw-accent-s), 92%); + --ccw-accent-hover: hsl(var(--ccw-accent-h), var(--ccw-accent-s), 55%); + --ccw-accent-light: hsl(var(--ccw-accent-h), var(--ccw-accent-s), 95%); - /* Background Colors */ + /* Background Colors - 匹配 Frontend */ --ccw-bg-h: 0; --ccw-bg-s: 0%; - --ccw-bg-l: 100%; + --ccw-bg-l: 98%; --ccw-bg: hsl(var(--ccw-bg-h), var(--ccw-bg-s), var(--ccw-bg-l)); - --ccw-bg-secondary: hsl(210, 20%, 98%); - --ccw-bg-tertiary: hsl(210, 20%, 96%); + --ccw-bg-secondary: hsl(220, 10%, 96%); + --ccw-bg-tertiary: hsl(220, 15%, 94%); --ccw-bg-elevated: hsl(0, 0%, 100%); --ccw-bg-overlay: hsla(0, 0%, 0%, 0.5); - /* Text Colors */ - --ccw-text-h: 222; - --ccw-text-s: 47%; - --ccw-text-l: 11%; + /* Text Colors - 匹配 Frontend */ + --ccw-text-h: 220; + --ccw-text-s: 30%; + --ccw-text-l: 15%; --ccw-text-primary: hsl(var(--ccw-text-h), var(--ccw-text-s), var(--ccw-text-l)); - --ccw-text-secondary: hsl(215, 16%, 47%); - --ccw-text-tertiary: hsl(215, 16%, 65%); + --ccw-text-secondary: hsl(220, 15%, 45%); + --ccw-text-tertiary: hsl(220, 15%, 60%); --ccw-text-disabled: hsl(215, 16%, 80%); --ccw-text-inverse: hsl(0, 0%, 100%); - /* Border Colors */ - --ccw-border-h: 214; - --ccw-border-s: 32%; - --ccw-border-l: 91%; + /* Border Colors - 匹配 Frontend */ + --ccw-border-h: 220; + --ccw-border-s: 20%; + --ccw-border-l: 88%; --ccw-border: hsl(var(--ccw-border-h), var(--ccw-border-s), var(--ccw-border-l)); --ccw-border-hover: hsl(var(--ccw-border-h), var(--ccw-border-s), 80%); --ccw-border-focus: var(--ccw-primary); @@ -118,11 +118,11 @@ ============================================================================= */ [data-theme='dark'] { - /* Primary Colors (adjusted for dark mode) */ - --ccw-primary: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 60%); - --ccw-primary-hover: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 50%); - --ccw-primary-active: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 45%); - --ccw-primary-light: hsl(var(--ccw-primary-h), 20%, 20%); + /* Primary Colors - 匹配 Frontend dark-blue 主题 */ + --ccw-primary: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 65%); + --ccw-primary-hover: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 55%); + --ccw-primary-active: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 50%); + --ccw-primary-light: hsl(var(--ccw-primary-h), 30%, 25%); --ccw-primary-dark: hsl(var(--ccw-primary-h), var(--ccw-primary-s), 30%); /* Secondary Colors */ @@ -130,29 +130,29 @@ --ccw-secondary-hover: hsl(217, 33%, 23%); --ccw-secondary-border: hsl(217, 33%, 28%); - /* Background Colors */ - --ccw-bg-h: 222; - --ccw-bg-s: 47%; - --ccw-bg-l: 11%; + /* Background Colors - 匹配 Frontend dark-blue */ + --ccw-bg-h: 220; + --ccw-bg-s: 30%; + --ccw-bg-l: 10%; --ccw-bg: hsl(var(--ccw-bg-h), var(--ccw-bg-s), var(--ccw-bg-l)); - --ccw-bg-secondary: hsl(217, 33%, 17%); - --ccw-bg-tertiary: hsl(215, 28%, 22%); - --ccw-bg-elevated: hsl(217, 33%, 20%); + --ccw-bg-secondary: hsl(220, 25%, 14%); + --ccw-bg-tertiary: hsl(220, 25%, 18%); + --ccw-bg-elevated: hsl(220, 25%, 16%); --ccw-bg-overlay: hsla(0, 0%, 0%, 0.7); - /* Text Colors */ - --ccw-text-primary: hsl(210, 40%, 98%); - --ccw-text-secondary: hsl(215, 16%, 75%); - --ccw-text-tertiary: hsl(215, 16%, 55%); - --ccw-text-disabled: hsl(215, 16%, 40%); + /* Text Colors - 匹配 Frontend dark-blue */ + --ccw-text-primary: hsl(220, 20%, 90%); + --ccw-text-secondary: hsl(220, 15%, 60%); + --ccw-text-tertiary: hsl(220, 15%, 50%); + --ccw-text-disabled: hsl(220, 15%, 40%); --ccw-text-inverse: hsl(0, 0%, 0%); - /* Border Colors */ - --ccw-border-h: 217; - --ccw-border-s: 33%; - --ccw-border-l: 17%; + /* Border Colors - 匹配 Frontend dark-blue */ + --ccw-border-h: 220; + --ccw-border-s: 20%; + --ccw-border-l: 22%; --ccw-border: hsl(var(--ccw-border-h), var(--ccw-border-s), var(--ccw-border-l)); - --ccw-border-hover: hsl(var(--ccw-border-h), var(--ccw-border-s), 25%); + --ccw-border-hover: hsl(var(--ccw-border-h), var(--ccw-border-s), 30%); --ccw-border-focus: var(--ccw-primary); /* Shadow (adjusted for dark mode) */ diff --git a/ccw/frontend/src/components/coordinator/CoordinatorEmptyState.tsx b/ccw/frontend/src/components/coordinator/CoordinatorEmptyState.tsx deleted file mode 100644 index 36ace1b2..00000000 --- a/ccw/frontend/src/components/coordinator/CoordinatorEmptyState.tsx +++ /dev/null @@ -1,192 +0,0 @@ -// ======================================== -// CoordinatorEmptyState Component -// ======================================== -// Modern empty state with tech-inspired design for coordinator start page - -import { useIntl } from 'react-intl'; -import { Play, Rocket, Zap, GitBranch } from 'lucide-react'; -import { Button } from '@/components/ui/Button'; -import { cn } from '@/lib/utils'; - -export interface CoordinatorEmptyStateProps { - onStart: () => void; - disabled?: boolean; - className?: string; -} - -/** - * Empty state component with modern tech-inspired design - * Displays when no coordinator execution is active - */ -export function CoordinatorEmptyState({ - onStart, - disabled = false, - className, -}: CoordinatorEmptyStateProps) { - const { formatMessage } = useIntl(); - - return ( -
- {/* Animated Background - Using theme colors with gradient utilities */} -
- {/* Grid Pattern */} -
- - {/* Animated Gradient Orbs - Using gradient utility classes */} -
-
-
-
- - {/* Main Content */} -
- {/* Hero Icon - Using gradient brand background */} -
-
-
- -
-
- - {/* Title */} -

- {formatMessage({ id: 'coordinator.emptyState.title' })} -

- - {/* Subtitle */} -

- {formatMessage({ id: 'coordinator.emptyState.subtitle' })} -

- - {/* Start Button - Using gradient and glow utilities */} - - - {/* Feature Cards */} -
- {/* Feature 1 */} -
-
-
-
- -
-

- {formatMessage({ id: 'coordinator.emptyState.feature1.title' })} -

-

- {formatMessage({ id: 'coordinator.emptyState.feature1.description' })} -

-
-
- - {/* Feature 2 */} -
-
-
-
- -
-

- {formatMessage({ id: 'coordinator.emptyState.feature2.title' })} -

-

- {formatMessage({ id: 'coordinator.emptyState.feature2.description' })} -

-
-
- - {/* Feature 3 */} -
-
-
-
- -
-

- {formatMessage({ id: 'coordinator.emptyState.feature3.title' })} -

-

- {formatMessage({ id: 'coordinator.emptyState.feature3.description' })} -

-
-
-
- - {/* Quick Start Guide */} -
-

- - ok - - {formatMessage({ id: 'coordinator.emptyState.quickStart.title' })} -

-
-
- - 1 - -

{formatMessage({ id: 'coordinator.emptyState.quickStart.step1' })}

-
-
- - 2 - -

{formatMessage({ id: 'coordinator.emptyState.quickStart.step2' })}

-
-
- - 3 - -

{formatMessage({ id: 'coordinator.emptyState.quickStart.step3' })}

-
-
-
-
-
- ); -} - -export default CoordinatorEmptyState; diff --git a/ccw/frontend/src/components/coordinator/CoordinatorInputModal.test.tsx b/ccw/frontend/src/components/coordinator/CoordinatorInputModal.test.tsx deleted file mode 100644 index 79d88e37..00000000 --- a/ccw/frontend/src/components/coordinator/CoordinatorInputModal.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { IntlProvider } from 'react-intl'; -import { CoordinatorInputModal } from './CoordinatorInputModal'; - -// Mock zustand stores -vi.mock('@/stores/coordinatorStore', () => ({ - useCoordinatorStore: () => ({ - startCoordinator: vi.fn(), - }), -})); - -vi.mock('@/hooks/useNotifications', () => ({ - useNotifications: () => ({ - success: vi.fn(), - error: vi.fn(), - }), -})); - -// Mock fetch -global.fetch = vi.fn(); - -const mockMessages = { - 'coordinator.modal.title': 'Start Coordinator', - 'coordinator.modal.description': 'Describe the task', - 'coordinator.form.taskDescription': 'Task Description', - 'coordinator.form.taskDescriptionPlaceholder': 'Enter task description', - 'coordinator.form.parameters': 'Parameters', - 'coordinator.form.parametersPlaceholder': '{"key": "value"}', - 'coordinator.form.parametersHelp': 'Optional JSON parameters', - 'coordinator.form.characterCount': '{current} / {max} characters (min: {min})', - 'coordinator.form.start': 'Start', - 'coordinator.form.starting': 'Starting...', - 'coordinator.validation.taskDescriptionRequired': 'Task description is required', - 'coordinator.validation.taskDescriptionTooShort': 'Too short', - 'coordinator.validation.taskDescriptionTooLong': 'Too long', - 'coordinator.validation.parametersInvalidJson': 'Invalid JSON', - 'common.actions.cancel': 'Cancel', -}; - -const renderWithIntl = (ui: React.ReactElement) => { - return render( - - {ui} - - ); -}; - -describe('CoordinatorInputModal', () => { - const mockOnClose = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should render when open', () => { - renderWithIntl(); - - expect(screen.getByText('Start Coordinator')).toBeInTheDocument(); - expect(screen.getByText('Describe the task')).toBeInTheDocument(); - }); - - it('should not render when closed', () => { - renderWithIntl(); - - expect(screen.queryByText('Start Coordinator')).not.toBeInTheDocument(); - }); - - it('should show validation error for empty task description', async () => { - renderWithIntl(); - - const startButton = screen.getByText('Start'); - fireEvent.click(startButton); - - await waitFor(() => { - expect(screen.getByText('Task description is required')).toBeInTheDocument(); - }); - }); - - it('should show validation error for short task description', async () => { - renderWithIntl(); - - const textarea = screen.getByPlaceholderText('Enter task description'); - fireEvent.change(textarea, { target: { value: 'Short' } }); - - const startButton = screen.getByText('Start'); - fireEvent.click(startButton); - - await waitFor(() => { - expect(screen.getByText('Too short')).toBeInTheDocument(); - }); - }); - - it('should show validation error for invalid JSON parameters', async () => { - renderWithIntl(); - - const textarea = screen.getByPlaceholderText('Enter task description'); - fireEvent.change(textarea, { target: { value: 'Valid task description here' } }); - - const paramsInput = screen.getByPlaceholderText('{"key": "value"}'); - fireEvent.change(paramsInput, { target: { value: 'invalid json' } }); - - const startButton = screen.getByText('Start'); - fireEvent.click(startButton); - - await waitFor(() => { - expect(screen.getByText('Invalid JSON')).toBeInTheDocument(); - }); - }); - - it('should submit with valid task description', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ success: true }), - }); - global.fetch = mockFetch; - - renderWithIntl(); - - const textarea = screen.getByPlaceholderText('Enter task description'); - fireEvent.change(textarea, { target: { value: 'Valid task description with more than 10 characters' } }); - - const startButton = screen.getByText('Start'); - fireEvent.click(startButton); - - await waitFor(() => { - expect(mockFetch).toHaveBeenCalledWith( - '/api/coordinator/start', - expect.objectContaining({ - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }) - ); - }); - }); -}); diff --git a/ccw/frontend/src/components/coordinator/CoordinatorInputModal.tsx b/ccw/frontend/src/components/coordinator/CoordinatorInputModal.tsx deleted file mode 100644 index 680dddd5..00000000 --- a/ccw/frontend/src/components/coordinator/CoordinatorInputModal.tsx +++ /dev/null @@ -1,441 +0,0 @@ -// ======================================== -// Coordinator Input Modal Component (Multi-Step) -// ======================================== -// Two-step modal: Welcome page -> Template & Parameters - -import { useState, useEffect } from 'react'; -import { useIntl } from 'react-intl'; -import { Loader2, Rocket, Zap, GitBranch, Eye, ChevronRight, ChevronLeft } from 'lucide-react'; -import { - Dialog, - DialogContent, - DialogTitle, -} from '@/components/ui/Dialog'; -import { Button } from '@/components/ui/Button'; -import { Textarea } from '@/components/ui/Textarea'; -import { Label } from '@/components/ui/Label'; -import { useCoordinatorStore } from '@/stores/coordinatorStore'; -import { useNotifications } from '@/hooks/useNotifications'; -import { cn } from '@/lib/utils'; - -// ========== Types ========== - -export interface CoordinatorInputModalProps { - open: boolean; - onClose: () => void; -} - -interface FormErrors { - taskDescription?: string; - parameters?: string; -} - -// ========== Constants ========== - -const TEMPLATES = [ - { id: 'feature-dev', nameKey: 'coordinator.multiStep.step2.templates.featureDev', description: 'Complete feature development workflow' }, - { id: 'api-integration', nameKey: 'coordinator.multiStep.step2.templates.apiIntegration', description: 'Third-party API integration' }, - { id: 'performance', nameKey: 'coordinator.multiStep.step2.templates.performanceOptimization', description: 'System performance analysis' }, - { id: 'documentation', nameKey: 'coordinator.multiStep.step2.templates.documentGeneration', description: 'Auto-generate documentation' }, -] as const; - -const TOTAL_STEPS = 2; - -// ========== Validation Helper ========== - -function validateForm(taskDescription: string, parameters: string): FormErrors { - const errors: FormErrors = {}; - - if (!taskDescription.trim()) { - errors.taskDescription = 'coordinator.validation.taskDescriptionRequired'; - } else { - const length = taskDescription.trim().length; - if (length < 10) { - errors.taskDescription = 'coordinator.validation.taskDescriptionTooShort'; - } else if (length > 2000) { - errors.taskDescription = 'coordinator.validation.taskDescriptionTooLong'; - } - } - - if (parameters.trim()) { - try { - JSON.parse(parameters.trim()); - } catch { - errors.parameters = 'coordinator.validation.parametersInvalidJson'; - } - } - - return errors; -} - -// ========== Feature Card Data ========== - -const FEATURES = [ - { - icon: Zap, - titleKey: 'coordinator.multiStep.step1.feature1.title', - descriptionKey: 'coordinator.multiStep.step1.feature1.description', - bgClass: 'bg-primary/10', - iconClass: 'text-primary', - }, - { - icon: GitBranch, - titleKey: 'coordinator.multiStep.step1.feature2.title', - descriptionKey: 'coordinator.multiStep.step1.feature2.description', - bgClass: 'bg-secondary/10', - iconClass: 'text-secondary-foreground', - }, - { - icon: Eye, - titleKey: 'coordinator.multiStep.step1.feature3.title', - descriptionKey: 'coordinator.multiStep.step1.feature3.description', - bgClass: 'bg-accent/10', - iconClass: 'text-accent-foreground', - }, -] as const; - -// ========== Component ========== - -export function CoordinatorInputModal({ open, onClose }: CoordinatorInputModalProps) { - const { formatMessage } = useIntl(); - const { success, error: showError } = useNotifications(); - const { startCoordinator } = useCoordinatorStore(); - - // Step state - const [step, setStep] = useState(1); - - // Form state - const [taskDescription, setTaskDescription] = useState(''); - const [parameters, setParameters] = useState(''); - const [selectedTemplate, setSelectedTemplate] = useState(null); - const [errors, setErrors] = useState({}); - const [isSubmitting, setIsSubmitting] = useState(false); - - // Reset all state when modal opens/closes - useEffect(() => { - if (open) { - setStep(1); - setTaskDescription(''); - setParameters(''); - setSelectedTemplate(null); - setErrors({}); - setIsSubmitting(false); - } - }, [open]); - - // Handle field change - const handleFieldChange = ( - field: 'taskDescription' | 'parameters', - value: string - ) => { - if (field === 'taskDescription') { - setTaskDescription(value); - } else { - setParameters(value); - } - - if (errors[field]) { - setErrors((prev) => ({ ...prev, [field]: undefined })); - } - }; - - // Handle template selection - const handleTemplateSelect = (templateId: string) => { - setSelectedTemplate(templateId); - const template = TEMPLATES.find((t) => t.id === templateId); - if (template) { - setTaskDescription(template.description); - if (errors.taskDescription) { - setErrors((prev) => ({ ...prev, taskDescription: undefined })); - } - } - }; - - // Handle submit - preserved exactly from original - const handleSubmit = async () => { - const validationErrors = validateForm(taskDescription, parameters); - if (Object.keys(validationErrors).length > 0) { - setErrors(validationErrors); - return; - } - - setIsSubmitting(true); - try { - const parsedParams = parameters.trim() ? JSON.parse(parameters.trim()) : undefined; - - const executionId = `exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - const response = await fetch('/api/coordinator/start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - executionId, - taskDescription: taskDescription.trim(), - parameters: parsedParams, - }), - }); - - if (!response.ok) { - const error = await response.json().catch(() => ({ message: 'Unknown error' })); - throw new Error(error.message || 'Failed to start coordinator'); - } - - await startCoordinator(executionId, taskDescription.trim(), parsedParams); - - success(formatMessage({ id: 'coordinator.success.started' })); - onClose(); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - showError('Error', errorMessage); - console.error('Failed to start coordinator:', error); - } finally { - setIsSubmitting(false); - } - }; - - // Navigation - const handleNext = () => setStep(2); - const handleBack = () => setStep(1); - - // ========== Step 1: Welcome ========== - - const renderStep1 = () => ( -
- {/* Hero Icon */} -
- -
- - {/* Title & Subtitle */} -

- {formatMessage({ id: 'coordinator.multiStep.step1.title' })} -

-

- {formatMessage({ id: 'coordinator.multiStep.step1.subtitle' })} -

- - {/* Feature Cards */} -
- {FEATURES.map((feature) => { - const Icon = feature.icon; - return ( -
- - - {formatMessage({ id: feature.titleKey })} - - - {formatMessage({ id: feature.descriptionKey })} - -
- ); - })} -
-
- ); - - // ========== Step 2: Template + Parameters ========== - - const renderStep2 = () => ( -
- {/* Left Column: Template Selection */} -
-

- {formatMessage({ id: 'coordinator.multiStep.step2.templateLabel' })} -

-
- {TEMPLATES.map((template) => { - const isSelected = selectedTemplate === template.id; - return ( - - ); - })} -
-
- - {/* Right Column: Parameter Form */} -
- {/* Task Description */} -
- -