@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 +56 -0
- package/README.md +127 -4
- package/agent-format.md +12 -2
- package/agents/general-purpose.md +1 -0
- package/agents/reviewer.md +2 -0
- package/agents/scout.md +2 -0
- package/agents/worker.md +1 -0
- package/extensions/agents.ts +120 -10
- package/extensions/index.ts +309 -235
- package/extensions/render.ts +7 -7
- package/extensions/runner.ts +88 -45
- package/extensions/security.ts +504 -0
- package/extensions/service.ts +29 -18
- package/extensions/thread-viewer.ts +4 -127
- package/extensions/threads.ts +4 -11
- package/package.json +26 -11
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 |
|
|
20
|
-
| `general-purpose` | parent model | off |
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
Child agent tools are validated against a fixed allowlist:
|
|
36
36
|
|
|
37
|
-
|
|
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.
|
package/agents/reviewer.md
CHANGED
|
@@ -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.
|
package/extensions/agents.ts
CHANGED
|
@@ -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(
|
|
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())
|
|
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")
|
|
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
|
|
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 } {
|