feat: add category and scope to specs for enhanced filtering and organization

- Introduced SpecCategory and SpecScope types to categorize specs by workflow stage and scope (global/project).
- Updated Spec interface to include category and scope properties.
- Enhanced SpecCard component to display category and scope badges.
- Implemented category and scope filtering in SpecsSettingsPage.
- Updated localization files to support new category and scope labels.
- Modified spec loading commands to utilize category instead of keywords.
- Adjusted spec index builder to handle category and scope during spec parsing.
- Updated seed documents to include category information.
This commit is contained in:
catlog22
2026-02-26 23:43:55 +08:00
parent 052e25dddb
commit dfa8e0d9f5
47 changed files with 619 additions and 179 deletions

View File

@@ -55,7 +55,7 @@ Phase 4: Output Generation
Read and memorize schema requirements BEFORE any analysis begins (feeds Phase 3 validation). Read and memorize schema requirements BEFORE any analysis begins (feeds Phase 3 validation).
3. **Project Context Loading** (from spec system): 3. **Project Context Loading** (from spec system):
- Load exploration specs using: `ccw spec load --keywords exploration` - Load exploration specs using: `ccw spec load --category exploration`
- Extract: `tech_stack`, `architecture`, `key_components`, `overview` - Extract: `tech_stack`, `architecture`, `key_components`, `overview`
- Usage: Align analysis scope and patterns with actual project technology choices - Usage: Align analysis scope and patterns with actual project technology choices
- If no specs are returned, proceed with fresh analysis (no error). - If no specs are returned, proceed with fresh analysis (no error).

View File

@@ -55,7 +55,7 @@ When invoked with `process_docs: true` in input context:
## Input Context ## Input Context
**Project Context** (loaded from spec system at startup): **Project Context** (loaded from spec system at startup):
- Load specs using: `ccw spec load --keywords "exploration architecture"` → tech_stack, architecture, key_components, conventions, constraints, quality_rules - Load specs using: `ccw spec load --category "exploration architecture"` → tech_stack, architecture, key_components, conventions, constraints, quality_rules
```javascript ```javascript
{ {

View File

@@ -79,7 +79,7 @@ if (file_exists(contextPackagePath)) {
```javascript ```javascript
// Load project-level context (from spec system) // Load project-level context (from spec system)
// These provide foundational constraints for ALL context gathering // These provide foundational constraints for ALL context gathering
const projectSpecs = Bash('ccw spec load --keywords "exploration architecture" --stdin'); const projectSpecs = Bash('ccw spec load --category "exploration architecture" --stdin');
const projectTech = projectSpecs?.tech_stack ? projectSpecs : null; const projectTech = projectSpecs?.tech_stack ? projectSpecs : null;
const projectGuidelines = projectSpecs?.coding_conventions ? projectSpecs : null; const projectGuidelines = projectSpecs?.coding_conventions ? projectSpecs : null;

View File

@@ -36,7 +36,7 @@ Phase 5: Fix & Verification
## Phase 1: Bug Analysis ## Phase 1: Bug Analysis
**Load Project Context** (from spec system): **Load Project Context** (from spec system):
- Load exploration specs using: `ccw spec load --keywords exploration` for tech stack context and coding constraints - Load exploration specs using: `ccw spec load --category exploration` for tech stack context and coding constraints
**Session Setup**: **Session Setup**:
```javascript ```javascript

View File

@@ -99,7 +99,7 @@ if (isPopulated) {
```javascript ```javascript
// Load project context via ccw spec load for planning context // Load project context via ccw spec load for planning context
const projectContext = Bash('ccw spec load --keywords planning 2>/dev/null || echo "{}"') const projectContext = Bash('ccw spec load --category planning 2>/dev/null || echo "{}"')
const specData = JSON.parse(projectContext) const specData = JSON.parse(projectContext)
// Extract key info from loaded specs for generating smart questions // Extract key info from loaded specs for generating smart questions

View File

@@ -148,7 +148,7 @@ Priority: <issue.priority>
## MANDATORY FIRST STEPS ## MANDATORY FIRST STEPS
1. Run: ccw tool exec get_modules_by_depth '{}' 1. Run: ccw tool exec get_modules_by_depth '{}'
2. Execute ACE searches based on issue keywords 2. Execute ACE searches based on issue keywords
3. Run: ccw spec load --keywords exploration 3. Run: ccw spec load --category exploration
## Exploration Focus ## Exploration Focus
- Identify files directly related to this issue - Identify files directly related to this issue

View File

@@ -81,7 +81,7 @@ Session: ${sessionFolder}
## MANDATORY FIRST STEPS ## MANDATORY FIRST STEPS
1. Run: ccw tool exec get_modules_by_depth '{}' 1. Run: ccw tool exec get_modules_by_depth '{}'
2. Execute searches: ${strategy.searches.map(s => `"${s}"`).join(', ')} 2. Execute searches: ${strategy.searches.map(s => `"${s}"`).join(', ')}
3. Run: ccw spec load --keywords exploration 3. Run: ccw spec load --category exploration
## Exploration Focus (${perspective} angle) ## Exploration Focus (${perspective} angle)
- **Depth**: ${strategy.depth} - **Depth**: ${strategy.depth}

View File

@@ -138,7 +138,7 @@ Session: <session-folder>
## MANDATORY FIRST STEPS ## MANDATORY FIRST STEPS
1. Run: ccw tool exec get_modules_by_depth '{}' 1. Run: ccw tool exec get_modules_by_depth '{}'
2. Execute relevant searches based on topic keywords 2. Execute relevant searches based on topic keywords
3. Run: ccw spec load --keywords exploration 3. Run: ccw spec load --category exploration
## Exploration Focus (<perspective> angle) ## Exploration Focus (<perspective> angle)
<dimensions map to exploration focus areas> <dimensions map to exploration focus areas>

View File

@@ -93,7 +93,7 @@ rg "password|token|secret|auth" -g "*.{ts,js,py}"
rg "eval|exec|innerHTML|dangerouslySetInnerHTML" -g "*.{ts,js,tsx}" rg "eval|exec|innerHTML|dangerouslySetInnerHTML" -g "*.{ts,js,tsx}"
# Gemini security analysis # Gemini security analysis
ccw spec load --keywords execution ccw spec load --category execution
ccw cli -p " ccw cli -p "
PURPOSE: Security audit of completed implementation PURPOSE: Security audit of completed implementation
TASK: Review code for security vulnerabilities, insecure patterns, auth/authz issues TASK: Review code for security vulnerabilities, insecure patterns, auth/authz issues
@@ -105,7 +105,7 @@ RULES: Focus on OWASP Top 10, authentication, authorization, data validation, in
**Architecture Review** (`architecture`): **Architecture Review** (`architecture`):
```bash ```bash
ccw spec load --keywords execution ccw spec load --category execution
ccw cli -p " ccw cli -p "
PURPOSE: Architecture compliance review PURPOSE: Architecture compliance review
TASK: Evaluate adherence to architectural patterns, identify technical debt, review design decisions TASK: Evaluate adherence to architectural patterns, identify technical debt, review design decisions
@@ -117,7 +117,7 @@ RULES: Check for patterns, separation of concerns, modularity, scalability
**Quality Review** (`quality`): **Quality Review** (`quality`):
```bash ```bash
ccw spec load --keywords execution ccw spec load --category execution
ccw cli -p " ccw cli -p "
PURPOSE: Code quality and best practices review PURPOSE: Code quality and best practices review
TASK: Assess code readability, maintainability, adherence to best practices TASK: Assess code readability, maintainability, adherence to best practices
@@ -139,7 +139,7 @@ for task_file in ${sessionPath}/.task/*.json; do
done done
# Cross-check implementation against requirements # Cross-check implementation against requirements
ccw spec load --keywords execution ccw spec load --category execution
ccw cli -p " ccw cli -p "
PURPOSE: Verify all requirements and acceptance criteria are met PURPOSE: Verify all requirements and acceptance criteria are met
TASK: Cross-check implementation summaries against original requirements TASK: Cross-check implementation summaries against original requirements

View File

@@ -127,10 +127,10 @@ After collecting preferences, enhance context and dispatch:
```javascript ```javascript
// Step 1: Load project context via ccw spec // Step 1: Load project context via ccw spec
Bash('ccw spec load --keywords planning') Bash('ccw spec load --category planning')
// Step 2: Log available context // Step 2: Log available context
console.log('Project context loaded via: ccw spec load --keywords planning') console.log('Project context loaded via: ccw spec load --category planning')
// Step 3: Dispatch to phase (workflowPreferences available as context) // Step 3: Dispatch to phase (workflowPreferences available as context)
if (mode === 'plan') { if (mode === 'plan') {

View File

@@ -495,7 +495,7 @@ Generate implementation plan and write plan.json.
Execute: cat ~/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json (get schema reference before generating plan) Execute: cat ~/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json (get schema reference before generating plan)
## Project Context (MANDATORY - Load via ccw spec) ## Project Context (MANDATORY - Load via ccw spec)
Execute: ccw spec load --keywords planning Execute: ccw spec load --category planning
This loads technology stack, architecture, key components, and user-defined constraints/conventions. This loads technology stack, architecture, key components, and user-defined constraints/conventions.
**CRITICAL**: All generated tasks MUST comply with constraints in specs/*.md **CRITICAL**: All generated tasks MUST comply with constraints in specs/*.md

View File

@@ -485,8 +485,8 @@ ${(t.test?.success_metrics || []).length > 0 ? `\n**Success metrics**: ${t.test.
context.push(`### Artifacts\nPlan: ${executionContext.session.artifacts.plan}`) context.push(`### Artifacts\nPlan: ${executionContext.session.artifacts.plan}`)
} }
// Project guidelines (user-defined constraints from /workflow:session:solidify) // Project guidelines (user-defined constraints from /workflow:session:solidify)
// Loaded via: ccw spec load --keywords planning // Loaded via: ccw spec load --category planning
context.push(`### Project Guidelines\n(Loaded via ccw spec load --keywords planning)`) context.push(`### Project Guidelines\n(Loaded via ccw spec load --category planning)`)
if (context.length > 0) sections.push(`## Context\n${context.join('\n\n')}`) if (context.length > 0) sections.push(`## Context\n${context.join('\n\n')}`)
sections.push(`Complete each task according to its "Done when" checklist.`) sections.push(`Complete each task according to its "Done when" checklist.`)

View File

@@ -101,10 +101,10 @@ After collecting preferences, enhance context and dispatch:
```javascript ```javascript
// Step 1: Load project context via ccw spec // Step 1: Load project context via ccw spec
Bash('ccw spec load --keywords planning') Bash('ccw spec load --category planning')
// Step 2: Log available context // Step 2: Log available context
console.log('Project context loaded via: ccw spec load --keywords planning') console.log('Project context loaded via: ccw spec load --category planning')
// Step 3: Dispatch to phase (workflowPreferences available as context) // Step 3: Dispatch to phase (workflowPreferences available as context)
if (mode === 'plan') { if (mode === 'plan') {

View File

@@ -203,7 +203,7 @@ This is the PRIMARY context source - all subsequent analysis must align with use
Execute complete context-search-agent workflow (Phase 1-3) for implementation planning. Execute complete context-search-agent workflow (Phase 1-3) for implementation planning.
Key emphasis: Key emphasis:
- Run: ccw spec load --keywords exploration FIRST (per your spec Phase 1.1b) - Run: ccw spec load --category exploration FIRST (per your spec Phase 1.1b)
- Synthesize exploration results with project context - Synthesize exploration results with project context
- Generate prioritized_context with user_intent alignment - Generate prioritized_context with user_intent alignment
- Apply specs/*.md constraints during conflict detection - Apply specs/*.md constraints during conflict detection

View File

@@ -171,7 +171,7 @@ Session ID: ${sessionId}
MCP Capabilities: {exa_code, exa_web, code_index} MCP Capabilities: {exa_code, exa_web, code_index}
## PROJECT CONTEXT (MANDATORY - load via ccw spec) ## PROJECT CONTEXT (MANDATORY - load via ccw spec)
Execute: ccw spec load --keywords planning Execute: ccw spec load --category planning
This loads: This loads:
- Technology stack, architecture, key components, build system, test framework - Technology stack, architecture, key components, build system, test framework

View File

@@ -221,7 +221,7 @@ Execute complete context-search-agent workflow for TDD implementation planning:
### Phase 1: Initialization & Pre-Analysis ### Phase 1: Initialization & Pre-Analysis
1. **Project State Loading**: 1. **Project State Loading**:
- Run: \`ccw spec load --keywords execution\` to load project context, tech stack, and guidelines. - Run: \`ccw spec load --category execution\` to load project context, tech stack, and guidelines.
- If files don't exist, proceed with fresh analysis. - If files don't exist, proceed with fresh analysis.
2. **Detection**: Check for existing context-package (early exit if valid) 2. **Detection**: Check for existing context-package (early exit if valid)
3. **Foundation**: Initialize CodexLens, get project structure, load docs 3. **Foundation**: Initialize CodexLens, get project structure, load docs

View File

@@ -231,14 +231,14 @@ MCP Capabilities: {exa_code, exa_web, code_index}
## PROJECT CONTEXT (MANDATORY - load before planning-notes) ## PROJECT CONTEXT (MANDATORY - load before planning-notes)
These files provide project-level constraints that apply to ALL tasks: These files provide project-level constraints that apply to ALL tasks:
1. **ccw spec load --keywords execution** (project specs and tech analysis) 1. **ccw spec load --category execution** (project specs and tech analysis)
- Contains: tech_stack, architecture_type, key_components, build_system, test_framework, coding_conventions, naming_rules, forbidden_patterns, quality_gates, custom_constraints - Contains: tech_stack, architecture_type, key_components, build_system, test_framework, coding_conventions, naming_rules, forbidden_patterns, quality_gates, custom_constraints
- Usage: Populate plan.json shared_context, align task tech choices, set correct test commands - Usage: Populate plan.json shared_context, align task tech choices, set correct test commands
- Apply as HARD CONSTRAINTS on all generated tasks — task implementation steps, - Apply as HARD CONSTRAINTS on all generated tasks — task implementation steps,
acceptance criteria, and convergence.verification MUST respect these guidelines acceptance criteria, and convergence.verification MUST respect these guidelines
- If empty/missing: No additional constraints (proceed normally) - If empty/missing: No additional constraints (proceed normally)
Loading order: \`ccw spec load --keywords execution\` → planning-notes.md → context-package.json Loading order: \`ccw spec load --category execution\` → planning-notes.md → context-package.json
## USER CONFIGURATION (from Phase 0) ## USER CONFIGURATION (from Phase 0)
Execution Method: ${userConfig.executionMethod} // agent|hybrid|cli Execution Method: ${userConfig.executionMethod} // agent|hybrid|cli

View File

@@ -345,7 +345,7 @@ Execute complete context-search-agent workflow for implementation planning:
### Phase 1: Initialization & Pre-Analysis ### Phase 1: Initialization & Pre-Analysis
1. **Project State Loading**: 1. **Project State Loading**:
- Run: \`ccw spec load --keywords execution\` to load project context, tech stack, and guidelines. - Run: \`ccw spec load --category execution\` to load project context, tech stack, and guidelines.
- If files don't exist, proceed with fresh analysis. - If files don't exist, proceed with fresh analysis.
2. **Detection**: Check for existing context-package (early exit if valid) 2. **Detection**: Check for existing context-package (early exit if valid)
3. **Foundation**: Initialize CodexLens, get project structure, load docs 3. **Foundation**: Initialize CodexLens, get project structure, load docs

View File

@@ -244,7 +244,7 @@ Task(
${selectedStrategy} - ${strategyDescription} ${selectedStrategy} - ${strategyDescription}
## PROJECT CONTEXT (MANDATORY) ## PROJECT CONTEXT (MANDATORY)
1. Run: \`ccw spec load --keywords execution\` (tech stack, test framework, build system, constraints) 1. Run: \`ccw spec load --category execution\` (tech stack, test framework, build system, constraints)
## MANDATORY FIRST STEPS ## MANDATORY FIRST STEPS
1. Read test results: ${session.test_results_path} 1. Read test results: ${session.test_results_path}

View File

@@ -21,7 +21,7 @@ Check these items. Report results as a checklist.
### 1.2 Strongly Recommended (warn if missing) ### 1.2 Strongly Recommended (warn if missing)
- **Project specs**: Run `ccw spec load --keywords execution` to load project context - **Project specs**: Run `ccw spec load --category execution` to load project context
- If spec system unavailable: Read `package.json` / `tsconfig.json` / `pyproject.toml` and generate a minimal version. Ask user: "检测到项目使用 [tech stack], 是否正确?需要补充什么?" - If spec system unavailable: Read `package.json` / `tsconfig.json` / `pyproject.toml` and generate a minimal version. Ask user: "检测到项目使用 [tech stack], 是否正确?需要补充什么?"
- **Test framework**: Detect from config files (jest.config, vitest.config, pytest.ini, etc.) - **Test framework**: Detect from config files (jest.config, vitest.config, pytest.ini, etc.)
- If missing: Ask user: "未检测到测试框架配置,请指定测试命令(如 `npm test`, `pytest`),或输入 'skip' 跳过测试验证" - If missing: Ask user: "未检测到测试框架配置,请指定测试命令(如 `npm test`, `pytest`),或输入 'skip' 跳过测试验证"
@@ -36,7 +36,7 @@ Print formatted checklist:
✓ 项目根目录: D:\myproject ✓ 项目根目录: D:\myproject
✓ 工作空间: .workflow/.cycle/ 就绪 ✓ 工作空间: .workflow/.cycle/ 就绪
⚠ Git: 3 个未提交变更 ⚠ Git: 3 个未提交变更
✓ Project specs: 已加载 (ccw spec load --keywords execution) ✓ Project specs: 已加载 (ccw spec load --category execution)
⚠ specs: 未找到 (已跳过) ⚠ specs: 未找到 (已跳过)
✓ 测试框架: jest (npm test) ✓ 测试框架: jest (npm test)
``` ```
@@ -170,7 +170,7 @@ Read the user's `$TASK` and score each dimension:
For dimensions still at score 1 after Q&A, auto-enhance from codebase: For dimensions still at score 1 after Q&A, auto-enhance from codebase:
- **Scope**: Use `Glob` and `Grep` to find related files, list them - **Scope**: Use `Glob` and `Grep` to find related files, list them
- **Context**: Run `ccw spec load --keywords execution` to load project context - **Context**: Run `ccw spec load --category execution` to load project context
- **Constraints**: Infer from `specs/*.md` and existing patterns - **Constraints**: Infer from `specs/*.md` and existing patterns
### 2.5 Assemble Refined Task ### 2.5 Assemble Refined Task

View File

@@ -21,7 +21,7 @@ Check these items. Report results as a checklist.
### 1.2 Strongly Recommended (warn if missing) ### 1.2 Strongly Recommended (warn if missing)
- **Project specs**: Run `ccw spec load --keywords planning` to load project context - **Project specs**: Run `ccw spec load --category planning` to load project context
- If spec system unavailable: WARN — Phase 1 will call `workflow:init` to initialize. Ask user: "检测到项目使用 [tech stack from package.json], 是否正确?需要补充什么?" - If spec system unavailable: WARN — Phase 1 will call `workflow:init` to initialize. Ask user: "检测到项目使用 [tech stack from package.json], 是否正确?需要补充什么?"
- **Test framework**: Detect from config files (jest.config, vitest.config, pytest.ini, etc.) - **Test framework**: Detect from config files (jest.config, vitest.config, pytest.ini, etc.)
- If missing: Ask: "未检测到测试框架,请指定测试命令(如 `npm test`),或输入 'skip' 跳过" - If missing: Ask: "未检测到测试框架,请指定测试命令(如 `npm test`),或输入 'skip' 跳过"
@@ -36,7 +36,7 @@ Print formatted checklist:
✓ 项目根目录: D:\myproject ✓ 项目根目录: D:\myproject
✓ .workflow/ 目录就绪 ✓ .workflow/ 目录就绪
⚠ Git: 3 个未提交变更 ⚠ Git: 3 个未提交变更
✓ Project specs: 已加载 (ccw spec load --keywords planning) ✓ Project specs: 已加载 (ccw spec load --category planning)
⚠ specs: 未找到 (Phase 1 将初始化) ⚠ specs: 未找到 (Phase 1 将初始化)
✓ 测试框架: jest (npm test) ✓ 测试框架: jest (npm test)
``` ```
@@ -160,7 +160,7 @@ Each dimension scores 0-2 (0=missing, 1=vague, 2=clear). **Total minimum: 6/10 t
For dimensions still at score 1 after Q&A, auto-enhance from codebase: For dimensions still at score 1 after Q&A, auto-enhance from codebase:
- **Scope**: Use `Glob` and `Grep` to find related files - **Scope**: Use `Glob` and `Grep` to find related files
- **Context**: Run `ccw spec load --keywords planning` to load project context - **Context**: Run `ccw spec load --category planning` to load project context
- **Constraints**: Infer from `specs/*.md` - **Constraints**: Infer from `specs/*.md`
### 2.5 Assemble Structured Description ### 2.5 Assemble Structured Description

View File

@@ -85,7 +85,7 @@ Step 1: Topic Understanding
Step 2: Exploration (Inline, No Agents) Step 2: Exploration (Inline, No Agents)
├─ Detect codebase → search relevant modules, patterns ├─ Detect codebase → search relevant modules, patterns
│ ├─ Run `ccw spec load --keywords exploration` (if spec system available) │ ├─ Run `ccw spec load --category exploration` (if spec system available)
│ └─ Use Grep, Glob, Read, mcp__ace-tool__search_context │ └─ Use Grep, Glob, Read, mcp__ace-tool__search_context
├─ Multi-perspective analysis (if selected, serial) ├─ Multi-perspective analysis (if selected, serial)
│ ├─ Single: Comprehensive analysis │ ├─ Single: Comprehensive analysis
@@ -297,7 +297,7 @@ const hasCodebase = Bash(`
if (hasCodebase !== 'none') { if (hasCodebase !== 'none') {
// 1. Read project metadata (if exists) // 1. Read project metadata (if exists)
// - Run `ccw spec load --keywords exploration` (load project specs) // - Run `ccw spec load --category exploration` (load project specs)
// - .workflow/specs/*.md (project conventions) // - .workflow/specs/*.md (project conventions)
// 2. Search codebase for relevant content // 2. Search codebase for relevant content

View File

@@ -282,7 +282,7 @@ Use built-in tools to understand the codebase structure before spawning perspect
**Context Gathering Activities**: **Context Gathering Activities**:
1. **Get project structure** - Execute `ccw tool exec get_modules_by_depth '{}'` 1. **Get project structure** - Execute `ccw tool exec get_modules_by_depth '{}'`
2. **Search for related code** - Use Grep/Glob to find files matching topic keywords 2. **Search for related code** - Use Grep/Glob to find files matching topic keywords
3. **Read project tech context** - Run `ccw spec load --keywords "exploration planning"` if spec system available 3. **Read project tech context** - Run `ccw spec load --category "exploration planning"` if spec system available
4. **Analyze patterns** - Identify common code patterns and architecture decisions 4. **Analyze patterns** - Identify common code patterns and architecture decisions
**exploration-codebase.json Structure**: **exploration-codebase.json Structure**:
@@ -358,7 +358,7 @@ const agentIds = perspectives.map(perspective => {
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/cli-explore-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/cli-explore-agent.md (MUST read first)
2. Run: `ccw spec load --keywords "exploration planning"` 2. Run: `ccw spec load --category "exploration planning"`
3. Read project tech context from loaded specs 3. Read project tech context from loaded specs
--- ---
@@ -566,7 +566,7 @@ const deepDiveAgent = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/cli-explore-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/cli-explore-agent.md (MUST read first)
2. Read: ${sessionFolder}/perspectives.json (prior findings) 2. Read: ${sessionFolder}/perspectives.json (prior findings)
3. Run: `ccw spec load --keywords "exploration planning"` 3. Run: `ccw spec load --category "exploration planning"`
--- ---

View File

@@ -194,7 +194,7 @@ Use built-in tools directly to understand the task scope and identify sub-domain
**Analysis Activities**: **Analysis Activities**:
1. **Search for references** — Find related documentation, README files, and architecture guides 1. **Search for references** — Find related documentation, README files, and architecture guides
- Use: `mcp__ace-tool__search_context`, Grep, Glob, Read - Use: `mcp__ace-tool__search_context`, Grep, Glob, Read
- Run: `ccw spec load --keywords planning` (if spec system available) - Run: `ccw spec load --category planning` (if spec system available)
2. **Extract task keywords** — Identify key terms and concepts from the task description 2. **Extract task keywords** — Identify key terms and concepts from the task description
3. **Identify ambiguities** — List any unclear points or multiple possible interpretations 3. **Identify ambiguities** — List any unclear points or multiple possible interpretations
4. **Clarify with user** — If ambiguities found, use AskUserQuestion for clarification 4. **Clarify with user** — If ambiguities found, use AskUserQuestion for clarification

View File

@@ -231,7 +231,7 @@ const agentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/{agent-type}.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/{agent-type}.md (MUST read first)
2. Execute: ccw spec load --keywords exploration 2. Execute: ccw spec load --category exploration
## TASK CONTEXT ## TASK CONTEXT
${taskContext} ${taskContext}

View File

@@ -268,7 +268,7 @@ const hasCodebase = bash(`
// 2. Codebase Exploration (only when hasCodebase !== 'none') // 2. Codebase Exploration (only when hasCodebase !== 'none')
if (hasCodebase !== 'none') { if (hasCodebase !== 'none') {
// Read project metadata (if exists) // Read project metadata (if exists)
// Run `ccw spec load --keywords planning` // Run `ccw spec load --category planning`
// Search codebase for requirement-relevant context // Search codebase for requirement-relevant context
// Use: mcp__ace-tool__search_context, Grep, Glob, Read // Use: mcp__ace-tool__search_context, Grep, Glob, Read

View File

@@ -303,7 +303,7 @@ const agentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/{agent-type}.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/{agent-type}.md (MUST read first)
2. Execute: ccw spec load --keywords "exploration execution" 2. Execute: ccw spec load --category "exploration execution"
--- ---

View File

@@ -94,7 +94,7 @@ dimensions.forEach(dimension => {
3. Get target files: Read resolved_files from review-state.json 3. Get target files: Read resolved_files from review-state.json
4. Validate file access: bash(ls -la ${targetFiles.join(' ')}) 4. Validate file access: bash(ls -la ${targetFiles.join(' ')})
5. Execute: cat ~/.ccw/workflows/cli-templates/schemas/review-dimension-results-schema.json (get output schema reference) 5. Execute: cat ~/.ccw/workflows/cli-templates/schemas/review-dimension-results-schema.json (get output schema reference)
6. Execute: ccw spec load --keywords "exploration execution" (technology stack and constraints) 6. Execute: ccw spec load --category "exploration execution" (technology stack and constraints)
--- ---
@@ -216,7 +216,7 @@ dimensions.forEach(dimension => {
4. Get changed files: bash(cd ${workflowDir} && git log --since="${sessionCreatedAt}" --name-only --pretty=format: | sort -u) 4. Get changed files: bash(cd ${workflowDir} && git log --since="${sessionCreatedAt}" --name-only --pretty=format: | sort -u)
5. Read review state: ${reviewStateJsonPath} 5. Read review state: ${reviewStateJsonPath}
6. Execute: cat ~/.ccw/workflows/cli-templates/schemas/review-dimension-results-schema.json (get output schema reference) 6. Execute: cat ~/.ccw/workflows/cli-templates/schemas/review-dimension-results-schema.json (get output schema reference)
7. Execute: ccw spec load --keywords "exploration execution" (technology stack and constraints) 7. Execute: ccw spec load --category "exploration execution" (technology stack and constraints)
--- ---
@@ -334,7 +334,7 @@ const deepDiveAgentId = spawn_agent({
4. Identify related code: bash(grep -r "import.*${basename(file)}" ${projectDir}/src --include="*.ts") 4. Identify related code: bash(grep -r "import.*${basename(file)}" ${projectDir}/src --include="*.ts")
5. Read test files: bash(find ${projectDir}/tests -name "*${basename(file, '.ts')}*" -type f) 5. Read test files: bash(find ${projectDir}/tests -name "*${basename(file, '.ts')}*" -type f)
6. Execute: cat ~/.ccw/workflows/cli-templates/schemas/review-deep-dive-results-schema.json (get output schema reference) 6. Execute: cat ~/.ccw/workflows/cli-templates/schemas/review-deep-dive-results-schema.json (get output schema reference)
7. Execute: ccw spec load --keywords "exploration execution" (technology stack and constraints for remediation) 7. Execute: ccw spec load --category "exploration execution" (technology stack and constraints for remediation)
--- ---

View File

@@ -105,7 +105,7 @@ const agentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/cli-planning-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/cli-planning-agent.md (MUST read first)
2. Execute: ccw spec load --keywords planning 2. Execute: ccw spec load --category planning
--- ---

View File

@@ -60,7 +60,7 @@ const execAgentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/cli-execution-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/cli-execution-agent.md (MUST read first)
2. Execute: ccw spec load --keywords execution 2. Execute: ccw spec load --category execution
--- ---

View File

@@ -38,7 +38,7 @@ completion report.
### Step 1: Load Context ### Step 1: Load Context
After reading role definition: After reading role definition:
- Run: `ccw spec load --keywords execution` - Run: `ccw spec load --category execution`
- Extract issue ID, solution file path, session dir from task message - Extract issue ID, solution file path, session dir from task message
### Step 2: Load Solution ### Step 2: Load Solution

View File

@@ -48,7 +48,7 @@ Outputs `ISSUE_READY:{issueId}` after each solution and waits for orchestrator t
### Step 1: Load Context ### Step 1: Load Context
After reading role definition, load project context: After reading role definition, load project context:
- Run: `ccw spec load --keywords planning` - Run: `ccw spec load --category planning`
- Extract session directory and artifacts directory from task message - Extract session directory and artifacts directory from task message
### Step 2: Parse Input ### Step 2: Parse Input
@@ -81,7 +81,7 @@ spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/issue-plan-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/issue-plan-agent.md (MUST read first)
2. Run: `ccw spec load --keywords planning` 2. Run: `ccw spec load --category planning`
--- ---

View File

@@ -85,7 +85,7 @@ const plannerAgent = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/planex-planner.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/planex-planner.md (MUST read first)
2. Run: `ccw spec load --keywords "planning execution"` 2. Run: `ccw spec load --category "planning execution"`
--- ---
@@ -155,7 +155,7 @@ while (true) {
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/planex-executor.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/planex-executor.md (MUST read first)
2. Run: `ccw spec load --keywords "planning execution"` 2. Run: `ccw spec load --category "planning execution"`
--- ---

View File

@@ -88,7 +88,7 @@ const agentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/{agent-type}.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/{agent-type}.md (MUST read first)
2. Run: `ccw spec load --keywords "planning execution"` 2. Run: `ccw spec load --category "planning execution"`
## TASK CONTEXT ## TASK CONTEXT
${taskContext} ${taskContext}

View File

@@ -75,7 +75,7 @@ const contextAgentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/test-context-search-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/test-context-search-agent.md (MUST read first)
2. Run: `ccw spec load --keywords planning` 2. Run: `ccw spec load --category planning`
--- ---
@@ -101,7 +101,7 @@ const contextAgentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/context-search-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/context-search-agent.md (MUST read first)
2. Run: `ccw spec load --keywords planning` 2. Run: `ccw spec load --category planning`
--- ---
@@ -174,7 +174,7 @@ const analysisAgentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/cli-execution-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/cli-execution-agent.md (MUST read first)
2. Run: `ccw spec load --keywords planning` 2. Run: `ccw spec load --category planning`
--- ---
@@ -242,7 +242,7 @@ const taskGenAgentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/action-planning-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/action-planning-agent.md (MUST read first)
2. Run: `ccw spec load --keywords planning` 2. Run: `ccw spec load --category planning`
--- ---

View File

@@ -90,7 +90,7 @@ const analysisAgentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/cli-planning-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/cli-planning-agent.md (MUST read first)
2. Run: `ccw spec load --keywords planning` 2. Run: `ccw spec load --category planning`
--- ---
@@ -156,7 +156,7 @@ const fixAgentId = spawn_agent({
### MANDATORY FIRST STEPS (Agent Execute) ### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/test-fix-agent.md (MUST read first) 1. **Read role definition**: ~/.codex/agents/test-fix-agent.md (MUST read first)
2. Run: `ccw spec load --keywords execution` 2. Run: `ccw spec load --category execution`
--- ---

View File

@@ -5,6 +5,7 @@
import { useState, useEffect, useCallback, useMemo } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
@@ -28,7 +29,7 @@ import {
Plug, Plug,
Download, Download,
CheckCircle2, CheckCircle2,
ExternalLink, Settings,
} from 'lucide-react'; } from 'lucide-react';
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings'; import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
@@ -325,32 +326,34 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex items-center gap-4"> <div className="flex items-center justify-between">
<Button <div className="flex items-center gap-4">
onClick={handleInstallAllHooks} <Button
disabled={allHooksInstalled || installingHookIds.length > 0} onClick={handleInstallAllHooks}
> disabled={allHooksInstalled || installingHookIds.length > 0}
{allHooksInstalled ? ( >
<> {allHooksInstalled ? (
<CheckCircle2 className="h-4 w-4 mr-2" /> <>
{formatMessage({ id: 'specs.allHooksInstalled', defaultMessage: 'All Hooks Installed' })} <CheckCircle2 className="h-4 w-4 mr-2" />
</> {formatMessage({ id: 'specs.allHooksInstalled', defaultMessage: 'All Hooks Installed' })}
) : ( </>
<> ) : (
<Download className="h-4 w-4 mr-2" /> <>
{formatMessage({ id: 'specs.installAllHooks', defaultMessage: 'Install All Hooks' })} <Download className="h-4 w-4 mr-2" />
</> {formatMessage({ id: 'specs.installAllHooks', defaultMessage: 'Install All Hooks' })}
)} </>
</Button> )}
<div className="text-sm text-muted-foreground"> </Button>
{installedCount} / {RECOMMENDED_HOOKS.length}{' '} <div className="text-sm text-muted-foreground">
{formatMessage({ id: 'specs.hooksInstalled', defaultMessage: 'installed' })} {installedCount} / {RECOMMENDED_HOOKS.length}{' '}
{formatMessage({ id: 'specs.hooksInstalled', defaultMessage: 'installed' })}
</div>
</div> </div>
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" asChild>
<a href="/hooks" target="_blank" rel="noopener noreferrer"> <Link to="/hooks">
<ExternalLink className="h-4 w-4 mr-1" /> <Settings className="h-4 w-4 mr-1" />
{formatMessage({ id: 'specs.manageHooks', defaultMessage: 'Manage Hooks' })} {formatMessage({ id: 'specs.manageHooks', defaultMessage: 'Manage Hooks' })}
</a> </Link>
</Button> </Button>
</div> </div>

View File

@@ -23,6 +23,10 @@ import {
Trash2, Trash2,
FileText, FileText,
Tag, Tag,
Eye,
Globe,
Folder,
Layers,
} from 'lucide-react'; } from 'lucide-react';
// ========== Types ========== // ========== Types ==========
@@ -32,6 +36,11 @@ import {
*/ */
export type SpecDimension = 'specs' | 'personal'; export type SpecDimension = 'specs' | 'personal';
/**
* Spec scope type
*/
export type SpecScope = 'global' | 'project';
/** /**
* Spec read mode type * Spec read mode type
*/ */
@@ -42,6 +51,11 @@ export type SpecReadMode = 'required' | 'optional';
*/ */
export type SpecPriority = 'critical' | 'high' | 'medium' | 'low'; export type SpecPriority = 'critical' | 'high' | 'medium' | 'low';
/**
* Spec category type for workflow stage-based loading
*/
export type SpecCategory = 'general' | 'exploration' | 'planning' | 'execution';
/** /**
* Spec data structure * Spec data structure
*/ */
@@ -54,6 +68,10 @@ export interface Spec {
file: string; file: string;
/** Spec dimension/category */ /** Spec dimension/category */
dimension: SpecDimension; dimension: SpecDimension;
/** Scope: global (from ~/.ccw/) or project (from .ccw/) */
scope: SpecScope;
/** Workflow stage category for system-level loading */
category?: SpecCategory;
/** Read mode: required (always inject) or optional (keyword match) */ /** Read mode: required (always inject) or optional (keyword match) */
readMode: SpecReadMode; readMode: SpecReadMode;
/** Priority level */ /** Priority level */
@@ -72,6 +90,8 @@ export interface Spec {
export interface SpecCardProps { export interface SpecCardProps {
/** Spec data */ /** Spec data */
spec: Spec; spec: Spec;
/** Called when view content action is triggered */
onView?: (spec: Spec) => void;
/** Called when edit action is triggered */ /** Called when edit action is triggered */
onEdit?: (spec: Spec) => void; onEdit?: (spec: Spec) => void;
/** Called when delete action is triggered */ /** Called when delete action is triggered */
@@ -108,6 +128,17 @@ const priorityConfig: Record<
low: { variant: 'secondary', labelKey: 'specs.priority.low' }, low: { variant: 'secondary', labelKey: 'specs.priority.low' },
}; };
// Category badge configuration for workflow stage
const categoryConfig: Record<
SpecCategory,
{ variant: 'default' | 'secondary' | 'outline'; labelKey: string }
> = {
general: { variant: 'secondary', labelKey: 'specs.category.general' },
exploration: { variant: 'outline', labelKey: 'specs.category.exploration' },
planning: { variant: 'outline', labelKey: 'specs.category.planning' },
execution: { variant: 'outline', labelKey: 'specs.category.execution' },
};
// ========== Component ========== // ========== Component ==========
/** /**
@@ -115,6 +146,7 @@ const priorityConfig: Record<
*/ */
export function SpecCard({ export function SpecCard({
spec, spec,
onView,
onEdit, onEdit,
onDelete, onDelete,
onToggle, onToggle,
@@ -181,6 +213,10 @@ export function SpecCard({
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onView?.(spec); }}>
<Eye className="mr-2 h-4 w-4" />
{formatMessage({ id: 'specs.actions.view', defaultMessage: 'View Content' })}
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleAction(e, 'edit')}> <DropdownMenuItem onClick={(e) => handleAction(e, 'edit')}>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
{formatMessage({ id: 'specs.actions.edit' })} {formatMessage({ id: 'specs.actions.edit' })}
@@ -201,6 +237,29 @@ export function SpecCard({
{/* Badges */} {/* Badges */}
<div className="mt-3 flex flex-wrap items-center gap-2"> <div className="mt-3 flex flex-wrap items-center gap-2">
{/* Category badge - workflow stage */}
{spec.category && (
<Badge variant={categoryConfig[spec.category].variant} className="text-xs gap-1">
<Layers className="h-3 w-3" />
{formatMessage({ id: categoryConfig[spec.category].labelKey, defaultMessage: spec.category })}
</Badge>
)}
{/* Scope badge - only show for personal specs */}
{spec.dimension === 'personal' && (
<Badge variant="outline" className="text-xs gap-1">
{spec.scope === 'global' ? (
<>
<Globe className="h-3 w-3" />
{formatMessage({ id: 'specs.scope.global', defaultMessage: 'Global' })}
</>
) : (
<>
<Folder className="h-3 w-3" />
{formatMessage({ id: 'specs.scope.project', defaultMessage: 'Project' })}
</>
)}
</Badge>
)}
<Badge variant={readMode.variant} className="text-xs"> <Badge variant={readMode.variant} className="text-xs">
{formatMessage({ id: readMode.labelKey })} {formatMessage({ id: readMode.labelKey })}
</Badge> </Badge>

View File

@@ -11,8 +11,10 @@ export {
export type { export type {
Spec, Spec,
SpecDimension, SpecDimension,
SpecScope,
SpecReadMode, SpecReadMode,
SpecPriority, SpecPriority,
SpecCategory,
SpecCardProps, SpecCardProps,
} from './SpecCard'; } from './SpecCard';

View File

@@ -350,6 +350,7 @@ export const specsSettingsKeys = {
all: ['specsSettings'] as const, all: ['specsSettings'] as const,
systemSettings: () => [...specsSettingsKeys.all, 'systemSettings'] as const, systemSettings: () => [...specsSettingsKeys.all, 'systemSettings'] as const,
specStats: (projectPath?: string) => [...specsSettingsKeys.all, 'specStats', projectPath] as const, specStats: (projectPath?: string) => [...specsSettingsKeys.all, 'specStats', projectPath] as const,
specsList: (projectPath?: string) => [...specsSettingsKeys.all, 'specsList', projectPath] as const,
}; };
// ======================================== // ========================================
@@ -492,7 +493,7 @@ export function useSpecsList(options: UseSpecsListOptions = {}): UseSpecsListRet
const { projectPath, enabled = true, staleTime = STALE_TIME } = options; const { projectPath, enabled = true, staleTime = STALE_TIME } = options;
const query = useQuery({ const query = useQuery({
queryKey: specsSettingsKeys.specStats(projectPath), // Reuse for specs list queryKey: specsSettingsKeys.specsList(projectPath),
queryFn: () => getSpecsList(projectPath), queryFn: () => getSpecsList(projectPath),
staleTime, staleTime,
enabled, enabled,
@@ -528,6 +529,7 @@ export function useRebuildSpecIndex(options: UseRebuildSpecIndexOptions = {}) {
onSuccess: () => { onSuccess: () => {
// Invalidate specs list and stats queries to refresh data // Invalidate specs list and stats queries to refresh data
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) }); queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) });
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specsList(projectPath) });
}, },
}); });
@@ -560,8 +562,9 @@ export function useUpdateSpecFrontmatter(options: UseUpdateSpecFrontmatterOption
mutationFn: ({ file, readMode }: { file: string; readMode: string }) => mutationFn: ({ file, readMode }: { file: string; readMode: string }) =>
updateSpecFrontmatter(file, readMode, projectPath), updateSpecFrontmatter(file, readMode, projectPath),
onSuccess: () => { onSuccess: () => {
// Invalidate specs list to refresh data // Invalidate specs list and stats to refresh data
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) }); queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) });
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specsList(projectPath) });
}, },
}); });

View File

@@ -7268,9 +7268,11 @@ export interface SpecEntry {
file: string; file: string;
title: string; title: string;
dimension: string; dimension: string;
category?: 'general' | 'exploration' | 'planning' | 'execution';
readMode: 'required' | 'optional' | 'keywords'; readMode: 'required' | 'optional' | 'keywords';
priority: 'critical' | 'high' | 'medium' | 'low'; priority: 'critical' | 'high' | 'medium' | 'low';
keywords: string[]; keywords: string[];
scope: 'global' | 'project';
} }
/** /**

View File

@@ -10,36 +10,67 @@
"rebuildIndex": "Rebuild Index", "rebuildIndex": "Rebuild Index",
"loading": "Loading...", "loading": "Loading...",
"noSpecs": "No specs found. Create specs in .ccw/ directory.", "noSpecs": "No specs found. Create specs in .ccw/ directory.",
"required": "required",
"dimension": {
"specs": "Project Specs",
"personal": "Personal"
},
"scope": {
"all": "All",
"global": "Global",
"project": "Project"
},
"filterByScope": "Filter by scope:",
"category": {
"general": "General",
"exploration": "Exploration",
"planning": "Planning",
"execution": "Execution"
},
"recommendedHooks": "Recommended Hooks", "recommendedHooks": "Recommended Hooks",
"recommendedHooksDesc": "One-click install system-preset spec injection hooks", "recommendedHooksDesc": "One-click install system-preset spec injection hooks",
"installAll": "Install All Recommended Hooks", "installAll": "Install All Recommended Hooks",
"installAllHooks": "Install All Hooks",
"allHooksInstalled": "All Hooks Installed",
"hooksInstalled": "installed",
"manageHooks": "Manage Hooks",
"hookEvent": "Event",
"hookScope": "Scope",
"install": "Install",
"installed": "Installed",
"installing": "Installing...",
"installedHooks": "Installed Hooks", "installedHooks": "Installed Hooks",
"installedHooksDesc": "Manage your installed hooks configuration", "installedHooksDesc": "Manage your installed hooks configuration",
"searchHooks": "Search hooks...", "searchHooks": "Search hooks...",
"noHooks": "No hooks installed. Install recommended hooks above.", "noHooks": "No hooks installed. Install recommended hooks above.",
"spec": { "spec": {
"edit": "Edit", "edit": "Edit Spec",
"toggle": "Toggle", "toggle": "Toggle Status",
"delete": "Delete", "delete": "Delete Spec",
"required": "Required", "deleteConfirm": "Are you sure you want to delete this spec?",
"optional": "Optional", "title": "Spec Title",
"priority": { "keywords": "Keywords",
"critical": "Critical", "keywordsPlaceholder": "Enter keywords, separated by commas",
"high": "High", "readMode": "Read Mode",
"medium": "Medium", "priority": "Priority",
"low": "Low" "file": "File Path"
}
}, },
"hook": { "hook": {
"install": "Install", "install": "Install",
"edit": "Edit", "uninstall": "Uninstall",
"toggle": "Toggle", "edit": "Edit Hook",
"delete": "Delete", "toggle": "Toggle Status",
"delete": "Delete Hook",
"enabled": "Enabled", "enabled": "Enabled",
"disabled": "Disabled", "disabled": "Disabled",
"installed": "Installed",
"notInstalled": "Not Installed",
"scope": { "scope": {
"global": "Global", "global": "Global",
"project": "Project" "project": "Project"
@@ -48,21 +79,142 @@
"SessionStart": "Session Start", "SessionStart": "Session Start",
"UserPromptSubmit": "Prompt Submit", "UserPromptSubmit": "Prompt Submit",
"SessionEnd": "Session End" "SessionEnd": "Session End"
},
"name": "Hook Name",
"eventLabel": "Trigger Event",
"command": "Command",
"scopeLabel": "Scope",
"timeout": "Timeout (ms)",
"failMode": "Fail Mode",
"failModeContinue": "Continue",
"failModeBlock": "Block",
"failModeWarn": "Warn"
},
"actions": {
"edit": "Edit",
"delete": "Delete",
"reset": "Reset",
"save": "Save",
"saving": "Saving...",
"view": "View Content"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled"
},
"readMode": {
"required": "Required",
"optional": "Optional"
},
"priority": {
"critical": "Critical",
"high": "High",
"medium": "Medium",
"low": "Low"
},
"hooks": {
"dialog": {
"createTitle": "Create Hook",
"editTitle": "Edit Hook",
"description": "Configure the hook trigger event, command, and other settings."
},
"fields": {
"name": "Hook Name",
"event": "Trigger Event",
"scope": "Scope",
"command": "Command",
"description": "Description",
"timeout": "Timeout",
"timeoutUnit": "ms",
"failMode": "Failure Mode"
},
"placeholders": {
"name": "Enter hook name",
"event": "Select event",
"command": "Enter command to execute",
"description": "Enter description (optional)"
},
"events": {
"sessionStart": "Session Start",
"userPromptSubmit": "Prompt Submit",
"sessionEnd": "Session End"
},
"scope": {
"global": "Global",
"project": "Project"
},
"failModes": {
"continue": "Continue",
"warn": "Show Warning",
"block": "Block Operation"
},
"validation": {
"nameRequired": "Name is required",
"commandRequired": "Command is required",
"timeoutMin": "Minimum timeout is 1000ms",
"timeoutMax": "Maximum timeout is 300000ms"
} }
}, },
"hints": {
"hookEvents": "Select when this hook should be triggered",
"hookScope": "Global hooks apply to all projects, project hooks only to current project",
"hookCommand": "Command to execute, can use environment variables",
"hookTimeout": "Timeout for command execution",
"hookFailMode": "How to handle command execution failure"
},
"common": {
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"reset": "Reset",
"confirm": "Confirm",
"close": "Close"
},
"content": {
"edit": "Edit",
"view": "View",
"metadata": "Metadata",
"markdownContent": "Markdown Content",
"noContent": "No content available",
"editHint": "Edit the full markdown content including frontmatter. Changes to frontmatter will be reflected in the spec metadata.",
"placeholder": "# Spec Title\n\nContent here..."
},
"injection": { "injection": {
"title": "Injection Control", "title": "Injection Control",
"description": "Monitor and manage spec injection length", "statusTitle": "Current Injection Status",
"settingsTitle": "Injection Control Settings",
"settingsDescription": "Configure how spec content is injected into AI context.",
"currentLength": "Current Length", "currentLength": "Current Length",
"maxLength": "Max Length", "maxLength": "Max Injection Length (characters)",
"maxLengthHelp": "Recommended: 4000-10000. Too large may consume too much context; too small may truncate important specs.",
"warnThreshold": "Warn Threshold", "warnThreshold": "Warn Threshold",
"warnThresholdLabel": "Warning Threshold (characters)",
"warnThresholdHelp": "A warning will be displayed when injection length exceeds this value.",
"percentage": "Usage", "percentage": "Usage",
"truncateOnExceed": "Truncate on Exceed", "truncateOnExceed": "Truncate on Exceed",
"truncateDescription": "Automatically truncate when injection exceeds max length", "truncateHelp": "Automatically truncate content when it exceeds the maximum length.",
"overLimit": "Over Limit", "overLimit": "Over Limit",
"warning": "Warning", "overLimitDescription": "Current injection content exceeds maximum length of {max} characters. Excess content will be truncated.",
"normal": "Normal" "warning": "Approaching Limit",
"normal": "Normal",
"characters": "characters",
"statsInfo": "Statistics",
"requiredLength": "Required specs length:",
"matchedLength": "Keyword-matched length:",
"remaining": "Remaining space:",
"loadError": "Failed to load stats",
"saveSuccess": "Settings saved successfully",
"saveError": "Failed to save settings"
}, },
"settings": { "settings": {
@@ -70,6 +222,7 @@
"description": "Configure personal spec defaults and system settings", "description": "Configure personal spec defaults and system settings",
"personalSpecDefaults": "Personal Spec Defaults", "personalSpecDefaults": "Personal Spec Defaults",
"defaultReadMode": "Default Read Mode", "defaultReadMode": "Default Read Mode",
"defaultReadModeHelp": "Default read mode for newly created personal specs",
"autoEnable": "Auto Enable", "autoEnable": "Auto Enable",
"autoEnableDescription": "Automatically enable newly created personal specs" "autoEnableDescription": "Automatically enable newly created personal specs"
}, },
@@ -77,17 +230,25 @@
"dialog": { "dialog": {
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"close": "Close",
"editSpec": "Edit Spec", "editSpec": "Edit Spec",
"editHook": "Edit Hook", "editHook": "Edit Hook",
"confirmDelete": "Confirm Delete",
"specTitle": "Spec Title", "specTitle": "Spec Title",
"keywords": "Keywords", "keywords": "Keywords",
"readMode": "Read Mode", "readMode": "Read Mode",
"priority": "Priority", "priority": "Priority",
"hookName": "Hook Name", "hookName": "Hook Name",
"hookEvent": "Event", "hookEvent": "Trigger Event",
"hookCommand": "Command", "hookCommand": "Command",
"hookScope": "Scope", "hookScope": "Scope",
"hookTimeout": "Timeout (ms)", "hookTimeout": "Timeout (ms)",
"hookFailMode": "Fail Mode" "hookFailMode": "Fail Mode"
},
"form": {
"readMode": "Read Mode",
"priority": "Priority",
"keywords": "Keywords"
} }
} }

View File

@@ -10,16 +10,47 @@
"rebuildIndex": "重建索引", "rebuildIndex": "重建索引",
"loading": "加载中...", "loading": "加载中...",
"noSpecs": "未找到规范。请在 .ccw/ 目录中创建规范文件。", "noSpecs": "未找到规范。请在 .ccw/ 目录中创建规范文件。",
"required": "必读",
"dimension": {
"specs": "项目规范",
"personal": "个人规范"
},
"scope": {
"all": "全部",
"global": "全局",
"project": "项目"
},
"filterByScope": "按范围筛选:",
"category": {
"general": "通用",
"exploration": "探索",
"planning": "规划",
"execution": "执行"
},
"recommendedHooks": "推荐钩子", "recommendedHooks": "推荐钩子",
"recommendedHooksDesc": "一键安装系统预设的规范注入钩子", "recommendedHooksDesc": "一键安装系统预设的规范注入钩子",
"installAll": "安装所有推荐钩子", "installAll": "安装所有推荐钩子",
"installAllHooks": "安装所有钩子",
"allHooksInstalled": "已安装所有钩子",
"hooksInstalled": "已安装",
"manageHooks": "管理钩子",
"hookEvent": "事件",
"hookScope": "范围",
"install": "安装",
"installed": "已安装",
"installing": "安装中...",
"installedHooks": "已安装钩子", "installedHooks": "已安装钩子",
"installedHooksDesc": "管理已安装的钩子配置", "installedHooksDesc": "管理已安装的钩子配置",
"searchHooks": "搜索钩子...", "searchHooks": "搜索钩子...",
"noHooks": "未安装钩子。请安装上方的推荐钩子。", "noHooks": "未安装钩子。请安装上方的推荐钩子。",
"actions": { "actions": {
"view": "查看内容",
"edit": "编辑", "edit": "编辑",
"delete": "删除", "delete": "删除",
"reset": "重置", "reset": "重置",
@@ -45,6 +76,7 @@
}, },
"spec": { "spec": {
"view": "查看内容",
"edit": "编辑规范", "edit": "编辑规范",
"toggle": "切换状态", "toggle": "切换状态",
"delete": "删除规范", "delete": "删除规范",
@@ -57,6 +89,23 @@
"file": "文件路径" "file": "文件路径"
}, },
"content": {
"edit": "编辑",
"view": "查看",
"metadata": "元数据",
"markdownContent": "Markdown 内容",
"noContent": "无内容",
"editHint": "编辑完整的 Markdown 内容(包括 frontmatter。frontmatter 的更改将反映到规范元数据中。",
"placeholder": "# 规范标题\n\n内容..."
},
"common": {
"cancel": "取消",
"save": "保存",
"saving": "保存中...",
"close": "关闭"
},
"hook": { "hook": {
"install": "安装", "install": "安装",
"uninstall": "卸载", "uninstall": "卸载",
@@ -88,6 +137,9 @@
}, },
"hooks": { "hooks": {
"installSuccess": "钩子安装成功",
"installError": "钩子安装失败",
"installAllSuccess": "所有钩子安装成功",
"dialog": { "dialog": {
"createTitle": "创建钩子", "createTitle": "创建钩子",
"editTitle": "编辑钩子", "editTitle": "编辑钩子",
@@ -122,6 +174,12 @@
"continue": "继续执行", "continue": "继续执行",
"warn": "显示警告", "warn": "显示警告",
"block": "阻止操作" "block": "阻止操作"
},
"validation": {
"nameRequired": "名称为必填项",
"commandRequired": "命令为必填项",
"timeoutMin": "最小超时时间为 1000ms",
"timeoutMax": "最大超时时间为 300000ms"
} }
}, },
@@ -133,18 +191,8 @@
"hookFailMode": "命令执行失败时的处理方式" "hookFailMode": "命令执行失败时的处理方式"
}, },
"common": {
"cancel": "取消",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"reset": "重置",
"confirm": "确认"
},
"injection": { "injection": {
"title": "注入控制", "title": "注入控制",
"description": "监控和管理规范注入长度",
"statusTitle": "当前注入状态", "statusTitle": "当前注入状态",
"settingsTitle": "注入控制设置", "settingsTitle": "注入控制设置",
"settingsDescription": "配置如何将规范内容注入到 AI 上下文中。", "settingsDescription": "配置如何将规范内容注入到 AI 上下文中。",
@@ -198,5 +246,11 @@
"hookScope": "作用域", "hookScope": "作用域",
"hookTimeout": "超时时间(ms)", "hookTimeout": "超时时间(ms)",
"hookFailMode": "失败模式" "hookFailMode": "失败模式"
},
"form": {
"readMode": "读取模式",
"priority": "优先级",
"keywords": "关键词"
} }
} }

View File

@@ -3,6 +3,7 @@
* *
* Main page for managing spec settings, injection control, and global settings. * Main page for managing spec settings, injection control, and global settings.
* Uses 4 tabs: Project Specs | Personal Specs | Injection | Settings * Uses 4 tabs: Project Specs | Personal Specs | Injection | Settings
* Supports category filtering (workflow stage) and scope filtering (personal only)
*/ */
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -10,8 +11,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs';
import { Card, CardContent } from '@/components/ui/Card'; import { Card, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { ScrollText, User, Gauge, Settings, RefreshCw, Search } from 'lucide-react'; import { ScrollText, User, Gauge, Settings, RefreshCw, Search, Globe, Folder, Filter, Layers } from 'lucide-react';
import { SpecCard, SpecDialog, SpecContentDialog, type Spec, type SpecFormData } from '@/components/specs'; import { SpecCard, SpecDialog, SpecContentDialog, type Spec, type SpecFormData, type SpecCategory } from '@/components/specs';
import { InjectionControlTab } from '@/components/specs/InjectionControlTab'; import { InjectionControlTab } from '@/components/specs/InjectionControlTab';
import { GlobalSettingsTab } from '@/components/specs/GlobalSettingsTab'; import { GlobalSettingsTab } from '@/components/specs/GlobalSettingsTab';
import { useSpecStats, useSpecsList, useRebuildSpecIndex } from '@/hooks/useSystemSettings'; import { useSpecStats, useSpecsList, useRebuildSpecIndex } from '@/hooks/useSystemSettings';
@@ -19,6 +20,11 @@ import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import type { SpecEntry } from '@/lib/api'; import type { SpecEntry } from '@/lib/api';
type SettingsTab = 'project-specs' | 'personal-specs' | 'injection' | 'settings'; type SettingsTab = 'project-specs' | 'personal-specs' | 'injection' | 'settings';
type PersonalScopeFilter = 'all' | 'global' | 'project';
type CategoryFilter = 'all' | SpecCategory;
// All available categories
const SPEC_CATEGORIES: SpecCategory[] = ['general', 'exploration', 'planning', 'execution'];
// Convert SpecEntry to Spec for display // Convert SpecEntry to Spec for display
function specEntryToSpec(entry: SpecEntry, dimension: string): Spec { function specEntryToSpec(entry: SpecEntry, dimension: string): Spec {
@@ -26,6 +32,8 @@ function specEntryToSpec(entry: SpecEntry, dimension: string): Spec {
id: entry.file, id: entry.file,
title: entry.title, title: entry.title,
dimension: dimension as Spec['dimension'], dimension: dimension as Spec['dimension'],
scope: entry.scope || 'project', // Default to project if not specified
category: entry.category || 'general', // Default to general if not specified
keywords: entry.keywords, keywords: entry.keywords,
readMode: entry.readMode as Spec['readMode'], readMode: entry.readMode as Spec['readMode'],
priority: entry.priority as Spec['priority'], priority: entry.priority as Spec['priority'],
@@ -39,8 +47,12 @@ export function SpecsSettingsPage() {
const projectPath = useWorkflowStore(selectProjectPath); const projectPath = useWorkflowStore(selectProjectPath);
const [activeTab, setActiveTab] = useState<SettingsTab>('project-specs'); const [activeTab, setActiveTab] = useState<SettingsTab>('project-specs');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [personalScopeFilter, setPersonalScopeFilter] = useState<PersonalScopeFilter>('all');
const [categoryFilter, setCategoryFilter] = useState<CategoryFilter>('all');
const [editDialogOpen, setEditDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
const [contentDialogOpen, setContentDialogOpen] = useState(false);
const [editingSpec, setEditingSpec] = useState<Spec | null>(null); const [editingSpec, setEditingSpec] = useState<Spec | null>(null);
const [viewingSpec, setViewingSpec] = useState<Spec | null>(null);
// Fetch real data // Fetch real data
const { data: specsListData, isLoading: specsLoading, refetch: refetchSpecs } = useSpecsList({ projectPath }); const { data: specsListData, isLoading: specsLoading, refetch: refetchSpecs } = useSpecsList({ projectPath });
@@ -48,26 +60,50 @@ export function SpecsSettingsPage() {
const rebuildMutation = useRebuildSpecIndex(); const rebuildMutation = useRebuildSpecIndex();
// Convert specs data to display format // Convert specs data to display format
const { projectSpecs, personalSpecs } = useMemo(() => { const { projectSpecs, personalSpecs, globalPersonalSpecs, projectPersonalSpecs, categoryCounts } = useMemo(() => {
if (!specsListData?.specs) { if (!specsListData?.specs) {
return { projectSpecs: [], personalSpecs: [] }; return {
projectSpecs: [],
personalSpecs: [],
globalPersonalSpecs: [],
projectPersonalSpecs: [],
categoryCounts: { general: 0, exploration: 0, planning: 0, execution: 0 }
};
} }
const specs: Spec[] = []; const specs: Spec[] = [];
const personal: Spec[] = []; const personal: Spec[] = [];
const globalPersonal: Spec[] = [];
const projectPersonal: Spec[] = [];
const counts: Record<SpecCategory, number> = { general: 0, exploration: 0, planning: 0, execution: 0 };
for (const [dimension, entries] of Object.entries(specsListData.specs)) { for (const [dimension, entries] of Object.entries(specsListData.specs)) {
for (const entry of entries) { for (const entry of entries) {
const spec = specEntryToSpec(entry, dimension); const spec = specEntryToSpec(entry, dimension);
// Count by category
if (spec.category) {
counts[spec.category]++;
}
if (dimension === 'personal') { if (dimension === 'personal') {
personal.push(spec); personal.push(spec);
if (spec.scope === 'global') {
globalPersonal.push(spec);
} else {
projectPersonal.push(spec);
}
} else { } else {
specs.push(spec); specs.push(spec);
} }
} }
} }
return { projectSpecs: specs, personalSpecs: personal }; return {
projectSpecs: specs,
personalSpecs: personal,
globalPersonalSpecs: globalPersonal,
projectPersonalSpecs: projectPersonal,
categoryCounts: counts
};
}, [specsListData]); }, [specsListData]);
const isLoading = specsLoading; const isLoading = specsLoading;
@@ -113,16 +149,34 @@ export function SpecsSettingsPage() {
}; };
const filterSpecs = (specs: Spec[]) => { const filterSpecs = (specs: Spec[]) => {
if (!searchQuery.trim()) return specs; let result = specs;
const query = searchQuery.toLowerCase(); // Filter by category
return specs.filter(spec => if (categoryFilter !== 'all') {
spec.title.toLowerCase().includes(query) || result = result.filter(spec => spec.category === categoryFilter);
spec.keywords.some(k => k.toLowerCase().includes(query)) }
); // Filter by search query
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
result = result.filter(spec =>
spec.title.toLowerCase().includes(query) ||
spec.keywords.some(k => k.toLowerCase().includes(query))
);
}
return result;
}; };
const renderSpecsTab = (dimension: 'project' | 'personal') => { const renderSpecsTab = (dimension: 'project' | 'personal') => {
const specs = dimension === 'project' ? projectSpecs : personalSpecs; let specs = dimension === 'project' ? projectSpecs : personalSpecs;
// Apply scope filter for personal specs
if (dimension === 'personal') {
if (personalScopeFilter === 'global') {
specs = globalPersonalSpecs;
} else if (personalScopeFilter === 'project') {
specs = projectPersonalSpecs;
}
}
const filteredSpecs = filterSpecs(specs); const filteredSpecs = filterSpecs(specs);
return ( return (
@@ -144,13 +198,77 @@ export function SpecsSettingsPage() {
</Button> </Button>
</div> </div>
{/* Scope filter for personal specs */}
{dimension === 'personal' && (
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{formatMessage({ id: 'specs.filterByScope', defaultMessage: 'Filter by scope:' })}
</span>
<div className="flex gap-2">
<Button
variant={personalScopeFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setPersonalScopeFilter('all')}
>
{formatMessage({ id: 'specs.scope.all', defaultMessage: 'All' })} ({personalSpecs.length})
</Button>
<Button
variant={personalScopeFilter === 'global' ? 'default' : 'outline'}
size="sm"
onClick={() => setPersonalScopeFilter('global')}
>
<Globe className="h-3 w-3 mr-1" />
{formatMessage({ id: 'specs.scope.global', defaultMessage: 'Global' })} ({globalPersonalSpecs.length})
</Button>
<Button
variant={personalScopeFilter === 'project' ? 'default' : 'outline'}
size="sm"
onClick={() => setPersonalScopeFilter('project')}
>
<Folder className="h-3 w-3 mr-1" />
{formatMessage({ id: 'specs.scope.project', defaultMessage: 'Project' })} ({projectPersonalSpecs.length})
</Button>
</div>
</div>
)}
{/* Category filter for workflow stage */}
<div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{formatMessage({ id: 'specs.filterByCategory', defaultMessage: 'Workflow stage:' })}
</span>
<div className="flex gap-2 flex-wrap">
<Button
variant={categoryFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter('all')}
>
{formatMessage({ id: 'specs.category.all', defaultMessage: 'All' })}
</Button>
{SPEC_CATEGORIES.map(cat => (
<Button
key={cat}
variant={categoryFilter === cat ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter(cat)}
>
{formatMessage({ id: `specs.category.${cat}`, defaultMessage: cat })} ({categoryCounts[cat]})
</Button>
))}
</div>
</div>
{/* Stats Summary */} {/* Stats Summary */}
{statsData?.dimensions && ( {statsData?.dimensions && (
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{Object.entries(statsData.dimensions).map(([dim, data]) => ( {Object.entries(statsData.dimensions).map(([dim, data]) => (
<Card key={dim}> <Card key={dim}>
<CardContent className="pt-4"> <CardContent className="pt-4">
<div className="text-sm text-muted-foreground capitalize">{dim}</div> <div className="text-sm text-muted-foreground">
{formatMessage({ id: `specs.dimension.${dim}`, defaultMessage: dim })}
</div>
<div className="text-2xl font-bold">{(data as { count: number }).count}</div> <div className="text-2xl font-bold">{(data as { count: number }).count}</div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{(data as { requiredCount: number }).requiredCount} {formatMessage({ id: 'specs.required', defaultMessage: 'required' })} {(data as { requiredCount: number }).requiredCount} {formatMessage({ id: 'specs.required', defaultMessage: 'required' })}
@@ -167,7 +285,7 @@ export function SpecsSettingsPage() {
<CardContent className="py-8 text-center text-muted-foreground"> <CardContent className="py-8 text-center text-muted-foreground">
{isLoading {isLoading
? formatMessage({ id: 'specs.loading', defaultMessage: 'Loading specs...' }) ? formatMessage({ id: 'specs.loading', defaultMessage: 'Loading specs...' })
: formatMessage({ id: 'specs.noSpecs', defaultMessage: 'No specs found. Create specs in .workflow/ directory.' }) : formatMessage({ id: 'specs.noSpecs', defaultMessage: 'No specs found. Create specs in .ccw/ directory.' })
} }
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -376,13 +376,13 @@ ${chalk.bold('EXAMPLES')}
ccw spec init ccw spec init
${chalk.gray('# Load exploration-phase specs:')} ${chalk.gray('# Load exploration-phase specs:')}
ccw spec load --keywords exploration ccw spec load --category exploration
${chalk.gray('# Load planning-phase specs with auth topic:')} ${chalk.gray('# Load planning-phase specs with auth topic:')}
ccw spec load --keywords "planning auth" ccw spec load --category "planning auth"
${chalk.gray('# Load execution-phase specs:')} ${chalk.gray('# Load execution-phase specs:')}
ccw spec load --keywords execution ccw spec load --category execution
${chalk.gray('# Load specs for a topic (CLI mode):')} ${chalk.gray('# Load specs for a topic (CLI mode):')}
ccw spec load --dimension specs --keywords "auth jwt security" ccw spec load --dimension specs --keywords "auth jwt security"

View File

@@ -10,6 +10,7 @@
* --- * ---
* title: "Document Title" * title: "Document Title"
* dimension: "specs" * dimension: "specs"
* category: "general" # general | exploration | planning | execution
* keywords: ["auth", "security"] * keywords: ["auth", "security"]
* readMode: "required" # required | optional * readMode: "required" # required | optional
* priority: "high" # critical | high | medium | low * priority: "high" # critical | high | medium | low
@@ -19,21 +20,25 @@
import matter from 'gray-matter'; import matter from 'gray-matter';
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs'; import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
import { join, basename, extname, relative } from 'path'; import { join, basename, extname, relative } from 'path';
import { homedir } from 'os';
// ============================================================================ // ============================================================================
// Types // Types
// ============================================================================ // ============================================================================
/** /**
* Spec categories for workflow stage-based loading (used as keywords). * Spec categories for workflow stage-based loading.
* - general: Applies to all stages (e.g. coding conventions)
* - exploration: Code exploration, analysis, debugging context * - exploration: Code exploration, analysis, debugging context
* - planning: Task planning, roadmap, requirements context * - planning: Task planning, roadmap, requirements context
* - execution: Implementation, testing, deployment context * - execution: Implementation, testing, deployment context
* *
* Usage: Add these as keywords in spec frontmatter, e.g.: * Usage: Set category field in spec frontmatter:
* keywords: [exploration, auth, security] * category: exploration
*
* System-level loading by stage: ccw spec load --category exploration
*/ */
export const SPEC_CATEGORIES = ['exploration', 'planning', 'execution'] as const; export const SPEC_CATEGORIES = ['general', 'exploration', 'planning', 'execution'] as const;
export type SpecCategory = typeof SPEC_CATEGORIES[number]; export type SpecCategory = typeof SPEC_CATEGORIES[number];
@@ -43,6 +48,7 @@ export type SpecCategory = typeof SPEC_CATEGORIES[number];
export interface SpecFrontmatter { export interface SpecFrontmatter {
title: string; title: string;
dimension: string; dimension: string;
category?: SpecCategory;
keywords: string[]; keywords: string[];
readMode: 'required' | 'optional'; readMode: 'required' | 'optional';
priority: 'critical' | 'high' | 'medium' | 'low'; priority: 'critical' | 'high' | 'medium' | 'low';
@@ -58,12 +64,16 @@ export interface SpecIndexEntry {
file: string; file: string;
/** Dimension this spec belongs to */ /** Dimension this spec belongs to */
dimension: string; dimension: string;
/** Keywords for matching against user prompts (may include category markers) */ /** Workflow stage category for system-level loading */
category: SpecCategory;
/** Keywords for matching against user prompts */
keywords: string[]; keywords: string[];
/** Whether this spec is required or optional */ /** Whether this spec is required or optional */
readMode: 'required' | 'optional'; readMode: 'required' | 'optional';
/** Priority level for ordering */ /** Priority level for ordering */
priority: 'critical' | 'high' | 'medium' | 'low'; priority: 'critical' | 'high' | 'medium' | 'low';
/** Scope: global (from ~/.ccw/) or project (from .ccw/) */
scope: 'global' | 'project';
} }
/** /**
@@ -101,6 +111,11 @@ const VALID_READ_MODES = ['required', 'optional'] as const;
*/ */
const VALID_PRIORITIES = ['critical', 'high', 'medium', 'low'] as const; const VALID_PRIORITIES = ['critical', 'high', 'medium', 'low'] as const;
/**
* Valid category values.
*/
const VALID_CATEGORIES = SPEC_CATEGORIES;
/** /**
* Directory name for spec index cache files (inside .ccw/). * Directory name for spec index cache files (inside .ccw/).
*/ */
@@ -149,45 +164,42 @@ export async function buildDimensionIndex(
projectPath: string, projectPath: string,
dimension: string dimension: string
): Promise<DimensionIndex> { ): Promise<DimensionIndex> {
const dimensionDir = getDimensionDir(projectPath, dimension);
const entries: SpecIndexEntry[] = []; const entries: SpecIndexEntry[] = [];
// If directory doesn't exist, return empty index // Helper function to scan a directory and add entries
if (!existsSync(dimensionDir)) { const scanDirectory = (dir: string, scope: 'global' | 'project') => {
return { if (!existsSync(dir)) return;
dimension,
entries: [],
built_at: new Date().toISOString(),
};
}
// Scan for .md files let files: string[];
let files: string[]; try {
try { files = readdirSync(dir).filter(f => extname(f).toLowerCase() === '.md');
files = readdirSync(dimensionDir).filter( } catch {
f => extname(f).toLowerCase() === '.md' return;
);
} catch {
// Directory read error - return empty index
return {
dimension,
entries: [],
built_at: new Date().toISOString(),
};
}
for (const file of files) {
const filePath = join(dimensionDir, file);
const entry = parseSpecFile(filePath, dimension, projectPath);
if (entry) {
entries.push(entry);
} else {
process.stderr.write(
`[spec-index-builder] Skipping malformed spec file: ${file}\n`
);
} }
for (const file of files) {
const filePath = join(dir, file);
const entry = parseSpecFile(filePath, dimension, projectPath, scope);
if (entry) {
entries.push(entry);
} else {
process.stderr.write(
`[spec-index-builder] Skipping malformed spec file: ${file}\n`
);
}
}
};
// For personal dimension, also scan global ~/.ccw/personal/
if (dimension === 'personal') {
const globalPersonalDir = join(homedir(), '.ccw', 'personal');
scanDirectory(globalPersonalDir, 'global');
} }
// Scan project dimension directory
const dimensionDir = getDimensionDir(projectPath, dimension);
scanDirectory(dimensionDir, 'project');
return { return {
dimension, dimension,
entries, entries,
@@ -315,7 +327,8 @@ export async function getDimensionIndex(
function parseSpecFile( function parseSpecFile(
filePath: string, filePath: string,
dimension: string, dimension: string,
projectPath: string projectPath: string,
scope: 'global' | 'project' = 'project'
): SpecIndexEntry | null { ): SpecIndexEntry | null {
let content: string; let content: string;
try { try {
@@ -340,10 +353,10 @@ function parseSpecFile(
if (!title) { if (!title) {
// Title is required - use filename as fallback // Title is required - use filename as fallback
const fallbackTitle = basename(filePath, extname(filePath)); const fallbackTitle = basename(filePath, extname(filePath));
return buildEntry(fallbackTitle, filePath, dimension, projectPath, data); return buildEntry(fallbackTitle, filePath, dimension, projectPath, data, scope);
} }
return buildEntry(title, filePath, dimension, projectPath, data); return buildEntry(title, filePath, dimension, projectPath, data, scope);
} }
/** /**
@@ -354,12 +367,17 @@ function buildEntry(
filePath: string, filePath: string,
dimension: string, dimension: string,
projectPath: string, projectPath: string,
data: Record<string, unknown> data: Record<string, unknown>,
scope: 'global' | 'project' = 'project'
): SpecIndexEntry { ): SpecIndexEntry {
// Compute relative file path from project root using path.relative // Compute relative file path from project root using path.relative
// Normalize to forward slashes for cross-platform consistency // Normalize to forward slashes for cross-platform consistency
const relativePath = relative(projectPath, filePath).replace(/\\/g, '/'); const relativePath = relative(projectPath, filePath).replace(/\\/g, '/');
// Extract category with validation (defaults to 'general')
const rawCategory = extractString(data, 'category');
const category = isValidCategory(rawCategory) ? rawCategory : 'general';
// Extract keywords - accept string[] or single string // Extract keywords - accept string[] or single string
const keywords = extractStringArray(data, 'keywords'); const keywords = extractStringArray(data, 'keywords');
@@ -375,9 +393,11 @@ function buildEntry(
title, title,
file: relativePath, file: relativePath,
dimension, dimension,
category,
keywords, keywords,
readMode, readMode,
priority, priority,
scope,
}; };
} }
@@ -435,3 +455,10 @@ function isValidReadMode(value: string | null): value is 'required' | 'optional'
function isValidPriority(value: string | null): value is 'critical' | 'high' | 'medium' | 'low' { function isValidPriority(value: string | null): value is 'critical' | 'high' | 'medium' | 'low' {
return value !== null && (VALID_PRIORITIES as readonly string[]).includes(value); return value !== null && (VALID_PRIORITIES as readonly string[]).includes(value);
} }
/**
* Type guard for valid category values.
*/
function isValidCategory(value: string | null): value is SpecCategory {
return value !== null && (VALID_CATEGORIES as readonly string[]).includes(value);
}

View File

@@ -18,6 +18,7 @@ import { join } from 'path';
export interface SpecFrontmatter { export interface SpecFrontmatter {
title: string; title: string;
dimension: string; dimension: string;
category?: 'general' | 'exploration' | 'planning' | 'execution';
keywords: string[]; keywords: string[];
readMode: 'required' | 'optional'; readMode: 'required' | 'optional';
priority: 'high' | 'medium' | 'low'; priority: 'high' | 'medium' | 'low';
@@ -55,7 +56,8 @@ export const SEED_DOCS: Map<string, SeedDoc[]> = new Map([
frontmatter: { frontmatter: {
title: 'Coding Conventions', title: 'Coding Conventions',
dimension: 'specs', dimension: 'specs',
keywords: ['typescript', 'naming', 'style', 'convention', 'exploration', 'planning', 'execution'], category: 'general',
keywords: ['typescript', 'naming', 'style', 'convention'],
readMode: 'required', readMode: 'required',
priority: 'high', priority: 'high',
}, },
@@ -91,7 +93,8 @@ export const SEED_DOCS: Map<string, SeedDoc[]> = new Map([
frontmatter: { frontmatter: {
title: 'Architecture Constraints', title: 'Architecture Constraints',
dimension: 'specs', dimension: 'specs',
keywords: ['architecture', 'module', 'layer', 'pattern', 'exploration', 'planning'], category: 'planning',
keywords: ['architecture', 'module', 'layer', 'pattern'],
readMode: 'required', readMode: 'required',
priority: 'high', priority: 'high',
}, },
@@ -126,6 +129,7 @@ export const SEED_DOCS: Map<string, SeedDoc[]> = new Map([
frontmatter: { frontmatter: {
title: 'Personal Coding Style', title: 'Personal Coding Style',
dimension: 'personal', dimension: 'personal',
category: 'general',
keywords: ['style', 'preference'], keywords: ['style', 'preference'],
readMode: 'optional', readMode: 'optional',
priority: 'medium', priority: 'medium',
@@ -153,6 +157,7 @@ export const SEED_DOCS: Map<string, SeedDoc[]> = new Map([
frontmatter: { frontmatter: {
title: 'Tool Preferences', title: 'Tool Preferences',
dimension: 'personal', dimension: 'personal',
category: 'general',
keywords: ['tool', 'cli', 'editor'], keywords: ['tool', 'cli', 'editor'],
readMode: 'optional', readMode: 'optional',
priority: 'low', priority: 'low',
@@ -186,16 +191,22 @@ export const SEED_DOCS: Map<string, SeedDoc[]> = new Map([
*/ */
export function formatFrontmatter(fm: SpecFrontmatter): string { export function formatFrontmatter(fm: SpecFrontmatter): string {
const keywordsYaml = fm.keywords.map((k) => ` - ${k}`).join('\n'); const keywordsYaml = fm.keywords.map((k) => ` - ${k}`).join('\n');
return [ const lines = [
'---', '---',
`title: "${fm.title}"`, `title: "${fm.title}"`,
`dimension: ${fm.dimension}`, `dimension: ${fm.dimension}`,
];
if (fm.category) {
lines.push(`category: ${fm.category}`);
}
lines.push(
`keywords:`, `keywords:`,
keywordsYaml, keywordsYaml,
`readMode: ${fm.readMode}`, `readMode: ${fm.readMode}`,
`priority: ${fm.priority}`, `priority: ${fm.priority}`,
'---', '---'
].join('\n'); );
return lines.join('\n');
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------