feat: fix skill transformation pipeline across all targets (#334)

This commit is contained in:
Trevin Chow
2026-03-21 19:45:20 -07:00
committed by GitHub
parent 0f6448d81c
commit 4087e1df82
33 changed files with 624 additions and 86 deletions

View File

@@ -9,7 +9,7 @@ model: inherit
Context: User wants to understand a new repository's structure and conventions before contributing. 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" 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." assistant: "I'll use the repo-research-analyst agent to conduct a thorough analysis of the repository structure and patterns."
<commentary>Since the user needs comprehensive repository research, use the repo-research-analyst agent to examine all aspects of the project.</commentary> <commentary>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.</commentary>
</example> </example>
<example> <example>
Context: User is preparing to create a GitHub issue and wants to follow project conventions. 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." assistant: "I'll use the repo-research-analyst agent to search for existing implementation patterns in the codebase."
<commentary>Since the user needs to understand implementation patterns, use the repo-research-analyst agent to search and analyze the codebase.</commentary> <commentary>Since the user needs to understand implementation patterns, use the repo-research-analyst agent to search and analyze the codebase.</commentary>
</example> </example>
<example>
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."
<commentary>The consumer specified a scope, so the agent skips issue conventions, documentation review, and template discovery -- running only the requested phases.</commentary>
</example>
</examples> </examples>
**Note: The current year is 2026.** Use this when searching for recent documentation and patterns. **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. 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)** **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. Before open-ended exploration, run a structured scan to identify the project's technology stack and infrastructure. This grounds all subsequent research.

View File

@@ -145,12 +145,13 @@ Prepare a concise planning context summary (a paragraph or two) to pass as input
Run these agents in parallel: 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) - Task compound-engineering:research:learnings-researcher(planning context summary)
Collect: Collect:
- Existing patterns and conventions to follow - Technology stack and versions (used in section 1.2 to make sharper external research decisions)
- Relevant files, modules, and tests - 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 - AGENTS.md guidance that materially affects the plan, with CLAUDE.md used only as compatibility fallback when present
- Institutional learnings from `docs/solutions/` - Institutional learnings from `docs/solutions/`

View File

@@ -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: 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) - Task compound-engineering:research:learnings-researcher(feature_description)
**What to look for:** **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) - **Learnings:** documented solutions in `docs/solutions/` that might apply (gotchas, patterns, lessons learned)
These findings inform the next step. These findings inform the next step.

View File

@@ -209,7 +209,7 @@ Use fully-qualified agent names inside Task calls.
**Requirements Trace / Open Questions classification** **Requirements Trace / Open Questions classification**
- `compound-engineering:workflow:spec-flow-analyzer` for missing user flows, edge cases, and handoff gaps - `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** **Context & Research / Sources & References gaps**
- `compound-engineering:research:learnings-researcher` for institutional knowledge and past solved problems - `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** **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: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 - 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** **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 - `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 - 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 #### 3.2 Agent Prompt Shape
For each selected section, pass: 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 - A short plan summary
- The exact section text - The exact section text
- Why the section was selected, including which checklist triggers fired - Why the section was selected, including which checklist triggers fired

View File

@@ -106,11 +106,15 @@ function convertCommandToSkill(
export function transformContentForCopilot(body: string): string { export function transformContentForCopilot(body: string): string {
let result = body let result = body
// 1. Transform Task agent calls // 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 const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
const skillName = normalizeName(agentName) const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName
return `${prefix}Use the ${skillName} skill to: ${args.trim()}` 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) // 2. Transform slash command references (replace colons with hyphens)

View File

@@ -119,15 +119,19 @@ function mapAgentTools(agent: ClaudeAgent): string[] | undefined {
* 2. Task agent calls: Task agent-name(args) → Task agent-name: args * 2. Task agent calls: Task agent-name(args) → Task agent-name: args
* 3. Agent references: @agent-name → the agent-name droid * 3. Agent references: @agent-name → the agent-name droid
*/ */
function transformContentForDroid(body: string): string { export function transformContentForDroid(body: string): string {
let result = body let result = body
// 1. Transform Task agent calls // 1. Transform Task agent calls
// Match: Task repo-research-analyst(feature_description) // 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 const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
const name = normalizeName(agentName) const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName
return `${prefix}Task ${name}: ${args.trim()}` const name = normalizeName(finalSegment)
const trimmedArgs = args.trim()
return trimmedArgs
? `${prefix}Task ${name}: ${trimmedArgs}`
: `${prefix}Task ${name}`
}) })
// 2. Transform slash command references // 2. Transform slash command references

View File

@@ -86,11 +86,15 @@ function convertCommand(command: ClaudeCommand, usedNames: Set<string>): GeminiC
export function transformContentForGemini(body: string): string { export function transformContentForGemini(body: string): string {
let result = body let result = body
// 1. Transform Task agent calls // 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 const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
const skillName = normalizeName(agentName) const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName
return `${prefix}Use the ${skillName} skill to: ${args.trim()}` 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/ // 2. Rewrite .claude/ paths to .gemini/

View File

@@ -135,10 +135,15 @@ function convertCommandToSkill(
export function transformContentForKiro(body: string, knownAgentNames: string[] = []): string { export function transformContentForKiro(body: string, knownAgentNames: string[] = []): string {
let result = body let result = body
// 1. Transform Task agent calls // 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 const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]*)\)/gm
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => { 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) // 2. Rewrite .claude/ paths to .kiro/ (with word-boundary-like lookbehind)

View File

@@ -90,16 +90,19 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): PiGeneratedSk
} }
} }
function transformContentForPi(body: string): string { export function transformContentForPi(body: string): string {
let result = body 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" // -> 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) => { 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, " ") 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 // Claude-specific tool references

View File

@@ -122,10 +122,15 @@ export function transformContentForWindsurf(body: string, knownAgentNames: strin
// In Windsurf, @skill-name is the native invocation syntax for skills. // In Windsurf, @skill-name is the native invocation syntax for skills.
// Since agents are now mapped to skills, @agent-name already works correctly. // Since agents are now mapped to skills, @agent-name already works correctly.
// 4. Transform Task agent calls to skill references // 4. Transform Task agent calls to skill references (supports namespaced names)
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) => { 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 return result

View File

@@ -1,6 +1,5 @@
import { promises as fs } from "fs"
import path from "path" 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 { CodexBundle } from "../types/codex"
import type { ClaudeMcpServer } from "../types/claude" import type { ClaudeMcpServer } from "../types/claude"
import { transformContentForCodex } from "../utils/codex-content" import { transformContentForCodex } from "../utils/codex-content"
@@ -19,10 +18,12 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle):
if (bundle.skillDirs.length > 0) { if (bundle.skillDirs.length > 0) {
const skillsRoot = path.join(codexRoot, "skills") const skillsRoot = path.join(codexRoot, "skills")
for (const skill of bundle.skillDirs) { for (const skill of bundle.skillDirs) {
await copyCodexSkillDir( await copySkillDir(
skill.sourceDir, skill.sourceDir,
path.join(skillsRoot, skill.name), 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<void> {
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 { function resolveCodexRoot(outputRoot: string): string {
return path.basename(outputRoot) === ".codex" ? outputRoot : path.join(outputRoot, ".codex") return path.basename(outputRoot) === ".codex" ? outputRoot : path.join(outputRoot, ".codex")
} }

View File

@@ -1,5 +1,6 @@
import path from "path" 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" import type { CopilotBundle } from "../types/copilot"
export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBundle): Promise<void> { export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBundle): Promise<void> {
@@ -23,7 +24,7 @@ export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBund
if (bundle.skillDirs.length > 0) { if (bundle.skillDirs.length > 0) {
const skillsDir = path.join(paths.githubDir, "skills") const skillsDir = path.join(paths.githubDir, "skills")
for (const skill of bundle.skillDirs) { 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)
} }
} }

View File

@@ -1,5 +1,6 @@
import path from "path" 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" import type { DroidBundle } from "../types/droid"
export async function writeDroidBundle(outputRoot: string, bundle: DroidBundle): Promise<void> { export async function writeDroidBundle(outputRoot: string, bundle: DroidBundle): Promise<void> {
@@ -24,7 +25,7 @@ export async function writeDroidBundle(outputRoot: string, bundle: DroidBundle):
if (bundle.skillDirs.length > 0) { if (bundle.skillDirs.length > 0) {
await ensureDir(paths.skillsDir) await ensureDir(paths.skillsDir)
for (const skill of bundle.skillDirs) { 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)
} }
} }
} }

View File

@@ -1,5 +1,6 @@
import path from "path" 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" import type { GeminiBundle } from "../types/gemini"
export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle): Promise<void> { export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle): Promise<void> {
@@ -14,7 +15,7 @@ export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle
if (bundle.skillDirs.length > 0) { if (bundle.skillDirs.length > 0) {
for (const skill of bundle.skillDirs) { 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)
} }
} }

View File

@@ -1,5 +1,6 @@
import path from "path" 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" import type { KiroBundle } from "../types/kiro"
export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): Promise<void> { export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): Promise<void> {
@@ -50,7 +51,10 @@ export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): P
continue continue
} }
await copyDir(skill.sourceDir, destDir) const knownAgentNames = bundle.agents.map((a) => a.name)
await copySkillDir(skill.sourceDir, destDir, (content) =>
transformContentForKiro(content, knownAgentNames),
)
} }
} }

View File

@@ -1,13 +1,14 @@
import path from "path" import path from "path"
import { import {
backupFile, backupFile,
copyDir, copySkillDir,
ensureDir, ensureDir,
pathExists, pathExists,
readText, readText,
writeJson, writeJson,
writeText, writeText,
} from "../utils/files" } from "../utils/files"
import { transformContentForPi } from "../converters/claude-to-pi"
import type { PiBundle } from "../types/pi" import type { PiBundle } from "../types/pi"
const PI_AGENTS_BLOCK_START = "<!-- BEGIN COMPOUND PI TOOL MAP -->" const PI_AGENTS_BLOCK_START = "<!-- BEGIN COMPOUND PI TOOL MAP -->"
@@ -37,7 +38,7 @@ export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promi
} }
for (const skill of bundle.skillDirs) { 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) { for (const skill of bundle.generatedSkills) {

View File

@@ -1,6 +1,7 @@
import path from "path" 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 { formatFrontmatter } from "../utils/frontmatter"
import { transformContentForWindsurf } from "../converters/claude-to-windsurf"
import type { WindsurfBundle } from "../types/windsurf" import type { WindsurfBundle } from "../types/windsurf"
import type { TargetScope } from "./index" import type { TargetScope } from "./index"
@@ -58,7 +59,10 @@ export async function writeWindsurfBundle(outputRoot: string, bundle: WindsurfBu
continue continue
} }
await copyDir(skill.sourceDir, destDir) const knownAgentNames = bundle.agentSkills.map((s) => s.name)
await copySkillDir(skill.sourceDir, destDir, (content) =>
transformContentForWindsurf(content, knownAgentNames),
)
} }
} }

View File

@@ -29,14 +29,16 @@ export function transformContentForCodex(
const skillTargets = targets?.skillTargets ?? {} const skillTargets = targets?.skillTargets ?? {}
const unknownSlashBehavior = options.unknownSlashBehavior ?? "prompt" 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) => { result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
// For namespaced calls like "compound-engineering:research:repo-research-analyst", // For namespaced calls like "compound-engineering:research:repo-research-analyst",
// use only the final segment as the skill name. // use only the final segment as the skill name.
const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName
const skillName = normalizeCodexName(finalSegment) const skillName = normalizeCodexName(finalSegment)
const trimmedArgs = args.trim() 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 = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi const slashCommandPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi

View File

@@ -104,3 +104,34 @@ export async function copyDir(sourceDir: string, targetDir: string): Promise<voi
} }
} }
} }
/**
* Copy a skill directory, optionally transforming SKILL.md content.
* All other files are copied verbatim. Used by target writers to apply
* platform-specific content transforms to pass-through skills.
*/
export async function copySkillDir(
sourceDir: string,
targetDir: string,
transformSkillContent?: (content: string) => string,
): Promise<void> {
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)
}
}
}
}

View File

@@ -248,6 +248,35 @@ Task compound-engineering:review:security-reviewer(code_diff)`,
expect(parsed.body).not.toContain("Task compound-engineering:") 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", () => { test("transforms slash commands to prompts syntax", () => {
const plugin: ClaudePlugin = { const plugin: ClaudePlugin = {
...fixturePlugin, ...fixturePlugin,

View File

@@ -177,6 +177,7 @@ Run these research agents:
Also run bare agents: Also run bare agents:
- Task best-practices-researcher(topic) - 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 // Bare Task calls should still be rewritten
expect(installedSkill).toContain("Use the $best-practices-researcher skill to: topic") expect(installedSkill).toContain("Use the $best-practices-researcher skill to: topic")
expect(installedSkill).not.toContain("Task best-practices-researcher") 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 () => { test("preserves unknown slash text in copied SKILL.md files", async () => {

View File

@@ -444,6 +444,27 @@ Task best-practices-researcher(topic)`
expect(result).not.toContain("Task repo-research-analyst(") 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", () => { test("replaces colons with hyphens in slash commands", () => {
const input = `1. Run /deepen-plan to enhance const input = `1. Run /deepen-plan to enhance
2. Start /workflows:work to implement 2. Start /workflows:work to implement

View File

@@ -165,6 +165,44 @@ describe("writeCopilotBundle", () => {
expect(backupFiles.length).toBeGreaterThanOrEqual(1) 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 () => { test("creates skill directories with SKILL.md", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-genskill-")) const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-genskill-"))
const bundle: CopilotBundle = { const bundle: CopilotBundle = {

View File

@@ -148,6 +148,63 @@ Task best-practices-researcher(topic)`,
expect(parsed.body).not.toContain("Task repo-research-analyst(") 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", () => { test("transforms slash commands by flattening namespaces", () => {
const plugin: ClaudePlugin = { const plugin: ClaudePlugin = {
...fixturePlugin, ...fixturePlugin,

View File

@@ -47,6 +47,44 @@ describe("writeDroidBundle", () => {
expect(droidContent).toContain("Droid content") 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 () => { test("writes directly into a .factory output root", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "droid-home-")) const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "droid-home-"))
const factoryRoot = path.join(tempRoot, ".factory") const factoryRoot = path.join(tempRoot, ".factory")

View File

@@ -338,6 +338,27 @@ Task best-practices-researcher(topic)`
expect(result).not.toContain("Task repo-research-analyst") 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", () => { test("transforms @agent references to skill references", () => {
const result = transformContentForGemini("Ask @security-sentinel for a review.") const result = transformContentForGemini("Ask @security-sentinel for a review.")
expect(result).toContain("the security-sentinel skill") expect(result).toContain("the security-sentinel skill")

View File

@@ -66,6 +66,44 @@ describe("writeGeminiBundle", () => {
expect(settingsContent.mcpServers.playwright.command).toBe("npx") 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 () => { test("namespaced commands create subdirectories", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "gemini-ns-")) const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "gemini-ns-"))
const bundle: GeminiBundle = { const bundle: GeminiBundle = {

View File

@@ -391,6 +391,27 @@ Task best-practices-researcher(topic)`
expect(result).not.toContain("Task repo-research-analyst") 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", () => { test("transforms @agent references for known agents only", () => {
const result = transformContentForKiro("Ask @security-sentinel for a review.", ["security-sentinel"]) const result = transformContentForKiro("Ask @security-sentinel for a review.", ["security-sentinel"])
expect(result).toContain("the security-sentinel agent") expect(result).toContain("the security-sentinel agent")

View File

@@ -99,6 +99,43 @@ describe("writeKiroBundle", () => {
expect(mcpContent.mcpServers.playwright.command).toBe("npx") 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 () => { test("does not double-nest when output root is .kiro", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kiro-home-")) const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kiro-home-"))
const kiroRoot = path.join(tempRoot, ".kiro") const kiroRoot = path.join(tempRoot, ".kiro")

View File

@@ -85,6 +85,70 @@ describe("convertClaudeToPi", () => {
expect(parsedPrompt.body).toContain("file-based todos (todos/ + /skill:file-todos)") 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", () => { test("appends MCPorter compatibility note when command references MCP", () => {
const plugin: ClaudePlugin = { const plugin: ClaudePlugin = {
root: "/tmp/plugin", root: "/tmp/plugin",

View File

@@ -50,6 +50,46 @@ describe("writePiBundle", () => {
expect(agentsContent).toContain("MCPorter") 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 () => { 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 tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-agent-root-"))
const outputRoot = path.join(tempRoot, "agent") const outputRoot = path.join(tempRoot, "agent")

View File

@@ -508,6 +508,27 @@ Task best-practices-researcher(topic)`
expect(result).not.toContain("Task repo-research-analyst") 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)", () => { test("keeps @agent references as-is for known agents (Windsurf skill invocation syntax)", () => {
const result = transformContentForWindsurf("Ask @security-sentinel for a review.", ["security-sentinel"]) const result = transformContentForWindsurf("Ask @security-sentinel for a review.", ["security-sentinel"])
expect(result).toContain("@security-sentinel") expect(result).toContain("@security-sentinel")

View File

@@ -85,6 +85,43 @@ describe("writeWindsurfBundle", () => {
expect(mcpContent.mcpServers.local).toEqual({ command: "echo", args: ["hello"] }) 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 () => { test("writes directly into outputRoot without nesting", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-direct-")) const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-direct-"))
const bundle: WindsurfBundle = { const bundle: WindsurfBundle = {