From 99a3561f7131571a857981c90d678121e4ebde54 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Fri, 27 Feb 2026 12:25:26 +0800 Subject: [PATCH] feat(workflow): add unified workflow spec command system - Add /workflow:init-specs command for interactive spec creation with scope selection (global/project) - Update /workflow:init to chain solidify and add --skip-specs flag - Add category field support to generated specs frontmatter - Add GET /api/project-tech/stats endpoint for development progress stats - Add devProgressInjection settings to system configuration - Add development progress injection control card to GlobalSettingsTab - Add i18n keys for new settings in en/zh locales --- .claude/commands/workflow/init-guidelines.md | 53 ++- .claude/commands/workflow/init-specs.md | 380 ++++++++++++++++++ .claude/commands/workflow/init.md | 69 +++- .claude/commands/workflow/session/solidify.md | 1 + .claude/commands/workflow/session/sync.md | 6 + .../components/specs/GlobalSettingsTab.tsx | 181 ++++++++- ccw/frontend/src/lib/api.ts | 58 ++- ccw/frontend/src/locales/en/mcp-manager.json | 37 ++ ccw/frontend/src/locales/zh/mcp-manager.json | 37 ++ ccw/src/core/routes/system-routes.ts | 88 ++++ 10 files changed, 877 insertions(+), 33 deletions(-) create mode 100644 .claude/commands/workflow/init-specs.md diff --git a/.claude/commands/workflow/init-guidelines.md b/.claude/commands/workflow/init-guidelines.md index 26423cf7..96abc6ae 100644 --- a/.claude/commands/workflow/init-guidelines.md +++ b/.claude/commands/workflow/init-guidelines.md @@ -322,27 +322,55 @@ AskUserQuestion({ ### Step 4: Write specs/*.md -For each category of collected answers, append rules to the corresponding spec MD file. Each spec file uses YAML frontmatter with `readMode`, `priority`, and `keywords`. +For each category of collected answers, append rules to the corresponding spec MD file. Each spec file uses YAML frontmatter with `readMode`, `priority`, `category`, and `keywords`. + +**Category Assignment**: Based on the round and question type: +- Round 1-2 (conventions): `category: general` (applies to all stages) +- Round 3 (architecture/tech): `category: planning` (planning phase) +- Round 4 (performance/security): `category: execution` (implementation phase) +- Round 5 (quality): `category: execution` (testing phase) ```javascript -// Helper: append rules to a spec MD file -function appendRulesToSpecFile(filePath, rules) { +// Helper: append rules to a spec MD file with category support +function appendRulesToSpecFile(filePath, rules, defaultCategory = 'general') { if (rules.length === 0) return + + // Check if file exists + if (!file_exists(filePath)) { + // Create file with frontmatter including category + const frontmatter = `--- +title: ${filePath.includes('conventions') ? 'Coding Conventions' : filePath.includes('constraints') ? 'Architecture Constraints' : 'Quality Rules'} +readMode: optional +priority: medium +category: ${defaultCategory} +scope: project +dimension: specs +keywords: [${defaultCategory}, ${filePath.includes('conventions') ? 'convention' : filePath.includes('constraints') ? 'constraint' : 'quality'}] +--- + +# ${filePath.includes('conventions') ? 'Coding Conventions' : filePath.includes('constraints') ? 'Architecture Constraints' : 'Quality Rules'} + +` + Write(filePath, frontmatter) + } + const existing = Read(filePath) // Append new rules as markdown list items after existing content const newContent = existing.trimEnd() + '\n' + rules.map(r => `- ${r}`).join('\n') + '\n' Write(filePath, newContent) } -// Write conventions +// Write conventions (general category) appendRulesToSpecFile('.workflow/specs/coding-conventions.md', - [...newCodingStyle, ...newNamingPatterns, ...newFileStructure, ...newDocumentation]) + [...newCodingStyle, ...newNamingPatterns, ...newFileStructure, ...newDocumentation], + 'general') -// Write constraints +// Write constraints (planning category) appendRulesToSpecFile('.workflow/specs/architecture-constraints.md', - [...newArchitecture, ...newTechStack, ...newPerformance, ...newSecurity]) + [...newArchitecture, ...newTechStack, ...newPerformance, ...newSecurity], + 'planning') -// Write quality rules (create file if needed) +// Write quality rules (execution category) if (newQualityRules.length > 0) { const qualityPath = '.workflow/specs/quality-rules.md' if (!file_exists(qualityPath)) { @@ -350,7 +378,10 @@ if (newQualityRules.length > 0) { title: Quality Rules readMode: required priority: high -keywords: [quality, testing, coverage, lint] +category: execution +scope: project +dimension: specs +keywords: [execution, quality, testing, coverage, lint] --- # Quality Rules @@ -358,7 +389,8 @@ keywords: [quality, testing, coverage, lint] `) } appendRulesToSpecFile(qualityPath, - newQualityRules.map(q => `${q.rule} (scope: ${q.scope}, enforced by: ${q.enforced_by})`)) + newQualityRules.map(q => `${q.rule} (scope: ${q.scope}, enforced by: ${q.enforced_by})`), + 'execution') } // Rebuild spec index after writing @@ -411,4 +443,5 @@ When converting user selections to guideline entries: ## Related Commands - `/workflow:init` - Creates scaffold; optionally calls this command +- `/workflow:init-specs` - Interactive wizard to create individual specs with scope selection - `/workflow:session:solidify` - Add individual rules one at a time diff --git a/.claude/commands/workflow/init-specs.md b/.claude/commands/workflow/init-specs.md new file mode 100644 index 00000000..b7cab254 --- /dev/null +++ b/.claude/commands/workflow/init-specs.md @@ -0,0 +1,380 @@ +--- +name: init-specs +description: Interactive wizard to create individual specs or personal constraints with scope selection +argument-hint: "[--scope ] [--dimension ] [--category ]" +examples: + - /workflow:init-specs + - /workflow:init-specs --scope global --dimension personal + - /workflow:init-specs --scope project --dimension specs +--- + +# Workflow Init Specs Command (/workflow:init-specs) + +## Overview + +Interactive wizard for creating individual specs or personal constraints with scope selection. This command provides a guided experience for adding new rules to the spec system. + +**Key Features**: +- Supports both project specs and personal specs +- Scope selection (global vs project) for personal specs +- Category-based organization for workflow stages +- Interactive mode with smart defaults + +## Usage +```bash +/workflow:init-specs # Interactive mode (all prompts) +/workflow:init-specs --scope global # Create global personal spec +/workflow:init-specs --scope project # Create project spec (default) +/workflow:init-specs --dimension specs # Project conventions/constraints +/workflow:init-specs --dimension personal # Personal preferences +/workflow:init-specs --category exploration # Workflow stage category +``` + +## Parameters + +| Parameter | Values | Default | Description | +|-----------|--------|---------|-------------| +| `--scope` | `global`, `project` | `project` | Where to store the spec (only for personal dimension) | +| `--dimension` | `specs`, `personal` | Interactive | Type of spec to create | +| `--category` | `general`, `exploration`, `planning`, `execution` | `general` | Workflow stage category | + +## Execution Process + +``` +Input Parsing: + ├─ Parse --scope (global | project) + ├─ Parse --dimension (specs | personal) + └─ Parse --category (general | exploration | planning | execution) + +Step 1: Gather Requirements (Interactive) + ├─ If dimension not specified → Ask dimension + ├─ If personal + scope not specified → Ask scope + ├─ If category not specified → Ask category + ├─ Ask type (convention | constraint | learning) + └─ Ask content (rule text) + +Step 2: Determine Target File + ├─ specs dimension → .workflow/specs/coding-conventions.md or architecture-constraints.md + └─ personal dimension → ~/.ccw/specs/personal/ or .ccw/specs/personal/ + +Step 3: Write Spec + ├─ Check if file exists, create if needed with proper frontmatter + ├─ Append rule to appropriate section + └─ Run ccw spec rebuild + +Step 4: Display Confirmation +``` + +## Implementation + +### Step 1: Parse Input and Gather Requirements + +```javascript +// Parse arguments +const args = $ARGUMENTS.toLowerCase() +const hasScope = args.includes('--scope') +const hasDimension = args.includes('--dimension') +const hasCategory = args.includes('--category') + +// Extract values from arguments +let scope = hasScope ? args.match(/--scope\s+(\w+)/)?.[1] : null +let dimension = hasDimension ? args.match(/--dimension\s+(\w+)/)?.[1] : null +let category = hasCategory ? args.match(/--category\s+(\w+)/)?.[1] : null + +// Validate values +if (scope && !['global', 'project'].includes(scope)) { + console.log("Invalid scope. Use 'global' or 'project'.") + return +} +if (dimension && !['specs', 'personal'].includes(dimension)) { + console.log("Invalid dimension. Use 'specs' or 'personal'.") + return +} +if (category && !['general', 'exploration', 'planning', 'execution'].includes(category)) { + console.log("Invalid category. Use 'general', 'exploration', 'planning', or 'execution'.") + return +} +``` + +### Step 2: Interactive Questions + +**If dimension not specified**: +```javascript +if (!dimension) { + const dimensionAnswer = AskUserQuestion({ + questions: [{ + question: "What type of spec do you want to create?", + header: "Dimension", + multiSelect: false, + options: [ + { + label: "Project Spec", + description: "Coding conventions, constraints, quality rules for this project (stored in .workflow/specs/)" + }, + { + label: "Personal Spec", + description: "Personal preferences and constraints that follow you across projects (stored in ~/.ccw/specs/personal/ or .ccw/specs/personal/)" + } + ] + }] + }) + dimension = dimensionAnswer.answers["Dimension"] === "Project Spec" ? "specs" : "personal" +} +``` + +**If personal dimension and scope not specified**: +```javascript +if (dimension === 'personal' && !scope) { + const scopeAnswer = AskUserQuestion({ + questions: [{ + question: "Where should this personal spec be stored?", + header: "Scope", + multiSelect: false, + options: [ + { + label: "Global (Recommended)", + description: "Apply to ALL projects (~/.ccw/specs/personal/)" + }, + { + label: "Project-only", + description: "Apply only to this project (.ccw/specs/personal/)" + } + ] + }] + }) + scope = scopeAnswer.answers["Scope"].includes("Global") ? "global" : "project" +} +``` + +**If category not specified**: +```javascript +if (!category) { + const categoryAnswer = AskUserQuestion({ + questions: [{ + question: "Which workflow stage does this spec apply to?", + header: "Category", + multiSelect: false, + options: [ + { + label: "General (Recommended)", + description: "Applies to all stages (default)" + }, + { + label: "Exploration", + description: "Code exploration, analysis, debugging" + }, + { + label: "Planning", + description: "Task planning, requirements gathering" + }, + { + label: "Execution", + description: "Implementation, testing, deployment" + } + ] + }] + }) + const categoryLabel = categoryAnswer.answers["Category"] + category = categoryLabel.includes("General") ? "general" + : categoryLabel.includes("Exploration") ? "exploration" + : categoryLabel.includes("Planning") ? "planning" + : "execution" +} +``` + +**Ask type**: +```javascript +const typeAnswer = AskUserQuestion({ + questions: [{ + question: "What type of rule is this?", + header: "Type", + multiSelect: false, + options: [ + { + label: "Convention", + description: "Coding style preference (e.g., use functional components)" + }, + { + label: "Constraint", + description: "Hard rule that must not be violated (e.g., no direct DB access)" + }, + { + label: "Learning", + description: "Insight or lesson learned (e.g., cache invalidation needs events)" + } + ] + }] +}) +const type = typeAnswer.answers["Type"] +const isConvention = type.includes("Convention") +const isConstraint = type.includes("Constraint") +const isLearning = type.includes("Learning") +``` + +**Ask content**: +```javascript +const contentAnswer = AskUserQuestion({ + questions: [{ + question: "Enter the rule or guideline text:", + header: "Content", + multiSelect: false, + options: [] + }] +}) +const ruleText = contentAnswer.answers["Content"] +``` + +### Step 3: Determine Target File + +```javascript +const path = require('path') +const os = require('os') + +let targetFile: string +let targetDir: string + +if (dimension === 'specs') { + // Project specs + targetDir = '.workflow/specs' + if (isConstraint) { + targetFile = path.join(targetDir, 'architecture-constraints.md') + } else { + targetFile = path.join(targetDir, 'coding-conventions.md') + } +} else { + // Personal specs + if (scope === 'global') { + targetDir = path.join(os.homedir(), '.ccw', 'specs', 'personal') + } else { + targetDir = path.join('.ccw', 'specs', 'personal') + } + + // Create category-based filename + const typePrefix = isConstraint ? 'constraints' : isLearning ? 'learnings' : 'conventions' + targetFile = path.join(targetDir, `${typePrefix}.md`) +} +``` + +### Step 4: Write Spec + +```javascript +const fs = require('fs') + +// Ensure directory exists +if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) +} + +// Check if file exists +const fileExists = fs.existsSync(targetFile) + +if (!fileExists) { + // Create new file with frontmatter + const frontmatter = `--- +title: ${dimension === 'specs' ? 'Project' : 'Personal'} ${isConstraint ? 'Constraints' : isLearning ? 'Learnings' : 'Conventions'} +readMode: optional +priority: medium +category: ${category} +scope: ${dimension === 'personal' ? scope : 'project'} +dimension: ${dimension} +keywords: [${category}, ${isConstraint ? 'constraint' : isLearning ? 'learning' : 'convention'}] +--- + +# ${dimension === 'specs' ? 'Project' : 'Personal'} ${isConstraint ? 'Constraints' : isLearning ? 'Learnings' : 'Conventions'} + +` + fs.writeFileSync(targetFile, frontmatter, 'utf8') +} + +// Read existing content +let content = fs.readFileSync(targetFile, 'utf8') + +// Format the new rule +const timestamp = new Date().toISOString().split('T')[0] +const rulePrefix = isLearning ? `- [learning] ` : `- [${category}] ` +const ruleSuffix = isLearning ? ` (${timestamp})` : '' +const newRule = `${rulePrefix}${ruleText}${ruleSuffix}` + +// Check for duplicate +if (content.includes(ruleText)) { + console.log(` +Rule already exists in ${targetFile} +Text: "${ruleText}" +`) + return +} + +// Append the rule +content = content.trimEnd() + '\n' + newRule + '\n' +fs.writeFileSync(targetFile, content, 'utf8') + +// Rebuild spec index +Bash('ccw spec rebuild') +``` + +### Step 5: Display Confirmation + +``` +Spec created successfully + +Dimension: ${dimension} +Scope: ${dimension === 'personal' ? scope : 'project'} +Category: ${category} +Type: ${type} +Rule: "${ruleText}" + +Location: ${targetFile} + +Use 'ccw spec list' to view all specs +Use 'ccw spec load --category ${category}' to load specs by category +``` + +## Target File Resolution + +### Project Specs (dimension: specs) +``` +.workflow/specs/ +├── coding-conventions.md ← conventions, learnings +├── architecture-constraints.md ← constraints +└── quality-rules.md ← quality rules +``` + +### Personal Specs (dimension: personal) +``` +# Global (~/.ccw/specs/personal/) +~/.ccw/specs/personal/ +├── conventions.md ← personal conventions (all projects) +├── constraints.md ← personal constraints (all projects) +└── learnings.md ← personal learnings (all projects) + +# Project-local (.ccw/specs/personal/) +.ccw/specs/personal/ +├── conventions.md ← personal conventions (this project only) +├── constraints.md ← personal constraints (this project only) +└── learnings.md ← personal learnings (this project only) +``` + +## Category Field Usage + +The `category` field in frontmatter enables filtered loading: + +| Category | Use Case | Example Rules | +|----------|----------|---------------| +| `general` | Applies to all stages | "Use TypeScript strict mode" | +| `exploration` | Code exploration, debugging | "Always trace the call stack before modifying" | +| `planning` | Task planning, requirements | "Break down tasks into 2-hour chunks" | +| `execution` | Implementation, testing | "Run tests after each file modification" | + +## Error Handling + +- **File not writable**: Check permissions, suggest manual creation +- **Duplicate rule**: Warn and skip (don't add duplicates) +- **Invalid path**: Exit with error message + +## Related Commands + +- `/workflow:init` - Initialize project with specs scaffold +- `/workflow:init-guidelines` - Interactive wizard to fill specs +- `/workflow:session:solidify` - Add rules during/after sessions +- `ccw spec list` - View all specs +- `ccw spec load --category ` - Load filtered specs diff --git a/.claude/commands/workflow/init.md b/.claude/commands/workflow/init.md index 82a85905..29dbc636 100644 --- a/.claude/commands/workflow/init.md +++ b/.claude/commands/workflow/init.md @@ -1,10 +1,11 @@ --- name: init description: Initialize project-level state with intelligent project analysis using cli-explore-agent -argument-hint: "[--regenerate]" +argument-hint: "[--regenerate] [--skip-specs]" examples: - /workflow:init - /workflow:init --regenerate + - /workflow:init --skip-specs --- # Workflow Init Command (/workflow:init) @@ -22,13 +23,15 @@ Initialize `.workflow/project-tech.json` and `.workflow/specs/*.md` with compreh ```bash /workflow:init # Initialize (skip if exists) /workflow:init --regenerate # Force regeneration +/workflow:init --skip-specs # Initialize project-tech only, skip spec initialization ``` ## Execution Process ``` Input Parsing: - └─ Parse --regenerate flag → regenerate = true | false + ├─ Parse --regenerate flag → regenerate = true | false + └─ Parse --skip-specs flag → skipSpecs = true | false Decision: ├─ BOTH_EXIST + no --regenerate → Exit: "Already initialized" @@ -42,27 +45,30 @@ Analysis Flow: │ ├─ Semantic analysis (Gemini CLI) │ ├─ Synthesis and merge │ └─ Write .workflow/project-tech.json - ├─ Create guidelines scaffold (if not exists) - │ └─ Write .workflow/specs/*.md (empty structure) - ├─ Display summary - └─ Ask about guidelines configuration - ├─ If guidelines empty → Ask user: "Configure now?" or "Skip" - │ ├─ Configure now → Skill(skill="workflow:init-guidelines") - │ └─ Skip → Show next steps - └─ If guidelines populated → Show next steps only + ├─ Spec Initialization (if not --skip-specs) + │ ├─ Check if specs/*.md exist + │ ├─ If NOT_FOUND → Run ccw spec init + │ ├─ Run ccw spec rebuild + │ └─ Ask about guidelines configuration + │ ├─ If guidelines empty → Ask user: "Configure now?" or "Skip" + │ │ ├─ Configure now → Skill(skill="workflow:init-guidelines") + │ │ └─ Skip → Show next steps + │ └─ If guidelines populated → Show next steps only + └─ Display summary Output: ├─ .workflow/project-tech.json (+ .backup if regenerate) - └─ .workflow/specs/*.md (scaffold or configured) + └─ .workflow/specs/*.md (scaffold or configured, unless --skip-specs) ``` ## Implementation ### Step 1: Parse Input and Check Existing State -**Parse --regenerate flag**: +**Parse flags**: ```javascript const regenerate = $ARGUMENTS.includes('--regenerate') +const skipSpecs = $ARGUMENTS.includes('--skip-specs') ``` **Check existing state**: @@ -159,13 +165,20 @@ Project root: ${projectRoot} ) ``` -### Step 3.5: Initialize Spec System (if not exists) +### Step 3.5: Initialize Spec System (if not --skip-specs) ```javascript -// Initialize spec system if not already initialized -if (!file_exists('.workflow/specs/coding-conventions.md')) { - Bash('ccw spec init'); - Bash('ccw spec rebuild'); +// Skip spec initialization if --skip-specs flag is provided +if (!skipSpecs) { + // Initialize spec system if not already initialized + const specsCheck = Bash('test -f .workflow/specs/coding-conventions.md && echo EXISTS || echo NOT_FOUND') + if (specsCheck.includes('NOT_FOUND')) { + console.log('Initializing spec system...') + Bash('ccw spec init') + Bash('ccw spec rebuild') + } +} else { + console.log('Skipping spec initialization (--skip-specs)') } ``` @@ -173,7 +186,7 @@ if (!file_exists('.workflow/specs/coding-conventions.md')) { ```javascript const projectTech = JSON.parse(Read('.workflow/project-tech.json')); -const specsInitialized = file_exists('.workflow/specs/coding-conventions.md'); +const specsInitialized = !skipSpecs && file_exists('.workflow/specs/coding-conventions.md'); console.log(` Project initialized successfully @@ -193,16 +206,27 @@ Components: ${projectTech.overview.key_components.length} core modules --- Files created: - Tech analysis: .workflow/project-tech.json -- Specs: .workflow/specs/ ${specsInitialized ? '(initialized)' : ''} +${!skipSpecs ? `- Specs: .workflow/specs/ ${specsInitialized ? '(initialized)' : ''}` : '- Specs: (skipped via --skip-specs)'} ${regenerate ? '- Backup: .workflow/project-tech.json.backup' : ''} `); ``` -### Step 5: Ask About Guidelines Configuration +### Step 5: Ask About Guidelines Configuration (if not --skip-specs) -After displaying the summary, ask the user if they want to configure project guidelines interactively. +After displaying the summary, ask the user if they want to configure project guidelines interactively. Skip this step if `--skip-specs` was provided. ```javascript +// Skip guidelines configuration if --skip-specs was provided +if (skipSpecs) { + console.log(` +Next steps: +- Use /workflow:init-specs to create individual specs +- Use /workflow:init-guidelines to configure specs interactively +- Use /workflow:plan to start planning +`); + return; +} + // Check if specs have user content beyond seed documents const specsList = Bash('ccw spec list --json'); const specsCount = JSON.parse(specsList).total || 0; @@ -233,6 +257,7 @@ if (specsCount <= 5) { } else { console.log(` Next steps: +- Use /workflow:init-specs to create individual specs - Use /workflow:init-guidelines to configure specs interactively - Use ccw spec load to import specs from external sources - Use /workflow:plan to start planning @@ -243,6 +268,7 @@ Next steps: Specs already configured (${specsCount} spec files). Next steps: +- Use /workflow:init-specs to create additional specs - Use /workflow:init-guidelines --reset to reconfigure - Use /workflow:session:solidify to add individual rules - Use /workflow:plan to start planning @@ -258,6 +284,7 @@ Next steps: ## Related Commands +- `/workflow:init-specs` - Interactive wizard to create individual specs with scope selection - `/workflow:init-guidelines` - Interactive wizard to configure project guidelines (called after init) - `/workflow:session:solidify` - Add individual rules/constraints one at a time - `workflow-plan` skill - Start planning with initialized project context diff --git a/.claude/commands/workflow/session/solidify.md b/.claude/commands/workflow/session/solidify.md index b5c781e1..e91bd49a 100644 --- a/.claude/commands/workflow/session/solidify.md +++ b/.claude/commands/workflow/session/solidify.md @@ -450,3 +450,4 @@ This ensures all future planning respects solidified rules without users needing - `/workflow:session:start` - Start a session (may prompt for solidify at end) - `/workflow:session:complete` - Complete session (prompts for learnings to solidify) - `/workflow:init` - Creates specs/*.md scaffold if missing +- `/workflow:init-specs` - Interactive wizard to create individual specs with scope selection diff --git a/.claude/commands/workflow/session/sync.md b/.claude/commands/workflow/session/sync.md index 9f757e08..1868ebf6 100644 --- a/.claude/commands/workflow/session/sync.md +++ b/.claude/commands/workflow/session/sync.md @@ -193,3 +193,9 @@ Write(techPath, JSON.stringify(tech, null, 2)) | No git history | Use user summary or session context only | | No meaningful updates | Skip guidelines, still add tech entry | | Duplicate entry | Skip silently (dedup check in Step 4) | + +## Related Commands + +- `/workflow:init` - Initialize project with specs scaffold +- `/workflow:init-specs` - Interactive wizard to create individual specs with scope selection +- `/workflow:session:solidify` - Add individual rules one at a time diff --git a/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx b/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx index 6b6305b0..f0996e70 100644 --- a/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx +++ b/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx @@ -7,7 +7,7 @@ import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useIntl } from 'react-intl'; import { toast } from 'sonner'; -import { Settings, RefreshCw } from 'lucide-react'; +import { Settings, RefreshCw, History } from 'lucide-react'; import { Card, CardHeader, @@ -18,6 +18,8 @@ import { import { Button } from '@/components/ui/Button'; import { Label } from '@/components/ui/Label'; import { Switch } from '@/components/ui/Switch'; +import { Input } from '@/components/ui/Input'; +import { Badge } from '@/components/ui/Badge'; import { Select, SelectTrigger, @@ -34,6 +36,12 @@ interface PersonalSpecDefaults { autoEnable: boolean; } +interface DevProgressInjection { + enabled: boolean; + maxEntriesPerCategory: number; + categories: ('feature' | 'enhancement' | 'bugfix' | 'refactor' | 'docs')[]; +} + interface SystemSettings { injectionControl: { maxLength: number; @@ -41,6 +49,7 @@ interface SystemSettings { truncateOnExceed: boolean; }; personalSpecDefaults: PersonalSpecDefaults; + devProgressInjection: DevProgressInjection; } interface SpecDimensionStats { @@ -61,6 +70,19 @@ interface SpecStats { }; } +interface ProjectTechStats { + total_features: number; + total_sessions: number; + last_updated: string | null; + categories: { + feature: number; + enhancement: number; + bugfix: number; + refactor: number; + docs: number; + }; +} + // ========== API Functions ========== const API_BASE = '/api'; @@ -103,12 +125,23 @@ async function fetchSpecStats(): Promise { return response.json(); } +async function fetchProjectTechStats(): Promise { + const response = await fetch(`${API_BASE}/project-tech/stats`, { + credentials: 'same-origin', + }); + if (!response.ok) { + throw new Error(`Failed to fetch project-tech stats: ${response.statusText}`); + } + return response.json(); +} + // ========== Query Keys ========== const settingsKeys = { all: ['system-settings'] as const, settings: () => [...settingsKeys.all, 'settings'] as const, stats: () => [...settingsKeys.all, 'stats'] as const, + projectTech: () => [...settingsKeys.all, 'project-tech'] as const, }; // ========== Component ========== @@ -123,6 +156,13 @@ export function GlobalSettingsTab() { autoEnable: true, }); + // Local state for dev progress injection + const [localDevProgress, setLocalDevProgress] = useState({ + enabled: true, + maxEntriesPerCategory: 10, + categories: ['feature', 'enhancement', 'bugfix', 'refactor', 'docs'], + }); + // Fetch system settings const { data: settings, @@ -146,6 +186,16 @@ export function GlobalSettingsTab() { staleTime: 30000, // 30 seconds }); + // Fetch project-tech stats + const { + data: projectTechStats, + isLoading: isLoadingProjectTech, + } = useQuery({ + queryKey: settingsKeys.projectTech(), + queryFn: fetchProjectTechStats, + staleTime: 60000, // 1 minute + }); + // Update settings mutation const updateMutation = useMutation({ mutationFn: updateSystemSettings, @@ -166,6 +216,9 @@ export function GlobalSettingsTab() { if (settings?.personalSpecDefaults) { setLocalDefaults(settings.personalSpecDefaults); } + if (settings?.devProgressInjection) { + setLocalDevProgress(settings.devProgressInjection); + } }, [settings]); // Handlers @@ -181,6 +234,31 @@ export function GlobalSettingsTab() { updateMutation.mutate({ personalSpecDefaults: newDefaults }); }; + // Dev progress injection handlers + const handleDevProgressToggle = (checked: boolean) => { + const newDevProgress = { ...localDevProgress, enabled: checked }; + setLocalDevProgress(newDevProgress); + updateMutation.mutate({ devProgressInjection: newDevProgress }); + }; + + const handleMaxEntriesChange = (value: string) => { + const numValue = parseInt(value, 10); + if (!isNaN(numValue) && numValue >= 1 && numValue <= 50) { + const newDevProgress = { ...localDevProgress, maxEntriesPerCategory: numValue }; + setLocalDevProgress(newDevProgress); + updateMutation.mutate({ devProgressInjection: newDevProgress }); + } + }; + + const handleCategoryToggle = (category: 'feature' | 'enhancement' | 'bugfix' | 'refactor' | 'docs') => { + const newCategories = localDevProgress.categories.includes(category) + ? localDevProgress.categories.filter(c => c !== category) + : [...localDevProgress.categories, category]; + const newDevProgress = { ...localDevProgress, categories: newCategories as DevProgressInjection['categories'] }; + setLocalDevProgress(newDevProgress); + updateMutation.mutate({ devProgressInjection: newDevProgress }); + }; + // Calculate totals - Only include specs and personal dimensions const dimensions = stats?.dimensions || {}; const dimensionEntries = Object.entries(dimensions) @@ -345,6 +423,107 @@ export function GlobalSettingsTab() { )} + + {/* Development Progress Injection Card */} + + +
+ + + {formatMessage({ id: 'specs.settings.devProgressInjection', defaultMessage: 'Development Progress Injection' })} + +
+ + {formatMessage({ id: 'specs.settings.devProgressInjectionDesc', defaultMessage: 'Control how development progress from project-tech.json is injected into AI context' })} + +
+ + {/* Enable Toggle */} +
+
+ +

+ {formatMessage({ id: 'specs.settings.enableDevProgressDesc', defaultMessage: 'Include development history in AI context' })} +

+
+ +
+ + {/* Max Entries */} +
+ + handleMaxEntriesChange(e.target.value)} + disabled={updateMutation.isPending || !localDevProgress.enabled} + className="w-24" + /> +

+ {formatMessage({ id: 'specs.settings.maxEntriesDesc', defaultMessage: 'Maximum number of entries to include per category (1-50)' })} +

+
+ + {/* Category Toggles */} +
+ +
+ {(['feature', 'enhancement', 'bugfix', 'refactor', 'docs'] as const).map(cat => ( + localDevProgress.enabled && handleCategoryToggle(cat)} + > + {cat} ({projectTechStats?.categories[cat] || 0}) + + ))} +
+

+ {formatMessage({ id: 'specs.settings.categoriesDesc', defaultMessage: 'Click to toggle category inclusion' })} +

+
+ + {/* Stats Summary */} + {projectTechStats && ( +
+ {projectTechStats.last_updated ? ( + formatMessage( + { id: 'specs.settings.devProgressStats', defaultMessage: '{total} entries from {sessions} sessions, last updated: {date}' }, + { + total: projectTechStats.total_features, + sessions: projectTechStats.total_sessions, + date: new Date(projectTechStats.last_updated).toLocaleDateString() + } + ) + ) : ( + formatMessage( + { id: 'specs.settings.devProgressStatsNoDate', defaultMessage: '{total} entries from {sessions} sessions' }, + { + total: projectTechStats.total_features, + sessions: projectTechStats.total_sessions + } + ) + )} +
+ )} +
+
); } diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts index b363d5c6..41f5fdc5 100644 --- a/ccw/frontend/src/lib/api.ts +++ b/ccw/frontend/src/lib/api.ts @@ -7340,11 +7340,13 @@ export interface InjectionPreviewResponse { * @param mode - 'required' | 'all' | 'keywords' * @param preview - Include content preview * @param projectPath - Optional project path + * @param category - Optional category filter */ export async function getInjectionPreview( mode: 'required' | 'all' | 'keywords' = 'required', preview: boolean = false, - projectPath?: string + projectPath?: string, + category?: string ): Promise { const params = new URLSearchParams(); params.set('mode', mode); @@ -7352,9 +7354,63 @@ export async function getInjectionPreview( if (projectPath) { params.set('path', projectPath); } + if (category) { + params.set('category', category); + } return fetchApi(`/api/specs/injection-preview?${params.toString()}`); } +/** + * Command preview configuration + */ +export interface CommandPreviewConfig { + command: string; + label: string; + description: string; + category?: string; + mode: 'required' | 'all'; +} + +/** + * Predefined command preview configurations + */ +export const COMMAND_PREVIEWS: CommandPreviewConfig[] = [ + { + command: 'ccw spec load', + label: 'Default (All Categories)', + description: 'Load all required specs without category filter', + mode: 'required', + }, + { + command: 'ccw spec load --category exploration', + label: 'Exploration', + description: 'Specs for code exploration, analysis, debugging', + category: 'exploration', + mode: 'required', + }, + { + command: 'ccw spec load --category planning', + label: 'Planning', + description: 'Specs for task planning, requirements', + category: 'planning', + mode: 'required', + }, + { + command: 'ccw spec load --category execution', + label: 'Execution', + description: 'Specs for implementation, testing, deployment', + category: 'execution', + mode: 'required', + }, + { + command: 'ccw spec load --category general', + label: 'General', + description: 'Specs that apply to all stages', + category: 'general', + mode: 'required', + }, +]; + /** * Update spec frontmatter (toggle readMode) */ diff --git a/ccw/frontend/src/locales/en/mcp-manager.json b/ccw/frontend/src/locales/en/mcp-manager.json index 212e8a57..6a4c7278 100644 --- a/ccw/frontend/src/locales/en/mcp-manager.json +++ b/ccw/frontend/src/locales/en/mcp-manager.json @@ -390,5 +390,42 @@ "enterprise": { "label": "Enterprise", "tooltip": "Enterprise MCP server" + }, + "specs": { + "settings": { + "personalSpecDefaults": "Personal Spec Defaults", + "personalSpecDefaultsDesc": "These settings will be applied when creating new personal specs", + "defaultReadMode": "Default Read Mode", + "selectReadMode": "Select read mode", + "defaultReadModeHelp": "The default read mode for newly created personal specs", + "autoEnable": "Auto Enable New Specs", + "autoEnableDescription": "Automatically enable newly created personal specs", + "specStatistics": "Spec Statistics", + "totalSpecs": "Total: {count} spec files", + "required": "required", + "readMode": { + "required": "Required", + "optional": "Optional" + }, + "dimension": { + "specs": "Project Specs", + "personal": "Personal Specs" + }, + "devProgressInjection": "Development Progress Injection", + "devProgressInjectionDesc": "Control how development progress from project-tech.json is injected into AI context", + "enableDevProgress": "Enable Injection", + "enableDevProgressDesc": "Include development history in AI context", + "maxEntries": "Max Entries per Category", + "maxEntriesDesc": "Maximum number of entries to include per category (1-50)", + "includeCategories": "Include Categories", + "categoriesDesc": "Click to toggle category inclusion", + "devProgressStats": "{total} entries from {sessions} sessions, last updated: {date}", + "devProgressStatsNoDate": "{total} entries from {sessions} sessions" + }, + "injection": { + "saveSuccess": "Settings saved successfully", + "saveError": "Failed to save settings: {error}", + "loadError": "Failed to load statistics" + } } } diff --git a/ccw/frontend/src/locales/zh/mcp-manager.json b/ccw/frontend/src/locales/zh/mcp-manager.json index fcb0e32e..8e7d72d5 100644 --- a/ccw/frontend/src/locales/zh/mcp-manager.json +++ b/ccw/frontend/src/locales/zh/mcp-manager.json @@ -390,5 +390,42 @@ "enterprise": { "label": "企业版", "tooltip": "企业版 MCP 服务器" + }, + "specs": { + "settings": { + "personalSpecDefaults": "个人规范默认设置", + "personalSpecDefaultsDesc": "这些设置将在创建新的个人规范时应用", + "defaultReadMode": "默认读取模式", + "selectReadMode": "选择读取模式", + "defaultReadModeHelp": "新创建的个人规范的默认读取模式", + "autoEnable": "自动启用新规范", + "autoEnableDescription": "自动启用新创建的个人规范", + "specStatistics": "规范统计", + "totalSpecs": "总计: {count} 个规范文件", + "required": "必读", + "readMode": { + "required": "必读", + "optional": "可选" + }, + "dimension": { + "specs": "项目规范", + "personal": "个人规范" + }, + "devProgressInjection": "开发进度注入", + "devProgressInjectionDesc": "控制如何将 project-tech.json 中的开发进度注入到 AI 上下文中", + "enableDevProgress": "启用注入", + "enableDevProgressDesc": "在 AI 上下文中包含开发历史", + "maxEntries": "每类别最大条目数", + "maxEntriesDesc": "每个类别包含的最大条目数 (1-50)", + "includeCategories": "包含类别", + "categoriesDesc": "点击切换类别包含状态", + "devProgressStats": "共 {total} 条记录来自 {sessions} 个会话,最后更新: {date}", + "devProgressStatsNoDate": "共 {total} 条记录来自 {sessions} 个会话" + }, + "injection": { + "saveSuccess": "设置保存成功", + "saveError": "保存设置失败: {error}", + "loadError": "加载统计数据失败" + } } } diff --git a/ccw/src/core/routes/system-routes.ts b/ccw/src/core/routes/system-routes.ts index 1ae8b53e..343852c5 100644 --- a/ccw/src/core/routes/system-routes.ts +++ b/ccw/src/core/routes/system-routes.ts @@ -43,6 +43,12 @@ const DEFAULT_PERSONAL_SPEC_DEFAULTS = { autoEnable: true }; +const DEFAULT_DEV_PROGRESS_INJECTION = { + enabled: true, + maxEntriesPerCategory: 10, + categories: ['feature', 'enhancement', 'bugfix', 'refactor', 'docs'] +}; + // Recommended hooks for spec injection const RECOMMENDED_HOOKS = [ { @@ -90,6 +96,7 @@ function readSettingsFile(filePath: string): Record { function getSystemSettings(): { injectionControl: typeof DEFAULT_INJECTION_CONTROL; personalSpecDefaults: typeof DEFAULT_PERSONAL_SPEC_DEFAULTS; + devProgressInjection: typeof DEFAULT_DEV_PROGRESS_INJECTION; recommendedHooks: typeof RECOMMENDED_HOOKS; } { const settings = readSettingsFile(GLOBAL_SETTINGS_PATH) as Record; @@ -105,6 +112,10 @@ function getSystemSettings(): { ...DEFAULT_PERSONAL_SPEC_DEFAULTS, ...((user.personalSpecDefaults || {}) as Record) } as typeof DEFAULT_PERSONAL_SPEC_DEFAULTS, + devProgressInjection: { + ...DEFAULT_DEV_PROGRESS_INJECTION, + ...((system.devProgressInjection || {}) as Record) + } as typeof DEFAULT_DEV_PROGRESS_INJECTION, recommendedHooks: RECOMMENDED_HOOKS }; } @@ -115,6 +126,7 @@ function getSystemSettings(): { function saveSystemSettings(updates: { injectionControl?: Partial; personalSpecDefaults?: Partial; + devProgressInjection?: Partial; }): { success: boolean; settings?: Record; error?: string } { try { const settings = readSettingsFile(GLOBAL_SETTINGS_PATH) as Record; @@ -143,6 +155,14 @@ function saveSystemSettings(updates: { }; } + if (updates.devProgressInjection) { + system.devProgressInjection = { + ...DEFAULT_DEV_PROGRESS_INJECTION, + ...((system.devProgressInjection || {}) as Record), + ...updates.devProgressInjection + }; + } + // Ensure directory exists const dirPath = dirname(GLOBAL_SETTINGS_PATH); if (!existsSync(dirPath)) { @@ -410,6 +430,7 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise sum + count, 0); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + total_features, + total_sessions: tech.statistics?.total_sessions || 0, + last_updated: tech._metadata?.last_updated || null, + categories + })); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (err as Error).message })); + } + return true; + } + // API: Install recommended hooks if (pathname === '/api/system/hooks/install-recommended' && req.method === 'POST') { handlePostRequest(req, res, async (body) => {