@ferris1225/pi-subagents 4.2.7 → 4.2.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/README.md CHANGED
@@ -183,7 +183,7 @@ two lines: what it is — agent, task, token flow, cost, provider/model, elapsed
183
183
  and, dim under the label column, what it is doing right now:
184
184
 
185
185
  ```text
186
- ● #12 executor src/cache.ts · wt:a91f3c · ↑5.2k ↓41.0k R210.0k W6.1k $1.9400 · 12m06s
186
+ ● #12 executor src/cache.ts · worktree:a91f3c · ↑5.2k ↓41.0k R210.0k W6.1k $1.9400 · 12m06s
187
187
  ↳ edit src/auth.ts
188
188
  ● #15 explorer src/models.ts · ↑1.2k ↓8.4k R31.0k W1.1k $0.0900 · openai/gpt-5-mini · 3m07s
189
189
  ↳ grep fallback
@@ -199,13 +199,36 @@ carries a dim `↻` in its agent column with its cumulative time. The widget is
199
199
  capped at ten lines: when many runs are live, extra runs collapse into a
200
200
  `… +N more` marker so the editor keeps its space.
201
201
 
202
+ The widget is the detailed surface, but it only pays off while you are looking
203
+ at it. A one-line roll-up in the always-visible footer answers "is anything
204
+ still working?" without opening the widget or asking:
205
+
206
+ ```text
207
+ subagents 2 running · 1 repo lane · 3 done
208
+ ```
209
+
210
+ It is count-only, keeps the same wait vocabulary as the widget, disappears when
211
+ nothing is active, and works in RPC hosts as well as the TUI.
212
+
202
213
  Completions resume the main agent on their own, with a compact block of at most 40
203
214
  lines by default; longer output lands unchanged in a Markdown artifact whose path
204
- comes with the message. Roles write result-only handoffs outcome, paths,
215
+ comes with the message, stated as how much was actually cut (`40 of 137 lines
216
+ shown`) and conditioned on the shown lines being insufficient, so the same
217
+ content does not enter the main context twice. Roles write result-only handoffs — outcome, paths,
205
218
  verification, unresolved blockers — and the main agent is told to add its
206
219
  conclusion rather than restate what you already read. A failed run adds its
207
220
  failed-tool diagnostics.
208
221
 
222
+ Delivery is held while a context compaction is in flight and released once it
223
+ settles — on failure and abort too — so a result a child spent minutes producing
224
+ is never swallowed by the summary that replaces the history.
225
+
226
+ A `wait: true` dispatch streams its progress onto the tool card while it waits,
227
+ and reports the awaited children's token spend as the tool call's own usage, so
228
+ sub-agent cost lands in the footer, `/session`, and RPC session totals. A
229
+ background dispatch returns before its children finish, so it reports no usage
230
+ rather than a fabricated number.
231
+
209
232
  ## Models, thinking, and tools
210
233
 
211
234
  Each agent runs on the current main model or on one you pick in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "4.2.7",
3
+ "version": "4.2.8",
4
4
  "description": "A managed sub-agent team for pi: specialized roles, pre-commit documentation sync, retained threads, auto-fix chains, model fallback, and Git worktree isolation.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/agents.ts CHANGED
@@ -1,237 +1,237 @@
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 { THINKING_LEVEL_VALUES, type AgentScope, type ThinkingLevel } from "./config.ts";
19
- import type { IsolationMode } from "./worktree.ts";
20
-
21
- export type AgentSource = "builtin" | "user" | "project";
22
-
23
- export interface AgentConfig {
24
- name: string;
25
- description: string;
26
- tools?: string[];
27
- /** Model ref this run was routed to; filled in by dispatch, never declared by the agent file. */
28
- model?: string;
29
- /** Per-agent default thinking strength (frontmatter `thinking`); config override wins. */
30
- thinking?: ThinkingLevel;
31
- /** Role-declared default isolation (frontmatter `isolation`); an explicit
32
- * per-call request wins, and `worktree` applies to write-capable roles only. */
33
- isolation?: IsolationMode;
34
- systemPrompt: string;
35
- source: AgentSource;
36
- filePath: string;
37
- }
38
-
39
- const SHELL_TOOL_NAMES = new Set(["bash", "powershell"]);
40
- /** The shell that actually fits the host: PowerShell on Windows, Bash elsewhere.
41
- * Only used to break a tie when the parent has both enabled — a parent running a
42
- * single shell is followed as configured, whatever it is. */
43
- const NATIVE_SHELL_TOOL = process.platform === "win32" ? "powershell" : "bash";
44
- const PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
45
- export const SUBAGENT_TOOL_NAMES = [
46
- "subagent",
47
- "subagent_control",
48
- "subagent_stop",
49
- ] as const;
50
- const SUBAGENT_TOOL_NAME_SET = new Set<string>(SUBAGENT_TOOL_NAMES);
51
-
52
- /** Resolve every child against the parent's live tool selection. Roles without
53
- * an allowlist inherit the complete active set. Explicit lists keep only their
54
- * declared Pi built-ins, adapt an existing shell slot, and gain active extension/
55
- * SDK tools. pi-subagents controls are always removed so children stay leaves.
56
- *
57
- * A declared shell is one slot, so it resolves to one shell: the parent's, and
58
- * the host-native one when the parent runs both. A child never inherits a shell
59
- * the parent does not have — Pi's `--tools` allowlist overrides the child's own
60
- * `defaultTools` setting, so naming a shell the user disabled would hand it a
61
- * terminal they deliberately turned off. */
62
- export function resolveAgentTools(
63
- agent: AgentConfig,
64
- activeToolNames: readonly string[],
65
- ): AgentConfig {
66
- const active = [...new Set(activeToolNames)].filter((tool) => !SUBAGENT_TOOL_NAME_SET.has(tool));
67
- if (!agent.tools) return { ...agent, tools: active };
68
-
69
- const parentShellTools = active.filter((tool) => SHELL_TOOL_NAMES.has(tool));
70
- const activeShellTools = parentShellTools.length > 1 && parentShellTools.includes(NATIVE_SHELL_TOOL)
71
- ? [NATIVE_SHELL_TOOL]
72
- : parentShellTools;
73
- const tools: string[] = [];
74
- let shellAdapted = false;
75
- for (const tool of agent.tools) {
76
- if (SHELL_TOOL_NAMES.has(tool)) {
77
- if (!shellAdapted) tools.push(...activeShellTools);
78
- shellAdapted = true;
79
- } else if (PI_BUILTIN_TOOL_NAMES.has(tool) && !tools.includes(tool)) {
80
- tools.push(tool);
81
- }
82
- }
83
- for (const tool of active) {
84
- if (PI_BUILTIN_TOOL_NAMES.has(tool) || tools.includes(tool)) continue;
85
- tools.push(tool);
86
- }
87
- return { ...agent, tools };
88
- }
89
-
90
- /** Filesystem-write capability used by worktree admission and repository-lane
91
- * safety. Built-in read-only role names remain read-only even when overridden;
92
- * an omitted tool list inherits the parent's active set, so it counts as
93
- * write-capable unless the parent itself is read-only. */
94
- export function isWriteCapableAgent(
95
- agent: Pick<AgentConfig, "name" | "tools">,
96
- ): boolean {
97
- if (agent.name === "explorer") return false;
98
- if (agent.name === "executor") return true;
99
- if (!agent.tools) return true;
100
- return agent.tools.includes("edit") || agent.tools.includes("write");
101
- }
102
-
103
- const here = dirname(fileURLToPath(import.meta.url));
104
- /** <package>/agents — the agents shipped with this extension. */
105
- export const BUILTIN_AGENTS_DIR = join(here, "..", "agents");
106
-
107
- /** Agents shipped with the package (used by the setup wizard for per-agent defaults). */
108
- export function loadBuiltinAgents(): AgentConfig[] {
109
- return loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
110
- }
111
-
112
- function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
113
- const agents: AgentConfig[] = [];
114
- if (!existsSync(dir)) return agents;
115
-
116
- let entries: Dirent[];
117
- try {
118
- entries = readdirSync(dir, { withFileTypes: true });
119
- } catch {
120
- return agents;
121
- }
122
-
123
- for (const entry of entries) {
124
- if (!entry.name.endsWith(".md")) continue;
125
- if (!entry.isFile() && !entry.isSymbolicLink()) continue;
126
-
127
- const filePath = join(dir, entry.name);
128
- let content: string;
129
- try {
130
- content = readFileSync(filePath, "utf-8");
131
- } catch {
132
- continue;
133
- }
134
-
135
- const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
136
- // YAML values are not guaranteed strings; anything non-string is invalid for these fields.
137
- const str = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
138
- const name = str(frontmatter.name);
139
- const description = str(frontmatter.description);
140
- // name + description are required; skip malformed files silently.
141
- if (!name || !description) continue;
142
-
143
- const rawTools = str(frontmatter.tools);
144
- const tools = rawTools
145
- ?.split(",")
146
- .map((t) => t.trim())
147
- .filter(Boolean);
148
- const rawThinking = str(frontmatter.thinking)?.trim();
149
- const thinking = (THINKING_LEVEL_VALUES as readonly string[]).includes(rawThinking ?? "")
150
- ? (rawThinking as ThinkingLevel)
151
- : undefined;
152
- const rawIsolation = str(frontmatter.isolation)?.trim();
153
- const isolation = rawIsolation === "worktree" || rawIsolation === "shared"
154
- ? (rawIsolation as IsolationMode)
155
- : undefined;
156
-
157
- agents.push({
158
- name,
159
- description,
160
- tools: tools && tools.length > 0 ? tools : undefined,
161
- ...(thinking ? { thinking } : {}),
162
- ...(isolation ? { isolation } : {}),
163
- systemPrompt: body,
164
- source,
165
- filePath,
166
- });
167
- }
168
-
169
- return agents;
170
- }
171
-
172
- function isDirectory(p: string): boolean {
173
- try {
174
- return statSync(p).isDirectory();
175
- } catch {
176
- return false;
177
- }
178
- }
179
-
180
- function findNearestProjectAgentsDir(cwd: string): string | null {
181
- let currentDir = cwd;
182
- while (true) {
183
- const candidate = join(currentDir, CONFIG_DIR_NAME, "agents");
184
- if (isDirectory(candidate)) return candidate;
185
- const parentDir = dirname(currentDir);
186
- if (parentDir === currentDir) return null;
187
- currentDir = parentDir;
188
- }
189
- }
190
-
191
- export interface DiscoverOptions {
192
- /** Which directories to read from. Default: "user". */
193
- scope?: AgentScope;
194
- /** If provided, only agents whose name is listed are returned. */
195
- enabledNames?: readonly string[];
196
- /** Project-controlled prompts are loaded only after Pi trusts the project. */
197
- projectTrusted?: boolean;
198
- /** Override the built-in agents directory (used by tests). */
199
- builtinDir?: string;
200
- }
201
-
202
- /**
203
- * Discover agents across scopes and apply the enabled-name filter.
204
- * Override priority for the same name: project > user > builtin.
205
- */
206
- export function discoverAgents(cwd: string, options: DiscoverOptions = {}): { agents: AgentConfig[] } {
207
- const scope = options.scope ?? "user";
208
- const builtinDir = options.builtinDir ?? BUILTIN_AGENTS_DIR;
209
- const projectAgentsDir = findNearestProjectAgentsDir(cwd);
210
-
211
- const builtin = loadAgentsFromDir(builtinDir, "builtin");
212
- const user = scope === "project" ? [] : loadAgentsFromDir(join(getAgentDir(), "agents"), "user");
213
- const project =
214
- scope === "user" || !projectAgentsDir || options.projectTrusted !== true
215
- ? []
216
- : loadAgentsFromDir(projectAgentsDir, "project");
217
-
218
- // Merge with override priority builtin < user < project.
219
- const byName = new Map<string, AgentConfig>();
220
- for (const agent of builtin) byName.set(agent.name, agent);
221
- for (const agent of user) byName.set(agent.name, agent);
222
- for (const agent of project) byName.set(agent.name, agent);
223
-
224
- let agents = Array.from(byName.values());
225
-
226
- if (options.enabledNames !== undefined) {
227
- const enabled = new Set(options.enabledNames);
228
- agents = agents.filter((agent) => enabled.has(agent.name));
229
- }
230
-
231
- return { agents };
232
- }
233
-
234
- /** One-line catalog entry for system-prompt injection and error messages. */
235
- export function formatCatalogEntry(agent: AgentConfig): string {
236
- return `- ${agent.name}: ${agent.description}`;
237
- }
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 { THINKING_LEVEL_VALUES, type AgentScope, type ThinkingLevel } from "./config.ts";
19
+ import type { IsolationMode } from "./worktree.ts";
20
+
21
+ export type AgentSource = "builtin" | "user" | "project";
22
+
23
+ export interface AgentConfig {
24
+ name: string;
25
+ description: string;
26
+ tools?: string[];
27
+ /** Model ref this run was routed to; filled in by dispatch, never declared by the agent file. */
28
+ model?: string;
29
+ /** Per-agent default thinking strength (frontmatter `thinking`); config override wins. */
30
+ thinking?: ThinkingLevel;
31
+ /** Role-declared default isolation (frontmatter `isolation`); an explicit
32
+ * per-call request wins, and `worktree` applies to write-capable roles only. */
33
+ isolation?: IsolationMode;
34
+ systemPrompt: string;
35
+ source: AgentSource;
36
+ filePath: string;
37
+ }
38
+
39
+ const SHELL_TOOL_NAMES = new Set(["bash", "powershell"]);
40
+ /** The shell that actually fits the host: PowerShell on Windows, Bash elsewhere.
41
+ * Only used to break a tie when the parent has both enabled — a parent running a
42
+ * single shell is followed as configured, whatever it is. */
43
+ const NATIVE_SHELL_TOOL = process.platform === "win32" ? "powershell" : "bash";
44
+ const PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
45
+ export const SUBAGENT_TOOL_NAMES = [
46
+ "subagent",
47
+ "subagent_control",
48
+ "subagent_stop",
49
+ ] as const;
50
+ const SUBAGENT_TOOL_NAME_SET = new Set<string>(SUBAGENT_TOOL_NAMES);
51
+
52
+ /** Resolve every child against the parent's live tool selection. Roles without
53
+ * an allowlist inherit the complete active set. Explicit lists keep only their
54
+ * declared Pi built-ins, adapt an existing shell slot, and gain active extension/
55
+ * SDK tools. pi-subagents controls are always removed so children stay leaves.
56
+ *
57
+ * A declared shell is one slot, so it resolves to one shell: the parent's, and
58
+ * the host-native one when the parent runs both. A child never inherits a shell
59
+ * the parent does not have — Pi's `--tools` allowlist overrides the child's own
60
+ * `defaultTools` setting, so naming a shell the user disabled would hand it a
61
+ * terminal they deliberately turned off. */
62
+ export function resolveAgentTools(
63
+ agent: AgentConfig,
64
+ activeToolNames: readonly string[],
65
+ ): AgentConfig {
66
+ const active = [...new Set(activeToolNames)].filter((tool) => !SUBAGENT_TOOL_NAME_SET.has(tool));
67
+ if (!agent.tools) return { ...agent, tools: active };
68
+
69
+ const parentShellTools = active.filter((tool) => SHELL_TOOL_NAMES.has(tool));
70
+ const activeShellTools = parentShellTools.length > 1 && parentShellTools.includes(NATIVE_SHELL_TOOL)
71
+ ? [NATIVE_SHELL_TOOL]
72
+ : parentShellTools;
73
+ const tools: string[] = [];
74
+ let shellAdapted = false;
75
+ for (const tool of agent.tools) {
76
+ if (SHELL_TOOL_NAMES.has(tool)) {
77
+ if (!shellAdapted) tools.push(...activeShellTools);
78
+ shellAdapted = true;
79
+ } else if (PI_BUILTIN_TOOL_NAMES.has(tool) && !tools.includes(tool)) {
80
+ tools.push(tool);
81
+ }
82
+ }
83
+ for (const tool of active) {
84
+ if (PI_BUILTIN_TOOL_NAMES.has(tool) || tools.includes(tool)) continue;
85
+ tools.push(tool);
86
+ }
87
+ return { ...agent, tools };
88
+ }
89
+
90
+ /** Filesystem-write capability used by worktree admission and repository-lane
91
+ * safety. Built-in read-only role names remain read-only even when overridden;
92
+ * an omitted tool list inherits the parent's active set, so it counts as
93
+ * write-capable unless the parent itself is read-only. */
94
+ export function isWriteCapableAgent(
95
+ agent: Pick<AgentConfig, "name" | "tools">,
96
+ ): boolean {
97
+ if (agent.name === "explorer") return false;
98
+ if (agent.name === "executor") return true;
99
+ if (!agent.tools) return true;
100
+ return agent.tools.includes("edit") || agent.tools.includes("write");
101
+ }
102
+
103
+ const here = dirname(fileURLToPath(import.meta.url));
104
+ /** <package>/agents — the agents shipped with this extension. */
105
+ export const BUILTIN_AGENTS_DIR = join(here, "..", "agents");
106
+
107
+ /** Agents shipped with the package (used by the setup wizard for per-agent defaults). */
108
+ export function loadBuiltinAgents(): AgentConfig[] {
109
+ return loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
110
+ }
111
+
112
+ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
113
+ const agents: AgentConfig[] = [];
114
+ if (!existsSync(dir)) return agents;
115
+
116
+ let entries: Dirent[];
117
+ try {
118
+ entries = readdirSync(dir, { withFileTypes: true });
119
+ } catch {
120
+ return agents;
121
+ }
122
+
123
+ for (const entry of entries) {
124
+ if (!entry.name.endsWith(".md")) continue;
125
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
126
+
127
+ const filePath = join(dir, entry.name);
128
+ let content: string;
129
+ try {
130
+ content = readFileSync(filePath, "utf-8");
131
+ } catch {
132
+ continue;
133
+ }
134
+
135
+ const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
136
+ // YAML values are not guaranteed strings; anything non-string is invalid for these fields.
137
+ const str = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
138
+ const name = str(frontmatter.name);
139
+ const description = str(frontmatter.description);
140
+ // name + description are required; skip malformed files silently.
141
+ if (!name || !description) continue;
142
+
143
+ const rawTools = str(frontmatter.tools);
144
+ const tools = rawTools
145
+ ?.split(",")
146
+ .map((t) => t.trim())
147
+ .filter(Boolean);
148
+ const rawThinking = str(frontmatter.thinking)?.trim();
149
+ const thinking = (THINKING_LEVEL_VALUES as readonly string[]).includes(rawThinking ?? "")
150
+ ? (rawThinking as ThinkingLevel)
151
+ : undefined;
152
+ const rawIsolation = str(frontmatter.isolation)?.trim();
153
+ const isolation = rawIsolation === "worktree" || rawIsolation === "shared"
154
+ ? (rawIsolation as IsolationMode)
155
+ : undefined;
156
+
157
+ agents.push({
158
+ name,
159
+ description,
160
+ tools: tools && tools.length > 0 ? tools : undefined,
161
+ ...(thinking ? { thinking } : {}),
162
+ ...(isolation ? { isolation } : {}),
163
+ systemPrompt: body,
164
+ source,
165
+ filePath,
166
+ });
167
+ }
168
+
169
+ return agents;
170
+ }
171
+
172
+ function isDirectory(p: string): boolean {
173
+ try {
174
+ return statSync(p).isDirectory();
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ function findNearestProjectAgentsDir(cwd: string): string | null {
181
+ let currentDir = cwd;
182
+ while (true) {
183
+ const candidate = join(currentDir, CONFIG_DIR_NAME, "agents");
184
+ if (isDirectory(candidate)) return candidate;
185
+ const parentDir = dirname(currentDir);
186
+ if (parentDir === currentDir) return null;
187
+ currentDir = parentDir;
188
+ }
189
+ }
190
+
191
+ export interface DiscoverOptions {
192
+ /** Which directories to read from. Default: "user". */
193
+ scope?: AgentScope;
194
+ /** If provided, only agents whose name is listed are returned. */
195
+ enabledNames?: readonly string[];
196
+ /** Project-controlled prompts are loaded only after Pi trusts the project. */
197
+ projectTrusted?: boolean;
198
+ /** Override the built-in agents directory (used by tests). */
199
+ builtinDir?: string;
200
+ }
201
+
202
+ /**
203
+ * Discover agents across scopes and apply the enabled-name filter.
204
+ * Override priority for the same name: project > user > builtin.
205
+ */
206
+ export function discoverAgents(cwd: string, options: DiscoverOptions = {}): { agents: AgentConfig[] } {
207
+ const scope = options.scope ?? "user";
208
+ const builtinDir = options.builtinDir ?? BUILTIN_AGENTS_DIR;
209
+ const projectAgentsDir = findNearestProjectAgentsDir(cwd);
210
+
211
+ const builtin = loadAgentsFromDir(builtinDir, "builtin");
212
+ const user = scope === "project" ? [] : loadAgentsFromDir(join(getAgentDir(), "agents"), "user");
213
+ const project =
214
+ scope === "user" || !projectAgentsDir || options.projectTrusted !== true
215
+ ? []
216
+ : loadAgentsFromDir(projectAgentsDir, "project");
217
+
218
+ // Merge with override priority builtin < user < project.
219
+ const byName = new Map<string, AgentConfig>();
220
+ for (const agent of builtin) byName.set(agent.name, agent);
221
+ for (const agent of user) byName.set(agent.name, agent);
222
+ for (const agent of project) byName.set(agent.name, agent);
223
+
224
+ let agents = Array.from(byName.values());
225
+
226
+ if (options.enabledNames !== undefined) {
227
+ const enabled = new Set(options.enabledNames);
228
+ agents = agents.filter((agent) => enabled.has(agent.name));
229
+ }
230
+
231
+ return { agents };
232
+ }
233
+
234
+ /** One-line catalog entry for system-prompt injection and error messages. */
235
+ export function formatCatalogEntry(agent: AgentConfig): string {
236
+ return `- ${agent.name}: ${agent.description}`;
237
+ }