refactor pi resource wiring by directory

This commit is contained in:
Rasyidan Akbar F. 2026-04-14 19:57:55 +07:00
commit b465e0aed0
19 changed files with 2087 additions and 0 deletions

View file

@ -0,0 +1,55 @@
# pi exa-tools scaffold
Global pi extension that adds Exa-backed retrieval tools.
## Tools
- `exa_search` — general web/docs/reference search
- `exa_code` — coding context and open-source implementation examples
## Mental model
- use `exa_search` for official docs, release notes, blog posts, issues, comparisons, and general web research
- use `exa_code` for library usage, framework patterns, API examples, and code-specific prior art
## Credentials
The extension looks for credentials in this order:
1. `EXA_API_KEY`
2. `~/.config/secrets/exa-api-key`
3. `~/.config/secrets/exa_api_key`
If none are found, the tools throw a helpful error.
Note: the Exa account also needs available credits. A valid key with no remaining credits will still fail at request time.
## Command
- `/exa-tools` — show credential status and available tools
## Installation
This repo deploys the extension declaratively through Home Manager to:
```text
~/.pi/agent/extensions/exa-tools
```
## Example usage
General web research:
```text
Use exa_search to find the official docs for Exa context search
```
Coding context:
```text
Use exa_code to find examples of React hook state management patterns
```
Subagent usage:
The `librarian` subagent can use these tools once the extension is globally installed.

View file

@ -0,0 +1,354 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import {
type ExtensionAPI,
DEFAULT_MAX_BYTES,
DEFAULT_MAX_LINES,
formatSize,
truncateHead,
} from "@mariozechner/pi-coding-agent";
import { StringEnum } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
const EXA_API_BASE = "https://api.exa.ai";
const DEFAULT_SEARCH_RESULTS = 5;
const MAX_SEARCH_RESULTS = 10;
const DEFAULT_HIGHLIGHT_CHARS = 1200;
const DEFAULT_CODE_TOKENS = 5000;
const MAX_CODE_TOKENS = 12000;
function clip(text: string, max = 400): string {
const normalized = text.replace(/\s+/g, " ").trim();
return normalized.length > max ? `${normalized.slice(0, max)}` : normalized;
}
function maybeString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function stringArray(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
}
function readSecretFile(secretPath: string): string | undefined {
try {
if (!fs.existsSync(secretPath)) return undefined;
const value = fs.readFileSync(secretPath, "utf8").trim();
return value || undefined;
} catch {
return undefined;
}
}
function resolveExaApiKey(): { key?: string; source: string } {
const envKey = process.env.EXA_API_KEY?.trim();
if (envKey) return { key: envKey, source: "EXA_API_KEY" };
const secretCandidates = [
path.join(os.homedir(), ".config", "secrets", "exa-api-key"),
path.join(os.homedir(), ".config", "secrets", "exa_api_key"),
];
for (const secretPath of secretCandidates) {
const value = readSecretFile(secretPath);
if (value) return { key: value, source: secretPath };
}
return { source: "missing" };
}
async function postExa(pathname: string, payload: Record<string, unknown>, signal?: AbortSignal): Promise<any> {
const auth = resolveExaApiKey();
if (!auth.key) {
throw new Error(
"Exa API key not found. Set EXA_API_KEY or create ~/.config/secrets/exa-api-key.",
);
}
const response = await fetch(`${EXA_API_BASE}${pathname}`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": auth.key,
},
body: JSON.stringify(payload),
signal,
});
const raw = await response.text();
let data: any;
try {
data = raw ? JSON.parse(raw) : {};
} catch {
data = { raw };
}
if (!response.ok) {
const detail = typeof data === "object" && data && maybeString(data.error) ? data.error : clip(raw, 600);
throw new Error(`Exa request failed (${response.status} ${response.statusText}): ${detail}`);
}
if (typeof data === "object" && data && maybeString(data.error)) {
const tag = maybeString(data.tag);
throw new Error(`Exa error${tag ? ` [${tag}]` : ""}: ${data.error}`);
}
return data;
}
async function writeTempOutput(prefix: string, content: string): Promise<string> {
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-exa-tools-"));
const filePath = path.join(dir, `${prefix}.txt`);
await fs.promises.writeFile(filePath, content, "utf8");
return filePath;
}
async function finalizeText(prefix: string, fullText: string): Promise<{ text: string; fullOutputPath?: string }> {
const truncation = truncateHead(fullText, {
maxLines: DEFAULT_MAX_LINES,
maxBytes: DEFAULT_MAX_BYTES,
});
if (!truncation.truncated) {
return { text: truncation.content };
}
const fullOutputPath = await writeTempOutput(prefix, fullText);
const notice = [
"",
`[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full output saved to: ${fullOutputPath}]`,
].join("\n");
return {
text: `${truncation.content}${notice}`,
fullOutputPath,
};
}
function formatSearchResult(result: any, index: number, includeText: boolean): string {
const title = maybeString(result?.title) ?? maybeString(result?.url) ?? `Result ${index + 1}`;
const lines = [`${index + 1}. ${title}`];
const url = maybeString(result?.url);
if (url) lines.push(` URL: ${url}`);
const published = maybeString(result?.publishedDate) ?? maybeString(result?.published_date);
if (published) lines.push(` Published: ${published}`);
const author = maybeString(result?.author);
if (author) lines.push(` Author: ${author}`);
const summary = maybeString(result?.summary);
if (summary) lines.push(` Summary: ${clip(summary, 500)}`);
const highlights = stringArray(result?.highlights);
if (highlights.length > 0) {
lines.push(" Highlights:");
for (const highlight of highlights.slice(0, 3)) {
lines.push(` - ${clip(highlight, 350)}`);
}
}
if (includeText) {
const text = maybeString(result?.text);
if (text) lines.push(` Text: ${clip(text, 700)}`);
}
return lines.join("\n");
}
async function buildSearchOutput(query: string, payload: Record<string, unknown>, data: any): Promise<{ text: string; fullOutputPath?: string }> {
const results = Array.isArray(data?.results) ? data.results : [];
const includeText = Boolean((payload.contents as any)?.text);
const lines: string[] = [
`Exa search results for: ${query}`,
`Results returned: ${results.length}`,
];
const requestId = maybeString(data?.requestId);
if (requestId) lines.push(`Request ID: ${requestId}`);
const costTotal = data?.costDollars?.total;
if (typeof costTotal === "number") lines.push(`Cost: $${costTotal}`);
lines.push("");
if (results.length === 0) {
lines.push("No results returned.");
} else {
for (const [index, result] of results.entries()) {
lines.push(formatSearchResult(result, index, includeText));
if (index < results.length - 1) lines.push("");
}
}
return finalizeText("exa-search", lines.join("\n"));
}
async function buildCodeOutput(query: string, data: any): Promise<{ text: string; fullOutputPath?: string }> {
const context =
maybeString(data?.context) ??
maybeString(data?.text) ??
maybeString(data?.output) ??
maybeString(data?.result);
const lines: string[] = [`Exa code context for: ${query}`];
const requestId = maybeString(data?.requestId);
if (requestId) lines.push(`Request ID: ${requestId}`);
if (typeof data?.resultsCount === "number") lines.push(`Results count: ${data.resultsCount}`);
if (typeof data?.outputTokens === "number") lines.push(`Output tokens: ${data.outputTokens}`);
if (typeof data?.searchTime === "number") lines.push(`Search time: ${Math.round(data.searchTime)}ms`);
if (typeof data?.costDollars?.total === "number") lines.push(`Cost: $${data.costDollars.total}`);
lines.push("");
if (context) {
lines.push(context.trim());
} else if (Array.isArray(data?.results)) {
lines.push("No combined context field returned. Raw result excerpts:");
lines.push("");
for (const [index, result] of data.results.entries()) {
const title = maybeString(result?.title) ?? maybeString(result?.url) ?? `Result ${index + 1}`;
lines.push(`${index + 1}. ${title}`);
const url = maybeString(result?.url);
if (url) lines.push(` URL: ${url}`);
const text = maybeString(result?.text) ?? maybeString(result?.snippet);
if (text) lines.push(` ${clip(text, 900)}`);
lines.push("");
}
} else {
lines.push("No code context returned.");
}
return finalizeText("exa-code", lines.join("\n"));
}
const SearchType = StringEnum(["auto", "neural", "keyword", "deep", "deep-lite", "deep-reasoning"] as const);
const ExaSearchParams = Type.Object({
query: Type.String({ description: "What to search for on the web" }),
type: Type.Optional(SearchType),
numResults: Type.Optional(Type.Integer({ description: `Number of results to return (max ${MAX_SEARCH_RESULTS})`, default: DEFAULT_SEARCH_RESULTS })),
category: Type.Optional(Type.String({ description: "Optional Exa category, e.g. research paper, company, news" })),
includeDomains: Type.Optional(Type.Array(Type.String(), { description: "Restrict results to these domains" })),
excludeDomains: Type.Optional(Type.Array(Type.String(), { description: "Exclude these domains" })),
includeText: Type.Optional(Type.Boolean({ description: "Include raw page text excerpts", default: false })),
includeSummary: Type.Optional(Type.Boolean({ description: "Ask Exa for result summaries", default: true })),
summaryQuery: Type.Optional(Type.String({ description: "Optional summary focus prompt" })),
includeHighlights: Type.Optional(Type.Boolean({ description: "Ask Exa for result highlights", default: true })),
highlightMaxCharacters: Type.Optional(Type.Integer({ description: "Maximum characters of highlights to request", default: DEFAULT_HIGHLIGHT_CHARS })),
});
const ExaCodeParams = Type.Object({
query: Type.String({ description: "Coding question, library usage pattern, or implementation topic to retrieve code context for" }),
tokensNum: Type.Optional(Type.Integer({ description: `Approximate token budget for returned context (max ${MAX_CODE_TOKENS})`, default: DEFAULT_CODE_TOKENS })),
});
export default function (pi: ExtensionAPI) {
pi.registerCommand("exa-tools", {
description: "Show Exa tools status and credential source",
handler: async (_args, ctx) => {
const auth = resolveExaApiKey();
const lines = [
"Exa tools status",
`API key: ${auth.key ? "configured" : "missing"}`,
`Source: ${auth.source}`,
"",
"Available tools:",
"- exa_search -> general web/docs/reference retrieval",
"- exa_code -> coding examples and library usage context",
];
if (ctx.hasUI) {
ctx.ui.setEditorText(lines.join("\n"));
ctx.ui.notify(`Exa tools ${auth.key ? "ready" : "missing API key"}`, auth.key ? "info" : "warning");
} else {
console.log(lines.join("\n"));
}
},
});
pi.registerTool({
name: "exa_search",
label: "Exa Search",
description: "Search the web with Exa for official docs, articles, release notes, issues, and general references. Requires EXA_API_KEY.",
promptSnippet: "Search the web for docs, references, release notes, articles, and current external information.",
promptGuidelines: [
"Use this for external web knowledge, not for files already in the current repository.",
"Prefer this tool when the user asks for official docs, comparisons, release notes, or broader web research.",
],
parameters: ExaSearchParams,
async execute(_toolCallId, params, signal) {
const numResults = Math.max(1, Math.min(params.numResults ?? DEFAULT_SEARCH_RESULTS, MAX_SEARCH_RESULTS));
const payload: Record<string, unknown> = {
query: params.query,
type: params.type ?? "auto",
numResults,
};
if (params.category) payload.category = params.category;
if (params.includeDomains && params.includeDomains.length > 0) payload.includeDomains = params.includeDomains;
if (params.excludeDomains && params.excludeDomains.length > 0) payload.excludeDomains = params.excludeDomains;
const contents: Record<string, unknown> = {};
if (params.includeText ?? false) contents.text = true;
if (params.includeSummary ?? true) contents.summary = { query: params.summaryQuery ?? params.query };
if (params.includeHighlights ?? true) {
contents.highlights = { maxCharacters: params.highlightMaxCharacters ?? DEFAULT_HIGHLIGHT_CHARS };
}
if (Object.keys(contents).length > 0) payload.contents = contents;
const data = await postExa("/search", payload, signal);
const rendered = await buildSearchOutput(params.query, payload, data);
return {
content: [{ type: "text", text: rendered.text }],
details: {
endpoint: "/search",
query: params.query,
requestId: data?.requestId,
resultsCount: Array.isArray(data?.results) ? data.results.length : undefined,
fullOutputPath: rendered.fullOutputPath,
},
};
},
});
pi.registerTool({
name: "exa_code",
label: "Exa Code",
description: "Retrieve coding-specific context and open-source implementation examples with Exa Code. Requires EXA_API_KEY.",
promptSnippet: "Find coding examples, library usage patterns, and implementation context from public code sources.",
promptGuidelines: [
"Use this when the user wants examples of how libraries, frameworks, or APIs are used in code.",
"Prefer this over general web search when the task asks for implementation patterns or open-source examples.",
],
parameters: ExaCodeParams,
async execute(_toolCallId, params, signal) {
const tokensNum = Math.max(500, Math.min(params.tokensNum ?? DEFAULT_CODE_TOKENS, MAX_CODE_TOKENS));
const payload = {
query: params.query,
tokensNum,
};
const data = await postExa("/context", payload, signal);
const rendered = await buildCodeOutput(params.query, data);
return {
content: [{ type: "text", text: rendered.text }],
details: {
endpoint: "/context",
query: params.query,
requestId: data?.requestId,
resultsCount: data?.resultsCount,
outputTokens: data?.outputTokens,
fullOutputPath: rendered.fullOutputPath,
},
};
},
});
}

View file

@ -0,0 +1,131 @@
# pi subagent scaffold
Foundation for a global pi subagent extension with parallel and chained delegation.
## What this includes
- `index.ts` — main `subagent` tool and `/subagents` command
- `agents.ts` — agent discovery and manifest parsing
- `agents/` — starter bundled agents:
- `scout``zai/glm-5.1`
- `planner``openai-codex/gpt-5.4:xhigh`
- `implementer``openai-codex/gpt-5.3-codex`
- `reviewer``openai-codex/gpt-5.4:xhigh`
- `librarian``zai/glm-5.1` + `exa_search` / `exa_code`
- `prompts/` — starter prompt templates:
- `/implement`
- `/scout-and-plan`
- `/implement-and-review`
- `/parallel-scout`
- `/research`
## Discovery model
This extension supports three agent sources:
- **bundled** — agents shipped in this directory (`./agents`)
- **user**`~/.pi/agent/agents`
- **project** — nearest `.pi/agents` from the current working directory upward
Scope values:
- `global` — bundled + user
- `project` — project only
- `both` — bundled + user + project
Precedence when names collide:
1. bundled
2. user
3. project
So project-local agents override user/global ones, and user/global agents override bundled defaults.
## Parallel safety
Parallel mode is intentionally conservative.
- agents with `parallelSafe: true` can run in parallel
- agents with `parallelSafe: false` are blocked in parallel mode
The bundled `implementer` is marked serial-only.
## Agent manifest format
Each agent is a markdown file with YAML frontmatter:
```md
---
name: planner
description: Creates implementation plans
model: anthropic/claude-sonnet-4-5
tools: read, grep, find, ls
parallelSafe: true
role: planner
tags: planning, design
---
System prompt goes here.
```
Notes:
- `model` accepts normal pi `--model` values, including `provider/id` and optional `:thinking`
- the current scaffold intentionally uses built-in pi providers (`openai-codex`, `zai`), so no custom provider extension is required yet
- `tools` should usually be explicit to avoid recursive delegation
- `parallelSafe` defaults to `false` if omitted
## Install globally from this repo
Recommended runtime target:
```bash
mkdir -p ~/.pi/agent/extensions
ln -sfn /Users/rasyidanakbar/Development/dotfiles/pi/extensions/subagent ~/.pi/agent/extensions/subagent
```
Then start pi and run:
```text
/reload
```
Because prompts and bundled agents live inside the extension directory, a single directory symlink is enough.
## Usage examples
Single agent:
```text
Use subagent planner to propose a plan for refactoring auth
```
Parallel:
```text
Use subagent with two tasks in parallel: scout auth flow, librarian find similar patterns
```
Chain:
```text
Use subagent chain: scout -> planner -> implementer
```
Prompt templates:
```text
/implement add request validation to the API
/scout-and-plan refactor auth to support oauth
/implement-and-review add pagination to the endpoint
/parallel-scout investigate session handling
/research evaluate the best pattern for background job retries in this codebase
```
## Good next steps
1. add `parallel_then_reduce`
2. add budget / timeout controls
3. add a custom `index_search` tool for librarian workflows
4. wire the whole `pi/extensions/` directory into Home Manager so `~/.pi/agent/extensions/` is managed automatically
5. pair librarian with the separate `pi/extensions/exa-tools` extension for web/docs/code retrieval

View file

@ -0,0 +1,186 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
export type AgentScope = "global" | "project" | "both";
export type AgentSource = "bundled" | "user" | "project" | "unknown";
export interface AgentConfig {
name: string;
description: string;
tools?: string[];
model?: string;
systemPrompt: string;
source: AgentSource;
filePath: string;
parallelSafe: boolean;
role?: string;
tags?: string[];
}
export interface AgentDiscoveryResult {
agents: AgentConfig[];
bundledAgentsDir: string;
userAgentsDir: string;
projectAgentsDir: string | null;
}
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function readBoolean(value: unknown): boolean | undefined {
if (typeof value === "boolean") return value;
if (typeof value === "number") return value !== 0;
if (typeof value !== "string") return undefined;
const normalized = value.trim().toLowerCase();
if (["true", "yes", "on", "1"].includes(normalized)) return true;
if (["false", "no", "off", "0"].includes(normalized)) return false;
return undefined;
}
function readStringList(value: unknown): string[] | undefined {
if (Array.isArray(value)) {
const items = value
.map((item) => (typeof item === "string" ? item.trim() : ""))
.filter(Boolean);
return items.length > 0 ? items : undefined;
}
if (typeof value === "string") {
const items = value
.split(",")
.map((item) => item.trim())
.filter(Boolean);
return items.length > 0 ? items : undefined;
}
return undefined;
}
function isDirectory(pathname: string): boolean {
try {
return fs.statSync(pathname).isDirectory();
} catch {
return false;
}
}
function getBundledAgentsDir(): string {
return fileURLToPath(new URL("./agents", import.meta.url));
}
function getUserAgentsDir(): string {
return path.join(getAgentDir(), "agents");
}
function findNearestProjectAgentsDir(cwd: string): string | null {
let currentDir = cwd;
while (true) {
const candidate = path.join(currentDir, ".pi", "agents");
if (isDirectory(candidate)) return candidate;
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) return null;
currentDir = parentDir;
}
}
function loadAgentsFromDir(dir: string, source: Exclude<AgentSource, "unknown">): AgentConfig[] {
const agents: AgentConfig[] = [];
if (!isDirectory(dir)) return agents;
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return agents;
}
for (const entry of entries) {
if (!entry.name.endsWith(".md")) continue;
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
const filePath = path.join(dir, entry.name);
let content: string;
try {
content = fs.readFileSync(filePath, "utf-8");
} catch {
continue;
}
const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
const name = readString(frontmatter.name);
const description = readString(frontmatter.description);
if (!name || !description) continue;
agents.push({
name,
description,
tools: readStringList(frontmatter.tools),
model: readString(frontmatter.model),
systemPrompt: body.trim(),
source,
filePath,
parallelSafe: readBoolean(frontmatter.parallelSafe) ?? false,
role: readString(frontmatter.role),
tags: readStringList(frontmatter.tags),
});
}
return agents;
}
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
const bundledAgentsDir = getBundledAgentsDir();
const userAgentsDir = getUserAgentsDir();
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
const bundledAgents = loadAgentsFromDir(bundledAgentsDir, "bundled");
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userAgentsDir, "user");
const projectAgents = scope === "global" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
const agentMap = new Map<string, AgentConfig>();
if (scope === "global") {
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
for (const agent of userAgents) agentMap.set(agent.name, agent);
}
if (scope === "project") {
for (const agent of projectAgents) agentMap.set(agent.name, agent);
}
if (scope === "both") {
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
for (const agent of userAgents) agentMap.set(agent.name, agent);
for (const agent of projectAgents) agentMap.set(agent.name, agent);
}
return {
agents: Array.from(agentMap.values()),
bundledAgentsDir,
userAgentsDir,
projectAgentsDir,
};
}
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
if (agents.length === 0) return { text: "none", remaining: 0 };
const listed = agents.slice(0, maxItems);
const remaining = Math.max(0, agents.length - listed.length);
const text = listed
.map((agent) => {
const mode = agent.parallelSafe ? "parallel" : "serial";
const role = agent.role ? ` ${agent.role}` : "";
return `${agent.name} (${agent.source}, ${mode}${role ? `, ${role}` : ""})`;
})
.join(", ");
return { text, remaining };
}

View file

@ -0,0 +1,34 @@
---
name: implementer
description: Executes approved plans and makes code changes carefully
tools: read, grep, find, ls, bash, edit, write
model: openai-codex/gpt-5.3-codex
parallelSafe: false
role: implementer
tags: implementation, coding, execution
---
You are an implementation specialist operating in an isolated subagent context.
Your job is to make the requested change safely and completely.
Rules:
- Follow the provided plan if one exists.
- Prefer minimal, targeted edits.
- Do not make unrelated refactors.
- When using bash, keep commands focused on verification and repository-aware workflows.
- If the task is ambiguous, make the smallest reasonable assumption and document it.
Output format:
## Completed
What you changed.
## Files Changed
- `path/to/file.ts` - summary of change
## Validation
Commands run, checks performed, or why validation was not run.
## Notes
Any assumptions, follow-up items, or risks for a reviewer.

View file

@ -0,0 +1,39 @@
---
name: librarian
description: Retrieves references, patterns, prior art, and supporting context
tools: read, grep, find, ls, exa_search, exa_code
model: zai/glm-5.1
parallelSafe: true
role: librarian
tags: search, references, indexing, docs
---
You are a librarian.
Your job is to gather supporting references from the repository so other agents can reason faster.
Focus on:
- similar implementations elsewhere in the repo
- naming conventions and file patterns
- related tests, docs, or configs
- existing abstractions worth reusing
- official docs, external references, and code examples when local context is not enough
Tool preference:
- use `exa_search` for official docs, web references, release notes, or broader external research
- use `exa_code` for library usage patterns, OSS examples, and coding-specific prior art
- use local repo tools first when the answer is already likely in the current codebase
Output format:
## Relevant References
- `path/to/file.ts` - why it is relevant
## Reusable Patterns
Summarize useful conventions or implementation patterns.
## Related Tests / Docs
Point to tests, fixtures, docs, or configs that should be consulted.
## Recommendation
What other agent should do with these references.

View file

@ -0,0 +1,36 @@
---
name: planner
description: Turns requirements and findings into a concrete execution plan
tools: read, grep, find, ls
model: openai-codex/gpt-5.4:xhigh
parallelSafe: true
role: planner
tags: planning, design, execution
---
You are a planning specialist.
You receive requirements, codebase findings, or both. Produce a concrete implementation plan that a separate implementation agent can follow.
Rules:
- Do not modify code.
- Do not invent files or architecture without stating that they are proposals.
- Keep steps small, ordered, and testable.
- Prefer minimal, low-risk changes over broad rewrites.
Output format:
## Goal
One concise sentence.
## Plan
Numbered, execution-ready steps.
## Files to Touch
List likely files and the intended change in each.
## Validation
List the checks, commands, or behavioral verification the implementer should run.
## Risks
Call out breakage risks, hidden dependencies, or edge cases.

View file

@ -0,0 +1,37 @@
---
name: reviewer
description: Reviews code and plans for correctness, regressions, and maintainability
tools: read, grep, find, ls
model: openai-codex/gpt-5.4:xhigh
parallelSafe: true
role: reviewer
tags: review, quality, safety
---
You are a reviewer.
Your job is to evaluate the delegated work for correctness, regression risk, maintainability, and missing validation.
Focus on:
- logic bugs
- edge cases
- mismatches between plan and implementation
- missing tests or validation
- unnecessary complexity
Output format:
## Summary
Two or three sentences on overall quality.
## Must Fix
Critical issues that block merging.
## Should Fix
Important but non-blocking issues.
## Nice to Improve
Optional cleanups or simplifications.
## Validation Gaps
What should still be checked.

View file

@ -0,0 +1,40 @@
---
name: scout
description: Fast codebase recon for locating relevant files, symbols, and flows
tools: read, grep, find, ls
model: zai/glm-5.1
parallelSafe: true
role: scout
tags: search, recon, context
---
You are a scout.
Your job is to quickly investigate a codebase and return compressed, high-signal findings that another agent can use without re-reading everything from scratch.
You are optimized for:
- locating the right files
- tracing imports and call paths
- identifying key types, interfaces, and entrypoints
- narrowing the search space for planner/reviewer/implementer agents
Do not propose broad speculative rewrites. Stay concrete.
Output format:
## Goal
Restate the delegated task in one or two lines.
## Key Files
List exact files and why they matter.
- `path/to/file.ts` - purpose
- `path/to/other.ts` - purpose
## Important Findings
Bullet the most relevant facts, APIs, constraints, and relationships.
## Suggested Next Read
Name the 1-3 files another agent should inspect first, and why.
## Open Questions
Any ambiguity or missing context that another agent should verify.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,10 @@
---
description: Implementer makes the change, reviewer critiques it, implementer applies the review
---
Use the `subagent` tool with `chain` mode for this workflow:
1. Run `implementer` to implement: $@
2. Run `reviewer` to review the implementation output using `{previous}`
3. Run `implementer` again to apply the review feedback using `{previous}`
Use a chain so each step receives the prior step's output via `{previous}`.

View file

@ -0,0 +1,10 @@
---
description: Scout gathers context, planner creates a plan, implementer applies it
---
Use the `subagent` tool with `chain` mode for this workflow:
1. Run `scout` to gather the most relevant code paths for: $@
2. Run `planner` to turn the findings into an implementation plan for: $@
3. Run `implementer` to execute the plan from the previous step using `{previous}`
Use a chain so each step receives the prior step's output via `{previous}`.

View file

@ -0,0 +1,9 @@
---
description: Run scout and librarian in parallel, then summarize their findings
---
Use the `subagent` tool with `tasks` mode to run these in parallel for: $@
- `scout`: identify the most relevant files, flows, and entrypoints
- `librarian`: find related patterns, prior art, tests, or docs
After both results return, synthesize them into one concise summary with recommended next steps.

View file

@ -0,0 +1,28 @@
---
description: Run local scout and external librarian research in parallel, then synthesize grounded guidance
---
Use the `subagent` tool with `tasks` mode to run these in parallel for: $@
- `scout`: inspect the current repository and identify the most relevant local files, entrypoints, constraints, and likely change surface
- `librarian`: gather external references and coding prior art, using `exa_search` for docs/web research and `exa_code` for implementation examples when useful
After both results return:
1. Synthesize them into one grounded response.
2. Clearly separate:
- local repository findings
- external references and docs
- recommended implementation patterns
3. Prefer official docs and high-signal sources over generic summaries.
4. Call out where external advice may not fit this repository's existing architecture.
5. Do **not** implement changes unless the user explicitly asks.
Return the final answer in this structure:
## Goal
## Local Findings
## External References
## Recommended Pattern
## Risks / Caveats
## Next Steps
## Sources

View file

@ -0,0 +1,9 @@
---
description: Scout gathers context, planner turns it into a concrete plan without implementation
---
Use the `subagent` tool with `chain` mode for this workflow:
1. Run `scout` to gather the most relevant code paths for: $@
2. Run `planner` to create a concrete implementation plan for: $@ using `{previous}` as context
Return the plan only. Do not implement changes.

0
pi/prompts/.keep Normal file
View file

0
pi/skills/.keep Normal file
View file

0
pi/themes/.keep Normal file
View file