feat(pi): first-class support via pi-subagents + pi-ask-user (#651)
Some checks failed
CI / pr-title (push) Has been cancelled
CI / test (push) Has been cancelled
Release PR / release-pr (push) Has been cancelled
Release PR / publish-cli (push) Has been cancelled

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trevin Chow
2026-04-22 10:26:29 -07:00
committed by GitHub
parent cce95fb814
commit 7ddfbed33b
53 changed files with 371 additions and 636 deletions

View File

@@ -8,7 +8,7 @@ import type { ClaudePlugin } from "../src/types/claude"
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
describe("convertClaudeToPi", () => {
test("converts commands, skills, extensions, and MCPorter config", async () => {
test("converts commands, skills, agents, and MCP servers without shipping a Pi extension", async () => {
const plugin = await loadClaudePlugin(fixtureRoot)
const bundle = convertClaudeToPi(plugin, {
agentMode: "subagent",
@@ -28,22 +28,61 @@ describe("convertClaudeToPi", () => {
const parsedPrompt = parseFrontmatter(workflowsReview!.content)
expect(parsedPrompt.data.description).toBe("Run a multi-agent review workflow")
// Existing skills are copied and agents are converted into generated Pi skills
// Existing skills are copied as skill dirs; Claude agents are converted to
// Pi agent files (under bundle.agents, written to .pi/agents/<name>.md) so
// that nicobailon/pi-subagents' `subagent` tool can resolve them by name.
expect(bundle.skillDirs.some((skill) => skill.name === "skill-one")).toBe(true)
expect(bundle.generatedSkills.some((skill) => skill.name === "repo-research-analyst")).toBe(true)
expect(bundle.agents.some((agent) => agent.name === "repo-research-analyst")).toBe(true)
// Agents no longer leak into generatedSkills — that field is reserved for
// commands-as-skills on other targets; Pi keeps it empty.
expect(bundle.generatedSkills).toEqual([])
// Pi compatibility extension is included (with subagent + MCPorter tools)
const compatExtension = bundle.extensions.find((extension) => extension.name === "compound-engineering-compat.ts")
expect(compatExtension).toBeDefined()
expect(compatExtension!.content).toContain('name: "subagent"')
expect(compatExtension!.content).toContain('name: "mcporter_call"')
// Pi installs now depend on the community pi-subagents and pi-ask-user extensions,
// so the converter emits no bundled extension. Legacy cleanup in the Pi writer
// removes any prior compound-engineering-compat.ts on upgrade.
expect(bundle.extensions).toEqual([])
// Claude MCP config is translated to MCPorter config
expect(bundle.mcporterConfig?.mcpServers.context7?.baseUrl).toBe("https://mcp.context7.com/mcp")
expect(bundle.mcporterConfig?.mcpServers["local-tooling"]?.command).toBe("echo")
// MCP servers declared in plugin.json are translated to Pi's mcporter.json
// shape so plugins with MCP wiring keep their backends after conversion.
// The fixture declares both an HTTP url server (context7) and a stdio
// command server (local-tooling).
expect(bundle.mcporterConfig).toEqual({
mcpServers: {
context7: {
baseUrl: "https://mcp.context7.com/mcp",
headers: undefined,
},
"local-tooling": {
command: "echo",
args: ["fixture"],
env: undefined,
headers: undefined,
},
},
})
})
test("transforms Task calls, AskUserQuestion, slash commands, and todo tool references", () => {
test("omits mcporterConfig when the plugin declares no MCP servers", () => {
const plugin: ClaudePlugin = {
root: "/tmp/plugin",
manifest: { name: "fixture", version: "1.0.0" },
agents: [],
commands: [],
skills: [],
hooks: undefined,
mcpServers: undefined,
}
const bundle = convertClaudeToPi(plugin, {
agentMode: "subagent",
inferTemperature: false,
permissions: "none",
})
expect(bundle.mcporterConfig).toBeUndefined()
})
test("transforms Task calls, slash commands, and todo tool references; preserves AskUserQuestion", () => {
const plugin: ClaudePlugin = {
root: "/tmp/plugin",
manifest: { name: "fixture", version: "1.0.0" },
@@ -79,7 +118,10 @@ describe("convertClaudeToPi", () => {
expect(parsedPrompt.body).toContain("Run subagent with agent=\"repo-research-analyst\" and task=\"feature_description\".")
expect(parsedPrompt.body).toContain("Run subagent with agent=\"learnings-researcher\" and task=\"feature_description\".")
expect(parsedPrompt.body).toContain("ask_user_question")
// AskUserQuestion is preserved; skill source-side enumerations name each platform's
// blocking-question tool (including `ask_user` for Pi via pi-ask-user), so the
// converter no longer rewrites the token.
expect(parsedPrompt.body).toContain("AskUserQuestion")
expect(parsedPrompt.body).toContain("/workflows-work")
expect(parsedPrompt.body).toContain("/todo-resolve")
expect(parsedPrompt.body).toContain("the platform's task-tracking primitive")
@@ -184,32 +226,4 @@ describe("convertClaudeToPi", () => {
expect(parsedPrompt.body).not.toContain("()")
})
test("appends MCPorter compatibility note when command references MCP", () => {
const plugin: ClaudePlugin = {
root: "/tmp/plugin",
manifest: { name: "fixture", version: "1.0.0" },
agents: [],
commands: [
{
name: "docs",
description: "Read MCP docs",
body: "Use MCP servers for docs lookup.",
sourcePath: "/tmp/plugin/commands/docs.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("Pi + MCPorter note")
expect(parsedPrompt.body).toContain("mcporter_call")
})
})