mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
@@ -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
|
||||
|
||||
380
.claude/commands/workflow/init-specs.md
Normal file
380
.claude/commands/workflow/init-specs.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "加载统计数据失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user