oira666_pi-subagent 0.1.7 → 0.1.8

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 CHANGED
@@ -1,21 +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.
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 CHANGED
@@ -22,9 +22,30 @@ pi remove npm:oira666_pi-subagent
22
22
 
23
23
  ## How It Works
24
24
 
25
- Each subagent runs as a **separate `pi` process** — isolated memory, its own model/tool loop.
25
+ The extension supports two execution modes, selectable via `PI_SUBAGENTS_MODE`.
26
26
 
27
- **`spawn` (default)** — Child receives only the task string. Best for isolated work, lower cost.
27
+ ### Execution mode: `subprocess` (default)
28
+
29
+ Each subagent runs as a **separate `pi` process** — fully isolated memory, its own model/tool loop.
30
+ Processes are spawned via the operating system and communicate through JSON-line stdout.
31
+
32
+ - Full OS-level isolation — a crashed subagent cannot affect the parent
33
+ - True parallel execution across all CPU cores
34
+ - Each subprocess boots a fresh Node.js runtime (adds ~200–500 ms per subagent)
35
+ - Requires the `pi` binary on `PATH`
36
+
37
+ ### Execution mode: `sdk`
38
+
39
+ Each subagent runs as an **in-process `AgentSession`** created via the pi SDK — no new process is spawned.
40
+
41
+ - No spawn overhead — sessions start in milliseconds
42
+ - No temp files for system prompts or fork snapshots
43
+ - Concurrency through the Node.js event loop (fine for I/O-bound LLM work)
44
+ - All sessions share the same memory and event loop
45
+
46
+ ### Context modes
47
+
48
+ **`spawn` (default)** — Child receives only the task string. Best for isolated work, lower cost.
28
49
  **`fork`** — Child receives a snapshot of the current session context + task. Best for follow-up work.
29
50
 
30
51
  The main agent receives only the **final text output** from subagents (no tool calls, no reasoning).
@@ -94,6 +115,32 @@ Available tools: `read`, `bash`, `edit`, `write`.
94
115
 
95
116
  The Markdown body becomes the agent's system prompt (appended to Pi's default, not replacing it).
96
117
 
118
+ ## Execution Mode
119
+
120
+ | Env Var | Default | Values | Description |
121
+ | --------------------- | ------------ | -------------------- | -------------------------------------- |
122
+ | `PI_SUBAGENTS_MODE` | `subprocess` | `subprocess` / `sdk` | How subagent sessions are created |
123
+
124
+ ```bash
125
+ # Run subagents as in-process SDK sessions (faster startup, no spawn overhead)
126
+ PI_SUBAGENTS_MODE=sdk pi
127
+
128
+ # Run subagents as isolated subprocess pi instances (default, full isolation)
129
+ PI_SUBAGENTS_MODE=subprocess pi
130
+ ```
131
+
132
+ ### Comparison
133
+
134
+ | | `subprocess` | `sdk` |
135
+ |---|---|---|
136
+ | Session isolation | Full OS-level | Shared memory/event loop |
137
+ | Startup overhead | ~200–500 ms per agent | ~10–50 ms per agent |
138
+ | Parallelism | True OS parallelism | Event-loop concurrency |
139
+ | Fork mode context | Temp JSONL file | Temp JSONL file (same format) |
140
+ | Depth tracking | Env vars in child process | Closure parameters |
141
+ | `pi` binary required | Yes | No |
142
+ | Crash isolation | Yes — subprocess crash is contained | No — exception bubbles up |
143
+
97
144
  ## Delegation Guards
98
145
 
99
146
  Depth and cycle guards prevent runaway recursive delegation.
@@ -124,9 +171,13 @@ pi --no-subagent-prevent-cycles # allow cycles (not recommended)
124
171
 
125
172
  ## CLI Argument Proxying
126
173
 
127
- All flags passed to the parent `pi` process are forwarded to subagent child processes, so they
128
- inherit the same provider, API key, model, and other runtime settings. Flags the extension manages
129
- itself are blocked from being forwarded.
174
+ > **Note:** CLI argument proxying only applies to `subprocess` mode. In `sdk` mode, subagents
175
+ > inherit provider and model configuration directly from the calling session's model registry
176
+ > and API keys from environment variables, so no forwarding is needed.
177
+
178
+ In `subprocess` mode, all flags passed to the parent `pi` process are forwarded to subagent child
179
+ processes, so they inherit the same provider, API key, model, and other runtime settings. Flags the
180
+ extension manages itself are blocked from being forwarded.
130
181
 
131
182
  **Always forwarded verbatim:**
132
183
 
@@ -421,7 +472,7 @@ for await (const line of jsonLines) {
421
472
 
422
473
  Note: if you also track the main agent's own usage from `message_end` events, make sure **not** to
423
474
  double-count the subagent costs there — the main agent's own token usage (from its own `message_end`
424
- events) does not include subagent work; they are always separate processes.
475
+ events) does not include subagent work (whether subprocess or SDK-mode).
425
476
 
426
477
  ---
427
478
 
@@ -1,24 +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.
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.
@@ -1,23 +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.
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.
@@ -1,18 +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.
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 CHANGED
@@ -1,188 +1,188 @@
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 (or $PI_CODING_AGENT_DIR/agents/ when env var is set)
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 { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
14
- import * as fs from "node:fs";
15
- import * as path from "node:path";
16
- import { fileURLToPath } from "node:url";
17
-
18
- export type AgentScope = "user" | "project" | "both";
19
- export type AgentSource = "user" | "project" | "builtin";
20
-
21
- export interface AgentConfig {
22
- name: string;
23
- description: string;
24
- tools?: string[];
25
- model?: string;
26
- thinking?: string;
27
- systemPrompt: string;
28
- source: AgentSource;
29
- filePath: string;
30
- }
31
-
32
- export interface AgentDiscoveryResult {
33
- agents: AgentConfig[];
34
- projectAgentsDir: string | null;
35
- }
36
-
37
- const BUNDLED_AGENTS_DIR = path.join(
38
- path.dirname(fileURLToPath(import.meta.url)),
39
- "agents",
40
- );
41
-
42
- // ---------------------------------------------------------------------------
43
- // Internal helpers
44
- // ---------------------------------------------------------------------------
45
-
46
- function isDirectory(p: string): boolean {
47
- try {
48
- return fs.statSync(p).isDirectory();
49
- } catch {
50
- return false;
51
- }
52
- }
53
-
54
- /** Walk up from `cwd` looking for a `.pi/agents` directory. */
55
- function findNearestProjectAgentsDir(cwd: string): string | null {
56
- let dir = cwd;
57
- while (true) {
58
- const candidate = path.join(dir, ".pi", "agents");
59
- if (isDirectory(candidate)) return candidate;
60
- const parent = path.dirname(dir);
61
- if (parent === dir) return null;
62
- dir = parent;
63
- }
64
- }
65
-
66
- /** Parse a single agent markdown file into an AgentConfig. Returns null on skip. */
67
- function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
68
- let content: string;
69
- try {
70
- content = fs.readFileSync(filePath, "utf-8");
71
- } catch {
72
- return null;
73
- }
74
-
75
- let parsed: { frontmatter: Record<string, unknown>; body: string };
76
- try {
77
- parsed = parseFrontmatter<Record<string, unknown>>(content);
78
- } catch (err) {
79
- const message = err instanceof Error ? err.message : String(err);
80
- console.warn(`[pi-subagent] Skipping invalid agent file "${filePath}": ${message}`);
81
- return null;
82
- }
83
-
84
- const frontmatter = parsed.frontmatter ?? {};
85
- const body = parsed.body ?? "";
86
-
87
- const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
88
- const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
89
- if (!name || !description) return null;
90
-
91
- let tools: string[] | undefined;
92
- if (typeof frontmatter.tools === "string") {
93
- const parsedTools = frontmatter.tools
94
- .split(",")
95
- .map((t) => t.trim())
96
- .filter(Boolean);
97
- if (parsedTools.length > 0) tools = parsedTools;
98
- } else if (Array.isArray(frontmatter.tools)) {
99
- const parsedTools = frontmatter.tools
100
- .filter((t): t is string => typeof t === "string")
101
- .map((t) => t.trim())
102
- .filter(Boolean);
103
- if (parsedTools.length > 0) tools = parsedTools;
104
- } else if (frontmatter.tools !== undefined) {
105
- console.warn(
106
- `[pi-subagent] Ignoring invalid tools field in "${filePath}". Expected a comma-separated string or string array.`,
107
- );
108
- }
109
-
110
- return {
111
- name,
112
- description,
113
- tools,
114
- model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
115
- thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
116
- systemPrompt: body,
117
- source,
118
- filePath,
119
- };
120
- }
121
-
122
- /** Load all agent definitions from a directory. */
123
- function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
124
- if (!fs.existsSync(dir)) return [];
125
-
126
- let entries: fs.Dirent[];
127
- try {
128
- entries = fs.readdirSync(dir, { withFileTypes: true });
129
- } catch {
130
- return [];
131
- }
132
-
133
- entries.sort((a, b) => a.name.localeCompare(b.name));
134
-
135
- const agents: AgentConfig[] = [];
136
- for (const entry of entries) {
137
- if (!entry.name.endsWith(".md")) continue;
138
- if (!entry.isFile() && !entry.isSymbolicLink()) continue;
139
-
140
- const agent = parseAgentFile(path.join(dir, entry.name), source);
141
- if (agent) agents.push(agent);
142
- }
143
- return agents;
144
- }
145
-
146
- /**
147
- * Merge agents with last-write-wins deduplication by name.
148
- * Priority (lowest → highest): user < project.
149
- */
150
- function dedupeAgents(
151
- userAgents: AgentConfig[],
152
- projectAgents: AgentConfig[],
153
- ): AgentConfig[] {
154
- const agentMap = new Map<string, AgentConfig>();
155
- for (const agent of userAgents) agentMap.set(agent.name, agent);
156
- for (const agent of projectAgents) agentMap.set(agent.name, agent);
157
- return Array.from(agentMap.values());
158
- }
159
-
160
- // ---------------------------------------------------------------------------
161
- // Public API
162
- // ---------------------------------------------------------------------------
163
-
164
- /**
165
- * Discover all available agents according to the requested scope.
166
- *
167
- * When scope is "both", project agents override user agents with the same name.
168
- * If no user or project agents exist at all, bundled fallback agents are returned.
169
- */
170
- export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
171
- const userDir = path.join(getAgentDir(), "agents");
172
- const projectAgentsDir = findNearestProjectAgentsDir(cwd);
173
-
174
- const userAgents = loadAgentsFromDir(userDir, "user");
175
- const projectAgents = projectAgentsDir ? loadAgentsFromDir(projectAgentsDir, "project") : [];
176
-
177
- const hasConfiguredAgents = userAgents.length > 0 || projectAgents.length > 0;
178
- if (!hasConfiguredAgents) {
179
- return {
180
- agents: loadAgentsFromDir(BUNDLED_AGENTS_DIR, "builtin"),
181
- projectAgentsDir,
182
- };
183
- }
184
-
185
- if (scope === "user") return { agents: userAgents, projectAgentsDir };
186
- if (scope === "project") return { agents: projectAgents, projectAgentsDir };
187
- return { agents: dedupeAgents(userAgents, projectAgents), projectAgentsDir };
188
- }
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 (or $PI_CODING_AGENT_DIR/agents/ when env var is set)
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 { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ export type AgentScope = "user" | "project" | "both";
19
+ export type AgentSource = "user" | "project" | "builtin";
20
+
21
+ export interface AgentConfig {
22
+ name: string;
23
+ description: string;
24
+ tools?: string[];
25
+ model?: string;
26
+ thinking?: string;
27
+ systemPrompt: string;
28
+ source: AgentSource;
29
+ filePath: string;
30
+ }
31
+
32
+ export interface AgentDiscoveryResult {
33
+ agents: AgentConfig[];
34
+ projectAgentsDir: string | null;
35
+ }
36
+
37
+ const BUNDLED_AGENTS_DIR = path.join(
38
+ path.dirname(fileURLToPath(import.meta.url)),
39
+ "agents",
40
+ );
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Internal helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ function isDirectory(p: string): boolean {
47
+ try {
48
+ return fs.statSync(p).isDirectory();
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /** Walk up from `cwd` looking for a `.pi/agents` directory. */
55
+ function findNearestProjectAgentsDir(cwd: string): string | null {
56
+ let dir = cwd;
57
+ while (true) {
58
+ const candidate = path.join(dir, ".pi", "agents");
59
+ if (isDirectory(candidate)) return candidate;
60
+ const parent = path.dirname(dir);
61
+ if (parent === dir) return null;
62
+ dir = parent;
63
+ }
64
+ }
65
+
66
+ /** Parse a single agent markdown file into an AgentConfig. Returns null on skip. */
67
+ function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
68
+ let content: string;
69
+ try {
70
+ content = fs.readFileSync(filePath, "utf-8");
71
+ } catch {
72
+ return null;
73
+ }
74
+
75
+ let parsed: { frontmatter: Record<string, unknown>; body: string };
76
+ try {
77
+ parsed = parseFrontmatter<Record<string, unknown>>(content);
78
+ } catch (err) {
79
+ const message = err instanceof Error ? err.message : String(err);
80
+ console.warn(`[pi-subagent] Skipping invalid agent file "${filePath}": ${message}`);
81
+ return null;
82
+ }
83
+
84
+ const frontmatter = parsed.frontmatter ?? {};
85
+ const body = parsed.body ?? "";
86
+
87
+ const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
88
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
89
+ if (!name || !description) return null;
90
+
91
+ let tools: string[] | undefined;
92
+ if (typeof frontmatter.tools === "string") {
93
+ const parsedTools = frontmatter.tools
94
+ .split(",")
95
+ .map((t) => t.trim())
96
+ .filter(Boolean);
97
+ if (parsedTools.length > 0) tools = parsedTools;
98
+ } else if (Array.isArray(frontmatter.tools)) {
99
+ const parsedTools = frontmatter.tools
100
+ .filter((t): t is string => typeof t === "string")
101
+ .map((t) => t.trim())
102
+ .filter(Boolean);
103
+ if (parsedTools.length > 0) tools = parsedTools;
104
+ } else if (frontmatter.tools !== undefined) {
105
+ console.warn(
106
+ `[pi-subagent] Ignoring invalid tools field in "${filePath}". Expected a comma-separated string or string array.`,
107
+ );
108
+ }
109
+
110
+ return {
111
+ name,
112
+ description,
113
+ tools,
114
+ model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
115
+ thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
116
+ systemPrompt: body,
117
+ source,
118
+ filePath,
119
+ };
120
+ }
121
+
122
+ /** Load all agent definitions from a directory. */
123
+ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
124
+ if (!fs.existsSync(dir)) return [];
125
+
126
+ let entries: fs.Dirent[];
127
+ try {
128
+ entries = fs.readdirSync(dir, { withFileTypes: true });
129
+ } catch {
130
+ return [];
131
+ }
132
+
133
+ entries.sort((a, b) => a.name.localeCompare(b.name));
134
+
135
+ const agents: AgentConfig[] = [];
136
+ for (const entry of entries) {
137
+ if (!entry.name.endsWith(".md")) continue;
138
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
139
+
140
+ const agent = parseAgentFile(path.join(dir, entry.name), source);
141
+ if (agent) agents.push(agent);
142
+ }
143
+ return agents;
144
+ }
145
+
146
+ /**
147
+ * Merge agents with last-write-wins deduplication by name.
148
+ * Priority (lowest → highest): user < project.
149
+ */
150
+ function dedupeAgents(
151
+ userAgents: AgentConfig[],
152
+ projectAgents: AgentConfig[],
153
+ ): AgentConfig[] {
154
+ const agentMap = new Map<string, AgentConfig>();
155
+ for (const agent of userAgents) agentMap.set(agent.name, agent);
156
+ for (const agent of projectAgents) agentMap.set(agent.name, agent);
157
+ return Array.from(agentMap.values());
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Public API
162
+ // ---------------------------------------------------------------------------
163
+
164
+ /**
165
+ * Discover all available agents according to the requested scope.
166
+ *
167
+ * When scope is "both", project agents override user agents with the same name.
168
+ * If no user or project agents exist at all, bundled fallback agents are returned.
169
+ */
170
+ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
171
+ const userDir = path.join(getAgentDir(), "agents");
172
+ const projectAgentsDir = findNearestProjectAgentsDir(cwd);
173
+
174
+ const userAgents = loadAgentsFromDir(userDir, "user");
175
+ const projectAgents = projectAgentsDir ? loadAgentsFromDir(projectAgentsDir, "project") : [];
176
+
177
+ const hasConfiguredAgents = userAgents.length > 0 || projectAgents.length > 0;
178
+ if (!hasConfiguredAgents) {
179
+ return {
180
+ agents: loadAgentsFromDir(BUNDLED_AGENTS_DIR, "builtin"),
181
+ projectAgentsDir,
182
+ };
183
+ }
184
+
185
+ if (scope === "user") return { agents: userAgents, projectAgentsDir };
186
+ if (scope === "project") return { agents: projectAgents, projectAgentsDir };
187
+ return { agents: dedupeAgents(userAgents, projectAgents), projectAgentsDir };
188
+ }