oira666_pi-subagent 0.1.13 → 0.1.15
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 +21 -21
- package/agents/code-architect.md +24 -24
- package/agents/code-reviwer.md +23 -23
- package/agents/code-writer.md +18 -18
- package/agents.ts +188 -188
- package/package.json +74 -74
- package/render.ts +427 -344
- package/runner-sdk.ts +52 -0
- package/runner.ts +906 -844
- package/types.ts +309 -286
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/agents/code-architect.md
CHANGED
|
@@ -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.
|
package/agents/code-reviwer.md
CHANGED
|
@@ -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.
|
package/agents/code-writer.md
CHANGED
|
@@ -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
|
+
}
|