From d80367037ce98f882bf1ef9bc637e9c1796ab456 Mon Sep 17 00:00:00 2001 From: pi Date: Sun, 12 Apr 2026 07:00:10 +0100 Subject: [PATCH] refactor: switch pi-subagents to generic subagents --- README.md | 10 +++- prompts/implement-and-review.md | 8 +-- prompts/implement.md | 10 ++-- prompts/scout-and-plan.md | 6 +-- src/agents.test.ts | 54 ------------------- src/agents.ts | 91 --------------------------------- src/builtin-agents.ts | 43 ---------------- src/monitor.test.ts | 2 +- src/package-manifest.test.ts | 3 ++ src/prompts.test.ts | 1 + src/runner.test.ts | 2 - 11 files changed, 26 insertions(+), 204 deletions(-) delete mode 100644 src/agents.test.ts delete mode 100644 src/agents.ts delete mode 100644 src/builtin-agents.ts diff --git a/README.md b/README.md index d8f5673..99de13a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pi-subagents -`pi-subagents` is a Pi extension package that runs subagent tasks in separate child sessions and ships the prompts and wrapper code needed to execute those runs. +`pi-subagents` is a Pi extension package that runs generic subagent tasks in separate child sessions and ships workflow prompt templates plus the wrapper code needed to execute those runs. ## Install @@ -26,6 +26,14 @@ pi install https://gitea.rwiesner.com/pi/pi-subagents - default: background child process runner - optional tmux runner: set `{"runner":"tmux"}` in `.pi/subagents.json` or `~/.pi/agent/subagents.json` +## What a subagent run gets + +- delegated task text +- selected model +- optional working directory + +Child runs are normal Pi sessions. This package does not add built-in role behavior, markdown-discovered subagents, per-agent tool restrictions, or appended role prompts. + ## Requirements - default process runner: no tmux requirement diff --git a/prompts/implement-and-review.md b/prompts/implement-and-review.md index e9d7854..478d88d 100644 --- a/prompts/implement-and-review.md +++ b/prompts/implement-and-review.md @@ -1,10 +1,10 @@ --- -description: Implement, review, then revise using subagents +description: Implement, review, then revise using generic subagents --- Use the `subagent` tool in chain mode: -1. `worker` to implement: $@ -2. `reviewer` to review `{previous}` and identify issues -3. `worker` to revise the implementation using `{previous}` +1. Run a generic subagent to implement: $@ +2. Run a generic subagent to review `{previous}` and identify issues +3. Run a generic subagent to revise the implementation using `{previous}` User request: $@ diff --git a/prompts/implement.md b/prompts/implement.md index 04b5238..115aa43 100644 --- a/prompts/implement.md +++ b/prompts/implement.md @@ -1,10 +1,10 @@ --- -description: Scout, plan, and implement using subagents +description: Inspect, plan, and implement using generic subagents --- -Use the `subagent` tool to handle this request in three stages: +Use the `subagent` tool in chain mode: -1. Run `scout` to inspect the codebase for: $@ -2. Run `planner` in chain mode, using `{previous}` from the scout output to produce a concrete implementation plan -3. Run `worker` in chain mode, using `{previous}` from the planner output to implement the approved plan +1. Run a generic subagent to inspect the codebase for: $@ +2. Run a generic subagent to turn `{previous}` into a concrete implementation plan for: $@ +3. Run a generic subagent to implement the approved plan using `{previous}` User request: $@ diff --git a/prompts/scout-and-plan.md b/prompts/scout-and-plan.md index 7e14e39..e6f3533 100644 --- a/prompts/scout-and-plan.md +++ b/prompts/scout-and-plan.md @@ -1,9 +1,9 @@ --- -description: Scout the codebase, then produce a plan using subagents +description: Inspect the codebase, then produce a plan using generic subagents --- Use the `subagent` tool in chain mode: -1. `scout` to inspect the codebase for: $@ -2. `planner` to turn `{previous}` into an implementation plan +1. Run a generic subagent to inspect the codebase for: $@ +2. Run a generic subagent to turn `{previous}` into a concrete implementation plan for: $@ User request: $@ diff --git a/src/agents.test.ts b/src/agents.test.ts deleted file mode 100644 index eea5079..0000000 --- a/src/agents.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdir, writeFile, mkdtemp } from "node:fs/promises"; -import { join } from "node:path"; -import { tmpdir } from "node:os"; -import { BUILTIN_AGENTS } from "./builtin-agents.ts"; -import { discoverAgents } from "./agents.ts"; - -test("discoverAgents returns built-ins and lets user markdown override by name", async () => { - const root = await mkdtemp(join(tmpdir(), "pi-subagents-agents-")); - const agentDir = join(root, "agent-home"); - const userAgentsDir = join(agentDir, "agents"); - await mkdir(userAgentsDir, { recursive: true }); - await writeFile( - join(userAgentsDir, "scout.md"), - `---\nname: scout\ndescription: User scout\nmodel: openai/gpt-5\n---\nUser override prompt`, - "utf8", - ); - - const result = discoverAgents(join(root, "repo"), { - scope: "user", - agentDir, - builtins: BUILTIN_AGENTS, - }); - - const scout = result.agents.find((agent) => agent.name === "scout"); - assert.equal(scout?.source, "user"); - assert.equal(scout?.description, "User scout"); - assert.equal(scout?.model, "openai/gpt-5"); -}); - -test("discoverAgents lets project agents override user agents when scope is both", async () => { - const root = await mkdtemp(join(tmpdir(), "pi-subagents-agents-")); - const repo = join(root, "repo"); - const agentDir = join(root, "agent-home"); - const userAgentsDir = join(agentDir, "agents"); - const projectAgentsDir = join(repo, ".pi", "agents"); - await mkdir(userAgentsDir, { recursive: true }); - await mkdir(projectAgentsDir, { recursive: true }); - - await writeFile(join(userAgentsDir, "worker.md"), `---\nname: worker\ndescription: User worker\n---\nUser worker`, "utf8"); - await writeFile(join(projectAgentsDir, "worker.md"), `---\nname: worker\ndescription: Project worker\n---\nProject worker`, "utf8"); - - const result = discoverAgents(repo, { - scope: "both", - agentDir, - builtins: BUILTIN_AGENTS, - }); - - const worker = result.agents.find((agent) => agent.name === "worker"); - assert.equal(worker?.source, "project"); - assert.equal(worker?.description, "Project worker"); - assert.equal(result.projectAgentsDir, projectAgentsDir); -}); diff --git a/src/agents.ts b/src/agents.ts deleted file mode 100644 index ce4a5fa..0000000 --- a/src/agents.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { existsSync, readdirSync, readFileSync, statSync, type Dirent } from "node:fs"; -import { dirname, join } from "node:path"; -import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent"; -import { BUILTIN_AGENTS, type AgentDefinition } from "./builtin-agents.ts"; -import type { AgentScope } from "./schema.ts"; - -export interface AgentDiscoveryOptions { - scope?: AgentScope; - agentDir?: string; - builtins?: AgentDefinition[]; -} - -export interface AgentDiscoveryResult { - agents: AgentDefinition[]; - projectAgentsDir: string | null; -} - -function loadMarkdownAgents(dir: string, source: "user" | "project"): AgentDefinition[] { - if (!existsSync(dir)) return []; - - let entries: Dirent[]; - try { - entries = readdirSync(dir, { withFileTypes: true }); - } catch { - return []; - } - - const agents: AgentDefinition[] = []; - for (const entry of entries) { - if (!entry.name.endsWith(".md")) continue; - if (!entry.isFile() && !entry.isSymbolicLink()) continue; - - const filePath = join(dir, entry.name); - const content = readFileSync(filePath, "utf8"); - const { frontmatter, body } = parseFrontmatter>(content); - if (!frontmatter.name || !frontmatter.description) continue; - - agents.push({ - name: frontmatter.name, - description: frontmatter.description, - tools: frontmatter.tools?.split(",").map((tool) => tool.trim()).filter(Boolean), - model: frontmatter.model, - systemPrompt: body.trim(), - source, - filePath, - }); - } - - return agents; -} - -function findNearestProjectAgentsDir(cwd: string): string | null { - let current = cwd; - while (true) { - const candidate = join(current, ".pi", "agents"); - try { - if (statSync(candidate).isDirectory()) return candidate; - } catch {} - - const parent = dirname(current); - if (parent === current) return null; - current = parent; - } -} - -export function discoverAgents(cwd: string, options: AgentDiscoveryOptions = {}): AgentDiscoveryResult { - const scope = options.scope ?? "user"; - const builtins = options.builtins ?? BUILTIN_AGENTS; - const userAgentDir = join(options.agentDir ?? getAgentDir(), "agents"); - const projectAgentsDir = findNearestProjectAgentsDir(cwd); - - const sources = new Map(); - for (const agent of builtins) sources.set(agent.name, agent); - - if (scope !== "project") { - for (const agent of loadMarkdownAgents(userAgentDir, "user")) { - sources.set(agent.name, agent); - } - } - - if (scope !== "user" && projectAgentsDir) { - for (const agent of loadMarkdownAgents(projectAgentsDir, "project")) { - sources.set(agent.name, agent); - } - } - - return { - agents: [...sources.values()], - projectAgentsDir, - }; -} diff --git a/src/builtin-agents.ts b/src/builtin-agents.ts deleted file mode 100644 index 1848d90..0000000 --- a/src/builtin-agents.ts +++ /dev/null @@ -1,43 +0,0 @@ -export interface AgentDefinition { - name: string; - description: string; - tools?: string[]; - model?: string; - systemPrompt: string; - source: "builtin" | "user" | "project"; - filePath?: string; -} - -export const BUILTIN_AGENTS: AgentDefinition[] = [ - { - name: "scout", - description: "Fast codebase recon and compressed context gathering", - tools: ["read", "grep", "find", "ls", "bash"], - model: "claude-haiku-4-5", - systemPrompt: "You are a scout. Explore quickly, summarize clearly, and avoid implementation.", - source: "builtin", - }, - { - name: "planner", - description: "Turns exploration into implementation plans", - tools: ["read", "grep", "find", "ls"], - model: "claude-sonnet-4-5", - systemPrompt: "You are a planner. Produce implementation plans, file lists, and risks.", - source: "builtin", - }, - { - name: "reviewer", - description: "Reviews code and identifies correctness and quality issues", - tools: ["read", "grep", "find", "ls", "bash"], - model: "claude-sonnet-4-5", - systemPrompt: "You are a reviewer. Inspect code critically and report concrete issues.", - source: "builtin", - }, - { - name: "worker", - description: "General-purpose implementation agent", - model: "claude-sonnet-4-5", - systemPrompt: "You are a worker. Execute the delegated task completely and report final results clearly.", - source: "builtin", - }, -]; diff --git a/src/monitor.test.ts b/src/monitor.test.ts index bebe45c..bd16bf4 100644 --- a/src/monitor.test.ts +++ b/src/monitor.test.ts @@ -23,7 +23,7 @@ test("monitorRun streams normalized events and resolves when result.json appears await appendFile(eventsPath, `${JSON.stringify({ type: "tool_call", toolName: "read", args: { path: "a.ts" } })}\n`, "utf8"); await writeFile( resultPath, - JSON.stringify({ runId: "run-1", exitCode: 0, finalText: "done", agent: "scout", task: "inspect auth" }, null, 2), + JSON.stringify({ runId: "run-1", exitCode: 0, finalText: "done", task: "inspect auth" }, null, 2), "utf8", ); diff --git a/src/package-manifest.test.ts b/src/package-manifest.test.ts index 60683ac..d966b5e 100644 --- a/src/package-manifest.test.ts +++ b/src/package-manifest.test.ts @@ -48,6 +48,9 @@ test("README documents local install, git install, and tmux PATH requirement", ( const readme = readFileSync(resolve(packageRoot, "README.md"), "utf8"); assert.match(readme, /pi install \/absolute\/path\/to\/subagents/); assert.match(readme, /pi install https:\/\/gitea\.rwiesner\.com\/pi\/pi-subagents/); + assert.match(readme, /generic subagent/i); + assert.doesNotMatch(readme, /specialized built-in roles/i); + assert.doesNotMatch(readme, /markdown agent discovery/i); assert.match(readme, /tmux.*PATH/i); }); diff --git a/src/prompts.test.ts b/src/prompts.test.ts index 9bc0f02..d913b33 100644 --- a/src/prompts.test.ts +++ b/src/prompts.test.ts @@ -16,5 +16,6 @@ test("package.json exposes the extension and workflow prompt templates", () => { const content = readFileSync(join(packageRoot, "prompts", name), "utf8"); assert.match(content, /^---\ndescription:/m); assert.match(content, /subagent/); + assert.doesNotMatch(content, /\b(?:scout|planner|reviewer|worker)\b/); } }); diff --git a/src/runner.test.ts b/src/runner.test.ts index d23780f..cb027ef 100644 --- a/src/runner.test.ts +++ b/src/runner.test.ts @@ -5,8 +5,6 @@ import { createConfiguredRunSingleTask } from "./runner.ts"; function makeResult(finalText: string) { return { runId: "run-1", - agent: "scout", - agentSource: "builtin" as const, task: "inspect auth", exitCode: 0, finalText,