Merge PR #7344: fix(agent): stop leaking scout into prompts when it is disabled (@szavadsky)

# Conflicts:
#	packages/coding-agent/src/prompts/system/system-prompt.md
This commit is contained in:
can1357
2026-08-02 20:52:56 +02:00
22 changed files with 252 additions and 42 deletions
@@ -46,6 +46,20 @@ describe("composeSpawnAdvisory", () => {
expect(advisory).toContain("Coordinate:");
});
it("drops the scout example from the specialization tip when scout is unavailable", () => {
const advisory = composeSpawnAdvisory({
agents: ["task", "task"],
items: [worker(), worker()],
depthCapacity: true,
ircEnabled: true,
willRunAsync: true,
scoutAvailable: false,
});
expect(advisory).toContain("generic");
expect(advisory).not.toContain("scout");
expect(advisory).toContain("Coordinate:");
});
it("drops the coordination suggestion on the sync path but keeps the specialization tip", () => {
const advisory = composeSpawnAdvisory({
agents: ["task", "task"],
@@ -78,7 +78,23 @@ describe("task tool advisory gating via suppressSpawnAdvisory", () => {
} as unknown as ToolSession;
}
async function spawnText(suppress: boolean): Promise<string> {
function sessionWithScoutDisabled(): ToolSession {
return {
cwd: "/tmp",
hasUI: false,
// `task.disabledAgents` is what the task tool reads to drop scout from
// the rendered description and the appended specialization advisory.
settings: Settings.isolated({
"task.isolation.mode": "none",
"task.batch": true,
"task.disabledAgents": ["scout"],
}),
getSessionFile: () => null,
getSessionSpawns: () => "*",
} as unknown as ToolSession;
}
async function spawnTextFor(s: ToolSession): Promise<string> {
vi.spyOn(discoveryModule, "discoverAgents").mockResolvedValue({ agents: [agent], projectAgentsDir: null });
vi.spyOn(executorModule, "runSubprocess").mockImplementation(
async (options): Promise<SingleResult> => ({
@@ -97,9 +113,7 @@ describe("task tool advisory gating via suppressSpawnAdvisory", () => {
requests: 1,
}),
);
const tool = await TaskTool.create(session(suppress));
// Both items omit `agent`, so each resolves to the generic spawn-policy
// default ("task") — the ≥2-generics condition the advisory gates on.
const tool = await TaskTool.create(s);
const result = await tool.execute("tc", {
context: "shared fan-out background",
tasks: [
@@ -111,10 +125,14 @@ describe("task tool advisory gating via suppressSpawnAdvisory", () => {
}
it("appends the specialization advisory when a batch resolves two generic workers", async () => {
expect(await spawnText(false)).toContain('`agent: "scout"`');
expect(await spawnTextFor(session(false))).toContain('`agent: "scout"`');
});
it("drops the scout example when scout is disabled", async () => {
expect(await spawnTextFor(sessionWithScoutDisabled())).not.toContain("scout");
});
it("omits the advisory entirely when the session suppresses it", async () => {
expect(await spawnText(true)).not.toContain('`agent: "scout"`');
expect(await spawnTextFor(session(true))).not.toContain('`agent: "scout"`');
});
});
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "bun:test";
import { Settings } from "../../src/config/settings";
import * as taskDiscovery from "../../src/task/discovery";
import { TaskTool } from "../../src/task/index";
import { isScoutSpawnable } from "../../src/task/spawn-policy";
import type { AgentDefinition } from "../../src/task/types";
import { getTaskSchema } from "../../src/task/types";
import type { ToolSession } from "../../src/tools";
@@ -60,3 +61,72 @@ describe("task spawn policy surfaces", () => {
expect(description).not.toContain("### oracle");
});
});
describe("isScoutSpawnable", () => {
it("is true with no disabled agents and unrestricted spawns", () => {
expect(isScoutSpawnable(undefined, "*")).toBe(true);
expect(isScoutSpawnable([], "*")).toBe(true);
});
it("is false when scout is disabled via task.disabledAgents", () => {
expect(isScoutSpawnable(["scout"], "*")).toBe(false);
expect(isScoutSpawnable(["scout", "reviewer"], "*")).toBe(false);
});
it("is false when spawning is disabled", () => {
expect(isScoutSpawnable(undefined, false)).toBe(false);
expect(isScoutSpawnable(undefined, "")).toBe(false);
});
it("is false when scout is not in the allowed spawn list", () => {
expect(isScoutSpawnable(undefined, "reviewer")).toBe(false);
});
it("is true when scout is in the allowed spawn list", () => {
expect(isScoutSpawnable(undefined, "scout,reviewer")).toBe(true);
expect(isScoutSpawnable(["reviewer"], "scout")).toBe(true);
});
});
describe("task tool description scout gating", () => {
afterEach(() => {
vi.restoreAllMocks();
});
async function renderDescription(disabledScout: boolean): Promise<string> {
vi.spyOn(taskDiscovery, "discoverAgents").mockResolvedValue({
agents: [
{ name: "scout", description: "Read-only scout.", systemPrompt: "Scout.", source: "bundled" },
{ name: "reviewer", description: "Reviewer.", systemPrompt: "Review.", source: "bundled" },
],
projectAgentsDir: null,
});
const settings = Settings.isolated({
"async.enabled": false,
"task.batch": true,
"task.isolation.mode": "none",
...(disabledScout ? { "task.disabledAgents": ["scout"] } : {}),
});
const tool = await TaskTool.create({
cwd: process.cwd(),
hasUI: false,
settings,
getSessionFile: () => null,
getSessionSpawns: () => "*",
} as unknown as ToolSession);
return tool.description;
}
it("mentions scout in the task description when scout is enabled", async () => {
expect(await renderDescription(false)).toContain("scout");
});
it("omits every scout reference from the task description when scout is disabled", async () => {
const description = await renderDescription(true);
expect(description).not.toContain("scout");
// The read-only agent remains listed as an available agent (the spawn
// policy only filters disabledAgents, so reviewer stays); only the
// hard-coded scout guidance is dropped.
expect(description).toContain("### reviewer");
});
});