@pi-archimedes/subagent 2.10.4 → 2.11.1
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 +2 -2
- package/package.json +10 -10
- package/src/agent-panel.mcp.test.ts +76 -0
- package/src/agent-panel.tools.test.ts +162 -0
- package/src/agent-panel.ts +37 -8
- package/src/agent-panel.width-audit.test.ts +258 -0
- package/src/agents-parse.test.ts +252 -0
- package/src/agents.ts +72 -5
- package/src/compact.test.ts +136 -57
- package/src/compact.ts +57 -37
- package/src/dispatch.test.ts +73 -1
- package/src/dispatch.ts +20 -2
- package/src/execute.test.ts +9 -1
- package/src/execute.ts +26 -29
- package/src/expanded.test.ts +472 -0
- package/src/expanded.ts +202 -82
- package/src/format.test.ts +71 -29
- package/src/format.ts +71 -28
- package/src/frontmatter-io.test.ts +62 -0
- package/src/frontmatter-io.ts +16 -2
- package/src/handlers.test.ts +21 -3
- package/src/handlers.ts +22 -3
- package/src/index.output-schema.test.ts +317 -0
- package/src/index.test.ts +566 -0
- package/src/index.ts +169 -16
- package/src/render.ts +46 -36
- package/src/row-block.test.ts +244 -0
- package/src/row-block.ts +209 -0
- package/src/run-outcome.test.ts +71 -0
- package/src/run-outcome.ts +84 -0
- package/src/spawn.test.ts +204 -19
- package/src/spawn.ts +49 -17
- package/src/stream.test.ts +215 -17
- package/src/stream.ts +47 -9
- package/src/thinking-validation.test.ts +96 -0
- package/src/thinking-validation.ts +78 -0
- package/src/tool-declarations.test.ts +58 -0
- package/src/tool-row.width-audit.test.ts +720 -0
- package/src/tool-schema.test.ts +154 -0
- package/src/tool-schema.ts +106 -4
- package/src/tool-validation.test.ts +321 -0
- package/src/tool-validation.ts +262 -0
- package/src/types.ts +86 -0
- package/src/usage.test.ts +95 -0
- package/src/usage.ts +87 -0
- package/src/cost.test.ts +0 -62
- package/src/cost.ts +0 -8
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-config **read** path: frontmatter on disk → AgentConfig.
|
|
3
|
+
*
|
|
4
|
+
* `discoverAgents()` is pure filesystem (no ExtensionAPI handle, no tool
|
|
5
|
+
* registry), so this is where a config can be rejected on **lexical** grounds
|
|
6
|
+
* and nowhere that a name's membership in the tool set could be checked.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
10
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { discoverAgents } from "./agents.js";
|
|
14
|
+
|
|
15
|
+
// Redirect getAgentDir() (and therefore the user agents dir) into a sandbox.
|
|
16
|
+
const testDir = join(tmpdir(), "pi-test-agents-parse");
|
|
17
|
+
process.env.PI_CODING_AGENT_DIR = testDir;
|
|
18
|
+
|
|
19
|
+
const agentsDir = join(testDir, "agents");
|
|
20
|
+
|
|
21
|
+
function writeAgent(file: string, frontmatter: string, body = "prompt"): void {
|
|
22
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(agentsDir, `${file}.md`),
|
|
25
|
+
`---\nname: ${file}\ndescription: desc for ${file}\n${frontmatter}---\n${body}\n`,
|
|
26
|
+
"utf-8",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Discover the named agent from the sandbox user dir. */
|
|
31
|
+
function readAgent(name: string) {
|
|
32
|
+
return discoverAgents(testDir).find((a) => a.name === name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("agent config read: mcp opt-in", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
38
|
+
mkdirSync(testDir, { recursive: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("mcp: true is parsed onto the config, not left in extraFields", () => {
|
|
46
|
+
writeAgent("closed", "tools: read, bash\nmcp: true\n");
|
|
47
|
+
const agent = readAgent("closed");
|
|
48
|
+
expect(agent?.mcp).toBe(true);
|
|
49
|
+
expect(agent?.extraFields ?? {}).not.toHaveProperty("mcp");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("an absent mcp key leaves the config untouched (the closed default needs no key)", () => {
|
|
53
|
+
writeAgent("quiet", "tools: read, bash\n");
|
|
54
|
+
const agent = readAgent("quiet");
|
|
55
|
+
expect(agent?.mcp).toBeUndefined();
|
|
56
|
+
expect(agent?.extraFields ?? {}).not.toHaveProperty("mcp");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("mcp: false is not an opt-in and is not kept as an unknown field", () => {
|
|
60
|
+
writeAgent("off", "tools: read\nmcp: false\n");
|
|
61
|
+
const agent = readAgent("off");
|
|
62
|
+
expect(agent?.mcp).not.toBe(true);
|
|
63
|
+
expect(agent?.extraFields ?? {}).not.toHaveProperty("mcp");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("a quoted mcp: \"true\" still means opted in (frontmatter strings survive quoting)", () => {
|
|
67
|
+
writeAgent("quoted", "tools: read\nmcp: \"true\"\n");
|
|
68
|
+
expect(readAgent("quoted")?.mcp).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("mcp without a tools allowlist is parsed but changes nothing downstream", () => {
|
|
72
|
+
writeAgent("noallow", "mcp: true\n");
|
|
73
|
+
expect(readAgent("noallow")?.mcp).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── The lexical half of validation (ADR 0028) ───────────────────────────────
|
|
78
|
+
//
|
|
79
|
+
// This read path has no tool registry — `discoverAgents()` touches only the
|
|
80
|
+
// filesystem — so nothing here may depend on whether a name is a real tool.
|
|
81
|
+
// A rejected `tools:` list is never applied (an allowlist that silently becomes
|
|
82
|
+
// a wider one is the failure this exists to prevent); the reason rides on the
|
|
83
|
+
// config so dispatch refuses it and the panel can show it.
|
|
84
|
+
|
|
85
|
+
describe("agent config read: malformed tools entries are rejected", () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
88
|
+
mkdirSync(testDir, { recursive: true });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("accepts a well-formed list, unchanged", () => {
|
|
96
|
+
writeAgent("good", "tools: read, bash\n");
|
|
97
|
+
const agent = readAgent("good");
|
|
98
|
+
expect(agent?.tools).toEqual(["read", "bash"]);
|
|
99
|
+
expect(agent?.toolsError).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("rejects an empty entry and names it on the config", () => {
|
|
103
|
+
writeAgent("empty", "tools: read, , bash\n");
|
|
104
|
+
const agent = readAgent("empty");
|
|
105
|
+
expect(agent?.tools).toBeUndefined();
|
|
106
|
+
expect(agent?.toolsError).toContain('""');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("rejects a trailing comma (the empty entry it creates)", () => {
|
|
110
|
+
writeAgent("trailing", "tools: read, bash,\n");
|
|
111
|
+
const agent = readAgent("trailing");
|
|
112
|
+
expect(agent?.tools).toBeUndefined();
|
|
113
|
+
expect(agent?.toolsError).toBeTruthy();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects a duplicated entry and names it", () => {
|
|
117
|
+
writeAgent("dupe", "tools: read, bash, read\n");
|
|
118
|
+
const agent = readAgent("dupe");
|
|
119
|
+
expect(agent?.tools).toBeUndefined();
|
|
120
|
+
expect(agent?.toolsError).toContain("read");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("rejects a +name entry and names it", () => {
|
|
124
|
+
writeAgent("mod", "tools: read, +web_search\n");
|
|
125
|
+
const agent = readAgent("mod");
|
|
126
|
+
expect(agent?.tools).toBeUndefined();
|
|
127
|
+
expect(agent?.toolsError).toContain("+web_search");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("rejects a -name entry and names it", () => {
|
|
131
|
+
writeAgent("neg", "tools: read, -bash\n");
|
|
132
|
+
const agent = readAgent("neg");
|
|
133
|
+
expect(agent?.tools).toBeUndefined();
|
|
134
|
+
expect(agent?.toolsError).toContain("-bash");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("still preserves a rejected list in extraFields, so saving the agent does not delete the user's line", () => {
|
|
138
|
+
writeAgent("mod2", "tools: read, +web_search\n");
|
|
139
|
+
const agent = readAgent("mod2");
|
|
140
|
+
// Preserved verbatim for the writer's benefit; nothing downstream reads
|
|
141
|
+
// extraFields as a tool list, and dispatch refuses the agent outright.
|
|
142
|
+
expect(agent?.extraFields?.tools).toBe("read, +web_search");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("the rejection message names the agent and the file it came from", () => {
|
|
146
|
+
writeAgent("dupe2", "tools: read, read\n");
|
|
147
|
+
const agent = readAgent("dupe2");
|
|
148
|
+
expect(agent?.toolsError).toContain("dupe2");
|
|
149
|
+
expect(agent?.toolsError).toContain(join(agentsDir, "dupe2.md"));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("a rejected tools list does not disturb the rest of the config", () => {
|
|
153
|
+
writeAgent("mixed", "tools: read, read\nmodel: anthropic/claude-opus-4.5\n", "body text");
|
|
154
|
+
const agent = readAgent("mixed");
|
|
155
|
+
expect(agent?.model).toBe("anthropic/claude-opus-4.5");
|
|
156
|
+
expect(agent?.systemPrompt).toContain("body text");
|
|
157
|
+
expect(agent?.toolsError).toBeTruthy();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("list_agents says which agents have a rejected tools list", async () => {
|
|
161
|
+
const { formatAgentList } = await import("./agents.js");
|
|
162
|
+
writeAgent("broken", "tools: read, +bash\n");
|
|
163
|
+
writeAgent("fine", "tools: read\n");
|
|
164
|
+
const out = formatAgentList(discoverAgents(testDir));
|
|
165
|
+
const brokenLine = out.split("\n").find((l) => l.includes("broken"));
|
|
166
|
+
const fineLine = out.split("\n").find((l) => l.includes("fine "));
|
|
167
|
+
expect(brokenLine).toMatch(/invalid tools|tools rejected/i);
|
|
168
|
+
expect(fineLine).not.toMatch(/invalid tools|tools rejected/i);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("agent config read: `tools:` written as a YAML list", () => {
|
|
173
|
+
// pi's own frontmatter parser turns both `tools: a, b`-style block lists and
|
|
174
|
+
// inline lists into real arrays, and a hand-written agent file is far more
|
|
175
|
+
// likely to use one than the comma form this suite's writer emits. Reading
|
|
176
|
+
// only the string form meant a list-shaped allowlist was dropped on the floor:
|
|
177
|
+
// no allowlist, no error, and a child that runs with every tool the host has —
|
|
178
|
+
// the exact "wider than the config asked for" outcome the read path exists to
|
|
179
|
+
// prevent (and which a live dispatch reproduced: `tools: [read]` + MCP on).
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
182
|
+
mkdirSync(testDir, { recursive: true });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
afterEach(() => {
|
|
186
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("applies a block list as the allowlist", () => {
|
|
190
|
+
writeAgent("blocky", "tools:\n - read\n - bash\nmcp: true\n");
|
|
191
|
+
const agent = readAgent("blocky");
|
|
192
|
+
expect(agent?.tools).toEqual(["read", "bash"]);
|
|
193
|
+
expect(agent?.toolsError).toBeUndefined();
|
|
194
|
+
// The list is an allowlist, so the MCP opt-in still has to survive next to it.
|
|
195
|
+
expect(agent?.mcp).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("applies an inline list the same way", () => {
|
|
199
|
+
writeAgent("inliney", "tools: [read, bash]\n");
|
|
200
|
+
expect(readAgent("inliney")?.tools).toEqual(["read", "bash"]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("applies a one-entry list (which YAML reads as a bare string only when written that way)", () => {
|
|
204
|
+
writeAgent("single", "tools:\n - read\n");
|
|
205
|
+
expect(readAgent("single")?.tools).toEqual(["read"]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("rejects a list entry pi cannot read as a name, and applies nothing", () => {
|
|
209
|
+
writeAgent("blockbad", "tools:\n - read\n - +web_search\n");
|
|
210
|
+
const agent = readAgent("blockbad");
|
|
211
|
+
expect(agent?.tools).toBeUndefined();
|
|
212
|
+
expect(agent?.toolsError).toContain("+web_search");
|
|
213
|
+
// Preserved for the writer, same contract as the comma form.
|
|
214
|
+
expect(agent?.extraFields?.tools).toEqual(["read", "+web_search"]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("rejects a duplicate inside a list", () => {
|
|
218
|
+
writeAgent("blockdupe", "tools:\n - read\n - read\n");
|
|
219
|
+
const agent = readAgent("blockdupe");
|
|
220
|
+
expect(agent?.tools).toBeUndefined();
|
|
221
|
+
expect(agent?.toolsError).toContain("read");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("rejects a list entry hiding a modifier behind a comma, naming the modifier", () => {
|
|
225
|
+
// The child re-splits `--tools` on commas, so this config is `--tools
|
|
226
|
+
// read,+bash` — which pi rejects outright, dropping the allowlist and
|
|
227
|
+
// leaving the child with the full default set. Read it as the two entries
|
|
228
|
+
// the child would see and refuse it here instead.
|
|
229
|
+
writeAgent("commainner", "tools:\n - read,+web_search\n");
|
|
230
|
+
const agent = readAgent("commainner");
|
|
231
|
+
expect(agent?.tools).toBeUndefined();
|
|
232
|
+
expect(agent?.toolsError).toContain("+web_search");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("rejects a list holding something that is not a name at all", () => {
|
|
236
|
+
writeAgent("mixed", "tools:\n - read\n - count: 2\n");
|
|
237
|
+
const agent = readAgent("mixed");
|
|
238
|
+
expect(agent?.tools).toBeUndefined();
|
|
239
|
+
expect(agent?.toolsError).toBeTruthy();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("rejects a tools value that is neither a list nor a list of names, instead of ignoring it", () => {
|
|
243
|
+
// `tools: 42` used to be preserved in extraFields and otherwise ignored —
|
|
244
|
+
// silently unbounded again. A config that means something by the key gets an
|
|
245
|
+
// error, not a shrug.
|
|
246
|
+
writeAgent("scalar", "tools: 42\n");
|
|
247
|
+
const agent = readAgent("scalar");
|
|
248
|
+
expect(agent?.tools).toBeUndefined();
|
|
249
|
+
expect(agent?.toolsError).toBeTruthy();
|
|
250
|
+
expect(agent?.extraFields?.tools).toBe(42);
|
|
251
|
+
});
|
|
252
|
+
});
|
package/src/agents.ts
CHANGED
|
@@ -8,16 +8,49 @@ import * as os from "node:os";
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { readLocalConfig } from "./local-config.js";
|
|
11
|
+
import { readToolEntries, validateToolEntries } from "./tool-validation.js";
|
|
11
12
|
|
|
12
13
|
export interface AgentConfig {
|
|
13
14
|
name: string;
|
|
14
15
|
description: string;
|
|
15
16
|
tools?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Opt this agent's child back in to connecting its MCP servers.
|
|
19
|
+
*
|
|
20
|
+
* pi 1.0.4 stopped a non-empty `--tools` filtering MCP tools: they stay
|
|
21
|
+
* registered (reachable from codemode and `tool_search`) unless the list names
|
|
22
|
+
* an `mcp__` entry (ADR 0028). The suite therefore passes `--no-mcp` alongside
|
|
23
|
+
* a non-empty allowlist; `mcp: true` is the one way a config asks for the
|
|
24
|
+
* servers back. Absent or false means the closed reading. Note the honest
|
|
25
|
+
* limit, verified against a live 1.2.0 child: pi does not *declare* MCP tools
|
|
26
|
+
* to a model whose allowlist does not match them, so with a non-empty `tools:`
|
|
27
|
+
* list the child sees the same declared set with or without this flag — what
|
|
28
|
+
* the flag changes is whether the servers connect at all (ADR 0028
|
|
29
|
+
* verification note). Parsed in `loadAgentsFromDir`, written by
|
|
30
|
+
* `serializeAgent`, shown on the `/agents` detail screen.
|
|
31
|
+
*/
|
|
32
|
+
mcp?: boolean;
|
|
16
33
|
model?: string;
|
|
17
34
|
thinking?: string;
|
|
18
35
|
systemPrompt: string;
|
|
19
36
|
source: "global" | "user" | "project";
|
|
20
37
|
filePath: string;
|
|
38
|
+
/**
|
|
39
|
+
* Set when the `tools:` list was **rejected at config read** — an entry pi
|
|
40
|
+
* cannot read as a tool name (empty, duplicated, containing a comma, or a
|
|
41
|
+
* `+`/`-` modifier), or a value that is not a tool list in any spelling pi's
|
|
42
|
+
* YAML parser produces (comma string, block list, inline list) — a number, a
|
|
43
|
+
* mapping, or a list holding something that is not a name.
|
|
44
|
+
*
|
|
45
|
+
* The list is never applied in that case: a rejected allowlist that falls back
|
|
46
|
+
* to "no allowlist" would hand the child a *wider* tool set than the config
|
|
47
|
+
* asked for. Dispatch refuses such an agent (naming the entry and the file),
|
|
48
|
+
* and the `/agents` panel and `list_agents` report it. The raw value stays in
|
|
49
|
+
* `extraFields.tools` so saving the agent does not silently delete the line; a
|
|
50
|
+
* list committed through `applyToolEntries` retires both the raw value and this
|
|
51
|
+
* error, so the fix actually reaches the file.
|
|
52
|
+
*/
|
|
53
|
+
toolsError?: string;
|
|
21
54
|
// Extra fields preserved from frontmatter but not editable in TUI
|
|
22
55
|
extraFields?: Record<string, unknown>;
|
|
23
56
|
}
|
|
@@ -52,11 +85,35 @@ function loadAgentsFromDir(dir: string, source: "global" | "user" | "project"):
|
|
|
52
85
|
|
|
53
86
|
// Handle tools specially — preserve non-string values in extraFields
|
|
54
87
|
let parsedTools: string[] | undefined;
|
|
55
|
-
|
|
56
|
-
|
|
88
|
+
let toolsError: string | undefined;
|
|
89
|
+
if (frontmatter.tools !== undefined) {
|
|
90
|
+
// Every YAML spelling is read (comma string, block list, inline list);
|
|
91
|
+
// anything that is not a list of names is rejected loudly rather than
|
|
92
|
+
// dropped, because an unread allowlist means an unrestricted child.
|
|
93
|
+
const read = readToolEntries(frontmatter.tools, {
|
|
94
|
+
agentName: String(frontmatter.name),
|
|
95
|
+
agentFilePath: filePath,
|
|
96
|
+
});
|
|
97
|
+
if ("error" in read) {
|
|
98
|
+
toolsError = read.error;
|
|
99
|
+
} else {
|
|
100
|
+
const check = validateToolEntries(read.entries, {
|
|
101
|
+
agentName: String(frontmatter.name),
|
|
102
|
+
agentFilePath: filePath,
|
|
103
|
+
});
|
|
104
|
+
if (check.ok) {
|
|
105
|
+
parsedTools = read.entries;
|
|
106
|
+
} else {
|
|
107
|
+
toolsError = check.error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
57
110
|
}
|
|
58
111
|
|
|
59
|
-
|
|
112
|
+
// `mcp: true` opts the child back in to MCP tools (ADR 0028). Quoted YAML
|
|
113
|
+
// gives the string "true", which means the same thing to a human reader.
|
|
114
|
+
const mcpOptIn = frontmatter.mcp === true || frontmatter.mcp === "true";
|
|
115
|
+
|
|
116
|
+
const knownKeys = new Set(["name", "description", "model", "thinking", "mcp"]);
|
|
60
117
|
const extraFields: Record<string, unknown> = {};
|
|
61
118
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
62
119
|
if (key === "tools") continue; // tools is handled separately above/below
|
|
@@ -66,7 +123,9 @@ function loadAgentsFromDir(dir: string, source: "global" | "user" | "project"):
|
|
|
66
123
|
extraFields[key] = value;
|
|
67
124
|
}
|
|
68
125
|
}
|
|
69
|
-
// If tools was non-string, store it in extraFields for round-trip preservation
|
|
126
|
+
// If tools was non-string, store it in extraFields for round-trip preservation.
|
|
127
|
+
// A rejected string list is preserved for the same reason: the writer must not
|
|
128
|
+
// quietly delete a line the user typed.
|
|
70
129
|
if (frontmatter.tools !== undefined && !parsedTools && "tools" in frontmatter) {
|
|
71
130
|
extraFields.tools = frontmatter.tools;
|
|
72
131
|
}
|
|
@@ -80,6 +139,8 @@ function loadAgentsFromDir(dir: string, source: "global" | "user" | "project"):
|
|
|
80
139
|
};
|
|
81
140
|
if (frontmatter.model) config.model = frontmatter.model as string;
|
|
82
141
|
if (frontmatter.thinking) config.thinking = frontmatter.thinking as string;
|
|
142
|
+
if (mcpOptIn) config.mcp = true;
|
|
143
|
+
if (toolsError) config.toolsError = toolsError;
|
|
83
144
|
if (parsedTools && parsedTools.length > 0) config.tools = parsedTools;
|
|
84
145
|
if (Object.keys(extraFields).length > 0) config.extraFields = extraFields;
|
|
85
146
|
|
|
@@ -226,7 +287,13 @@ export function formatAgentList(agents: AgentConfig[]): string {
|
|
|
226
287
|
let line = `• ${a.name} [${a.source}] — ${a.description}`;
|
|
227
288
|
const extras: string[] = [];
|
|
228
289
|
if (a.model) extras.push(`model: ${a.model}`);
|
|
229
|
-
if (a.
|
|
290
|
+
if (a.toolsError) {
|
|
291
|
+
// The list was rejected at read, so no allowlist is in force — say so here
|
|
292
|
+
// rather than letting the agent look like an unconfigured one.
|
|
293
|
+
extras.push("tools rejected — fix the tools: field");
|
|
294
|
+
} else if (a.tools && a.tools.length > 0) {
|
|
295
|
+
extras.push(`${a.tools.length} tools`);
|
|
296
|
+
}
|
|
230
297
|
if (extras.length > 0) line += ` (${extras.join(", ")})`;
|
|
231
298
|
return line;
|
|
232
299
|
});
|