@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.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/package.json +10 -10
  3. package/src/agent-panel.mcp.test.ts +76 -0
  4. package/src/agent-panel.tools.test.ts +162 -0
  5. package/src/agent-panel.ts +37 -8
  6. package/src/agent-panel.width-audit.test.ts +258 -0
  7. package/src/agents-parse.test.ts +252 -0
  8. package/src/agents.ts +72 -5
  9. package/src/compact.test.ts +136 -57
  10. package/src/compact.ts +57 -37
  11. package/src/dispatch.test.ts +73 -1
  12. package/src/dispatch.ts +20 -2
  13. package/src/execute.test.ts +9 -1
  14. package/src/execute.ts +26 -29
  15. package/src/expanded.test.ts +472 -0
  16. package/src/expanded.ts +202 -82
  17. package/src/format.test.ts +71 -29
  18. package/src/format.ts +71 -28
  19. package/src/frontmatter-io.test.ts +62 -0
  20. package/src/frontmatter-io.ts +16 -2
  21. package/src/handlers.test.ts +21 -3
  22. package/src/handlers.ts +22 -3
  23. package/src/index.output-schema.test.ts +317 -0
  24. package/src/index.test.ts +566 -0
  25. package/src/index.ts +169 -16
  26. package/src/render.ts +46 -36
  27. package/src/row-block.test.ts +244 -0
  28. package/src/row-block.ts +209 -0
  29. package/src/run-outcome.test.ts +71 -0
  30. package/src/run-outcome.ts +84 -0
  31. package/src/spawn.test.ts +204 -19
  32. package/src/spawn.ts +49 -17
  33. package/src/stream.test.ts +215 -17
  34. package/src/stream.ts +47 -9
  35. package/src/thinking-validation.test.ts +96 -0
  36. package/src/thinking-validation.ts +78 -0
  37. package/src/tool-declarations.test.ts +58 -0
  38. package/src/tool-row.width-audit.test.ts +720 -0
  39. package/src/tool-schema.test.ts +154 -0
  40. package/src/tool-schema.ts +106 -4
  41. package/src/tool-validation.test.ts +321 -0
  42. package/src/tool-validation.ts +262 -0
  43. package/src/types.ts +86 -0
  44. package/src/usage.test.ts +95 -0
  45. package/src/usage.ts +87 -0
  46. package/src/cost.test.ts +0 -62
  47. package/src/cost.ts +0 -8
package/src/spawn.test.ts CHANGED
@@ -1,18 +1,46 @@
1
1
  import { describe, it, expect, vi, afterEach } from "vitest";
2
2
  import * as net from "node:net";
3
- import { getBus, Events } from "@pi-archimedes/core/bus";
4
- import { startAskSocketServer } from "./spawn.js";
3
+ import { EventEmitter } from "node:events";
4
+ import type { ChildProcess } from "node:child_process";
5
+ import { Events } from "@pi-archimedes/core/events";
6
+ import type { AgentConfig } from "./agents.js";
7
+
8
+ // The child's argv is a contract with the `pi` CLI, and CLI strings are invisible
9
+ // to tsc — these tests are the only guard on what the child actually receives.
10
+ // `node:child_process` is mocked so no real pi process is spawned.
11
+ const spawnMock = vi.hoisted(() => vi.fn());
12
+ vi.mock("node:child_process", () => ({ spawn: spawnMock }));
13
+
14
+ const { startAskSocketServer, spawnSubagent } = await import("./spawn.js");
5
15
 
6
16
  // startAskSocketServer is tested directly (not via spawnSubagent, which would
7
- // spawn a real `pi` process). The real bus is used — subscriptions are
8
- // unsubscribed in afterEach so they don't leak into other test files in the
9
- // same worker (the bus is a globalThis singleton).
17
+ // spawn a real `pi` process). Each test gets its OWN bus stand-in — the host
18
+ // handle is passed in rather than reached for, so there is no process-wide
19
+ // singleton to leak between tests any more.
20
+
21
+ /** Stands in for the host's `createEventBus()` (same emit/on/unsubscribe shape). */
22
+ function fakeBus() {
23
+ const listeners = new Map<string, Array<(data: unknown) => void>>();
24
+ return {
25
+ emit(channel: string, data: unknown): void {
26
+ for (const handler of [...(listeners.get(channel) ?? [])]) handler(data);
27
+ },
28
+ on(channel: string, handler: (data: unknown) => void): () => void {
29
+ const subs = listeners.get(channel) ?? [];
30
+ subs.push(handler);
31
+ listeners.set(channel, subs);
32
+ return () => {
33
+ const current = listeners.get(channel) ?? [];
34
+ const idx = current.indexOf(handler);
35
+ if (idx !== -1) current.splice(idx, 1);
36
+ };
37
+ },
38
+ };
39
+ }
10
40
 
11
41
  const cleanups: Array<() => void> = [];
12
- const unsubs: Array<() => void> = [];
13
42
 
14
43
  afterEach(() => {
15
- unsubs.splice(0).forEach((u) => u());
16
44
  cleanups.splice(0).forEach((c) => c());
17
45
  });
18
46
 
@@ -20,6 +48,83 @@ function track(fn: () => void): void {
20
48
  cleanups.push(fn);
21
49
  }
22
50
 
51
+ // ── spawnSubagent argv ──────────────────────────────────────────────────────
52
+
53
+ /** A minimal spawn target: the argv assertions never touch the process. */
54
+ function fakeSpawnResult(): ChildProcess {
55
+ const child = new EventEmitter() as ChildProcess & { pid: number };
56
+ Object.assign(child, { pid: 4242, killed: false, kill: () => true });
57
+ return child;
58
+ }
59
+
60
+ /** Run spawnSubagent against the mocked spawn and return the argv it received. */
61
+ function argvFor(agent: Partial<AgentConfig> | undefined): string[] {
62
+ spawnMock.mockReset();
63
+ spawnMock.mockReturnValue(fakeSpawnResult());
64
+ const events = fakeBus();
65
+ const socket = startAskSocketServer(agent?.name ?? "general", events);
66
+ track(socket.cleanup);
67
+ spawnSubagent({
68
+ task: "do the thing",
69
+ model: undefined,
70
+ activeModel: undefined,
71
+ cwd: "/tmp",
72
+ signal: undefined,
73
+ events,
74
+ agent: agent
75
+ ? { name: agent.name ?? "general", description: "d", systemPrompt: "p", source: "user", filePath: "/tmp/a.md", ...agent }
76
+ : undefined,
77
+ });
78
+ const call = spawnMock.mock.calls[0] as [string, string[]] | undefined;
79
+ if (!call) throw new Error("spawn was not called");
80
+ return call[1];
81
+ }
82
+
83
+ describe("spawnSubagent tool allowlist (closed set, ADR 0028)", () => {
84
+ afterEach(() => {
85
+ spawnMock.mockReset();
86
+ });
87
+
88
+ it("a non-empty tools allowlist also passes --no-mcp, so the list stays closed", () => {
89
+ const argv = argvFor({ tools: ["read", "bash"] });
90
+ expect(argv).toContain("--tools");
91
+ expect(argv[argv.indexOf("--tools") + 1]).toBe("read,bash");
92
+ expect(argv).toContain("--no-mcp");
93
+ });
94
+
95
+ it("mcp: true is the only way the allowlist stops being closed", () => {
96
+ const argv = argvFor({ tools: ["read", "bash"], mcp: true });
97
+ expect(argv).toContain("--tools");
98
+ expect(argv).not.toContain("--no-mcp");
99
+ });
100
+
101
+ it("an empty tools list changes neither the tool set nor MCP", () => {
102
+ const argv = argvFor({ tools: [] });
103
+ expect(argv).not.toContain("--tools");
104
+ expect(argv).not.toContain("--no-mcp");
105
+ });
106
+
107
+ it("mcp: true without an allowlist passes neither flag (nothing to close)", () => {
108
+ const argv = argvFor({ mcp: true });
109
+ expect(argv).not.toContain("--tools");
110
+ expect(argv).not.toContain("--no-mcp");
111
+ });
112
+
113
+ it("the recursion guard survives the closed allowlist", () => {
114
+ const argv = argvFor({ tools: ["read"] });
115
+ const excludeIdx = argv.indexOf("--exclude-tools");
116
+ expect(excludeIdx).toBeGreaterThan(-1);
117
+ expect(argv[excludeIdx + 1]).toContain("subagent");
118
+ });
119
+
120
+ it("an agent with no config at all gets neither --tools nor --no-mcp", () => {
121
+ const argv = argvFor(undefined);
122
+ expect(argv).not.toContain("--tools");
123
+ expect(argv).not.toContain("--no-mcp");
124
+ expect(argv).toContain("--exclude-tools");
125
+ });
126
+ });
127
+
23
128
  function connectClient(socketPath: string): Promise<net.Socket> {
24
129
  return new Promise((resolve, reject) => {
25
130
  const client = net.connect(socketPath, () => resolve(client));
@@ -40,12 +145,13 @@ function writeAskRequest(client: net.Socket, requestId: string, toolCallId?: str
40
145
 
41
146
  describe("startAskSocketServer", () => {
42
147
  it("a child ask_request carrying toolCallId → the bus ASK_REQUEST carries the same toolCallId", async () => {
43
- const { socketPath, cleanup } = startAskSocketServer("test-agent");
148
+ const events = fakeBus();
149
+ const { socketPath, cleanup } = startAskSocketServer("test-agent", events);
44
150
  track(cleanup);
45
151
  await new Promise((r) => setTimeout(r, 50)); // let the server listen
46
152
 
47
153
  const requests: unknown[] = [];
48
- unsubs.push(getBus().on(Events.ASK_REQUEST, (p) => requests.push(p)));
154
+ events.on(Events.ASK_REQUEST, (p) => requests.push(p));
49
155
 
50
156
  const client = await connectClient(socketPath);
51
157
  writeAskRequest(client, "req-1", "tool-9");
@@ -57,9 +163,9 @@ describe("startAskSocketServer", () => {
57
163
  expect(req.toolCallId).toBe("tool-9");
58
164
  expect(req.questions).toEqual([{ id: "q1", question: "Which?", options: [{ label: "A" }] }]);
59
165
 
60
- // Deliver the response (drains pending) before closing so no ASK_CANCEL
61
- // leaks onto the global bus queue for the next test
62
- getBus().emit(Events.ASK_RESPONSE, {
166
+ // Deliver the response (drains pending) before closing so no ASK_CANCEL is
167
+ // signalled for a request that was already answered.
168
+ events.emit(Events.ASK_RESPONSE, {
63
169
  requestId: "req-1",
64
170
  cancelled: false,
65
171
  results: [{ id: "q1", selectedOptions: ["A"] }],
@@ -69,15 +175,60 @@ describe("startAskSocketServer", () => {
69
175
  await new Promise((r) => setTimeout(r, 50)); // close handler ran (no-op)
70
176
  });
71
177
 
178
+ /**
179
+ * The ask round-trip, over the handle: the child's socket request goes out on
180
+ * the bus, the answer comes back on the SAME handle, and the server writes it
181
+ * back down the socket. Before this migration both halves went through the
182
+ * suite's `globalThis` bus; the transport changed, the socket contract did not.
183
+ */
184
+ it("round-trip: ask_request in → ASK_REQUEST out on the handle → ASK_RESPONSE in → ask_response back down the socket", async () => {
185
+ const events = fakeBus();
186
+ const { socketPath, cleanup } = startAskSocketServer("test-agent", events);
187
+ track(cleanup);
188
+ await new Promise((r) => setTimeout(r, 50));
189
+
190
+ const frames: unknown[] = [];
191
+ const requests: unknown[] = [];
192
+ events.on(Events.ASK_REQUEST, (p) => requests.push(p));
193
+
194
+ const client = await connectClient(socketPath);
195
+ client.on("data", (chunk) => {
196
+ for (const line of chunk.toString("utf-8").split("\n")) {
197
+ if (line.trim()) frames.push(JSON.parse(line));
198
+ }
199
+ });
200
+
201
+ writeAskRequest(client, "req-rt", "tool-rt");
202
+ await vi.waitFor(() => expect(requests.length).toBe(1));
203
+
204
+ events.emit(Events.ASK_RESPONSE, {
205
+ requestId: "req-rt",
206
+ cancelled: false,
207
+ results: [{ id: "q1", selectedOptions: ["A"], customInput: undefined }],
208
+ });
209
+
210
+ await vi.waitFor(() => expect(frames.length).toBe(1));
211
+ expect(frames[0]).toEqual({
212
+ type: "ask_response",
213
+ requestId: "req-rt",
214
+ cancelled: false,
215
+ results: [{ id: "q1", selectedOptions: ["A"] }],
216
+ });
217
+
218
+ client.destroy();
219
+ await new Promise((r) => setTimeout(r, 50));
220
+ });
221
+
72
222
  it("closing the child socket → ASK_CANCEL for the pending requestId + source", async () => {
73
- const { socketPath, cleanup } = startAskSocketServer("test-agent");
223
+ const events = fakeBus();
224
+ const { socketPath, cleanup } = startAskSocketServer("test-agent", events);
74
225
  track(cleanup);
75
226
  await new Promise((r) => setTimeout(r, 50)); // let the server listen
76
227
 
77
228
  const cancels: unknown[] = [];
78
- unsubs.push(getBus().on(Events.ASK_CANCEL, (p) => cancels.push(p)));
229
+ events.on(Events.ASK_CANCEL, (p) => cancels.push(p));
79
230
  const requests: unknown[] = [];
80
- unsubs.push(getBus().on(Events.ASK_REQUEST, (p) => requests.push(p)));
231
+ events.on(Events.ASK_REQUEST, (p) => requests.push(p));
81
232
 
82
233
  const client = await connectClient(socketPath);
83
234
  writeAskRequest(client, "req-2");
@@ -91,21 +242,22 @@ describe("startAskSocketServer", () => {
91
242
  });
92
243
 
93
244
  it("no ASK_CANCEL once the response has already been delivered (pending drained)", async () => {
94
- const { socketPath, cleanup } = startAskSocketServer("test-agent");
245
+ const events = fakeBus();
246
+ const { socketPath, cleanup } = startAskSocketServer("test-agent", events);
95
247
  track(cleanup);
96
248
  await new Promise((r) => setTimeout(r, 50)); // let the server listen
97
249
 
98
250
  const cancels: unknown[] = [];
99
- unsubs.push(getBus().on(Events.ASK_CANCEL, (p) => cancels.push(p)));
251
+ events.on(Events.ASK_CANCEL, (p) => cancels.push(p));
100
252
  const requests: unknown[] = [];
101
- unsubs.push(getBus().on(Events.ASK_REQUEST, (p) => requests.push(p)));
253
+ events.on(Events.ASK_REQUEST, (p) => requests.push(p));
102
254
 
103
255
  const client = await connectClient(socketPath);
104
256
  writeAskRequest(client, "req-3");
105
257
  await vi.waitFor(() => expect(requests.length).toBe(1));
106
258
 
107
259
  // Deliver the response (drains pending) before the socket closes
108
- getBus().emit(Events.ASK_RESPONSE, {
260
+ events.emit(Events.ASK_RESPONSE, {
109
261
  requestId: "req-3",
110
262
  cancelled: false,
111
263
  results: [{ id: "q1", selectedOptions: ["A"] }],
@@ -117,3 +269,36 @@ describe("startAskSocketServer", () => {
117
269
  expect(cancels.length).toBe(0);
118
270
  });
119
271
  });
272
+
273
+ // ── thinking level (pi warns and drops; we must not feed it one) ───────────
274
+
275
+ describe("spawnSubagent thinking level", () => {
276
+ afterEach(() => {
277
+ spawnMock.mockReset();
278
+ });
279
+
280
+ it("passes a level pi accepts through to --thinking", () => {
281
+ const argv = argvFor({ thinking: "xhigh" });
282
+ const at = argv.indexOf("--thinking");
283
+ expect(at).toBeGreaterThan(-1);
284
+ expect(argv[at + 1]).toBe("xhigh");
285
+ });
286
+
287
+ it("does not pass a level pi would warn about and drop", () => {
288
+ // `--thinking ultra` is a `warning` diagnostic and a dropped flag in pi's
289
+ // own parser (dist/cli/args.js), so the child ran at the default thinking
290
+ // level while its config said otherwise — and in `--mode json` that warning
291
+ // is stderr, which the stream reader only shows for a failed run. Passing
292
+ // nothing is the same outcome, said out loud at dispatch instead.
293
+ const argv = argvFor({ thinking: "ultra" });
294
+ expect(argv).not.toContain("--thinking");
295
+ });
296
+
297
+ it("is case-sensitive, exactly like pi's list", () => {
298
+ expect(argvFor({ thinking: "Medium" })).not.toContain("--thinking");
299
+ });
300
+
301
+ it("passes no --thinking when the config sets none", () => {
302
+ expect(argvFor({})).not.toContain("--thinking");
303
+ });
304
+ });
package/src/spawn.ts CHANGED
@@ -4,7 +4,9 @@ import * as net from "node:net";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { randomUUID } from "node:crypto";
7
- import { getBus, Events } from "@pi-archimedes/core/bus";
7
+ import type { EventBus } from "@earendil-works/pi-coding-agent";
8
+ import { Events, emitOn } from "@pi-archimedes/core/events";
9
+ import { resolveThinkingLevel } from "./thinking-validation.js";
8
10
  import type { AgentConfig } from "./agents.js";
9
11
 
10
12
  export interface SpawnOptions {
@@ -14,6 +16,8 @@ export interface SpawnOptions {
14
16
  cwd: string | undefined;
15
17
  signal: AbortSignal | undefined;
16
18
  agent: AgentConfig | undefined;
19
+ /** The parent's host handle (`pi.events`) — see `startAskSocketServer`. */
20
+ events: EventBus;
17
21
  }
18
22
 
19
23
  /**
@@ -73,25 +77,32 @@ function resolvePiBinary(): string {
73
77
  }
74
78
 
75
79
  /**
76
- * Start a Unix socket server that bridges the child's ask tool to the parent bus.
80
+ * Start a Unix socket server that bridges the child's ask tool to the parent's
81
+ * event handle.
77
82
  *
78
83
  * The child's ask tool (in packages/ask) connects to PI_SUBAGENT_SOCKET and sends:
79
84
  * { type: "ask_request", requestId, questions, toolCallId } as a JSON line
80
85
  * and waits for:
81
86
  * { type: "ask_response", requestId, cancelled, results } as a JSON line
82
87
  *
83
- * We forward the request onto the bus (ASK_REQUEST, carrying toolCallId), the
88
+ * We forward the request onto the handle (ASK_REQUEST, carrying toolCallId), the
84
89
  * ask package (or the bridge, in bridge mode) answers it, then emits
85
- * ASK_RESPONSE on the bus, and we write it back to the socket connection.
90
+ * ASK_RESPONSE on the same handle, and we write it back to the socket connection.
86
91
  *
87
92
  * When the socket closes (child exit, or the child's own 5-min timeout →
88
93
  * socket.end()) we signal the bridge via ASK_CANCEL for any still-pending ask.
89
94
  * We do NOT emit ASK_RESPONSE ourselves — the bridge is the sole emitter
90
95
  * (no double-decrement of its refcount).
91
96
  *
97
+ * @param events the parent's `pi.events`, injected: this is a plain function, not
98
+ * an extension, so the handle has to come from the caller that has
99
+ * one. The subscription it opens is closed by the returned
100
+ * `cleanup()` — a per-spawn subscription with a per-spawn
101
+ * teardown, which is why it cannot accumulate across runs.
102
+ *
92
103
  * Returns the socket path and a cleanup function.
93
104
  */
94
- export function startAskSocketServer(agentName: string): { socketPath: string; cleanup: () => void } {
105
+ export function startAskSocketServer(agentName: string, events: EventBus): { socketPath: string; cleanup: () => void } {
95
106
  // Use named pipes on Windows, Unix domain sockets elsewhere.
96
107
  // Linux socket path limit is 108 chars — keep it short.
97
108
  const id = randomUUID().slice(0, 8);
@@ -103,8 +114,8 @@ export function startAskSocketServer(agentName: string): { socketPath: string; c
103
114
  // Map of pending ask requests: requestId → write-back callback
104
115
  const pending = new Map<string, (response: unknown) => void>();
105
116
 
106
- // Listen for ASK_RESPONSE from the bus and route back to the waiting socket conn
107
- const unsubResponse = getBus().on(Events.ASK_RESPONSE, (payload: unknown) => {
117
+ // Listen for ASK_RESPONSE on the handle and route back to the waiting socket conn
118
+ const unsubResponse = events.on(Events.ASK_RESPONSE, (payload: unknown) => {
108
119
  const data = payload as {
109
120
  requestId: string;
110
121
  cancelled: boolean;
@@ -152,9 +163,9 @@ export function startAskSocketServer(agentName: string): { socketPath: string; c
152
163
  }
153
164
  });
154
165
  socketRequestIds.add(msg.requestId);
155
- // Forward to bus — the ask package (or the bridge, in bridge mode)
166
+ // Forward onto the handle — the ask package (or the bridge, in bridge mode)
156
167
  // answers it; toolCallId rides along for Client correlation
157
- getBus().emit(Events.ASK_REQUEST, {
168
+ emitOn(events, Events.ASK_REQUEST, {
158
169
  source: `subagent:${agentName}`,
159
170
  requestId: msg.requestId,
160
171
  questions: msg.questions,
@@ -174,7 +185,7 @@ export function startAskSocketServer(agentName: string): { socketPath: string; c
174
185
  socket.on("close", () => {
175
186
  for (const rid of socketRequestIds) {
176
187
  if (pending.has(rid)) {
177
- getBus().emit(Events.ASK_CANCEL, { requestId: rid, source: `subagent:${agentName}` });
188
+ emitOn(events, Events.ASK_CANCEL, { requestId: rid, source: `subagent:${agentName}` });
178
189
  pending.delete(rid);
179
190
  }
180
191
  }
@@ -208,7 +219,7 @@ export function spawnSubagent(options: SpawnOptions): ChildProcess {
208
219
  const agentName = options.agent?.name ?? "general";
209
220
 
210
221
  // Start ask bridge socket before spawning so the env var is ready
211
- const { socketPath, cleanup: cleanupSocket } = startAskSocketServer(agentName);
222
+ const { socketPath, cleanup: cleanupSocket } = startAskSocketServer(agentName, options.events);
212
223
 
213
224
  // Build CLI args
214
225
  const args: string[] = ["--mode", "json", "--no-session", "-p"];
@@ -219,14 +230,35 @@ export function spawnSubagent(options: SpawnOptions): ChildProcess {
219
230
  args.push("--model", model);
220
231
  }
221
232
 
222
- // Thinking level from agent config
223
- if (options.agent?.thinking) {
224
- args.push("--thinking", options.agent.thinking);
233
+ // Thinking level from agent config. An unknown level is NOT passed on: pi
234
+ // warns and drops the flag itself, which leaves the child on the default
235
+ // level with the explanation in a stderr nobody reads for a successful run.
236
+ // `index.ts` reports the same drop where the user can see it.
237
+ const thinking = resolveThinkingLevel(options.agent?.thinking);
238
+ if (thinking.level !== undefined) {
239
+ args.push("--thinking", thinking.level);
225
240
  }
226
241
 
227
- // Tool allowlist from agent config
228
- if (options.agent?.tools && options.agent.tools.length > 0) {
229
- args.push("--tools", options.agent.tools.join(","));
242
+ // Tool allowlist from agent config.
243
+ //
244
+ // pi 1.0.4 changed what a non-empty `--tools` does to MCP tools. It no longer
245
+ // filters them: unless the list is empty or names an `mcp__` entry, they stay
246
+ // REGISTERED — reachable through codemode and `tool_search` — even though they
247
+ // are not *declared* to the model (`agent-session.js`: `_allowlistFiltersMcp`
248
+ // false → `_isAllowedTool` keeps them, `_isActivatable` withholds the
249
+ // declaration unless `tool_search` can load them). So the child's tool box is
250
+ // wider than the three names in the config, which is the hole ADR 0028 closed:
251
+ // `--no-mcp` stops the servers connecting at all, so there is nothing to reach.
252
+ // A config that wants the servers asks with `mcp: true` — verified against a
253
+ // live 1.2.0 child: with `--tools read` it sees exactly `read` either way, so
254
+ // the opt-in's observable effect today is registration, not a declared list
255
+ // (see ADR 0028's verification note for the open follow-up).
256
+ const tools = options.agent?.tools ?? [];
257
+ if (tools.length > 0) {
258
+ args.push("--tools", tools.join(","));
259
+ if (options.agent?.mcp !== true) {
260
+ args.push("--no-mcp");
261
+ }
230
262
  }
231
263
 
232
264
  // Always exclude the subagent tool itself to prevent infinite recursion