pi-maestro-teammate 0.4.2 → 0.4.4

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
@@ -44,11 +44,12 @@ Precedence is task-level `model` → top-level `model` → explicit `taskType` m
44
44
 
45
45
  ```
46
46
  { tasks: [
47
- { agent: "scout", task: "Find all API endpoints" },
48
- { agent: "scout", task: "Map database schemas" },
49
- { agent: "scout", task: "List external dependencies" }
47
+ { agent: "explorer", name: "api", task: "Find all API endpoints" },
48
+ { agent: "explorer", name: "db", task: "Map database schemas" },
49
+ { agent: "explorer", name: "deps", task: "List external dependencies" }
50
50
  ],
51
- concurrency: 3
51
+ concurrency: 3,
52
+ background: false
52
53
  }
53
54
  ```
54
55
 
@@ -56,9 +57,11 @@ Precedence is task-level `model` → top-level `model` → explicit `taskType` m
56
57
 
57
58
  ```
58
59
  { tasks: [
59
- { agent: "scout", name: "recon", task: "Find the auth module structure" },
60
- { agent: "delegate", task: "Based on this context: {recon}\n\nRefactor the auth module" }
61
- ]
60
+ { agent: "explorer", name: "recon", task: "Find the auth module structure" },
61
+ { agent: "delegate", name: "implement", task: "Based on this context: {recon}\n\nRefactor the auth module" }
62
+ ],
63
+ concurrency: 1,
64
+ background: false
62
65
  }
63
66
  ```
64
67
 
@@ -66,24 +69,31 @@ Precedence is task-level `model` → top-level `model` → explicit `taskType` m
66
69
 
67
70
  ```
68
71
  { tasks: [
69
- { agent: "scout", name: "api", task: "List all API routes",
72
+ { agent: "explorer", name: "api", task: "List all API routes",
70
73
  outputSchema: {
71
74
  type: "object",
72
75
  properties: { routes: { type: "array", items: { type: "string" } } },
73
76
  required: ["routes"]
74
77
  } },
75
- { agent: "scout", name: "db", task: "Map the database schema" },
76
- { agent: "reviewer", task: "Routes: {api.routes}\nDB: {db}\n\nCheck consistency" }
77
- ]
78
+ { agent: "explorer", name: "db", task: "Map the database schema" },
79
+ { agent: "delegate", name: "verify", task: "Routes: {api.routes}\nDB: {db}\n\nCheck consistency" }
80
+ ],
81
+ concurrency: 2,
82
+ background: false
78
83
  }
79
84
  ```
80
85
 
81
- `api` and `db` run in parallel. `reviewer` waits for both, with `{api.routes}` resolved from structured output and `{db}` from text output.
86
+ `api` and `db` run in parallel. `delegate` waits for both, with `{api.routes}` resolved from structured output and `{db}` from text output.
87
+
88
+ For production nested dispatch, give every task a unique `name`, set an explicit
89
+ provider-safe `concurrency`, and use `background: false` whenever the parent must
90
+ consume all child results before it continues. Background runs return immediately
91
+ and report completion to the root session.
82
92
 
83
93
  ### Structured Output
84
94
 
85
95
  ```
86
- { agent: "scout", task: "List all API routes",
96
+ { agent: "explorer", task: "List all API routes",
87
97
  outputSchema: {
88
98
  type: "object",
89
99
  properties: { routes: { type: "array", items: { type: "string" } } },
@@ -164,12 +174,12 @@ dispatch → running → turn complete → sleeping → teammate-send → runnin
164
174
 
165
175
  Time spent sleeping is excluded from the displayed duration. `sleepMs` accumulates total sleep time; displayed uptime = wall clock − sleep time.
166
176
 
167
- ### Agent Fallback
177
+ ### Agent Resolution
168
178
 
169
- Any agent name works — if no `.md` definition file exists, a generic config is used:
170
- - `tools`: read, grep, find, ls, bash, edit, write (+ teammate proxy tools)
171
- - `systemPromptMode`: append (inherits pi default system prompt)
172
- - `inheritProjectContext`: true
179
+ Agent names must resolve to one of the three reserved builtin roles (`delegate`,
180
+ `explorer`, `workflow`) or to a discovered project/user role. Unknown names return
181
+ an error with the available role catalog instead of silently using a generic
182
+ configuration. The legacy name `coordinator` resolves to `workflow`.
173
183
 
174
184
  ## TaskSpec Schema
175
185
 
@@ -221,14 +231,14 @@ The `chain` field is preserved for backward compatibility. It normalizes interna
221
231
  ```
222
232
  // This chain:
223
233
  { chain: [
224
- { agent: "scout", task: "Find auth code" },
234
+ { agent: "explorer", task: "Find auth code" },
225
235
  { agent: "delegate", task: "Fix: {previous}" }
226
236
  ]
227
237
  }
228
238
 
229
239
  // Is equivalent to:
230
240
  { tasks: [
231
- { agent: "scout", name: "_step0", task: "Find auth code" },
241
+ { agent: "explorer", name: "_step0", task: "Find auth code" },
232
242
  { agent: "delegate", name: "_step1", task: "Fix: {_step0}" }
233
243
  ]
234
244
  }
@@ -241,18 +251,19 @@ All agents are managed by the root process in a single flat `activeRuns` pool, r
241
251
  ### How It Works
242
252
 
243
253
  ```
244
- coordinator calls teammate({ agent: "scout", name: "recon" })
254
+ workflow calls teammate({ agent: "explorer", name: "recon" })
245
255
  │ IPC: teammate_proxy_request (process.send)
246
256
  ▼
247
- Root spawns scout → registers in root's activeRuns/namedAgents
257
+ Root spawns explorer → registers in root's activeRuns/namedAgents
248
258
  │ IPC: teammate_proxy_result (child.send)
249
259
  ▼
250
- coordinator receives result
260
+ workflow receives result
251
261
  ```
252
262
 
253
263
  All agents are flat peers:
254
264
  - `teammate-send({ to: "name" })` = one lookup in `namedAgents` → stdin. Direct delivery.
255
- - `teammate-list` = iterate `activeRuns`. Flat, simple.
265
+ - `teammate-list({ view: "active" | "named" | "all" })` = iterate running instances.
266
+ - `teammate-list({ view: "roles" })` = list all available builtin, project, and user-defined roles with descriptions.
256
267
  - `teammate-watch` = read agent's `outputLog`. Direct.
257
268
 
258
269
  ### Child Proxy Tools
@@ -263,12 +274,26 @@ Every child process automatically gets proxy versions of all 4 teammate tools (i
263
274
 
264
275
  The root's IPC message listener (`child.on("message")`) intercepts these requests and executes them locally.
265
276
 
277
+ ### Main-Session Interaction Relay
278
+
279
+ Parent extensions can register themselves for inheritance by teammate children. `pi-maestro-flow` uses this bridge automatically, so every child loads its permission hooks and receives the `ask-user-question` tool even when the role has an explicit `tools` whitelist.
280
+
281
+ When a child needs user input, it sends a reply-capable `teammate_interaction_request` to the root session:
282
+
283
+ - Permission requests are displayed in the main UI with `Allow once`, `Always allow`, and `Deny`. The selected action is returned to the blocked child tool call.
284
+ - `ask-user-question` requests are displayed in the main UI and return structured option/free-text answers to the child.
285
+ - Pending requests are tracked by `requestId` on the active agent and serialized through the root interaction queue, preventing overlapping prompts.
286
+ - Headless root sessions still run the parent permission broker: silent allow/deny decisions work, while permissions that require a dialog fail closed and questionnaires are cancelled.
287
+
288
+ The response uses `teammate_interaction_response` with the original `requestId`, so late or duplicate responses cannot resolve a different child request.
289
+
266
290
  ## Reliability
267
291
 
268
292
  - **Model fallback chain** — primary model → `fallbackModels[]` from agent config → automatic retry
269
293
  - **Flat agent pool** — all agents managed by root process; child proxy tools forward spawn requests to root; depth guard (`PI_TEAMMATE_DEPTH`) prevents runaway recursion
270
294
  - **Resident lifecycle** — agents sleep after turn completion; process stays alive for follow-up; only killed on explicit abort or session shutdown
271
295
  - **IPC disconnect guard** — child proxy resolves all pending requests with error on disconnect (root crash / agent abort)
296
+ - **Interaction fail-closed** — missing main UI, timeout, or relay failure never grants a child permission request
272
297
  - **Windows-safe pi resolution** — `getPiSpawnCommand()` resolves the pi binary via env override, Windows script detection, or PATH
273
298
  - **Abort signal** — SIGTERM → 5s grace → SIGKILL
274
299
 
@@ -331,11 +356,23 @@ pi install ./pi-teammate
331
356
  MIT
332
357
  # Agent discovery
333
358
 
334
- Agent Markdown files are loaded with project-over-user-over-builtin precedence:
359
+ The package always provides exactly three reserved builtin roles:
360
+
361
+ - `delegate`: general-purpose execution, including fixed prompt templates
362
+ - `explorer`: read-only code discovery and call-chain tracing
363
+ - `workflow`: dependency-aware DAG decomposition and teammate delegation
364
+
365
+ Other Agent Markdown files are discovered with the following precedence:
335
366
 
336
367
  1. nearest project `.pi/agents/*.md`
337
- 2. `~/.pi/agent/extensions/teammate/agents/*.md`
338
- 3. this npm package's bundled `agents/*.md`
368
+ 2. nearest project `.agents/*.md`
369
+ 3. `~/.agents/*.md`
370
+ 4. legacy `~/.pi/agent/extensions/teammate/agents/*.md`
371
+
372
+ Project and user files cannot override the three reserved builtin names. Their
373
+ `name` and `description` fields are refreshed into the active system prompt on
374
+ every `before_agent_start`; the Markdown body is loaded only after the role is
375
+ selected. Files without both fields are ignored. Unknown role names are rejected.
339
376
 
340
377
  Pi has no native `pi.agents` package manifest field. Builtin teammate agents are
341
378
  resolved relative to the installed extension module, so npm, git, global and local
@@ -1,13 +1,15 @@
1
1
  ---
2
2
  name: delegate
3
- description: Lightweight teammate agent that inherits the parent model for single-task execution
3
+ description: General-purpose teammate for direct tasks or reusable prompt templates
4
4
  systemPromptMode: append
5
5
  inheritProjectContext: true
6
6
  tools: read, grep, find, ls, bash, edit, write
7
7
  inheritSkills: false
8
8
  ---
9
9
 
10
- You are a delegated teammate agent. Execute the assigned task using the provided tools. Be direct, efficient, and keep the response focused on the requested work.
10
+ You are the general-purpose teammate agent. Execute the assigned task or resolved prompt template using the provided tools. Be direct, efficient, and keep the response focused on the requested work.
11
+
12
+ If the task specifies MODE: analysis, do not modify files. If it specifies MODE: write, implement the requested changes and verify them.
11
13
 
12
14
  Guidelines:
13
15
  - Read existing code before making changes
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: explorer
3
+ description: Read-only codebase discovery and call-chain tracing specialist
4
+ systemPromptMode: replace
5
+ thinking: low
6
+ tools: read, grep, find, ls
7
+ inheritProjectContext: false
8
+ inheritSkills: false
9
+ ---
10
+
11
+ You are a fast, read-only codebase exploration agent. Find concrete files, definitions, call sites, and data-flow relationships without modifying the workspace.
12
+
13
+ Your approach:
14
+ 1. Parse the request into target, scope, and acceptance conditions
15
+ 2. Search within the stated scope before widening it
16
+ 3. Read the most relevant matches to verify them
17
+ 4. Return concise findings with file and line anchors
18
+
19
+ Report ambiguity and negative evidence explicitly. Do not edit or create files.
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: goal-verifier
3
+ description: Independent read-only verifier for Goal completion claims
4
+ systemPromptMode: replace
5
+ inheritProjectContext: false
6
+ tools: read, grep, find, ls, bash
7
+ inheritSkills: false
8
+ ---
9
+
10
+ <role>
11
+ You are the independent, read-only verifier spawned automatically after a normal Goal agent loop ends.
12
+ Your only job is to decide whether the supplied completion claim satisfies every explicit requirement of the original Goal.
13
+
14
+ You do not own Goal lifecycle transitions. The parent extension applies your structured verdict: pass completes, fail continues, and missing or invalid output holds the active Goal.
15
+
16
+ Core responsibilities:
17
+ - Evaluate the supplied session and canonical Workflow evidence before doing any spot check.
18
+ - Perform only the smallest necessary read-only checks when decisive evidence is missing or stale.
19
+ - Return a grounded pass or fail verdict through `structured_output`.
20
+ </role>
21
+
22
+ <verdict_policy>
23
+ Treat missing evidence as a valid failure verdict, never as a reason to omit the result.
24
+
25
+ | Condition | Verdict |
26
+ |-----------|---------|
27
+ | Every explicit requirement has concrete, consistent evidence | `pass=true`, `unmet=[]` |
28
+ | Any requirement is incomplete, contradicted, or unsupported | `pass=false`, list it in `unmet` |
29
+ | A read-only check fails or cannot run | `pass=false`, name the verification gap in `unmet` |
30
+
31
+ Do not edit files, delegate work, broaden the Goal, attempt fixes, or run a broad unit-test suite unless the Goal explicitly requires that suite.
32
+ </verdict_policy>
33
+
34
+ <evidence_policy>
35
+ Prefer evidence already supplied by the parent session. A successful tool call or result in the transcript is valid evidence for that observed action.
36
+
37
+ | Good evidence | Bad substitution |
38
+ |---------------|------------------|
39
+ | The transcript contains the requested Goal action and its result | Running unrelated repository tests |
40
+ | A focused read-only command confirms a completion claim | Exploring the whole codebase without a concrete gap |
41
+
42
+ Canonical Workflow evidence is relevant only when it belongs to the Goal being judged; note unrelated Workflow state without treating it as proof.
43
+ </evidence_policy>
44
+
45
+ <output_contract>
46
+ The `structured_output` tool is available and mandatory. Call it exactly once as the final action on every path, including failure, missing evidence, or check errors. Populate all four fields: `pass`, `reasoning`, `unmet`, and `evidence`. Do not emit prose after the tool call.
47
+ </output_contract>
48
+
49
+ <quality_gate>
50
+ Before calling `structured_output`, verify:
51
+ - [ ] Every explicit Goal requirement has a corresponding evidence item or `unmet` entry.
52
+ - [ ] `pass=true` is used only when `unmet` is empty and evidence is concrete.
53
+ - [ ] Missing evidence produces `pass=false`, not a prose-only or inconclusive response.
54
+ - [ ] No write, delegation, or unrelated broad test was performed.
55
+ </quality_gate>
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: workflow
3
+ description: Decomposes complex problems and dispatches dependency-aware teammate DAGs
4
+ systemPromptMode: replace
5
+ inheritProjectContext: true
6
+ thinking: high
7
+ tools: read, grep, find, ls, bash, edit, write, teammate, teammate-send, teammate-list, teammate-watch
8
+ inheritSkills: false
9
+ ---
10
+
11
+ You are the workflow teammate responsible for solving multi-step problems through dependency-aware delegation.
12
+
13
+ When dispatching work with the teammate tool:
14
+ - Give every task a stable unique `name`
15
+ - Use `{name}` and `{name.field}` references to declare dependencies
16
+ - Use `outputSchema` when downstream tasks require structured fields
17
+ - Keep independent tasks in the same wave so the runtime can execute them in parallel
18
+ - Set `concurrency` explicitly to a provider-safe bound
19
+ - Set `background: false` whenever your next step depends on the child results
20
+ - Let the teammate runtime resolve and execute the DAG; do not simulate scheduling in prose
21
+
22
+ Your approach:
23
+ 1. Analyze the requested outcome and identify independently verifiable tasks
24
+ 2. Build the smallest useful DAG with explicit data flow
25
+ 3. Dispatch the DAG and monitor only when status materially affects the next decision
26
+ 4. Validate task outputs before synthesizing the final result
27
+ 5. Recover or report a concrete blocker when a dependency fails
28
+
29
+ Use teammate-send for targeted follow-up and teammate-list or teammate-watch only when live state is needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-maestro-teammate",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Pi extension — teammate agent dispatch with DAG task graphs, RPC messaging, and compact TUI",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -52,5 +52,11 @@
52
52
  "@earendil-works/pi-ai": "0.80.3",
53
53
  "@earendil-works/pi-coding-agent": "0.80.3",
54
54
  "@earendil-works/pi-tui": "0.80.3"
55
- }
55
+ },
56
+ "main": "index.js",
57
+ "directories": {
58
+ "test": "test"
59
+ },
60
+ "author": "",
61
+ "license": "ISC"
56
62
  }
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Agent discovery and configuration.
3
3
  *
4
- * Discovers agent definitions from three locations (in priority order):
5
- * 1. Project agents: .pi/agents/ in the nearest project root
6
- * 2. User agents: ~/.pi/agent/extensions/teammate/agents/
7
- * 3. Builtin agents: bundled agents/ directory in this package
4
+ * Discovers agent definitions from compatible project and user locations.
5
+ * Precedence: project .pi/agents > project .agents > ~/.agents > legacy user > builtin.
8
6
  */
9
7
 
10
8
  import * as fs from "node:fs";
@@ -12,17 +10,28 @@ import * as os from "node:os";
12
10
  import * as path from "node:path";
13
11
  import { fileURLToPath } from "node:url";
14
12
  import { parseFrontmatter } from "./frontmatter.ts";
13
+ import { parseTeammateThinkingLevel, type TeammateThinkingLevel } from "../shared/thinking.ts";
15
14
 
16
15
  type SystemPromptMode = "append" | "replace";
17
16
  export type AgentSource = "builtin" | "user" | "project";
18
17
 
18
+ export const BUILTIN_AGENT_NAMES = ["delegate", "explorer", "goal-verifier", "workflow"] as const;
19
+ export type BuiltinAgentName = (typeof BUILTIN_AGENT_NAMES)[number];
20
+
21
+ const LEGACY_AGENT_ALIASES: Readonly<Record<string, BuiltinAgentName>> = {
22
+ coordinator: "workflow",
23
+ };
24
+
25
+ const AGENT_CATALOG_START_MARKER = "<!-- teammate-agent-catalog:start -->";
26
+ const AGENT_CATALOG_END_MARKER = "<!-- teammate-agent-catalog:end -->";
27
+
19
28
  export interface AgentConfig {
20
29
  name: string;
21
30
  description: string;
22
31
  tools?: string[];
23
32
  model?: string;
24
33
  fallbackModels?: string[];
25
- thinking?: string;
34
+ thinking?: TeammateThinkingLevel;
26
35
  systemPromptMode: SystemPromptMode;
27
36
  inheritProjectContext: boolean;
28
37
  inheritSkills: boolean;
@@ -38,6 +47,11 @@ export interface AgentSummary {
38
47
  source: AgentSource;
39
48
  }
40
49
 
50
+ export interface AgentCatalogSnapshot {
51
+ signature: string;
52
+ systemPrompt: string;
53
+ }
54
+
41
55
  const BUILTIN_AGENTS_DIR = path.resolve(
42
56
  path.dirname(fileURLToPath(import.meta.url)),
43
57
  "..",
@@ -123,7 +137,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
123
137
  tools: rawTools && rawTools.length > 0 ? rawTools : undefined,
124
138
  model: frontmatter.model,
125
139
  fallbackModels: rawFallbackModels && rawFallbackModels.length > 0 ? rawFallbackModels : undefined,
126
- thinking: frontmatter.thinking,
140
+ thinking: parseTeammateThinkingLevel(frontmatter.thinking),
127
141
  systemPromptMode,
128
142
  inheritProjectContext,
129
143
  inheritSkills,
@@ -137,27 +151,43 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
137
151
  return agents;
138
152
  }
139
153
 
154
+ export function isBuiltinAgentName(name: string): name is BuiltinAgentName {
155
+ return (BUILTIN_AGENT_NAMES as readonly string[]).includes(name);
156
+ }
157
+
158
+ function isReservedAgentName(name: string): boolean {
159
+ return isBuiltinAgentName(name) || Object.hasOwn(LEGACY_AGENT_ALIASES, name);
160
+ }
161
+
162
+ function canonicalAgentName(name: string): string {
163
+ return LEGACY_AGENT_ALIASES[name] ?? name;
164
+ }
165
+
140
166
  /**
141
167
  * Discover all agent definitions, merged by priority:
142
168
  * project > user > builtin (name collisions: higher priority wins).
143
169
  */
144
- export function discoverAgents(cwd: string): AgentConfig[] {
145
- const userAgentsDir = path.join(
146
- os.homedir(),
170
+ export function discoverAgents(cwd: string, homeDir = os.homedir()): AgentConfig[] {
171
+ const legacyUserAgentsDir = path.join(
172
+ homeDir,
147
173
  ".pi",
148
174
  "agent",
149
175
  "extensions",
150
176
  "teammate",
151
177
  "agents",
152
178
  );
179
+ const userAgentsDir = path.join(homeDir, ".agents");
153
180
 
154
- // Find project root (first ancestor with .pi/ directory)
155
- let projectAgentsDir: string | null = null;
181
+ // Find the nearest ancestor containing either supported project directory.
182
+ let projectPiAgentsDir: string | null = null;
183
+ let projectCompatAgentsDir: string | null = null;
156
184
  let currentDir = cwd;
157
185
  while (true) {
158
- const piDir = path.join(currentDir, ".pi", "agents");
159
- if (fs.existsSync(piDir)) {
160
- projectAgentsDir = piDir;
186
+ const piAgentsDir = path.join(currentDir, ".pi", "agents");
187
+ const compatAgentsDir = path.join(currentDir, ".agents");
188
+ if (fs.existsSync(piAgentsDir) || fs.existsSync(compatAgentsDir)) {
189
+ projectPiAgentsDir = fs.existsSync(piAgentsDir) ? piAgentsDir : null;
190
+ projectCompatAgentsDir = fs.existsSync(compatAgentsDir) ? compatAgentsDir : null;
161
191
  break;
162
192
  }
163
193
  const parentDir = path.dirname(currentDir);
@@ -165,17 +195,36 @@ export function discoverAgents(cwd: string): AgentConfig[] {
165
195
  currentDir = parentDir;
166
196
  }
167
197
 
168
- const builtinAgents = loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
169
- const userAgents = loadAgentsFromDir(userAgentsDir, "user");
170
- const projectAgents = projectAgentsDir
171
- ? loadAgentsFromDir(projectAgentsDir, "project")
198
+ const builtinByName = new Map(
199
+ loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin")
200
+ .filter((agent) => isBuiltinAgentName(agent.name))
201
+ .map((agent) => [agent.name, agent]),
202
+ );
203
+ const builtinAgents = BUILTIN_AGENT_NAMES
204
+ .map((name) => builtinByName.get(name))
205
+ .filter((agent): agent is AgentConfig => agent !== undefined);
206
+ const loadCustomAgents = (dir: string, source: AgentSource): AgentConfig[] =>
207
+ loadAgentsFromDir(dir, source)
208
+ .filter((agent) => !isReservedAgentName(agent.name));
209
+
210
+ // Builtin names are reserved so project/user definitions cannot silently
211
+ // replace the stable general, exploration, and DAG orchestration roles.
212
+ const legacyUserAgents = loadCustomAgents(legacyUserAgentsDir, "user");
213
+ const userAgents = loadCustomAgents(userAgentsDir, "user");
214
+ const projectCompatAgents = projectCompatAgentsDir
215
+ ? loadCustomAgents(projectCompatAgentsDir, "project")
216
+ : [];
217
+ const projectPiAgents = projectPiAgentsDir
218
+ ? loadCustomAgents(projectPiAgentsDir, "project")
172
219
  : [];
173
220
 
174
- // Merge: project > user > builtin
221
+ // Merge from lowest to highest priority.
175
222
  const agentMap = new Map<string, AgentConfig>();
176
223
  for (const agent of builtinAgents) agentMap.set(agent.name, agent);
224
+ for (const agent of legacyUserAgents) agentMap.set(agent.name, agent);
177
225
  for (const agent of userAgents) agentMap.set(agent.name, agent);
178
- for (const agent of projectAgents) agentMap.set(agent.name, agent);
226
+ for (const agent of projectCompatAgents) agentMap.set(agent.name, agent);
227
+ for (const agent of projectPiAgents) agentMap.set(agent.name, agent);
179
228
 
180
229
  return Array.from(agentMap.values());
181
230
  }
@@ -188,7 +237,8 @@ export function resolveAgent(
188
237
  agentName: string,
189
238
  ): AgentConfig | undefined {
190
239
  const agents = discoverAgents(cwd);
191
- return agents.find((a) => a.name === agentName);
240
+ const canonicalName = canonicalAgentName(agentName);
241
+ return agents.find((a) => a.name === canonicalName);
192
242
  }
193
243
 
194
244
  /** Return resolved role metadata without exposing the role prompt body. */
@@ -222,3 +272,60 @@ export function formatAgentCatalog(
222
272
 
223
273
  return lines.join("\n");
224
274
  }
275
+
276
+ /** Build the compact role directory appended to the active parent prompt. */
277
+ export function createAgentCatalogSnapshot(
278
+ cwd: string,
279
+ maxDescriptionLength = 160,
280
+ ): AgentCatalogSnapshot {
281
+ const summaries = listAgentSummaries(cwd);
282
+ const byName = new Map(summaries.map((agent) => [agent.name, agent]));
283
+ const builtins = BUILTIN_AGENT_NAMES
284
+ .map((name) => byName.get(name))
285
+ .filter((agent): agent is AgentSummary => agent !== undefined);
286
+ const discovered = summaries
287
+ .filter((agent) => !isBuiltinAgentName(agent.name));
288
+
289
+ const formatLine = (agent: AgentSummary): string => {
290
+ const normalized = agent.description.replace(/\s+/g, " ").trim();
291
+ const description = normalized.length > maxDescriptionLength
292
+ ? `${normalized.slice(0, Math.max(1, maxDescriptionLength - 1)).trimEnd()}…`
293
+ : normalized;
294
+ return `- ${agent.name}: ${description}`;
295
+ };
296
+
297
+ const lines = [
298
+ AGENT_CATALOG_START_MARKER,
299
+ "# Available Teammate Agents",
300
+ "",
301
+ "Built-in roles:",
302
+ ...builtins.map(formatLine),
303
+ "",
304
+ "Discovered project and user roles:",
305
+ ...(discovered.length > 0 ? discovered.map(formatLine) : ["(none)"]),
306
+ ];
307
+
308
+ lines.push(
309
+ "",
310
+ "Use the exact agent name. Unknown names are invalid. Agent prompt bodies are loaded only after a role is selected.",
311
+ AGENT_CATALOG_END_MARKER,
312
+ );
313
+
314
+ return {
315
+ signature: summaries
316
+ .map((agent) => `${agent.name}:${agent.source}:${agent.description}`)
317
+ .join("\n"),
318
+ systemPrompt: lines.join("\n"),
319
+ };
320
+ }
321
+
322
+ /** Replace an existing role directory or append a fresh one to the prompt. */
323
+ export function appendAgentCatalog(systemPrompt: string, cwd: string): string {
324
+ const snapshot = createAgentCatalogSnapshot(cwd);
325
+ const start = systemPrompt.indexOf(AGENT_CATALOG_START_MARKER);
326
+ const end = systemPrompt.indexOf(AGENT_CATALOG_END_MARKER);
327
+ if (start >= 0 && end >= start) {
328
+ return `${systemPrompt.slice(0, start)}${snapshot.systemPrompt}${systemPrompt.slice(end + AGENT_CATALOG_END_MARKER.length)}`;
329
+ }
330
+ return `${systemPrompt}\n\n${snapshot.systemPrompt}`;
331
+ }