@bacnh85/pi-subagent 0.5.0 → 0.7.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/CHANGELOG.md ADDED
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ ## 0.6.0 (2026-07-12)
4
+
5
+ ### Security (breaking changes)
6
+
7
+ - **Project-agent confirmation removed from tool schema.** The `confirmProjectAgents` parameter is no longer exposed to the LLM. Project-agent approval is enforced via trusted configuration only. Interactive sessions prompt for confirmation; headless sessions fail closed unless `allowUnconfirmedProjectAgents` is explicitly enabled through trusted configuration (environment variable `PI_SUBAGENT_ALLOW_UNCONFIRMED_PROJECT_AGENTS=true` or pi settings).
8
+
9
+ - **Child working directories confined to the workspace.** Tool-specified `cwd` values are validated against the workspace root. Relative paths are resolved within the workspace; `..` traversal, absolute paths outside the workspace, and symlink escapes are rejected. A trusted `allowExternalCwd` setting (env `PI_SUBAGENT_ALLOW_EXTERNAL_CWD=true` or pi settings) can opt out.
10
+
11
+ - **Tool allowlist enforced.** Child agent tools are validated against a fixed allowlist: `read`, `grep`, `find`, `ls`, `bash`, `edit`, `write`. The `subagent` tool is always rejected. Unknown or misspelled tool names produce clear errors. Read-only service execution cannot gain `bash`, `edit`, or `write`.
12
+
13
+ - **Default timeout added.** Every child execution receives a default 10-minute timeout (`DEFAULT_TIMEOUT_MS`). Maximum allowed timeout is 60 minutes (`MAX_TIMEOUT_MS`). Timeout errors are distinguishable from parent cancellation.
14
+
15
+ ### Reliability
16
+
17
+ - **Abort signal composition fixed.** `createCombinedAbortSignal()` correctly combines multiple abort signals with proper listener cleanup. Works without `AbortSignal.any()` via a manual fallback that removes all listeners after the first abort.
18
+
19
+ - **Parallel abort listeners cleaned up.** Parent-signal listeners attached during parallel execution are removed in a `finally` block after completion.
20
+
21
+ - **Canonical result status.** `SubAgentResult.status` classifies outcomes as `"success"`, `"partial"`, `"error"`, `"aborted"`, or `"timeout"`. Known Pi SDK stop reasons are classified explicitly; unknown reasons default conservatively to `"error"`.
22
+
23
+ - **Validation hardened.** Numeric and collection limits (timeout, max parallel tasks, concurrency, chain length, output cap, instructions length) are enforced at both schema and runtime levels.
24
+
25
+ - **Parallel result ordering preserved.** Results remain in input-task order regardless of completion order.
26
+
27
+ - **`abortOnFailure` behavior deterministic.** First canonical failure aborts running siblings; queued tasks never start; completed tasks retain their results.
28
+
29
+ ### Agent discovery
30
+
31
+ - **Malformed agent files produce diagnostics.** Missing name, missing description, empty name, invalid model, invalid thinking levels, and unreadable files are reported with file path and severity. Valid agents continue to load.
32
+
33
+ ### Packaging
34
+
35
+ - **Peer dependency ranges constrained.** `@earendil-works/pi-*` dependencies use `>=0.80.0 <0.81.0`; `typebox` uses `>=1.3.0 <2.0.0`.
36
+
37
+ - **Node engine requirement added.** `engines.node: ">=20.18"`.
38
+
39
+ - **Scripts fixed.** `npm test` uses locally installed `mocha` (no `npx`). Added `npm run check` (typecheck + test).
40
+
41
+ - **Package metadata updated.** `homepage` points to the package subdirectory.
42
+
43
+ - **`security.ts` added to published files.**
44
+
45
+ - **`CHANGELOG.md` added to published files.**
46
+
47
+ ### Documentation
48
+
49
+ - **Security model section** added to README covering project-agent trust, cwd confinement, tool validation, timeout defaults, cancellation, result status, and compatibility.
50
+
51
+ ### Backward compatibility
52
+
53
+ - `SubAgentResult.status` is a new field; existing consumers that ignore unknown fields remain compatible.
54
+ - Tool schema no longer accepts `confirmProjectAgents`; model-generated calls using it will be silently ignored (the field is fully removed from the schema, not just deprecated).
55
+ - Child `cwd` values that previously worked outside the workspace are now rejected unless the trusted `allowExternalCwd` setting is enabled.
56
+ - `combineAbortSignals()` is still exported from `runner.ts` but delegates to `createCombinedAbortSignal()` internally.
package/README.md CHANGED
@@ -10,14 +10,16 @@ pi install npm:@bacnh85/pi-subagent
10
10
  pi install ./pi-subagent
11
11
  ```
12
12
 
13
+ Requires Node.js >= 20.18.
14
+
13
15
  ## Bundled roles
14
16
 
15
17
  | Role | Model | Thinking | Tools |
16
18
  | --- | --- | --- | --- |
17
19
  | `scout` | parent model | low | read, grep, find, ls |
18
20
  | `reviewer` | parent model | high | read, grep, find, ls |
19
- | `worker` | parent model | medium | standard coding tools |
20
- | `general-purpose` | parent model | off | standard coding tools |
21
+ | `worker` | parent model | medium | read, bash, edit, write, grep, find, ls |
22
+ | `general-purpose` | parent model | off | read, bash, edit, write, grep, find, ls |
21
23
 
22
24
  Bundled roles inherit the parent model so they work with the account already active in Pi. User/project agent files may override `model` and `thinking`.
23
25
 
@@ -30,14 +32,13 @@ Create `~/.pi/agent/agents/*.md` or `.pi/agents/*.md`:
30
32
  name: scout-fast
31
33
  description: Locate relevant files and symbols
32
34
  tools: read, grep, find, ls
33
- thinking: low
34
35
  model: optional-provider/optional-model
35
36
  ---
36
37
 
37
38
  Return concise evidence with file/symbol anchors.
38
39
  ```
39
40
 
40
- Project agents require confirmation when requested through the public tool. Definitions are cached with file-signature invalidation; `/subagent reload` clears the cache.
41
+ Agent definitions are cached with file-signature invalidation; `/subagent reload` clears the cache.
41
42
 
42
43
  ## Context and limits
43
44
 
@@ -45,8 +46,130 @@ Children use in-memory SDK sessions with no extensions, skills, prompt templates
45
46
 
46
47
  Threads are session-memory only and are cleared when Pi replaces or reloads the session. Timeout and parent cancellation propagate to child sessions. Subagents cannot recursively invoke `subagent`.
47
48
 
49
+ ## Security model
50
+
51
+ ### Project-local agents
52
+
53
+ Agent files under `.pi/agents/` are controlled by the current repository. A project agent's system prompt may instruct a child to execute shell commands or modify files.
54
+
55
+ - **Project-agent approval cannot be disabled by the model.** The `confirmProjectAgents` parameter is not exposed in the tool schema. Confirmation policy comes from trusted user configuration only.
56
+ - **Interactive sessions** prompt the user before executing project agents.
57
+ - **Headless sessions fail closed.** Project agents are not executed without UI confirmation unless the trusted setting `allowUnconfirmedProjectAgents` is enabled (via `PI_SUBAGENT_ALLOW_UNCONFIRMED_PROJECT_AGENTS=true` environment variable or pi settings).
58
+ - **The extension service path** (`pi-subagent:run` event) follows the same policy.
59
+
60
+ ### Child working directories
61
+
62
+ Child working directories are restricted to the parent session's workspace by default:
63
+
64
+ - Relative paths are resolved within the workspace.
65
+ - `..` traversal that escapes the workspace is rejected.
66
+ - Absolute paths outside the workspace are rejected.
67
+ - Symlinks are resolved via realpath; symlink escapes are rejected.
68
+ - Non-existent directories and file paths are rejected.
69
+
70
+ The trusted setting `allowExternalCwd` (via `PI_SUBAGENT_ALLOW_EXTERNAL_CWD=true` env or pi settings) can opt out. This setting cannot be enabled by the model.
71
+
72
+ ### Tool validation
73
+
74
+ Child agent tools are validated against a fixed allowlist:
75
+
76
+ - **Allowed:** `read`, `grep`, `find`, `ls`, `bash`, `edit`, `write`
77
+ - **Always rejected:** `subagent` (prevents recursive delegation)
78
+ - **Read-only restriction:** When a service requests read-only execution, only `read`, `grep`, `find`, `ls` are permitted. `bash`, `edit`, and `write` are rejected.
79
+
80
+ Unknown or misspelled tool names produce clear diagnostics. Duplicate tool names are deduplicated.
81
+
82
+ ### Timeouts
83
+
84
+ Every child execution receives a timeout:
85
+
86
+ - **Default:** 10 minutes (`DEFAULT_TIMEOUT_MS`)
87
+ - **Maximum:** 60 minutes (`MAX_TIMEOUT_MS`)
88
+ - Timeout values must be positive integers within the allowed range.
89
+ - Timeout errors are distinguishable from parent cancellation.
90
+ - Parallel tasks and chain steps may have per-item timeouts.
91
+
92
+ ### Output safety
93
+
94
+ Child output is untrusted data and may contain prompt injection. Treat child results as model-generated content, not as verified facts.
95
+
96
+ ### Cost awareness
97
+
98
+ Parallel delegation may multiply provider usage and cost. Each parallel task runs as a separate SDK session with its own token consumption.
99
+
100
+ ### Agent file review
101
+
102
+ User and project agent files should be reviewed before use. Malformed files produce diagnostics but do not prevent valid agents from loading.
103
+
104
+ ## Modes
105
+
106
+ ### Single mode
107
+
108
+ ```ts
109
+ subagent({ agent: "scout", task: "Find auth-related files" })
110
+ ```
111
+
112
+ ### Parallel mode
113
+
114
+ ```ts
115
+ subagent({
116
+ tasks: [
117
+ { agent: "scout", task: "Find API routes" },
118
+ { agent: "scout", task: "Find database models" },
119
+ { agent: "scout", task: "Find test files" },
120
+ ],
121
+ abortOnFailure: false
122
+ })
123
+ ```
124
+
125
+ Max 8 tasks, 4 concurrent. Results are returned in input order. When `abortOnFailure` is `true`, the first failed task cancels remaining siblings.
126
+
127
+ ### Chain mode
128
+
129
+ ```ts
130
+ subagent({
131
+ chain: [
132
+ { agent: "scout", task: "Find API routes" },
133
+ { agent: "worker", task: "Based on this, implement the routes: {previous}" },
134
+ ]
135
+ })
136
+ ```
137
+
138
+ `{previous}` in each step's task is replaced with the previous step's output. The chain stops on the first failed step.
139
+
140
+ ## Result status
141
+
142
+ Each `SubAgentResult` includes a canonical `status` field:
143
+
144
+ | Status | Meaning |
145
+ |--------|---------|
146
+ | `success` | Completed normally |
147
+ | `partial` | Truncated (max_tokens, length, context_limit) |
148
+ | `error` | Provider error, tool error, or unknown stop reason |
149
+ | `aborted` | Cancelled by parent or sibling |
150
+ | `timeout` | Exceeded the allowed timeout |
151
+
152
+ The raw `stopReason` from the Pi SDK is preserved in the result.
153
+
154
+ ## Timeout and cancellation
155
+
156
+ - **Default timeout:** 10 minutes per child.
157
+ - **Per-task/step override:** Use `timeout` in task/step params.
158
+ - **Parent cancellation:** Aborting the parent tool call cancels all children.
159
+ - **Sibling cancellation:** In parallel mode with `abortOnFailure: true`, the first failed task cancels running siblings.
160
+ - **Timeout vs. abort:** Timeout errors set `status: "timeout"` and `stopReason: "timeout"`; parent cancellation sets `status: "aborted"`.
161
+
48
162
  ## Extension contract
49
163
 
50
164
  `pi-subagent` owns the `pi-subagent:run` event contract for one named-agent request. `pi-review` uses it for isolated review. Requests use an immediate boolean `accept()` claim and exactly one `respond()` callback; this suppresses duplicate responders while missing services and timeouts remain caller-controlled.
51
165
 
166
+ ## Compatibility
167
+
168
+ - Requires `@earendil-works/pi-coding-agent >=0.80.0 <0.81.0`
169
+ - Requires `@earendil-works/pi-ai >=0.80.0 <0.81.0`
170
+ - Requires `@earendil-works/pi-agent-core >=0.80.0 <0.81.0`
171
+ - Requires `@earendil-works/pi-tui >=0.80.0 <0.81.0`
172
+ - Requires `typebox >=1.3.0 <2.0.0`
173
+ - Requires Node.js >= 20.18
174
+
52
175
  See [`agent-format.md`](./agent-format.md) for all frontmatter fields.
package/agent-format.md CHANGED
@@ -32,12 +32,22 @@ The body after frontmatter becomes the agent's **entire system prompt**. No pi d
32
32
 
33
33
  ## Available Tools
34
34
 
35
- Built-in pi tool names: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`
35
+ Child agent tools are validated against a fixed allowlist:
36
36
 
37
- The `subagent` tool is never available to sub-agents (prevents accidental recursion). Sub-agents run at one level of delegation only; they cannot spawn further sub-agents.
37
+ | Category | Tools |
38
+ |----------|-------|
39
+ | Read-only | `read`, `grep`, `find`, `ls` |
40
+ | Mutation | `edit`, `write` |
41
+ | Execution | `bash` |
42
+
43
+ The `subagent` tool is always rejected to prevent recursive delegation.
44
+ Unknown or misspelled tool names produce a clear error.
45
+ Duplicate names are deduplicated automatically.
38
46
 
39
47
  Custom/extension tools are NOT available to sub-agents by default (each runs in an isolated in-memory session with no extensions).
40
48
 
49
+ Read-only service execution (used by `pi-review`) restricts tools to the read-only category.
50
+
41
51
  ## Model Resolution
42
52
 
43
53
  Model IDs are resolved via `getModel("provider", "id")`. Common values:
@@ -2,6 +2,7 @@
2
2
  name: general-purpose
3
3
  description: General-purpose sub-agent for any delegated task. Use when no specialized agent fits. Good for complex research, multi-step operations, and code modifications.
4
4
  tools: read, bash, edit, write, grep, find, ls
5
+ color: yellow
5
6
  ---
6
7
 
7
8
  You are a capable coding assistant running as a sub-agent. Complete the delegated task efficiently and return a concise summary of your findings or changes.
@@ -3,6 +3,8 @@ name: reviewer
3
3
  description: Code review specialist. Use for correctness, security, regression, and meaningful test-gap review.
4
4
  tools: read, grep, find, ls
5
5
  thinking: high
6
+ color: purple
7
+ sandbox: read-only
6
8
  ---
7
9
 
8
10
  You are an independent senior code reviewer. Inspect the requested Git scope with read-only tools.
package/agents/scout.md CHANGED
@@ -3,6 +3,8 @@ name: scout
3
3
  description: Fast codebase recon that returns compressed context for handoff. Use for finding files, understanding structure, locating symbols.
4
4
  tools: read, grep, find, ls
5
5
  thinking: low
6
+ color: cyan
7
+ sandbox: read-only
6
8
  ---
7
9
 
8
10
  You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
package/agents/worker.md CHANGED
@@ -2,6 +2,7 @@
2
2
  name: worker
3
3
  description: General-purpose coding agent with full tool access. Use only when explicitly requested for isolated implementation.
4
4
  thinking: medium
5
+ color: green
5
6
  ---
6
7
 
7
8
  You are a skilled software engineer. Implement the requested task with care and precision.
@@ -13,12 +13,16 @@ import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/
13
13
 
14
14
  export type AgentScope = "user" | "project" | "both";
15
15
 
16
+ export type AgentColor = "red" | "blue" | "green" | "yellow" | "purple" | "orange" | "pink" | "cyan";
17
+
16
18
  export interface AgentConfig {
17
19
  name: string;
18
20
  description: string;
19
21
  tools?: string[];
20
22
  model?: string;
21
23
  thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
24
+ sandbox?: "read-only" | "workspace-write";
25
+ color?: AgentColor;
22
26
  systemPrompt: string;
23
27
  source: "user" | "project" | "bundled";
24
28
  filePath: string;
@@ -27,6 +31,14 @@ export interface AgentConfig {
27
31
  export interface AgentDiscoveryResult {
28
32
  agents: AgentConfig[];
29
33
  projectAgentsDir: string | null;
34
+ diagnostics: AgentDiscoveryDiagnostic[];
35
+ }
36
+
37
+ export interface AgentDiscoveryDiagnostic {
38
+ filePath: string;
39
+ issue: string;
40
+ /** 'warn' for recoverable issues, 'error' for file-skip issues. */
41
+ severity: "warn" | "error";
30
42
  }
31
43
 
32
44
  interface AgentCache {
@@ -47,7 +59,11 @@ export function invalidateAgentCache(): void {
47
59
  _cache = null;
48
60
  }
49
61
 
50
- function loadAgentsFromDir(dir: string, source: "user" | "project" | "bundled"): AgentConfig[] {
62
+ function loadAgentsFromDir(
63
+ dir: string,
64
+ source: "user" | "project" | "bundled",
65
+ diagnostics: AgentDiscoveryDiagnostic[],
66
+ ): AgentConfig[] {
51
67
  const agents: AgentConfig[] = [];
52
68
 
53
69
  if (!fs.existsSync(dir)) return agents;
@@ -55,25 +71,72 @@ function loadAgentsFromDir(dir: string, source: "user" | "project" | "bundled"):
55
71
  let entries: fs.Dirent[];
56
72
  try {
57
73
  entries = fs.readdirSync(dir, { withFileTypes: true });
58
- } catch {
74
+ } catch (err) {
75
+ diagnostics.push({
76
+ filePath: dir,
77
+ issue: `Cannot read directory: ${err instanceof Error ? err.message : String(err)}`,
78
+ severity: "warn",
79
+ });
59
80
  return agents;
60
81
  }
61
82
 
62
83
  for (const entry of entries) {
63
84
  if (!entry.name.endsWith(".md")) continue;
64
- if (!entry.isFile() && !entry.isSymbolicLink()) continue;
85
+ if (!entry.isFile() && !entry.isSymbolicLink()) {
86
+ diagnostics.push({
87
+ filePath: path.join(dir, entry.name),
88
+ issue: `Not a regular file or symlink, skipping.`,
89
+ severity: "warn",
90
+ });
91
+ continue;
92
+ }
65
93
 
66
94
  const filePath = path.join(dir, entry.name);
67
95
  let content: string;
68
96
  try {
69
97
  content = fs.readFileSync(filePath, "utf-8");
70
- } catch {
98
+ } catch (err) {
99
+ diagnostics.push({
100
+ filePath,
101
+ issue: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,
102
+ severity: "error",
103
+ });
71
104
  continue;
72
105
  }
73
106
 
74
107
  const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
75
108
 
76
- if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") continue;
109
+ if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") {
110
+ if (typeof frontmatter.name !== "string" && typeof frontmatter.description !== "string") {
111
+ diagnostics.push({
112
+ filePath,
113
+ issue: `Missing both "name" and "description" in frontmatter. Agent file skipped.`,
114
+ severity: "error",
115
+ });
116
+ } else if (typeof frontmatter.name !== "string") {
117
+ diagnostics.push({
118
+ filePath,
119
+ issue: `Missing "name" in frontmatter. Agent file skipped.`,
120
+ severity: "error",
121
+ });
122
+ } else {
123
+ diagnostics.push({
124
+ filePath,
125
+ issue: `Missing "description" in frontmatter. Agent file skipped.`,
126
+ severity: "error",
127
+ });
128
+ }
129
+ continue;
130
+ }
131
+
132
+ if (!frontmatter.name.trim()) {
133
+ diagnostics.push({
134
+ filePath,
135
+ issue: `"name" in frontmatter is empty. Agent file skipped.`,
136
+ severity: "error",
137
+ });
138
+ continue;
139
+ }
77
140
 
78
141
  const tools =
79
142
  typeof frontmatter.tools === "string"
@@ -82,6 +145,45 @@ function loadAgentsFromDir(dir: string, source: "user" | "project" | "bundled"):
82
145
  ? (frontmatter.tools as unknown[]).filter((t): t is string => typeof t === "string")
83
146
  : undefined;
84
147
 
148
+ if (typeof frontmatter.model === "string" && frontmatter.model && !frontmatter.model.includes("/")) {
149
+ diagnostics.push({
150
+ filePath,
151
+ issue: `Model "${frontmatter.model}" does not include a provider prefix (e.g., "anthropic/claude-sonnet-4-20250514"). Resolution may fail.`,
152
+ severity: "warn",
153
+ });
154
+ }
155
+
156
+ if (typeof frontmatter.thinking === "string" && frontmatter.thinking) {
157
+ const validLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
158
+ if (!validLevels.includes(frontmatter.thinking)) {
159
+ diagnostics.push({
160
+ filePath,
161
+ issue: `Invalid thinking level "${frontmatter.thinking}". Valid values: ${validLevels.join(", ")}. Using default.`,
162
+ severity: "warn",
163
+ });
164
+ }
165
+ }
166
+
167
+ if (typeof frontmatter.sandbox === "string" && frontmatter.sandbox) {
168
+ const validSandboxes = ["read-only", "workspace-write"];
169
+ if (!validSandboxes.includes(frontmatter.sandbox)) {
170
+ diagnostics.push({
171
+ filePath,
172
+ issue: `Invalid sandbox mode "${frontmatter.sandbox}". Valid values: ${validSandboxes.join(", ")}. Using default.`,
173
+ severity: "warn",
174
+ });
175
+ }
176
+ }
177
+
178
+ const VALID_COLORS = ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan"] as const;
179
+ if (typeof frontmatter.color === "string" && frontmatter.color && !VALID_COLORS.includes(frontmatter.color as any)) {
180
+ diagnostics.push({
181
+ filePath,
182
+ issue: `Invalid color "${frontmatter.color}". Valid values: ${VALID_COLORS.join(", ")}. Ignoring.`,
183
+ severity: "warn",
184
+ });
185
+ }
186
+
85
187
  agents.push({
86
188
  name: frontmatter.name,
87
189
  description: frontmatter.description,
@@ -90,6 +192,12 @@ function loadAgentsFromDir(dir: string, source: "user" | "project" | "bundled"):
90
192
  thinking: typeof frontmatter.thinking === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(frontmatter.thinking)
91
193
  ? frontmatter.thinking as AgentConfig["thinking"]
92
194
  : undefined,
195
+ sandbox: typeof frontmatter.sandbox === "string" && ["read-only", "workspace-write"].includes(frontmatter.sandbox)
196
+ ? frontmatter.sandbox as "read-only" | "workspace-write"
197
+ : undefined,
198
+ color: typeof frontmatter.color === "string" && VALID_COLORS.includes(frontmatter.color as any)
199
+ ? frontmatter.color as AgentColor
200
+ : undefined,
93
201
  systemPrompt: body,
94
202
  source,
95
203
  filePath,
@@ -169,16 +277,18 @@ export function discoverAgents(
169
277
  }
170
278
  }
171
279
  if (!stale) {
172
- return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir };
280
+ return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir, diagnostics: [] };
173
281
  }
174
282
  // Cache is stale — rebuild below
175
283
  _cache = null;
176
284
  }
177
285
 
178
- const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
286
+ const diagnostics: AgentDiscoveryDiagnostic[] = [];
287
+
288
+ const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user", diagnostics);
179
289
  const projectAgents =
180
- scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
181
- const bundledAgents = loadAgentsFromDir(bundledAgentsDir, "bundled");
290
+ scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project", diagnostics);
291
+ const bundledAgents = loadAgentsFromDir(bundledAgentsDir, "bundled", diagnostics);
182
292
 
183
293
  const agentMap = new Map<string, AgentConfig>();
184
294
 
@@ -209,7 +319,7 @@ export function discoverAgents(
209
319
  dirSignatures,
210
320
  };
211
321
 
212
- return { agents, projectAgentsDir };
322
+ return { agents, projectAgentsDir, diagnostics };
213
323
  }
214
324
 
215
325
  export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {