oira666_pi-subagent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Jakl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,327 @@
1
+ # Pi Subagent
2
+
3
+ **Delegate tasks to specialized subagents with configurable context modes (`spawn` / `fork`).**
4
+
5
+ There are many subagent extensions for pi, this one is mine.
6
+
7
+ ## Why Pi Subagent
8
+
9
+ **Specialization** — Use tailored agents for specific tasks like refactoring, documentation, or research.
10
+
11
+ **Context Control** — Choose `spawn` (fresh context) or `fork` (inherit current session context), depending on the task.
12
+
13
+ **Parallel Execution** — Run multiple agents at once.
14
+
15
+ **A Simpler Fork** — This extension intentionally keeps the surface area small and predictable compared to heavier implementations. It supports nested delegation with depth/cycle guards, but avoids broader scope-selection complexity. If you want the minimal, “just delegate” experience, this is it.
16
+
17
+ ## Install
18
+
19
+ ### Option 1: Install from npm (recommended)
20
+
21
+ ```bash
22
+ pi install npm:@mjakl/pi-subagent
23
+ ```
24
+
25
+ ### Option 2: Install via git
26
+
27
+ ```bash
28
+ pi install git:github.com/mjakl/pi-subagent
29
+ ```
30
+
31
+ ### Option 3: Manual Installation
32
+
33
+ Clone this repository to your Pi extensions directory:
34
+
35
+ ```bash
36
+ cd ~/.pi/agent/extensions
37
+ git clone https://github.com/mjakl/pi-subagent.git
38
+ cd pi-subagent
39
+ npm install
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ ### Delegation Guards (Depth + Cycle Prevention)
45
+
46
+ By default, this extension enforces two runtime guards:
47
+
48
+ 1. **Depth guard** (`--subagent-max-depth`, default `3`)
49
+ - Main agent starts at depth `0`
50
+ - Delegation is allowed while `currentDepth < maxDepth`
51
+ - With default depth `3`: depth `0`, `1`, and `2` can delegate; depth `3` cannot
52
+ 2. **Cycle guard** (`--subagent-prevent-cycles`, default `true`)
53
+ - Blocks delegating to any agent name already present in the current delegation stack
54
+ - Prevents self-recursion (`writer -> writer`) and loops (`planner -> reviewer -> planner`)
55
+
56
+ You can configure depth with either:
57
+
58
+ - CLI flag: `--subagent-max-depth <n>`
59
+ - Environment variable: `PI_SUBAGENT_MAX_DEPTH=<n>`
60
+
61
+ `n` must be a non-negative integer.
62
+
63
+ You can configure cycle prevention with either:
64
+
65
+ - CLI flag: `--subagent-prevent-cycles` / `--no-subagent-prevent-cycles`
66
+ - Environment variable: `PI_SUBAGENT_PREVENT_CYCLES=true|false`
67
+
68
+ Internal env vars managed by the extension and propagated to child processes:
69
+
70
+ - `PI_SUBAGENT_DEPTH`
71
+ - `PI_SUBAGENT_MAX_DEPTH`
72
+ - `PI_SUBAGENT_STACK` (JSON array of ancestor agent names, e.g. `["scout","planner"]`)
73
+ - `PI_SUBAGENT_PREVENT_CYCLES`
74
+
75
+ Examples:
76
+
77
+ ```bash
78
+ # Default behavior: depth 3 + cycle prevention enabled
79
+ pi
80
+
81
+ # Restrict to one nested level (main -> child -> grandchild)
82
+ pi --subagent-max-depth 2
83
+
84
+ # Disable subagent delegation entirely
85
+ pi --subagent-max-depth 0
86
+
87
+ # Allow depth 3 but disable cycle prevention (not recommended)
88
+ pi --subagent-max-depth 3 --no-subagent-prevent-cycles
89
+ ```
90
+
91
+ ### Tool Call Shape
92
+
93
+ `subagent` always accepts a top-level `tasks` array:
94
+
95
+ - One task = single-agent delegation
96
+ - Multiple tasks = parallel delegation
97
+
98
+ Single-task example:
99
+
100
+ ```json
101
+ { "tasks": [{ "agent": "code-writer", "task": "Implement the API change" }], "mode": "spawn" }
102
+ ```
103
+
104
+ Multi-task example:
105
+
106
+ ```json
107
+ { "tasks": [{ "agent": "code-writer", "task": "Draft the implementation" }, { "agent": "code-reviwer", "task": "Review the plan" }], "mode": "fork" }
108
+ ```
109
+
110
+ Each task item supports:
111
+
112
+ - `agent` — subagent name
113
+ - `task` — delegated task text
114
+ - `cwd` — optional working directory override for that task
115
+
116
+ ### Parallel Execution Limits
117
+
118
+ For multi-task calls, two environment variables control fan-out:
119
+
120
+ - `PI_SUBAGENT_MAX_PARALLEL_TASKS` — maximum number of tasks allowed in one call (default: `16`)
121
+ - `PI_SUBAGENT_MAX_CONCURRENCY` — maximum number of subagents running at the same time inside that call (default: `8`)
122
+
123
+ `PI_SUBAGENT_MAX_CONCURRENCY` is effectively clamped to at least `1`.
124
+
125
+ ### Project-local Agent Confirmation
126
+
127
+ Project-local agents from `.pi/agents/*.md` can be gated by `PI_SUBAGENT_CONFIRM_PROJECT_AGENTS`:
128
+
129
+ - `true`, `ask`, or `once` (default) — prompt with **Yes once**, **Yes for this session**, or **No**
130
+ - `false` or `never` — skip the prompt and allow project-local agents immediately
131
+ - `session` — allow project-local agents for the rest of the current session without prompting
132
+
133
+ If you choose **Yes for this session** in the UI, the choice is remembered and you will not be asked again in that session. In non-UI mode, `ask` blocks execution because the extension cannot prompt.
134
+
135
+ ### Context Mode (`spawn` vs `fork`)
136
+
137
+ `subagent` supports a top-level `mode` switch:
138
+
139
+ - `spawn` (default) — Child receives only the task string (`Task: ...`). Best for isolated, reproducible work; typically lower token/cost and less context leakage.
140
+ - `fork` — Child receives a forked snapshot of the current session context **plus** the task string. Best for follow-up work that depends on prior context; typically higher token/cost and may include sensitive context.
141
+
142
+ Quick rule of thumb:
143
+
144
+ - Start with `spawn` for one-off tasks.
145
+ - Use `fork` when the delegated task depends on the current session's prior discussion, reads, or decisions.
146
+
147
+ Examples:
148
+
149
+ ```json
150
+ { "tasks": [{ "agent": "code-writer", "task": "Implement the migration" }], "mode": "spawn" }
151
+ ```
152
+
153
+ ```json
154
+ { "tasks": [{ "agent": "code-reviwer", "task": "Double-check this migration" }], "mode": "fork" }
155
+ ```
156
+
157
+ If omitted, mode defaults to `spawn`.
158
+
159
+ ### Subagent Definitions
160
+
161
+ Subagents are defined as Markdown files with YAML frontmatter.
162
+
163
+ **User Agents:** `~/.pi/agent/agents/*.md`
164
+ **Project Agents:** `.pi/agents/*.md`
165
+ **Bundled Fallback Agents:** `agents/code-writer.md`, `agents/code-reviwer.md`, `agents/code-architect.md`
166
+
167
+ The extension always loads user and project agents first. If a project agent shares a name with a user agent, the project agent wins. The bundled fallback agents are only discovered when no user or project agents are found at all. If you have any user or project agents configured, the bundled defaults are hidden and not discoverable. When project agents are requested, Pi can prompt for confirmation before running them, depending on `PI_SUBAGENT_CONFIRM_PROJECT_AGENTS`.
168
+
169
+ If nothing is configured yet, these fallback agents are available by default:
170
+
171
+ - `code-writer` — implementation and refactoring
172
+ - `code-reviwer` — code review and risk finding
173
+ - `code-architect` — technical design and approach selection
174
+
175
+ Example agent (`~/.pi/agent/agents/writer.md`):
176
+
177
+ ```markdown
178
+ ---
179
+ name: writer
180
+ description: Expert technical writer and editor
181
+ model: anthropic/claude-3-5-sonnet
182
+ tools: read, write
183
+ ---
184
+
185
+ You are an expert technical writer. Your task is to improve the clarity and conciseness of the provided text.
186
+ ```
187
+
188
+ Note: this repository includes bundled fallback agents in `agents/code-writer.md`, `agents/code-reviwer.md`, and `agents/code-architect.md`.
189
+
190
+ ### Frontmatter Fields
191
+
192
+ | Field | Required | Default | Description |
193
+ | ------------- | -------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
194
+ | `name` | Yes | — | Agent identifier used in tool calls (must match exactly) |
195
+ | `description` | Yes | — | What the agent does (shown to the main agent) |
196
+ | `model` | No | Uses the default pi model | Overrides the model for this agent. You can include a provider prefix (e.g. `anthropic/claude-3-5-sonnet` or `openrouter/claude-3.5-sonnet`) to force a specific provider. |
197
+ | `thinking` | No | Uses Pi's default thinking level | Sets the thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`). Equivalent to `--thinking`. |
198
+ | `tools` | No | `read,bash,edit,write` | Comma-separated list of **built-in** tools to enable for this agent. If omitted, defaults apply. |
199
+
200
+ Notes:
201
+
202
+ - `model` accepts `provider/model` syntax — this is a Pi feature. Use it when multiple providers offer the same model ID.
203
+ - `thinking` uses the same values as Pi's `--thinking` flag; it's recommended to set it explicitly since thinking support varies by model.
204
+ - `tools` only controls built-in tools. Extension tools remain available unless extensions are disabled.
205
+ - The Markdown body below the frontmatter becomes the agent's system prompt and is **appended** to Pi's default system prompt (it does **not** replace it).
206
+
207
+ ### Writing a Good Agent File
208
+
209
+ - **Description matters** — the main agent uses the `description` to decide which subagent to call, so be specific about what the agent is good at.
210
+ - **Tool scope is optional but helpful** — reducing tools can keep the agent focused, but you can leave defaults if unsure.
211
+ - **Model + thinking is the power combo** — selecting the right model and thinking level is often the biggest quality boost.
212
+
213
+ ### Available Built-in Tools
214
+
215
+ Available Tools (default: `read`, `bash`, `edit`, `write`):
216
+
217
+ - `read` — Read file contents
218
+ - `bash` — Execute bash commands
219
+ - `edit` — Edit files with find/replace
220
+ - `write` — Write files (creates/overwrites)
221
+ - `grep` — Search file contents (read-only, off by default)
222
+ - `find` — Find files by glob pattern (read-only, off by default)
223
+ - `ls` — List directory contents (read-only, off by default)
224
+
225
+ Tip: for a read-only tool selection, use `read,find,ls,grep`. As soon as you include `edit`, `write`, or `bash`, the agent can practically go wild.
226
+
227
+ ## How Communication Works
228
+
229
+ ### The Isolation Model
230
+
231
+ Each subagent always runs in a **separate `pi` process**:
232
+
233
+ - ❌ No shared memory/state with the parent process
234
+ - ❌ No visibility into sibling subagents
235
+ - ✅ Its own model/tool/runtime loop
236
+ - ✅ Started with `PI_OFFLINE=1` to skip startup network operations and reduce spawn latency
237
+
238
+ What it can see depends on `mode`:
239
+
240
+ - `spawn` (default)
241
+ - ✅ Receives: subagent system prompt + `Task: ...`
242
+ - ❌ Does **not** receive parent session history
243
+ - `fork`
244
+ - ✅ Receives: forked snapshot of current parent session context + `Task: ...`
245
+
246
+ ### What Gets Sent to Subagents
247
+
248
+ #### `spawn` mode (default)
249
+
250
+ `subagent({ tasks: [{ agent: "writer", task: "Document the API" }] })` sends:
251
+
252
+ ```
253
+ [System Prompt from ~/.pi/agent/agents/writer.md]
254
+
255
+ User: Task: Document the API
256
+ ```
257
+
258
+ No parent conversation history is included. In `spawn`, include all required context in `task`.
259
+
260
+ #### `fork` mode
261
+
262
+ `subagent({ tasks: [{ agent: "writer", task: "Document the API" }], mode: "fork" })` sends:
263
+
264
+ ```
265
+ [Forked snapshot of current session context]
266
+ [System Prompt from ~/.pi/agent/agents/writer.md]
267
+
268
+ User: Task: Document the API
269
+ ```
270
+
271
+ Note: `fork` copies session context, not transient runtime-only prompt mutations from the parent process.
272
+
273
+ ### What Comes Back to the Main Agent
274
+
275
+ | Data | Main Agent Sees | TUI Shows |
276
+ | --------------------------- | ------------------------ | ---------------------- |
277
+ | Final text output | ✅ Yes — full, unbounded | ✅ Yes |
278
+ | Tool calls made by subagent | ❌ No | ✅ Yes (expanded view) |
279
+ | Token usage / cost | ❌ No | ✅ Yes |
280
+ | Reasoning/thinking steps | ❌ No | ❌ No |
281
+ | Error messages | ✅ Yes (on failure) | ✅ Yes |
282
+
283
+ **Key point:** The main agent receives **only the final assistant text** from each subagent. Not the tool calls, not the reasoning, not the intermediate steps. This prevents context pollution while still giving you the results.
284
+
285
+ ### Parallel Mode Behavior
286
+
287
+ When running multiple agents in parallel:
288
+
289
+ - Subagents run concurrently up to `PI_SUBAGENT_MAX_CONCURRENCY` (default `8`)
290
+ - The top-level `mode` applies to all tasks in that call
291
+ - Main agent receives a combined result after all finish:
292
+
293
+ ```
294
+ Parallel: 3/3 succeeded
295
+
296
+ [writer] completed: Full output text here...
297
+ [tester] completed: Full output text here...
298
+ [reviewer] completed: Full output text here...
299
+ ```
300
+
301
+ ## Features
302
+
303
+ - **Auto-Discovery** — Agents are found at startup and their descriptions are injected into the main agent's system prompt.
304
+ - **Context Mode Switch** — `spawn` (fresh context) and `fork` (session snapshot + task) per call.
305
+ - **Depth + Cycle Guards** — Depth limiting and ancestry-cycle checks prevent runaway recursive delegation by default.
306
+ - **Streaming Updates** — Watch subagent progress in real-time as tool calls and outputs stream in.
307
+ - **Nested Delegation** — Subagents can call `subagent` again, subject to depth and cycle guards.
308
+ - **Rich TUI Rendering** — Collapsed/expanded views with usage stats, nested delegation trees, tool call previews, and markdown output.
309
+ - **Security Confirmation** — Project-local agents can require explicit user approval, with one-time and session-wide approval options.
310
+
311
+ ## Project Structure
312
+
313
+ ```
314
+ index.ts — Extension entry point: lifecycle hooks, tool registration, mode orchestration
315
+ agents.ts — Agent discovery: reads and parses .md files from user/project directories
316
+ runner.ts — Process runner: starts `pi` subprocesses in spawn/fork context modes and streams JSON events
317
+ render.ts — TUI rendering: renderCall and renderResult for the subagent tool
318
+ types.ts — Shared types and pure helper functions
319
+ ```
320
+
321
+ ## Attribution
322
+
323
+ Inspired by implementations from [vaayne/agent-kit](https://github.com/vaayne/agent-kit) and [mariozechner/pi-mono](https://github.com/badlogic/pi-mono).
324
+
325
+ ## License
326
+
327
+ MIT
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: code-architect
3
+ description: Technical design agent for shaping implementations, APIs, module boundaries, and tradeoffs before coding. Use this agent for plans and architecture decisions.
4
+ thinking: high
5
+ tools: read,bash,grep,find,ls
6
+ ---
7
+
8
+ You are a senior software architect focused on practical design.
9
+
10
+ Your job is to propose implementation approaches that balance simplicity,
11
+ maintainability, extensibility, and delivery speed.
12
+
13
+ Guidelines:
14
+ - Start from the current codebase and constraints, not an idealized rewrite.
15
+ - Prefer simple designs with clear ownership and minimal moving parts.
16
+ - Call out tradeoffs, risks, migration concerns, and compatibility implications.
17
+ - Recommend concrete module boundaries, data flow, and rollout steps when useful.
18
+ - Avoid unnecessary abstraction.
19
+
20
+ In your final response:
21
+ - Present the recommended approach first.
22
+ - Include 1-2 viable alternatives when relevant.
23
+ - Explain why the recommendation fits this codebase.
24
+ - Highlight the biggest implementation risks or unknowns.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: code-reviwer
3
+ description: Code review specialist for finding bugs, regressions, edge cases, and maintainability issues. Use this agent to review code, plans, or patches.
4
+ thinking: high
5
+ tools: read,bash,grep,find,ls
6
+ ---
7
+
8
+ You are a skeptical, detail-oriented code reviewer.
9
+
10
+ Your goal is to identify the most important correctness, reliability, security,
11
+ and maintainability issues in the provided code or plan.
12
+
13
+ Guidelines:
14
+ - Prioritize concrete issues over stylistic preferences.
15
+ - Look for broken assumptions, missing edge-case handling, risky changes, and test gaps.
16
+ - Prefer concise findings with clear reasoning and likely impact.
17
+ - If the code looks good, say so explicitly instead of inventing problems.
18
+ - Do not edit files; focus on analysis and recommendations.
19
+
20
+ In your final response:
21
+ - List findings ordered by severity.
22
+ - Include file paths or symbols when possible.
23
+ - If there are no meaningful issues, say "No significant issues found" and mention any residual risks briefly.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: code-writer
3
+ description: Focused implementation agent for writing and refactoring code with small, reliable diffs. Use this agent when you want code changes made directly.
4
+ thinking: medium
5
+ tools: read,bash,edit,write
6
+ ---
7
+
8
+ You are a pragmatic software engineer focused on implementation.
9
+
10
+ Your job is to turn requirements into small, correct code changes.
11
+
12
+ Guidelines:
13
+ - Read the relevant files before editing.
14
+ - Prefer minimal diffs that fit the existing style and architecture.
15
+ - Preserve working behavior unless the task explicitly changes it.
16
+ - When details are ambiguous, choose the simplest reasonable implementation and state your assumption.
17
+ - If helpful, run targeted commands to inspect the codebase or validate your changes.
18
+ - In your final response, summarize what you changed, note any assumptions, and mention any validation you performed.
package/agents.ts ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Agent discovery and configuration.
3
+ *
4
+ * Agents are Markdown files with YAML frontmatter that define name, description,
5
+ * optional model/tools, and a system prompt body.
6
+ *
7
+ * Lookup locations:
8
+ * - User agents: ~/.pi/agent/agents/*.md
9
+ * - Project agents: .pi/agents/*.md (walks up from cwd)
10
+ * - Bundled agents: ./agents/*.md (fallback only when no user/project agents exist)
11
+ */
12
+
13
+ import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
14
+ import * as fs from "node:fs";
15
+ import * as os from "node:os";
16
+ import * as path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ export type AgentScope = "user" | "project" | "both";
20
+ export type AgentSource = "user" | "project" | "builtin";
21
+
22
+ export interface AgentConfig {
23
+ name: string;
24
+ description: string;
25
+ tools?: string[];
26
+ model?: string;
27
+ thinking?: string;
28
+ systemPrompt: string;
29
+ source: AgentSource;
30
+ filePath: string;
31
+ }
32
+
33
+ export interface AgentDiscoveryResult {
34
+ agents: AgentConfig[];
35
+ projectAgentsDir: string | null;
36
+ }
37
+
38
+ const BUNDLED_AGENTS_DIR = path.join(
39
+ path.dirname(fileURLToPath(import.meta.url)),
40
+ "agents",
41
+ );
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Internal helpers
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function isDirectory(p: string): boolean {
48
+ try {
49
+ return fs.statSync(p).isDirectory();
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /** Walk up from `cwd` looking for a `.pi/agents` directory. */
56
+ function findNearestProjectAgentsDir(cwd: string): string | null {
57
+ let dir = cwd;
58
+ while (true) {
59
+ const candidate = path.join(dir, ".pi", "agents");
60
+ if (isDirectory(candidate)) return candidate;
61
+ const parent = path.dirname(dir);
62
+ if (parent === dir) return null;
63
+ dir = parent;
64
+ }
65
+ }
66
+
67
+ /** Parse a single agent markdown file into an AgentConfig. Returns null on skip. */
68
+ function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
69
+ let content: string;
70
+ try {
71
+ content = fs.readFileSync(filePath, "utf-8");
72
+ } catch {
73
+ return null;
74
+ }
75
+
76
+ let parsed: { frontmatter: Record<string, unknown>; body: string };
77
+ try {
78
+ parsed = parseFrontmatter<Record<string, unknown>>(content);
79
+ } catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ console.warn(`[pi-subagent] Skipping invalid agent file "${filePath}": ${message}`);
82
+ return null;
83
+ }
84
+
85
+ const frontmatter = parsed.frontmatter ?? {};
86
+ const body = parsed.body ?? "";
87
+
88
+ const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
89
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
90
+ if (!name || !description) return null;
91
+
92
+ let tools: string[] | undefined;
93
+ if (typeof frontmatter.tools === "string") {
94
+ const parsedTools = frontmatter.tools
95
+ .split(",")
96
+ .map((t) => t.trim())
97
+ .filter(Boolean);
98
+ if (parsedTools.length > 0) tools = parsedTools;
99
+ } else if (Array.isArray(frontmatter.tools)) {
100
+ const parsedTools = frontmatter.tools
101
+ .filter((t): t is string => typeof t === "string")
102
+ .map((t) => t.trim())
103
+ .filter(Boolean);
104
+ if (parsedTools.length > 0) tools = parsedTools;
105
+ } else if (frontmatter.tools !== undefined) {
106
+ console.warn(
107
+ `[pi-subagent] Ignoring invalid tools field in "${filePath}". Expected a comma-separated string or string array.`,
108
+ );
109
+ }
110
+
111
+ return {
112
+ name,
113
+ description,
114
+ tools,
115
+ model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
116
+ thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
117
+ systemPrompt: body,
118
+ source,
119
+ filePath,
120
+ };
121
+ }
122
+
123
+ /** Load all agent definitions from a directory. */
124
+ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
125
+ if (!fs.existsSync(dir)) return [];
126
+
127
+ let entries: fs.Dirent[];
128
+ try {
129
+ entries = fs.readdirSync(dir, { withFileTypes: true });
130
+ } catch {
131
+ return [];
132
+ }
133
+
134
+ entries.sort((a, b) => a.name.localeCompare(b.name));
135
+
136
+ const agents: AgentConfig[] = [];
137
+ for (const entry of entries) {
138
+ if (!entry.name.endsWith(".md")) continue;
139
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
140
+
141
+ const agent = parseAgentFile(path.join(dir, entry.name), source);
142
+ if (agent) agents.push(agent);
143
+ }
144
+ return agents;
145
+ }
146
+
147
+ function dedupeAgents(
148
+ userAgents: AgentConfig[],
149
+ projectAgents: AgentConfig[],
150
+ ): AgentConfig[] {
151
+ const agentMap = new Map<string, AgentConfig>();
152
+ for (const agent of userAgents) agentMap.set(agent.name, agent);
153
+ for (const agent of projectAgents) agentMap.set(agent.name, agent);
154
+ return Array.from(agentMap.values());
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Public API
159
+ // ---------------------------------------------------------------------------
160
+
161
+ /**
162
+ * Discover all available agents according to the requested scope.
163
+ *
164
+ * When scope is "both", project agents override user agents with the same name.
165
+ * If no user or project agents exist at all, bundled fallback agents are returned.
166
+ */
167
+ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
168
+ const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
169
+ const projectAgentsDir = findNearestProjectAgentsDir(cwd);
170
+
171
+ const userAgents = loadAgentsFromDir(userDir, "user");
172
+ const projectAgents = projectAgentsDir ? loadAgentsFromDir(projectAgentsDir, "project") : [];
173
+
174
+ const hasConfiguredAgents = userAgents.length > 0 || projectAgents.length > 0;
175
+ if (!hasConfiguredAgents) {
176
+ return {
177
+ agents: loadAgentsFromDir(BUNDLED_AGENTS_DIR, "builtin"),
178
+ projectAgentsDir,
179
+ };
180
+ }
181
+
182
+ if (scope === "user") return { agents: userAgents, projectAgentsDir };
183
+ if (scope === "project") return { agents: projectAgents, projectAgentsDir };
184
+ return { agents: dedupeAgents(userAgents, projectAgents), projectAgentsDir };
185
+ }