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
This commit is contained in:
catlog22
2026-02-27 12:25:26 +08:00
parent 4d755ff9b4
commit 99a3561f71
10 changed files with 877 additions and 33 deletions

View File

@@ -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

View File

@@ -0,0 +1,380 @@
---
name: init-specs
description: Interactive wizard to create individual specs or personal constraints with scope selection
argument-hint: "[--scope <global|project>] [--dimension <specs|personal>] [--category <general|exploration|planning|execution>]"
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 <cat>` - Load filtered specs

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<SpecStats> {
return response.json();
}
async function fetchProjectTechStats(): Promise<ProjectTechStats> {
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<DevProgressInjection>({
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() {
)}
</CardContent>
</Card>
{/* Development Progress Injection Card */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<History className="h-5 w-5 text-muted-foreground" />
<CardTitle>
{formatMessage({ id: 'specs.settings.devProgressInjection', defaultMessage: 'Development Progress Injection' })}
</CardTitle>
</div>
<CardDescription>
{formatMessage({ id: 'specs.settings.devProgressInjectionDesc', defaultMessage: 'Control how development progress from project-tech.json is injected into AI context' })}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Enable Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>
{formatMessage({ id: 'specs.settings.enableDevProgress', defaultMessage: 'Enable Injection' })}
</Label>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'specs.settings.enableDevProgressDesc', defaultMessage: 'Include development history in AI context' })}
</p>
</div>
<Switch
checked={localDevProgress.enabled}
onCheckedChange={handleDevProgressToggle}
disabled={updateMutation.isPending}
/>
</div>
{/* Max Entries */}
<div className="space-y-2">
<Label>
{formatMessage({ id: 'specs.settings.maxEntries', defaultMessage: 'Max Entries per Category' })}
</Label>
<Input
type="number"
min={1}
max={50}
value={localDevProgress.maxEntriesPerCategory}
onChange={(e) => handleMaxEntriesChange(e.target.value)}
disabled={updateMutation.isPending || !localDevProgress.enabled}
className="w-24"
/>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'specs.settings.maxEntriesDesc', defaultMessage: 'Maximum number of entries to include per category (1-50)' })}
</p>
</div>
{/* Category Toggles */}
<div className="space-y-2">
<Label>
{formatMessage({ id: 'specs.settings.includeCategories', defaultMessage: 'Include Categories' })}
</Label>
<div className="flex flex-wrap gap-2">
{(['feature', 'enhancement', 'bugfix', 'refactor', 'docs'] as const).map(cat => (
<Badge
key={cat}
variant={localDevProgress.categories.includes(cat) ? 'default' : 'outline'}
className={cn(
'cursor-pointer transition-colors',
!localDevProgress.enabled && 'opacity-50 cursor-not-allowed'
)}
onClick={() => localDevProgress.enabled && handleCategoryToggle(cat)}
>
{cat} ({projectTechStats?.categories[cat] || 0})
</Badge>
))}
</div>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'specs.settings.categoriesDesc', defaultMessage: 'Click to toggle category inclusion' })}
</p>
</div>
{/* Stats Summary */}
{projectTechStats && (
<div className="text-sm text-muted-foreground pt-4 border-t border-border">
{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
}
)
)}
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -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<InjectionPreviewResponse> {
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<InjectionPreviewResponse>(`/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)
*/

View File

@@ -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"
}
}
}

View File

@@ -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": "加载统计数据失败"
}
}
}

View File

@@ -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<string, unknown> {
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<string, unknown>;
@@ -105,6 +112,10 @@ function getSystemSettings(): {
...DEFAULT_PERSONAL_SPEC_DEFAULTS,
...((user.personalSpecDefaults || {}) as Record<string, unknown>)
} as typeof DEFAULT_PERSONAL_SPEC_DEFAULTS,
devProgressInjection: {
...DEFAULT_DEV_PROGRESS_INJECTION,
...((system.devProgressInjection || {}) as Record<string, unknown>)
} as typeof DEFAULT_DEV_PROGRESS_INJECTION,
recommendedHooks: RECOMMENDED_HOOKS
};
}
@@ -115,6 +126,7 @@ function getSystemSettings(): {
function saveSystemSettings(updates: {
injectionControl?: Partial<typeof DEFAULT_INJECTION_CONTROL>;
personalSpecDefaults?: Partial<typeof DEFAULT_PERSONAL_SPEC_DEFAULTS>;
devProgressInjection?: Partial<typeof DEFAULT_DEV_PROGRESS_INJECTION>;
}): { success: boolean; settings?: Record<string, unknown>; error?: string } {
try {
const settings = readSettingsFile(GLOBAL_SETTINGS_PATH) as Record<string, unknown>;
@@ -143,6 +155,14 @@ function saveSystemSettings(updates: {
};
}
if (updates.devProgressInjection) {
system.devProgressInjection = {
...DEFAULT_DEV_PROGRESS_INJECTION,
...((system.devProgressInjection || {}) as Record<string, unknown>),
...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<boole
const updates = body as {
injectionControl?: { maxLength?: number; warnThreshold?: number; truncateOnExceed?: boolean };
personalSpecDefaults?: { defaultReadMode?: string; autoEnable?: boolean };
devProgressInjection?: { enabled?: boolean; maxEntriesPerCategory?: number; categories?: string[] };
};
const result = saveSystemSettings(updates);
@@ -421,6 +442,73 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise<boole
return true;
}
// API: Get project-tech stats for development progress injection
if (pathname === '/api/project-tech/stats' && req.method === 'GET') {
const projectPath = url.searchParams.get('path') || initialPath;
const resolvedPath = resolvePath(projectPath);
const techPath = join(resolvedPath, '.workflow', 'project-tech.json');
if (!existsSync(techPath)) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
total_features: 0,
total_sessions: 0,
last_updated: null,
categories: {
feature: 0,
enhancement: 0,
bugfix: 0,
refactor: 0,
docs: 0
}
}));
return true;
}
try {
const rawContent = readFileSync(techPath, 'utf-8');
const tech = JSON.parse(rawContent) as {
development_index?: {
feature?: unknown[];
enhancement?: unknown[];
bugfix?: unknown[];
refactor?: unknown[];
docs?: unknown[];
};
_metadata?: {
last_updated?: string;
};
statistics?: {
total_features?: number;
total_sessions?: number;
};
};
const devIndex = tech.development_index || {};
const categories = {
feature: (devIndex.feature || []).length,
enhancement: (devIndex.enhancement || []).length,
bugfix: (devIndex.bugfix || []).length,
refactor: (devIndex.refactor || []).length,
docs: (devIndex.docs || []).length
};
const total_features = Object.values(categories).reduce((sum, count) => 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) => {