From dfa8e0d9f563e993dfc85b9aa7a555f72075e1b4 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Thu, 26 Feb 2026 23:43:55 +0800 Subject: [PATCH] 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. --- .claude/agents/cli-explore-agent.md | 2 +- .claude/agents/cli-lite-planning-agent.md | 2 +- .claude/agents/context-search-agent.md | 2 +- .claude/agents/debug-explore-agent.md | 2 +- .claude/commands/workflow/init-guidelines.md | 2 +- .claude/skills/team-issue/roles/explorer.md | 2 +- .../roles/explorer/commands/explore.md | 2 +- .../team-ultra-analyze/roles/explorer/role.md | 2 +- .../workflow-execute/phases/06-review.md | 8 +- .claude/skills/workflow-lite-plan/SKILL.md | 4 +- .../workflow-lite-plan/phases/01-lite-plan.md | 2 +- .../phases/02-lite-execute.md | 4 +- .../skills/workflow-multi-cli-plan/SKILL.md | 4 +- .../phases/02-context-gathering.md | 2 +- .../phases/04-task-generation.md | 2 +- .../phases/02-context-gathering.md | 2 +- .../phases/05-tdd-task-generation.md | 4 +- .../phases/02-test-context-gather.md | 2 +- .../phases/05-test-cycle-execute.md | 2 +- .codex/prompts/prep-cycle.md | 6 +- .codex/prompts/prep-plan.md | 6 +- .codex/skills/analyze-with-file/SKILL.md | 4 +- .codex/skills/brainstorm-with-file/SKILL.md | 6 +- .../collaborative-plan-with-file/SKILL.md | 2 +- .codex/skills/issue-discover/SKILL.md | 2 +- .codex/skills/req-plan-with-file/SKILL.md | 2 +- .codex/skills/review-cycle/SKILL.md | 2 +- .../review-cycle/phases/02-parallel-review.md | 6 +- .../phases/07-fix-parallel-planning.md | 2 +- .../review-cycle/phases/08-fix-execution.md | 2 +- .codex/skills/team-planex/agents/executor.md | 2 +- .codex/skills/team-planex/agents/planner.md | 4 +- .codex/skills/team-planex/orchestrator.md | 4 +- .../skills/workflow-test-fix-cycle/SKILL.md | 2 +- .../phases/01-test-fix-gen.md | 8 +- .../phases/02-test-cycle-execute.md | 4 +- .../components/specs/InjectionControlTab.tsx | 51 ++--- .../src/components/specs/SpecCard.tsx | 59 +++++ ccw/frontend/src/components/specs/index.ts | 2 + ccw/frontend/src/hooks/useSystemSettings.ts | 7 +- ccw/frontend/src/lib/api.ts | 2 + ccw/frontend/src/locales/en/specs.json | 201 ++++++++++++++++-- ccw/frontend/src/locales/zh/specs.json | 74 ++++++- ccw/frontend/src/pages/SpecsSettingsPage.tsx | 148 +++++++++++-- ccw/src/commands/spec.ts | 6 +- ccw/src/tools/spec-index-builder.ts | 111 ++++++---- ccw/src/tools/spec-init.ts | 21 +- 47 files changed, 619 insertions(+), 179 deletions(-) diff --git a/.claude/agents/cli-explore-agent.md b/.claude/agents/cli-explore-agent.md index c0d61b9f..b72493de 100644 --- a/.claude/agents/cli-explore-agent.md +++ b/.claude/agents/cli-explore-agent.md @@ -55,7 +55,7 @@ Phase 4: Output Generation Read and memorize schema requirements BEFORE any analysis begins (feeds Phase 3 validation). 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` - Usage: Align analysis scope and patterns with actual project technology choices - If no specs are returned, proceed with fresh analysis (no error). diff --git a/.claude/agents/cli-lite-planning-agent.md b/.claude/agents/cli-lite-planning-agent.md index 8e5b1ccb..893a8f8f 100644 --- a/.claude/agents/cli-lite-planning-agent.md +++ b/.claude/agents/cli-lite-planning-agent.md @@ -55,7 +55,7 @@ When invoked with `process_docs: true` in input context: ## Input Context **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 { diff --git a/.claude/agents/context-search-agent.md b/.claude/agents/context-search-agent.md index 2f18a9ce..f1efce6b 100644 --- a/.claude/agents/context-search-agent.md +++ b/.claude/agents/context-search-agent.md @@ -79,7 +79,7 @@ if (file_exists(contextPackagePath)) { ```javascript // Load project-level context (from spec system) // 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 projectGuidelines = projectSpecs?.coding_conventions ? projectSpecs : null; diff --git a/.claude/agents/debug-explore-agent.md b/.claude/agents/debug-explore-agent.md index 7e39a17e..ba591711 100644 --- a/.claude/agents/debug-explore-agent.md +++ b/.claude/agents/debug-explore-agent.md @@ -36,7 +36,7 @@ Phase 5: Fix & Verification ## Phase 1: Bug Analysis **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**: ```javascript diff --git a/.claude/commands/workflow/init-guidelines.md b/.claude/commands/workflow/init-guidelines.md index d8035259..26423cf7 100644 --- a/.claude/commands/workflow/init-guidelines.md +++ b/.claude/commands/workflow/init-guidelines.md @@ -99,7 +99,7 @@ if (isPopulated) { ```javascript // 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) // Extract key info from loaded specs for generating smart questions diff --git a/.claude/skills/team-issue/roles/explorer.md b/.claude/skills/team-issue/roles/explorer.md index 9923a318..71e397b8 100644 --- a/.claude/skills/team-issue/roles/explorer.md +++ b/.claude/skills/team-issue/roles/explorer.md @@ -148,7 +148,7 @@ Priority: ## MANDATORY FIRST STEPS 1. Run: ccw tool exec get_modules_by_depth '{}' 2. Execute ACE searches based on issue keywords -3. Run: ccw spec load --keywords exploration +3. Run: ccw spec load --category exploration ## Exploration Focus - Identify files directly related to this issue diff --git a/.claude/skills/team-ultra-analyze/roles/explorer/commands/explore.md b/.claude/skills/team-ultra-analyze/roles/explorer/commands/explore.md index b6ea82ee..a5db791d 100644 --- a/.claude/skills/team-ultra-analyze/roles/explorer/commands/explore.md +++ b/.claude/skills/team-ultra-analyze/roles/explorer/commands/explore.md @@ -81,7 +81,7 @@ Session: ${sessionFolder} ## MANDATORY FIRST STEPS 1. Run: ccw tool exec get_modules_by_depth '{}' 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) - **Depth**: ${strategy.depth} diff --git a/.claude/skills/team-ultra-analyze/roles/explorer/role.md b/.claude/skills/team-ultra-analyze/roles/explorer/role.md index 505bca78..96aa9d22 100644 --- a/.claude/skills/team-ultra-analyze/roles/explorer/role.md +++ b/.claude/skills/team-ultra-analyze/roles/explorer/role.md @@ -138,7 +138,7 @@ Session: ## MANDATORY FIRST STEPS 1. Run: ccw tool exec get_modules_by_depth '{}' 2. Execute relevant searches based on topic keywords -3. Run: ccw spec load --keywords exploration +3. Run: ccw spec load --category exploration ## Exploration Focus ( angle) diff --git a/.claude/skills/workflow-execute/phases/06-review.md b/.claude/skills/workflow-execute/phases/06-review.md index 0cb28090..57799840 100644 --- a/.claude/skills/workflow-execute/phases/06-review.md +++ b/.claude/skills/workflow-execute/phases/06-review.md @@ -93,7 +93,7 @@ rg "password|token|secret|auth" -g "*.{ts,js,py}" rg "eval|exec|innerHTML|dangerouslySetInnerHTML" -g "*.{ts,js,tsx}" # Gemini security analysis -ccw spec load --keywords execution +ccw spec load --category execution ccw cli -p " PURPOSE: Security audit of completed implementation 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`): ```bash -ccw spec load --keywords execution +ccw spec load --category execution ccw cli -p " PURPOSE: Architecture compliance review 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`): ```bash -ccw spec load --keywords execution +ccw spec load --category execution ccw cli -p " PURPOSE: Code quality and best practices review TASK: Assess code readability, maintainability, adherence to best practices @@ -139,7 +139,7 @@ for task_file in ${sessionPath}/.task/*.json; do done # Cross-check implementation against requirements -ccw spec load --keywords execution +ccw spec load --category execution ccw cli -p " PURPOSE: Verify all requirements and acceptance criteria are met TASK: Cross-check implementation summaries against original requirements diff --git a/.claude/skills/workflow-lite-plan/SKILL.md b/.claude/skills/workflow-lite-plan/SKILL.md index 93bddbda..25cba7ea 100644 --- a/.claude/skills/workflow-lite-plan/SKILL.md +++ b/.claude/skills/workflow-lite-plan/SKILL.md @@ -127,10 +127,10 @@ After collecting preferences, enhance context and dispatch: ```javascript // 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 -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) if (mode === 'plan') { diff --git a/.claude/skills/workflow-lite-plan/phases/01-lite-plan.md b/.claude/skills/workflow-lite-plan/phases/01-lite-plan.md index 7a2767bf..0efc642f 100644 --- a/.claude/skills/workflow-lite-plan/phases/01-lite-plan.md +++ b/.claude/skills/workflow-lite-plan/phases/01-lite-plan.md @@ -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) ## 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. **CRITICAL**: All generated tasks MUST comply with constraints in specs/*.md diff --git a/.claude/skills/workflow-lite-plan/phases/02-lite-execute.md b/.claude/skills/workflow-lite-plan/phases/02-lite-execute.md index bb420559..38c29dd6 100644 --- a/.claude/skills/workflow-lite-plan/phases/02-lite-execute.md +++ b/.claude/skills/workflow-lite-plan/phases/02-lite-execute.md @@ -485,8 +485,8 @@ ${(t.test?.success_metrics || []).length > 0 ? `\n**Success metrics**: ${t.test. context.push(`### Artifacts\nPlan: ${executionContext.session.artifacts.plan}`) } // Project guidelines (user-defined constraints from /workflow:session:solidify) - // Loaded via: ccw spec load --keywords planning - context.push(`### Project Guidelines\n(Loaded via ccw spec load --keywords planning)`) + // Loaded via: ccw spec load --category 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')}`) sections.push(`Complete each task according to its "Done when" checklist.`) diff --git a/.claude/skills/workflow-multi-cli-plan/SKILL.md b/.claude/skills/workflow-multi-cli-plan/SKILL.md index 51b68074..903abc55 100644 --- a/.claude/skills/workflow-multi-cli-plan/SKILL.md +++ b/.claude/skills/workflow-multi-cli-plan/SKILL.md @@ -101,10 +101,10 @@ After collecting preferences, enhance context and dispatch: ```javascript // 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 -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) if (mode === 'plan') { diff --git a/.claude/skills/workflow-plan/phases/02-context-gathering.md b/.claude/skills/workflow-plan/phases/02-context-gathering.md index 1509f9b6..6748f7e5 100644 --- a/.claude/skills/workflow-plan/phases/02-context-gathering.md +++ b/.claude/skills/workflow-plan/phases/02-context-gathering.md @@ -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. 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 - Generate prioritized_context with user_intent alignment - Apply specs/*.md constraints during conflict detection diff --git a/.claude/skills/workflow-plan/phases/04-task-generation.md b/.claude/skills/workflow-plan/phases/04-task-generation.md index f14e43c0..523ff891 100644 --- a/.claude/skills/workflow-plan/phases/04-task-generation.md +++ b/.claude/skills/workflow-plan/phases/04-task-generation.md @@ -171,7 +171,7 @@ Session ID: ${sessionId} MCP Capabilities: {exa_code, exa_web, code_index} ## 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, build system, test framework diff --git a/.claude/skills/workflow-tdd/phases/02-context-gathering.md b/.claude/skills/workflow-tdd/phases/02-context-gathering.md index b6b01505..4fe12b2f 100644 --- a/.claude/skills/workflow-tdd/phases/02-context-gathering.md +++ b/.claude/skills/workflow-tdd/phases/02-context-gathering.md @@ -221,7 +221,7 @@ Execute complete context-search-agent workflow for TDD implementation planning: ### Phase 1: Initialization & Pre-Analysis 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. 2. **Detection**: Check for existing context-package (early exit if valid) 3. **Foundation**: Initialize CodexLens, get project structure, load docs diff --git a/.claude/skills/workflow-tdd/phases/05-tdd-task-generation.md b/.claude/skills/workflow-tdd/phases/05-tdd-task-generation.md index bf3922e2..a99b2b4f 100644 --- a/.claude/skills/workflow-tdd/phases/05-tdd-task-generation.md +++ b/.claude/skills/workflow-tdd/phases/05-tdd-task-generation.md @@ -231,14 +231,14 @@ MCP Capabilities: {exa_code, exa_web, code_index} ## PROJECT CONTEXT (MANDATORY - load before planning-notes) 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 - 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, acceptance criteria, and convergence.verification MUST respect these guidelines - 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) Execution Method: ${userConfig.executionMethod} // agent|hybrid|cli diff --git a/.claude/skills/workflow-test-fix/phases/02-test-context-gather.md b/.claude/skills/workflow-test-fix/phases/02-test-context-gather.md index ac8f2e4d..74c3c48f 100644 --- a/.claude/skills/workflow-test-fix/phases/02-test-context-gather.md +++ b/.claude/skills/workflow-test-fix/phases/02-test-context-gather.md @@ -345,7 +345,7 @@ Execute complete context-search-agent workflow for implementation planning: ### Phase 1: Initialization & Pre-Analysis 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. 2. **Detection**: Check for existing context-package (early exit if valid) 3. **Foundation**: Initialize CodexLens, get project structure, load docs diff --git a/.claude/skills/workflow-test-fix/phases/05-test-cycle-execute.md b/.claude/skills/workflow-test-fix/phases/05-test-cycle-execute.md index 218fb1fb..7fdb5ed2 100644 --- a/.claude/skills/workflow-test-fix/phases/05-test-cycle-execute.md +++ b/.claude/skills/workflow-test-fix/phases/05-test-cycle-execute.md @@ -244,7 +244,7 @@ Task( ${selectedStrategy} - ${strategyDescription} ## 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 1. Read test results: ${session.test_results_path} diff --git a/.codex/prompts/prep-cycle.md b/.codex/prompts/prep-cycle.md index 47b28f48..d47b2f81 100644 --- a/.codex/prompts/prep-cycle.md +++ b/.codex/prompts/prep-cycle.md @@ -21,7 +21,7 @@ Check these items. Report results as a checklist. ### 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], 是否正确?需要补充什么?" - **Test framework**: Detect from config files (jest.config, vitest.config, pytest.ini, etc.) - If missing: Ask user: "未检测到测试框架配置,请指定测试命令(如 `npm test`, `pytest`),或输入 'skip' 跳过测试验证" @@ -36,7 +36,7 @@ Print formatted checklist: ✓ 项目根目录: D:\myproject ✓ 工作空间: .workflow/.cycle/ 就绪 ⚠ Git: 3 个未提交变更 -✓ Project specs: 已加载 (ccw spec load --keywords execution) +✓ Project specs: 已加载 (ccw spec load --category execution) ⚠ specs: 未找到 (已跳过) ✓ 测试框架: 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: - **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 ### 2.5 Assemble Refined Task diff --git a/.codex/prompts/prep-plan.md b/.codex/prompts/prep-plan.md index 6c511f42..5c9242f6 100644 --- a/.codex/prompts/prep-plan.md +++ b/.codex/prompts/prep-plan.md @@ -21,7 +21,7 @@ Check these items. Report results as a checklist. ### 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], 是否正确?需要补充什么?" - **Test framework**: Detect from config files (jest.config, vitest.config, pytest.ini, etc.) - If missing: Ask: "未检测到测试框架,请指定测试命令(如 `npm test`),或输入 'skip' 跳过" @@ -36,7 +36,7 @@ Print formatted checklist: ✓ 项目根目录: D:\myproject ✓ .workflow/ 目录就绪 ⚠ Git: 3 个未提交变更 -✓ Project specs: 已加载 (ccw spec load --keywords planning) +✓ Project specs: 已加载 (ccw spec load --category planning) ⚠ specs: 未找到 (Phase 1 将初始化) ✓ 测试框架: 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: - **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` ### 2.5 Assemble Structured Description diff --git a/.codex/skills/analyze-with-file/SKILL.md b/.codex/skills/analyze-with-file/SKILL.md index f3d38e47..32773434 100644 --- a/.codex/skills/analyze-with-file/SKILL.md +++ b/.codex/skills/analyze-with-file/SKILL.md @@ -85,7 +85,7 @@ Step 1: Topic Understanding Step 2: Exploration (Inline, No Agents) ├─ 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 ├─ Multi-perspective analysis (if selected, serial) │ ├─ Single: Comprehensive analysis @@ -297,7 +297,7 @@ const hasCodebase = Bash(` if (hasCodebase !== 'none') { // 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) // 2. Search codebase for relevant content diff --git a/.codex/skills/brainstorm-with-file/SKILL.md b/.codex/skills/brainstorm-with-file/SKILL.md index 22f4a453..3bfc2cf9 100644 --- a/.codex/skills/brainstorm-with-file/SKILL.md +++ b/.codex/skills/brainstorm-with-file/SKILL.md @@ -282,7 +282,7 @@ Use built-in tools to understand the codebase structure before spawning perspect **Context Gathering Activities**: 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 -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 **exploration-codebase.json Structure**: @@ -358,7 +358,7 @@ const agentIds = perspectives.map(perspective => { ### MANDATORY FIRST STEPS (Agent Execute) 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 --- @@ -566,7 +566,7 @@ const deepDiveAgent = spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 1. **Read role definition**: ~/.codex/agents/cli-explore-agent.md (MUST read first) 2. Read: ${sessionFolder}/perspectives.json (prior findings) -3. Run: `ccw spec load --keywords "exploration planning"` +3. Run: `ccw spec load --category "exploration planning"` --- diff --git a/.codex/skills/collaborative-plan-with-file/SKILL.md b/.codex/skills/collaborative-plan-with-file/SKILL.md index 0936fea1..ea4bcca4 100644 --- a/.codex/skills/collaborative-plan-with-file/SKILL.md +++ b/.codex/skills/collaborative-plan-with-file/SKILL.md @@ -194,7 +194,7 @@ Use built-in tools directly to understand the task scope and identify sub-domain **Analysis Activities**: 1. **Search for references** — Find related documentation, README files, and architecture guides - 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 3. **Identify ambiguities** — List any unclear points or multiple possible interpretations 4. **Clarify with user** — If ambiguities found, use AskUserQuestion for clarification diff --git a/.codex/skills/issue-discover/SKILL.md b/.codex/skills/issue-discover/SKILL.md index a9fa27bf..4562522d 100644 --- a/.codex/skills/issue-discover/SKILL.md +++ b/.codex/skills/issue-discover/SKILL.md @@ -231,7 +231,7 @@ const agentId = spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 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 ${taskContext} diff --git a/.codex/skills/req-plan-with-file/SKILL.md b/.codex/skills/req-plan-with-file/SKILL.md index 93c4b8ff..ab07de73 100644 --- a/.codex/skills/req-plan-with-file/SKILL.md +++ b/.codex/skills/req-plan-with-file/SKILL.md @@ -268,7 +268,7 @@ const hasCodebase = bash(` // 2. Codebase Exploration (only when hasCodebase !== 'none') if (hasCodebase !== 'none') { // Read project metadata (if exists) - // Run `ccw spec load --keywords planning` + // Run `ccw spec load --category planning` // Search codebase for requirement-relevant context // Use: mcp__ace-tool__search_context, Grep, Glob, Read diff --git a/.codex/skills/review-cycle/SKILL.md b/.codex/skills/review-cycle/SKILL.md index bc758653..17bd76a2 100644 --- a/.codex/skills/review-cycle/SKILL.md +++ b/.codex/skills/review-cycle/SKILL.md @@ -303,7 +303,7 @@ const agentId = spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 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" --- diff --git a/.codex/skills/review-cycle/phases/02-parallel-review.md b/.codex/skills/review-cycle/phases/02-parallel-review.md index cb6d6c5c..71e4c059 100644 --- a/.codex/skills/review-cycle/phases/02-parallel-review.md +++ b/.codex/skills/review-cycle/phases/02-parallel-review.md @@ -94,7 +94,7 @@ dimensions.forEach(dimension => { 3. Get target files: Read resolved_files from review-state.json 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) -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) 5. Read review state: ${reviewStateJsonPath} 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") 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) -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) --- diff --git a/.codex/skills/review-cycle/phases/07-fix-parallel-planning.md b/.codex/skills/review-cycle/phases/07-fix-parallel-planning.md index c918b7fc..e69c3b80 100644 --- a/.codex/skills/review-cycle/phases/07-fix-parallel-planning.md +++ b/.codex/skills/review-cycle/phases/07-fix-parallel-planning.md @@ -105,7 +105,7 @@ const agentId = spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 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 --- diff --git a/.codex/skills/review-cycle/phases/08-fix-execution.md b/.codex/skills/review-cycle/phases/08-fix-execution.md index 5fc07fdd..a90f0f4f 100644 --- a/.codex/skills/review-cycle/phases/08-fix-execution.md +++ b/.codex/skills/review-cycle/phases/08-fix-execution.md @@ -60,7 +60,7 @@ const execAgentId = spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 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 --- diff --git a/.codex/skills/team-planex/agents/executor.md b/.codex/skills/team-planex/agents/executor.md index 93fa1e8a..02c09269 100644 --- a/.codex/skills/team-planex/agents/executor.md +++ b/.codex/skills/team-planex/agents/executor.md @@ -38,7 +38,7 @@ completion report. ### Step 1: Load Context 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 ### Step 2: Load Solution diff --git a/.codex/skills/team-planex/agents/planner.md b/.codex/skills/team-planex/agents/planner.md index 9d847853..8ca395db 100644 --- a/.codex/skills/team-planex/agents/planner.md +++ b/.codex/skills/team-planex/agents/planner.md @@ -48,7 +48,7 @@ Outputs `ISSUE_READY:{issueId}` after each solution and waits for orchestrator t ### Step 1: Load 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 ### Step 2: Parse Input @@ -81,7 +81,7 @@ spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 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` --- diff --git a/.codex/skills/team-planex/orchestrator.md b/.codex/skills/team-planex/orchestrator.md index 21f88812..c5f2c574 100644 --- a/.codex/skills/team-planex/orchestrator.md +++ b/.codex/skills/team-planex/orchestrator.md @@ -85,7 +85,7 @@ const plannerAgent = spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 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) 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"` --- diff --git a/.codex/skills/workflow-test-fix-cycle/SKILL.md b/.codex/skills/workflow-test-fix-cycle/SKILL.md index 60998e31..5efb749c 100644 --- a/.codex/skills/workflow-test-fix-cycle/SKILL.md +++ b/.codex/skills/workflow-test-fix-cycle/SKILL.md @@ -88,7 +88,7 @@ const agentId = spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 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 ${taskContext} diff --git a/.codex/skills/workflow-test-fix-cycle/phases/01-test-fix-gen.md b/.codex/skills/workflow-test-fix-cycle/phases/01-test-fix-gen.md index 9537e9d8..e1d9fab0 100644 --- a/.codex/skills/workflow-test-fix-cycle/phases/01-test-fix-gen.md +++ b/.codex/skills/workflow-test-fix-cycle/phases/01-test-fix-gen.md @@ -75,7 +75,7 @@ const contextAgentId = spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 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) 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) 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) 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` --- diff --git a/.codex/skills/workflow-test-fix-cycle/phases/02-test-cycle-execute.md b/.codex/skills/workflow-test-fix-cycle/phases/02-test-cycle-execute.md index ef1dcdd9..b2176232 100644 --- a/.codex/skills/workflow-test-fix-cycle/phases/02-test-cycle-execute.md +++ b/.codex/skills/workflow-test-fix-cycle/phases/02-test-cycle-execute.md @@ -90,7 +90,7 @@ const analysisAgentId = spawn_agent({ ### MANDATORY FIRST STEPS (Agent Execute) 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) 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` --- diff --git a/ccw/frontend/src/components/specs/InjectionControlTab.tsx b/ccw/frontend/src/components/specs/InjectionControlTab.tsx index faecfa35..bf047190 100644 --- a/ccw/frontend/src/components/specs/InjectionControlTab.tsx +++ b/ccw/frontend/src/components/specs/InjectionControlTab.tsx @@ -5,6 +5,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { @@ -28,7 +29,7 @@ import { Plug, Download, CheckCircle2, - ExternalLink, + Settings, } from 'lucide-react'; import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings'; @@ -325,32 +326,34 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) { -
- -
- {installedCount} / {RECOMMENDED_HOOKS.length}{' '} - {formatMessage({ id: 'specs.hooksInstalled', defaultMessage: 'installed' })} +
+
+ +
+ {installedCount} / {RECOMMENDED_HOOKS.length}{' '} + {formatMessage({ id: 'specs.hooksInstalled', defaultMessage: 'installed' })} +
diff --git a/ccw/frontend/src/components/specs/SpecCard.tsx b/ccw/frontend/src/components/specs/SpecCard.tsx index f77bbaa5..70d5c848 100644 --- a/ccw/frontend/src/components/specs/SpecCard.tsx +++ b/ccw/frontend/src/components/specs/SpecCard.tsx @@ -23,6 +23,10 @@ import { Trash2, FileText, Tag, + Eye, + Globe, + Folder, + Layers, } from 'lucide-react'; // ========== Types ========== @@ -32,6 +36,11 @@ import { */ export type SpecDimension = 'specs' | 'personal'; +/** + * Spec scope type + */ +export type SpecScope = 'global' | 'project'; + /** * Spec read mode type */ @@ -42,6 +51,11 @@ export type SpecReadMode = 'required' | 'optional'; */ 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 */ @@ -54,6 +68,10 @@ export interface Spec { file: string; /** Spec dimension/category */ 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) */ readMode: SpecReadMode; /** Priority level */ @@ -72,6 +90,8 @@ export interface Spec { export interface SpecCardProps { /** Spec data */ spec: Spec; + /** Called when view content action is triggered */ + onView?: (spec: Spec) => void; /** Called when edit action is triggered */ onEdit?: (spec: Spec) => void; /** Called when delete action is triggered */ @@ -108,6 +128,17 @@ const priorityConfig: Record< 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 ========== /** @@ -115,6 +146,7 @@ const priorityConfig: Record< */ export function SpecCard({ spec, + onView, onEdit, onDelete, onToggle, @@ -181,6 +213,10 @@ export function SpecCard({ + { e.stopPropagation(); onView?.(spec); }}> + + {formatMessage({ id: 'specs.actions.view', defaultMessage: 'View Content' })} + handleAction(e, 'edit')}> {formatMessage({ id: 'specs.actions.edit' })} @@ -201,6 +237,29 @@ export function SpecCard({ {/* Badges */}
+ {/* Category badge - workflow stage */} + {spec.category && ( + + + {formatMessage({ id: categoryConfig[spec.category].labelKey, defaultMessage: spec.category })} + + )} + {/* Scope badge - only show for personal specs */} + {spec.dimension === 'personal' && ( + + {spec.scope === 'global' ? ( + <> + + {formatMessage({ id: 'specs.scope.global', defaultMessage: 'Global' })} + + ) : ( + <> + + {formatMessage({ id: 'specs.scope.project', defaultMessage: 'Project' })} + + )} + + )} {formatMessage({ id: readMode.labelKey })} diff --git a/ccw/frontend/src/components/specs/index.ts b/ccw/frontend/src/components/specs/index.ts index 7b83bd62..46f021a3 100644 --- a/ccw/frontend/src/components/specs/index.ts +++ b/ccw/frontend/src/components/specs/index.ts @@ -11,8 +11,10 @@ export { export type { Spec, SpecDimension, + SpecScope, SpecReadMode, SpecPriority, + SpecCategory, SpecCardProps, } from './SpecCard'; diff --git a/ccw/frontend/src/hooks/useSystemSettings.ts b/ccw/frontend/src/hooks/useSystemSettings.ts index 0bf61781..8f71cf4a 100644 --- a/ccw/frontend/src/hooks/useSystemSettings.ts +++ b/ccw/frontend/src/hooks/useSystemSettings.ts @@ -350,6 +350,7 @@ export const specsSettingsKeys = { all: ['specsSettings'] as const, systemSettings: () => [...specsSettingsKeys.all, 'systemSettings'] 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 query = useQuery({ - queryKey: specsSettingsKeys.specStats(projectPath), // Reuse for specs list + queryKey: specsSettingsKeys.specsList(projectPath), queryFn: () => getSpecsList(projectPath), staleTime, enabled, @@ -528,6 +529,7 @@ export function useRebuildSpecIndex(options: UseRebuildSpecIndexOptions = {}) { onSuccess: () => { // Invalidate specs list and stats queries to refresh data 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 }) => updateSpecFrontmatter(file, readMode, projectPath), 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.specsList(projectPath) }); }, }); diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts index 3a6befe8..f28289cc 100644 --- a/ccw/frontend/src/lib/api.ts +++ b/ccw/frontend/src/lib/api.ts @@ -7268,9 +7268,11 @@ export interface SpecEntry { file: string; title: string; dimension: string; + category?: 'general' | 'exploration' | 'planning' | 'execution'; readMode: 'required' | 'optional' | 'keywords'; priority: 'critical' | 'high' | 'medium' | 'low'; keywords: string[]; + scope: 'global' | 'project'; } /** diff --git a/ccw/frontend/src/locales/en/specs.json b/ccw/frontend/src/locales/en/specs.json index e2c37d7a..78675e09 100644 --- a/ccw/frontend/src/locales/en/specs.json +++ b/ccw/frontend/src/locales/en/specs.json @@ -10,36 +10,67 @@ "rebuildIndex": "Rebuild Index", "loading": "Loading...", "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", "recommendedHooksDesc": "One-click install system-preset spec injection 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", "installedHooksDesc": "Manage your installed hooks configuration", "searchHooks": "Search hooks...", "noHooks": "No hooks installed. Install recommended hooks above.", "spec": { - "edit": "Edit", - "toggle": "Toggle", - "delete": "Delete", - "required": "Required", - "optional": "Optional", - "priority": { - "critical": "Critical", - "high": "High", - "medium": "Medium", - "low": "Low" - } + "edit": "Edit Spec", + "toggle": "Toggle Status", + "delete": "Delete Spec", + "deleteConfirm": "Are you sure you want to delete this spec?", + "title": "Spec Title", + "keywords": "Keywords", + "keywordsPlaceholder": "Enter keywords, separated by commas", + "readMode": "Read Mode", + "priority": "Priority", + "file": "File Path" }, "hook": { "install": "Install", - "edit": "Edit", - "toggle": "Toggle", - "delete": "Delete", + "uninstall": "Uninstall", + "edit": "Edit Hook", + "toggle": "Toggle Status", + "delete": "Delete Hook", "enabled": "Enabled", "disabled": "Disabled", + "installed": "Installed", + "notInstalled": "Not Installed", "scope": { "global": "Global", "project": "Project" @@ -48,21 +79,142 @@ "SessionStart": "Session Start", "UserPromptSubmit": "Prompt Submit", "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": { "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", - "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", + "warnThresholdLabel": "Warning Threshold (characters)", + "warnThresholdHelp": "A warning will be displayed when injection length exceeds this value.", "percentage": "Usage", "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", - "warning": "Warning", - "normal": "Normal" + "overLimitDescription": "Current injection content exceeds maximum length of {max} characters. Excess content will be truncated.", + "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": { @@ -70,6 +222,7 @@ "description": "Configure personal spec defaults and system settings", "personalSpecDefaults": "Personal Spec Defaults", "defaultReadMode": "Default Read Mode", + "defaultReadModeHelp": "Default read mode for newly created personal specs", "autoEnable": "Auto Enable", "autoEnableDescription": "Automatically enable newly created personal specs" }, @@ -77,17 +230,25 @@ "dialog": { "cancel": "Cancel", "save": "Save", + "close": "Close", "editSpec": "Edit Spec", "editHook": "Edit Hook", + "confirmDelete": "Confirm Delete", "specTitle": "Spec Title", "keywords": "Keywords", "readMode": "Read Mode", "priority": "Priority", "hookName": "Hook Name", - "hookEvent": "Event", + "hookEvent": "Trigger Event", "hookCommand": "Command", "hookScope": "Scope", "hookTimeout": "Timeout (ms)", "hookFailMode": "Fail Mode" + }, + + "form": { + "readMode": "Read Mode", + "priority": "Priority", + "keywords": "Keywords" } } diff --git a/ccw/frontend/src/locales/zh/specs.json b/ccw/frontend/src/locales/zh/specs.json index a367dcb6..d7c07a7d 100644 --- a/ccw/frontend/src/locales/zh/specs.json +++ b/ccw/frontend/src/locales/zh/specs.json @@ -10,16 +10,47 @@ "rebuildIndex": "重建索引", "loading": "加载中...", "noSpecs": "未找到规范。请在 .ccw/ 目录中创建规范文件。", + "required": "必读", + + "dimension": { + "specs": "项目规范", + "personal": "个人规范" + }, + + "scope": { + "all": "全部", + "global": "全局", + "project": "项目" + }, + "filterByScope": "按范围筛选:", + + "category": { + "general": "通用", + "exploration": "探索", + "planning": "规划", + "execution": "执行" + }, "recommendedHooks": "推荐钩子", "recommendedHooksDesc": "一键安装系统预设的规范注入钩子", "installAll": "安装所有推荐钩子", + "installAllHooks": "安装所有钩子", + "allHooksInstalled": "已安装所有钩子", + "hooksInstalled": "已安装", + "manageHooks": "管理钩子", + "hookEvent": "事件", + "hookScope": "范围", + "install": "安装", + "installed": "已安装", + "installing": "安装中...", + "installedHooks": "已安装钩子", "installedHooksDesc": "管理已安装的钩子配置", "searchHooks": "搜索钩子...", "noHooks": "未安装钩子。请安装上方的推荐钩子。", "actions": { + "view": "查看内容", "edit": "编辑", "delete": "删除", "reset": "重置", @@ -45,6 +76,7 @@ }, "spec": { + "view": "查看内容", "edit": "编辑规范", "toggle": "切换状态", "delete": "删除规范", @@ -57,6 +89,23 @@ "file": "文件路径" }, + "content": { + "edit": "编辑", + "view": "查看", + "metadata": "元数据", + "markdownContent": "Markdown 内容", + "noContent": "无内容", + "editHint": "编辑完整的 Markdown 内容(包括 frontmatter)。frontmatter 的更改将反映到规范元数据中。", + "placeholder": "# 规范标题\n\n内容..." + }, + + "common": { + "cancel": "取消", + "save": "保存", + "saving": "保存中...", + "close": "关闭" + }, + "hook": { "install": "安装", "uninstall": "卸载", @@ -88,6 +137,9 @@ }, "hooks": { + "installSuccess": "钩子安装成功", + "installError": "钩子安装失败", + "installAllSuccess": "所有钩子安装成功", "dialog": { "createTitle": "创建钩子", "editTitle": "编辑钩子", @@ -122,6 +174,12 @@ "continue": "继续执行", "warn": "显示警告", "block": "阻止操作" + }, + "validation": { + "nameRequired": "名称为必填项", + "commandRequired": "命令为必填项", + "timeoutMin": "最小超时时间为 1000ms", + "timeoutMax": "最大超时时间为 300000ms" } }, @@ -133,18 +191,8 @@ "hookFailMode": "命令执行失败时的处理方式" }, - "common": { - "cancel": "取消", - "save": "保存", - "delete": "删除", - "edit": "编辑", - "reset": "重置", - "confirm": "确认" - }, - "injection": { "title": "注入控制", - "description": "监控和管理规范注入长度", "statusTitle": "当前注入状态", "settingsTitle": "注入控制设置", "settingsDescription": "配置如何将规范内容注入到 AI 上下文中。", @@ -198,5 +246,11 @@ "hookScope": "作用域", "hookTimeout": "超时时间(ms)", "hookFailMode": "失败模式" + }, + + "form": { + "readMode": "读取模式", + "priority": "优先级", + "keywords": "关键词" } } diff --git a/ccw/frontend/src/pages/SpecsSettingsPage.tsx b/ccw/frontend/src/pages/SpecsSettingsPage.tsx index 7b3cc992..e9253d96 100644 --- a/ccw/frontend/src/pages/SpecsSettingsPage.tsx +++ b/ccw/frontend/src/pages/SpecsSettingsPage.tsx @@ -3,6 +3,7 @@ * * Main page for managing spec settings, injection control, and global 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 { 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 { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; -import { ScrollText, User, Gauge, Settings, RefreshCw, Search } from 'lucide-react'; -import { SpecCard, SpecDialog, SpecContentDialog, type Spec, type SpecFormData } from '@/components/specs'; +import { ScrollText, User, Gauge, Settings, RefreshCw, Search, Globe, Folder, Filter, Layers } from 'lucide-react'; +import { SpecCard, SpecDialog, SpecContentDialog, type Spec, type SpecFormData, type SpecCategory } from '@/components/specs'; import { InjectionControlTab } from '@/components/specs/InjectionControlTab'; import { GlobalSettingsTab } from '@/components/specs/GlobalSettingsTab'; import { useSpecStats, useSpecsList, useRebuildSpecIndex } from '@/hooks/useSystemSettings'; @@ -19,6 +20,11 @@ import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; import type { SpecEntry } from '@/lib/api'; 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 function specEntryToSpec(entry: SpecEntry, dimension: string): Spec { @@ -26,6 +32,8 @@ function specEntryToSpec(entry: SpecEntry, dimension: string): Spec { id: entry.file, title: entry.title, 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, readMode: entry.readMode as Spec['readMode'], priority: entry.priority as Spec['priority'], @@ -39,8 +47,12 @@ export function SpecsSettingsPage() { const projectPath = useWorkflowStore(selectProjectPath); const [activeTab, setActiveTab] = useState('project-specs'); const [searchQuery, setSearchQuery] = useState(''); + const [personalScopeFilter, setPersonalScopeFilter] = useState('all'); + const [categoryFilter, setCategoryFilter] = useState('all'); const [editDialogOpen, setEditDialogOpen] = useState(false); + const [contentDialogOpen, setContentDialogOpen] = useState(false); const [editingSpec, setEditingSpec] = useState(null); + const [viewingSpec, setViewingSpec] = useState(null); // Fetch real data const { data: specsListData, isLoading: specsLoading, refetch: refetchSpecs } = useSpecsList({ projectPath }); @@ -48,26 +60,50 @@ export function SpecsSettingsPage() { const rebuildMutation = useRebuildSpecIndex(); // Convert specs data to display format - const { projectSpecs, personalSpecs } = useMemo(() => { + const { projectSpecs, personalSpecs, globalPersonalSpecs, projectPersonalSpecs, categoryCounts } = useMemo(() => { if (!specsListData?.specs) { - return { projectSpecs: [], personalSpecs: [] }; + return { + projectSpecs: [], + personalSpecs: [], + globalPersonalSpecs: [], + projectPersonalSpecs: [], + categoryCounts: { general: 0, exploration: 0, planning: 0, execution: 0 } + }; } const specs: Spec[] = []; const personal: Spec[] = []; + const globalPersonal: Spec[] = []; + const projectPersonal: Spec[] = []; + const counts: Record = { general: 0, exploration: 0, planning: 0, execution: 0 }; for (const [dimension, entries] of Object.entries(specsListData.specs)) { for (const entry of entries) { const spec = specEntryToSpec(entry, dimension); + // Count by category + if (spec.category) { + counts[spec.category]++; + } if (dimension === 'personal') { personal.push(spec); + if (spec.scope === 'global') { + globalPersonal.push(spec); + } else { + projectPersonal.push(spec); + } } else { specs.push(spec); } } } - return { projectSpecs: specs, personalSpecs: personal }; + return { + projectSpecs: specs, + personalSpecs: personal, + globalPersonalSpecs: globalPersonal, + projectPersonalSpecs: projectPersonal, + categoryCounts: counts + }; }, [specsListData]); const isLoading = specsLoading; @@ -113,16 +149,34 @@ export function SpecsSettingsPage() { }; const filterSpecs = (specs: Spec[]) => { - if (!searchQuery.trim()) return specs; - const query = searchQuery.toLowerCase(); - return specs.filter(spec => - spec.title.toLowerCase().includes(query) || - spec.keywords.some(k => k.toLowerCase().includes(query)) - ); + let result = specs; + // Filter by category + if (categoryFilter !== 'all') { + result = result.filter(spec => spec.category === categoryFilter); + } + // 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 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); return ( @@ -144,13 +198,77 @@ export function SpecsSettingsPage() {
+ {/* Scope filter for personal specs */} + {dimension === 'personal' && ( +
+ + + {formatMessage({ id: 'specs.filterByScope', defaultMessage: 'Filter by scope:' })} + +
+ + + +
+
+ )} + + {/* Category filter for workflow stage */} +
+ + + {formatMessage({ id: 'specs.filterByCategory', defaultMessage: 'Workflow stage:' })} + +
+ + {SPEC_CATEGORIES.map(cat => ( + + ))} +
+
+ {/* Stats Summary */} {statsData?.dimensions && ( -
+
{Object.entries(statsData.dimensions).map(([dim, data]) => ( -
{dim}
+
+ {formatMessage({ id: `specs.dimension.${dim}`, defaultMessage: dim })} +
{(data as { count: number }).count}
{(data as { requiredCount: number }).requiredCount} {formatMessage({ id: 'specs.required', defaultMessage: 'required' })} @@ -167,7 +285,7 @@ export function SpecsSettingsPage() { {isLoading ? 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.' }) } diff --git a/ccw/src/commands/spec.ts b/ccw/src/commands/spec.ts index 5ba0770e..049d52e3 100644 --- a/ccw/src/commands/spec.ts +++ b/ccw/src/commands/spec.ts @@ -376,13 +376,13 @@ ${chalk.bold('EXAMPLES')} ccw spec init ${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:')} - ccw spec load --keywords "planning auth" + ccw spec load --category "planning auth" ${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):')} ccw spec load --dimension specs --keywords "auth jwt security" diff --git a/ccw/src/tools/spec-index-builder.ts b/ccw/src/tools/spec-index-builder.ts index 842a931c..ebbb5b3f 100644 --- a/ccw/src/tools/spec-index-builder.ts +++ b/ccw/src/tools/spec-index-builder.ts @@ -10,6 +10,7 @@ * --- * title: "Document Title" * dimension: "specs" + * category: "general" # general | exploration | planning | execution * keywords: ["auth", "security"] * readMode: "required" # required | optional * priority: "high" # critical | high | medium | low @@ -19,21 +20,25 @@ import matter from 'gray-matter'; import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs'; import { join, basename, extname, relative } from 'path'; +import { homedir } from 'os'; // ============================================================================ // 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 * - planning: Task planning, roadmap, requirements context * - execution: Implementation, testing, deployment context * - * Usage: Add these as keywords in spec frontmatter, e.g.: - * keywords: [exploration, auth, security] + * Usage: Set category field in spec frontmatter: + * 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]; @@ -43,6 +48,7 @@ export type SpecCategory = typeof SPEC_CATEGORIES[number]; export interface SpecFrontmatter { title: string; dimension: string; + category?: SpecCategory; keywords: string[]; readMode: 'required' | 'optional'; priority: 'critical' | 'high' | 'medium' | 'low'; @@ -58,12 +64,16 @@ export interface SpecIndexEntry { file: string; /** Dimension this spec belongs to */ 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[]; /** Whether this spec is required or optional */ readMode: 'required' | 'optional'; /** Priority level for ordering */ 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; +/** + * Valid category values. + */ +const VALID_CATEGORIES = SPEC_CATEGORIES; + /** * Directory name for spec index cache files (inside .ccw/). */ @@ -149,45 +164,42 @@ export async function buildDimensionIndex( projectPath: string, dimension: string ): Promise { - const dimensionDir = getDimensionDir(projectPath, dimension); const entries: SpecIndexEntry[] = []; - // If directory doesn't exist, return empty index - if (!existsSync(dimensionDir)) { - return { - dimension, - entries: [], - built_at: new Date().toISOString(), - }; - } + // Helper function to scan a directory and add entries + const scanDirectory = (dir: string, scope: 'global' | 'project') => { + if (!existsSync(dir)) return; - // Scan for .md files - let files: string[]; - try { - files = readdirSync(dimensionDir).filter( - f => extname(f).toLowerCase() === '.md' - ); - } 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` - ); + let files: string[]; + try { + files = readdirSync(dir).filter(f => extname(f).toLowerCase() === '.md'); + } catch { + return; } + + 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 { dimension, entries, @@ -315,7 +327,8 @@ export async function getDimensionIndex( function parseSpecFile( filePath: string, dimension: string, - projectPath: string + projectPath: string, + scope: 'global' | 'project' = 'project' ): SpecIndexEntry | null { let content: string; try { @@ -340,10 +353,10 @@ function parseSpecFile( if (!title) { // Title is required - use filename as fallback 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, dimension: string, projectPath: string, - data: Record + data: Record, + scope: 'global' | 'project' = 'project' ): SpecIndexEntry { // Compute relative file path from project root using path.relative // Normalize to forward slashes for cross-platform consistency 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 const keywords = extractStringArray(data, 'keywords'); @@ -375,9 +393,11 @@ function buildEntry( title, file: relativePath, dimension, + category, keywords, readMode, 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' { 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); +} diff --git a/ccw/src/tools/spec-init.ts b/ccw/src/tools/spec-init.ts index 7b0f5e46..80403e40 100644 --- a/ccw/src/tools/spec-init.ts +++ b/ccw/src/tools/spec-init.ts @@ -18,6 +18,7 @@ import { join } from 'path'; export interface SpecFrontmatter { title: string; dimension: string; + category?: 'general' | 'exploration' | 'planning' | 'execution'; keywords: string[]; readMode: 'required' | 'optional'; priority: 'high' | 'medium' | 'low'; @@ -55,7 +56,8 @@ export const SEED_DOCS: Map = new Map([ frontmatter: { title: 'Coding Conventions', dimension: 'specs', - keywords: ['typescript', 'naming', 'style', 'convention', 'exploration', 'planning', 'execution'], + category: 'general', + keywords: ['typescript', 'naming', 'style', 'convention'], readMode: 'required', priority: 'high', }, @@ -91,7 +93,8 @@ export const SEED_DOCS: Map = new Map([ frontmatter: { title: 'Architecture Constraints', dimension: 'specs', - keywords: ['architecture', 'module', 'layer', 'pattern', 'exploration', 'planning'], + category: 'planning', + keywords: ['architecture', 'module', 'layer', 'pattern'], readMode: 'required', priority: 'high', }, @@ -126,6 +129,7 @@ export const SEED_DOCS: Map = new Map([ frontmatter: { title: 'Personal Coding Style', dimension: 'personal', + category: 'general', keywords: ['style', 'preference'], readMode: 'optional', priority: 'medium', @@ -153,6 +157,7 @@ export const SEED_DOCS: Map = new Map([ frontmatter: { title: 'Tool Preferences', dimension: 'personal', + category: 'general', keywords: ['tool', 'cli', 'editor'], readMode: 'optional', priority: 'low', @@ -186,16 +191,22 @@ export const SEED_DOCS: Map = new Map([ */ export function formatFrontmatter(fm: SpecFrontmatter): string { const keywordsYaml = fm.keywords.map((k) => ` - ${k}`).join('\n'); - return [ + const lines = [ '---', `title: "${fm.title}"`, `dimension: ${fm.dimension}`, + ]; + if (fm.category) { + lines.push(`category: ${fm.category}`); + } + lines.push( `keywords:`, keywordsYaml, `readMode: ${fm.readMode}`, `priority: ${fm.priority}`, - '---', - ].join('\n'); + '---' + ); + return lines.join('\n'); } // ---------------------------------------------------------------------------