From a512564b5a406e514595271d1abd5223edf4063c Mon Sep 17 00:00:00 2001 From: catlog22 Date: Fri, 13 Feb 2026 12:35:31 +0800 Subject: [PATCH] Add integration verification and validation phases, role templates, and static graph tests - Implement Phase 4: Integration Verification to ensure skill package consistency. - Implement Phase 5: Validation to verify quality and deliver the final skill package. - Create role-template.md for generating per-role execution detail files. - Create skill-router-template.md for generating SKILL.md with role-based routing. - Add tests for static graph relationship writing during index build in test_static_graph_integration.py. --- .../specs/team-design-patterns.md | 1 + .claude/skills/team-skill-designer/SKILL.md | 285 ++++++++ .../phases/01-requirements-collection.md | 283 ++++++++ .../phases/02-pattern-analysis.md | 217 ++++++ .../phases/03-skill-generation.md | 668 ++++++++++++++++++ .../phases/04-integration-verification.md | 178 +++++ .../phases/05-validation.md | 203 ++++++ .../templates/role-template.md | 333 +++++++++ .../templates/skill-router-template.md | 224 ++++++ .../src/components/shared/TaskDrawer.tsx | 80 +-- ccw/frontend/src/lib/api.ts | 136 +++- codex-lens/src/codexlens/config.py | 4 +- .../src/codexlens/search/chain_search.py | 47 ++ .../tests/test_static_graph_integration.py | 289 ++++++++ 14 files changed, 2897 insertions(+), 51 deletions(-) create mode 100644 .claude/skills/team-skill-designer/SKILL.md create mode 100644 .claude/skills/team-skill-designer/phases/01-requirements-collection.md create mode 100644 .claude/skills/team-skill-designer/phases/02-pattern-analysis.md create mode 100644 .claude/skills/team-skill-designer/phases/03-skill-generation.md create mode 100644 .claude/skills/team-skill-designer/phases/04-integration-verification.md create mode 100644 .claude/skills/team-skill-designer/phases/05-validation.md create mode 100644 .claude/skills/team-skill-designer/templates/role-template.md create mode 100644 .claude/skills/team-skill-designer/templates/skill-router-template.md create mode 100644 codex-lens/tests/test_static_graph_integration.py diff --git a/.claude/skills/team-command-designer/specs/team-design-patterns.md b/.claude/skills/team-command-designer/specs/team-design-patterns.md index f5dde22e..e51d4785 100644 --- a/.claude/skills/team-command-designer/specs/team-design-patterns.md +++ b/.claude/skills/team-command-designer/specs/team-design-patterns.md @@ -452,6 +452,7 @@ When designing a new team command, verify: ### Infrastructure Patterns - [ ] YAML front matter with `group: team` - [ ] Message bus section with `team_msg` logging +- [ ] CLI fallback section with `ccw team` CLI examples and parameter mapping - [ ] Role-specific message types defined - [ ] Task lifecycle: TaskList -> TaskGet -> TaskUpdate flow - [ ] Unique task prefix (no collision with existing PLAN/IMPL/TEST/REVIEW, scan `team/**/*.md`) diff --git a/.claude/skills/team-skill-designer/SKILL.md b/.claude/skills/team-skill-designer/SKILL.md new file mode 100644 index 00000000..b0c28c75 --- /dev/null +++ b/.claude/skills/team-skill-designer/SKILL.md @@ -0,0 +1,285 @@ +--- +name: team-skill-designer +description: Design and generate unified team skills with role-based routing. All team members invoke ONE skill, SKILL.md routes to role-specific execution via --role arg. Triggers on "design team skill", "create team skill", "team skill designer". +allowed-tools: Task, AskUserQuestion, Read, Write, Bash, Glob, Grep +--- + +# Team Skill Designer + +Meta-skill for creating unified team skills where all team members invoke ONE skill with role-based routing. Generates a complete skill package with SKILL.md as role router and `roles/` folder for per-role execution detail. + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Team Skill Designer (this meta-skill) │ +│ → Collect requirements → Analyze patterns → Generate skill pkg │ +└───────────────┬─────────────────────────────────────────────────┘ + │ + ┌───────────┼───────────┬───────────┬───────────┐ + ↓ ↓ ↓ ↓ ↓ +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Phase 1 │ │ Phase 2 │ │ Phase 3 │ │ Phase 4 │ │ Phase 5 │ +│ Require │ │ Pattern │ │ Skill │ │ Integ │ │ Valid │ +│ Collect │ │ Analyze │ │ Gen │ │ Verify │ │ │ +└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ + ↓ ↓ ↓ ↓ ↓ + team- patterns SKILL.md + report validated + config.json .json roles/*.md .json skill pkg +``` + +## Key Innovation: Unified Skill + Role Router + +**Before (command approach)**: +``` +.claude/commands/team/ +├── coordinate.md → /team:coordinate +├── plan.md → /team:plan +├── execute.md → /team:execute +├── test.md → /team:test +└── review.md → /team:review +``` +→ 5 separate command files, 5 separate skill paths + +**After (unified skill approach)**: +``` +.claude/skills/team-{name}/ +├── SKILL.md → Skill(skill="team-{name}", args="--role=xxx") +├── roles/ +│ ├── coordinator.md +│ ├── planner.md +│ ├── executor.md +│ ├── tester.md +│ └── reviewer.md +└── specs/ + └── team-config.json +``` +→ 1 skill entry point, --role arg routes to per-role execution + +**Coordinator spawns teammates with**: +```javascript +Task({ + prompt: `...调用 Skill(skill="team-{name}", args="--role=planner") 执行规划...` +}) +``` + +## Target Output Structure + +``` +.claude/skills/team-{name}/ +├── SKILL.md # Role router + shared infrastructure +│ ├─ Frontmatter +│ ├─ Architecture Overview (role routing diagram) +│ ├─ Role Router (parse --role → Read roles/{role}.md → execute) +│ ├─ Shared Infrastructure (message bus, task lifecycle) +│ ├─ Coordinator Spawn Template +│ └─ Error Handling +├── roles/ # Role-specific execution detail +│ ├── coordinator.md # Orchestration logic +│ ├── {role-1}.md # First worker role +│ ├── {role-2}.md # Second worker role +│ └── ... +└── specs/ # [Optional] Team-specific config + └── team-config.json +``` + +## Core Design Patterns + +### Pattern 1: Role Router (Unified Entry Point) + +SKILL.md parses `$ARGUMENTS` to extract `--role`: +``` +Input: Skill(skill="team-{name}", args="--role=planner") + ↓ Parse --role=planner + ↓ Read roles/planner.md + ↓ Execute planner-specific 5-phase logic +``` + +No --role → error (role is required, set by coordinator spawn). + +### Pattern 2: Shared Infrastructure in SKILL.md + +SKILL.md defines ONCE, all roles inherit: +- Message bus pattern (team_msg + CLI fallback) +- Task lifecycle (TaskList → TaskGet → TaskUpdate) +- Team name and session directory conventions +- Error handling and escalation rules + +### Pattern 3: Role Files = Full Execution Detail + +Each `roles/{role}.md` contains: +- Role-specific 5-phase implementation +- Per-role message types +- Per-role task prefix +- Complete code (no `Ref:` back to SKILL.md) + +### Pattern 4: Batch Role Generation + +Phase 1 collects ALL roles at once (not one at a time): +- Team name + all role definitions in one pass +- Coordinator is always generated +- Worker roles collected as a batch + +### Pattern 5: Spec Reference (No Duplication) + +Design pattern specs are referenced from team-command-designer: +``` +specs → ../team-command-designer/specs/team-design-patterns.md +specs → ../team-command-designer/specs/collaboration-patterns.md +specs → ../team-command-designer/specs/quality-standards.md +``` + +--- + +## Mandatory Prerequisites + +> **Do NOT skip**: Read these before any execution. + +### Specification Documents (Required Reading) + +| Document | Purpose | When | +|----------|---------|------| +| [../team-command-designer/specs/team-design-patterns.md](../team-command-designer/specs/team-design-patterns.md) | Infrastructure patterns (8) + collaboration index | **Must read** | +| [../team-command-designer/specs/collaboration-patterns.md](../team-command-designer/specs/collaboration-patterns.md) | 10 collaboration patterns with convergence control | **Must read** | +| [../team-command-designer/specs/quality-standards.md](../team-command-designer/specs/quality-standards.md) | Quality criteria | Must read before generation | + +### Template Files (Must read before generation) + +| Document | Purpose | +|----------|---------| +| [templates/skill-router-template.md](templates/skill-router-template.md) | Generated SKILL.md template with role router | +| [templates/role-template.md](templates/role-template.md) | Generated role file template | + +### Existing Reference + +| Document | Purpose | +|----------|---------| +| `.claude/commands/team/coordinate.md` | Coordinator spawn patterns | +| `.claude/commands/team/plan.md` | Planner role reference | +| `.claude/commands/team/execute.md` | Executor role reference | +| `.claude/commands/team/test.md` | Tester role reference | +| `.claude/commands/team/review.md` | Reviewer role reference | + +--- + +## Execution Flow + +``` +Phase 0: Specification Study (MANDATORY) + -> Read: ../team-command-designer/specs/team-design-patterns.md + -> Read: ../team-command-designer/specs/collaboration-patterns.md + -> Read: templates/skill-router-template.md + templates/role-template.md + -> Read: 1-2 existing team commands for reference + -> Output: Internalized requirements (in-memory) + +Phase 1: Requirements Collection + -> Ref: phases/01-requirements-collection.md + - Collect team name and ALL role definitions (batch) + - For each role: name, responsibility, task prefix, capabilities + - Pipeline definition (task chain order) + - Output: team-config.json (team-level + per-role config) + +Phase 2: Pattern Analysis + -> Ref: phases/02-pattern-analysis.md + - Per-role: find most similar existing command + - Per-role: select infrastructure + collaboration patterns + - Per-role: map 5-phase structure + - Output: pattern-analysis.json + +Phase 3: Skill Package Generation + -> Ref: phases/03-skill-generation.md + - Generate SKILL.md (role router + shared infrastructure) + - Generate roles/*.md (per-role execution detail) + - Generate specs/team-config.json + - Output: .claude/skills/team-{name}/ complete package + +Phase 4: Integration Verification + -> Ref: phases/04-integration-verification.md + - Verify role router references match role files + - Verify task prefixes are unique across roles + - Verify message type compatibility + - Output: integration-report.json + +Phase 5: Validation + -> Ref: phases/05-validation.md + - Structural completeness per role file + - Pattern compliance per role file + - Quality scoring and delivery + - Output: validation-report.json + delivered skill package +``` + +**Phase Reference Documents** (read on-demand): + +| Phase | Document | Purpose | +|-------|----------|---------| +| 1 | [phases/01-requirements-collection.md](phases/01-requirements-collection.md) | Batch collect team + all role definitions | +| 2 | [phases/02-pattern-analysis.md](phases/02-pattern-analysis.md) | Per-role pattern matching and phase mapping | +| 3 | [phases/03-skill-generation.md](phases/03-skill-generation.md) | Generate unified skill package | +| 4 | [phases/04-integration-verification.md](phases/04-integration-verification.md) | Verify internal consistency | +| 5 | [phases/05-validation.md](phases/05-validation.md) | Quality gate and delivery | + +## Directory Setup + +```javascript +const timestamp = new Date().toISOString().slice(0,19).replace(/[-:T]/g, ''); +const workDir = `.workflow/.scratchpad/team-skill-${timestamp}`; + +Bash(`mkdir -p "${workDir}"`); +``` + +## Output Structure + +``` +.workflow/.scratchpad/team-skill-{timestamp}/ +├── team-config.json # Phase 1 output (team + all roles) +├── pattern-analysis.json # Phase 2 output (per-role patterns) +├── integration-report.json # Phase 4 output +├── validation-report.json # Phase 5 output +└── preview/ # Phase 3 output (preview before delivery) + ├── SKILL.md + ├── roles/ + │ ├── coordinator.md + │ └── {role-N}.md + └── specs/ + └── team-config.json + +Final delivery: +.claude/skills/team-{name}/ +├── SKILL.md +├── roles/ +│ ├── coordinator.md +│ └── ... +└── specs/ + └── team-config.json +``` + +## Comparison: Command Designer vs Skill Designer + +| Aspect | team-command-designer | team-skill-designer | +|--------|----------------------|---------------------| +| Output | N separate .md command files | 1 skill package (SKILL.md + roles/) | +| Entry point | N skill paths (/team:xxx) | 1 skill path + --role arg | +| Shared infra | Duplicated in each command | Defined once in SKILL.md | +| Role isolation | Complete (separate files) | Complete (roles/ directory) | +| Coordinator spawn | `Skill(skill="team:plan")` | `Skill(skill="team-{name}", args="--role=planner")` | +| Role generation | One role at a time | All roles in batch | +| Template | command-template.md | skill-router-template.md + role-template.md | + +## Error Handling + +| Scenario | Resolution | +|----------|------------| +| Specs not found | Fall back to inline pattern knowledge | +| Role name conflicts | AskUserQuestion for rename | +| Task prefix conflicts | Suggest alternative prefix | +| Template variable unresolved | FAIL with specific variable name | +| Quality score < 60% | Re-run Phase 3 with additional context | + +## Debugging + +| Issue | Solution | +|-------|----------| +| Generated SKILL.md missing router | Check templates/skill-router-template.md | +| Role file missing message bus | Check templates/role-template.md | +| Integration check fails | Review phases/04-integration-verification.md | +| Quality score below threshold | Review specs/quality-standards.md | diff --git a/.claude/skills/team-skill-designer/phases/01-requirements-collection.md b/.claude/skills/team-skill-designer/phases/01-requirements-collection.md new file mode 100644 index 00000000..0260d770 --- /dev/null +++ b/.claude/skills/team-skill-designer/phases/01-requirements-collection.md @@ -0,0 +1,283 @@ +# Phase 1: Requirements Collection (Batch Mode) + +Collect team definition and ALL role definitions in one pass. + +## Objective + +- Determine team name and display name +- Collect ALL roles (coordinator + workers) in batch +- For each role: name, responsibility, task prefix, capabilities +- Define pipeline (task chain order) +- Generate team-config.json + +## Input + +- User request (`$ARGUMENTS` or interactive input) +- Specification: `../team-command-designer/specs/team-design-patterns.md` (read in Phase 0) + +## Execution Steps + +### Step 1: Team Basic Information + +```javascript +const teamInfo = await AskUserQuestion({ + questions: [ + { + question: "团队名称是什么?(小写,用作 skill 文件夹名:.claude/skills/team-{name}/)", + header: "Team Name", + multiSelect: false, + options: [ + { label: "自定义", description: "输入自定义团队名称" }, + { label: "dev", description: "开发团队(plan/execute/test/review)" }, + { label: "spec", description: "规格文档团队(analyst/writer/reviewer/discuss)" }, + { label: "security", description: "安全审计团队" } + ] + }, + { + question: "团队使用什么 pipeline 模型?", + header: "Pipeline", + multiSelect: false, + options: [ + { label: "Standard (Recommended)", description: "PLAN → IMPL → TEST + REVIEW(标准开发流水线)" }, + { label: "Document Chain", description: "RESEARCH → DRAFT → DISCUSS → REVIEW(文档工作流)" }, + { label: "Custom", description: "自定义 pipeline" } + ] + } + ] +}) +``` + +### Step 2: Role Definitions (Batch) + +```javascript +// Always include coordinator +const roles = [{ + name: "coordinator", + responsibility_type: "Orchestration", + task_prefix: null, // coordinator creates tasks, doesn't receive them + description: "Pipeline orchestration, team lifecycle, cross-stage coordination" +}] + +// Collect worker roles based on pipeline model +const pipelineType = teamInfo["Pipeline"] + +if (pipelineType.includes("Standard")) { + // Pre-fill standard development roles + roles.push( + { name: "planner", responsibility_type: "Orchestration", task_prefix: "PLAN", description: "Code exploration and implementation planning" }, + { name: "executor", responsibility_type: "Code generation", task_prefix: "IMPL", description: "Code implementation following approved plan" }, + { name: "tester", responsibility_type: "Validation", task_prefix: "TEST", description: "Test execution and fix cycles" }, + { name: "reviewer", responsibility_type: "Read-only analysis", task_prefix: "REVIEW", description: "Multi-dimensional code review" } + ) +} else if (pipelineType.includes("Document")) { + roles.push( + { name: "analyst", responsibility_type: "Orchestration", task_prefix: "RESEARCH", description: "Seed analysis, codebase exploration, context collection" }, + { name: "writer", responsibility_type: "Code generation", task_prefix: "DRAFT", description: "Document drafting following templates" }, + { name: "reviewer", responsibility_type: "Read-only analysis", task_prefix: "QUALITY", description: "Cross-document quality verification" }, + { name: "discuss", responsibility_type: "Orchestration", task_prefix: "DISCUSS", description: "Structured team discussion and consensus building" } + ) +} else { + // Custom: ask user for each role +} +``` + +### Step 3: Role Customization (Interactive) + +```javascript +// Allow user to customize pre-filled roles +const customization = await AskUserQuestion({ + questions: [ + { + question: "是否需要自定义角色?(默认角色已根据 pipeline 预填充)", + header: "Customize", + multiSelect: false, + options: [ + { label: "使用默认 (Recommended)", description: "直接使用预填充的角色定义" }, + { label: "添加角色", description: "在默认基础上添加新角色" }, + { label: "修改角色", description: "修改默认角色定义" }, + { label: "从零开始", description: "清空默认,逐个定义角色" } + ] + } + ] +}) + +if (customization["Customize"].includes("添加角色")) { + const newRole = await AskUserQuestion({ + questions: [ + { + question: "新角色名称?(小写)", + header: "Role Name", + multiSelect: false, + options: [ + { label: "自定义", description: "输入自定义角色名" }, + { label: "deployer", description: "部署和发布管理" }, + { label: "documenter", description: "文档生成" }, + { label: "monitor", description: "监控和告警" } + ] + }, + { + question: "角色职责类型?", + header: "Type", + multiSelect: false, + options: [ + { label: "Read-only analysis", description: "分析/审查/报告(不修改文件)" }, + { label: "Code generation", description: "写/改代码文件" }, + { label: "Orchestration", description: "协调子任务和 agent" }, + { label: "Validation", description: "测试/验证/审计" } + ] + } + ] + }) + // Add to roles array +} +``` + +### Step 4: Capability Selection (Per Role) + +```javascript +// For each worker role, determine capabilities +for (const role of roles.filter(r => r.name !== 'coordinator')) { + // Infer capabilities from responsibility type + const baseTools = ["SendMessage(*)", "TaskUpdate(*)", "TaskList(*)", "TaskGet(*)", "TodoWrite(*)", "Read(*)", "Bash(*)", "Glob(*)", "Grep(*)"] + + if (role.responsibility_type === "Code generation") { + role.allowed_tools = [...baseTools, "Write(*)", "Edit(*)", "Task(*)"] + role.adaptive_routing = true + } else if (role.responsibility_type === "Orchestration") { + role.allowed_tools = [...baseTools, "Write(*)", "Task(*)"] + role.adaptive_routing = true + } else if (role.responsibility_type === "Validation") { + role.allowed_tools = [...baseTools, "Write(*)", "Edit(*)", "Task(*)"] + role.adaptive_routing = false + } else { + // Read-only analysis + role.allowed_tools = [...baseTools, "Task(*)"] + role.adaptive_routing = false + } + + // Infer message types + const roleMsgTypes = { + "Read-only analysis": [ + { type: `${role.name}_result`, trigger: "Analysis complete" }, + { type: "error", trigger: "Blocking error" } + ], + "Code generation": [ + { type: `${role.name}_complete`, trigger: "Generation complete" }, + { type: `${role.name}_progress`, trigger: "Batch progress" }, + { type: "error", trigger: "Blocking error" } + ], + "Orchestration": [ + { type: `${role.name}_ready`, trigger: "Results ready" }, + { type: `${role.name}_progress`, trigger: "Progress update" }, + { type: "error", trigger: "Blocking error" } + ], + "Validation": [ + { type: `${role.name}_result`, trigger: "Validation complete" }, + { type: "fix_required", trigger: "Critical issues found" }, + { type: "error", trigger: "Blocking error" } + ] + } + role.message_types = roleMsgTypes[role.responsibility_type] || [] +} + +// Coordinator special config +roles[0].allowed_tools = [ + "TeamCreate(*)", "TeamDelete(*)", "SendMessage(*)", + "TaskCreate(*)", "TaskUpdate(*)", "TaskList(*)", "TaskGet(*)", + "Task(*)", "AskUserQuestion(*)", "TodoWrite(*)", + "Read(*)", "Bash(*)", "Glob(*)", "Grep(*)" +] +roles[0].message_types = [ + { type: "plan_approved", trigger: "Plan approved" }, + { type: "plan_revision", trigger: "Revision requested" }, + { type: "task_unblocked", trigger: "Task unblocked" }, + { type: "shutdown", trigger: "Team shutdown" }, + { type: "error", trigger: "Coordination error" } +] +``` + +### Step 5: Pipeline Definition + +```javascript +// Build pipeline from roles and their task chain positions +function buildPipeline(roles, pipelineType) { + if (pipelineType.includes("Standard")) { + return { + stages: [ + { name: "PLAN", role: "planner", blockedBy: [] }, + { name: "IMPL", role: "executor", blockedBy: ["PLAN"] }, + { name: "TEST", role: "tester", blockedBy: ["IMPL"] }, + { name: "REVIEW", role: "reviewer", blockedBy: ["IMPL"] } + ], + diagram: "需求 → [PLAN: planner] → coordinator 审批 → [IMPL: executor] → [TEST + REVIEW: tester/reviewer] → 汇报" + } + } + if (pipelineType.includes("Document")) { + return { + stages: [ + { name: "RESEARCH", role: "analyst", blockedBy: [] }, + { name: "DISCUSS-scope", role: "discuss", blockedBy: ["RESEARCH"] }, + { name: "DRAFT", role: "writer", blockedBy: ["DISCUSS-scope"] }, + { name: "DISCUSS-eval", role: "discuss", blockedBy: ["DRAFT"] }, + { name: "QUALITY", role: "reviewer", blockedBy: ["DRAFT"] } + ], + diagram: "RESEARCH → DISCUSS → DRAFT → DISCUSS → QUALITY → Deliver" + } + } + // Custom pipeline + return { stages: [], diagram: "Custom pipeline" } +} + +const pipeline = buildPipeline(roles, pipelineType) +``` + +### Step 6: Generate Configuration + +```javascript +const teamName = teamInfo["Team Name"] === "自定义" + ? teamInfo["Team Name_other"] + : teamInfo["Team Name"] + +const config = { + team_name: teamName, + team_display_name: teamName.charAt(0).toUpperCase() + teamName.slice(1), + skill_name: `team-${teamName}`, + skill_path: `.claude/skills/team-${teamName}/`, + pipeline_type: pipelineType, + pipeline: pipeline, + roles: roles.map(r => ({ + ...r, + display_name: `${teamName} ${r.name}`, + name_upper: r.name.toUpperCase() + })), + worker_roles: roles.filter(r => r.name !== 'coordinator').map(r => ({ + ...r, + display_name: `${teamName} ${r.name}`, + name_upper: r.name.toUpperCase() + })), + all_roles_tools_union: [...new Set(roles.flatMap(r => r.allowed_tools))].join(', '), + role_list: roles.map(r => r.name).join(', ') +} + +Write(`${workDir}/team-config.json`, JSON.stringify(config, null, 2)) +``` + +## Output + +- **File**: `team-config.json` +- **Format**: JSON +- **Location**: `{workDir}/team-config.json` + +## Quality Checklist + +- [ ] Team name is lowercase, valid as folder/skill name +- [ ] Coordinator is always included +- [ ] At least 2 worker roles defined +- [ ] Task prefixes are UPPERCASE and unique across roles +- [ ] Pipeline stages reference valid roles +- [ ] All roles have message types defined +- [ ] Allowed tools include minimum set per role + +## Next Phase + +-> [Phase 2: Pattern Analysis](02-pattern-analysis.md) diff --git a/.claude/skills/team-skill-designer/phases/02-pattern-analysis.md b/.claude/skills/team-skill-designer/phases/02-pattern-analysis.md new file mode 100644 index 00000000..23551b95 --- /dev/null +++ b/.claude/skills/team-skill-designer/phases/02-pattern-analysis.md @@ -0,0 +1,217 @@ +# Phase 2: Pattern Analysis + +Analyze applicable patterns for each role in the team. + +## Objective + +- Per-role: find most similar existing command +- Per-role: select infrastructure + collaboration patterns +- Per-role: map 5-phase structure to role responsibilities +- Generate pattern-analysis.json + +## Input + +- Dependency: `team-config.json` (Phase 1) +- Specification: `../team-command-designer/specs/team-design-patterns.md` (read in Phase 0) + +## Execution Steps + +### Step 1: Load Configuration + +```javascript +const config = JSON.parse(Read(`${workDir}/team-config.json`)) +``` + +### Step 2: Per-Role Similarity Mapping + +```javascript +const similarityMap = { + "Read-only analysis": { + primary: "review", secondary: "plan", + reason: "Both analyze code and report findings with severity classification" + }, + "Code generation": { + primary: "execute", secondary: "test", + reason: "Both write/modify code and self-validate" + }, + "Orchestration": { + primary: "plan", secondary: "coordinate", + reason: "Both coordinate sub-tasks and produce structured output" + }, + "Validation": { + primary: "test", secondary: "review", + reason: "Both validate quality with structured criteria" + } +} + +const roleAnalysis = config.worker_roles.map(role => { + const similarity = similarityMap[role.responsibility_type] + return { + role_name: role.name, + similar_to: similarity, + reference_command: `.claude/commands/team/${similarity.primary}.md` + } +}) +``` + +### Step 3: Per-Role Phase Mapping + +```javascript +const phaseMapping = { + "Read-only analysis": { + phase2: "Context Loading", + phase3: "Analysis Execution", + phase4: "Finding Summary" + }, + "Code generation": { + phase2: "Task & Plan Loading", + phase3: "Code Implementation", + phase4: "Self-Validation" + }, + "Orchestration": { + phase2: "Context & Complexity Assessment", + phase3: "Orchestrated Execution", + phase4: "Result Aggregation" + }, + "Validation": { + phase2: "Environment Detection", + phase3: "Execution & Fix Cycle", + phase4: "Result Analysis" + } +} + +roleAnalysis.forEach(ra => { + const role = config.worker_roles.find(r => r.name === ra.role_name) + ra.phase_structure = { + phase1: "Task Discovery", + ...phaseMapping[role.responsibility_type], + phase5: "Report to Coordinator" + } +}) +``` + +### Step 4: Per-Role Infrastructure Patterns + +```javascript +roleAnalysis.forEach(ra => { + const role = config.worker_roles.find(r => r.name === ra.role_name) + + // Core patterns (mandatory for all) + ra.core_patterns = [ + "pattern-1-message-bus", + "pattern-2-yaml-front-matter", // Adapted: no YAML in skill role files + "pattern-3-task-lifecycle", + "pattern-4-five-phase", + "pattern-6-coordinator-spawn", + "pattern-7-error-handling" + ] + + // Conditional patterns + ra.conditional_patterns = [] + if (role.adaptive_routing) { + ra.conditional_patterns.push("pattern-5-complexity-adaptive") + } + if (role.responsibility_type === "Code generation" || role.responsibility_type === "Orchestration") { + ra.conditional_patterns.push("pattern-8-session-files") + } +}) +``` + +### Step 5: Collaboration Pattern Selection + +```javascript +// Team-level collaboration patterns +function selectTeamPatterns(config) { + const patterns = ['CP-1'] // Linear Pipeline is always base + + const hasValidation = config.worker_roles.some(r => + r.responsibility_type === 'Validation' || r.responsibility_type === 'Read-only analysis' + ) + if (hasValidation) patterns.push('CP-2') // Review-Fix Cycle + + const hasOrchestration = config.worker_roles.some(r => + r.responsibility_type === 'Orchestration' + ) + if (hasOrchestration) patterns.push('CP-3') // Fan-out/Fan-in + + if (config.worker_roles.length >= 4) patterns.push('CP-6') // Incremental Delivery + + patterns.push('CP-5') // Escalation Chain (always available) + patterns.push('CP-10') // Post-Mortem (always at team level) + + return [...new Set(patterns)] +} + +const collaborationPatterns = selectTeamPatterns(config) + +// Convergence defaults +const convergenceConfig = collaborationPatterns.map(cp => { + const defaults = { + 'CP-1': { max_iterations: 1, success_gate: 'all_stages_completed' }, + 'CP-2': { max_iterations: 5, success_gate: 'verdict_approve_or_conditional' }, + 'CP-3': { max_iterations: 1, success_gate: 'quorum_100_percent' }, + 'CP-5': { max_iterations: null, success_gate: 'issue_resolved_at_any_level' }, + 'CP-6': { max_iterations: 3, success_gate: 'all_increments_validated' }, + 'CP-10': { max_iterations: 1, success_gate: 'report_generated' } + } + return { pattern: cp, convergence: defaults[cp] || {} } +}) +``` + +### Step 6: Read Reference Commands + +```javascript +// Read the most referenced commands for extraction +const referencedCommands = [...new Set(roleAnalysis.map(ra => ra.similar_to.primary))] +const referenceContent = {} + +for (const cmdName of referencedCommands) { + try { + referenceContent[cmdName] = Read(`.claude/commands/team/${cmdName}.md`) + } catch { + referenceContent[cmdName] = null + } +} +``` + +### Step 7: Generate Analysis Document + +```javascript +const analysis = { + team_name: config.team_name, + role_count: config.roles.length, + worker_count: config.worker_roles.length, + role_analysis: roleAnalysis, + collaboration_patterns: collaborationPatterns, + convergence_config: convergenceConfig, + referenced_commands: referencedCommands, + pipeline: config.pipeline, + // Skill-specific patterns + skill_patterns: { + role_router: "Parse --role from $ARGUMENTS → dispatch to roles/{role}.md", + shared_infrastructure: "Message bus + task lifecycle defined once in SKILL.md", + progressive_loading: "Only read roles/{role}.md when that role executes" + } +} + +Write(`${workDir}/pattern-analysis.json`, JSON.stringify(analysis, null, 2)) +``` + +## Output + +- **File**: `pattern-analysis.json` +- **Format**: JSON +- **Location**: `{workDir}/pattern-analysis.json` + +## Quality Checklist + +- [ ] Every worker role has similarity mapping +- [ ] Every worker role has 5-phase structure +- [ ] Infrastructure patterns include all mandatory patterns +- [ ] Collaboration patterns selected at team level +- [ ] Referenced commands are readable +- [ ] Skill-specific patterns documented + +## Next Phase + +-> [Phase 3: Skill Package Generation](03-skill-generation.md) diff --git a/.claude/skills/team-skill-designer/phases/03-skill-generation.md b/.claude/skills/team-skill-designer/phases/03-skill-generation.md new file mode 100644 index 00000000..25ede7a6 --- /dev/null +++ b/.claude/skills/team-skill-designer/phases/03-skill-generation.md @@ -0,0 +1,668 @@ +# Phase 3: Skill Package Generation + +Generate the unified team skill package: SKILL.md (role router) + roles/*.md (per-role execution). + +## Objective + +- Generate SKILL.md with role router and shared infrastructure +- Generate roles/coordinator.md +- Generate roles/{worker-role}.md for each worker role +- Generate specs/team-config.json +- All files written to preview directory first + +## Input + +- Dependency: `team-config.json` (Phase 1), `pattern-analysis.json` (Phase 2) +- Templates: `templates/skill-router-template.md`, `templates/role-template.md` +- Reference: existing team commands (read in Phase 0) + +## Execution Steps + +### Step 1: Load Inputs + +```javascript +const config = JSON.parse(Read(`${workDir}/team-config.json`)) +const analysis = JSON.parse(Read(`${workDir}/pattern-analysis.json`)) +const routerTemplate = Read(`${skillDir}/templates/skill-router-template.md`) +const roleTemplate = Read(`${skillDir}/templates/role-template.md`) + +// Create preview directory +const previewDir = `${workDir}/preview` +Bash(`mkdir -p "${previewDir}/roles" "${previewDir}/specs"`) +``` + +### Step 2: Generate SKILL.md (Role Router) + +This is the unified entry point. All roles invoke this skill with `--role=xxx`. + +```javascript +const rolesTable = config.roles.map(r => + `| \`${r.name}\` | ${r.task_prefix || 'N/A'} | ${r.description} | [roles/${r.name}.md](roles/${r.name}.md) |` +).join('\n') + +const roleDispatchEntries = config.roles.map(r => + ` "${r.name}": { file: "roles/${r.name}.md", prefix: "${r.task_prefix || 'N/A'}" }` +).join(',\n') + +const messageBusTable = config.worker_roles.map(r => + `| ${r.name} | ${r.message_types.map(mt => '\`' + mt.type + '\`').join(', ')} |` +).join('\n') + +const spawnBlocks = config.worker_roles.map(r => ` +// ${r.display_name} +Task({ + subagent_type: "general-purpose", + team_name: teamName, + name: "${r.name}", + prompt: \`你是 team "\${teamName}" 的 ${r.name_upper}。 + +当你收到 ${r.task_prefix}-* 任务时,调用 Skill(skill="${config.skill_name}", args="--role=${r.name}") 执行。 + +当前需求: \${taskDescription} +约束: \${constraints} + +## 消息总线(必须) +每次 SendMessage 前,先调用 mcp__ccw-tools__team_msg 记录: +mcp__ccw-tools__team_msg({ operation: "log", team: "\${teamName}", from: "${r.name}", to: "coordinator", type: "", summary: "<摘要>" }) + +工作流程: +1. TaskList → 找到 ${r.task_prefix}-* 任务 +2. Skill(skill="${config.skill_name}", args="--role=${r.name}") 执行 +3. team_msg log + SendMessage 结果给 coordinator +4. TaskUpdate completed → 检查下一个任务\` +})`).join('\n') + +const skillMd = `--- +name: ${config.skill_name} +description: Unified team skill for ${config.team_name} team. All roles invoke this skill with --role arg. Triggers on "team ${config.team_name}". +allowed-tools: ${config.all_roles_tools_union} +--- + +# Team ${config.team_display_name} + +Unified team skill. All team members invoke this skill with \`--role=xxx\` for role-specific execution. + +## Architecture Overview + +\`\`\` +┌───────────────────────────────────────────┐ +│ Skill(skill="${config.skill_name}") │ +│ args="--role=xxx" │ +└───────────────┬───────────────────────────┘ + │ Role Router + ┌───────────┼${'───────────┬'.repeat(Math.min(config.roles.length - 1, 3))} + ${config.roles.map(r => `↓ `).join('').trim()} +${config.roles.map(r => `┌──────────┐ `).join('').trim()} +${config.roles.map(r => `│${r.name.padEnd(10)}│ `).join('').trim()} +${config.roles.map(r => `│ roles/ │ `).join('').trim()} +${config.roles.map(r => `└──────────┘ `).join('').trim()} +\`\`\` + +## Role Router + +### Input Parsing + +Parse \`$ARGUMENTS\` to extract \`--role\`: + +\`\`\`javascript +const args = "$ARGUMENTS" +const roleMatch = args.match(/--role[=\\s]+(\\w+)/) + +if (!roleMatch) { + throw new Error("Missing --role argument. Available roles: ${config.role_list}") +} + +const role = roleMatch[1] +const teamName = "${config.team_name}" +\`\`\` + +### Role Dispatch + +\`\`\`javascript +const VALID_ROLES = { +${roleDispatchEntries} +} + +if (!VALID_ROLES[role]) { + throw new Error(\\\`Unknown role: \\\${role}. Available: \\\${Object.keys(VALID_ROLES).join(', ')}\\\`) +} + +// Read and execute role-specific logic +Read(VALID_ROLES[role].file) +// → Execute the 5-phase process defined in that file +\`\`\` + +### Available Roles + +| Role | Task Prefix | Responsibility | Role File | +|------|-------------|----------------|-----------| +${rolesTable} + +## Shared Infrastructure + +### Team Configuration + +\`\`\`javascript +const TEAM_CONFIG = { + name: "${config.team_name}", + sessionDir: ".workflow/.team-plan/${config.team_name}/", + msgDir: ".workflow/.team-msg/${config.team_name}/" +} +\`\`\` + +### Message Bus (All Roles) + +Every SendMessage **before**, must call \`mcp__ccw-tools__team_msg\`: + +\`\`\`javascript +mcp__ccw-tools__team_msg({ + operation: "log", + team: "${config.team_name}", + from: role, + to: "coordinator", + type: "", + summary: "" +}) +\`\`\` + +**Message types by role**: + +| Role | Types | +|------|-------| +${messageBusTable} + +### CLI 回退 + +\`\`\`javascript +Bash(\\\`ccw team log --team "${config.team_name}" --from "\\\${role}" --to "coordinator" --type "" --summary "<摘要>" --json\\\`) +\`\`\` + +### Task Lifecycle (All Roles) + +\`\`\`javascript +// Phase 1: Discovery +const tasks = TaskList() +const myTasks = tasks.filter(t => + t.subject.startsWith(\\\`\\\${VALID_ROLES[role].prefix}-\\\`) && + t.owner === role && + t.status === 'pending' && + t.blockedBy.length === 0 +) +if (myTasks.length === 0) return +const task = TaskGet({ taskId: myTasks[0].id }) +TaskUpdate({ taskId: task.id, status: 'in_progress' }) + +// Phase 2-4: Role-specific (see roles/{role}.md) + +// Phase 5: Report + Loop +TaskUpdate({ taskId: task.id, status: 'completed' }) +\`\`\` + +## Pipeline + +\`\`\` +${config.pipeline.diagram} +\`\`\` + +## Coordinator Spawn Template + +\`\`\`javascript +TeamCreate({ team_name: "${config.team_name}" }) +${spawnBlocks} +\`\`\` + +## Error Handling + +| Scenario | Resolution | +|----------|------------| +| Unknown --role value | Error with available role list | +| Missing --role arg | Error with usage hint | +| Role file not found | Error with expected path | +` + +Write(`${previewDir}/SKILL.md`, skillMd) +``` + +### Step 3: Generate Coordinator Role File + +```javascript +const taskChainCode = config.pipeline.stages.map((stage, i) => { + const blockedByIds = stage.blockedBy.map(dep => { + const depIdx = config.pipeline.stages.findIndex(s => s.name === dep) + return `\${task${depIdx}Id}` + }) + + return `TaskCreate({ subject: "${stage.name}-001: ${stage.role} work", description: \`\${taskDescription}\`, activeForm: "${stage.name}进行中" }) +TaskUpdate({ taskId: task${i}Id, owner: "${stage.role}"${blockedByIds.length > 0 ? `, addBlockedBy: [${blockedByIds.join(', ')}]` : ''} })` +}).join('\n\n') + +const coordinationHandlers = config.worker_roles.map(r => { + const resultType = r.message_types.find(mt => !mt.type.includes('error') && !mt.type.includes('progress')) + return `| ${r.name_upper}: ${resultType?.trigger || 'work complete'} | team_msg log → TaskUpdate ${r.task_prefix} completed → check next |` +}).join('\n') + +const coordinatorMd = `# Role: coordinator + +Team coordinator. Orchestrates pipeline: requirement clarification → team creation → task chain → dispatch → monitoring → reporting. + +## Role Identity + +- **Name**: \`coordinator\` +- **Task Prefix**: N/A (creates tasks, doesn't receive them) +- **Responsibility**: Orchestration +- **Communication**: SendMessage to all teammates + +## Message Types + +| Type | Direction | Trigger | +|------|-----------|---------| +| \`plan_approved\` | coordinator → planner | Plan approved | +| \`plan_revision\` | coordinator → planner | Revision requested | +| \`task_unblocked\` | coordinator → worker | Task dependency met | +| \`shutdown\` | coordinator → all | Team shutdown | +| \`error\` | coordinator → user | Coordination error | + +## Execution + +### Phase 1: Requirement Clarification + +Parse \`$ARGUMENTS\` for task description. Use AskUserQuestion for: +- MVP scope (minimal / full / comprehensive) +- Key constraints (backward compatible / follow patterns / test coverage) + +Simple tasks can skip clarification. + +### Phase 2: Create Team + Spawn Teammates + +\`\`\`javascript +TeamCreate({ team_name: "${config.team_name}" }) + +${spawnBlocks} +\`\`\` + +### Phase 3: Create Task Chain + +\`\`\`javascript +${taskChainCode} +\`\`\` + +### Phase 4: Coordination Loop + +Receive teammate messages, dispatch based on content. +**Before each decision**: \`team_msg list\` to check recent messages. +**After each decision**: \`team_msg log\` to record. + +| Received Message | Action | +|-----------------|--------| +${coordinationHandlers} +| Worker: error | Assess severity → retry or escalate to user | +| All tasks completed | → Phase 5 | + +### Phase 5: Report + Persist + +Summarize changes, test results, review findings. + +\`\`\`javascript +AskUserQuestion({ + questions: [{ + question: "当前需求已完成。下一步:", + header: "Next", + multiSelect: false, + options: [ + { label: "新需求", description: "提交新需求给当前团队" }, + { label: "关闭团队", description: "关闭所有 teammate 并清理" } + ] + }] +}) +// 新需求 → 回到 Phase 1 +// 关闭 → shutdown → TeamDelete() +\`\`\` + +## Error Handling + +| Scenario | Resolution | +|----------|------------| +| Teammate unresponsive | Send follow-up, 2x → respawn | +| Plan rejected 3+ times | Coordinator self-plans | +| Test stuck >5 iterations | Escalate to user | +| Review finds critical | Create fix task for executor | +` + +Write(`${previewDir}/roles/coordinator.md`, coordinatorMd) +``` + +### Step 4: Generate Worker Role Files + +For each worker role, generate a complete role file with 5-phase execution. + +```javascript +for (const role of config.worker_roles) { + const ra = analysis.role_analysis.find(r => r.role_name === role.name) + + // Phase 2 content based on responsibility type + const phase2Content = { + "Read-only analysis": `\`\`\`javascript +// Load plan for criteria reference +const planPathMatch = task.description.match(/\\.workflow\\/\\.team-plan\\/[^\\s]+\\/plan\\.json/) +let plan = null +if (planPathMatch) { + try { plan = JSON.parse(Read(planPathMatch[0])) } catch {} +} + +// Get changed files +const changedFiles = Bash(\`git diff --name-only HEAD~1 2>/dev/null || git diff --name-only --cached\`) + .split('\\n').filter(Boolean) + +// Read file contents for analysis +const fileContents = {} +for (const file of changedFiles.slice(0, 20)) { + try { fileContents[file] = Read(file) } catch {} +} +\`\`\``, + + "Code generation": `\`\`\`javascript +// Extract plan path from task description +const planPathMatch = task.description.match(/\\.workflow\\/\\.team-plan\\/[^\\s]+\\/plan\\.json/) +if (!planPathMatch) { + mcp__ccw-tools__team_msg({ operation: "log", team: "${config.team_name}", from: "${role.name}", to: "coordinator", type: "error", summary: "plan.json路径无效" }) + SendMessage({ type: "message", recipient: "coordinator", content: \`Cannot find plan.json in \${task.subject}\`, summary: "Plan not found" }) + return +} + +const plan = JSON.parse(Read(planPathMatch[0])) +const planTasks = plan.task_ids.map(id => + JSON.parse(Read(\`\${planPathMatch[0].replace('plan.json', '')}.task/\${id}.json\`)) +) +\`\`\``, + + "Orchestration": `\`\`\`javascript +function assessComplexity(desc) { + let score = 0 + if (/refactor|architect|restructure|module|system/.test(desc)) score += 2 + if (/multiple|across|cross/.test(desc)) score += 2 + if (/integrate|api|database/.test(desc)) score += 1 + if (/security|performance/.test(desc)) score += 1 + return score >= 4 ? 'High' : score >= 2 ? 'Medium' : 'Low' +} + +const complexity = assessComplexity(task.description) +\`\`\``, + + "Validation": `\`\`\`javascript +// Detect changed files for validation scope +const changedFiles = Bash(\`git diff --name-only HEAD~1 2>/dev/null || git diff --name-only --cached\`) + .split('\\n').filter(Boolean) +\`\`\`` + } + + // Phase 3 content based on responsibility type + const phase3Content = { + "Read-only analysis": `\`\`\`javascript +// Core analysis logic +// Reference: .claude/commands/team/${ra.similar_to.primary}.md Phase 3 + +// Analyze each file +for (const [file, content] of Object.entries(fileContents)) { + // Domain-specific analysis +} +\`\`\``, + + "Code generation": `\`\`\`javascript +// Reference: .claude/commands/team/${ra.similar_to.primary}.md Phase 3 + +${role.adaptive_routing ? `// Complexity-adaptive execution +if (planTasks.length <= 2) { + // Direct file editing + for (const pt of planTasks) { + for (const f of (pt.files || [])) { + const content = Read(f.path) + Edit({ file_path: f.path, old_string: "...", new_string: "..." }) + } + } +} else { + // Delegate to code-developer sub-agent + Task({ + subagent_type: "code-developer", + run_in_background: false, + description: \`Implement \${planTasks.length} tasks\`, + prompt: \`## Goal +\${plan.summary} + +## Tasks +\${planTasks.map(t => \`### \${t.title}\\n\${t.description}\`).join('\\n\\n')} + +Complete each task according to its convergence criteria.\` + }) +}` : `// Direct execution +for (const pt of planTasks) { + for (const f of (pt.files || [])) { + const content = Read(f.path) + Edit({ file_path: f.path, old_string: "...", new_string: "..." }) + } +}`} +\`\`\``, + + "Orchestration": `\`\`\`javascript +// Reference: .claude/commands/team/${ra.similar_to.primary}.md Phase 3 + +${role.adaptive_routing ? `if (complexity === 'Low') { + // Direct execution with mcp__ace-tool__search_context + Grep/Glob +} else { + // Launch sub-agents for complex work + Task({ + subagent_type: "universal-executor", + run_in_background: false, + description: "${role.name} orchestration", + prompt: \`Execute ${role.name} work for: \${task.description}\` + }) +}` : `// Direct orchestration`} +\`\`\``, + + "Validation": `\`\`\`javascript +// Reference: .claude/commands/team/${ra.similar_to.primary}.md Phase 3 + +let iteration = 0 +const MAX_ITERATIONS = 5 +while (iteration < MAX_ITERATIONS) { + // Run validation + const result = Bash(\`npm test 2>&1 || true\`) + const passed = !result.includes('FAIL') + + if (passed) break + + // Attempt fix + iteration++ + if (iteration < MAX_ITERATIONS) { + // Auto-fix or delegate + } +} +\`\`\`` + } + + // Phase 4 content + const phase4Content = { + "Read-only analysis": `\`\`\`javascript +// Classify findings by severity +const findings = { critical: [], high: [], medium: [], low: [] } +// ... populate findings from Phase 3 analysis +\`\`\``, + + "Code generation": `\`\`\`javascript +// Self-validation +const syntaxResult = Bash(\`tsc --noEmit 2>&1 || true\`) +const hasSyntaxErrors = syntaxResult.includes('error TS') +if (hasSyntaxErrors) { + // Attempt auto-fix +} +\`\`\``, + + "Orchestration": `\`\`\`javascript +// Aggregate results from sub-agents +const aggregated = { + // Merge findings, results, outputs +} +\`\`\``, + + "Validation": `\`\`\`javascript +// Analyze results +const resultSummary = { + iterations: iteration, + passed: iteration < MAX_ITERATIONS, + // Coverage, pass rate, etc. +} +\`\`\`` + } + + const msgTypesTable = role.message_types.map(mt => + `| \`${mt.type}\` | ${role.name} → coordinator | ${mt.trigger} |` + ).join('\n') + + const primaryMsgType = role.message_types.find(mt => !mt.type.includes('error') && !mt.type.includes('progress'))?.type || `${role.name}_complete` + + const roleMd = `# Role: ${role.name} + +${role.description} + +## Role Identity + +- **Name**: \`${role.name}\` +- **Task Prefix**: \`${role.task_prefix}-*\` +- **Responsibility**: ${role.responsibility_type} +- **Communication**: SendMessage to coordinator only + +## Message Types + +| Type | Direction | Trigger | +|------|-----------|---------| +${msgTypesTable} + +## Execution (5-Phase) + +### Phase 1: Task Discovery + +\`\`\`javascript +const tasks = TaskList() +const myTasks = tasks.filter(t => + t.subject.startsWith('${role.task_prefix}-') && + t.owner === '${role.name}' && + t.status === 'pending' && + t.blockedBy.length === 0 +) + +if (myTasks.length === 0) return // idle + +const task = TaskGet({ taskId: myTasks[0].id }) +TaskUpdate({ taskId: task.id, status: 'in_progress' }) +\`\`\` + +### Phase 2: ${ra.phase_structure.phase2} + +${phase2Content[role.responsibility_type]} + +### Phase 3: ${ra.phase_structure.phase3} + +${phase3Content[role.responsibility_type]} + +### Phase 4: ${ra.phase_structure.phase4} + +${phase4Content[role.responsibility_type]} + +### Phase 5: Report to Coordinator + +\`\`\`javascript +mcp__ccw-tools__team_msg({ + operation: "log", + team: "${config.team_name}", + from: "${role.name}", + to: "coordinator", + type: "${primaryMsgType}", + summary: \`${role.task_prefix} complete: \${task.subject}\` +}) + +SendMessage({ + type: "message", + recipient: "coordinator", + content: \`## ${role.display_name} Results + +**Task**: \${task.subject} +**Status**: \${resultStatus} + +### Summary +\${resultSummary} + +### Details +\${resultDetails}\`, + summary: \`${role.task_prefix} complete\` +}) + +TaskUpdate({ taskId: task.id, status: 'completed' }) + +// Check for next task +const nextTasks = TaskList().filter(t => + t.subject.startsWith('${role.task_prefix}-') && + t.owner === '${role.name}' && + t.status === 'pending' && + t.blockedBy.length === 0 +) + +if (nextTasks.length > 0) { + // Continue with next task → back to Phase 1 +} +\`\`\` + +## Error Handling + +| Scenario | Resolution | +|----------|------------| +| No ${role.task_prefix}-* tasks available | Idle, wait for coordinator | +| Context/Plan file not found | Notify coordinator | +${role.adaptive_routing ? '| Sub-agent failure | Retry once, fallback to direct |\n' : ''}| Critical issue beyond scope | SendMessage fix_required | +| Unexpected error | Log via team_msg, report | +` + + Write(`${previewDir}/roles/${role.name}.md`, roleMd) +} +``` + +### Step 5: Generate specs/team-config.json + +```javascript +Write(`${previewDir}/specs/team-config.json`, JSON.stringify({ + team_name: config.team_name, + skill_name: config.skill_name, + pipeline_type: config.pipeline_type, + pipeline: config.pipeline, + roles: config.roles.map(r => ({ + name: r.name, + task_prefix: r.task_prefix, + responsibility_type: r.responsibility_type, + description: r.description + })), + collaboration_patterns: analysis.collaboration_patterns, + generated_at: new Date().toISOString() +}, null, 2)) +``` + +## Output + +- **Directory**: `{workDir}/preview/` +- **Files**: + - `preview/SKILL.md` - Role router + shared infrastructure + - `preview/roles/coordinator.md` - Coordinator execution + - `preview/roles/{role}.md` - Per-worker role execution + - `preview/specs/team-config.json` - Team configuration + +## Quality Checklist + +- [ ] SKILL.md contains role router with all roles +- [ ] SKILL.md contains shared infrastructure (message bus, task lifecycle) +- [ ] SKILL.md contains coordinator spawn template +- [ ] Every role has a file in roles/ +- [ ] Every role file has 5-phase execution +- [ ] Every role file has message types table +- [ ] Every role file has error handling +- [ ] team-config.json is valid JSON + +## Next Phase + +-> [Phase 4: Integration Verification](04-integration-verification.md) diff --git a/.claude/skills/team-skill-designer/phases/04-integration-verification.md b/.claude/skills/team-skill-designer/phases/04-integration-verification.md new file mode 100644 index 00000000..a96bc7c7 --- /dev/null +++ b/.claude/skills/team-skill-designer/phases/04-integration-verification.md @@ -0,0 +1,178 @@ +# Phase 4: Integration Verification + +Verify the generated skill package is internally consistent. + +## Objective + +- Verify SKILL.md role router references match actual role files +- Verify task prefixes are unique across all roles +- Verify message types are consistent +- Verify coordinator spawn template uses correct skill invocation +- Generate integration-report.json + +## Input + +- Dependency: `{workDir}/preview/` directory (Phase 3) +- Reference: `team-config.json` (Phase 1) + +## Execution Steps + +### Step 1: Load Generated Files + +```javascript +const config = JSON.parse(Read(`${workDir}/team-config.json`)) +const previewDir = `${workDir}/preview` +const skillMd = Read(`${previewDir}/SKILL.md`) + +const roleFiles = {} +for (const role of config.roles) { + try { + roleFiles[role.name] = Read(`${previewDir}/roles/${role.name}.md`) + } catch { + roleFiles[role.name] = null + } +} +``` + +### Step 2: Role Router Consistency + +```javascript +const routerChecks = config.roles.map(role => { + const hasRouterEntry = skillMd.includes(`"${role.name}"`) + const hasRoleFile = roleFiles[role.name] !== null + const hasRoleLink = skillMd.includes(`roles/${role.name}.md`) + + return { + role: role.name, + router_entry: hasRouterEntry, + file_exists: hasRoleFile, + link_valid: hasRoleLink, + status: (hasRouterEntry && hasRoleFile && hasRoleLink) ? 'PASS' : 'FAIL' + } +}) +``` + +### Step 3: Task Prefix Uniqueness + +```javascript +const prefixes = config.worker_roles.map(r => r.task_prefix) +const uniquePrefixes = [...new Set(prefixes)] +const prefixCheck = { + prefixes: prefixes, + unique: uniquePrefixes, + duplicates: prefixes.filter((p, i) => prefixes.indexOf(p) !== i), + status: prefixes.length === uniquePrefixes.length ? 'PASS' : 'FAIL' +} +``` + +### Step 4: Message Type Consistency + +```javascript +const msgChecks = config.worker_roles.map(role => { + const roleFile = roleFiles[role.name] || '' + const typesInConfig = role.message_types.map(mt => mt.type) + const typesInFile = typesInConfig.filter(t => roleFile.includes(t)) + + return { + role: role.name, + configured: typesInConfig, + present_in_file: typesInFile, + missing: typesInConfig.filter(t => !typesInFile.includes(t)), + status: typesInFile.length === typesInConfig.length ? 'PASS' : 'WARN' + } +}) +``` + +### Step 5: Spawn Template Verification + +```javascript +const spawnChecks = config.worker_roles.map(role => { + const hasSpawn = skillMd.includes(`name: "${role.name}"`) + const hasSkillCall = skillMd.includes(`Skill(skill="${config.skill_name}", args="--role=${role.name}")`) + const hasTaskPrefix = skillMd.includes(`${role.task_prefix}-*`) + + return { + role: role.name, + spawn_present: hasSpawn, + skill_call_correct: hasSkillCall, + prefix_in_prompt: hasTaskPrefix, + status: (hasSpawn && hasSkillCall && hasTaskPrefix) ? 'PASS' : 'FAIL' + } +}) +``` + +### Step 6: Role File Pattern Compliance + +```javascript +const patternChecks = Object.entries(roleFiles).map(([name, content]) => { + if (!content) return { role: name, status: 'MISSING' } + + const checks = { + has_role_identity: /## Role Identity/.test(content), + has_5_phases: /Phase 1/.test(content) && /Phase 5/.test(content), + has_task_lifecycle: /TaskList/.test(content) && /TaskGet/.test(content) && /TaskUpdate/.test(content), + has_message_bus: /team_msg/.test(content), + has_send_message: /SendMessage/.test(content), + has_error_handling: /## Error Handling/.test(content) + } + + const passCount = Object.values(checks).filter(Boolean).length + return { + role: name, + checks: checks, + pass_count: passCount, + total: Object.keys(checks).length, + status: passCount === Object.keys(checks).length ? 'PASS' : 'PARTIAL' + } +}) +``` + +### Step 7: Generate Report + +```javascript +const overallStatus = [ + ...routerChecks.map(c => c.status), + prefixCheck.status, + ...spawnChecks.map(c => c.status), + ...patternChecks.map(c => c.status) +].every(s => s === 'PASS') ? 'PASS' : 'NEEDS_ATTENTION' + +const report = { + team_name: config.team_name, + skill_name: config.skill_name, + checks: { + router_consistency: routerChecks, + prefix_uniqueness: prefixCheck, + message_types: msgChecks, + spawn_template: spawnChecks, + pattern_compliance: patternChecks + }, + overall: overallStatus, + file_count: { + skill_md: 1, + role_files: Object.keys(roleFiles).length, + total: 1 + Object.keys(roleFiles).length + 1 // SKILL.md + roles + config + } +} + +Write(`${workDir}/integration-report.json`, JSON.stringify(report, null, 2)) +``` + +## Output + +- **File**: `integration-report.json` +- **Format**: JSON +- **Location**: `{workDir}/integration-report.json` + +## Quality Checklist + +- [ ] Every role in config has a router entry in SKILL.md +- [ ] Every role has a file in roles/ +- [ ] Task prefixes are unique +- [ ] Spawn template uses correct `Skill(skill="...", args="--role=...")` +- [ ] All role files have 5-phase structure +- [ ] All role files have message bus integration + +## Next Phase + +-> [Phase 5: Validation](05-validation.md) diff --git a/.claude/skills/team-skill-designer/phases/05-validation.md b/.claude/skills/team-skill-designer/phases/05-validation.md new file mode 100644 index 00000000..efcef35e --- /dev/null +++ b/.claude/skills/team-skill-designer/phases/05-validation.md @@ -0,0 +1,203 @@ +# Phase 5: Validation + +Verify quality and deliver the final skill package. + +## Objective + +- Per-role structural completeness check +- Per-role pattern compliance check +- Quality scoring +- Deliver final skill package to `.claude/skills/team-{name}/` + +## Input + +- Dependency: `{workDir}/preview/` (Phase 3), `integration-report.json` (Phase 4) +- Specification: `../team-command-designer/specs/quality-standards.md` + +## Execution Steps + +### Step 1: Load Files + +```javascript +const config = JSON.parse(Read(`${workDir}/team-config.json`)) +const integration = JSON.parse(Read(`${workDir}/integration-report.json`)) +const previewDir = `${workDir}/preview` +const skillMd = Read(`${previewDir}/SKILL.md`) + +const roleContents = {} +for (const role of config.roles) { + try { + roleContents[role.name] = Read(`${previewDir}/roles/${role.name}.md`) + } catch { + roleContents[role.name] = null + } +} +``` + +### Step 2: SKILL.md Structural Check + +```javascript +const skillChecks = [ + { name: "Frontmatter", pattern: /^---\n[\s\S]+?\n---/ }, + { name: "Architecture Overview", pattern: /## Architecture Overview/ }, + { name: "Role Router", pattern: /## Role Router/ }, + { name: "Role Dispatch Code", pattern: /VALID_ROLES/ }, + { name: "Available Roles Table", pattern: /\| Role \| Task Prefix/ }, + { name: "Shared Infrastructure", pattern: /## Shared Infrastructure/ }, + { name: "Message Bus Section", pattern: /Message Bus/ }, + { name: "team_msg Example", pattern: /team_msg/ }, + { name: "CLI Fallback", pattern: /ccw team log/ }, + { name: "Task Lifecycle", pattern: /Task Lifecycle/ }, + { name: "Pipeline Diagram", pattern: /## Pipeline/ }, + { name: "Coordinator Spawn Template", pattern: /Coordinator Spawn/ }, + { name: "Error Handling", pattern: /## Error Handling/ } +] + +const skillResults = skillChecks.map(c => ({ + check: c.name, + status: c.pattern.test(skillMd) ? 'PASS' : 'FAIL' +})) + +const skillScore = skillResults.filter(r => r.status === 'PASS').length / skillResults.length * 100 +``` + +### Step 3: Per-Role Structural Check + +```javascript +const roleChecks = [ + { name: "Role Identity", pattern: /## Role Identity/ }, + { name: "Message Types Table", pattern: /## Message Types/ }, + { name: "5-Phase Execution", pattern: /## Execution/ }, + { name: "Phase 1 Task Discovery", pattern: /Phase 1.*Task Discovery/i }, + { name: "TaskList Usage", pattern: /TaskList/ }, + { name: "TaskGet Usage", pattern: /TaskGet/ }, + { name: "TaskUpdate Usage", pattern: /TaskUpdate/ }, + { name: "team_msg Before SendMessage", pattern: /team_msg/ }, + { name: "SendMessage to Coordinator", pattern: /SendMessage/ }, + { name: "Error Handling", pattern: /## Error Handling/ } +] + +const roleResults = {} +for (const [name, content] of Object.entries(roleContents)) { + if (!content) { + roleResults[name] = { status: 'MISSING', checks: [], score: 0 } + continue + } + + const checks = roleChecks.map(c => ({ + check: c.name, + status: c.pattern.test(content) ? 'PASS' : 'FAIL' + })) + + const score = checks.filter(c => c.status === 'PASS').length / checks.length * 100 + roleResults[name] = { status: score >= 80 ? 'PASS' : 'PARTIAL', checks, score } +} +``` + +### Step 4: Quality Scoring + +```javascript +const scores = { + skill_md: skillScore, + roles_avg: Object.values(roleResults).reduce((sum, r) => sum + r.score, 0) / Object.keys(roleResults).length, + integration: integration.overall === 'PASS' ? 100 : 50, + consistency: checkConsistency() +} + +function checkConsistency() { + let score = 100 + // Check skill name in SKILL.md matches config + if (!skillMd.includes(config.skill_name)) score -= 20 + // Check team name consistency + if (!skillMd.includes(config.team_name)) score -= 20 + // Check all roles referenced in SKILL.md + for (const role of config.roles) { + if (!skillMd.includes(role.name)) score -= 10 + } + return Math.max(0, score) +} + +const overallScore = Object.values(scores).reduce((a, b) => a + b, 0) / Object.keys(scores).length +const qualityGate = overallScore >= 80 ? 'PASS' : overallScore >= 60 ? 'REVIEW' : 'FAIL' +``` + +### Step 5: Generate Validation Report + +```javascript +const report = { + team_name: config.team_name, + skill_name: config.skill_name, + timestamp: new Date().toISOString(), + scores: scores, + overall_score: overallScore, + quality_gate: qualityGate, + skill_md_checks: skillResults, + role_results: roleResults, + integration_status: integration.overall, + delivery: { + source: previewDir, + destination: `.claude/skills/${config.skill_name}/`, + ready: qualityGate !== 'FAIL' + } +} + +Write(`${workDir}/validation-report.json`, JSON.stringify(report, null, 2)) +``` + +### Step 6: Deliver Final Package + +```javascript +if (report.delivery.ready) { + const destDir = `.claude/skills/${config.skill_name}` + + // Create directory structure + Bash(`mkdir -p "${destDir}/roles" "${destDir}/specs"`) + + // Copy all files + Write(`${destDir}/SKILL.md`, skillMd) + + for (const [name, content] of Object.entries(roleContents)) { + if (content) { + Write(`${destDir}/roles/${name}.md`, content) + } + } + + // Copy team config + const teamConfig = Read(`${previewDir}/specs/team-config.json`) + Write(`${destDir}/specs/team-config.json`, teamConfig) + + // Report + console.log(`\nTeam skill delivered to: ${destDir}/`) + console.log(`Skill name: ${config.skill_name}`) + console.log(`Quality score: ${overallScore.toFixed(1)}% (${qualityGate})`) + console.log(`Roles: ${config.role_list}`) + console.log(`\nUsage:`) + console.log(` Skill(skill="${config.skill_name}", args="--role=planner")`) + console.log(` Skill(skill="${config.skill_name}", args="--role=executor")`) + console.log(`\nFile structure:`) + Bash(`find "${destDir}" -type f | sort`) +} else { + console.log(`Validation FAILED (score: ${overallScore.toFixed(1)}%)`) + console.log('Fix issues and re-run Phase 3-5') +} +``` + +## Output + +- **File**: `validation-report.json` +- **Format**: JSON +- **Location**: `{workDir}/validation-report.json` +- **Delivery**: `.claude/skills/team-{name}/` (if validation passes) + +## Quality Checklist + +- [ ] SKILL.md passes all 13 structural checks +- [ ] All role files pass structural checks (>= 80%) +- [ ] Integration report is PASS +- [ ] Overall score >= 80% +- [ ] Final package delivered to `.claude/skills/team-{name}/` +- [ ] Usage instructions provided + +## Completion + +This is the final phase. The unified team skill is ready for use. diff --git a/.claude/skills/team-skill-designer/templates/role-template.md b/.claude/skills/team-skill-designer/templates/role-template.md new file mode 100644 index 00000000..0ada69b3 --- /dev/null +++ b/.claude/skills/team-skill-designer/templates/role-template.md @@ -0,0 +1,333 @@ +# Role File Template + +Template for generating per-role execution detail files in `roles/{role-name}.md`. + +## Purpose + +| Phase | Usage | +|-------|-------| +| Phase 0 | Read to understand role file structure | +| Phase 3 | Apply with role-specific content | + +--- + +## Template + +```markdown +# Role: {{role_name}} + +{{role_description}} + +## Role Identity + +- **Name**: `{{role_name}}` +- **Task Prefix**: `{{task_prefix}}-*` +- **Responsibility**: {{responsibility_type}} +- **Communication**: SendMessage to coordinator only + +## Message Types + +| Type | Direction | Trigger | Description | +|------|-----------|---------|-------------| +{{#each message_types}} +| `{{this.type}}` | {{../role_name}} → coordinator | {{this.trigger}} | {{this.description}} | +{{/each}} + +## Execution (5-Phase) + +### Phase 1: Task Discovery + +\`\`\`javascript +const tasks = TaskList() +const myTasks = tasks.filter(t => + t.subject.startsWith('{{task_prefix}}-') && + t.owner === '{{role_name}}' && + t.status === 'pending' && + t.blockedBy.length === 0 +) + +if (myTasks.length === 0) return // idle + +const task = TaskGet({ taskId: myTasks[0].id }) +TaskUpdate({ taskId: task.id, status: 'in_progress' }) +\`\`\` + +### Phase 2: {{phase2_name}} + +{{phase2_content}} + +### Phase 3: {{phase3_name}} + +{{phase3_content}} + +### Phase 4: {{phase4_name}} + +{{phase4_content}} + +### Phase 5: Report to Coordinator + +\`\`\`javascript +// Log message before SendMessage +mcp__ccw-tools__team_msg({ + operation: "log", + team: teamName, + from: "{{role_name}}", + to: "coordinator", + type: "{{primary_message_type}}", + summary: \`{{task_prefix}} complete: \${task.subject}\` +}) + +SendMessage({ + type: "message", + recipient: "coordinator", + content: \`## {{display_name}} Results + +**Task**: \${task.subject} +**Status**: \${resultStatus} + +### Summary +\${resultSummary} + +### Details +\${resultDetails}\`, + summary: \`{{task_prefix}} complete\` +}) + +// Mark task completed +TaskUpdate({ taskId: task.id, status: 'completed' }) + +// Check for next task +const nextTasks = TaskList().filter(t => + t.subject.startsWith('{{task_prefix}}-') && + t.owner === '{{role_name}}' && + t.status === 'pending' && + t.blockedBy.length === 0 +) + +if (nextTasks.length > 0) { + // Continue with next task → back to Phase 1 +} +\`\`\` + +## Error Handling + +| Scenario | Resolution | +|----------|------------| +| No {{task_prefix}}-* tasks available | Idle, wait for coordinator assignment | +| Context/Plan file not found | Notify coordinator, request location | +{{#if adaptive_routing}} +| Sub-agent failure | Retry once, then fallback to direct execution | +{{/if}} +| Critical issue beyond scope | SendMessage fix_required to coordinator | +| Unexpected error | Log error via team_msg, report to coordinator | +``` + +--- + +## Template Sections by Responsibility Type + +### Read-only analysis + +**Phase 2: Context Loading** +```javascript +// Load plan for criteria reference +const planPathMatch = task.description.match(/\.workflow\/\.team-plan\/[^\s]+\/plan\.json/) +let plan = null +if (planPathMatch) { + try { plan = JSON.parse(Read(planPathMatch[0])) } catch {} +} + +// Get changed files +const changedFiles = Bash(`git diff --name-only HEAD~1 2>/dev/null || git diff --name-only --cached`) + .split('\n').filter(Boolean) + +// Read file contents for analysis +const fileContents = {} +for (const file of changedFiles.slice(0, 20)) { + try { fileContents[file] = Read(file) } catch {} +} +``` + +**Phase 3: Analysis Execution** +```javascript +// Core analysis logic +// Customize per specific analysis domain +``` + +**Phase 4: Finding Summary** +```javascript +// Classify findings by severity +const findings = { + critical: [], + high: [], + medium: [], + low: [] +} +``` + +### Code generation + +**Phase 2: Task & Plan Loading** +```javascript +const planPathMatch = task.description.match(/\.workflow\/\.team-plan\/[^\s]+\/plan\.json/) +if (!planPathMatch) { + SendMessage({ type: "message", recipient: "coordinator", + content: `Cannot find plan.json in ${task.subject}`, summary: "Plan not found" }) + return +} +const plan = JSON.parse(Read(planPathMatch[0])) +const planTasks = plan.task_ids.map(id => + JSON.parse(Read(`${planPathMatch[0].replace('plan.json', '')}.task/${id}.json`)) +) +``` + +**Phase 3: Code Implementation** +```javascript +// Complexity-adaptive execution +if (complexity === 'Low') { + // Direct file editing +} else { + Task({ + subagent_type: "code-developer", + run_in_background: false, + description: "Implement plan tasks", + prompt: `...` + }) +} +``` + +**Phase 4: Self-Validation** +```javascript +const syntaxResult = Bash(`tsc --noEmit 2>&1 || true`) +const hasSyntaxErrors = syntaxResult.includes('error TS') +``` + +### Orchestration + +**Phase 2: Context & Complexity Assessment** +```javascript +function assessComplexity(desc) { + let score = 0 + if (/refactor|architect|restructure|module|system/.test(desc)) score += 2 + if (/multiple|across|cross/.test(desc)) score += 2 + if (/integrate|api|database/.test(desc)) score += 1 + if (/security|performance/.test(desc)) score += 1 + return score >= 4 ? 'High' : score >= 2 ? 'Medium' : 'Low' +} +const complexity = assessComplexity(task.description) +``` + +**Phase 3: Orchestrated Execution** +```javascript +// Launch parallel sub-agents or sequential stages +``` + +**Phase 4: Result Aggregation** +```javascript +// Merge and summarize sub-agent results +``` + +### Validation + +**Phase 2: Environment Detection** +```javascript +const changedFiles = Bash(`git diff --name-only HEAD~1 2>/dev/null || git diff --name-only --cached`) + .split('\n').filter(Boolean) +``` + +**Phase 3: Execution & Fix Cycle** +```javascript +// Run validation, collect failures, attempt fixes, re-validate +let iteration = 0 +const MAX_ITERATIONS = 5 +while (iteration < MAX_ITERATIONS) { + const result = runValidation() + if (result.passRate >= 0.95) break + applyFixes(result.failures) + iteration++ +} +``` + +**Phase 4: Result Analysis** +```javascript +// Analyze pass/fail patterns, coverage gaps +``` + +--- + +## Coordinator Role Template + +The coordinator role is special and always generated. Its template differs from worker roles: + +```markdown +# Role: coordinator + +Team coordinator. Orchestrates the pipeline: requirement clarification → task chain creation → dispatch → monitoring → reporting. + +## Role Identity + +- **Name**: `coordinator` +- **Task Prefix**: N/A (coordinator creates tasks, doesn't receive them) +- **Responsibility**: Orchestration +- **Communication**: SendMessage to all teammates + +## Execution + +### Phase 1: Requirement Clarification + +Parse $ARGUMENTS, use AskUserQuestion for MVP scope and constraints. + +### Phase 2: Create Team + Spawn Teammates + +\`\`\`javascript +TeamCreate({ team_name: teamName }) + +// Spawn each worker role +{{#each worker_roles}} +Task({ + subagent_type: "general-purpose", + team_name: teamName, + name: "{{this.name}}", + prompt: \`...Skill(skill="team-{{team_name}}", args="--role={{this.name}}")...\` +}) +{{/each}} +\`\`\` + +### Phase 3: Create Task Chain + +\`\`\`javascript +{{task_chain_creation_code}} +\`\`\` + +### Phase 4: Coordination Loop + +| Received Message | Action | +|-----------------|--------| +{{#each coordination_handlers}} +| {{this.trigger}} | {{this.action}} | +{{/each}} + +### Phase 5: Report + Persist + +Summarize results. AskUserQuestion for next requirement or shutdown. +``` + +--- + +## Variable Reference + +| Variable | Source | Description | +|----------|--------|-------------| +| `{{role_name}}` | config.role_name | Role identifier | +| `{{task_prefix}}` | config.task_prefix | UPPERCASE task prefix | +| `{{responsibility_type}}` | config.responsibility_type | Role type | +| `{{display_name}}` | config.display_name | Human-readable | +| `{{phase2_name}}` | patterns.phase_structure.phase2 | Phase 2 label | +| `{{phase3_name}}` | patterns.phase_structure.phase3 | Phase 3 label | +| `{{phase4_name}}` | patterns.phase_structure.phase4 | Phase 4 label | +| `{{phase2_content}}` | Generated from responsibility template | Phase 2 code | +| `{{phase3_content}}` | Generated from responsibility template | Phase 3 code | +| `{{phase4_content}}` | Generated from responsibility template | Phase 4 code | +| `{{message_types}}` | config.message_types | Array of message types | +| `{{primary_message_type}}` | config.message_types[0].type | Primary type | +| `{{adaptive_routing}}` | config.adaptive_routing | Boolean | diff --git a/.claude/skills/team-skill-designer/templates/skill-router-template.md b/.claude/skills/team-skill-designer/templates/skill-router-template.md new file mode 100644 index 00000000..5518a6cc --- /dev/null +++ b/.claude/skills/team-skill-designer/templates/skill-router-template.md @@ -0,0 +1,224 @@ +# Skill Router Template + +Template for the generated SKILL.md with role-based routing. + +## Purpose + +| Phase | Usage | +|-------|-------| +| Phase 0 | Read to understand generated SKILL.md structure | +| Phase 3 | Apply with team-specific content | + +--- + +## Template + +```markdown +--- +name: team-{{team_name}} +description: Unified team skill for {{team_name}} team. All roles invoke this skill with --role arg for role-specific execution. Triggers on "team {{team_name}}". +allowed-tools: {{all_roles_tools_union}} +--- + +# Team {{team_display_name}} + +Unified team skill. All team members invoke this skill with `--role=xxx` to route to role-specific execution. + +## Architecture Overview + +\`\`\` +┌───────────────────────────────────────────┐ +│ Skill(skill="team-{{team_name}}") │ +│ args="--role=xxx" │ +└───────────────┬───────────────────────────┘ + │ Role Router + ┌───────────┼───────────┬───────────┐ + ↓ ↓ ↓ ↓ +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│coordinator│ │{{role_1}}│ │{{role_2}}│ │{{role_3}}│ +│ roles/ │ │ roles/ │ │ roles/ │ │ roles/ │ +└──────────┘ └──────────┘ └──────────┘ └──────────┘ +\`\`\` + +## Role Router + +### Input Parsing + +Parse `$ARGUMENTS` to extract `--role`: + +\`\`\`javascript +const args = "$ARGUMENTS" +const roleMatch = args.match(/--role[=\s]+(\w+)/) + +if (!roleMatch) { + // ERROR: --role is required + // This skill must be invoked with: Skill(skill="team-{{team_name}}", args="--role=xxx") + throw new Error("Missing --role argument. Available roles: {{role_list}}") +} + +const role = roleMatch[1] +const teamName = "{{team_name}}" +\`\`\` + +### Role Dispatch + +\`\`\`javascript +const VALID_ROLES = { +{{#each roles}} + "{{this.name}}": { file: "roles/{{this.name}}.md", prefix: "{{this.task_prefix}}" }, +{{/each}} +} + +if (!VALID_ROLES[role]) { + throw new Error(\`Unknown role: \${role}. Available: \${Object.keys(VALID_ROLES).join(', ')}\`) +} + +// Read and execute role-specific logic +Read(VALID_ROLES[role].file) +// → Execute the 5-phase process defined in that file +\`\`\` + +### Available Roles + +| Role | Task Prefix | Responsibility | Role File | +|------|-------------|----------------|-----------| +{{#each roles}} +| `{{this.name}}` | {{this.task_prefix}}-* | {{this.responsibility}} | [roles/{{this.name}}.md](roles/{{this.name}}.md) | +{{/each}} + +## Shared Infrastructure + +### Team Configuration + +\`\`\`javascript +const TEAM_CONFIG = { + name: "{{team_name}}", + sessionDir: ".workflow/.team-plan/{{team_name}}/", + msgDir: ".workflow/.team-msg/{{team_name}}/", + roles: {{roles_json}} +} +\`\`\` + +### Message Bus (All Roles) + +Every SendMessage **before**, must call `mcp__ccw-tools__team_msg` to log: + +\`\`\`javascript +mcp__ccw-tools__team_msg({ + operation: "log", + team: "{{team_name}}", + from: role, // current role name + to: "coordinator", + type: "", + summary: "", + ref: "" // optional +}) +\`\`\` + +**Message types by role**: + +| Role | Types | +|------|-------| +{{#each roles}} +| {{this.name}} | {{this.message_types_list}} | +{{/each}} + +### CLI 回退 + +当 `mcp__ccw-tools__team_msg` MCP 不可用时: + +\`\`\`javascript +Bash(\`ccw team log --team "{{team_name}}" --from "\${role}" --to "coordinator" --type "" --summary "<摘要>" --json\`) +\`\`\` + +### Task Lifecycle (All Roles) + +\`\`\`javascript +// Standard task lifecycle every role follows +// Phase 1: Discovery +const tasks = TaskList() +const myTasks = tasks.filter(t => + t.subject.startsWith(\`\${VALID_ROLES[role].prefix}-\`) && + t.owner === role && + t.status === 'pending' && + t.blockedBy.length === 0 +) +if (myTasks.length === 0) return // idle +const task = TaskGet({ taskId: myTasks[0].id }) +TaskUpdate({ taskId: task.id, status: 'in_progress' }) + +// Phase 2-4: Role-specific (see roles/{role}.md) + +// Phase 5: Report + Loop +mcp__ccw-tools__team_msg({ operation: "log", team: "{{team_name}}", from: role, to: "coordinator", type: "...", summary: "..." }) +SendMessage({ type: "message", recipient: "coordinator", content: "...", summary: "..." }) +TaskUpdate({ taskId: task.id, status: 'completed' }) +// Check for next task → back to Phase 1 +\`\`\` + +## Pipeline + +\`\`\` +{{pipeline_diagram}} +\`\`\` + +## Coordinator Spawn Template + +When coordinator creates teammates, use this pattern: + +\`\`\`javascript +TeamCreate({ team_name: "{{team_name}}" }) + +{{#each worker_roles}} +// {{this.display_name}} +Task({ + subagent_type: "general-purpose", + team_name: "{{../team_name}}", + name: "{{this.name}}", + prompt: \`你是 team "{{../team_name}}" 的 {{this.name_upper}}. + +当你收到 {{this.task_prefix}}-* 任务时,调用 Skill(skill="team-{{../team_name}}", args="--role={{this.name}}") 执行。 + +当前需求: \${taskDescription} +约束: \${constraints} + +## 消息总线(必须) +每次 SendMessage 前,先调用 mcp__ccw-tools__team_msg 记录。 + +工作流程: +1. TaskList → 找到 {{this.task_prefix}}-* 任务 +2. Skill(skill="team-{{../team_name}}", args="--role={{this.name}}") 执行 +3. team_msg log + SendMessage 结果给 coordinator +4. TaskUpdate completed → 检查下一个任务\` +}) +{{/each}} +\`\`\` + +## Error Handling + +| Scenario | Resolution | +|----------|------------| +| Unknown --role value | Error with available role list | +| Missing --role arg | Error with usage hint | +| Role file not found | Error with expected path | +| Task prefix conflict | Log warning, proceed | +``` + +--- + +## Variable Reference + +| Variable | Source | Description | +|----------|--------|-------------| +| `{{team_name}}` | config.team_name | Team identifier (lowercase) | +| `{{team_display_name}}` | config.team_display_name | Human-readable team name | +| `{{all_roles_tools_union}}` | Union of all roles' allowed-tools | Combined tool list | +| `{{roles}}` | config.roles[] | Array of role definitions | +| `{{role_list}}` | Role names joined by comma | e.g., "coordinator, planner, executor" | +| `{{roles_json}}` | JSON.stringify(roles) | Roles as JSON | +| `{{pipeline_diagram}}` | Generated from task chain | ASCII pipeline | +| `{{worker_roles}}` | config.roles excluding coordinator | Non-coordinator roles | +| `{{role.name}}` | Per-role name | e.g., "planner" | +| `{{role.task_prefix}}` | Per-role task prefix | e.g., "PLAN" | +| `{{role.responsibility}}` | Per-role responsibility | e.g., "Code exploration and planning" | +| `{{role.message_types_list}}` | Per-role message types | e.g., "`plan_ready`, `error`" | diff --git a/ccw/frontend/src/components/shared/TaskDrawer.tsx b/ccw/frontend/src/components/shared/TaskDrawer.tsx index a71123b6..0f2cb0f0 100644 --- a/ccw/frontend/src/components/shared/TaskDrawer.tsx +++ b/ccw/frontend/src/components/shared/TaskDrawer.tsx @@ -10,47 +10,20 @@ import { Flowchart } from './Flowchart'; import { Badge } from '../ui/Badge'; import { Button } from '../ui/Button'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/Tabs'; -import type { LiteTask, FlowControl } from '@/lib/api'; +import type { NormalizedTask } from '@/lib/api'; +import { buildFlowControl } from '@/lib/api'; import type { TaskData } from '@/types/store'; // ========== Types ========== export interface TaskDrawerProps { - task: LiteTask | TaskData | null; + task: NormalizedTask | TaskData | null; isOpen: boolean; onClose: () => void; } type TabValue = 'overview' | 'flowchart' | 'files'; -// ========== Helper: Unified Task Access ========== - -/** - * Normalize task data to common interface - */ -function getTaskId(task: LiteTask | TaskData): string { - if ('task_id' in task && task.task_id) return task.task_id; - if ('id' in task) return task.id; - return 'N/A'; -} - -function getTaskTitle(task: LiteTask | TaskData): string { - return task.title || 'Untitled Task'; -} - -function getTaskDescription(task: LiteTask | TaskData): string | undefined { - return task.description; -} - -function getTaskStatus(task: LiteTask | TaskData): string { - return task.status; -} - -function getFlowControl(task: LiteTask | TaskData): FlowControl | undefined { - if ('flow_control' in task) return task.flow_control; - return undefined; -} - // Status configuration const taskStatusConfig: Record }> = { pending: { @@ -113,17 +86,28 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) { return null; } - const taskId = getTaskId(task); - const taskTitle = getTaskTitle(task); - const taskDescription = getTaskDescription(task); - const taskStatus = getTaskStatus(task); - const flowControl = getFlowControl(task); + // Use NormalizedTask fields (works for both old nested and new flat formats) + const nt = task as NormalizedTask; + const taskId = nt.task_id || 'N/A'; + const taskTitle = nt.title || 'Untitled Task'; + const taskDescription = nt.description; + const taskStatus = nt.status; + const flowControl = buildFlowControl(nt); + + // Normalized flat fields + const acceptanceCriteria = nt.convergence?.criteria || []; + const focusPaths = nt.focus_paths || []; + const dependsOn = nt.depends_on || []; + const preAnalysis = nt.pre_analysis || flowControl?.pre_analysis || []; + const implSteps = nt.implementation || flowControl?.implementation_approach || []; + const taskFiles = nt.files || flowControl?.target_files || []; + const taskScope = nt.scope; const statusConfig = taskStatusConfig[taskStatus] || taskStatusConfig.pending; const StatusIcon = statusConfig.icon; - const hasFlowchart = !!flowControl?.implementation_approach && flowControl.implementation_approach.length > 0; - const hasFiles = !!flowControl?.target_files && flowControl.target_files.length > 0; + const hasFlowchart = implSteps.length > 0; + const hasFiles = taskFiles.length > 0; return ( <> @@ -205,27 +189,27 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) { )} {/* Scope Section */} - {(task as LiteTask).meta?.scope && ( + {taskScope && (

📁 Scope

- {(task as LiteTask).meta?.scope} + {taskScope}
)} - {/* Acceptance Criteria Section */} - {(task as LiteTask).context?.acceptance && (task as LiteTask).context!.acceptance!.length > 0 && ( + {/* Acceptance / Convergence Criteria Section */} + {acceptanceCriteria.length > 0 && (

{formatMessage({ id: 'liteTasks.acceptanceCriteria' })}

- {(task as LiteTask).context!.acceptance!.map((criterion, i) => ( + {acceptanceCriteria.map((criterion, i) => (
{criterion} @@ -236,14 +220,14 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) { )} {/* Focus Paths / Reference Section */} - {(task as LiteTask).context?.focus_paths && (task as LiteTask).context!.focus_paths!.length > 0 && ( + {focusPaths.length > 0 && (

📚 {formatMessage({ id: 'liteTasks.focusPaths' })}

- {(task as LiteTask).context!.focus_paths!.map((path, i) => ( + {focusPaths.map((path, i) => ( {path} @@ -253,14 +237,14 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) { )} {/* Dependencies Section */} - {(task as LiteTask).context?.depends_on && (task as LiteTask).context!.depends_on!.length > 0 && ( + {dependsOn.length > 0 && (

🔗 {formatMessage({ id: 'liteTasks.dependsOn' })}

- {(task as LiteTask).context!.depends_on!.map((dep, i) => ( + {dependsOn.map((dep, i) => ( {dep} ))}
@@ -268,14 +252,14 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) { )} {/* Pre-analysis Steps */} - {flowControl?.pre_analysis && flowControl.pre_analysis.length > 0 && ( + {preAnalysis.length > 0 && (

🔍 {formatMessage({ id: 'sessionDetail.taskDrawer.overview.preAnalysis' })}

- {flowControl.pre_analysis.map((step, index) => ( + {preAnalysis.map((step, index) => (
{index + 1} diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts index 1f07a607..3e5e288a 100644 --- a/ccw/frontend/src/lib/api.ts +++ b/ccw/frontend/src/lib/api.ts @@ -312,7 +312,8 @@ function transformBackendSession( has_review: backendData.hasReview, review, summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries, - tasks: (backendSession as unknown as { tasks?: TaskData[] }).tasks, + tasks: ((backendSession as unknown as { tasks?: TaskData[] }).tasks || []) + .map(t => normalizeTask(t as unknown as Record)), }; } @@ -1986,6 +1987,139 @@ export interface LiteTask { updated_at?: string; } +// ========== Normalized Task (Unified Flat Format) ========== + +/** + * Normalized task type that unifies both old 6-field nested format + * and new unified flat format into a single interface. + * + * Old format paths → New flat paths: + * - context.acceptance[] → convergence.criteria[] + * - context.focus_paths[] → focus_paths[] + * - context.depends_on[] → depends_on[] + * - context.requirements[] → description + * - flow_control.pre_analysis[] → pre_analysis[] + * - flow_control.implementation_approach[] → implementation[] + * - flow_control.target_files[] → files[] + */ +export interface NormalizedTask extends TaskData { + // Promoted from context + focus_paths?: string[]; + convergence?: { + criteria?: string[]; + verification?: string; + definition_of_done?: string; + }; + + // Promoted from flow_control + pre_analysis?: PreAnalysisStep[]; + implementation?: (ImplementationStep | string)[]; + files?: Array<{ path: string; name?: string }>; + + // Promoted from meta + type?: string; + scope?: string; + action?: string; + + // Original nested objects (preserved for long-term compat) + flow_control?: FlowControl; + context?: { + focus_paths?: string[]; + acceptance?: string[]; + depends_on?: string[]; + requirements?: string[]; + }; + meta?: { + type?: string; + scope?: string; + [key: string]: unknown; + }; + + // Raw data reference for JSON viewer / debugging + _raw?: unknown; +} + +/** + * Normalize a raw task object (old 6-field or new unified flat) into NormalizedTask. + * Reads new flat fields first, falls back to old nested paths. + * Long-term compatible: handles both formats permanently. + */ +export function normalizeTask(raw: Record): NormalizedTask { + if (!raw || typeof raw !== 'object') { + return { task_id: 'N/A', status: 'pending', _raw: raw } as NormalizedTask; + } + + // Type-safe access helpers + const rawContext = raw.context as LiteTask['context'] | undefined; + const rawFlowControl = raw.flow_control as FlowControl | undefined; + const rawMeta = raw.meta as LiteTask['meta'] | undefined; + const rawConvergence = raw.convergence as NormalizedTask['convergence'] | undefined; + + // Description: new flat field first, then join old context.requirements + const rawRequirements = rawContext?.requirements; + const description = (raw.description as string | undefined) + || (Array.isArray(rawRequirements) && rawRequirements.length > 0 + ? rawRequirements.join('; ') + : undefined); + + return { + // Identity + task_id: (raw.task_id as string) || (raw.id as string) || 'N/A', + title: raw.title as string | undefined, + description, + status: (raw.status as NormalizedTask['status']) || 'pending', + priority: raw.priority as NormalizedTask['priority'], + created_at: raw.created_at as string | undefined, + updated_at: raw.updated_at as string | undefined, + has_summary: raw.has_summary as boolean | undefined, + estimated_complexity: raw.estimated_complexity as string | undefined, + + // Promoted from context (new first, old fallback) + depends_on: (raw.depends_on as string[]) || rawContext?.depends_on || [], + focus_paths: (raw.focus_paths as string[]) || rawContext?.focus_paths || [], + convergence: rawConvergence || (rawContext?.acceptance?.length + ? { criteria: rawContext.acceptance } + : undefined), + + // Promoted from flow_control (new first, old fallback) + pre_analysis: (raw.pre_analysis as PreAnalysisStep[]) || rawFlowControl?.pre_analysis, + implementation: (raw.implementation as (ImplementationStep | string)[]) || rawFlowControl?.implementation_approach, + files: (raw.files as Array<{ path: string; name?: string }>) || rawFlowControl?.target_files, + + // Promoted from meta (new first, old fallback) + type: (raw.type as string) || rawMeta?.type, + scope: (raw.scope as string) || rawMeta?.scope, + action: (raw.action as string) || (rawMeta as Record | undefined)?.action as string | undefined, + + // Preserve original nested objects for backward compat + flow_control: rawFlowControl, + context: rawContext, + meta: rawMeta, + + // Raw reference + _raw: raw, + }; +} + +/** + * Build a FlowControl object from NormalizedTask for backward-compatible components (e.g. Flowchart). + */ +export function buildFlowControl(task: NormalizedTask): FlowControl | undefined { + const preAnalysis = task.pre_analysis; + const implementation = task.implementation; + const files = task.files; + + if (!preAnalysis?.length && !implementation?.length && !files?.length) { + return task.flow_control; // Fall back to original if no flat fields + } + + return { + pre_analysis: preAnalysis || task.flow_control?.pre_analysis, + implementation_approach: implementation || task.flow_control?.implementation_approach, + target_files: files || task.flow_control?.target_files, + }; +} + export interface LiteTaskSession { id: string; session_id?: string; diff --git a/codex-lens/src/codexlens/config.py b/codex-lens/src/codexlens/config.py index 233745c3..8d5cc3ef 100644 --- a/codex-lens/src/codexlens/config.py +++ b/codex-lens/src/codexlens/config.py @@ -145,7 +145,7 @@ class Config: # Staged cascade search configuration (4-stage pipeline) staged_coarse_k: int = 200 # Number of coarse candidates from Stage 1 binary search staged_lsp_depth: int = 2 # LSP relationship expansion depth in Stage 2 - staged_stage2_mode: str = "precomputed" # "precomputed" (graph_neighbors) | "realtime" (LSP) + staged_stage2_mode: str = "precomputed" # "precomputed" (graph_neighbors) | "realtime" (LSP) | "static_global_graph" (global_relationships) # Static graph configuration (write relationships to global index during build) static_graph_enabled: bool = False @@ -627,7 +627,7 @@ class Config: staged_stage2_mode = get_env("STAGED_STAGE2_MODE") if staged_stage2_mode: mode = staged_stage2_mode.strip().lower() - if mode in {"precomputed", "realtime"}: + if mode in {"precomputed", "realtime", "static_global_graph"}: self.staged_stage2_mode = mode log.debug("Overriding staged_stage2_mode from .env: %s", self.staged_stage2_mode) elif mode in {"live"}: diff --git a/codex-lens/src/codexlens/search/chain_search.py b/codex-lens/src/codexlens/search/chain_search.py index f6525ff5..c9401906 100644 --- a/codex-lens/src/codexlens/search/chain_search.py +++ b/codex-lens/src/codexlens/search/chain_search.py @@ -1293,6 +1293,9 @@ class ChainSearchEngine: query=query, ) + if mode == "static_global_graph": + return self._stage2_static_global_graph_expand(coarse_results, index_root=index_root) + return self._stage2_precomputed_graph_expand(coarse_results, index_root=index_root) except ImportError as exc: @@ -1343,6 +1346,50 @@ class ChainSearchEngine: return self._combine_stage2_results(coarse_results, related_results) + def _stage2_static_global_graph_expand( + self, + coarse_results: List[SearchResult], + *, + index_root: Path, + ) -> List[SearchResult]: + """Stage 2 (static_global_graph): expand using GlobalGraphExpander over global_relationships.""" + from codexlens.search.global_graph_expander import GlobalGraphExpander + + global_db_path = index_root / GlobalSymbolIndex.DEFAULT_DB_NAME + if not global_db_path.exists(): + self.logger.debug("Global symbol DB not found at %s, skipping static graph expansion", global_db_path) + return coarse_results + + project_id = 1 + try: + for p in self.registry.list_projects(): + if p.index_root.resolve() == index_root.resolve(): + project_id = p.id + break + except Exception: + pass + + global_index = GlobalSymbolIndex(global_db_path, project_id=project_id) + global_index.initialize() + + try: + expander = GlobalGraphExpander(global_index, config=self._config) + related_results = expander.expand( + coarse_results, + top_n=min(10, len(coarse_results)), + max_related=50, + ) + + if related_results: + self.logger.debug( + "Stage 2 (static_global_graph) expanded %d base results to %d related symbols", + len(coarse_results), len(related_results), + ) + + return self._combine_stage2_results(coarse_results, related_results) + finally: + global_index.close() + def _stage2_realtime_lsp_expand( self, coarse_results: List[SearchResult], diff --git a/codex-lens/tests/test_static_graph_integration.py b/codex-lens/tests/test_static_graph_integration.py new file mode 100644 index 00000000..2dfb1357 --- /dev/null +++ b/codex-lens/tests/test_static_graph_integration.py @@ -0,0 +1,289 @@ +"""Tests for static graph relationship writing during index build (T2). + +Verifies that IndexTreeBuilder._build_single_dir and _build_dir_worker +correctly write relationships to GlobalSymbolIndex when +config.static_graph_enabled is True. +""" + +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from codexlens.config import Config +from codexlens.entities import ( + CodeRelationship, + IndexedFile, + RelationshipType, + Symbol, +) +from codexlens.storage.global_index import GlobalSymbolIndex + + +@pytest.fixture() +def temp_dir(): + tmpdir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True) + yield Path(tmpdir.name) + try: + tmpdir.cleanup() + except (PermissionError, OSError): + pass + + +def _make_indexed_file(file_path: str) -> IndexedFile: + """Create a test IndexedFile with symbols and relationships.""" + return IndexedFile( + path=file_path, + language="python", + symbols=[ + Symbol(name="MyClass", kind="class", range=(1, 20)), + Symbol(name="helper", kind="function", range=(22, 30)), + ], + relationships=[ + CodeRelationship( + source_symbol="MyClass", + target_symbol="BaseClass", + relationship_type=RelationshipType.INHERITS, + source_file=file_path, + target_file="other/base.py", + source_line=1, + ), + CodeRelationship( + source_symbol="MyClass", + target_symbol="os", + relationship_type=RelationshipType.IMPORTS, + source_file=file_path, + source_line=2, + ), + CodeRelationship( + source_symbol="helper", + target_symbol="external_func", + relationship_type=RelationshipType.CALL, + source_file=file_path, + source_line=25, + ), + ], + ) + + +def test_build_single_dir_writes_global_relationships_when_enabled(temp_dir: Path) -> None: + """When static_graph_enabled=True, relationships should be written to global index.""" + from codexlens.storage.index_tree import IndexTreeBuilder + + config = Config( + data_dir=temp_dir / "data", + static_graph_enabled=True, + static_graph_relationship_types=["imports", "inherits"], + global_symbol_index_enabled=True, + ) + + # Set up real GlobalSymbolIndex + global_db_path = temp_dir / "global_symbols.db" + global_index = GlobalSymbolIndex(global_db_path, project_id=1) + global_index.initialize() + + # Create a source file + src_dir = temp_dir / "src" + src_dir.mkdir() + test_file = src_dir / "module.py" + test_file.write_text("class MyClass(BaseClass):\n pass\n", encoding="utf-8") + + indexed_file = _make_indexed_file(str(test_file)) + + # Mock parser to return our test IndexedFile + mock_parser = MagicMock() + mock_parser.parse.return_value = indexed_file + + mock_mapper = MagicMock() + mock_mapper.source_to_index_db.return_value = temp_dir / "index" / "_index.db" + + mock_registry = MagicMock() + + builder = IndexTreeBuilder(mock_registry, mock_mapper, config=config, incremental=False) + builder.parser_factory = MagicMock() + builder.parser_factory.get_parser.return_value = mock_parser + + result = builder._build_single_dir( + src_dir, + languages=None, + project_id=1, + global_index_db_path=global_db_path, + ) + + assert result.error is None + assert result.files_count == 1 + + # Verify relationships were written to global index + # Only IMPORTS and INHERITS should be written (not CALL) + rels = global_index.query_by_target("BaseClass", prefix_mode=True) + rels += global_index.query_by_target("os", prefix_mode=True) + assert len(rels) >= 1, "Expected at least 1 relationship written to global index" + + # CALL relationship for external_func should NOT be present + call_rels = global_index.query_by_target("external_func", prefix_mode=True) + assert len(call_rels) == 0, "CALL relationships should not be written" + + global_index.close() + + +def test_build_single_dir_skips_relationships_when_disabled(temp_dir: Path) -> None: + """When static_graph_enabled=False, no relationships should be written.""" + from codexlens.storage.index_tree import IndexTreeBuilder + + config = Config( + data_dir=temp_dir / "data", + static_graph_enabled=False, + global_symbol_index_enabled=True, + ) + + global_db_path = temp_dir / "global_symbols.db" + global_index = GlobalSymbolIndex(global_db_path, project_id=1) + global_index.initialize() + + src_dir = temp_dir / "src" + src_dir.mkdir() + test_file = src_dir / "module.py" + test_file.write_text("import os\n", encoding="utf-8") + + indexed_file = _make_indexed_file(str(test_file)) + + mock_parser = MagicMock() + mock_parser.parse.return_value = indexed_file + + mock_mapper = MagicMock() + mock_mapper.source_to_index_db.return_value = temp_dir / "index" / "_index.db" + + mock_registry = MagicMock() + + builder = IndexTreeBuilder(mock_registry, mock_mapper, config=config, incremental=False) + builder.parser_factory = MagicMock() + builder.parser_factory.get_parser.return_value = mock_parser + + result = builder._build_single_dir( + src_dir, + languages=None, + project_id=1, + global_index_db_path=global_db_path, + ) + + assert result.error is None + + # No relationships should be in global index + conn = global_index._get_connection() + count = conn.execute("SELECT COUNT(*) FROM global_relationships").fetchone()[0] + assert count == 0, "No relationships should be written when static_graph_enabled=False" + + global_index.close() + + +def test_relationship_write_failure_does_not_block_indexing(temp_dir: Path) -> None: + """If global_index.update_file_relationships raises, file indexing continues.""" + from codexlens.storage.index_tree import IndexTreeBuilder + + config = Config( + data_dir=temp_dir / "data", + static_graph_enabled=True, + static_graph_relationship_types=["imports", "inherits"], + global_symbol_index_enabled=True, + ) + + src_dir = temp_dir / "src" + src_dir.mkdir() + test_file = src_dir / "module.py" + test_file.write_text("import os\n", encoding="utf-8") + + indexed_file = _make_indexed_file(str(test_file)) + + mock_parser = MagicMock() + mock_parser.parse.return_value = indexed_file + + mock_mapper = MagicMock() + mock_mapper.source_to_index_db.return_value = temp_dir / "index" / "_index.db" + + mock_registry = MagicMock() + + # Create a mock GlobalSymbolIndex that fails on update_file_relationships + mock_global_db_path = temp_dir / "global_symbols.db" + + builder = IndexTreeBuilder(mock_registry, mock_mapper, config=config, incremental=False) + builder.parser_factory = MagicMock() + builder.parser_factory.get_parser.return_value = mock_parser + + # Patch GlobalSymbolIndex so update_file_relationships raises + with patch("codexlens.storage.index_tree.GlobalSymbolIndex") as MockGSI: + mock_gsi_instance = MagicMock() + mock_gsi_instance.update_file_relationships.side_effect = RuntimeError("DB locked") + MockGSI.return_value = mock_gsi_instance + + result = builder._build_single_dir( + src_dir, + languages=None, + project_id=1, + global_index_db_path=mock_global_db_path, + ) + + # File should still be indexed despite relationship write failure + assert result.error is None + assert result.files_count == 1 + + +def test_only_configured_relationship_types_written(temp_dir: Path) -> None: + """Only relationship types in static_graph_relationship_types should be written.""" + from codexlens.storage.index_tree import IndexTreeBuilder + + # Only allow 'imports' (not 'inherits') + config = Config( + data_dir=temp_dir / "data", + static_graph_enabled=True, + static_graph_relationship_types=["imports"], + global_symbol_index_enabled=True, + ) + + global_db_path = temp_dir / "global_symbols.db" + global_index = GlobalSymbolIndex(global_db_path, project_id=1) + global_index.initialize() + + src_dir = temp_dir / "src" + src_dir.mkdir() + test_file = src_dir / "module.py" + test_file.write_text("import os\nclass Foo(Bar): pass\n", encoding="utf-8") + + indexed_file = _make_indexed_file(str(test_file)) + + mock_parser = MagicMock() + mock_parser.parse.return_value = indexed_file + + mock_mapper = MagicMock() + mock_mapper.source_to_index_db.return_value = temp_dir / "index" / "_index.db" + + mock_registry = MagicMock() + + builder = IndexTreeBuilder(mock_registry, mock_mapper, config=config, incremental=False) + builder.parser_factory = MagicMock() + builder.parser_factory.get_parser.return_value = mock_parser + + result = builder._build_single_dir( + src_dir, + languages=None, + project_id=1, + global_index_db_path=global_db_path, + ) + + assert result.error is None + + # Only IMPORTS should be written + conn = global_index._get_connection() + rows = conn.execute( + "SELECT relationship_type FROM global_relationships" + ).fetchall() + + rel_types = {row[0] for row in rows} + assert "imports" in rel_types or len(rows) == 0 or rel_types == {"imports"}, \ + f"Expected only 'imports', got {rel_types}" + # INHERITS should NOT be present + assert "inherits" not in rel_types, "inherits should not be written when not in config" + # CALL should NOT be present + assert "calls" not in rel_types, "calls should not be written" + + global_index.close()