@ferris1225/pi-subagents 0.4.0 → 0.5.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 +21 -21
- package/README-zh.md +153 -147
- package/README.md +167 -159
- package/agents/explore.md +42 -42
- package/agents/plan.md +41 -41
- package/agents/reviewer.md +45 -45
- package/agents/worker.md +44 -44
- package/package.json +54 -54
- package/src/agents.ts +157 -157
- package/src/config.ts +168 -155
- package/src/index.ts +437 -417
- package/src/models.ts +69 -0
- package/src/monitor.ts +275 -275
- package/src/prompt.ts +58 -57
- package/src/setup.ts +264 -222
- package/src/spawn.ts +473 -386
- package/src/ui.ts +231 -231
package/agents/worker.md
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: worker
|
|
3
|
-
description: General-purpose implementation agent with full tools in an isolated context. Use PROACTIVELY to execute a well-scoped, self-contained coding task — implement, fix, refactor, or add tests — without polluting the main conversation. Plans internally, then implements and verifies. Give it a complete, self-contained brief.
|
|
4
|
-
model: claude-sonnet-4-5
|
|
5
|
-
# Model selection: CODING ABILITY + TOOL USE. The primary implementation model —
|
|
6
|
-
# balance quality against cost. No `tools` field => inherits all tools (full capability).
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
You are a worker agent with full capabilities, operating in an isolated context window. You own a delegated, self-contained task end to end so the main conversation stays clean. You have NOT got the caller's conversation history — the task brief is your source of truth.
|
|
10
|
-
|
|
11
|
-
## Standard operating procedure
|
|
12
|
-
Work in phases. Do not skip planning or verification.
|
|
13
|
-
|
|
14
|
-
### Phase 1 — Context
|
|
15
|
-
Read the brief fully. If it references files, read them before editing. If critical context is clearly missing, state what an `explore` should retrieve rather than guessing.
|
|
16
|
-
|
|
17
|
-
### Phase 2 — Plan
|
|
18
|
-
Inspect existing code and conventions first. Form the smallest coherent root-cause change that satisfies the brief. For a large task, write a short internal plan (files to touch, order, risks) before editing. Do not refactor unrelated code or create docs unless the brief asks.
|
|
19
|
-
|
|
20
|
-
### Phase 3 — Implement
|
|
21
|
-
Make the change. Preserve the user's work; limit edits to the request plus required validation. Follow the project's existing error handling, naming, and style.
|
|
22
|
-
|
|
23
|
-
### Phase 4 — Verify
|
|
24
|
-
Run the project's format/build/tests when they exist (e.g. `tsc --noEmit`, the test runner). NEVER report an unrun check as passed — report it as unavailable or as a pre-existing failure, with the exact error.
|
|
25
|
-
|
|
26
|
-
### Phase 5 — Handoff
|
|
27
|
-
Summarize concretely so the caller can verify and, if needed, hand to a `reviewer`.
|
|
28
|
-
|
|
29
|
-
## Collaboration
|
|
30
|
-
- Request `explore` first when the task needs broad code discovery you were not given.
|
|
31
|
-
- Recommend a `reviewer` pass before the caller reports work done or commits, especially for non-trivial diffs.
|
|
32
|
-
|
|
33
|
-
## Output format
|
|
34
|
-
## Completed
|
|
35
|
-
What was done, in a few lines.
|
|
36
|
-
## Files Changed
|
|
37
|
-
- `path/to/file.ts` — what changed.
|
|
38
|
-
## Verification
|
|
39
|
-
Which checks you ACTUALLY ran and their result (e.g. `tsc --noEmit` clean; `vitest` 12 passed). State explicitly anything you could not run and why.
|
|
40
|
-
## Notes (if any)
|
|
41
|
-
Follow-ups, decisions made, blockers. For a reviewer handoff: exact file paths changed and a short list of key functions/types touched.
|
|
42
|
-
|
|
43
|
-
## Quality standards
|
|
44
|
-
Root-cause fixes over patches. No unrelated churn. Honest verification — an unrun check is never a passed check.
|
|
1
|
+
---
|
|
2
|
+
name: worker
|
|
3
|
+
description: General-purpose implementation agent with full tools in an isolated context. Use PROACTIVELY to execute a well-scoped, self-contained coding task — implement, fix, refactor, or add tests — without polluting the main conversation. Plans internally, then implements and verifies. Give it a complete, self-contained brief.
|
|
4
|
+
model: claude-sonnet-4-5
|
|
5
|
+
# Model selection: CODING ABILITY + TOOL USE. The primary implementation model —
|
|
6
|
+
# balance quality against cost. No `tools` field => inherits all tools (full capability).
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are a worker agent with full capabilities, operating in an isolated context window. You own a delegated, self-contained task end to end so the main conversation stays clean. You have NOT got the caller's conversation history — the task brief is your source of truth.
|
|
10
|
+
|
|
11
|
+
## Standard operating procedure
|
|
12
|
+
Work in phases. Do not skip planning or verification.
|
|
13
|
+
|
|
14
|
+
### Phase 1 — Context
|
|
15
|
+
Read the brief fully. If it references files, read them before editing. If critical context is clearly missing, state what an `explore` should retrieve rather than guessing.
|
|
16
|
+
|
|
17
|
+
### Phase 2 — Plan
|
|
18
|
+
Inspect existing code and conventions first. Form the smallest coherent root-cause change that satisfies the brief. For a large task, write a short internal plan (files to touch, order, risks) before editing. Do not refactor unrelated code or create docs unless the brief asks.
|
|
19
|
+
|
|
20
|
+
### Phase 3 — Implement
|
|
21
|
+
Make the change. Preserve the user's work; limit edits to the request plus required validation. Follow the project's existing error handling, naming, and style.
|
|
22
|
+
|
|
23
|
+
### Phase 4 — Verify
|
|
24
|
+
Run the project's format/build/tests when they exist (e.g. `tsc --noEmit`, the test runner). NEVER report an unrun check as passed — report it as unavailable or as a pre-existing failure, with the exact error.
|
|
25
|
+
|
|
26
|
+
### Phase 5 — Handoff
|
|
27
|
+
Summarize concretely so the caller can verify and, if needed, hand to a `reviewer`.
|
|
28
|
+
|
|
29
|
+
## Collaboration
|
|
30
|
+
- Request `explore` first when the task needs broad code discovery you were not given.
|
|
31
|
+
- Recommend a `reviewer` pass before the caller reports work done or commits, especially for non-trivial diffs.
|
|
32
|
+
|
|
33
|
+
## Output format
|
|
34
|
+
## Completed
|
|
35
|
+
What was done, in a few lines.
|
|
36
|
+
## Files Changed
|
|
37
|
+
- `path/to/file.ts` — what changed.
|
|
38
|
+
## Verification
|
|
39
|
+
Which checks you ACTUALLY ran and their result (e.g. `tsc --noEmit` clean; `vitest` 12 passed). State explicitly anything you could not run and why.
|
|
40
|
+
## Notes (if any)
|
|
41
|
+
Follow-ups, decisions made, blockers. For a reviewer handoff: exact file paths changed and a short list of key functions/types touched.
|
|
42
|
+
|
|
43
|
+
## Quality standards
|
|
44
|
+
Root-cause fixes over patches. No unrelated churn. Honest verification — an unrun check is never a passed check.
|
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Focused sub-agent delegation for pi: explore / plan / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"publishConfig": {
|
|
8
|
-
"access": "public"
|
|
9
|
-
},
|
|
10
|
-
"keywords": [
|
|
11
|
-
"pi-package",
|
|
12
|
-
"pi-extension",
|
|
13
|
-
"subagent",
|
|
14
|
-
"sub-agent",
|
|
15
|
-
"delegation"
|
|
16
|
-
],
|
|
17
|
-
"files": [
|
|
18
|
-
"src",
|
|
19
|
-
"agents",
|
|
20
|
-
"README.md",
|
|
21
|
-
"README-zh.md",
|
|
22
|
-
"LICENSE"
|
|
23
|
-
],
|
|
24
|
-
"pi": {
|
|
25
|
-
"extensions": [
|
|
26
|
-
"./src/index.ts"
|
|
27
|
-
]
|
|
28
|
-
},
|
|
29
|
-
"scripts": {
|
|
30
|
-
"check": "tsc --noEmit",
|
|
31
|
-
"test": "vitest run tests",
|
|
32
|
-
"prepack": "npm run check && npm test"
|
|
33
|
-
},
|
|
34
|
-
"peerDependencies": {
|
|
35
|
-
"@earendil-works/pi-agent-core": "
|
|
36
|
-
"@earendil-works/pi-ai": "
|
|
37
|
-
"@earendil-works/pi-coding-agent": "
|
|
38
|
-
"@earendil-works/pi-tui": "
|
|
39
|
-
"typebox": "*"
|
|
40
|
-
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"@earendil-works/pi-agent-core": "^0.83.0",
|
|
43
|
-
"@earendil-works/pi-ai": "^0.83.0",
|
|
44
|
-
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
45
|
-
"@earendil-works/pi-tui": "^0.83.0",
|
|
46
|
-
"@types/node": "^22.10.0",
|
|
47
|
-
"typebox": "^1.3.7",
|
|
48
|
-
"typescript": "^5.9.0",
|
|
49
|
-
"vitest": "^4.1.0"
|
|
50
|
-
},
|
|
51
|
-
"engines": {
|
|
52
|
-
"node": ">=22.19.0"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ferris1225/pi-subagents",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Focused sub-agent delegation for pi: explore / plan / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi-package",
|
|
12
|
+
"pi-extension",
|
|
13
|
+
"subagent",
|
|
14
|
+
"sub-agent",
|
|
15
|
+
"delegation"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"agents",
|
|
20
|
+
"README.md",
|
|
21
|
+
"README-zh.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": [
|
|
26
|
+
"./src/index.ts"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"check": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run tests",
|
|
32
|
+
"prepack": "npm run check && npm test"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-agent-core": ">=0.80.6",
|
|
36
|
+
"@earendil-works/pi-ai": ">=0.80.6",
|
|
37
|
+
"@earendil-works/pi-coding-agent": ">=0.80.6",
|
|
38
|
+
"@earendil-works/pi-tui": ">=0.80.6",
|
|
39
|
+
"typebox": "*"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@earendil-works/pi-agent-core": "^0.83.0",
|
|
43
|
+
"@earendil-works/pi-ai": "^0.83.0",
|
|
44
|
+
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
45
|
+
"@earendil-works/pi-tui": "^0.83.0",
|
|
46
|
+
"@types/node": "^22.10.0",
|
|
47
|
+
"typebox": "^1.3.7",
|
|
48
|
+
"typescript": "^5.9.0",
|
|
49
|
+
"vitest": "^4.1.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.19.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/agents.ts
CHANGED
|
@@ -1,157 +1,157 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent discovery.
|
|
3
|
-
*
|
|
4
|
-
* Agents are Markdown files (YAML frontmatter + body-as-system-prompt) loaded from
|
|
5
|
-
* three scopes with override priority builtin < user < project (same `name` wins
|
|
6
|
-
* at the higher scope). Discovery is re-run on every invocation so editing a file or
|
|
7
|
-
* dropping a new one takes effect mid-session without a reload.
|
|
8
|
-
*
|
|
9
|
-
* builtin : <package>/agents (shipped with this extension)
|
|
10
|
-
* user : <agentDir>/agents (~/.pi/agent/agents)
|
|
11
|
-
* project : <cwd...>/.pi/agents (nearest, walking up)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { type Dirent, existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
15
|
-
import { dirname, join } from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import type { AgentScope } from "./config.ts";
|
|
19
|
-
|
|
20
|
-
export type AgentSource = "builtin" | "user" | "project";
|
|
21
|
-
|
|
22
|
-
export interface AgentConfig {
|
|
23
|
-
name: string;
|
|
24
|
-
description: string;
|
|
25
|
-
tools?: string[];
|
|
26
|
-
model?: 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 here = dirname(fileURLToPath(import.meta.url));
|
|
38
|
-
/** <package>/agents — the agents shipped with this extension. */
|
|
39
|
-
export const BUILTIN_AGENTS_DIR = join(here, "..", "agents");
|
|
40
|
-
|
|
41
|
-
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
42
|
-
const agents: AgentConfig[] = [];
|
|
43
|
-
if (!existsSync(dir)) return agents;
|
|
44
|
-
|
|
45
|
-
let entries: Dirent[];
|
|
46
|
-
try {
|
|
47
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
48
|
-
} catch {
|
|
49
|
-
return agents;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
for (const entry of entries) {
|
|
53
|
-
if (!entry.name.endsWith(".md")) continue;
|
|
54
|
-
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
55
|
-
|
|
56
|
-
const filePath = join(dir, entry.name);
|
|
57
|
-
let content: string;
|
|
58
|
-
try {
|
|
59
|
-
content = readFileSync(filePath, "utf-8");
|
|
60
|
-
} catch {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
65
|
-
// name + description are required; skip malformed files silently.
|
|
66
|
-
if (!frontmatter.name || !frontmatter.description) continue;
|
|
67
|
-
|
|
68
|
-
const tools = frontmatter.tools
|
|
69
|
-
?.split(",")
|
|
70
|
-
.map((t) => t.trim())
|
|
71
|
-
.filter(Boolean);
|
|
72
|
-
|
|
73
|
-
agents.push({
|
|
74
|
-
name: frontmatter.name,
|
|
75
|
-
description: frontmatter.description,
|
|
76
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
77
|
-
model: frontmatter.model,
|
|
78
|
-
systemPrompt: body,
|
|
79
|
-
source,
|
|
80
|
-
filePath,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return agents;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function isDirectory(p: string): boolean {
|
|
88
|
-
try {
|
|
89
|
-
return statSync(p).isDirectory();
|
|
90
|
-
} catch {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
96
|
-
let currentDir = cwd;
|
|
97
|
-
while (true) {
|
|
98
|
-
const candidate = join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
99
|
-
if (isDirectory(candidate)) return candidate;
|
|
100
|
-
const parentDir = dirname(currentDir);
|
|
101
|
-
if (parentDir === currentDir) return null;
|
|
102
|
-
currentDir = parentDir;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface DiscoverOptions {
|
|
107
|
-
/** Which directories to read from. Default: "user". */
|
|
108
|
-
scope?: AgentScope;
|
|
109
|
-
/** If provided and non-empty, only agents whose name is listed are returned. */
|
|
110
|
-
enabledNames?: readonly string[];
|
|
111
|
-
/** Per-agent model override ("provider/model-id"), keyed by agent name. */
|
|
112
|
-
modelOverrides?: Record<string, string>;
|
|
113
|
-
/** Override the built-in agents directory (used by tests). */
|
|
114
|
-
builtinDir?: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Discover agents across scopes, apply enable-filter and model overrides.
|
|
119
|
-
* Override priority for the same name: project > user > builtin.
|
|
120
|
-
*/
|
|
121
|
-
export function discoverAgents(cwd: string, options: DiscoverOptions = {}): AgentDiscoveryResult {
|
|
122
|
-
const scope = options.scope ?? "user";
|
|
123
|
-
const builtinDir = options.builtinDir ?? BUILTIN_AGENTS_DIR;
|
|
124
|
-
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
125
|
-
|
|
126
|
-
const builtin = loadAgentsFromDir(builtinDir, "builtin");
|
|
127
|
-
const user = scope === "project" ? [] : loadAgentsFromDir(join(getAgentDir(), "agents"), "user");
|
|
128
|
-
const project =
|
|
129
|
-
scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
130
|
-
|
|
131
|
-
// Merge with override priority builtin < user < project.
|
|
132
|
-
const byName = new Map<string, AgentConfig>();
|
|
133
|
-
for (const agent of builtin) byName.set(agent.name, agent);
|
|
134
|
-
for (const agent of user) byName.set(agent.name, agent);
|
|
135
|
-
for (const agent of project) byName.set(agent.name, agent);
|
|
136
|
-
|
|
137
|
-
let agents = Array.from(byName.values());
|
|
138
|
-
|
|
139
|
-
if (options.enabledNames && options.enabledNames.length > 0) {
|
|
140
|
-
const enabled = new Set(options.enabledNames);
|
|
141
|
-
agents = agents.filter((agent) => enabled.has(agent.name));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (options.modelOverrides) {
|
|
145
|
-
agents = agents.map((agent) => {
|
|
146
|
-
const override = options.modelOverrides?.[agent.name];
|
|
147
|
-
return override ? { ...agent, model: override } : agent;
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return { agents, projectAgentsDir };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** One-line catalog entry for system-prompt injection and error messages. */
|
|
155
|
-
export function formatCatalogEntry(agent: AgentConfig): string {
|
|
156
|
-
return `- ${agent.name}: ${agent.description}`;
|
|
157
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery.
|
|
3
|
+
*
|
|
4
|
+
* Agents are Markdown files (YAML frontmatter + body-as-system-prompt) loaded from
|
|
5
|
+
* three scopes with override priority builtin < user < project (same `name` wins
|
|
6
|
+
* at the higher scope). Discovery is re-run on every invocation so editing a file or
|
|
7
|
+
* dropping a new one takes effect mid-session without a reload.
|
|
8
|
+
*
|
|
9
|
+
* builtin : <package>/agents (shipped with this extension)
|
|
10
|
+
* user : <agentDir>/agents (~/.pi/agent/agents)
|
|
11
|
+
* project : <cwd...>/.pi/agents (nearest, walking up)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { type Dirent, existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import type { AgentScope } from "./config.ts";
|
|
19
|
+
|
|
20
|
+
export type AgentSource = "builtin" | "user" | "project";
|
|
21
|
+
|
|
22
|
+
export interface AgentConfig {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
tools?: string[];
|
|
26
|
+
model?: 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 here = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
/** <package>/agents — the agents shipped with this extension. */
|
|
39
|
+
export const BUILTIN_AGENTS_DIR = join(here, "..", "agents");
|
|
40
|
+
|
|
41
|
+
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
42
|
+
const agents: AgentConfig[] = [];
|
|
43
|
+
if (!existsSync(dir)) return agents;
|
|
44
|
+
|
|
45
|
+
let entries: Dirent[];
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
} catch {
|
|
49
|
+
return agents;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
54
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
55
|
+
|
|
56
|
+
const filePath = join(dir, entry.name);
|
|
57
|
+
let content: string;
|
|
58
|
+
try {
|
|
59
|
+
content = readFileSync(filePath, "utf-8");
|
|
60
|
+
} catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
65
|
+
// name + description are required; skip malformed files silently.
|
|
66
|
+
if (!frontmatter.name || !frontmatter.description) continue;
|
|
67
|
+
|
|
68
|
+
const tools = frontmatter.tools
|
|
69
|
+
?.split(",")
|
|
70
|
+
.map((t) => t.trim())
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
|
|
73
|
+
agents.push({
|
|
74
|
+
name: frontmatter.name,
|
|
75
|
+
description: frontmatter.description,
|
|
76
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
77
|
+
model: frontmatter.model,
|
|
78
|
+
systemPrompt: body,
|
|
79
|
+
source,
|
|
80
|
+
filePath,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return agents;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isDirectory(p: string): boolean {
|
|
88
|
+
try {
|
|
89
|
+
return statSync(p).isDirectory();
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
96
|
+
let currentDir = cwd;
|
|
97
|
+
while (true) {
|
|
98
|
+
const candidate = join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
99
|
+
if (isDirectory(candidate)) return candidate;
|
|
100
|
+
const parentDir = dirname(currentDir);
|
|
101
|
+
if (parentDir === currentDir) return null;
|
|
102
|
+
currentDir = parentDir;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DiscoverOptions {
|
|
107
|
+
/** Which directories to read from. Default: "user". */
|
|
108
|
+
scope?: AgentScope;
|
|
109
|
+
/** If provided and non-empty, only agents whose name is listed are returned. */
|
|
110
|
+
enabledNames?: readonly string[];
|
|
111
|
+
/** Per-agent model override ("provider/model-id"), keyed by agent name. */
|
|
112
|
+
modelOverrides?: Record<string, string>;
|
|
113
|
+
/** Override the built-in agents directory (used by tests). */
|
|
114
|
+
builtinDir?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Discover agents across scopes, apply enable-filter and model overrides.
|
|
119
|
+
* Override priority for the same name: project > user > builtin.
|
|
120
|
+
*/
|
|
121
|
+
export function discoverAgents(cwd: string, options: DiscoverOptions = {}): AgentDiscoveryResult {
|
|
122
|
+
const scope = options.scope ?? "user";
|
|
123
|
+
const builtinDir = options.builtinDir ?? BUILTIN_AGENTS_DIR;
|
|
124
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
125
|
+
|
|
126
|
+
const builtin = loadAgentsFromDir(builtinDir, "builtin");
|
|
127
|
+
const user = scope === "project" ? [] : loadAgentsFromDir(join(getAgentDir(), "agents"), "user");
|
|
128
|
+
const project =
|
|
129
|
+
scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
130
|
+
|
|
131
|
+
// Merge with override priority builtin < user < project.
|
|
132
|
+
const byName = new Map<string, AgentConfig>();
|
|
133
|
+
for (const agent of builtin) byName.set(agent.name, agent);
|
|
134
|
+
for (const agent of user) byName.set(agent.name, agent);
|
|
135
|
+
for (const agent of project) byName.set(agent.name, agent);
|
|
136
|
+
|
|
137
|
+
let agents = Array.from(byName.values());
|
|
138
|
+
|
|
139
|
+
if (options.enabledNames && options.enabledNames.length > 0) {
|
|
140
|
+
const enabled = new Set(options.enabledNames);
|
|
141
|
+
agents = agents.filter((agent) => enabled.has(agent.name));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.modelOverrides) {
|
|
145
|
+
agents = agents.map((agent) => {
|
|
146
|
+
const override = options.modelOverrides?.[agent.name];
|
|
147
|
+
return override ? { ...agent, model: override } : agent;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { agents, projectAgentsDir };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** One-line catalog entry for system-prompt injection and error messages. */
|
|
155
|
+
export function formatCatalogEntry(agent: AgentConfig): string {
|
|
156
|
+
return `- ${agent.name}: ${agent.description}`;
|
|
157
|
+
}
|