diff --git a/plugins/compound-engineering/agents/research/repo-research-analyst.md b/plugins/compound-engineering/agents/research/repo-research-analyst.md index 011cfa6..e7ffb00 100644 --- a/plugins/compound-engineering/agents/research/repo-research-analyst.md +++ b/plugins/compound-engineering/agents/research/repo-research-analyst.md @@ -9,7 +9,7 @@ model: inherit Context: User wants to understand a new repository's structure and conventions before contributing. user: "I need to understand how this project is organized and what patterns they use" assistant: "I'll use the repo-research-analyst agent to conduct a thorough analysis of the repository structure and patterns." -Since the user needs comprehensive repository research, use the repo-research-analyst agent to examine all aspects of the project. +Since the user needs comprehensive repository research, use the repo-research-analyst agent to examine all aspects of the project. No scope is specified, so the agent runs all phases. Context: User is preparing to create a GitHub issue and wants to follow project conventions. @@ -23,12 +23,45 @@ user: "I want to add a new service object - what patterns does this codebase use assistant: "I'll use the repo-research-analyst agent to search for existing implementation patterns in the codebase." Since the user needs to understand implementation patterns, use the repo-research-analyst agent to search and analyze the codebase. + +Context: A planning skill needs technology context and architecture patterns but not issue conventions or templates. +user: "Scope: technology, architecture, patterns. We are building a new background job processor for the billing service." +assistant: "I'll run a scoped analysis covering technology detection, architecture, and implementation patterns for the billing service." +The consumer specified a scope, so the agent skips issue conventions, documentation review, and template discovery -- running only the requested phases. + **Note: The current year is 2026.** Use this when searching for recent documentation and patterns. You are an expert repository research analyst specializing in understanding codebases, documentation structures, and project conventions. Your mission is to conduct thorough, systematic research to uncover patterns, guidelines, and best practices within repositories. +**Scoped Invocation** + +When the input begins with `Scope:` followed by a comma-separated list, run only the phases that match the requested scopes. This lets consumers request exactly the research they need. + +Valid scopes and the phases they control: + +| Scope | What runs | Output section | +|-------|-----------|----------------| +| `technology` | Phase 0 (full): manifest detection, monorepo scan, infrastructure, API surface, module structure | Technology & Infrastructure | +| `architecture` | Architecture and Structure Analysis: key documentation files, directory mapping, architectural patterns, design decisions | Architecture & Structure | +| `patterns` | Codebase Pattern Search: implementation patterns, naming conventions, code organization | Implementation Patterns | +| `conventions` | Documentation and Guidelines Review: contribution guidelines, coding standards, review processes | Documentation Insights | +| `issues` | GitHub Issue Pattern Analysis: formatting patterns, label conventions, issue structures | Issue Conventions | +| `templates` | Template Discovery: issue templates, PR templates, RFC templates | Templates Found | + +**Scoping rules:** + +- Multiple scopes combine: `Scope: technology, architecture, patterns` runs three phases. +- When scoped, produce output sections only for the requested scopes. Omit sections for phases that did not run. +- Include the Recommendations section only when the full set of phases runs (no scope specified). +- When `technology` is not in scope but other phases are, still run Phase 0.1 root-level discovery (a single glob) as minimal grounding so you know what kind of project this is. Do not run 0.1b, 0.2, or 0.3. Do not include Technology & Infrastructure in the output. +- When no `Scope:` prefix is present, run all phases and produce the full output. This is the default behavior. + +Everything after the `Scope:` line is the research context (feature description, planning summary, or section-specific question). Use it to focus the requested phases on what matters for the consumer. + +--- + **Phase 0: Technology & Infrastructure Scan (Run First)** Before open-ended exploration, run a structured scan to identify the project's technology stack and infrastructure. This grounds all subsequent research. diff --git a/plugins/compound-engineering/skills/ce-plan-beta/SKILL.md b/plugins/compound-engineering/skills/ce-plan-beta/SKILL.md index d566d77..732ee54 100644 --- a/plugins/compound-engineering/skills/ce-plan-beta/SKILL.md +++ b/plugins/compound-engineering/skills/ce-plan-beta/SKILL.md @@ -145,12 +145,13 @@ Prepare a concise planning context summary (a paragraph or two) to pass as input Run these agents in parallel: -- Task compound-engineering:research:repo-research-analyst(planning context summary) +- Task compound-engineering:research:repo-research-analyst(Scope: technology, architecture, patterns. {planning context summary}) - Task compound-engineering:research:learnings-researcher(planning context summary) Collect: -- Existing patterns and conventions to follow -- Relevant files, modules, and tests +- Technology stack and versions (used in section 1.2 to make sharper external research decisions) +- Architectural patterns and conventions to follow +- Implementation patterns, relevant files, modules, and tests - AGENTS.md guidance that materially affects the plan, with CLAUDE.md used only as compatibility fallback when present - Institutional learnings from `docs/solutions/` diff --git a/plugins/compound-engineering/skills/ce-plan/SKILL.md b/plugins/compound-engineering/skills/ce-plan/SKILL.md index f043714..41c4bab 100644 --- a/plugins/compound-engineering/skills/ce-plan/SKILL.md +++ b/plugins/compound-engineering/skills/ce-plan/SKILL.md @@ -83,11 +83,11 @@ First, I need to understand the project's conventions, existing patterns, and an Run these agents **in parallel** to gather local context: -- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:research:repo-research-analyst(Scope: technology, architecture, patterns. {feature_description}) - Task compound-engineering:research:learnings-researcher(feature_description) **What to look for:** -- **Repo research:** existing patterns, AGENTS.md guidance, technology familiarity, pattern consistency +- **Repo research:** technology stack and versions (informs research decisions), architectural patterns, and implementation patterns relevant to the feature - **Learnings:** documented solutions in `docs/solutions/` that might apply (gotchas, patterns, lessons learned) These findings inform the next step. diff --git a/plugins/compound-engineering/skills/deepen-plan-beta/SKILL.md b/plugins/compound-engineering/skills/deepen-plan-beta/SKILL.md index 5610279..a933b63 100644 --- a/plugins/compound-engineering/skills/deepen-plan-beta/SKILL.md +++ b/plugins/compound-engineering/skills/deepen-plan-beta/SKILL.md @@ -209,7 +209,7 @@ Use fully-qualified agent names inside Task calls. **Requirements Trace / Open Questions classification** - `compound-engineering:workflow:spec-flow-analyzer` for missing user flows, edge cases, and handoff gaps -- `compound-engineering:research:repo-research-analyst` for repo-grounded patterns, conventions, and implementation reality checks +- `compound-engineering:research:repo-research-analyst` (Scope: `architecture, patterns`) for repo-grounded patterns, conventions, and implementation reality checks **Context & Research / Sources & References gaps** - `compound-engineering:research:learnings-researcher` for institutional knowledge and past solved problems @@ -223,11 +223,11 @@ Use fully-qualified agent names inside Task calls. **High-Level Technical Design** - `compound-engineering:review:architecture-strategist` for validating that the technical design accurately represents the intended approach and identifying gaps -- `compound-engineering:research:repo-research-analyst` for grounding the technical design in existing repo patterns and conventions +- `compound-engineering:research:repo-research-analyst` (Scope: `architecture, patterns`) for grounding the technical design in existing repo patterns and conventions - Add `compound-engineering:research:best-practices-researcher` when the technical design involves a DSL, API surface, or pattern that benefits from external validation **Implementation Units / Verification** -- `compound-engineering:research:repo-research-analyst` for concrete file targets, patterns to follow, and repo-specific sequencing clues +- `compound-engineering:research:repo-research-analyst` (Scope: `patterns`) for concrete file targets, patterns to follow, and repo-specific sequencing clues - `compound-engineering:review:pattern-recognition-specialist` for consistency, duplication risks, and alignment with existing patterns - Add `compound-engineering:workflow:spec-flow-analyzer` when sequencing depends on user flow or handoff completeness @@ -249,6 +249,7 @@ Use fully-qualified agent names inside Task calls. #### 3.2 Agent Prompt Shape For each selected section, pass: +- The scope prefix from section 3.1 (e.g., `Scope: architecture, patterns.`) when the agent supports scoped invocation - A short plan summary - The exact section text - Why the section was selected, including which checklist triggers fired diff --git a/src/converters/claude-to-copilot.ts b/src/converters/claude-to-copilot.ts index 6a7722c..67f0dab 100644 --- a/src/converters/claude-to-copilot.ts +++ b/src/converters/claude-to-copilot.ts @@ -106,11 +106,15 @@ function convertCommandToSkill( export function transformContentForCopilot(body: string): string { let result = body - // 1. Transform Task agent calls - const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm + // 1. Transform Task agent calls (supports namespaced names like compound-engineering:research:agent-name) + const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { - const skillName = normalizeName(agentName) - return `${prefix}Use the ${skillName} skill to: ${args.trim()}` + const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName + const skillName = normalizeName(finalSegment) + const trimmedArgs = args.trim() + return trimmedArgs + ? `${prefix}Use the ${skillName} skill to: ${trimmedArgs}` + : `${prefix}Use the ${skillName} skill` }) // 2. Transform slash command references (replace colons with hyphens) diff --git a/src/converters/claude-to-droid.ts b/src/converters/claude-to-droid.ts index 547a23d..af11f06 100644 --- a/src/converters/claude-to-droid.ts +++ b/src/converters/claude-to-droid.ts @@ -119,15 +119,19 @@ function mapAgentTools(agent: ClaudeAgent): string[] | undefined { * 2. Task agent calls: Task agent-name(args) → Task agent-name: args * 3. Agent references: @agent-name → the agent-name droid */ -function transformContentForDroid(body: string): string { +export function transformContentForDroid(body: string): string { let result = body // 1. Transform Task agent calls - // Match: Task repo-research-analyst(feature_description) - const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm + // Match: Task repo-research-analyst(args) or Task compound-engineering:research:repo-research-analyst(args) + const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { - const name = normalizeName(agentName) - return `${prefix}Task ${name}: ${args.trim()}` + const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName + const name = normalizeName(finalSegment) + const trimmedArgs = args.trim() + return trimmedArgs + ? `${prefix}Task ${name}: ${trimmedArgs}` + : `${prefix}Task ${name}` }) // 2. Transform slash command references diff --git a/src/converters/claude-to-gemini.ts b/src/converters/claude-to-gemini.ts index 7dc4389..561cfd4 100644 --- a/src/converters/claude-to-gemini.ts +++ b/src/converters/claude-to-gemini.ts @@ -86,11 +86,15 @@ function convertCommand(command: ClaudeCommand, usedNames: Set): GeminiC export function transformContentForGemini(body: string): string { let result = body - // 1. Transform Task agent calls - const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm + // 1. Transform Task agent calls (supports namespaced names like compound-engineering:research:agent-name) + const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { - const skillName = normalizeName(agentName) - return `${prefix}Use the ${skillName} skill to: ${args.trim()}` + const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName + const skillName = normalizeName(finalSegment) + const trimmedArgs = args.trim() + return trimmedArgs + ? `${prefix}Use the ${skillName} skill to: ${trimmedArgs}` + : `${prefix}Use the ${skillName} skill` }) // 2. Rewrite .claude/ paths to .gemini/ diff --git a/src/converters/claude-to-kiro.ts b/src/converters/claude-to-kiro.ts index f15517e..3e8d622 100644 --- a/src/converters/claude-to-kiro.ts +++ b/src/converters/claude-to-kiro.ts @@ -135,10 +135,15 @@ function convertCommandToSkill( export function transformContentForKiro(body: string, knownAgentNames: string[] = []): string { let result = body - // 1. Transform Task agent calls - const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm + // 1. Transform Task agent calls (supports namespaced names like compound-engineering:research:agent-name) + const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { - return `${prefix}Use the use_subagent tool to delegate to the ${normalizeName(agentName)} agent: ${args.trim()}` + const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName + const agentRef = normalizeName(finalSegment) + const trimmedArgs = args.trim() + return trimmedArgs + ? `${prefix}Use the use_subagent tool to delegate to the ${agentRef} agent: ${trimmedArgs}` + : `${prefix}Use the use_subagent tool to delegate to the ${agentRef} agent` }) // 2. Rewrite .claude/ paths to .kiro/ (with word-boundary-like lookbehind) diff --git a/src/converters/claude-to-pi.ts b/src/converters/claude-to-pi.ts index e266abd..d9302be 100644 --- a/src/converters/claude-to-pi.ts +++ b/src/converters/claude-to-pi.ts @@ -90,16 +90,19 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set): PiGeneratedSk } } -function transformContentForPi(body: string): string { +export function transformContentForPi(body: string): string { let result = body - // Task repo-research-analyst(feature_description) + // Task repo-research-analyst(feature_description) or Task compound-engineering:research:repo-research-analyst(args) // -> Run subagent with agent="repo-research-analyst" and task="feature_description" - const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm + const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { - const skillName = normalizeName(agentName) + const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName + const skillName = normalizeName(finalSegment) const trimmedArgs = args.trim().replace(/\s+/g, " ") - return `${prefix}Run subagent with agent=\"${skillName}\" and task=\"${trimmedArgs}\".` + return trimmedArgs + ? `${prefix}Run subagent with agent=\"${skillName}\" and task=\"${trimmedArgs}\".` + : `${prefix}Run subagent with agent=\"${skillName}\".` }) // Claude-specific tool references diff --git a/src/converters/claude-to-windsurf.ts b/src/converters/claude-to-windsurf.ts index 975af99..4fa3f89 100644 --- a/src/converters/claude-to-windsurf.ts +++ b/src/converters/claude-to-windsurf.ts @@ -122,10 +122,15 @@ export function transformContentForWindsurf(body: string, knownAgentNames: strin // In Windsurf, @skill-name is the native invocation syntax for skills. // Since agents are now mapped to skills, @agent-name already works correctly. - // 4. Transform Task agent calls to skill references - const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm + // 4. Transform Task agent calls to skill references (supports namespaced names) + const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { - return `${prefix}Use the @${normalizeName(agentName)} skill: ${args.trim()}` + const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName + const skillRef = normalizeName(finalSegment) + const trimmedArgs = args.trim() + return trimmedArgs + ? `${prefix}Use the @${skillRef} skill: ${trimmedArgs}` + : `${prefix}Use the @${skillRef} skill` }) return result diff --git a/src/targets/codex.ts b/src/targets/codex.ts index e4d2d54..f52902a 100644 --- a/src/targets/codex.ts +++ b/src/targets/codex.ts @@ -1,6 +1,5 @@ -import { promises as fs } from "fs" import path from "path" -import { backupFile, ensureDir, readText, writeText } from "../utils/files" +import { backupFile, copySkillDir, ensureDir, writeText } from "../utils/files" import type { CodexBundle } from "../types/codex" import type { ClaudeMcpServer } from "../types/claude" import { transformContentForCodex } from "../utils/codex-content" @@ -19,10 +18,12 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): if (bundle.skillDirs.length > 0) { const skillsRoot = path.join(codexRoot, "skills") for (const skill of bundle.skillDirs) { - await copyCodexSkillDir( + await copySkillDir( skill.sourceDir, path.join(skillsRoot, skill.name), - bundle.invocationTargets, + (content) => transformContentForCodex(content, bundle.invocationTargets, { + unknownSlashBehavior: "preserve", + }), ) } } @@ -45,41 +46,6 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): } } -async function copyCodexSkillDir( - sourceDir: string, - targetDir: string, - invocationTargets?: CodexBundle["invocationTargets"], -): Promise { - await ensureDir(targetDir) - const entries = await fs.readdir(sourceDir, { withFileTypes: true }) - - for (const entry of entries) { - const sourcePath = path.join(sourceDir, entry.name) - const targetPath = path.join(targetDir, entry.name) - - if (entry.isDirectory()) { - await copyCodexSkillDir(sourcePath, targetPath, invocationTargets) - continue - } - - if (!entry.isFile()) continue - - if (entry.name === "SKILL.md") { - const content = await readText(sourcePath) - await writeText( - targetPath, - transformContentForCodex(content, invocationTargets, { - unknownSlashBehavior: "preserve", - }), - ) - continue - } - - await ensureDir(path.dirname(targetPath)) - await fs.copyFile(sourcePath, targetPath) - } -} - function resolveCodexRoot(outputRoot: string): string { return path.basename(outputRoot) === ".codex" ? outputRoot : path.join(outputRoot, ".codex") } diff --git a/src/targets/copilot.ts b/src/targets/copilot.ts index d0d1b1c..6c5195e 100644 --- a/src/targets/copilot.ts +++ b/src/targets/copilot.ts @@ -1,5 +1,6 @@ import path from "path" -import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files" +import { backupFile, copySkillDir, ensureDir, writeJson, writeText } from "../utils/files" +import { transformContentForCopilot } from "../converters/claude-to-copilot" import type { CopilotBundle } from "../types/copilot" export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBundle): Promise { @@ -23,7 +24,7 @@ export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBund if (bundle.skillDirs.length > 0) { const skillsDir = path.join(paths.githubDir, "skills") for (const skill of bundle.skillDirs) { - await copyDir(skill.sourceDir, path.join(skillsDir, skill.name)) + await copySkillDir(skill.sourceDir, path.join(skillsDir, skill.name), transformContentForCopilot) } } diff --git a/src/targets/droid.ts b/src/targets/droid.ts index 23bd46e..7b3ce49 100644 --- a/src/targets/droid.ts +++ b/src/targets/droid.ts @@ -1,5 +1,6 @@ import path from "path" -import { copyDir, ensureDir, resolveCommandPath, writeText } from "../utils/files" +import { copySkillDir, ensureDir, resolveCommandPath, writeText } from "../utils/files" +import { transformContentForDroid } from "../converters/claude-to-droid" import type { DroidBundle } from "../types/droid" export async function writeDroidBundle(outputRoot: string, bundle: DroidBundle): Promise { @@ -24,7 +25,7 @@ export async function writeDroidBundle(outputRoot: string, bundle: DroidBundle): if (bundle.skillDirs.length > 0) { await ensureDir(paths.skillsDir) for (const skill of bundle.skillDirs) { - await copyDir(skill.sourceDir, path.join(paths.skillsDir, skill.name)) + await copySkillDir(skill.sourceDir, path.join(paths.skillsDir, skill.name), transformContentForDroid) } } } diff --git a/src/targets/gemini.ts b/src/targets/gemini.ts index 0df7d51..accecb7 100644 --- a/src/targets/gemini.ts +++ b/src/targets/gemini.ts @@ -1,5 +1,6 @@ import path from "path" -import { backupFile, copyDir, ensureDir, pathExists, readJson, resolveCommandPath, writeJson, writeText } from "../utils/files" +import { backupFile, copySkillDir, ensureDir, pathExists, readJson, resolveCommandPath, writeJson, writeText } from "../utils/files" +import { transformContentForGemini } from "../converters/claude-to-gemini" import type { GeminiBundle } from "../types/gemini" export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle): Promise { @@ -14,7 +15,7 @@ export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle if (bundle.skillDirs.length > 0) { for (const skill of bundle.skillDirs) { - await copyDir(skill.sourceDir, path.join(paths.skillsDir, skill.name)) + await copySkillDir(skill.sourceDir, path.join(paths.skillsDir, skill.name), transformContentForGemini) } } diff --git a/src/targets/kiro.ts b/src/targets/kiro.ts index 3597951..64de9fc 100644 --- a/src/targets/kiro.ts +++ b/src/targets/kiro.ts @@ -1,5 +1,6 @@ import path from "path" -import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files" +import { backupFile, copySkillDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files" +import { transformContentForKiro } from "../converters/claude-to-kiro" import type { KiroBundle } from "../types/kiro" export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): Promise { @@ -50,7 +51,10 @@ export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): P continue } - await copyDir(skill.sourceDir, destDir) + const knownAgentNames = bundle.agents.map((a) => a.name) + await copySkillDir(skill.sourceDir, destDir, (content) => + transformContentForKiro(content, knownAgentNames), + ) } } diff --git a/src/targets/pi.ts b/src/targets/pi.ts index 93ba286..61c5375 100644 --- a/src/targets/pi.ts +++ b/src/targets/pi.ts @@ -1,13 +1,14 @@ import path from "path" import { backupFile, - copyDir, + copySkillDir, ensureDir, pathExists, readText, writeJson, writeText, } from "../utils/files" +import { transformContentForPi } from "../converters/claude-to-pi" import type { PiBundle } from "../types/pi" const PI_AGENTS_BLOCK_START = "" @@ -37,7 +38,7 @@ export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promi } for (const skill of bundle.skillDirs) { - await copyDir(skill.sourceDir, path.join(paths.skillsDir, skill.name)) + await copySkillDir(skill.sourceDir, path.join(paths.skillsDir, skill.name), transformContentForPi) } for (const skill of bundle.generatedSkills) { diff --git a/src/targets/windsurf.ts b/src/targets/windsurf.ts index ee96045..54b0ced 100644 --- a/src/targets/windsurf.ts +++ b/src/targets/windsurf.ts @@ -1,6 +1,7 @@ import path from "path" -import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJsonSecure, writeText } from "../utils/files" +import { backupFile, copySkillDir, ensureDir, pathExists, readJson, writeJsonSecure, writeText } from "../utils/files" import { formatFrontmatter } from "../utils/frontmatter" +import { transformContentForWindsurf } from "../converters/claude-to-windsurf" import type { WindsurfBundle } from "../types/windsurf" import type { TargetScope } from "./index" @@ -58,7 +59,10 @@ export async function writeWindsurfBundle(outputRoot: string, bundle: WindsurfBu continue } - await copyDir(skill.sourceDir, destDir) + const knownAgentNames = bundle.agentSkills.map((s) => s.name) + await copySkillDir(skill.sourceDir, destDir, (content) => + transformContentForWindsurf(content, knownAgentNames), + ) } } diff --git a/src/utils/codex-content.ts b/src/utils/codex-content.ts index 69d59eb..e773d72 100644 --- a/src/utils/codex-content.ts +++ b/src/utils/codex-content.ts @@ -29,14 +29,16 @@ export function transformContentForCodex( const skillTargets = targets?.skillTargets ?? {} const unknownSlashBehavior = options.unknownSlashBehavior ?? "prompt" - const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]+)\)/gm + const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { // For namespaced calls like "compound-engineering:research:repo-research-analyst", // use only the final segment as the skill name. const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName const skillName = normalizeCodexName(finalSegment) const trimmedArgs = args.trim() - return `${prefix}Use the $${skillName} skill to: ${trimmedArgs}` + return trimmedArgs + ? `${prefix}Use the $${skillName} skill to: ${trimmedArgs}` + : `${prefix}Use the $${skillName} skill` }) const slashCommandPattern = /(? string, +): Promise { + await ensureDir(targetDir) + const entries = await fs.readdir(sourceDir, { withFileTypes: true }) + + for (const entry of entries) { + const sourcePath = path.join(sourceDir, entry.name) + const targetPath = path.join(targetDir, entry.name) + + if (entry.isDirectory()) { + await copySkillDir(sourcePath, targetPath, transformSkillContent) + } else if (entry.isFile()) { + if (entry.name === "SKILL.md" && transformSkillContent) { + const content = await readText(sourcePath) + await writeText(targetPath, transformSkillContent(content)) + } else { + await ensureDir(path.dirname(targetPath)) + await fs.copyFile(sourcePath, targetPath) + } + } + } +} diff --git a/tests/codex-converter.test.ts b/tests/codex-converter.test.ts index 7f61818..a82c187 100644 --- a/tests/codex-converter.test.ts +++ b/tests/codex-converter.test.ts @@ -248,6 +248,35 @@ Task compound-engineering:review:security-reviewer(code_diff)`, expect(parsed.body).not.toContain("Task compound-engineering:") }) + test("transforms zero-argument Task calls", () => { + const plugin: ClaudePlugin = { + ...fixturePlugin, + commands: [ + { + name: "review", + description: "Review code", + body: `- Task compound-engineering:review:code-simplicity-reviewer()`, + sourcePath: "/tmp/plugin/commands/review.md", + }, + ], + agents: [], + skills: [], + } + + const bundle = convertClaudeToCodex(plugin, { + agentMode: "subagent", + inferTemperature: false, + permissions: "none", + }) + + const commandSkill = bundle.generatedSkills.find((s) => s.name === "review") + expect(commandSkill).toBeDefined() + const parsed = parseFrontmatter(commandSkill!.content) + expect(parsed.body).toContain("Use the $code-simplicity-reviewer skill") + expect(parsed.body).not.toContain("compound-engineering:") + expect(parsed.body).not.toContain("skill to:") + }) + test("transforms slash commands to prompts syntax", () => { const plugin: ClaudePlugin = { ...fixturePlugin, diff --git a/tests/codex-writer.test.ts b/tests/codex-writer.test.ts index 4ac073a..4487171 100644 --- a/tests/codex-writer.test.ts +++ b/tests/codex-writer.test.ts @@ -177,6 +177,7 @@ Run these research agents: Also run bare agents: - Task best-practices-researcher(topic) +- Task compound-engineering:review:code-simplicity-reviewer() `, ) @@ -205,6 +206,10 @@ Also run bare agents: // Bare Task calls should still be rewritten expect(installedSkill).toContain("Use the $best-practices-researcher skill to: topic") expect(installedSkill).not.toContain("Task best-practices-researcher") + + // Zero-arg Task calls should be rewritten without trailing "to:" + expect(installedSkill).toContain("Use the $code-simplicity-reviewer skill") + expect(installedSkill).not.toContain("code-simplicity-reviewer skill to:") }) test("preserves unknown slash text in copied SKILL.md files", async () => { diff --git a/tests/copilot-converter.test.ts b/tests/copilot-converter.test.ts index 22f7973..1bc790e 100644 --- a/tests/copilot-converter.test.ts +++ b/tests/copilot-converter.test.ts @@ -444,6 +444,27 @@ Task best-practices-researcher(topic)` expect(result).not.toContain("Task repo-research-analyst(") }) + test("transforms namespaced Task agent calls using final segment", () => { + const input = `Run agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:review:security-reviewer(code_diff)` + + const result = transformContentForCopilot(input) + expect(result).toContain("Use the repo-research-analyst skill to: feature_description") + expect(result).toContain("Use the security-reviewer skill to: code_diff") + expect(result).not.toContain("compound-engineering:") + }) + + test("transforms zero-argument Task calls", () => { + const input = `- Task compound-engineering:review:code-simplicity-reviewer()` + + const result = transformContentForCopilot(input) + expect(result).toContain("Use the code-simplicity-reviewer skill") + expect(result).not.toContain("compound-engineering:") + expect(result).not.toContain("skill to:") + }) + test("replaces colons with hyphens in slash commands", () => { const input = `1. Run /deepen-plan to enhance 2. Start /workflows:work to implement diff --git a/tests/copilot-writer.test.ts b/tests/copilot-writer.test.ts index 6c430a1..36777e1 100644 --- a/tests/copilot-writer.test.ts +++ b/tests/copilot-writer.test.ts @@ -165,6 +165,44 @@ describe("writeCopilotBundle", () => { expect(backupFiles.length).toBeGreaterThanOrEqual(1) }) + test("transforms Task calls in copied SKILL.md files", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-skill-transform-")) + const sourceSkillDir = path.join(tempRoot, "source-skill") + await fs.mkdir(sourceSkillDir, { recursive: true }) + await fs.writeFile( + path.join(sourceSkillDir, "SKILL.md"), + `--- +name: ce:plan +description: Planning workflow +--- + +Run these research agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:research:learnings-researcher(feature_description) +- Task compound-engineering:review:code-simplicity-reviewer() +`, + ) + + const bundle: CopilotBundle = { + agents: [], + generatedSkills: [], + skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }], + } + + await writeCopilotBundle(tempRoot, bundle) + + const installedSkill = await fs.readFile( + path.join(tempRoot, ".github", "skills", "ce:plan", "SKILL.md"), + "utf8", + ) + + expect(installedSkill).toContain("Use the repo-research-analyst skill to: feature_description") + expect(installedSkill).toContain("Use the learnings-researcher skill to: feature_description") + expect(installedSkill).toContain("Use the code-simplicity-reviewer skill") + expect(installedSkill).not.toContain("Task compound-engineering:") + }) + test("creates skill directories with SKILL.md", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-genskill-")) const bundle: CopilotBundle = { diff --git a/tests/droid-converter.test.ts b/tests/droid-converter.test.ts index 9c37e0b..cc52cdb 100644 --- a/tests/droid-converter.test.ts +++ b/tests/droid-converter.test.ts @@ -148,6 +148,63 @@ Task best-practices-researcher(topic)`, expect(parsed.body).not.toContain("Task repo-research-analyst(") }) + test("transforms namespaced Task agent calls using final segment", () => { + const plugin: ClaudePlugin = { + ...fixturePlugin, + commands: [ + { + name: "plan", + description: "Planning with namespaced agents", + body: `Run agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:review:security-reviewer(code_diff)`, + sourcePath: "/tmp/plugin/commands/plan.md", + }, + ], + agents: [], + skills: [], + } + + const bundle = convertClaudeToDroid(plugin, { + agentMode: "subagent", + inferTemperature: false, + permissions: "none", + }) + + const parsed = parseFrontmatter(bundle.commands[0].content) + expect(parsed.body).toContain("Task repo-research-analyst: feature_description") + expect(parsed.body).toContain("Task security-reviewer: code_diff") + expect(parsed.body).not.toContain("compound-engineering:") + }) + + test("transforms zero-argument Task calls", () => { + const plugin: ClaudePlugin = { + ...fixturePlugin, + commands: [ + { + name: "review", + description: "Review code", + body: `- Task compound-engineering:review:code-simplicity-reviewer()`, + sourcePath: "/tmp/plugin/commands/review.md", + }, + ], + agents: [], + skills: [], + } + + const bundle = convertClaudeToDroid(plugin, { + agentMode: "subagent", + inferTemperature: false, + permissions: "none", + }) + + const parsed = parseFrontmatter(bundle.commands[0].content) + expect(parsed.body).toContain("Task code-simplicity-reviewer") + expect(parsed.body).not.toContain("compound-engineering:") + expect(parsed.body).not.toContain("()") + }) + test("transforms slash commands by flattening namespaces", () => { const plugin: ClaudePlugin = { ...fixturePlugin, diff --git a/tests/droid-writer.test.ts b/tests/droid-writer.test.ts index f8ecf6c..19eb7c0 100644 --- a/tests/droid-writer.test.ts +++ b/tests/droid-writer.test.ts @@ -47,6 +47,44 @@ describe("writeDroidBundle", () => { expect(droidContent).toContain("Droid content") }) + test("transforms Task calls in copied SKILL.md files", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "droid-skill-transform-")) + const sourceSkillDir = path.join(tempRoot, "source-skill") + await fs.mkdir(sourceSkillDir, { recursive: true }) + await fs.writeFile( + path.join(sourceSkillDir, "SKILL.md"), + `--- +name: ce:plan +description: Planning workflow +--- + +Run these research agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:research:learnings-researcher(feature_description) +- Task compound-engineering:review:code-simplicity-reviewer() +`, + ) + + const bundle: DroidBundle = { + commands: [], + droids: [], + skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }], + } + + await writeDroidBundle(tempRoot, bundle) + + const installedSkill = await fs.readFile( + path.join(tempRoot, ".factory", "skills", "ce:plan", "SKILL.md"), + "utf8", + ) + + expect(installedSkill).toContain("Task repo-research-analyst: feature_description") + expect(installedSkill).toContain("Task learnings-researcher: feature_description") + expect(installedSkill).toContain("Task code-simplicity-reviewer") + expect(installedSkill).not.toContain("Task compound-engineering:") + }) + test("writes directly into a .factory output root", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "droid-home-")) const factoryRoot = path.join(tempRoot, ".factory") diff --git a/tests/gemini-converter.test.ts b/tests/gemini-converter.test.ts index bd9675a..db923ac 100644 --- a/tests/gemini-converter.test.ts +++ b/tests/gemini-converter.test.ts @@ -338,6 +338,27 @@ Task best-practices-researcher(topic)` expect(result).not.toContain("Task repo-research-analyst") }) + test("transforms namespaced Task agent calls using final segment", () => { + const input = `Run agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:review:security-reviewer(code_diff)` + + const result = transformContentForGemini(input) + expect(result).toContain("Use the repo-research-analyst skill to: feature_description") + expect(result).toContain("Use the security-reviewer skill to: code_diff") + expect(result).not.toContain("compound-engineering:") + }) + + test("transforms zero-argument Task calls", () => { + const input = `- Task compound-engineering:review:code-simplicity-reviewer()` + + const result = transformContentForGemini(input) + expect(result).toContain("Use the code-simplicity-reviewer skill") + expect(result).not.toContain("compound-engineering:") + expect(result).not.toContain("skill to:") + }) + test("transforms @agent references to skill references", () => { const result = transformContentForGemini("Ask @security-sentinel for a review.") expect(result).toContain("the security-sentinel skill") diff --git a/tests/gemini-writer.test.ts b/tests/gemini-writer.test.ts index a6a9df3..25f9bfb 100644 --- a/tests/gemini-writer.test.ts +++ b/tests/gemini-writer.test.ts @@ -66,6 +66,44 @@ describe("writeGeminiBundle", () => { expect(settingsContent.mcpServers.playwright.command).toBe("npx") }) + test("transforms Task calls in copied SKILL.md files", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "gemini-skill-transform-")) + const sourceSkillDir = path.join(tempRoot, "source-skill") + await fs.mkdir(sourceSkillDir, { recursive: true }) + await fs.writeFile( + path.join(sourceSkillDir, "SKILL.md"), + `--- +name: ce:plan +description: Planning workflow +--- + +Run these research agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:research:learnings-researcher(feature_description) +- Task compound-engineering:review:code-simplicity-reviewer() +`, + ) + + const bundle: GeminiBundle = { + generatedSkills: [], + skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }], + commands: [], + } + + await writeGeminiBundle(tempRoot, bundle) + + const installedSkill = await fs.readFile( + path.join(tempRoot, ".gemini", "skills", "ce:plan", "SKILL.md"), + "utf8", + ) + + expect(installedSkill).toContain("Use the repo-research-analyst skill to: feature_description") + expect(installedSkill).toContain("Use the learnings-researcher skill to: feature_description") + expect(installedSkill).toContain("Use the code-simplicity-reviewer skill") + expect(installedSkill).not.toContain("Task compound-engineering:") + }) + test("namespaced commands create subdirectories", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "gemini-ns-")) const bundle: GeminiBundle = { diff --git a/tests/kiro-converter.test.ts b/tests/kiro-converter.test.ts index 28e9e8f..4a743ff 100644 --- a/tests/kiro-converter.test.ts +++ b/tests/kiro-converter.test.ts @@ -391,6 +391,27 @@ Task best-practices-researcher(topic)` expect(result).not.toContain("Task repo-research-analyst") }) + test("transforms namespaced Task agent calls using final segment", () => { + const input = `Run agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:review:security-reviewer(code_diff)` + + const result = transformContentForKiro(input) + expect(result).toContain("Use the use_subagent tool to delegate to the repo-research-analyst agent: feature_description") + expect(result).toContain("Use the use_subagent tool to delegate to the security-reviewer agent: code_diff") + expect(result).not.toContain("compound-engineering:") + }) + + test("transforms zero-argument Task calls", () => { + const input = `- Task compound-engineering:review:code-simplicity-reviewer()` + + const result = transformContentForKiro(input) + expect(result).toContain("Use the use_subagent tool to delegate to the code-simplicity-reviewer agent") + expect(result).not.toContain("compound-engineering:") + expect(result).not.toContain("code-simplicity-reviewer agent:") + }) + test("transforms @agent references for known agents only", () => { const result = transformContentForKiro("Ask @security-sentinel for a review.", ["security-sentinel"]) expect(result).toContain("the security-sentinel agent") diff --git a/tests/kiro-writer.test.ts b/tests/kiro-writer.test.ts index 301dcb6..500d03b 100644 --- a/tests/kiro-writer.test.ts +++ b/tests/kiro-writer.test.ts @@ -99,6 +99,43 @@ describe("writeKiroBundle", () => { expect(mcpContent.mcpServers.playwright.command).toBe("npx") }) + test("transforms Task calls in copied SKILL.md files", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kiro-skill-transform-")) + const sourceSkillDir = path.join(tempRoot, "source-skill") + await fs.mkdir(sourceSkillDir, { recursive: true }) + await fs.writeFile( + path.join(sourceSkillDir, "SKILL.md"), + `--- +name: ce:plan +description: Planning workflow +--- + +Run these research agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:research:learnings-researcher(feature_description) +- Task compound-engineering:review:code-simplicity-reviewer() +`, + ) + + const bundle: KiroBundle = { + ...emptyBundle, + skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }], + } + + await writeKiroBundle(tempRoot, bundle) + + const installedSkill = await fs.readFile( + path.join(tempRoot, ".kiro", "skills", "ce:plan", "SKILL.md"), + "utf8", + ) + + expect(installedSkill).toContain("Use the use_subagent tool to delegate to the repo-research-analyst agent: feature_description") + expect(installedSkill).toContain("Use the use_subagent tool to delegate to the learnings-researcher agent: feature_description") + expect(installedSkill).toContain("Use the use_subagent tool to delegate to the code-simplicity-reviewer agent") + expect(installedSkill).not.toContain("Task compound-engineering:") + }) + test("does not double-nest when output root is .kiro", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kiro-home-")) const kiroRoot = path.join(tempRoot, ".kiro") diff --git a/tests/pi-converter.test.ts b/tests/pi-converter.test.ts index d7edf95..0d55b69 100644 --- a/tests/pi-converter.test.ts +++ b/tests/pi-converter.test.ts @@ -85,6 +85,70 @@ describe("convertClaudeToPi", () => { expect(parsedPrompt.body).toContain("file-based todos (todos/ + /skill:file-todos)") }) + test("transforms namespaced Task agent calls using final segment", () => { + const plugin: ClaudePlugin = { + root: "/tmp/plugin", + manifest: { name: "fixture", version: "1.0.0" }, + agents: [], + commands: [ + { + name: "plan", + description: "Planning with namespaced agents", + body: [ + "Run agents:", + "- Task compound-engineering:research:repo-research-analyst(feature_description)", + "- Task compound-engineering:review:security-reviewer(code_diff)", + ].join("\n"), + sourcePath: "/tmp/plugin/commands/plan.md", + }, + ], + skills: [], + hooks: undefined, + mcpServers: undefined, + } + + const bundle = convertClaudeToPi(plugin, { + agentMode: "subagent", + inferTemperature: false, + permissions: "none", + }) + + const parsedPrompt = parseFrontmatter(bundle.prompts[0].content) + expect(parsedPrompt.body).toContain('Run subagent with agent="repo-research-analyst" and task="feature_description".') + expect(parsedPrompt.body).toContain('Run subagent with agent="security-reviewer" and task="code_diff".') + expect(parsedPrompt.body).not.toContain("compound-engineering:") + }) + + test("transforms zero-argument Task calls", () => { + const plugin: ClaudePlugin = { + root: "/tmp/plugin", + manifest: { name: "fixture", version: "1.0.0" }, + agents: [], + commands: [ + { + name: "review", + description: "Review code", + body: "- Task compound-engineering:review:code-simplicity-reviewer()", + sourcePath: "/tmp/plugin/commands/review.md", + }, + ], + skills: [], + hooks: undefined, + mcpServers: undefined, + } + + const bundle = convertClaudeToPi(plugin, { + agentMode: "subagent", + inferTemperature: false, + permissions: "none", + }) + + const parsedPrompt = parseFrontmatter(bundle.prompts[0].content) + expect(parsedPrompt.body).toContain('Run subagent with agent="code-simplicity-reviewer".') + expect(parsedPrompt.body).not.toContain("compound-engineering:") + expect(parsedPrompt.body).not.toContain("()") + }) + test("appends MCPorter compatibility note when command references MCP", () => { const plugin: ClaudePlugin = { root: "/tmp/plugin", diff --git a/tests/pi-writer.test.ts b/tests/pi-writer.test.ts index 5af7ea6..eec28d9 100644 --- a/tests/pi-writer.test.ts +++ b/tests/pi-writer.test.ts @@ -50,6 +50,46 @@ describe("writePiBundle", () => { expect(agentsContent).toContain("MCPorter") }) + test("transforms Task calls in copied SKILL.md files", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-skill-transform-")) + const outputRoot = path.join(tempRoot, ".pi") + const sourceSkillDir = path.join(tempRoot, "source-skill") + await fs.mkdir(sourceSkillDir, { recursive: true }) + await fs.writeFile( + path.join(sourceSkillDir, "SKILL.md"), + `--- +name: ce:plan +description: Planning workflow +--- + +Run these research agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:research:learnings-researcher(feature_description) +- Task compound-engineering:review:code-simplicity-reviewer() +`, + ) + + const bundle: PiBundle = { + prompts: [], + skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }], + generatedSkills: [], + extensions: [], + } + + await writePiBundle(outputRoot, bundle) + + const installedSkill = await fs.readFile( + path.join(outputRoot, "skills", "ce:plan", "SKILL.md"), + "utf8", + ) + + expect(installedSkill).toContain('Run subagent with agent="repo-research-analyst" and task="feature_description".') + expect(installedSkill).toContain('Run subagent with agent="learnings-researcher" and task="feature_description".') + expect(installedSkill).toContain('Run subagent with agent="code-simplicity-reviewer".') + expect(installedSkill).not.toContain("Task compound-engineering:") + }) + test("writes to ~/.pi/agent style roots without nesting under .pi", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-agent-root-")) const outputRoot = path.join(tempRoot, "agent") diff --git a/tests/windsurf-converter.test.ts b/tests/windsurf-converter.test.ts index 4264a17..5f76a25 100644 --- a/tests/windsurf-converter.test.ts +++ b/tests/windsurf-converter.test.ts @@ -508,6 +508,27 @@ Task best-practices-researcher(topic)` expect(result).not.toContain("Task repo-research-analyst") }) + test("transforms namespaced Task agent calls using final segment", () => { + const input = `Run agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:review:security-reviewer(code_diff)` + + const result = transformContentForWindsurf(input) + expect(result).toContain("Use the @repo-research-analyst skill: feature_description") + expect(result).toContain("Use the @security-reviewer skill: code_diff") + expect(result).not.toContain("compound-engineering:") + }) + + test("transforms zero-argument Task calls", () => { + const input = `- Task compound-engineering:review:code-simplicity-reviewer()` + + const result = transformContentForWindsurf(input) + expect(result).toContain("Use the @code-simplicity-reviewer skill") + expect(result).not.toContain("compound-engineering:") + expect(result).not.toContain("code-simplicity-reviewer skill:") + }) + test("keeps @agent references as-is for known agents (Windsurf skill invocation syntax)", () => { const result = transformContentForWindsurf("Ask @security-sentinel for a review.", ["security-sentinel"]) expect(result).toContain("@security-sentinel") diff --git a/tests/windsurf-writer.test.ts b/tests/windsurf-writer.test.ts index 9d1129c..fdeb9a7 100644 --- a/tests/windsurf-writer.test.ts +++ b/tests/windsurf-writer.test.ts @@ -85,6 +85,43 @@ describe("writeWindsurfBundle", () => { expect(mcpContent.mcpServers.local).toEqual({ command: "echo", args: ["hello"] }) }) + test("transforms Task calls in copied SKILL.md files", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-skill-transform-")) + const sourceSkillDir = path.join(tempRoot, "source-skill") + await fs.mkdir(sourceSkillDir, { recursive: true }) + await fs.writeFile( + path.join(sourceSkillDir, "SKILL.md"), + `--- +name: ce:plan +description: Planning workflow +--- + +Run these research agents: + +- Task compound-engineering:research:repo-research-analyst(feature_description) +- Task compound-engineering:research:learnings-researcher(feature_description) +- Task compound-engineering:review:code-simplicity-reviewer() +`, + ) + + const bundle: WindsurfBundle = { + ...emptyBundle, + skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }], + } + + await writeWindsurfBundle(tempRoot, bundle) + + const installedSkill = await fs.readFile( + path.join(tempRoot, "skills", "ce:plan", "SKILL.md"), + "utf8", + ) + + expect(installedSkill).toContain("Use the @repo-research-analyst skill: feature_description") + expect(installedSkill).toContain("Use the @learnings-researcher skill: feature_description") + expect(installedSkill).toContain("Use the @code-simplicity-reviewer skill") + expect(installedSkill).not.toContain("Task compound-engineering:") + }) + test("writes directly into outputRoot without nesting", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-direct-")) const bundle: WindsurfBundle = {