From 91bf1a31cd3f3d3b460169477141d68aeb29c4e7 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 15:13:17 -0700 Subject: [PATCH 1/8] test(prompts): promptResidual appended once for grok leaf --- src/agent/model-family-policy.test.ts | 27 +++++++++++++++++++++++++++ src/agent/prompt-sizes.test.ts | 19 +++++++++++++++++++ src/agent/prompts.test.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/src/agent/model-family-policy.test.ts b/src/agent/model-family-policy.test.ts index ed2843717..d59e52c10 100644 --- a/src/agent/model-family-policy.test.ts +++ b/src/agent/model-family-policy.test.ts @@ -102,4 +102,31 @@ describe("resolveModelFamilyPolicy", () => { expect(muse.toolDisciplineRules).toContain("Never re-read a file"); expect(base.toolDisciplineRules).toBeUndefined(); }); + + describe("promptResidual (CL-8297)", () => { + test("grok leaf carries the generic 4-line tool-budget residual", () => { + const leaf = resolveModelFamilyPolicy({ + providerName: "xai/default", + model: "grok-4.6", + }); + expect(leaf.family).toBe("grok"); + expect(leaf.promptResidual).toBeDefined(); + expect(leaf.promptResidual!.split("\n")).toHaveLength(4); + expect(leaf.promptResidual).toContain("Tool budget:"); + }); + + test("grok orchestrators and default family carry no residual", () => { + const orchestrator = resolveModelFamilyPolicy({ + providerName: "xai/default", + model: "grok-4.6", + orchestrator: true, + }); + expect(orchestrator.promptResidual).toBeUndefined(); + const base = resolveModelFamilyPolicy({ + providerName: "anthropic", + model: "claude-sonnet-4", + }); + expect(base.promptResidual).toBeUndefined(); + }); + }); }); diff --git a/src/agent/prompt-sizes.test.ts b/src/agent/prompt-sizes.test.ts index deef84e8e..92d2b40e2 100644 --- a/src/agent/prompt-sizes.test.ts +++ b/src/agent/prompt-sizes.test.ts @@ -332,3 +332,22 @@ describe("skywalker grok prefix (infer envelope vs trimmed director)", () => { } }); }); + +describe("grok tool-budget residual (CL-8297)", () => { + const countOccurrences = (haystack: string, needle: string): number => + haystack.split(needle).length - 1; + + test("a grok leaf director prompt contains the tool budget exactly once", () => { + const prompt = assembleDirectorPrompt("builder", "grok"); + expect(countOccurrences(prompt, "Tool budget:")).toBe(1); + }); + + test("default-family and orchestrator prompts carry no tool budget", () => { + expect(assembleDirectorPrompt("builder", "default")).not.toContain( + "Tool budget:", + ); + expect(assembleDirectorPrompt("skywalker", "grok")).not.toContain( + "Tool budget:", + ); + }); +}); diff --git a/src/agent/prompts.test.ts b/src/agent/prompts.test.ts index bc36b8d0e..2a05ccafc 100644 --- a/src/agent/prompts.test.ts +++ b/src/agent/prompts.test.ts @@ -370,3 +370,29 @@ describe("grok finish-bias residual gating (extends existing provider-family tes expect(prompt.toLowerCase()).not.toContain("kimi"); }); }); + +describe("promptResidual assembly (CL-8297)", () => { + const TOOL_BUDGET = + "Tool budget:\n" + + "- Batch independent tool calls into a single turn.\n" + + "- Never re-issue a tool call whose result you already have.\n" + + "- When the next call would only repeat prior work, write the report instead."; + + it("appends promptResidual exactly once at the tail for a grok leaf", () => { + const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, { + orchestrator: false, + grokAntiThrash: true, + promptResidual: TOOL_BUDGET, + }); + expect(countOccurrences(prompt, TOOL_BUDGET)).toBe(1); + expect(prompt.trimEnd().endsWith(TOOL_BUDGET)).toBe(true); + }); + + it("omits the tool budget when promptResidual is unset", () => { + const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, { + orchestrator: false, + grokAntiThrash: true, + }); + expect(prompt).not.toContain("Tool budget:"); + }); +}); From d001f4817db7bb6b8c172c3f058a7bcaa01c3e31 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 15:17:08 -0700 Subject: [PATCH 2/8] feat(prompts): promptResidual with grok tool budget, appended once --- src/agent/model-family-policy.ts | 21 +++++++++++++++++++++ src/agent/prompt-sizes.ts | 7 ++++++- src/agent/prompts.ts | 8 ++++++++ src/subagent/run.ts | 1 + 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/agent/model-family-policy.ts b/src/agent/model-family-policy.ts index e77057594..916f76488 100644 --- a/src/agent/model-family-policy.ts +++ b/src/agent/model-family-policy.ts @@ -38,6 +38,14 @@ export interface ModelFamilyPolicy { * at the tail so it cannot disturb the cached prompt prefix. */ toolDisciplineRules?: string; + /** + * Provider-family residual appended once to the assembled leaf system + * prompt (CL-8297). Generic tool-budget text today (grok only); the + * ceremony / Claude / GPT seams stay unfilled in sibling lanes. Withheld + * from orchestrators and appended at the tail so it cannot disturb the + * cached prompt prefix. Undefined for families that need none. + */ + promptResidual?: string | undefined; } const DEFAULT_WRAP_UP_NUDGE_TEXT = @@ -66,6 +74,16 @@ const DEFAULT_POLICY: Omit = { advertisedToolDeny: [], }; +// Generic 4-line tool-budget residual (CL-8297). Grok leaves get this via +// promptResidual today; other families leave the seam unfilled until their +// own lanes land. Deliberately free of ceremony lines and family-specific +// routing — pure tool-loop budget. +export const GROK_TOOL_BUDGET_RESIDUAL = + "Tool budget:\n" + + "- Batch independent tool calls into a single turn.\n" + + "- Never re-issue a tool call whose result you already have.\n" + + "- When the next call would only repeat prior work, write the report instead."; + // A directly observed 14-turn pure-tool-call session for this family // previously motivated a tightened nudge/pause pair here (6/10). That pair // was miscalibrated: it fired on a session that was making real progress @@ -83,6 +101,8 @@ const GROK_POLICY: Omit = { applyGrokFinishBias: true, // Leaf value; the resolver clears it for orchestrators below. advertisedToolDeny: ["skill_search"], + // Leaf value; the resolver clears it for orchestrators below. + promptResidual: GROK_TOOL_BUDGET_RESIDUAL, }; // Kimi (Moonshot) detection ships now so callers can branch on family, but @@ -126,6 +146,7 @@ export function resolveModelFamilyPolicy(input: { ...policy, applyGrokFinishBias: policy.applyGrokFinishBias && !orchestrator, advertisedToolDeny: orchestrator ? [] : policy.advertisedToolDeny, + promptResidual: orchestrator ? undefined : policy.promptResidual, }; } case "kimi": diff --git a/src/agent/prompt-sizes.ts b/src/agent/prompt-sizes.ts index a9721a554..cc705a3f2 100644 --- a/src/agent/prompt-sizes.ts +++ b/src/agent/prompt-sizes.ts @@ -17,6 +17,7 @@ import { MAX_AGENTS_MD_BYTES, } from "./context-extensions.js"; import { shouldApplyGrokAntiThrash } from "../subagent/provider-family.js"; +import { resolveModelFamilyPolicy } from "./model-family-policy.js"; import { isCodexProviderName } from "../config/codex-providers.js"; import { shellCollectDefinition } from "./background-shell-tool.js"; import { @@ -35,7 +36,9 @@ import { webSearchDefinition } from "../tools/web-search.js"; * Assembles each director prompt exactly as src/subagent/run.ts does: * extensions=[director systemPromptRole] + environment + tools + * appendix, with the Grok finish-bias note gated by - * shouldApplyGrokAntiThrash (leaves on Grok-family providers only). + * shouldApplyGrokAntiThrash (leaves on Grok-family providers only) and the + * family promptResidual (CL-8297 tool budget, grok leaves only) resolved + * from the model family policy. * * The env and provider inputs are pinned here so sizes never drift with the * machine, date, or checkout — only real prompt changes move the numbers. @@ -156,6 +159,7 @@ export function assembleDirectorPrompt( const pkg = DIRECTOR_REGISTRY[directorId]; const orchestrator = pkg.spawn.maySpawn; const provider = family === "grok" ? GROK_PROVIDER : DEFAULT_PROVIDER; + const policy = resolveModelFamilyPolicy({ ...provider, orchestrator }); return buildSubAgentSystemPrompt( [formatDirectorSystemPrompt(pkg)], CANONICAL_PROMPT_ENV, @@ -164,6 +168,7 @@ export function assembleDirectorPrompt( orchestrator, toolNames: canonicalToolNamesForDirector(pkg, family), grokAntiThrash: shouldApplyGrokAntiThrash({ ...provider, orchestrator }), + promptResidual: policy.promptResidual, }, ); } diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index fab10da33..2022e7cbc 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -530,6 +530,11 @@ export function buildSubAgentSystemPrompt( toolNames?: readonly string[]; /** When true, append the tiny Grok/xAI finish-bias note (provider residual). */ grokAntiThrash?: boolean; + /** + * Family policy residual (CL-8297) appended once at the tail so it + * cannot disturb the cached prompt prefix. Unset for families with none. + */ + promptResidual?: string | undefined; } = {}, ): string { const toolListForPrompt = @@ -556,5 +561,8 @@ export function buildSubAgentSystemPrompt( if (opts.grokAntiThrash === true) { sections.push(buildGrokLeafAntiThrashNote()); } + if (opts.promptResidual !== undefined && opts.promptResidual.length > 0) { + sections.push(opts.promptResidual); + } return joinSections(sections); } diff --git a/src/subagent/run.ts b/src/subagent/run.ts index 5373793f2..48db889ff 100644 --- a/src/subagent/run.ts +++ b/src/subagent/run.ts @@ -1055,6 +1055,7 @@ async function runSubAgentInner( model: params.provider.model, orchestrator: params.orchestrator === true, }), + promptResidual: modelFamilyPolicy.promptResidual, }, ); From 0bac9ea249f7cbdf6423c82b4c7ffac4b81d2b9a Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 15:26:57 -0700 Subject: [PATCH 3/8] style(tests): narrow promptResidual instead of non-null assertion --- src/agent/model-family-policy.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agent/model-family-policy.test.ts b/src/agent/model-family-policy.test.ts index d59e52c10..8a5d8a552 100644 --- a/src/agent/model-family-policy.test.ts +++ b/src/agent/model-family-policy.test.ts @@ -111,7 +111,8 @@ describe("resolveModelFamilyPolicy", () => { }); expect(leaf.family).toBe("grok"); expect(leaf.promptResidual).toBeDefined(); - expect(leaf.promptResidual!.split("\n")).toHaveLength(4); + if (!leaf.promptResidual) throw new Error("expected promptResidual to be defined"); + expect(leaf.promptResidual.split("\n")).toHaveLength(4); expect(leaf.promptResidual).toContain("Tool budget:"); }); From eaf7ca9d9650bc5f877304eb932c630f2e5d849d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 15:29:38 -0700 Subject: [PATCH 4/8] style(tests): oxfmt model-family-policy guard --- src/agent/model-family-policy.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agent/model-family-policy.test.ts b/src/agent/model-family-policy.test.ts index 8a5d8a552..0271bf360 100644 --- a/src/agent/model-family-policy.test.ts +++ b/src/agent/model-family-policy.test.ts @@ -111,7 +111,8 @@ describe("resolveModelFamilyPolicy", () => { }); expect(leaf.family).toBe("grok"); expect(leaf.promptResidual).toBeDefined(); - if (!leaf.promptResidual) throw new Error("expected promptResidual to be defined"); + if (!leaf.promptResidual) + throw new Error("expected promptResidual to be defined"); expect(leaf.promptResidual.split("\n")).toHaveLength(4); expect(leaf.promptResidual).toContain("Tool budget:"); }); From 29cb5578fb600bd51347827f339bf5176e6ae46e Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 15:15:15 -0700 Subject: [PATCH 5/8] test(agent): red test for single merged grok residual with ceremony lines --- src/agent/grok-residual.test.ts | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/agent/grok-residual.test.ts diff --git a/src/agent/grok-residual.test.ts b/src/agent/grok-residual.test.ts new file mode 100644 index 000000000..336dfd2b0 --- /dev/null +++ b/src/agent/grok-residual.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "bun:test"; +import { promptResidual } from "./model-family-policy.js"; +import { + buildGrokLeafAntiThrashNote, + buildSubAgentSystemPrompt, +} from "./prompts.js"; + +// The three Grok ceremony lines (CL-7768 Design, merged by CL-8296): no git, +// no pre-plan, verify once. +const CEREMONY_LINES = [ + "- Never run git add, git commit, git stash, or any other state-changing git command unless the user asks.", + "- Do not narrate a plan before acting on a small task; act, then report.", + "- Verify with the test command once at the end, not after every edit.", +] as const; + +function countOccurrences(haystack: string, needle: string): number { + return haystack.split(needle).length - 1; +} + +describe("grok ceremony merge (CL-8296)", () => { + it("exposes a single grok residual with each ceremony line exactly once", () => { + const residual = promptResidual("grok"); + expect(residual).toContain("Finish bias (xAI / Grok worker):"); + for (const line of CEREMONY_LINES) { + expect(countOccurrences(residual, line)).toBe(1); + } + }); + + it("keeps the don't re-read line exactly once — no duplicate", () => { + const residual = promptResidual("grok"); + expect(countOccurrences(residual, "re-open paths you already read")).toBe( + 1, + ); + }); + + it("is grok-only: every other family resolves to an empty residual", () => { + expect(promptResidual("default")).toBe(""); + expect(promptResidual("kimi")).toBe(""); + expect(promptResidual("muse")).toBe(""); + }); + + it("buildGrokLeafAntiThrashNote is the same single residual (one source of truth)", () => { + expect(buildGrokLeafAntiThrashNote()).toBe(promptResidual("grok")); + }); + + it("the assembled grok worker prompt carries the merged residual exactly once", () => { + const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, { + orchestrator: false, + grokAntiThrash: true, + }); + expect( + countOccurrences(prompt, "Finish bias (xAI / Grok worker):"), + ).toBe(1); + for (const line of CEREMONY_LINES) { + expect(countOccurrences(prompt, line)).toBe(1); + } + }); +}); From d9fd440d88a3dab0a4bf92f2d66b6e229955fc99 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 15:17:01 -0700 Subject: [PATCH 6/8] feat(agent): merge grok ceremony lines into the one residual --- src/agent/model-family-policy.ts | 28 ++++++++++++++++++++++++++++ src/agent/prompts.ts | 13 ++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/agent/model-family-policy.ts b/src/agent/model-family-policy.ts index 916f76488..cdf9411d5 100644 --- a/src/agent/model-family-policy.ts +++ b/src/agent/model-family-policy.ts @@ -129,6 +129,34 @@ const MUSE_POLICY: Omit = { toolDisciplineRules: MUSE_TOOL_DISCIPLINE_RULES, }; +// Single grok prompt residual (CL-8296): the finish-bias bullets plus the +// three ceremony lines from the CL-7768 design (no git, no pre-plan, verify +// once), merged into one block with no line twice. The don't re-read idea +// appears exactly once (the "re-open paths" bullet) — it is not repeated. +// Grok-only: detectModelFamily has no glm family, so per the <30min rule no +// GLM row ships here. The text lives here once; buildGrokLeafAntiThrashNote +// (prompts.ts) returns it verbatim, so the prompt carries exactly one grok +// residual and the sibling CL-8297 hook resolves to the same block. +const GROK_PROMPT_RESIDUAL = [ + "Finish bias (xAI / Grok worker):", + "- Once you can answer the dispatch brief, prefer the structured report over another speculative tool call.", + "- If the next call would only re-open paths you already read, write the report instead.", + "- When the dispatch brief's done-definition is met, write the report envelope instead of making one more search or micro-edit.", + "- Route file and web work through the dedicated tools, never run_shell — mining showed grok reaching for shell first when a typed tool already covered the job.", + "- Never run git add, git commit, git stash, or any other state-changing git command unless the user asks.", + "- Do not narrate a plan before acting on a small task; act, then report.", + "- Verify with the test command once at the end, not after every edit.", +].join("\n"); + +/** + * Minimal per-family prompt residual hook. The sibling CL-8297 owns the + * canonical hook; this local copy keeps the branch self-contained so tests + * pass standalone — the two reconcile when CL-8297 lands. + */ +export function promptResidual(family: ModelFamily): string { + return family === "grok" ? GROK_PROMPT_RESIDUAL : ""; +} + export function resolveModelFamilyPolicy(input: { providerName: string; model?: string; diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index 2022e7cbc..fcd1ec241 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -11,6 +11,7 @@ import { buildWorkerContract, buildWorkerToolNames, } from "./worker-contract.js"; +import { promptResidual } from "./model-family-policy.js"; // Advertise every gated core tool when the caller has no session-start facts // (tests, ad-hoc prompt previews) — except wait_agents, which is mount-gated: @@ -511,14 +512,12 @@ export function buildSubAgentReportContract( // Tiny residual for Grok/xAI workers: mining showed higher tools-only thrash // than Codex on the same harness. Shared thrash harness + spawn contracts do // the structural work; this is only a finish-bias nudge, not a full rewrite. +// Single source of truth is the promptResidual("grok") block in +// model-family-policy.ts (CL-8296 merged the three ceremony lines into it); +// this composes that block verbatim so the prompt carries one grok residual +// with no line twice. export function buildGrokLeafAntiThrashNote(): string { - return [ - "Finish bias (xAI / Grok worker):", - "- Once you can answer the dispatch brief, prefer the structured report over another speculative tool call.", - "- If the next call would only re-open paths you already read, write the report instead.", - "- When the dispatch brief's done-definition is met, write the report envelope instead of making one more search or micro-edit.", - "- Route file and web work through the dedicated tools, never run_shell — mining showed grok reaching for shell first when a typed tool already covered the job.", - ].join("\n"); + return promptResidual("grok"); } export function buildSubAgentSystemPrompt( From 2350730f1785e364342fdce8a7a80be976c5e931 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 15:26:49 -0700 Subject: [PATCH 7/8] style(agent): oxfmt fix-up for grok-residual test --- src/agent/grok-residual.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agent/grok-residual.test.ts b/src/agent/grok-residual.test.ts index 336dfd2b0..afacba997 100644 --- a/src/agent/grok-residual.test.ts +++ b/src/agent/grok-residual.test.ts @@ -48,9 +48,9 @@ describe("grok ceremony merge (CL-8296)", () => { orchestrator: false, grokAntiThrash: true, }); - expect( - countOccurrences(prompt, "Finish bias (xAI / Grok worker):"), - ).toBe(1); + expect(countOccurrences(prompt, "Finish bias (xAI / Grok worker):")).toBe( + 1, + ); for (const line of CEREMONY_LINES) { expect(countOccurrences(prompt, line)).toBe(1); } From d9be39ac1584816953fb944354737e9c7ca61545 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 20 Sep 2026 19:55:18 -0700 Subject: [PATCH 8/8] fix(agent): single-source grok residual, drop duplicate promptResidual seam --- src/agent/grok-residual.test.ts | 46 +++++++++++++++++++++++--------- src/agent/model-family-policy.ts | 29 ++++++++------------ src/agent/prompts.ts | 8 +++--- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/agent/grok-residual.test.ts b/src/agent/grok-residual.test.ts index afacba997..9a50612e6 100644 --- a/src/agent/grok-residual.test.ts +++ b/src/agent/grok-residual.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from "bun:test"; -import { promptResidual } from "./model-family-policy.js"; +import { GROK_PROMPT_RESIDUAL } from "./model-family-policy.js"; import { buildGrokLeafAntiThrashNote, buildSubAgentSystemPrompt, } from "./prompts.js"; +import { shouldApplyGrokAntiThrash } from "../subagent/provider-family.js"; // The three Grok ceremony lines (CL-7768 Design, merged by CL-8296): no git, // no pre-plan, verify once. @@ -19,28 +20,47 @@ function countOccurrences(haystack: string, needle: string): number { describe("grok ceremony merge (CL-8296)", () => { it("exposes a single grok residual with each ceremony line exactly once", () => { - const residual = promptResidual("grok"); - expect(residual).toContain("Finish bias (xAI / Grok worker):"); + expect(GROK_PROMPT_RESIDUAL).toContain("Finish bias (xAI / Grok worker):"); for (const line of CEREMONY_LINES) { - expect(countOccurrences(residual, line)).toBe(1); + expect(countOccurrences(GROK_PROMPT_RESIDUAL, line)).toBe(1); } }); it("keeps the don't re-read line exactly once — no duplicate", () => { - const residual = promptResidual("grok"); - expect(countOccurrences(residual, "re-open paths you already read")).toBe( - 1, - ); + expect( + countOccurrences(GROK_PROMPT_RESIDUAL, "re-open paths you already read"), + ).toBe(1); }); - it("is grok-only: every other family resolves to an empty residual", () => { - expect(promptResidual("default")).toBe(""); - expect(promptResidual("kimi")).toBe(""); - expect(promptResidual("muse")).toBe(""); + it("is grok-only: the finish-bias gate fires for grok leaves alone", () => { + expect( + shouldApplyGrokAntiThrash({ + providerName: "xai/default", + model: "grok-4.6", + orchestrator: false, + }), + ).toBe(true); + for (const input of [ + { providerName: "anthropic", model: "claude-sonnet-4" }, + { providerName: "moonshot", model: "kimi-k2" }, + { providerName: "opencode-go", model: "muse-spark-1.3-contributor" }, + { providerName: "openai", model: "gpt-4.1" }, + ] as const) { + expect(shouldApplyGrokAntiThrash({ ...input, orchestrator: false })).toBe( + false, + ); + } + expect( + shouldApplyGrokAntiThrash({ + providerName: "xai/default", + model: "grok-4.6", + orchestrator: true, + }), + ).toBe(false); }); it("buildGrokLeafAntiThrashNote is the same single residual (one source of truth)", () => { - expect(buildGrokLeafAntiThrashNote()).toBe(promptResidual("grok")); + expect(buildGrokLeafAntiThrashNote()).toBe(GROK_PROMPT_RESIDUAL); }); it("the assembled grok worker prompt carries the merged residual exactly once", () => { diff --git a/src/agent/model-family-policy.ts b/src/agent/model-family-policy.ts index cdf9411d5..a3bd854f0 100644 --- a/src/agent/model-family-policy.ts +++ b/src/agent/model-family-policy.ts @@ -129,15 +129,17 @@ const MUSE_POLICY: Omit = { toolDisciplineRules: MUSE_TOOL_DISCIPLINE_RULES, }; -// Single grok prompt residual (CL-8296): the finish-bias bullets plus the -// three ceremony lines from the CL-7768 design (no git, no pre-plan, verify -// once), merged into one block with no line twice. The don't re-read idea -// appears exactly once (the "re-open paths" bullet) — it is not repeated. -// Grok-only: detectModelFamily has no glm family, so per the <30min rule no -// GLM row ships here. The text lives here once; buildGrokLeafAntiThrashNote -// (prompts.ts) returns it verbatim, so the prompt carries exactly one grok -// residual and the sibling CL-8297 hook resolves to the same block. -const GROK_PROMPT_RESIDUAL = [ +// Single grok finish-bias + ceremony residual (CL-8296): the finish-bias +// bullets plus the three ceremony lines from the CL-7768 design (no git, no +// pre-plan, verify once), merged into one block with no line twice. The don't +// re-read idea appears exactly once (the "re-open paths" bullet) — it is not +// repeated. Grok-only: detectModelFamily has no glm family, so per the <30min +// rule no GLM row ships here. The text lives here once; exported for +// buildGrokLeafAntiThrashNote (prompts.ts), which returns it verbatim, so the +// prompt carries exactly one copy. This is a different block from the CL-8297 +// tool-budget hook (GROK_TOOL_BUDGET_RESIDUAL, surfaced via the +// ModelFamilyPolicy.promptResidual field): grok leaves carry both, each once. +export const GROK_PROMPT_RESIDUAL = [ "Finish bias (xAI / Grok worker):", "- Once you can answer the dispatch brief, prefer the structured report over another speculative tool call.", "- If the next call would only re-open paths you already read, write the report instead.", @@ -148,15 +150,6 @@ const GROK_PROMPT_RESIDUAL = [ "- Verify with the test command once at the end, not after every edit.", ].join("\n"); -/** - * Minimal per-family prompt residual hook. The sibling CL-8297 owns the - * canonical hook; this local copy keeps the branch self-contained so tests - * pass standalone — the two reconcile when CL-8297 lands. - */ -export function promptResidual(family: ModelFamily): string { - return family === "grok" ? GROK_PROMPT_RESIDUAL : ""; -} - export function resolveModelFamilyPolicy(input: { providerName: string; model?: string; diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index fcd1ec241..8f7402261 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -11,7 +11,7 @@ import { buildWorkerContract, buildWorkerToolNames, } from "./worker-contract.js"; -import { promptResidual } from "./model-family-policy.js"; +import { GROK_PROMPT_RESIDUAL } from "./model-family-policy.js"; // Advertise every gated core tool when the caller has no session-start facts // (tests, ad-hoc prompt previews) — except wait_agents, which is mount-gated: @@ -512,12 +512,12 @@ export function buildSubAgentReportContract( // Tiny residual for Grok/xAI workers: mining showed higher tools-only thrash // than Codex on the same harness. Shared thrash harness + spawn contracts do // the structural work; this is only a finish-bias nudge, not a full rewrite. -// Single source of truth is the promptResidual("grok") block in +// Single source of truth is the GROK_PROMPT_RESIDUAL block in // model-family-policy.ts (CL-8296 merged the three ceremony lines into it); -// this composes that block verbatim so the prompt carries one grok residual +// this returns that block verbatim so the prompt carries one grok residual // with no line twice. export function buildGrokLeafAntiThrashNote(): string { - return promptResidual("grok"); + return GROK_PROMPT_RESIDUAL; } export function buildSubAgentSystemPrompt(