diff --git a/.claude/.disabled-skills/ccw-loop/SKILL.md b/.claude/skills/ccw-loop/SKILL.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/SKILL.md rename to .claude/skills/ccw-loop/SKILL.md diff --git a/.claude/.disabled-skills/ccw-loop/phases/actions/action-complete.md b/.claude/skills/ccw-loop/phases/actions/action-complete.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/phases/actions/action-complete.md rename to .claude/skills/ccw-loop/phases/actions/action-complete.md diff --git a/.claude/.disabled-skills/ccw-loop/phases/actions/action-debug-with-file.md b/.claude/skills/ccw-loop/phases/actions/action-debug-with-file.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/phases/actions/action-debug-with-file.md rename to .claude/skills/ccw-loop/phases/actions/action-debug-with-file.md diff --git a/.claude/.disabled-skills/ccw-loop/phases/actions/action-develop-with-file.md b/.claude/skills/ccw-loop/phases/actions/action-develop-with-file.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/phases/actions/action-develop-with-file.md rename to .claude/skills/ccw-loop/phases/actions/action-develop-with-file.md diff --git a/.claude/.disabled-skills/ccw-loop/phases/actions/action-init.md b/.claude/skills/ccw-loop/phases/actions/action-init.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/phases/actions/action-init.md rename to .claude/skills/ccw-loop/phases/actions/action-init.md diff --git a/.claude/.disabled-skills/ccw-loop/phases/actions/action-menu.md b/.claude/skills/ccw-loop/phases/actions/action-menu.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/phases/actions/action-menu.md rename to .claude/skills/ccw-loop/phases/actions/action-menu.md diff --git a/.claude/.disabled-skills/ccw-loop/phases/actions/action-validate-with-file.md b/.claude/skills/ccw-loop/phases/actions/action-validate-with-file.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/phases/actions/action-validate-with-file.md rename to .claude/skills/ccw-loop/phases/actions/action-validate-with-file.md diff --git a/.claude/.disabled-skills/ccw-loop/phases/orchestrator.md b/.claude/skills/ccw-loop/phases/orchestrator.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/phases/orchestrator.md rename to .claude/skills/ccw-loop/phases/orchestrator.md diff --git a/.claude/.disabled-skills/ccw-loop/phases/state-schema.md b/.claude/skills/ccw-loop/phases/state-schema.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/phases/state-schema.md rename to .claude/skills/ccw-loop/phases/state-schema.md diff --git a/.claude/.disabled-skills/ccw-loop/specs/action-catalog.md b/.claude/skills/ccw-loop/specs/action-catalog.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/specs/action-catalog.md rename to .claude/skills/ccw-loop/specs/action-catalog.md diff --git a/.claude/.disabled-skills/ccw-loop/specs/loop-requirements.md b/.claude/skills/ccw-loop/specs/loop-requirements.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/specs/loop-requirements.md rename to .claude/skills/ccw-loop/specs/loop-requirements.md diff --git a/.claude/.disabled-skills/ccw-loop/templates/progress-template.md b/.claude/skills/ccw-loop/templates/progress-template.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/templates/progress-template.md rename to .claude/skills/ccw-loop/templates/progress-template.md diff --git a/.claude/.disabled-skills/ccw-loop/templates/understanding-template.md b/.claude/skills/ccw-loop/templates/understanding-template.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/templates/understanding-template.md rename to .claude/skills/ccw-loop/templates/understanding-template.md diff --git a/.claude/.disabled-skills/ccw-loop/templates/validation-template.md b/.claude/skills/ccw-loop/templates/validation-template.md similarity index 100% rename from .claude/.disabled-skills/ccw-loop/templates/validation-template.md rename to .claude/skills/ccw-loop/templates/validation-template.md diff --git a/.claude/.disabled-skills/copyright-docs/SKILL.md b/.claude/skills/copyright-docs/SKILL.md similarity index 100% rename from .claude/.disabled-skills/copyright-docs/SKILL.md rename to .claude/skills/copyright-docs/SKILL.md diff --git a/.claude/.disabled-skills/copyright-docs/phases/01-metadata-collection.md b/.claude/skills/copyright-docs/phases/01-metadata-collection.md similarity index 100% rename from .claude/.disabled-skills/copyright-docs/phases/01-metadata-collection.md rename to .claude/skills/copyright-docs/phases/01-metadata-collection.md diff --git a/.claude/.disabled-skills/copyright-docs/phases/01.5-project-exploration.md b/.claude/skills/copyright-docs/phases/01.5-project-exploration.md similarity index 100% rename from .claude/.disabled-skills/copyright-docs/phases/01.5-project-exploration.md rename to .claude/skills/copyright-docs/phases/01.5-project-exploration.md diff --git a/.claude/.disabled-skills/copyright-docs/phases/02-deep-analysis.md b/.claude/skills/copyright-docs/phases/02-deep-analysis.md similarity index 100% rename from .claude/.disabled-skills/copyright-docs/phases/02-deep-analysis.md rename to .claude/skills/copyright-docs/phases/02-deep-analysis.md diff --git a/.claude/.disabled-skills/copyright-docs/phases/02.5-consolidation.md b/.claude/skills/copyright-docs/phases/02.5-consolidation.md similarity index 100% rename from .claude/.disabled-skills/copyright-docs/phases/02.5-consolidation.md rename to .claude/skills/copyright-docs/phases/02.5-consolidation.md diff --git a/.claude/.disabled-skills/copyright-docs/phases/04-document-assembly.md b/.claude/skills/copyright-docs/phases/04-document-assembly.md similarity index 100% rename from .claude/.disabled-skills/copyright-docs/phases/04-document-assembly.md rename to .claude/skills/copyright-docs/phases/04-document-assembly.md diff --git a/.claude/.disabled-skills/copyright-docs/phases/05-compliance-refinement.md b/.claude/skills/copyright-docs/phases/05-compliance-refinement.md similarity index 100% rename from .claude/.disabled-skills/copyright-docs/phases/05-compliance-refinement.md rename to .claude/skills/copyright-docs/phases/05-compliance-refinement.md diff --git a/.claude/.disabled-skills/copyright-docs/specs/cpcc-requirements.md b/.claude/skills/copyright-docs/specs/cpcc-requirements.md similarity index 100% rename from .claude/.disabled-skills/copyright-docs/specs/cpcc-requirements.md rename to .claude/skills/copyright-docs/specs/cpcc-requirements.md diff --git a/.claude/.disabled-skills/copyright-docs/templates/agent-base.md b/.claude/skills/copyright-docs/templates/agent-base.md similarity index 100% rename from .claude/.disabled-skills/copyright-docs/templates/agent-base.md rename to .claude/skills/copyright-docs/templates/agent-base.md diff --git a/.claude/skills/skill-generator/SKILL.md b/.claude/skills/skill-generator/SKILL.md index 411de0ed..8b8364f9 100644 --- a/.claude/skills/skill-generator/SKILL.md +++ b/.claude/skills/skill-generator/SKILL.md @@ -12,26 +12,24 @@ Meta-skill for creating new Claude Code skills with configurable execution modes ``` ┌─────────────────────────────────────────────────────────────────┐ -│ Skill Generator Architecture │ -├─────────────────────────────────────────────────────────────────┤ +│ Skill Generator │ │ │ -│ ⚠️ Phase 0: Specification → 阅读并理解设计规范 (强制前置) │ -│ Study SKILL-DESIGN-SPEC.md + 模板 │ -│ ↓ │ -│ Phase 1: Requirements → skill-config.json │ -│ Discovery (name, type, mode, agents) │ -│ ↓ │ -│ Phase 2: Structure → 目录结构 + 核心文件骨架 │ -│ Generation │ -│ ↓ │ -│ Phase 3: Phase → phases/*.md (根据 mode 生成) │ -│ Generation Sequential | Autonomous │ -│ ↓ │ -│ Phase 4: Specs & → specs/*.md + templates/*.md │ -│ Templates │ -│ ↓ │ -│ Phase 5: Validation → 验证完整性 + 生成使用说明 │ -│ & Documentation │ +│ Input: User Request (skill name, purpose, mode) │ +│ ↓ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Phase 0-5: Sequential Pipeline │ │ +│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ +│ │ │ P0 │→│ P1 │→│ P2 │→│ P3 │→│ P4 │→│ P5 │ │ │ +│ │ │Spec│ │Req │ │Dir │ │Gen │ │Spec│ │Val │ │ │ +│ │ └────┘ └────┘ └────┘ └─┬──┘ └────┘ └────┘ │ │ +│ │ │ │ │ +│ │ ┌────┴────┐ │ │ +│ │ ↓ ↓ │ │ +│ │ Sequential Autonomous │ │ +│ │ (phases/) (actions/) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ Output: .claude/skills/{skill-name}/ (complete package) │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` @@ -134,93 +132,289 @@ Phase 01 → Phase 02 → Phase 03 → ... → Phase N ## Execution Flow ``` -┌─────────────────────────────────────────────────────────────────┐ -│ ⚠️ Phase 0: Specification Study (强制前置 - 禁止跳过) │ -│ → Read: ../_shared/SKILL-DESIGN-SPEC.md (通用设计规范) │ -│ → Read: templates/*.md (所有相关模板文件) │ -│ → 理解: Skill 结构规范、命名约定、质量标准 │ -│ → Output: 内化规范要求,确保后续生成符合标准 │ -│ ⛔ 未完成 Phase 0 禁止进入 Phase 1 │ -├─────────────────────────────────────────────────────────────────┤ -│ Phase 1: Requirements Discovery │ -│ → AskUserQuestion: Skill 名称、目标、执行模式 │ -│ → Output: skill-config.json │ -├─────────────────────────────────────────────────────────────────┤ -│ Phase 2: Structure Generation │ -│ → 创建目录结构: phases/, specs/, templates/, scripts/ │ -│ → 生成 SKILL.md 入口文件 │ -│ → Output: 完整目录结构 │ -├─────────────────────────────────────────────────────────────────┤ -│ Phase 3: Phase Generation │ -│ → Sequential: 生成 01-xx.md, 02-xx.md, ... │ -│ → Autonomous: 生成 orchestrator.md + actions/*.md │ -│ → Output: phases/*.md │ -├─────────────────────────────────────────────────────────────────┤ -│ Phase 4: Specs & Templates │ -│ → 生成领域规范: specs/{domain}-requirements.md │ -│ → 生成质量标准: specs/quality-standards.md │ -│ → 生成模板: templates/agent-base.md │ -│ → Output: specs/*.md, templates/*.md │ -├─────────────────────────────────────────────────────────────────┤ -│ Phase 5: Validation & Documentation │ -│ → 验证文件完整性 │ -│ → 生成 README.md 使用说明 │ -│ → Output: 验证报告 + README.md │ -└─────────────────────────────────────────────────────────────────┘ +Input Parsing: + └─ Convert user request to structured format (skill-name/purpose/mode) + +Phase 0: Specification Study (⚠️ MANDATORY - 禁止跳过) + └─ Read specification documents + ├─ Load: ../_shared/SKILL-DESIGN-SPEC.md + ├─ Load: All templates/*.md files + ├─ Understand: Structure rules, naming conventions, quality standards + └─ Output: Internalized requirements (in-memory, no file output) + └─ Validation: ⛔ MUST complete before Phase 1 + +Phase 1: Requirements Discovery + └─ Gather skill requirements via user interaction + ├─ Tool: AskUserQuestion + │ ├─ Prompt: Skill name, purpose, execution mode + │ ├─ Prompt: Phase/Action definition + │ └─ Prompt: Tool dependencies, output format + ├─ Process: Generate configuration object + └─ Output: skill-config.json → ${workDir}/ + ├─ skill_name: string + ├─ execution_mode: "sequential" | "autonomous" + ├─ phases/actions: array + └─ allowed_tools: array + +Phase 2: Structure Generation + └─ Create directory structure and entry file + ├─ Input: skill-config.json (from Phase 1) + ├─ Tool: Bash + │ └─ Execute: mkdir -p .claude/skills/{skill-name}/{phases,specs,templates,scripts} + ├─ Tool: Write + │ └─ Generate: SKILL.md (entry point with architecture diagram) + └─ Output: Complete directory structure + ├─ .claude/skills/{skill-name}/SKILL.md + ├─ .claude/skills/{skill-name}/phases/ + ├─ .claude/skills/{skill-name}/specs/ + ├─ .claude/skills/{skill-name}/templates/ + └─ .claude/skills/{skill-name}/scripts/ + +Phase 3: Phase/Action Generation + └─ Decision (execution_mode check): + ├─ execution_mode === "sequential" → Generate Sequential Phases + │ ├─ Tool: Read (template: templates/sequential-phase.md) + │ ├─ Loop: For each phase in config.sequential_config.phases + │ │ ├─ Generate: phases/{phase-id}.md + │ │ └─ Link: Previous phase output → Current phase input + │ ├─ Tool: Write (orchestrator: phases/_orchestrator.md) + │ ├─ Tool: Write (workflow definition: workflow.json) + │ └─ Output: phases/01-{name}.md, phases/02-{name}.md, ... + │ + └─ execution_mode === "autonomous" → Generate Orchestrator + Actions + ├─ Tool: Read (template: templates/autonomous-orchestrator.md) + ├─ Tool: Write (state schema: phases/state-schema.md) + ├─ Tool: Write (orchestrator: phases/orchestrator.md) + ├─ Tool: Write (action catalog: specs/action-catalog.md) + ├─ Loop: For each action in config.autonomous_config.actions + │ ├─ Tool: Read (template: templates/autonomous-action.md) + │ └─ Generate: phases/actions/{action-id}.md + └─ Output: phases/orchestrator.md, phases/actions/*.md + +Phase 4: Specs & Templates + └─ Generate domain specifications and templates + ├─ Input: skill-config.json (domain context) + ├─ Tool: Write + │ ├─ Generate: specs/{domain}-requirements.md + │ ├─ Generate: specs/quality-standards.md + │ └─ Generate: templates/agent-base.md (if needed) + └─ Output: Domain-specific documentation + ├─ specs/{skill-name}-requirements.md + ├─ specs/quality-standards.md + └─ templates/agent-base.md + +Phase 5: Validation & Documentation + └─ Verify completeness and generate usage guide + ├─ Input: All generated files from previous phases + ├─ Tool: Glob + Read + │ └─ Check: Required files exist and contain proper structure + ├─ Tool: Write + │ ├─ Generate: README.md (usage instructions) + │ └─ Generate: validation-report.json (completeness check) + └─ Output: Final documentation + ├─ README.md (how to use this skill) + └─ validation-report.json (quality gate results) + +Return: + └─ Summary with skill location and next steps + ├─ Skill path: .claude/skills/{skill-name}/ + ├─ Status: ✅ All phases completed + └─ Suggestion: "Review SKILL.md and customize phase files as needed" ``` -## Directory Setup +**Execution Protocol**: ```javascript -const skillName = config.skill_name; -const skillDir = `.claude/skills/${skillName}`; +// Phase 0: Read specifications (in-memory) +Read('.claude/skills/_shared/SKILL-DESIGN-SPEC.md'); +Read('.claude/skills/skill-generator/templates/*.md'); // All templates -// 创建目录结构 -Bash(`mkdir -p "${skillDir}/phases"`); -Bash(`mkdir -p "${skillDir}/specs"`); -Bash(`mkdir -p "${skillDir}/templates"`); +// Phase 1: Gather requirements +const answers = AskUserQuestion({ + questions: [ + { question: "Skill name?", header: "Name", options: [...] }, + { question: "Execution mode?", header: "Mode", options: ["Sequential", "Autonomous"] } + ] +}); -// Autonomous 模式额外目录 -if (config.execution_mode === 'autonomous') { - Bash(`mkdir -p "${skillDir}/phases/actions"`); +const config = generateConfig(answers); +const workDir = `.workflow/.scratchpad/skill-gen-${timestamp}`; +Write(`${workDir}/skill-config.json`, JSON.stringify(config)); + +// Phase 2: Create structure +const skillDir = `.claude/skills/${config.skill_name}`; +Bash(`mkdir -p "${skillDir}/phases" "${skillDir}/specs" "${skillDir}/templates"`); +Write(`${skillDir}/SKILL.md`, generateSkillEntry(config)); + +// Phase 3: Generate phases (mode-dependent) +if (config.execution_mode === 'sequential') { + Write(`${skillDir}/phases/_orchestrator.md`, generateOrchestrator(config)); + Write(`${skillDir}/workflow.json`, generateWorkflowDef(config)); + config.sequential_config.phases.forEach(phase => { + Write(`${skillDir}/phases/${phase.id}.md`, generatePhase(phase, config)); + }); +} else { + Write(`${skillDir}/phases/orchestrator.md`, generateAutonomousOrchestrator(config)); + Write(`${skillDir}/phases/state-schema.md`, generateStateSchema(config)); + config.autonomous_config.actions.forEach(action => { + Write(`${skillDir}/phases/actions/${action.id}.md`, generateAction(action, config)); + }); } + +// Phase 4: Generate specs +Write(`${skillDir}/specs/${config.skill_name}-requirements.md`, generateRequirements(config)); +Write(`${skillDir}/specs/quality-standards.md`, generateQualityStandards(config)); + + +// Phase 5: Validate & Document +const validation = validateStructure(skillDir); +Write(`${skillDir}/validation-report.json`, JSON.stringify(validation)); +Write(`${skillDir}/README.md`, generateReadme(config, validation)); ``` +--- + +## Phase Reference Guide + +Navigation and entry points for each execution phase: + +### Phase 0: Specification Study (Mandatory) + +**Document**: 🔗 [SKILL-DESIGN-SPEC.md](../_shared/SKILL-DESIGN-SPEC.md) + +**Purpose**: Understand skill design standards before generating + +**What to Read**: +- Skill structure conventions +- Naming standards +- Quality criteria +- Output format specifications + +--- + +### Phase 1: Requirements Discovery + +**Document**: 🔗 [phases/01-requirements-discovery.md](phases/01-requirements-discovery.md) + +| Attribute | Value | +|-----------|-------| +| **Purpose** | Gather configuration from user via interactive prompts | +| **Input** | User responses (Skill name, purpose, execution mode) | +| **Output** | `skill-config.json` (configuration file) | +| **Key Decision** | Execution mode: Sequential vs Autonomous vs Hybrid | + +--- + +### Phase 2: Structure Generation + +**Document**: 🔗 [phases/02-structure-generation.md](phases/02-structure-generation.md) + +| Attribute | Value | +|-----------|-------| +| **Purpose** | Create directory structure and entry file | +| **Input** | `skill-config.json` (from Phase 1) | +| **Output** | `.claude/skills/{skill-name}/` directory + `SKILL.md` | + +--- + +### Phase 3: Phase/Action Generation + +**Document**: 🔗 [phases/03-phase-generation.md](phases/03-phase-generation.md) + +| Attribute | Value | +|-----------|-------| +| **Purpose** | Generate execution phases or actions based on mode | +| **Input** | `skill-config.json` + Templates | +| **Output** | Sequential: `phases/01-*.md` ... OR Autonomous: `phases/orchestrator.md` + `actions/*.md` | + +**Decision Logic**: +``` +IF execution_mode === "sequential": + └─ Generate sequential phases with linear flow +ELSE IF execution_mode === "autonomous": + └─ Generate orchestrator + independent actions +``` + +--- + +### Phase 4: Specs & Templates + +**Document**: 🔗 [phases/04-specs-templates.md](phases/04-specs-templates.md) + +| Attribute | Value | +|-----------|-------| +| **Purpose** | Generate domain specifications and template files | +| **Input** | `skill-config.json` (domain context) | +| **Output** | `specs/{skill-name}-requirements.md`, `specs/quality-standards.md` | + +--- + +### Phase 5: Validation & Documentation + +**Document**: 🔗 [phases/05-validation.md](phases/05-validation.md) + +| Attribute | Value | +|-----------|-------| +| **Purpose** | Verify completeness and generate usage documentation | +| **Input** | All files from previous phases | +| **Output** | `README.md`, `validation-report.json` | + +--- + +## Template Reference + +| Template | Generated For | When Used | +|----------|--------------|-----------| +| [skill-md.md](templates/skill-md.md) | SKILL.md entry file | Phase 2 | +| [sequential-phase.md](templates/sequential-phase.md) | Sequential phase files | Phase 3 | +| [autonomous-orchestrator.md](templates/autonomous-orchestrator.md) | Orchestrator (autonomous) | Phase 3 | +| [autonomous-action.md](templates/autonomous-action.md) | Action files | Phase 3 | +| [code-analysis-action.md](templates/code-analysis-action.md) | Code analysis actions | Phase 3 | +| [llm-action.md](templates/llm-action.md) | LLM-powered actions | Phase 3 | +| [script-bash.md](templates/script-bash.md) | Bash scripts | Phase 3/4 | +| [script-python.md](templates/script-python.md) | Python scripts | Phase 3/4 | + ## Output Structure ### Sequential Mode ``` .claude/skills/{skill-name}/ -├── SKILL.md +├── SKILL.md # 入口文件 ├── phases/ -│ ├── 01-{step-one}.md -│ ├── 02-{step-two}.md -│ └── 03-{step-three}.md +│ ├── _orchestrator.md # 声明式编排器 +│ ├── workflow.json # 工作流定义 +│ ├── 01-{step-one}.md # 阶段 1 +│ ├── 02-{step-two}.md # 阶段 2 +│ └── 03-{step-three}.md # 阶段 3 ├── specs/ -│ ├── {domain}-requirements.md +│ ├── {skill-name}-requirements.md │ └── quality-standards.md -└── templates/ - └── agent-base.md +├── templates/ +│ └── agent-base.md +├── scripts/ +└── README.md ``` ### Autonomous Mode ``` .claude/skills/{skill-name}/ -├── SKILL.md +├── SKILL.md # 入口文件 ├── phases/ -│ ├── orchestrator.md # 编排器:读取状态 → 选择动作 -│ ├── state-schema.md # 状态结构定义 -│ └── actions/ # 独立动作(无顺序) -│ ├── action-{a}.md -│ ├── action-{b}.md -│ └── action-{c}.md +│ ├── orchestrator.md # 编排器 (状态驱动) +│ ├── state-schema.md # 状态结构定义 +│ └── actions/ +│ ├── action-init.md +│ ├── action-create.md +│ └── action-list.md ├── specs/ -│ ├── {domain}-requirements.md -│ ├── action-catalog.md # 动作目录(描述、前置条件、效果) +│ ├── {skill-name}-requirements.md +│ ├── action-catalog.md │ └── quality-standards.md -└── templates/ - ├── orchestrator-base.md # 编排器模板 - └── action-base.md # 动作模板 -``` +├── templates/ +│ ├── orchestrator-base.md +│ └── action-base.md +├── scripts/ +└── README.md +``` \ No newline at end of file diff --git a/.claude/skills/skill-generator/phases/01-requirements-discovery.md b/.claude/skills/skill-generator/phases/01-requirements-discovery.md index 4d5d3f7a..d404bdf5 100644 --- a/.claude/skills/skill-generator/phases/01-requirements-discovery.md +++ b/.claude/skills/skill-generator/phases/01-requirements-discovery.md @@ -1,15 +1,4 @@ -# Phase 1: Requirements Discovery -收集新 Skill 的需求信息,生成配置文件。 - -## Objective - -- 收集 Skill 基本信息(名称、描述、触发词) -- 确定执行模式(Sequential / Autonomous) -- 定义阶段/动作 -- 配置工具依赖和输出格式 - -## Execution Steps ### Step 1: 基本信息收集 @@ -228,12 +217,4 @@ Bash(`mkdir -p "${workDir}"`); Write(`${workDir}/skill-config.json`, JSON.stringify(config, null, 2)); ``` -## Output -- **File**: `skill-config.json` -- **Location**: `.workflow/.scratchpad/skill-gen-{timestamp}/` -- **Format**: JSON - -## Next Phase - -→ [Phase 2: Structure Generation](02-structure-generation.md) diff --git a/.claude/skills/skill-generator/phases/02-structure-generation.md b/.claude/skills/skill-generator/phases/02-structure-generation.md index dd7058dd..b840e362 100644 --- a/.claude/skills/skill-generator/phases/02-structure-generation.md +++ b/.claude/skills/skill-generator/phases/02-structure-generation.md @@ -8,9 +8,7 @@ - 生成 SKILL.md 入口文件 - 根据执行模式创建对应的子目录 -## Input -- 依赖: `skill-config.json` (Phase 1 产出) ## Execution Steps @@ -192,16 +190,4 @@ function generateReferenceTable(config) { } ``` -## Output -- **Directory**: `.claude/skills/{skill-name}/` -- **Files**: - - `SKILL.md` (入口文件) - - `phases/` (执行阶段目录) - - `specs/` (规范文档目录) - - `templates/` (模板目录) - - `scripts/` (脚本目录,存放 Python/Bash 确定性脚本) - -## Next Phase - -→ [Phase 3: Phase Generation](03-phase-generation.md) diff --git a/.claude/skills/skill-generator/phases/03-phase-generation.md b/.claude/skills/skill-generator/phases/03-phase-generation.md index 09a2abea..11570b5e 100644 --- a/.claude/skills/skill-generator/phases/03-phase-generation.md +++ b/.claude/skills/skill-generator/phases/03-phase-generation.md @@ -8,10 +8,7 @@ - Autonomous 模式:生成编排器和动作文件 - 支持 **文件上下文** 和 **内存上下文** 两种策略 -## Input -- 依赖: `skill-config.json`, SKILL.md (Phase 1-2 产出) -- 模板: `templates/sequential-phase.md`, `templates/autonomous-*.md` ## 上下文策略 (P0 增强) @@ -782,21 +779,4 @@ function getPreconditionCheck(action) { } ``` -## Output -### Sequential 模式 - -- `phases/_orchestrator.md` (声明式编排器) -- `workflow.json` (工作流定义) -- `phases/01-{step}.md`, `02-{step}.md`, ... - -### Autonomous 模式 - -- `phases/orchestrator.md` (增强版编排器) -- `phases/state-schema.md` -- `specs/action-catalog.md` (声明式动作目录) -- `phases/actions/action-{name}.md` (多个) - -## Next Phase - -→ [Phase 4: Specs & Templates](04-specs-templates.md) diff --git a/.claude/skills/skill-generator/phases/04-specs-templates.md b/.claude/skills/skill-generator/phases/04-specs-templates.md index 27756f50..6870bed0 100644 --- a/.claude/skills/skill-generator/phases/04-specs-templates.md +++ b/.claude/skills/skill-generator/phases/04-specs-templates.md @@ -1,26 +1,107 @@ -# Phase 4: Specs & Templates Generation +# Phase 4: Specifications & Templates Generation -生成规范文件和模板文件。 +Generate domain requirements, quality standards, agent templates, and action catalogs. ## Objective -- 生成领域规范 (`specs/{domain}-requirements.md`) -- 生成质量标准 (`specs/quality-standards.md`) -- 生成 Agent 模板 (`templates/agent-base.md`) -- Autonomous 模式额外生成动作目录 (`specs/action-catalog.md`) +Generate comprehensive specifications and templates: +- Domain requirements document with validation function +- Quality standards with automated check system +- Agent base template with prompt structure +- Action catalog for autonomous mode (conditional) ## Input -- 依赖: `skill-config.json`, SKILL.md, phases/*.md +**File Dependencies**: +- `skill-config.json` (from Phase 1) +- `.claude/skills/{skill-name}/` directory (from Phase 2) +- Generated phase/action files (from Phase 3) -## Execution Steps +**Required Information**: +- Skill name, display name, description +- Execution mode (determines if action-catalog.md is generated) +- Output format and location +- Phase/action definitions -### Step 1: 生成领域规范 +## Output + +**Generated Files**: + +| File | Purpose | Generation Condition | +|------|---------|---------------------| +| `specs/{skill-name}-requirements.md` | Domain requirements with validation | Always | +| `specs/quality-standards.md` | Quality evaluation criteria | Always | +| `templates/agent-base.md` | Agent prompt template | Always | +| `specs/action-catalog.md` | Action dependency graph and selection priority | Autonomous/Hybrid mode only | + +**File Structure**: + +**Domain Requirements** (`specs/{skill-name}-requirements.md`): +```markdown +# {display_name} Requirements +- When to Use (phase/action reference table) +- Domain Requirements (功能要求, 输出要求, 质量要求) +- Validation Function (JavaScript code) +- Error Handling (recovery strategies) +``` + +**Quality Standards** (`specs/quality-standards.md`): +```markdown +# Quality Standards +- Quality Dimensions (Completeness 25%, Consistency 25%, Accuracy 25%, Usability 25%) +- Quality Gates (Pass ≥80%, Review 60-79%, Fail <60%) +- Issue Classification (Errors, Warnings, Info) +- Automated Checks (runQualityChecks function) +``` + +**Agent Base** (`templates/agent-base.md`): +```markdown +# Agent Base Template +- 通用 Prompt 结构 (ROLE, PROJECT CONTEXT, TASK, CONSTRAINTS, OUTPUT_FORMAT, QUALITY_CHECKLIST) +- 变量说明 (workDir, output_path) +- 返回格式 (AgentReturn interface) +- 角色定义参考 (phase/action specific agents) +``` + +**Action Catalog** (`specs/action-catalog.md`, Autonomous/Hybrid only): +```markdown +# Action Catalog +- Available Actions (table with Purpose, Preconditions, Effects) +- Action Dependencies (Mermaid diagram) +- State Transitions (state machine table) +- Selection Priority (ordered action list) +``` + +## Decision Logic + +``` +Decision (execution_mode check): + ├─ mode === 'sequential' → Generate 3 files only + │ └─ Files: requirements.md, quality-standards.md, agent-base.md + │ + ├─ mode === 'autonomous' → Generate 4 files + │ ├─ Files: requirements.md, quality-standards.md, agent-base.md + │ └─ Additional: action-catalog.md (with action dependencies) + │ + └─ mode === 'hybrid' → Generate 4 files + ├─ Files: requirements.md, quality-standards.md, agent-base.md + └─ Additional: action-catalog.md (with hybrid logic) +``` + +## Execution Protocol ```javascript +// Phase 4: Generate Specifications & Templates +// Reference: phases/04-specs-templates.md + +// Load config and setup const config = JSON.parse(Read(`${workDir}/skill-config.json`)); const skillDir = `.claude/skills/${config.skill_name}`; +// Ensure specs and templates directories exist (created in Phase 2) +// skillDir structure: phases/, specs/, templates/ + +// Step 1: Generate domain requirements const domainRequirements = `# ${config.display_name} Requirements ${config.description} @@ -29,8 +110,8 @@ ${config.description} | Phase | Usage | Reference | |-------|-------|-----------| -${config.execution_mode === 'sequential' ? - config.sequential_config.phases.map((p, i) => +${config.execution_mode === 'sequential' ? + config.sequential_config.phases.map((p, i) => `| Phase ${i+1} | ${p.name} | ${p.id}.md |` ).join('\n') : `| Orchestrator | 动作选择 | orchestrator.md | @@ -67,7 +148,7 @@ function validate${toPascalCase(config.skill_name)}(output) { { name: "格式正确", pass: output.format === "${config.output.format}" }, { name: "内容完整", pass: output.content?.length > 0 } ]; - + return { passed: checks.filter(c => c.pass).length, total: checks.length, @@ -86,11 +167,8 @@ function validate${toPascalCase(config.skill_name)}(output) { `; Write(`${skillDir}/specs/${config.skill_name}-requirements.md`, domainRequirements); -``` -### Step 2: 生成质量标准 - -```javascript +// Step 2: Generate quality standards const qualityStandards = `# Quality Standards ${config.display_name} 的质量评估标准。 @@ -176,7 +254,7 @@ function runQualityChecks(workDir) { return { score: results.overall, - gate: results.overall >= 80 ? 'pass' : + gate: results.overall >= 80 ? 'pass' : results.overall >= 60 ? 'review' : 'fail', details: results }; @@ -185,11 +263,8 @@ function runQualityChecks(workDir) { `; Write(`${skillDir}/specs/quality-standards.md`, qualityStandards); -``` -### Step 3: 生成 Agent 模板 - -```javascript +// Step 3: Generate agent base template const agentBase = `# Agent Base Template ${config.display_name} 的 Agent 基础模板。 @@ -246,20 +321,17 @@ interface AgentReturn { ## 角色定义参考 ${config.execution_mode === 'sequential' ? - config.sequential_config.phases.map((p, i) => + config.sequential_config.phases.map((p, i) => `- **Phase ${i+1} Agent**: ${p.name} 专家` ).join('\n') : - config.autonomous_config.actions.map(a => + config.autonomous_config.actions.map(a => `- **${a.name} Agent**: ${a.description || a.name + ' 执行者'}` ).join('\n')} `; Write(`${skillDir}/templates/agent-base.md`, agentBase); -``` -### Step 4: Autonomous 模式 - 动作目录 - -```javascript +// Step 4: Conditional - Generate action catalog for autonomous/hybrid mode if (config.execution_mode === 'autonomous' || config.execution_mode === 'hybrid') { const actionCatalog = `# Action Catalog @@ -269,7 +341,7 @@ ${config.display_name} 的可用动作目录。 | Action | Purpose | Preconditions | Effects | |--------|---------|---------------|---------| -${config.autonomous_config.actions.map(a => +${config.autonomous_config.actions.map(a => `| [${a.id}](../phases/actions/${a.id}.md) | ${a.description || a.name} | ${a.preconditions?.join(', ') || '-'} | ${a.effects?.join(', ') || '-'} |` ).join('\n')} @@ -289,7 +361,7 @@ ${config.autonomous_config.actions.map((a, i, arr) => { | From State | Action | To State | |------------|--------|----------| | pending | action-init | running | -${config.autonomous_config.actions.slice(1).map(a => +${config.autonomous_config.actions.slice(1).map(a => `| running | ${a.id} | running |` ).join('\n')} | running | action-complete | completed | @@ -299,30 +371,28 @@ ${config.autonomous_config.actions.slice(1).map(a => 当多个动作的前置条件都满足时,按以下优先级选择: -${config.autonomous_config.actions.map((a, i) => +${config.autonomous_config.actions.map((a, i) => `${i + 1}. \`${a.id}\` - ${a.name}` ).join('\n')} `; Write(`${skillDir}/specs/action-catalog.md`, actionCatalog); } -``` -### Step 5: 辅助函数 - -```javascript +// Helper function function toPascalCase(str) { return str.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(''); } + +// Phase output summary +console.log('Phase 4 complete: Generated specs and templates'); ``` -## Output - -- `specs/{skill-name}-requirements.md` - 领域规范 -- `specs/quality-standards.md` - 质量标准 -- `specs/action-catalog.md` - 动作目录 (Autonomous 模式) -- `templates/agent-base.md` - Agent 模板 - ## Next Phase → [Phase 5: Validation](05-validation.md) + +**Data Flow to Phase 5**: +- All generated files in `specs/` and `templates/` +- skill-config.json for validation reference +- Complete skill directory structure ready for final validation diff --git a/ccw/src/core/routes/commands-routes.ts b/ccw/src/core/routes/commands-routes.ts index 8d4f35c1..c2786f71 100644 --- a/ccw/src/core/routes/commands-routes.ts +++ b/ccw/src/core/routes/commands-routes.ts @@ -51,6 +51,17 @@ interface CommandOperationResult { status?: number; } +interface GroupDefinition { + name: string; + icon?: string; + color?: string; +} + +interface CommandGroupsConfig { + groups: Record; // Custom group definitions + assignments: Record; // commandName -> groupId mapping +} + // ========== Helper Functions ========== function isRecord(value: unknown): value is Record { @@ -125,19 +136,79 @@ function parseCommandFrontmatter(content: string): CommandMetadata { } /** - * Infer group from command path if not specified in frontmatter + * Get command groups config file path */ -function inferGroupFromPath(relativePath: string, metadata: CommandMetadata): string { - // If group is specified in frontmatter, use it - if (metadata.group && metadata.group !== 'other') { - return metadata.group; +function getGroupsConfigPath(location: CommandLocation, projectPath: string): string { + const baseDir = location === 'project' + ? join(projectPath, '.claude') + : join(homedir(), '.claude'); + return join(baseDir, 'command-groups.json'); +} + +/** + * Load command groups configuration + */ +function loadGroupsConfig(location: CommandLocation, projectPath: string): CommandGroupsConfig { + const configPath = getGroupsConfigPath(location, projectPath); + + const defaultConfig: CommandGroupsConfig = { + groups: {}, + assignments: {} + }; + + if (!existsSync(configPath)) { + return defaultConfig; } - // Infer from directory structure + try { + const content = readFileSync(configPath, 'utf8'); + const parsed = JSON.parse(content); + + return { + groups: isRecord(parsed.groups) ? parsed.groups as Record : {}, + assignments: isRecord(parsed.assignments) ? parsed.assignments as Record : {} + }; + } catch (err) { + console.error(`[Commands] Failed to load groups config from ${configPath}:`, err); + return defaultConfig; + } +} + +/** + * Save command groups configuration + */ +function saveGroupsConfig(location: CommandLocation, projectPath: string, config: CommandGroupsConfig): void { + const configPath = getGroupsConfigPath(location, projectPath); + const configDir = dirname(configPath); + + if (!existsSync(configDir)) { + mkdirSync(configDir, { recursive: true }); + } + + try { + const content = JSON.stringify(config, null, 2); + require('fs').writeFileSync(configPath, content, 'utf8'); + } catch (err) { + console.error(`[Commands] Failed to save groups config to ${configPath}:`, err); + } +} + +/** + * Get group for a command (from config or inferred from path) + */ +function getCommandGroup(commandName: string, relativePath: string, location: CommandLocation, projectPath: string): string { + // First check custom assignments + const config = loadGroupsConfig(location, projectPath); + if (config.assignments[commandName]) { + return config.assignments[commandName]; + } + + // Fallback to path-based inference - use full directory path as group const parts = relativePath.split(/[/\\]/); if (parts.length > 1) { - // Use first directory as group (e.g., 'workflow', 'issue', 'memory') - return parts[0]; + // Use full directory path (excluding filename) as group + // e.g., 'workflow/review/code-review.md' -> 'workflow/review' + return parts.slice(0, -1).join('/'); } return 'other'; @@ -150,7 +221,8 @@ function scanCommandsRecursive( baseDir: string, currentDir: string, location: CommandLocation, - enabled: boolean + enabled: boolean, + projectPath: string ): CommandInfo[] { const results: CommandInfo[] = []; @@ -168,17 +240,20 @@ function scanCommandsRecursive( if (entry.isDirectory()) { // Skip _disabled directory when scanning enabled commands if (entry.name === '_disabled') continue; - + // Recursively scan subdirectories - results.push(...scanCommandsRecursive(baseDir, fullPath, location, enabled)); + results.push(...scanCommandsRecursive(baseDir, fullPath, location, enabled, projectPath)); } else if (entry.isFile() && entry.name.endsWith('.md')) { try { const content = readFileSync(fullPath, 'utf8'); const metadata = parseCommandFrontmatter(content); - const group = inferGroupFromPath(relativePath, metadata); + const commandName = metadata.name || basename(entry.name, '.md'); + + // Get group from external config (not from frontmatter) + const group = getCommandGroup(commandName, relativePath, location, projectPath); results.push({ - name: metadata.name || basename(entry.name, '.md'), + name: commandName, description: metadata.description, group, enabled, @@ -217,28 +292,28 @@ function getCommandsConfig(projectPath: string): CommandsConfig { // Scan project commands const projectDir = getCommandsDir('project', projectPath); const projectDisabledDir = getDisabledCommandsDir('project', projectPath); - + // Enabled project commands - const enabledProject = scanCommandsRecursive(projectDir, projectDir, 'project', true); + const enabledProject = scanCommandsRecursive(projectDir, projectDir, 'project', true, projectPath); result.projectCommands.push(...enabledProject); - + // Disabled project commands if (existsSync(projectDisabledDir)) { - const disabledProject = scanCommandsRecursive(projectDisabledDir, projectDisabledDir, 'project', false); + const disabledProject = scanCommandsRecursive(projectDisabledDir, projectDisabledDir, 'project', false, projectPath); result.projectCommands.push(...disabledProject); } // Scan user commands const userDir = getCommandsDir('user', projectPath); const userDisabledDir = getDisabledCommandsDir('user', projectPath); - + // Enabled user commands - const enabledUser = scanCommandsRecursive(userDir, userDir, 'user', true); + const enabledUser = scanCommandsRecursive(userDir, userDir, 'user', true, projectPath); result.userCommands.push(...enabledUser); - + // Disabled user commands if (existsSync(userDisabledDir)) { - const disabledUser = scanCommandsRecursive(userDisabledDir, userDisabledDir, 'user', false); + const disabledUser = scanCommandsRecursive(userDisabledDir, userDisabledDir, 'user', false, projectPath); result.userCommands.push(...disabledUser); } @@ -449,8 +524,17 @@ export async function handleCommandsRoutes(ctx: RouteContext): Promise }); const config = getCommandsConfig(validatedProjectPath); + + // Include groups config from both project and user + const projectGroupsConfig = loadGroupsConfig('project', validatedProjectPath); + const userGroupsConfig = loadGroupsConfig('user', validatedProjectPath); + res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(config)); + res.end(JSON.stringify({ + ...config, + projectGroupsConfig, + userGroupsConfig + })); } catch (err) { const message = err instanceof Error ? err.message : String(err); const status = message.includes('Access denied') ? 403 : 400; @@ -513,5 +597,78 @@ export async function handleCommandsRoutes(ctx: RouteContext): Promise return true; } + // GET /api/commands/groups - Get groups configuration + if (pathname === '/api/commands/groups' && req.method === 'GET') { + const projectPathParam = url.searchParams.get('path') || initialPath; + const location = url.searchParams.get('location') || 'project'; + + try { + const validatedProjectPath = await validateAllowedPath(projectPathParam, { + mustExist: true, + allowedDirectories: [initialPath] + }); + + if (location !== 'project' && location !== 'user') { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Invalid location' })); + return true; + } + + const groupsConfig = loadGroupsConfig(location as CommandLocation, validatedProjectPath); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(groupsConfig)); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const status = message.includes('Access denied') ? 403 : 400; + res.writeHead(status, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: message })); + } + return true; + } + + // PUT /api/commands/groups - Update groups configuration + if (pathname === '/api/commands/groups' && req.method === 'PUT') { + const projectPathParam = url.searchParams.get('path') || initialPath; + const location = url.searchParams.get('location') || 'project'; + + handlePostRequest(req, res, async (body) => { + try { + const validatedProjectPath = await validateAllowedPath(projectPathParam, { + mustExist: true, + allowedDirectories: [initialPath] + }); + + if (location !== 'project' && location !== 'user') { + return { error: 'Invalid location', status: 400 }; + } + + if (!isRecord(body)) { + return { error: 'Invalid request body', status: 400 }; + } + + // Validate and save groups config + const config: CommandGroupsConfig = { + groups: isRecord(body.groups) ? body.groups as Record : {}, + assignments: isRecord(body.assignments) ? body.assignments as Record : {} + }; + + saveGroupsConfig(location as CommandLocation, validatedProjectPath, config); + + return { + success: true, + message: 'Groups configuration updated', + data: config, + status: 200 + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const status = message.includes('Access denied') ? 403 : 400; + console.error(`[Commands] Failed to update groups config: ${message}`); + return { error: message, status }; + } + }); + return true; + } + return false; } diff --git a/ccw/src/core/routes/skills-routes.ts b/ccw/src/core/routes/skills-routes.ts index d2756730..ad62e850 100644 --- a/ccw/src/core/routes/skills-routes.ts +++ b/ccw/src/core/routes/skills-routes.ts @@ -2,7 +2,7 @@ * Skills Routes Module * Handles all Skills-related API endpoints */ -import { readFileSync, existsSync, readdirSync, statSync, unlinkSync, renameSync, writeFileSync, mkdirSync, cpSync, rmSync, promises as fsPromises } from 'fs'; +import { readFileSync, existsSync, readdirSync, statSync, unlinkSync, renameSync, promises as fsPromises } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { executeCliTool } from '../../tools/cli-executor.js'; @@ -16,8 +16,6 @@ import type { SkillsConfig, SkillInfo, SkillFolderValidation, - DisabledSkillInfo, - DisabledSkillsConfig, DisabledSkillSummary, ExtendedSkillsConfig, SkillOperationResult @@ -40,106 +38,15 @@ function isRecord(value: unknown): value is Record { // ========== Skills Helper Functions ========== -// ========== Disabled Skills Helper Functions ========== - /** - * Get disabled skills directory path - */ -function getDisabledSkillsDir(location: SkillLocation, projectPath: string): string { - if (location === 'project') { - return join(projectPath, '.claude', '.disabled-skills'); - } - return join(homedir(), '.claude', '.disabled-skills'); -} - -/** - * Get disabled skills config file path - */ -function getDisabledSkillsConfigPath(location: SkillLocation, projectPath: string): string { - if (location === 'project') { - return join(projectPath, '.claude', 'disabled-skills.json'); - } - return join(homedir(), '.claude', 'disabled-skills.json'); -} - -/** - * Load disabled skills configuration - * Throws on JSON parse errors to surface config corruption - */ -function loadDisabledSkillsConfig(location: SkillLocation, projectPath: string): DisabledSkillsConfig { - const configPath = getDisabledSkillsConfigPath(location, projectPath); - - if (!existsSync(configPath)) { - return { skills: {} }; - } - - try { - const content = readFileSync(configPath, 'utf8'); - const config = JSON.parse(content); - return { skills: config.skills || {} }; - } catch (error) { - // Throw on JSON parse errors to surface config corruption - if (error instanceof SyntaxError) { - throw new Error(`Config file corrupted: ${configPath}`); - } - // Log and return empty for other errors (permission, etc.) - console.error(`[Skills] Failed to load disabled skills config: ${error}`); - return { skills: {} }; - } -} - -/** - * Save disabled skills configuration - */ -function saveDisabledSkillsConfig(location: SkillLocation, projectPath: string, config: DisabledSkillsConfig): void { - const configPath = getDisabledSkillsConfigPath(location, projectPath); - const configDir = join(configPath, '..'); - - if (!existsSync(configDir)) { - mkdirSync(configDir, { recursive: true }); - } - - writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8'); -} - -/** - * Move directory with fallback to copy-delete and rollback on failure - */ -function moveDirectory(source: string, target: string): void { - try { - // Try atomic rename first - renameSync(source, target); - } catch (error: unknown) { - const err = error as NodeJS.ErrnoException; - // If rename fails (cross-filesystem, permission issues), fallback to copy-delete - if (err.code === 'EXDEV' || err.code === 'EPERM' || err.code === 'EBUSY') { - cpSync(source, target, { recursive: true, force: true }); - try { - rmSync(source, { recursive: true, force: true }); - } catch (rmError) { - // Rollback: remove the copied target directory to avoid duplicates - try { - rmSync(target, { recursive: true, force: true }); - } catch { - // Ignore rollback errors - } - throw new Error(`Failed to remove source directory after copy: ${(rmError as Error).message}`); - } - } else { - throw error; - } - } -} - -/** - * Disable a skill by moving it to disabled directory + * Disable a skill by renaming SKILL.md to SKILL.md.disabled */ async function disableSkill( skillName: string, location: SkillLocation, projectPath: string, initialPath: string, - reason?: string + reason?: string // Kept for API compatibility but no longer used ): Promise { try { // Validate skill name @@ -147,7 +54,7 @@ async function disableSkill( return { success: false, message: 'Invalid skill name', status: 400 }; } - // Get source directory + // Get skill directory let skillsDir: string; if (location === 'project') { try { @@ -161,42 +68,23 @@ async function disableSkill( skillsDir = join(homedir(), '.claude', 'skills'); } - const sourceDir = join(skillsDir, skillName); - if (!existsSync(sourceDir)) { + const skillDir = join(skillsDir, skillName); + if (!existsSync(skillDir)) { return { success: false, message: 'Skill not found', status: 404 }; } - // Get target directory - const disabledDir = getDisabledSkillsDir(location, projectPath); - if (!existsSync(disabledDir)) { - mkdirSync(disabledDir, { recursive: true }); + const skillMdPath = join(skillDir, 'SKILL.md'); + if (!existsSync(skillMdPath)) { + return { success: false, message: 'SKILL.md not found', status: 404 }; } - const targetDir = join(disabledDir, skillName); - if (existsSync(targetDir)) { - return { success: false, message: 'Skill already exists in disabled directory', status: 409 }; + const disabledPath = join(skillDir, 'SKILL.md.disabled'); + if (existsSync(disabledPath)) { + return { success: false, message: 'Skill already disabled', status: 409 }; } - // Move skill to disabled directory - moveDirectory(sourceDir, targetDir); - - // Update config with rollback on failure - try { - const config = loadDisabledSkillsConfig(location, projectPath); - config.skills[skillName] = { - disabledAt: new Date().toISOString(), - reason - }; - saveDisabledSkillsConfig(location, projectPath, config); - } catch (configError) { - // Rollback: move the skill back to original location - try { - moveDirectory(targetDir, sourceDir); - } catch { - // Ignore rollback errors - skill is in disabled directory but not in config - } - throw new Error(`Failed to update config: ${(configError as Error).message}`); - } + // Rename: SKILL.md → SKILL.md.disabled + renameSync(skillMdPath, disabledPath); return { success: true, message: 'Skill disabled', skillName, location }; } catch (error) { @@ -205,7 +93,7 @@ async function disableSkill( } /** - * Enable a skill by moving it back from disabled directory + * Enable a skill by renaming SKILL.md.disabled back to SKILL.md */ async function enableSkill( skillName: string, @@ -219,14 +107,7 @@ async function enableSkill( return { success: false, message: 'Invalid skill name', status: 400 }; } - // Get source directory (disabled) - const disabledDir = getDisabledSkillsDir(location, projectPath); - const sourceDir = join(disabledDir, skillName); - if (!existsSync(sourceDir)) { - return { success: false, message: 'Disabled skill not found', status: 404 }; - } - - // Get target directory (skills) + // Get skill directory let skillsDir: string; if (location === 'project') { try { @@ -240,33 +121,24 @@ async function enableSkill( skillsDir = join(homedir(), '.claude', 'skills'); } - if (!existsSync(skillsDir)) { - mkdirSync(skillsDir, { recursive: true }); + const skillDir = join(skillsDir, skillName); + if (!existsSync(skillDir)) { + return { success: false, message: 'Skill not found', status: 404 }; } - const targetDir = join(skillsDir, skillName); - if (existsSync(targetDir)) { - return { success: false, message: 'Skill already exists in skills directory', status: 409 }; + const disabledPath = join(skillDir, 'SKILL.md.disabled'); + if (!existsSync(disabledPath)) { + return { success: false, message: 'Disabled skill not found', status: 404 }; } - // Move skill back to skills directory - moveDirectory(sourceDir, targetDir); - - // Update config with rollback on failure - try { - const config = loadDisabledSkillsConfig(location, projectPath); - delete config.skills[skillName]; - saveDisabledSkillsConfig(location, projectPath, config); - } catch (configError) { - // Rollback: move the skill back to disabled directory - try { - moveDirectory(targetDir, sourceDir); - } catch { - // Ignore rollback errors - skill is in skills directory but still in config - } - throw new Error(`Failed to update config: ${(configError as Error).message}`); + const skillMdPath = join(skillDir, 'SKILL.md'); + if (existsSync(skillMdPath)) { + return { success: false, message: 'Skill already enabled', status: 409 }; } + // Rename: SKILL.md.disabled → SKILL.md + renameSync(disabledPath, skillMdPath); + return { success: true, message: 'Skill enabled', skillName, location }; } catch (error) { return { success: false, message: (error as Error).message, status: 500 }; @@ -274,28 +146,33 @@ async function enableSkill( } /** - * Get list of disabled skills + * Get list of disabled skills by checking for SKILL.md.disabled files */ function getDisabledSkillsList(location: SkillLocation, projectPath: string): DisabledSkillSummary[] { - const disabledDir = getDisabledSkillsDir(location, projectPath); - const config = loadDisabledSkillsConfig(location, projectPath); const result: DisabledSkillSummary[] = []; - if (!existsSync(disabledDir)) { + // Get skills directory (not a separate disabled directory) + let skillsDir: string; + if (location === 'project') { + skillsDir = join(projectPath, '.claude', 'skills'); + } else { + skillsDir = join(homedir(), '.claude', 'skills'); + } + + if (!existsSync(skillsDir)) { return result; } try { - const skills = readdirSync(disabledDir, { withFileTypes: true }); + const skills = readdirSync(skillsDir, { withFileTypes: true }); for (const skill of skills) { if (skill.isDirectory()) { - const skillMdPath = join(disabledDir, skill.name, 'SKILL.md'); - if (existsSync(skillMdPath)) { - const content = readFileSync(skillMdPath, 'utf8'); + const disabledPath = join(skillsDir, skill.name, 'SKILL.md.disabled'); + if (existsSync(disabledPath)) { + const content = readFileSync(disabledPath, 'utf8'); const parsed = parseSkillFrontmatter(content); - const skillDir = join(disabledDir, skill.name); + const skillDir = join(skillsDir, skill.name); const supportingFiles = getSupportingFiles(skillDir); - const disabledInfo = config.skills[skill.name] || { disabledAt: new Date().toISOString() }; result.push({ name: parsed.name || skill.name, @@ -306,8 +183,8 @@ function getDisabledSkillsList(location: SkillLocation, projectPath: string): Di location, path: skillDir, supportingFiles, - disabledAt: disabledInfo.disabledAt, - reason: disabledInfo.reason + disabledAt: new Date().toISOString(), // Cannot get exact time without config file + reason: undefined // No longer stored }); } } @@ -396,7 +273,8 @@ function getSupportingFiles(skillDir: string): string[] { try { const entries = readdirSync(skillDir, { withFileTypes: true }); for (const entry of entries) { - if (entry.name !== 'SKILL.md') { + // Exclude SKILL.md and SKILL.md.disabled from supporting files + if (entry.name !== 'SKILL.md' && entry.name !== 'SKILL.md.disabled') { if (entry.isFile()) { files.push(entry.name); } else if (entry.isDirectory()) { diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index a7695d23..8848911a 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -164,6 +164,7 @@ const MODULE_FILES = [ 'views/prompt-history.js', 'views/skills-manager.js', 'views/rules-manager.js', + 'views/commands-manager.js', 'views/claude-manager.js', 'views/api-settings.js', 'views/help.js', diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index c73988b8..4b0a7ddc 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -1603,6 +1603,46 @@ const i18n = { // Rules 'nav.rules': 'Rules', + 'nav.commands': 'Commands', + 'title.commandsManager': 'Commands Manager', + 'commands.title': 'Commands Manager', + 'commands.description': 'Manage Claude Code commands - enable, disable, and organize by group', + 'commands.totalCommands': 'Total Commands', + 'commands.enabledCommands': 'Enabled Commands', + 'commands.disabledCommands': 'Disabled Commands', + 'commands.showDisabled': 'Show Disabled', + 'commands.hideDisabled': 'Hide Disabled', + 'commands.noDescription': 'No description', + 'commands.disabledAt': 'Disabled at', + 'commands.enableConfirm': 'Enable command "{name}"?', + 'commands.disableConfirm': 'Disable command "{name}"?', + 'commands.enableSuccess': 'Command "{name}" enabled successfully', + 'commands.disableSuccess': 'Command "{name}" disabled successfully', + 'commands.toggleError': 'Failed to toggle command status', + 'commands.enabled': 'enabled', + 'commands.disabled': 'disabled', + 'commands.name': 'Name', + 'commands.description': 'Description', + 'commands.scope': 'Scope', + 'commands.status': 'Status', + 'commands.group.cli': 'CLI', + 'commands.group.workflow': 'Workflow', + 'commands.group.memory': 'Memory', + 'commands.group.task': 'Task', + 'commands.group.issue': 'Issue', + 'commands.group.other': 'Other', + 'commands.enableAll': 'Enable All', + 'commands.disableAll': 'Disable All', + 'commands.enableGroupConfirm': 'Enable all commands in "{group}" group?', + 'commands.disableGroupConfirm': 'Disable all commands in "{group}" group?', + 'commands.enableGroupSuccess': 'Group "{group}" enabled successfully', + 'commands.disableGroupSuccess': 'Group "{group}" disabled successfully', + 'commands.locationProject': 'Project', + 'commands.locationUser': 'Global', + 'commands.clickToEnableAll': 'Click to enable all commands in this group', + 'commands.clickToDisableAll': 'Click to disable all commands in this group', + + // Rules 'title.rulesManager': 'Rules Manager', 'rules.title': 'Rules Manager', 'rules.description': 'Manage project and user rules for Claude Code', @@ -4239,6 +4279,46 @@ const i18n = { // Rules 'nav.rules': '规则', + 'nav.commands': '命令', + 'title.commandsManager': '命令管理', + 'commands.title': '命令管理', + 'commands.description': '管理 Claude Code 命令 - 启用、禁用和按组织分组', + 'commands.totalCommands': '总命令数', + 'commands.enabledCommands': '已启用命令', + 'commands.disabledCommands': '已禁用命令', + 'commands.showDisabled': '显示已禁用', + 'commands.hideDisabled': '隐藏已禁用', + 'commands.noDescription': '无描述', + 'commands.disabledAt': '禁用于', + 'commands.enableConfirm': '启用命令 "{name}"?', + 'commands.disableConfirm': '禁用命令 "{name}"?', + 'commands.enableSuccess': '命令 "{name}" 已成功启用', + 'commands.disableSuccess': '命令 "{name}" 已成功禁用', + 'commands.toggleError': '切换命令状态失败', + 'commands.enabled': '已启用', + 'commands.disabled': '已禁用', + 'commands.name': '名称', + 'commands.description': '描述', + 'commands.scope': '作用域', + 'commands.status': '状态', + 'commands.group.cli': 'CLI', + 'commands.group.workflow': '工作流', + 'commands.group.memory': '记忆', + 'commands.group.task': '任务', + 'commands.group.issue': '问题', + 'commands.group.other': '其他', + 'commands.enableAll': '全部启用', + 'commands.disableAll': '全部禁用', + 'commands.enableGroupConfirm': '启用 "{group}" 分组中的所有命令?', + 'commands.disableGroupConfirm': '禁用 "{group}" 分组中的所有命令?', + 'commands.enableGroupSuccess': '分组 "{group}" 已全部启用', + 'commands.disableGroupSuccess': '分组 "{group}" 已全部禁用', + 'commands.locationProject': '项目', + 'commands.locationUser': '全局', + 'commands.clickToEnableAll': '点击启用该分组所有命令', + 'commands.clickToDisableAll': '点击禁用该分组所有命令', + + // Rules 'title.rulesManager': '规则管理', 'rules.title': '规则管理', 'rules.description': '管理 Claude Code 的项目和用户规则', diff --git a/ccw/src/templates/dashboard-js/views/commands-manager.js b/ccw/src/templates/dashboard-js/views/commands-manager.js index 01fdb106..611b5020 100644 --- a/ccw/src/templates/dashboard-js/views/commands-manager.js +++ b/ccw/src/templates/dashboard-js/views/commands-manager.js @@ -4,7 +4,9 @@ // ========== Commands State ========== var commandsData = { groups: {}, // Organized by group name: { cli: [...], workflow: [...], memory: [...], task: [...], issue: [...] } - allCommands: [] + allCommands: [], + projectGroupsConfig: { groups: {}, assignments: {} }, + userGroupsConfig: { groups: {}, assignments: {} } }; var expandedGroups = { cli: true, @@ -15,6 +17,7 @@ var expandedGroups = { }; var showDisabledCommands = false; var commandsLoading = false; +var currentLocation = 'project'; // 'project' or 'user' // ========== Main Render Function ========== async function renderCommandsManager() { @@ -47,11 +50,20 @@ async function loadCommandsData() { if (!response.ok) throw new Error('Failed to load commands'); const data = await response.json(); + // Store groups config + commandsData.projectGroupsConfig = data.projectGroupsConfig || { groups: {}, assignments: {} }; + commandsData.userGroupsConfig = data.userGroupsConfig || { groups: {}, assignments: {} }; + + // Filter commands based on currentLocation + const allCommands = currentLocation === 'project' + ? (data.projectCommands || []) + : (data.userCommands || []); + // Organize commands by group commandsData.groups = {}; - commandsData.allCommands = data.commands || []; + commandsData.allCommands = allCommands; - data.commands.forEach(cmd => { + allCommands.forEach(cmd => { const group = cmd.group || 'other'; if (!commandsData.groups[group]) { commandsData.groups[group] = []; @@ -63,7 +75,12 @@ async function loadCommandsData() { updateCommandsBadge(); } catch (err) { console.error('Failed to load commands:', err); - commandsData = { groups: {}, allCommands: [] }; + commandsData = { + groups: {}, + allCommands: [], + projectGroupsConfig: { groups: {}, assignments: {} }, + userGroupsConfig: { groups: {}, assignments: {} } + }; } finally { commandsLoading = false; } @@ -77,12 +94,47 @@ function updateCommandsBadge() { } } +async function switchLocation(location) { + if (location === currentLocation) return; + currentLocation = location; + await loadCommandsData(); + renderCommandsView(); +} + function renderCommandsView() { const container = document.getElementById('mainContent'); if (!container) return; const groups = commandsData.groups || {}; - const groupNames = ['cli', 'workflow', 'memory', 'task', 'issue', 'other']; + + // Dynamic groups: known groups first, then custom groups hierarchically sorted, 'other' last + const knownOrder = ['cli', 'workflow', 'memory', 'task', 'issue']; + const allGroupNames = Object.keys(groups); + + // Separate top-level known groups and nested groups + const topLevelKnown = allGroupNames.filter(g => knownOrder.includes(g)); + const nestedAndCustom = allGroupNames.filter(g => g !== 'other' && !knownOrder.includes(g)); + + // Sort nested/custom groups hierarchically + nestedAndCustom.sort((a, b) => { + // Split by path separator + const aParts = a.split('/'); + const bParts = b.split('/'); + + // Compare level by level + for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) { + if (aParts[i] !== bParts[i]) { + return aParts[i].localeCompare(bParts[i]); + } + } + + // If all parts are equal, shorter path comes first + return aParts.length - bParts.length; + }); + + const groupNames = [...topLevelKnown.filter(g => groups[g] && groups[g].length > 0), + ...nestedAndCustom.filter(g => groups[g] && groups[g].length > 0), + 'other'].filter(g => groups[g] && groups[g].length > 0); const totalEnabled = commandsData.allCommands.filter(cmd => cmd.enabled).length; const totalDisabled = commandsData.allCommands.filter(cmd => !cmd.enabled).length; @@ -100,11 +152,27 @@ function renderCommandsView() {

${t('commands.description') || 'Enable/disable CCW commands'}

- +
+ +
+ + +
+ + +
@@ -128,11 +196,7 @@ function renderCommandsView() {
- ${groupNames.map(groupName => { - const commands = groups[groupName] || []; - if (commands.length === 0) return ''; - return renderAccordionGroup(groupName, commands); - }).join('')} + ${groupNames.map(groupName => renderAccordionGroup(groupName, groups[groupName])).join('')}
`; @@ -142,6 +206,8 @@ function renderCommandsView() { } function renderAccordionGroup(groupName, commands) { + // Default to expanded for new/custom groups + if (expandedGroups[groupName] === undefined) expandedGroups[groupName] = true; const isExpanded = expandedGroups[groupName]; const enabledCommands = commands.filter(cmd => cmd.enabled); const disabledCommands = commands.filter(cmd => !cmd.enabled); @@ -177,26 +243,53 @@ function renderAccordionGroup(groupName, commands) { return `
-
-
+
+
-

${groupName}

-

${enabledCommands.length}/${commands.length} enabled

+

${t('commands.group.' + groupName) || groupName}

+

${enabledCommands.length}/${commands.length} ${t('commands.enabled') || 'enabled'}

- ${commands.length} +
+ + + ${commands.length} +
- + ${isExpanded ? `
-
- ${visibleCommands.map(cmd => renderCommandCard(cmd)).join('')} +
+ + + + + + + + + + + + + + + + + ${visibleCommands.map(cmd => renderCommandRow(cmd)).join('')} + +
${t('commands.name') || 'Name'}${t('commands.description') || 'Description'}${t('commands.scope') || 'Scope'}${t('commands.status') || 'Status'}
` : ''} @@ -204,53 +297,40 @@ function renderAccordionGroup(groupName, commands) { `; } -function renderCommandCard(command) { +function renderCommandRow(command) { const isDisabled = !command.enabled; - const cardOpacity = isDisabled ? 'opacity-60' : ''; return ` -
-
-
-

${escapeHtml(command.name)}

- - ${command.group || 'other'} - -
-
- -
-
- -

${escapeHtml(command.description || t('commands.noDescription') || 'No description available')}

- -
-
- - - ${command.scope || 'project'} - + + +
+ ${escapeHtml(command.name)} ${command.triggers && command.triggers.length > 0 ? ` - - - ${command.triggers.length} trigger${command.triggers.length > 1 ? 's' : ''} + + ${command.triggers.length} ` : ''}
- ${isDisabled && command.disabledAt ? ` - - ${t('commands.disabledAt') || 'Disabled'}: ${formatDisabledDate(command.disabledAt)} - - ` : ''} -
-
+ + +
${escapeHtml(command.description || t('commands.noDescription') || '-')}
+ + + ${command.scope || 'project'} + + +
+ +
+ + `; } @@ -284,20 +364,6 @@ async function toggleCommandEnabled(commandName, currentlyEnabled) { var loadingKey = commandName; if (toggleLoadingCommands[loadingKey]) return; - var action = currentlyEnabled ? 'disable' : 'enable'; - var confirmMessage = currentlyEnabled - ? t('commands.disableConfirm', { name: commandName }) || `Disable command "${commandName}"?` - : t('commands.enableConfirm', { name: commandName }) || `Enable command "${commandName}"?`; - - if (!confirm(confirmMessage)) { - // Reset toggle state if user cancels - const toggleInput = document.querySelector(`[data-command-toggle="${commandName}"]`); - if (toggleInput) { - toggleInput.checked = currentlyEnabled; - } - return; - } - // Set loading state toggleLoadingCommands[loadingKey] = true; var toggleInput = document.querySelector('[data-command-toggle="' + commandName + '"]'); @@ -306,10 +372,14 @@ async function toggleCommandEnabled(commandName, currentlyEnabled) { } try { - var response = await fetch('/api/commands/' + encodeURIComponent(commandName) + '/' + action, { + var response = await fetch('/api/commands/' + encodeURIComponent(commandName) + '/toggle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ projectPath: projectPath }) + body: JSON.stringify({ + projectPath: projectPath, + location: currentLocation, + enable: !currentlyEnabled + }) }); if (!response.ok) { @@ -352,6 +422,50 @@ async function toggleCommandEnabled(commandName, currentlyEnabled) { } } +async function toggleGroupEnabled(groupName, currentlyAllEnabled) { + const enable = !currentlyAllEnabled; + + try { + const response = await fetch('/api/commands/group/' + encodeURIComponent(groupName) + '/toggle', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + projectPath: projectPath, + location: currentLocation, + enable: enable + }) + }); + + if (!response.ok) { + var errorMessage = 'Operation failed'; + try { + var error = await response.json(); + errorMessage = error.message || errorMessage; + } catch (jsonErr) { + errorMessage = response.statusText || errorMessage; + } + throw new Error(errorMessage); + } + + // Reload commands data + await loadCommandsData(); + renderCommandsView(); + + if (window.showToast) { + const groupLabel = t('commands.group.' + groupName) || groupName; + const message = enable + ? (t('commands.enableGroupSuccess', { group: groupLabel }) || `Group "${groupLabel}" enabled`) + : (t('commands.disableGroupSuccess', { group: groupLabel }) || `Group "${groupLabel}" disabled`); + showToast(message, 'success'); + } + } catch (err) { + console.error('Failed to toggle group:', err); + if (window.showToast) { + showToast(err.message || t('commands.toggleError') || 'Failed to toggle group', 'error'); + } + } +} + function formatDisabledDate(isoString) { try { const date = new Date(isoString); diff --git a/ccw/src/types/skill-types.ts b/ccw/src/types/skill-types.ts index 2e1c9526..86cb4468 100644 --- a/ccw/src/types/skill-types.ts +++ b/ccw/src/types/skill-types.ts @@ -8,25 +8,6 @@ */ export type SkillLocation = 'project' | 'user'; -/** - * Information about a disabled skill - */ -export interface DisabledSkillInfo { - /** When the skill was disabled */ - disabledAt: string; - /** Optional reason for disabling */ - reason?: string; -} - -/** - * Configuration for disabled skills - * Stored in disabled-skills.json - */ -export interface DisabledSkillsConfig { - /** Map of skill name to disabled info */ - skills: Record; -} - /** * Result of a skill operation (enable/disable) */