diff --git a/.claude/agents/issue-plan-agent.md b/.claude/agents/issue-plan-agent.md index 870c0fbb..cf7a1a1c 100644 --- a/.claude/agents/issue-plan-agent.md +++ b/.claude/agents/issue-plan-agent.md @@ -199,7 +199,7 @@ async function ripgrepFallback(issue, projectRoot) { ## Phase 3: Solution Planning -### Task Decomposition +### Task Decomposition (Closed-Loop) ```javascript function decomposeTasks(issue, exploration) { @@ -217,15 +217,104 @@ function decomposeTasks(issue, exploration) { action: inferAction(group), description: group.description, modification_points: group.points, + + // Phase 1: Implementation implementation: generateImplementationSteps(group, exploration), + + // Phase 2: Test + test: generateTestRequirements(group, exploration, issue.lifecycle_requirements), + + // Phase 3: Regression + regression: generateRegressionChecks(group, issue.lifecycle_requirements), + + // Phase 4: Acceptance acceptance: generateAcceptanceCriteria(group), + + // Phase 5: Commit + commit: generateCommitSpec(group, issue), + depends_on: inferDependencies(group, tasks), - estimated_minutes: estimateTime(group) + estimated_minutes: estimateTime(group), + executor: inferExecutor(group) }) } return tasks } + +function generateTestRequirements(group, exploration, lifecycle) { + const test = { + unit: [], + integration: [], + commands: [], + coverage_target: 80 + } + + // Generate unit test requirements based on action + if (group.action === 'Create' || group.action === 'Implement') { + test.unit.push(`Test ${group.title} happy path`) + test.unit.push(`Test ${group.title} error cases`) + } + + // Generate test commands based on project patterns + if (exploration.test_patterns?.includes('jest')) { + test.commands.push(`npm test -- --grep '${group.scope}'`) + } else if (exploration.test_patterns?.includes('vitest')) { + test.commands.push(`npx vitest run ${group.scope}`) + } else { + test.commands.push(`npm test`) + } + + // Add integration tests if needed + if (lifecycle?.test_strategy === 'integration' || lifecycle?.test_strategy === 'e2e') { + test.integration.push(`Integration test for ${group.title}`) + } + + return test +} + +function generateRegressionChecks(group, lifecycle) { + const regression = [] + + switch (lifecycle?.regression_scope) { + case 'full': + regression.push('npm test') + regression.push('npm run test:integration') + break + case 'related': + regression.push(`npm test -- --grep '${group.scope}'`) + regression.push(`npm test -- --changed`) + break + case 'affected': + default: + regression.push(`npm test -- --findRelatedTests ${group.points[0]?.file}`) + break + } + + return regression +} + +function generateCommitSpec(group, issue) { + const typeMap = { + 'Create': 'feat', + 'Implement': 'feat', + 'Update': 'feat', + 'Fix': 'fix', + 'Refactor': 'refactor', + 'Test': 'test', + 'Configure': 'chore', + 'Delete': 'chore' + } + + const scope = group.scope.split('/').pop()?.replace(/\..*$/, '') || 'core' + + return { + type: typeMap[group.action] || 'feat', + scope: scope, + message_template: `${typeMap[group.action] || 'feat'}(${scope}): ${group.title.toLowerCase()}\n\n${group.description || ''}`, + breaking: false + } +} ``` ### Action Type Inference @@ -347,11 +436,15 @@ function generateImplementationSteps(group, exploration) { } ``` -### Acceptance Criteria Generation +### Acceptance Criteria Generation (Closed-Loop) ```javascript function generateAcceptanceCriteria(task) { - const criteria = [] + const acceptance = { + criteria: [], + verification: [], + manual_checks: [] + } // Action-specific criteria const actionCriteria = { @@ -363,14 +456,41 @@ function generateAcceptanceCriteria(task) { 'Configure': [`Configuration applied correctly`] } - criteria.push(...(actionCriteria[task.action] || [])) + acceptance.criteria.push(...(actionCriteria[task.action] || [])) // Add quantified criteria if (task.modification_points.length > 0) { - criteria.push(`${task.modification_points.length} file(s) modified correctly`) + acceptance.criteria.push(`${task.modification_points.length} file(s) modified correctly`) } - return criteria.slice(0, 4) // Max 4 criteria + // Generate verification steps for each criterion + for (const criterion of acceptance.criteria) { + acceptance.verification.push(generateVerificationStep(criterion, task)) + } + + // Limit to reasonable counts + acceptance.criteria = acceptance.criteria.slice(0, 4) + acceptance.verification = acceptance.verification.slice(0, 4) + + return acceptance +} + +function generateVerificationStep(criterion, task) { + // Generate executable verification for criterion + if (criterion.includes('file created')) { + return `ls -la ${task.modification_points[0]?.file} && head -20 ${task.modification_points[0]?.file}` + } + if (criterion.includes('test')) { + return `npm test -- --grep '${task.scope}'` + } + if (criterion.includes('export')) { + return `node -e "console.log(require('./${task.modification_points[0]?.file}'))"` + } + if (criterion.includes('API') || criterion.includes('endpoint')) { + return `curl -X GET http://localhost:3000/${task.scope} -v` + } + // Default: describe manual check + return `Manually verify: ${criterion}` } ``` @@ -413,20 +533,61 @@ function validateSolution(solution) { function validateTask(task) { const errors = [] + // Basic fields if (!/^T\d+$/.test(task.id)) errors.push('Invalid task ID format') if (!task.title?.trim()) errors.push('Missing title') if (!task.scope?.trim()) errors.push('Missing scope') if (!['Create', 'Update', 'Implement', 'Refactor', 'Configure', 'Test', 'Fix', 'Delete'].includes(task.action)) { errors.push('Invalid action type') } + + // Phase 1: Implementation if (!task.implementation || task.implementation.length < 2) { errors.push('Need 2+ implementation steps') } - if (!task.acceptance || task.acceptance.length < 1) { - errors.push('Need 1+ acceptance criteria') + + // Phase 2: Test + if (!task.test) { + errors.push('Missing test phase') + } else { + if (!task.test.commands || task.test.commands.length < 1) { + errors.push('Need 1+ test commands') + } } - if (task.acceptance?.some(a => /works correctly|good performance|properly/i.test(a))) { - errors.push('Vague acceptance criteria') + + // Phase 3: Regression + if (!task.regression || task.regression.length < 1) { + errors.push('Need 1+ regression checks') + } + + // Phase 4: Acceptance + if (!task.acceptance) { + errors.push('Missing acceptance phase') + } else { + if (!task.acceptance.criteria || task.acceptance.criteria.length < 1) { + errors.push('Need 1+ acceptance criteria') + } + if (!task.acceptance.verification || task.acceptance.verification.length < 1) { + errors.push('Need 1+ verification steps') + } + if (task.acceptance.criteria?.some(a => /works correctly|good performance|properly/i.test(a))) { + errors.push('Vague acceptance criteria') + } + } + + // Phase 5: Commit + if (!task.commit) { + errors.push('Missing commit phase') + } else { + if (!['feat', 'fix', 'refactor', 'test', 'docs', 'chore'].includes(task.commit.type)) { + errors.push('Invalid commit type') + } + if (!task.commit.scope?.trim()) { + errors.push('Missing commit scope') + } + if (!task.commit.message_template?.trim()) { + errors.push('Missing commit message template') + } } return errors @@ -500,7 +661,9 @@ function generateOutput(solutions, conflicts) { } ``` -### Solution Schema +### Solution Schema (Closed-Loop Tasks) + +Each task MUST include ALL 5 lifecycle phases: ```json { @@ -517,10 +680,62 @@ function generateOutput(solutions, conflicts) { "modification_points": [ { "file": "src/middleware/auth.ts", "target": "new file", "change": "Create middleware" } ], - "implementation": ["Step 1", "Step 2", "..."], - "acceptance": ["Criterion 1", "Criterion 2"], + + "implementation": [ + "Create auth.ts file in src/middleware/", + "Implement JWT token extraction from Authorization header", + "Add token validation using jsonwebtoken library", + "Handle error cases (missing, invalid, expired tokens)", + "Export middleware function" + ], + + "test": { + "unit": [ + "Test valid token passes through", + "Test invalid token returns 401", + "Test expired token returns 401", + "Test missing token returns 401" + ], + "integration": [ + "Protected route returns 401 without token", + "Protected route returns 200 with valid token" + ], + "commands": [ + "npm test -- --grep 'auth middleware'", + "npm run test:coverage -- src/middleware/auth.ts" + ], + "coverage_target": 80 + }, + + "regression": [ + "npm test -- --grep 'existing routes'", + "npm run test:integration" + ], + + "acceptance": { + "criteria": [ + "Middleware validates JWT tokens successfully", + "Returns 401 with appropriate error for invalid tokens", + "Passes decoded user payload to request context" + ], + "verification": [ + "curl -H 'Authorization: Bearer ' /api/protected → 200", + "curl /api/protected → 401 {error: 'No token'}", + "curl -H 'Authorization: Bearer invalid' /api/protected → 401" + ], + "manual_checks": [] + }, + + "commit": { + "type": "feat", + "scope": "auth", + "message_template": "feat(auth): add JWT validation middleware\n\n- Implement token extraction and validation\n- Add error handling for invalid/expired tokens\n- Export middleware for route protection", + "breaking": false + }, + "depends_on": [], - "estimated_minutes": 30 + "estimated_minutes": 30, + "executor": "codex" } ], "exploration_context": { @@ -622,6 +837,14 @@ Before outputting solution: 6. Include file:line references in modification_points where possible 7. Detect and report cross-issue file conflicts in batch mode 8. Include exploration_context with patterns and relevant_files +9. **Generate ALL 5 lifecycle phases for each task**: + - `implementation`: 2-7 concrete steps + - `test`: unit tests, commands, coverage target + - `regression`: regression check commands + - `acceptance`: criteria + verification steps + - `commit`: type, scope, message template +10. Infer test commands from project's test framework +11. Generate commit message following conventional commits **NEVER**: 1. Execute implementation (return plan only) @@ -632,3 +855,5 @@ Before outputting solution: 6. Assume file exists without verification 7. Generate more than 10 tasks per issue 8. Skip ACE search (unless fallback triggered) +9. **Omit any of the 5 lifecycle phases** (test, regression, acceptance, commit) +10. Skip verification steps in acceptance criteria diff --git a/.claude/commands/issue/execute.md b/.claude/commands/issue/execute.md index 1e5305bf..c417a9cc 100644 --- a/.claude/commands/issue/execute.md +++ b/.claude/commands/issue/execute.md @@ -148,15 +148,15 @@ TodoWrite({ }); ``` -### Phase 3: Codex Coordination (Single Task Mode) +### Phase 3: Codex Coordination (Single Task Mode - Full Lifecycle) ```javascript -// Execute tasks - single codex instance per task +// Execute tasks - single codex instance per task with full lifecycle async function executeTask(queueItem) { const codexPrompt = ` -## Single Task Execution +## Single Task Execution - CLOSED-LOOP LIFECYCLE -You are executing ONE task from the issue queue. Follow these steps exactly: +You are executing ONE task from the issue queue. Each task has 5 phases that MUST ALL complete successfully. ### Step 1: Fetch Task Run this command to get your task: @@ -164,35 +164,71 @@ Run this command to get your task: ccw issue next \`\`\` -This returns JSON with: -- queue_id: Queue item ID -- task: Task definition with implementation steps -- context: Exploration context -- execution_hints: Executor and time estimate +This returns JSON with full lifecycle definition: +- task.implementation: Implementation steps +- task.test: Test requirements and commands +- task.regression: Regression check commands +- task.acceptance: Acceptance criteria and verification +- task.commit: Commit specification -### Step 2: Execute Task -Read the returned task object and: +### Step 2: Execute Full Lifecycle + +**Phase 1: IMPLEMENT** 1. Follow task.implementation steps in order -2. Meet all task.acceptance criteria -3. Use provided context.relevant_files for reference +2. Modify files specified in modification_points +3. Use context.relevant_files for reference 4. Use context.patterns for code style +**Phase 2: TEST** +1. Run test commands from task.test.commands +2. Ensure all unit tests pass (task.test.unit) +3. Run integration tests if specified (task.test.integration) +4. Verify coverage meets task.test.coverage_target if specified +5. If tests fail → fix code and re-run, do NOT proceed until tests pass + +**Phase 3: REGRESSION** +1. Run all commands in task.regression +2. Ensure no existing tests are broken +3. If regression fails → fix and re-run + +**Phase 4: ACCEPTANCE** +1. Verify each criterion in task.acceptance.criteria +2. Execute verification steps in task.acceptance.verification +3. Complete any manual_checks if specified +4. All criteria MUST pass before proceeding + +**Phase 5: COMMIT** +1. Stage all modified files +2. Use task.commit.message_template as commit message +3. Commit with: git commit -m "$(cat <<'EOF'\n\nEOF\n)" +4. If commit_strategy is 'per-task', commit now +5. If commit_strategy is 'atomic' or 'squash', stage but don't commit + ### Step 3: Report Completion -When done, run: +When ALL phases complete successfully: \`\`\`bash -ccw issue complete --result '{"files_modified": ["path1", "path2"], "summary": "What was done"}' +ccw issue complete --result '{ + "files_modified": ["path1", "path2"], + "tests_passed": true, + "regression_passed": true, + "acceptance_passed": true, + "committed": true, + "commit_hash": "", + "summary": "What was done" +}' \`\`\` -If task fails, run: +If any phase fails and cannot be fixed: \`\`\`bash -ccw issue fail --reason "Why it failed" +ccw issue fail --reason "Phase X failed:
" \`\`\` ### Rules -- NEVER read task files directly - use ccw issue next -- Execute the FULL task before marking complete -- Do NOT loop - execute ONE task only -- Report accurate files_modified in result +- NEVER skip any lifecycle phase +- Tests MUST pass before proceeding to acceptance +- Regression MUST pass before commit +- ALL acceptance criteria MUST be verified +- Report accurate lifecycle status in result ### Start Now Begin by running: ccw issue next diff --git a/.claude/commands/issue/new.md b/.claude/commands/issue/new.md index e781da30..d09f56da 100644 --- a/.claude/commands/issue/new.md +++ b/.claude/commands/issue/new.md @@ -15,7 +15,7 @@ Creates a new structured issue from either: Outputs a well-formed issue entry to `.workflow/issues/issues.jsonl`. -## Issue Structure +## Issue Structure (Closed-Loop) ```typescript interface Issue { @@ -27,14 +27,22 @@ interface Issue { source: 'github' | 'text'; // Input source type source_url?: string; // GitHub URL if applicable labels?: string[]; // Categorization labels - + // Structured extraction problem_statement: string; // What is the problem? expected_behavior?: string; // What should happen? actual_behavior?: string; // What actually happens? affected_components?: string[];// Files/modules affected reproduction_steps?: string[]; // Steps to reproduce - + + // Closed-loop requirements (guide plan generation) + lifecycle_requirements: { + test_strategy: 'unit' | 'integration' | 'e2e' | 'manual' | 'auto'; + regression_scope: 'affected' | 'related' | 'full'; // Which tests to run + acceptance_type: 'automated' | 'manual' | 'both'; // How to verify + commit_strategy: 'per-task' | 'squash' | 'atomic'; // Commit granularity + }; + // Metadata bound_solution_id: null; solution_count: 0; @@ -43,6 +51,52 @@ interface Issue { } ``` +## Task Lifecycle (Each Task is Closed-Loop) + +When `/issue:plan` generates tasks, each task MUST include: + +```typescript +interface SolutionTask { + id: string; + title: string; + scope: string; + action: string; + + // Phase 1: Implementation + implementation: string[]; // Step-by-step implementation + modification_points: { file: string; target: string; change: string }[]; + + // Phase 2: Testing + test: { + unit?: string[]; // Unit test requirements + integration?: string[]; // Integration test requirements + commands?: string[]; // Test commands to run + coverage_target?: number; // Minimum coverage % + }; + + // Phase 3: Regression + regression: string[]; // Regression check commands/points + + // Phase 4: Acceptance + acceptance: { + criteria: string[]; // Testable acceptance criteria + verification: string[]; // How to verify each criterion + manual_checks?: string[]; // Manual verification if needed + }; + + // Phase 5: Commit + commit: { + type: 'feat' | 'fix' | 'refactor' | 'test' | 'docs' | 'chore'; + scope: string; // e.g., "auth", "api" + message_template: string; // Commit message template + breaking?: boolean; + }; + + depends_on: string[]; + executor: 'codex' | 'gemini' | 'agent' | 'auto'; +} +``` + ## Usage ```bash @@ -206,7 +260,58 @@ async function parseTextDescription(text) { } ``` -### Phase 4: User Confirmation +### Phase 4: Lifecycle Configuration + +```javascript +// Ask for lifecycle requirements (or use smart defaults) +const lifecycleAnswer = AskUserQuestion({ + questions: [ + { + question: 'Test strategy for this issue?', + header: 'Test', + multiSelect: false, + options: [ + { label: 'auto', description: 'Auto-detect based on affected files (Recommended)' }, + { label: 'unit', description: 'Unit tests only' }, + { label: 'integration', description: 'Integration tests' }, + { label: 'e2e', description: 'End-to-end tests' }, + { label: 'manual', description: 'Manual testing only' } + ] + }, + { + question: 'Regression scope?', + header: 'Regression', + multiSelect: false, + options: [ + { label: 'affected', description: 'Only affected module tests (Recommended)' }, + { label: 'related', description: 'Affected + dependent modules' }, + { label: 'full', description: 'Full test suite' } + ] + }, + { + question: 'Commit strategy?', + header: 'Commit', + multiSelect: false, + options: [ + { label: 'per-task', description: 'One commit per task (Recommended)' }, + { label: 'atomic', description: 'Single commit for entire issue' }, + { label: 'squash', description: 'Squash at the end' } + ] + } + ] +}); + +const lifecycle = { + test_strategy: lifecycleAnswer.test || 'auto', + regression_scope: lifecycleAnswer.regression || 'affected', + acceptance_type: 'automated', + commit_strategy: lifecycleAnswer.commit || 'per-task' +}; + +issueData.lifecycle_requirements = lifecycle; +``` + +### Phase 5: User Confirmation ```javascript // Show parsed data and ask for confirmation @@ -224,6 +329,11 @@ ${issueData.expected_behavior ? `### Expected Behavior\n${issueData.expected_beh ${issueData.actual_behavior ? `### Actual Behavior\n${issueData.actual_behavior}\n` : ''} ${issueData.affected_components?.length ? `### Affected Components\n${issueData.affected_components.map(c => `- ${c}`).join('\n')}\n` : ''} ${issueData.reproduction_steps?.length ? `### Reproduction Steps\n${issueData.reproduction_steps.map((s, i) => `${i+1}. ${s}`).join('\n')}\n` : ''} + +### Lifecycle Configuration +- **Test Strategy**: ${lifecycle.test_strategy} +- **Regression Scope**: ${lifecycle.regression_scope} +- **Commit Strategy**: ${lifecycle.commit_strategy} `); // Ask user to confirm or edit @@ -264,7 +374,7 @@ if (answer.includes('Edit Title')) { } ``` -### Phase 5: Write to JSONL +### Phase 6: Write to JSONL ```javascript // Construct final issue object @@ -280,14 +390,22 @@ const newIssue = { source: issueData.source, source_url: issueData.source_url || null, labels: [...(issueData.labels || []), ...labels], - + // Structured fields problem_statement: issueData.problem_statement, expected_behavior: issueData.expected_behavior || null, actual_behavior: issueData.actual_behavior || null, affected_components: issueData.affected_components || [], reproduction_steps: issueData.reproduction_steps || [], - + + // Closed-loop lifecycle requirements + lifecycle_requirements: issueData.lifecycle_requirements || { + test_strategy: 'auto', + regression_scope: 'affected', + acceptance_type: 'automated', + commit_strategy: 'per-task' + }, + // Metadata bound_solution_id: null, solution_count: 0, diff --git a/.claude/commands/issue/plan.md b/.claude/commands/issue/plan.md index 94caf925..f994034a 100644 --- a/.claude/commands/issue/plan.md +++ b/.claude/commands/issue/plan.md @@ -133,28 +133,59 @@ TodoWrite({ for (const [batchIndex, batch] of batches.entries()) { updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress'); - // Build issue prompt for agent + // Build issue prompt for agent with lifecycle requirements const issuePrompt = ` -## Issues to Plan +## Issues to Plan (Closed-Loop Tasks Required) ${batch.map((issue, i) => ` ### Issue ${i + 1}: ${issue.id} **Title**: ${issue.title} **Context**: ${issue.context || 'No context provided'} +**Affected Components**: ${issue.affected_components?.join(', ') || 'Not specified'} + +**Lifecycle Requirements**: +- Test Strategy: ${issue.lifecycle_requirements?.test_strategy || 'auto'} +- Regression Scope: ${issue.lifecycle_requirements?.regression_scope || 'affected'} +- Commit Strategy: ${issue.lifecycle_requirements?.commit_strategy || 'per-task'} `).join('\n')} ## Project Root ${process.cwd()} -## Requirements +## Requirements - CLOSED-LOOP TASKS + +Each task MUST include ALL lifecycle phases: + +### 1. Implementation +- implementation: string[] (2-7 concrete steps) +- modification_points: { file, target, change }[] + +### 2. Test +- test.unit: string[] (unit test requirements) +- test.integration: string[] (integration test requirements if needed) +- test.commands: string[] (actual test commands to run) +- test.coverage_target: number (minimum coverage %) + +### 3. Regression +- regression: string[] (commands to run for regression check) +- Based on issue's regression_scope setting + +### 4. Acceptance +- acceptance.criteria: string[] (testable acceptance criteria) +- acceptance.verification: string[] (how to verify each criterion) +- acceptance.manual_checks: string[] (manual checks if needed) + +### 5. Commit +- commit.type: feat|fix|refactor|test|docs|chore +- commit.scope: string (module name) +- commit.message_template: string (full commit message) +- commit.breaking: boolean + +## Additional Requirements 1. Use ACE semantic search (mcp__ace-tool__search_context) for exploration -2. Generate complete solution with task breakdown -3. Each task must have: - - implementation steps (2-7 steps) - - acceptance criteria (1-4 testable criteria) - - modification_points (exact file locations) - - depends_on (task dependencies) -4. Detect file conflicts if multiple issues +2. Detect file conflicts if multiple issues +3. Generate executable test commands based on project's test framework +4. Infer commit scope from affected files `; // Launch issue-plan-agent (combines explore + plan) @@ -281,7 +312,7 @@ ${issues.map(i => { `); ``` -## Solution Format +## Solution Format (Closed-Loop Tasks) Each solution line in `solutions/{issue-id}.jsonl`: @@ -299,18 +330,56 @@ Each solution line in `solutions/{issue-id}.jsonl`: "modification_points": [ { "file": "src/middleware/auth.ts", "target": "new file", "change": "Create middleware" } ], + "implementation": [ - "Create auth.ts file", - "Implement JWT validation", - "Add error handling", - "Export middleware" + "Create auth.ts file in src/middleware/", + "Implement JWT token validation using jsonwebtoken", + "Add error handling for invalid/expired tokens", + "Export middleware function" ], - "acceptance": [ - "Middleware validates JWT tokens", - "Returns 401 for invalid tokens" + + "test": { + "unit": [ + "Test valid token passes through", + "Test invalid token returns 401", + "Test expired token returns 401", + "Test missing token returns 401" + ], + "commands": [ + "npm test -- --grep 'auth middleware'", + "npm run test:coverage -- src/middleware/auth.ts" + ], + "coverage_target": 80 + }, + + "regression": [ + "npm test -- --grep 'protected routes'", + "npm run test:integration -- auth" ], + + "acceptance": { + "criteria": [ + "Middleware validates JWT tokens successfully", + "Returns 401 for invalid or missing tokens", + "Passes decoded token to request context" + ], + "verification": [ + "curl -H 'Authorization: Bearer valid_token' /api/protected → 200", + "curl /api/protected → 401", + "curl -H 'Authorization: Bearer invalid' /api/protected → 401" + ] + }, + + "commit": { + "type": "feat", + "scope": "auth", + "message_template": "feat(auth): add JWT validation middleware\n\n- Implement token validation\n- Add error handling for invalid tokens\n- Export for route protection", + "breaking": false + }, + "depends_on": [], - "estimated_minutes": 30 + "estimated_minutes": 30, + "executor": "codex" } ], "exploration_context": { diff --git a/.claude/commands/issue/queue.md b/.claude/commands/issue/queue.md index fd78e1c5..cbdc9f1d 100644 --- a/.claude/commands/issue/queue.md +++ b/.claude/commands/issue/queue.md @@ -20,30 +20,67 @@ Queue formation command using **issue-queue-agent** that analyzes all bound solu - Parallel/Sequential group assignment - Output global queue.json -## Storage Structure (Flat JSONL) +## Storage Structure (Queue History) ``` .workflow/issues/ ├── issues.jsonl # All issues (one per line) -├── queue.json # Execution queue (output) +├── 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", + "queues": [ + { + "id": "QUE-20251227-143000", + "status": "active", + "issue_ids": ["GH-123", "GH-124"], + "total_tasks": 8, + "completed_tasks": 3, + "created_at": "2025-12-27T14:30:00Z" + }, + { + "id": "QUE-20251226-100000", + "status": "completed", + "issue_ids": ["GH-120"], + "total_tasks": 5, + "completed_tasks": 5, + "created_at": "2025-12-26T10:00:00Z", + "completed_at": "2025-12-26T12:30:00Z" + } + ] +} +``` + ## Usage ```bash /issue:queue [FLAGS] # Examples -/issue:queue # Form queue from all bound solutions -/issue:queue --rebuild # Rebuild queue (clear and regenerate) -/issue:queue --issue GH-123 # Add only specific issue to queue +/issue:queue # Form NEW queue from all bound solutions +/issue:queue --issue GH-123 # Form queue for specific issue only +/issue:queue --append GH-124 # Append to active queue +/issue:queue --list # List all queues (history) +/issue:queue --switch QUE-xxx # Switch active queue +/issue:queue --archive # Archive completed active queue # Flags ---rebuild Clear existing queue and regenerate ---issue Add only specific issue's tasks +--issue Form queue for specific issue only +--append Append issue to active queue (don't create new) +--list List all queues with status +--switch Switch active queue +--archive Archive current queue (mark completed) +--clear Delete a queue from history ``` ## Execution Process @@ -215,10 +252,15 @@ ${(queueOutput.execution_groups || []).map(g => { ## Queue Schema -Output `queue.json`: +Output `queues/{queue-id}.json`: ```json { + "id": "QUE-20251227-143000", + "name": "Auth Feature Queue", + "status": "active", + "issue_ids": ["GH-123", "GH-124"], + "queue": [ { "queue_id": "Q-001", @@ -233,6 +275,7 @@ Output `queue.json`: "queued_at": "2025-12-26T10:00:00Z" } ], + "conflicts": [ { "type": "file_conflict", @@ -244,24 +287,32 @@ Output `queue.json`: "resolved": true } ], + "execution_groups": [ { "id": "P1", "type": "parallel", "task_count": 3, "tasks": ["GH-123:T1", "GH-124:T1", "GH-125:T1"] }, { "id": "S2", "type": "sequential", "task_count": 2, "tasks": ["GH-123:T2", "GH-124:T2"] } ], + "_metadata": { "version": "2.0", - "storage": "jsonl", "total_tasks": 5, - "total_conflicts": 1, - "resolved_conflicts": 1, - "parallel_groups": 1, - "sequential_groups": 1, - "timestamp": "2025-12-26T10:00:00Z", + "pending_count": 3, + "completed_count": 2, + "failed_count": 0, + "created_at": "2025-12-26T10:00:00Z", + "updated_at": "2025-12-26T11:00:00Z", "source": "issue-queue-agent" } } ``` +### Queue ID Format + +``` +QUE-YYYYMMDD-HHMMSS +例如: QUE-20251227-143052 +``` + ## Semantic Priority Rules | Factor | Priority Boost | diff --git a/ccw/src/commands/issue.ts b/ccw/src/commands/issue.ts index e60548ab..2bfc9de6 100644 --- a/ccw/src/commands/issue.ts +++ b/ccw/src/commands/issue.ts @@ -36,6 +36,26 @@ interface Issue { completed_at?: string; } +interface TaskTest { + unit?: string[]; // Unit test requirements + integration?: string[]; // Integration test requirements + commands?: string[]; // Test commands to run + coverage_target?: number; // Minimum coverage % (optional) +} + +interface TaskAcceptance { + criteria: string[]; // Acceptance criteria (testable) + verification: string[]; // How to verify each criterion + manual_checks?: string[]; // Manual verification steps if needed +} + +interface TaskCommit { + type: 'feat' | 'fix' | 'refactor' | 'test' | 'docs' | 'chore'; + scope: string; // Commit scope (e.g., "auth", "api") + message_template: string; // Commit message template + breaking?: boolean; // Breaking change flag +} + interface SolutionTask { id: string; title: string; @@ -43,11 +63,26 @@ interface SolutionTask { action: string; description?: string; modification_points?: { file: string; target: string; change: string }[]; - implementation: string[]; - acceptance: string[]; + + // Lifecycle phases (closed-loop) + implementation: string[]; // Implementation steps + test: TaskTest; // Test requirements + regression: string[]; // Regression check points + acceptance: TaskAcceptance; // Acceptance criteria & verification + commit: TaskCommit; // Commit specification + depends_on: string[]; estimated_minutes?: number; executor: 'codex' | 'gemini' | 'agent' | 'auto'; + + // Lifecycle status tracking + lifecycle_status?: { + implemented: boolean; + tested: boolean; + regression_passed: boolean; + accepted: boolean; + committed: boolean; + }; status?: string; priority?: number; } @@ -83,8 +118,13 @@ interface QueueItem { } interface Queue { + id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS + name?: string; // Optional queue name + status: 'active' | 'completed' | 'archived' | 'failed'; + issue_ids: string[]; // Issues in this queue queue: QueueItem[]; conflicts: any[]; + execution_groups?: any[]; _metadata: { version: string; total_tasks: number; @@ -92,10 +132,24 @@ interface Queue { executing_count: number; completed_count: number; failed_count: number; - last_updated: string; + created_at: string; + updated_at: string; }; } +interface QueueIndex { + active_queue_id: string | null; + queues: { + id: string; + status: string; + issue_ids: string[]; + total_tasks: number; + completed_tasks: number; + created_at: string; + completed_at?: string; + }[]; +} + interface IssueOptions { status?: string; title?: string; @@ -208,40 +262,121 @@ function generateSolutionId(): string { return `SOL-${ts}`; } -// ============ Queue JSON ============ +// ============ Queue Management (Multi-Queue) ============ -function readQueue(): Queue { - const path = join(getIssuesDir(), 'queue.json'); +function getQueuesDir(): string { + return join(getIssuesDir(), 'queues'); +} + +function ensureQueuesDir(): void { + const dir = getQueuesDir(); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } +} + +function readQueueIndex(): QueueIndex { + const path = join(getQueuesDir(), 'index.json'); if (!existsSync(path)) { - return { - queue: [], - conflicts: [], - _metadata: { - version: '2.0', - total_tasks: 0, - pending_count: 0, - executing_count: 0, - completed_count: 0, - failed_count: 0, - last_updated: new Date().toISOString() - } - }; + return { active_queue_id: null, queues: [] }; } return JSON.parse(readFileSync(path, 'utf-8')); } +function writeQueueIndex(index: QueueIndex): void { + ensureQueuesDir(); + writeFileSync(join(getQueuesDir(), 'index.json'), JSON.stringify(index, null, 2), 'utf-8'); +} + +function generateQueueFileId(): string { + const now = new Date(); + const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14); + return `QUE-${ts}`; +} + +function readQueue(queueId?: string): Queue | null { + const index = readQueueIndex(); + const targetId = queueId || index.active_queue_id; + + if (!targetId) return null; + + const path = join(getQueuesDir(), `${targetId}.json`); + if (!existsSync(path)) return null; + + return JSON.parse(readFileSync(path, 'utf-8')); +} + +function readActiveQueue(): Queue { + const queue = readQueue(); + if (queue) return queue; + + // Return empty queue structure if no active queue + return createEmptyQueue(); +} + +function createEmptyQueue(): Queue { + return { + id: generateQueueFileId(), + status: 'active', + issue_ids: [], + queue: [], + conflicts: [], + _metadata: { + version: '2.0', + total_tasks: 0, + pending_count: 0, + executing_count: 0, + completed_count: 0, + failed_count: 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + }; +} + function writeQueue(queue: Queue): void { - ensureIssuesDir(); + ensureQueuesDir(); + + // Update metadata counts queue._metadata.total_tasks = queue.queue.length; queue._metadata.pending_count = queue.queue.filter(q => q.status === 'pending').length; queue._metadata.executing_count = queue.queue.filter(q => q.status === 'executing').length; queue._metadata.completed_count = queue.queue.filter(q => q.status === 'completed').length; queue._metadata.failed_count = queue.queue.filter(q => q.status === 'failed').length; - queue._metadata.last_updated = new Date().toISOString(); - writeFileSync(join(getIssuesDir(), 'queue.json'), JSON.stringify(queue, null, 2), 'utf-8'); + queue._metadata.updated_at = new Date().toISOString(); + + // Write queue file + const path = join(getQueuesDir(), `${queue.id}.json`); + writeFileSync(path, JSON.stringify(queue, null, 2), 'utf-8'); + + // Update index + const index = readQueueIndex(); + const existingIdx = index.queues.findIndex(q => q.id === queue.id); + + const indexEntry = { + id: queue.id, + status: queue.status, + issue_ids: queue.issue_ids, + total_tasks: queue._metadata.total_tasks, + completed_tasks: queue._metadata.completed_count, + created_at: queue._metadata.created_at, + completed_at: queue.status === 'completed' ? new Date().toISOString() : undefined + }; + + if (existingIdx >= 0) { + index.queues[existingIdx] = indexEntry; + } else { + index.queues.unshift(indexEntry); + } + + if (queue.status === 'active') { + index.active_queue_id = queue.id; + } + + writeQueueIndex(index); } -function generateQueueId(queue: Queue): string { +function generateQueueItemId(queue: Queue): string { const maxNum = queue.queue.reduce((max, q) => { const match = q.queue_id.match(/^Q-(\d+)$/); return match ? Math.max(max, parseInt(match[1])) : max; @@ -379,17 +514,19 @@ async function listAction(issueId: string | undefined, options: IssueOptions): P async function statusAction(issueId: string | undefined, options: IssueOptions): Promise { if (!issueId) { // Show queue status - const queue = readQueue(); + const queue = readActiveQueue(); const issues = readIssues(); + const index = readQueueIndex(); if (options.json) { - console.log(JSON.stringify({ queue: queue._metadata, issues: issues.length }, null, 2)); + console.log(JSON.stringify({ queue: queue._metadata, issues: issues.length, queues: index.queues.length }, null, 2)); return; } console.log(chalk.bold.cyan('\nSystem Status\n')); console.log(`Issues: ${issues.length}`); - console.log(`Queue: ${queue._metadata.total_tasks} tasks`); + console.log(`Queues: ${index.queues.length} (Active: ${index.active_queue_id || 'none'})`); + console.log(`Active Queue: ${queue._metadata.total_tasks} tasks`); console.log(` Pending: ${queue._metadata.pending_count}`); console.log(` Executing: ${queue._metadata.executing_count}`); console.log(` Completed: ${queue._metadata.completed_count}`); @@ -497,7 +634,20 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine action: 'Implement', description: options.description || options.title, implementation: [], - acceptance: ['Task completed successfully'], + test: { + unit: [], + commands: ['npm test'] + }, + regression: ['npm test'], + acceptance: { + criteria: ['Task completed successfully'], + verification: ['Manual verification'] + }, + commit: { + type: 'feat', + scope: 'core', + message_template: `feat(core): ${options.title}` + }, depends_on: [], executor: (options.executor as any) || 'auto' }; @@ -590,13 +740,90 @@ async function bindAction(issueId: string | undefined, solutionId: string | unde } /** - * queue - Queue management (list / add) + * queue - Queue management (list / add / history) */ async function queueAction(subAction: string | undefined, issueId: string | undefined, options: IssueOptions): Promise { - const queue = readQueue(); + // List all queues (history) + if (subAction === 'list' || subAction === 'history') { + const index = readQueueIndex(); + if (options.json) { + console.log(JSON.stringify(index, null, 2)); + return; + } + + console.log(chalk.bold.cyan('\nQueue History\n')); + console.log(chalk.gray(`Active: ${index.active_queue_id || 'none'}`)); + console.log(); + + if (index.queues.length === 0) { + console.log(chalk.yellow('No queues found')); + console.log(chalk.gray('Create one: ccw issue queue add ')); + return; + } + + console.log(chalk.gray('ID'.padEnd(22) + 'Status'.padEnd(12) + 'Tasks'.padEnd(10) + 'Issues')); + console.log(chalk.gray('-'.repeat(70))); + + for (const q of index.queues) { + const statusColor = { + 'active': chalk.green, + 'completed': chalk.cyan, + 'archived': chalk.gray, + 'failed': chalk.red + }[q.status] || chalk.white; + + const marker = q.id === index.active_queue_id ? '→ ' : ' '; + console.log( + marker + + q.id.padEnd(20) + + statusColor(q.status.padEnd(12)) + + `${q.completed_tasks}/${q.total_tasks}`.padEnd(10) + + q.issue_ids.join(', ') + ); + } + return; + } + + // Switch active queue + if (subAction === 'switch' && issueId) { + const queueId = issueId; // issueId is actually queue ID here + const targetQueue = readQueue(queueId); + + if (!targetQueue) { + console.error(chalk.red(`Queue "${queueId}" not found`)); + process.exit(1); + } + + const index = readQueueIndex(); + index.active_queue_id = queueId; + writeQueueIndex(index); + + console.log(chalk.green(`✓ Switched to queue ${queueId}`)); + return; + } + + // Archive current queue + if (subAction === 'archive') { + const queue = readActiveQueue(); + if (!queue.id || queue.queue.length === 0) { + console.log(chalk.yellow('No active queue to archive')); + return; + } + + queue.status = 'archived'; + writeQueue(queue); + + const index = readQueueIndex(); + index.active_queue_id = null; + writeQueueIndex(index); + + console.log(chalk.green(`✓ Archived queue ${queue.id}`)); + return; + } + + // Add issue tasks to queue if (subAction === 'add' && issueId) { - // Add issue tasks to queue const issue = findIssue(issueId); if (!issue) { console.error(chalk.red(`Issue "${issueId}" not found`)); @@ -610,13 +837,27 @@ async function queueAction(subAction: string | undefined, issueId: string | unde process.exit(1); } + // Get or create active queue (create new if current is completed/archived) + let queue = readActiveQueue(); + const isNewQueue = queue.queue.length === 0 || queue.status !== 'active'; + + if (queue.status !== 'active') { + // Create new queue if current is not active + queue = createEmptyQueue(); + } + + // Add issue to queue's issue list + if (!queue.issue_ids.includes(issueId)) { + queue.issue_ids.push(issueId); + } + let added = 0; for (const task of solution.tasks) { const exists = queue.queue.some(q => q.issue_id === issueId && q.task_id === task.id); if (exists) continue; queue.queue.push({ - queue_id: generateQueueId(queue), + queue_id: generateQueueItemId(queue), issue_id: issueId, solution_id: solution.id, task_id: task.id, @@ -637,26 +878,35 @@ async function queueAction(subAction: string | undefined, issueId: string | unde writeQueue(queue); updateIssue(issueId, { status: 'queued', queued_at: new Date().toISOString() }); - console.log(chalk.green(`✓ Added ${added} tasks to queue from ${solution.id}`)); + if (isNewQueue) { + console.log(chalk.green(`✓ Created queue ${queue.id}`)); + } + console.log(chalk.green(`✓ Added ${added} tasks from ${solution.id}`)); return; } - // List queue + // Show current queue + const queue = readActiveQueue(); + if (options.json) { console.log(JSON.stringify(queue, null, 2)); return; } - console.log(chalk.bold.cyan('\nExecution Queue\n')); - console.log(chalk.gray(`Total: ${queue._metadata.total_tasks} | Pending: ${queue._metadata.pending_count} | Executing: ${queue._metadata.executing_count} | Completed: ${queue._metadata.completed_count}`)); - console.log(); + console.log(chalk.bold.cyan('\nActive Queue\n')); - if (queue.queue.length === 0) { - console.log(chalk.yellow('Queue is empty')); - console.log(chalk.gray('Add tasks: ccw issue queue add ')); + if (!queue.id || queue.queue.length === 0) { + console.log(chalk.yellow('No active queue')); + console.log(chalk.gray('Create one: ccw issue queue add ')); + console.log(chalk.gray('Or list history: ccw issue queue list')); return; } + console.log(chalk.gray(`Queue: ${queue.id}`)); + console.log(chalk.gray(`Issues: ${queue.issue_ids.join(', ')}`)); + console.log(chalk.gray(`Total: ${queue._metadata.total_tasks} | Pending: ${queue._metadata.pending_count} | Executing: ${queue._metadata.executing_count} | Completed: ${queue._metadata.completed_count}`)); + console.log(); + console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor')); console.log(chalk.gray('-'.repeat(60))); @@ -684,7 +934,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde * next - Get next ready task for execution (JSON output) */ async function nextAction(options: IssueOptions): Promise { - const queue = readQueue(); + const queue = readActiveQueue(); // Find ready tasks const readyTasks = queue.queue.filter(item => { @@ -749,7 +999,7 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P process.exit(1); } - const queue = readQueue(); + const queue = readActiveQueue(); const idx = queue.queue.findIndex(q => q.queue_id === queueId); if (idx === -1) { @@ -771,31 +1021,49 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P } } - writeQueue(queue); - // Check if all issue tasks are complete const issueId = queue.queue[idx].issue_id; const issueTasks = queue.queue.filter(q => q.issue_id === issueId); - const allComplete = issueTasks.every(q => q.status === 'completed'); - const anyFailed = issueTasks.some(q => q.status === 'failed'); + const allIssueComplete = issueTasks.every(q => q.status === 'completed'); + const anyIssueFailed = issueTasks.some(q => q.status === 'failed'); - if (allComplete) { + if (allIssueComplete) { updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() }); console.log(chalk.green(`✓ ${queueId} completed`)); console.log(chalk.green(`✓ Issue ${issueId} completed (all tasks done)`)); - } else if (anyFailed) { + } else if (anyIssueFailed) { updateIssue(issueId, { status: 'failed' }); console.log(chalk.red(`✗ ${queueId} failed`)); } else { console.log(isFail ? chalk.red(`✗ ${queueId} failed`) : chalk.green(`✓ ${queueId} completed`)); } + + // Check if entire queue is complete + const allQueueComplete = queue.queue.every(q => q.status === 'completed'); + const anyQueueFailed = queue.queue.some(q => q.status === 'failed'); + + if (allQueueComplete) { + queue.status = 'completed'; + console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all tasks done)`)); + } else if (anyQueueFailed && queue.queue.every(q => q.status === 'completed' || q.status === 'failed')) { + queue.status = 'failed'; + console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`)); + } + + writeQueue(queue); } /** * retry - Retry failed tasks */ async function retryAction(issueId: string | undefined, options: IssueOptions): Promise { - const queue = readQueue(); + const queue = readActiveQueue(); + + if (!queue.id || queue.queue.length === 0) { + console.log(chalk.yellow('No active queue')); + return; + } + let updated = 0; for (const item of queue.queue) { @@ -815,6 +1083,11 @@ async function retryAction(issueId: string | undefined, options: IssueOptions): return; } + // Reset queue status if it was failed + if (queue.status === 'failed') { + queue.status = 'active'; + } + writeQueue(queue); if (issueId) { @@ -873,7 +1146,7 @@ export async function issueCommand( await doneAction(argsArray[0], { ...options, fail: true }); break; default: - console.log(chalk.bold.cyan('\nCCW Issue Management (v2.0 - Unified JSONL)\n')); + console.log(chalk.bold.cyan('\nCCW Issue Management (v3.0 - Multi-Queue + Lifecycle)\n')); console.log(chalk.bold('Core Commands:')); console.log(chalk.gray(' init Initialize new issue')); console.log(chalk.gray(' list [issue-id] List issues or tasks')); @@ -882,8 +1155,11 @@ export async function issueCommand( console.log(chalk.gray(' bind [sol-id] Bind solution (--solution to register)')); console.log(); console.log(chalk.bold('Queue Commands:')); - console.log(chalk.gray(' queue [list] Show execution queue')); - console.log(chalk.gray(' queue add Add bound solution tasks to queue')); + console.log(chalk.gray(' queue Show active queue')); + console.log(chalk.gray(' queue list List all queues (history)')); + console.log(chalk.gray(' queue add Add issue to active queue (or create new)')); + console.log(chalk.gray(' queue switch Switch active queue')); + console.log(chalk.gray(' queue archive Archive current queue')); console.log(chalk.gray(' retry [issue-id] Retry failed tasks')); console.log(); console.log(chalk.bold('Execution Endpoints:')); @@ -902,6 +1178,7 @@ export async function issueCommand( console.log(chalk.bold('Storage:')); console.log(chalk.gray(' .workflow/issues/issues.jsonl All issues')); console.log(chalk.gray(' .workflow/issues/solutions/*.jsonl Solutions per issue')); - console.log(chalk.gray(' .workflow/issues/queue.json Execution queue')); + console.log(chalk.gray(' .workflow/issues/queues/ Queue files (multi-queue)')); + console.log(chalk.gray(' .workflow/issues/queues/index.json Queue index')); } } diff --git a/ccw/src/core/routes/issue-routes.ts b/ccw/src/core/routes/issue-routes.ts index 80848bf8..b3ab0f78 100644 --- a/ccw/src/core/routes/issue-routes.ts +++ b/ccw/src/core/routes/issue-routes.ts @@ -72,21 +72,68 @@ function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[ } function readQueue(issuesDir: string) { - const queuePath = join(issuesDir, 'queue.json'); - if (!existsSync(queuePath)) { - return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } }; + // Try new multi-queue structure first + const queuesDir = join(issuesDir, 'queues'); + const indexPath = join(queuesDir, 'index.json'); + + if (existsSync(indexPath)) { + try { + const index = JSON.parse(readFileSync(indexPath, 'utf8')); + const activeQueueId = index.active_queue_id; + + if (activeQueueId) { + const queueFilePath = join(queuesDir, `${activeQueueId}.json`); + if (existsSync(queueFilePath)) { + return JSON.parse(readFileSync(queueFilePath, 'utf8')); + } + } + } catch { + // Fall through to legacy check + } } - try { - return JSON.parse(readFileSync(queuePath, 'utf8')); - } catch { - return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } }; + + // Fallback to legacy queue.json + const legacyQueuePath = join(issuesDir, 'queue.json'); + if (existsSync(legacyQueuePath)) { + try { + return JSON.parse(readFileSync(legacyQueuePath, 'utf8')); + } catch { + // Return empty queue + } } + + return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } }; } function writeQueue(issuesDir: string, queue: any) { if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true }); - queue._metadata = { ...queue._metadata, last_updated: new Date().toISOString(), total_tasks: queue.queue?.length || 0 }; - writeFileSync(join(issuesDir, 'queue.json'), JSON.stringify(queue, null, 2)); + queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.queue?.length || 0 }; + + // Check if using new multi-queue structure + const queuesDir = join(issuesDir, 'queues'); + const indexPath = join(queuesDir, 'index.json'); + + if (existsSync(indexPath) && queue.id) { + // Write to new structure + const queueFilePath = join(queuesDir, `${queue.id}.json`); + writeFileSync(queueFilePath, JSON.stringify(queue, null, 2)); + + // Update index metadata + try { + const index = JSON.parse(readFileSync(indexPath, 'utf8')); + const queueEntry = index.queues?.find((q: any) => q.id === queue.id); + if (queueEntry) { + queueEntry.total_tasks = queue.queue?.length || 0; + queueEntry.completed_tasks = queue.queue?.filter((i: any) => i.status === 'completed').length || 0; + writeFileSync(indexPath, JSON.stringify(index, null, 2)); + } + } catch { + // Ignore index update errors + } + } else { + // Fallback to legacy queue.json + writeFileSync(join(issuesDir, 'queue.json'), JSON.stringify(queue, null, 2)); + } } function getIssueDetail(issuesDir: string, issueId: string) { diff --git a/ccw/src/templates/dashboard-css/32-issue-manager.css b/ccw/src/templates/dashboard-css/32-issue-manager.css index e2d632cd..a47de4a1 100644 --- a/ccw/src/templates/dashboard-css/32-issue-manager.css +++ b/ccw/src/templates/dashboard-css/32-issue-manager.css @@ -276,9 +276,105 @@ color: hsl(var(--muted-foreground)); } -.queue-empty svg { +.queue-empty svg, +.queue-empty > i { margin-bottom: 1rem; opacity: 0.5; + color: hsl(var(--muted-foreground)); +} + +.queue-empty-container { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; +} + +.queue-empty-title { + font-size: 1.125rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 0.5rem; +} + +.queue-empty-hint { + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); + margin-bottom: 1.5rem; +} + +.queue-create-btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.625rem 1.25rem; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.queue-create-btn:hover { + background: hsl(var(--primary) / 0.9); + transform: translateY(-1px); +} + +/* Queue Toolbar */ +.queue-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem 1rem; + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; +} + +.queue-stats { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.queue-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Command Box */ +.command-option { + margin-bottom: 0.75rem; +} + +.command-box { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem; + background: hsl(var(--muted) / 0.5); + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; +} + +.command-text { + flex: 1; + font-family: var(--font-mono); + font-size: 0.875rem; + color: hsl(var(--foreground)); + word-break: break-all; +} + +.command-info { + padding: 0.75rem; + background: hsl(var(--primary) / 0.05); + border-radius: 0.375rem; + border-left: 3px solid hsl(var(--primary)); } /* Issue ID */ @@ -1349,6 +1445,20 @@ cursor: pointer; } +/* Input with action button */ +.input-with-action { + display: flex; + gap: 0.5rem; +} + +.input-with-action input { + flex: 1; +} + +.input-with-action .btn-icon { + flex-shrink: 0; +} + /* ========================================== BUTTON STYLES ========================================== */ @@ -1759,3 +1869,676 @@ -webkit-overflow-scrolling: touch; } } + +/* ========================================== + SOLUTION DETAIL MODAL + ========================================== */ + +.solution-modal { + position: fixed; + inset: 0; + z-index: 1100; + display: flex; + align-items: center; + justify-content: center; +} + +.solution-modal.hidden { + display: none; +} + +.solution-modal-backdrop { + position: absolute; + inset: 0; + background: hsl(var(--foreground) / 0.6); + animation: fadeIn 0.15s ease-out; +} + +.solution-modal-content { + position: relative; + width: 90%; + max-width: 720px; + max-height: 85vh; + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 0.75rem; + box-shadow: 0 25px 50px hsl(var(--foreground) / 0.2); + display: flex; + flex-direction: column; + animation: modalSlideIn 0.2s ease-out; +} + +.solution-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + border-bottom: 1px solid hsl(var(--border)); + flex-shrink: 0; +} + +.solution-modal-title h3 { + font-size: 1.125rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin-top: 0.25rem; +} + +.solution-modal-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.solution-modal-body { + flex: 1; + overflow-y: auto; + padding: 1.25rem; +} + +/* Solution Overview Stats */ +.solution-overview { + display: flex; + gap: 1.5rem; + padding: 1rem; + background: hsl(var(--muted) / 0.3); + border-radius: 0.5rem; + margin-bottom: 1rem; +} + +.solution-stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; +} + +.solution-stat-value { + font-size: 1.5rem; + font-weight: 700; + color: hsl(var(--foreground)); +} + +.solution-stat-label { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Solution Detail Section */ +.solution-detail-section { + margin-bottom: 1.5rem; +} + +.solution-detail-section:last-child { + margin-bottom: 0; +} + +.solution-detail-section-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 0.75rem; +} + +/* Solution Tasks Detail */ +.solution-tasks-detail { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.solution-task-card { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + overflow: hidden; +} + +.solution-task-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: hsl(var(--muted) / 0.3); + cursor: pointer; + transition: background 0.15s ease; +} + +.solution-task-header:hover { + background: hsl(var(--muted) / 0.5); +} + +.solution-task-info { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.solution-task-index { + font-size: 0.75rem; + font-weight: 600; + color: hsl(var(--muted-foreground)); + min-width: 1.5rem; +} + +.solution-task-id { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +.task-expand-icon { + transition: transform 0.2s ease; + color: hsl(var(--muted-foreground)); +} + +.solution-task-title { + padding: 0.75rem 1rem; + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--foreground)); + border-top: 1px solid hsl(var(--border) / 0.5); +} + +.solution-task-details { + padding: 0.75rem 1rem; + border-top: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.15); +} + +.solution-task-scope { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; + padding: 0.5rem; + background: hsl(var(--primary) / 0.1); + border-radius: 0.375rem; +} + +.solution-task-scope-label { + font-size: 0.75rem; + font-weight: 500; + color: hsl(var(--muted-foreground)); +} + +.solution-task-subtitle { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.75rem; + font-weight: 600; + color: hsl(var(--muted-foreground)); + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.025em; +} + +.solution-task-mod-points, +.solution-task-impl-steps, +.solution-task-acceptance, +.solution-task-deps { + margin-bottom: 0.75rem; +} + +.solution-task-list, +.solution-impl-list, +.solution-acceptance-list { + margin: 0; + padding-left: 1.25rem; + font-size: 0.8125rem; + color: hsl(var(--foreground)); +} + +.solution-task-list li, +.solution-impl-list li, +.solution-acceptance-list li { + margin: 0.25rem 0; +} + +.solution-mod-point { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.mod-point-file { + color: hsl(var(--primary)); + font-size: 0.8125rem; +} + +.mod-point-change { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + margin-left: 0.5rem; +} + +.solution-deps-list { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; +} + +.solution-dep-tag { + padding: 0.125rem 0.5rem; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + border-radius: 0.25rem; + font-size: 0.75rem; +} + +/* ========================================== + LIFECYCLE PHASE BADGES + ========================================== */ + +.phase-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.25rem; + height: 1.25rem; + border-radius: 9999px; + font-size: 0.6875rem; + font-weight: 600; + margin-right: 0.25rem; +} + +.phase-badge.phase-1 { + background: hsl(217 91% 60% / 0.2); + color: hsl(217 91% 60%); +} + +.phase-badge.phase-2 { + background: hsl(262 83% 58% / 0.2); + color: hsl(262 83% 58%); +} + +.phase-badge.phase-3 { + background: hsl(25 95% 53% / 0.2); + color: hsl(25 95% 53%); +} + +.phase-badge.phase-4 { + background: hsl(142 71% 45% / 0.2); + color: hsl(142 71% 45%); +} + +.phase-badge.phase-5 { + background: hsl(199 89% 48% / 0.2); + color: hsl(199 89% 48%); +} + +/* ========================================== + QUEUE STATS GRID + ========================================== */ + +.queue-stats-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0.75rem; +} + +@media (max-width: 768px) { + .queue-stats-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 480px) { + .queue-stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +.queue-stat-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0.75rem 1rem; + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + text-align: center; +} + +.queue-stat-card .queue-stat-value { + font-size: 1.5rem; + font-weight: 700; + color: hsl(var(--foreground)); + line-height: 1.2; +} + +.queue-stat-card .queue-stat-label { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + text-transform: uppercase; + letter-spacing: 0.025em; + margin-top: 0.25rem; +} + +.queue-stat-card.pending { + border-color: hsl(var(--muted-foreground) / 0.3); +} + +.queue-stat-card.pending .queue-stat-value { + color: hsl(var(--muted-foreground)); +} + +.queue-stat-card.executing { + border-color: hsl(45 93% 47% / 0.5); + background: hsl(45 93% 47% / 0.05); +} + +.queue-stat-card.executing .queue-stat-value { + color: hsl(45 93% 47%); +} + +.queue-stat-card.completed { + border-color: hsl(var(--success) / 0.5); + background: hsl(var(--success) / 0.05); +} + +.queue-stat-card.completed .queue-stat-value { + color: hsl(var(--success)); +} + +.queue-stat-card.failed { + border-color: hsl(var(--destructive) / 0.5); + background: hsl(var(--destructive) / 0.05); +} + +.queue-stat-card.failed .queue-stat-value { + color: hsl(var(--destructive)); +} + +/* ========================================== + QUEUE INFO CARDS + ========================================== */ + +.queue-info-card { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.queue-info-label { + font-size: 0.6875rem; + color: hsl(var(--muted-foreground)); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.queue-info-value { + font-size: 0.875rem; + color: hsl(var(--foreground)); +} + +/* Queue Status Badge */ +.queue-status-badge { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.6875rem; + font-weight: 500; + text-transform: uppercase; +} + +.queue-status-badge.active { + background: hsl(217 91% 60% / 0.15); + color: hsl(217 91% 60%); +} + +.queue-status-badge.completed { + background: hsl(var(--success) / 0.15); + color: hsl(var(--success)); +} + +.queue-status-badge.failed { + background: hsl(var(--destructive) / 0.15); + color: hsl(var(--destructive)); +} + +.queue-status-badge.archived { + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +/* ========================================== + SOLUTION TASK SECTIONS + ========================================== */ + +.solution-task-section { + margin-bottom: 0.75rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid hsl(var(--border) / 0.3); +} + +.solution-task-section:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +/* ========================================== + TEST SECTION STYLES + ========================================== */ + +.test-subsection, +.acceptance-subsection { + margin-bottom: 0.5rem; +} + +.test-subsection:last-child, +.acceptance-subsection:last-child { + margin-bottom: 0; +} + +.test-label, +.acceptance-label { + display: block; + font-size: 0.6875rem; + font-weight: 500; + color: hsl(var(--muted-foreground)); + text-transform: uppercase; + letter-spacing: 0.025em; + margin-bottom: 0.25rem; +} + +.test-list { + margin: 0; + padding-left: 1.25rem; + font-size: 0.8125rem; + color: hsl(var(--foreground)); +} + +.test-list li { + margin: 0.125rem 0; +} + +.test-commands, +.verification-commands { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.test-command, +.verification-command { + display: block; + padding: 0.375rem 0.625rem; + background: hsl(var(--muted) / 0.5); + border: 1px solid hsl(var(--border)); + border-radius: 0.25rem; + font-family: var(--font-mono); + font-size: 0.75rem; + color: hsl(var(--foreground)); + word-break: break-all; +} + +.coverage-target { + font-size: 0.6875rem; + font-weight: 400; + color: hsl(var(--muted-foreground)); + margin-left: 0.25rem; +} + +/* ========================================== + COMMIT INFO STYLES + ========================================== */ + +.commit-info { + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + padding: 0.625rem; +} + +.commit-type { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.commit-type-badge { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: lowercase; +} + +.commit-type-badge.feat { + background: hsl(142 71% 45% / 0.15); + color: hsl(142 71% 45%); +} + +.commit-type-badge.fix { + background: hsl(0 84% 60% / 0.15); + color: hsl(0 84% 60%); +} + +.commit-type-badge.refactor { + background: hsl(262 83% 58% / 0.15); + color: hsl(262 83% 58%); +} + +.commit-type-badge.test { + background: hsl(199 89% 48% / 0.15); + color: hsl(199 89% 48%); +} + +.commit-type-badge.docs { + background: hsl(45 93% 47% / 0.15); + color: hsl(45 93% 47%); +} + +.commit-type-badge.chore { + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +.commit-scope { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +.commit-breaking { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.375rem; + background: hsl(var(--destructive) / 0.15); + color: hsl(var(--destructive)); + border-radius: 0.25rem; + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.05em; +} + +.commit-message { + margin: 0; + padding: 0.5rem; + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 0.25rem; + font-family: var(--font-mono); + font-size: 0.75rem; + color: hsl(var(--foreground)); + white-space: pre-wrap; + word-break: break-word; +} + +/* Modification Point Target */ +.mod-point-target { + font-size: 0.75rem; + color: hsl(var(--primary)); + font-family: var(--font-mono); +} + +/* JSON Toggle */ +.solution-json-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.75rem 1rem; + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + color: hsl(var(--muted-foreground)); + font-size: 0.875rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.solution-json-toggle:hover { + background: hsl(var(--muted) / 0.5); + color: hsl(var(--foreground)); +} + +.solution-json-content { + margin-top: 0.5rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + overflow: hidden; +} + +.solution-json-pre { + margin: 0; + padding: 1rem; + background: hsl(var(--muted) / 0.3); + font-family: var(--font-mono); + font-size: 0.75rem; + line-height: 1.5; + color: hsl(var(--foreground)); + overflow-x: auto; + white-space: pre-wrap; + word-break: break-all; + max-height: 300px; + overflow-y: auto; +} + +/* Responsive Solution Modal */ +@media (max-width: 640px) { + .solution-modal-content { + max-height: 95vh; + margin: 0.5rem; + } + + .solution-overview { + flex-wrap: wrap; + justify-content: center; + } + + .solution-stat { + min-width: 80px; + } +} diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 4f1ac3eb..b6c0d4d1 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -1772,6 +1772,45 @@ const i18n = { 'issues.created': 'Issue created successfully', 'issues.confirmDelete': 'Are you sure you want to delete this issue?', 'issues.deleted': 'Issue deleted', + 'issues.idAutoGenerated': 'Auto-generated', + 'issues.regenerateId': 'Regenerate ID', + // Solution detail + 'issues.solutionDetail': 'Solution Details', + 'issues.bind': 'Bind', + 'issues.unbind': 'Unbind', + 'issues.bound': 'Bound', + 'issues.totalTasks': 'Total Tasks', + 'issues.bindStatus': 'Bind Status', + 'issues.createdAt': 'Created', + 'issues.taskList': 'Task List', + 'issues.noTasks': 'No tasks in this solution', + 'issues.noSolutions': 'No solutions', + 'issues.viewJson': 'View Raw JSON', + 'issues.scope': 'Scope', + 'issues.modificationPoints': 'Modification Points', + 'issues.implementationSteps': 'Implementation Steps', + 'issues.acceptanceCriteria': 'Acceptance Criteria', + 'issues.dependencies': 'Dependencies', + 'issues.solutionBound': 'Solution bound successfully', + 'issues.solutionUnbound': 'Solution unbound', + // Queue operations + 'issues.queueEmptyHint': 'Generate execution queue from bound solutions', + 'issues.createQueue': 'Create Queue', + 'issues.regenerate': 'Regenerate', + 'issues.regenerateQueue': 'Regenerate Queue', + 'issues.refreshQueue': 'Refresh', + 'issues.executionGroups': 'groups', + 'issues.totalItems': 'items', + 'issues.queueRefreshed': 'Queue refreshed', + 'issues.confirmCreateQueue': 'This will execute /issue:queue command via Claude Code CLI to generate execution queue from bound solutions.\n\nContinue?', + 'issues.creatingQueue': 'Creating execution queue...', + 'issues.queueExecutionStarted': 'Queue generation started', + 'issues.queueCreated': 'Queue created successfully', + 'issues.queueCreationFailed': 'Queue creation failed', + 'issues.queueCommandHint': 'Run one of the following commands in your terminal to generate the execution queue from bound solutions:', + 'issues.queueCommandInfo': 'After running the command, click "Refresh" to see the updated queue.', + 'issues.alternative': 'Alternative', + 'issues.refreshAfter': 'Refresh Queue', // issue.* keys (legacy) 'issue.viewIssues': 'Issues', 'issue.viewQueue': 'Queue', @@ -3595,6 +3634,45 @@ const i18n = { 'issues.created': '议题创建成功', 'issues.confirmDelete': '确定要删除此议题吗?', 'issues.deleted': '议题已删除', + 'issues.idAutoGenerated': '自动生成', + 'issues.regenerateId': '重新生成ID', + // Solution detail + 'issues.solutionDetail': '解决方案详情', + 'issues.bind': '绑定', + 'issues.unbind': '解绑', + 'issues.bound': '已绑定', + 'issues.totalTasks': '任务总数', + 'issues.bindStatus': '绑定状态', + 'issues.createdAt': '创建时间', + 'issues.taskList': '任务列表', + 'issues.noTasks': '此解决方案无任务', + 'issues.noSolutions': '暂无解决方案', + 'issues.viewJson': '查看原始JSON', + 'issues.scope': '作用域', + 'issues.modificationPoints': '修改点', + 'issues.implementationSteps': '实现步骤', + 'issues.acceptanceCriteria': '验收标准', + 'issues.dependencies': '依赖项', + 'issues.solutionBound': '解决方案已绑定', + 'issues.solutionUnbound': '解决方案已解绑', + // Queue operations + 'issues.queueEmptyHint': '从绑定的解决方案生成执行队列', + 'issues.createQueue': '创建队列', + 'issues.regenerate': '重新生成', + 'issues.regenerateQueue': '重新生成队列', + 'issues.refreshQueue': '刷新', + 'issues.executionGroups': '个执行组', + 'issues.totalItems': '个任务', + 'issues.queueRefreshed': '队列已刷新', + 'issues.confirmCreateQueue': '这将通过 Claude Code CLI 执行 /issue:queue 命令,从绑定的解决方案生成执行队列。\n\n是否继续?', + 'issues.creatingQueue': '正在创建执行队列...', + 'issues.queueExecutionStarted': '队列生成已启动', + 'issues.queueCreated': '队列创建成功', + 'issues.queueCreationFailed': '队列创建失败', + 'issues.queueCommandHint': '在终端中运行以下命令之一,从绑定的解决方案生成执行队列:', + 'issues.queueCommandInfo': '运行命令后,点击"刷新"查看更新后的队列。', + 'issues.alternative': '或者', + 'issues.refreshAfter': '刷新队列', // issue.* keys (legacy) 'issue.viewIssues': '议题', 'issue.viewQueue': '队列', diff --git a/ccw/src/templates/dashboard-js/views/hook-manager.js b/ccw/src/templates/dashboard-js/views/hook-manager.js index 45da359e..87983a63 100644 --- a/ccw/src/templates/dashboard-js/views/hook-manager.js +++ b/ccw/src/templates/dashboard-js/views/hook-manager.js @@ -168,16 +168,22 @@ async function loadAvailableSkills() { if (!response.ok) throw new Error('Failed to load skills'); const data = await response.json(); + // Combine project and user skills (API returns { projectSkills: [], userSkills: [] }) + const allSkills = [ + ...(data.projectSkills || []).map(s => ({ ...s, scope: 'project' })), + ...(data.userSkills || []).map(s => ({ ...s, scope: 'user' })) + ]; + const container = document.getElementById('skill-discovery-skill-context'); - if (container && data.skills) { - if (data.skills.length === 0) { + if (container) { + if (allSkills.length === 0) { container.innerHTML = ` ${t('hook.wizard.availableSkills')} ${t('hook.wizard.noSkillsFound').split('.')[0]} `; } else { - const skillBadges = data.skills.map(skill => ` - ${escapeHtml(skill.name)} + const skillBadges = allSkills.map(skill => ` + ${escapeHtml(skill.name)} `).join(''); container.innerHTML = ` ${t('hook.wizard.availableSkills')} @@ -187,7 +193,7 @@ async function loadAvailableSkills() { } // Store skills for wizard use - window.availableSkills = data.skills || []; + window.availableSkills = allSkills; } catch (err) { console.error('Failed to load skills:', err); const container = document.getElementById('skill-discovery-skill-context'); diff --git a/ccw/src/templates/dashboard-js/views/issue-manager.js b/ccw/src/templates/dashboard-js/views/issue-manager.js index bacb1072..d9c43f5d 100644 --- a/ccw/src/templates/dashboard-js/views/issue-manager.js +++ b/ccw/src/templates/dashboard-js/views/issue-manager.js @@ -9,6 +9,7 @@ var issueData = { queue: { queue: [], conflicts: [], execution_groups: [], grouped_items: {} }, selectedIssue: null, selectedSolution: null, + selectedSolutionIssueId: null, statusFilter: 'all', searchQuery: '', viewMode: 'issues' // 'issues' | 'queue' @@ -148,6 +149,31 @@ function renderIssueView() { + + +