From 80560d48e6d9f443e075ec8637a089587ab0fdf9 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 18:32:12 -0700 Subject: [PATCH 1/3] Ship keepstyle guideline split and dedupe worker report envelopes --- .../directors/bruckheimer/package.test.ts | 11 +- src/agent/directors/bruckheimer/package.ts | 14 +- src/agent/directors/intern/package.test.ts | 8 +- src/agent/directors/intern/package.ts | 14 +- src/agent/directors/neckbeard/package.test.ts | 11 +- src/agent/directors/neckbeard/package.ts | 14 +- src/agent/directors/testsmith/package.test.ts | 12 +- src/agent/directors/testsmith/package.ts | 14 +- src/agent/prompts.test.ts | 88 ++++++++++ src/agent/prompts.ts | 164 +++++++++++++----- src/config/index.ts | 6 + src/config/profiles.ts | 6 + src/exec/runner.ts | 3 + src/session/runtime-assembly.ts | 6 + src/tui/runner/session.ts | 3 + 15 files changed, 256 insertions(+), 118 deletions(-) diff --git a/src/agent/directors/bruckheimer/package.test.ts b/src/agent/directors/bruckheimer/package.test.ts index b3f896381..805dec1be 100644 --- a/src/agent/directors/bruckheimer/package.test.ts +++ b/src/agent/directors/bruckheimer/package.test.ts @@ -80,12 +80,13 @@ describe("bruckheimerPackage", () => { expect(p).not.toMatch(/maySpawn:\s*true/); }); - test("systemPrompt teaches worker report shape Summary/Findings/Blockers/Paths", () => { + test("systemPrompt points at the scaffold-owned worker report envelope (no re-spec)", () => { const p = bruckheimerPackage.systemPrompt; - expect(p).toContain("## Summary"); - expect(p).toContain("## Findings"); - expect(p).toContain("## Blockers"); - expect(p).toContain("## Paths"); + expect(p).toMatch(/Corbits report envelope/); + expect(p).not.toContain("## Summary"); + expect(p).not.toContain("## Findings"); + expect(p).not.toContain("## Blockers"); + expect(p).not.toContain("## Paths"); expect(p).toMatch(/brief file you wrote/i); expect(p).toContain("DONE GATE"); }); diff --git a/src/agent/directors/bruckheimer/package.ts b/src/agent/directors/bruckheimer/package.ts index 688956342..9312b84f5 100644 --- a/src/agent/directors/bruckheimer/package.ts +++ b/src/agent/directors/bruckheimer/package.ts @@ -129,19 +129,7 @@ Use \`read_file\`, \`write_file\`, and \`edit_file\` to manage the brief. Use \` # Report (when dispatched as a worker) -When you finish a discovery brief for a parent session, stop tooling and reply with ONLY: - -## Summary -One or two sentences: what you accomplished or concluded. - -## Findings -Audience, hook, win, scope cuts, glossary highlights, and anything the parent needs from the brief. - -## Blockers -Open questions, assumptions, or blockers. Write "None." if clear. Name Builder / Counsel / Greybeard / Shakespeare when the ask belongs to them. - -## Paths -The brief file you wrote (one path). Write "None." if you refused a brief because the bar was not met. +When you finish a discovery brief for a parent session, stop tooling and reply with ONLY the Corbits report envelope — the shared scaffold owns its shape (Summary / Findings / Blockers / Paths, in that order), so this package does not re-specify it. Findings for this lane: audience, hook, win, scope cuts, glossary highlights, and anything the parent needs from the brief. Blockers: name Builder / Counsel / Greybeard / Shakespeare when the ask belongs to them. Paths: the brief file you wrote (one path); "None." if you refused a brief because the bar was not met. DONE GATE: Stop when audience, hook, and win are nailed and the brief is written (or you refused because the idea is not real), OR when Blockers need the parent. Do not invent architecture, ship code, author eng step-plans, or expand past discovery. diff --git a/src/agent/directors/intern/package.test.ts b/src/agent/directors/intern/package.test.ts index 4f45cf82a..e56011322 100644 --- a/src/agent/directors/intern/package.test.ts +++ b/src/agent/directors/intern/package.test.ts @@ -18,9 +18,11 @@ describe("internPackage", () => { expect(p).toMatch(/execute clear (mechanical )?instructions/i); expect(p).toMatch(/STOP/i); expect(p).toMatch(/Blockers/i); - expect(p).toMatch(/## Summary/); - expect(p).toMatch(/## Findings/); - expect(p).toMatch(/## Paths/); + // Envelope shape is scaffold-owned: point at it, do not re-specify it. + expect(p).toMatch(/Corbits report envelope/); + expect(p).not.toMatch(/## Summary/); + expect(p).not.toMatch(/## Findings/); + expect(p).not.toMatch(/## Paths/); expect(p).toMatch(/run_shell/); // Role forbids debugging; body states the ban explicitly expect(p).toMatch(/You do NOT:[\s\S]*Debug failures/); diff --git a/src/agent/directors/intern/package.ts b/src/agent/directors/intern/package.ts index e7fae5f49..0cff142f0 100644 --- a/src/agent/directors/intern/package.ts +++ b/src/agent/directors/intern/package.ts @@ -119,17 +119,5 @@ STOP. You are outside your role. Report Blockers for the parent (Skywalker) inst # Report Contract -When done (or blocked), stop calling tools and reply with ONLY this markdown envelope: - -## Summary -One or two sentences: what you ran or why you stopped. - -## Findings -Commands run and their outputs (verbatim where useful). Observable facts only. - -## Blockers -Ambiguity, failures, missing inputs, or decisions needed. Write "None." if clear. Do not invent fixes. - -## Paths -Key file paths you read or changed (one per line). Write "None." if none.`, +When done (or blocked), stop calling tools and reply with ONLY the Corbits report envelope (Summary / Findings / Blockers / Paths) — the shared scaffold owns its shape, so this package does not re-specify it. See How to Report Back for what goes under Findings / Blockers.`, }; diff --git a/src/agent/directors/neckbeard/package.test.ts b/src/agent/directors/neckbeard/package.test.ts index 74102af42..21caa3e91 100644 --- a/src/agent/directors/neckbeard/package.test.ts +++ b/src/agent/directors/neckbeard/package.test.ts @@ -48,15 +48,16 @@ describe("neckbeardPackage", () => { expect(p).toMatch(/code \(when the brief asks\)|code review/i); }); - test("systemPrompt bakes style/philosophy and reports to parent", () => { + test("systemPrompt bakes style/philosophy and points at the shared envelope", () => { const p = neckbeardPackage.systemPrompt; expect(p).toMatch(/use_skill.*not mounted|not mounted.*use_skill/i); expect(p).toMatch(/violently disagree/); expect(p).toMatch(/report to the parent/i); - expect(p).toMatch(/## Summary/); - expect(p).toMatch(/## Findings/); - expect(p).toMatch(/## Blockers/); - expect(p).toMatch(/## Paths/); + expect(p).toMatch(/Corbits report envelope/); + expect(p).not.toMatch(/## Summary/); + expect(p).not.toMatch(/## Findings/); + expect(p).not.toMatch(/## Blockers/); + expect(p).not.toMatch(/## Paths/); expect(p).toMatch(/ranked nits with evidence|evidence paths/i); }); diff --git a/src/agent/directors/neckbeard/package.ts b/src/agent/directors/neckbeard/package.ts index 9c4090631..055a613fb 100644 --- a/src/agent/directors/neckbeard/package.ts +++ b/src/agent/directors/neckbeard/package.ts @@ -569,17 +569,5 @@ Do not apply fixes. Do not become Builder, Critic, or Greybeard as your primary # Reporting back -When done, stop calling tools and reply with ONLY this markdown envelope: - -## Summary -One or two sentences: condescending overall take (Rust optional but encouraged). - -## Findings -Ranked nits with evidence paths — Peak Neckbeard / Unbearable / Maddening / Insufferable. Each item cites a path (and line/symbol when available). Comic voice allowed ("Actually,", "Well technically,"); no emoji glyphs. - -## Blockers -Open questions, missing docs/code, or out-of-lane routing. Write "None." if clear. If blocked, ask_director; after the cap, report remaining questions here. - -## Paths -Key file paths you read (one per line). Write "None." if none.`, +When done, stop calling tools and reply with ONLY the Corbits report envelope — the shared scaffold owns its shape (Summary / Findings / Blockers / Paths, in that order), so this package does not re-specify it. Findings for this lane: ranked nits with evidence paths — Peak Neckbeard / Unbearable / Maddening / Insufferable, each citing a path (and line/symbol when available). Comic voice allowed ("Actually,", "Well technically,"); no emoji glyphs. Blockers: ... ask_director; after the cap, report remaining questions here. Paths: key file paths you read (one per line).`, }; diff --git a/src/agent/directors/testsmith/package.test.ts b/src/agent/directors/testsmith/package.test.ts index e47abccd8..2f3c63272 100644 --- a/src/agent/directors/testsmith/package.test.ts +++ b/src/agent/directors/testsmith/package.test.ts @@ -48,13 +48,15 @@ describe("testsmithPackage", () => { expect(p).toMatch(/Hand off/i); }); - test("systemPrompt states Corbits report shape", () => { + test("systemPrompt points at the scaffold-owned report envelope (no re-spec)", () => { const p = testsmithPackage.systemPrompt; - expect(p).toContain("## Summary"); - expect(p).toContain("## Findings"); - expect(p).toContain("## Blockers"); - expect(p).toContain("## Paths"); expect(p).toMatch(/Corbits report shape/i); + expect(p).toMatch(/Corbits report envelope/); + expect(p).toMatch(/coverage map/); + expect(p).not.toContain("## Summary"); + expect(p).not.toContain("## Findings"); + expect(p).not.toContain("## Blockers"); + expect(p).not.toContain("## Paths"); }); test("systemPrompt has no tool-schema restatement or fake caps", () => { diff --git a/src/agent/directors/testsmith/package.ts b/src/agent/directors/testsmith/package.ts index 84061755c..6c6d2d285 100644 --- a/src/agent/directors/testsmith/package.ts +++ b/src/agent/directors/testsmith/package.ts @@ -61,19 +61,7 @@ Defer or omit: # Corbits report shape -When done, stop tooling and reply with ONLY this envelope: - -## Summary -One or two sentences: strategy and coverage scope designed. - -## Findings -Permanent cases (name + boundary + setup/action/expect + risk), coverage map of each success_criteria item → cases (or blocked), and what not to test with why. - -## Blockers -Open questions, missing acceptance criteria, or assumptions. Write "None." if clear. - -## Paths -Files/suites you read to ground the design (one per line). Write "None." if none. +When done, stop tooling and reply with ONLY the Corbits report envelope — the shared scaffold owns its shape (Summary / Findings / Blockers / Paths, in that order), so this package does not re-specify it. Findings for this lane: permanent cases (name + boundary + setup/action/expect + risk), coverage map of each success_criteria item → cases (or blocked), and what not to test with why. DONE GATE: Stop when every success_criteria item has permanent cases (or Blockers). Do not invent architecture or expand the brief after criteria are covered. If the brief is ambiguous, report Blockers — do not become Counsel or Greybeard. diff --git a/src/agent/prompts.test.ts b/src/agent/prompts.test.ts index f6eecd431..99cea2f38 100644 --- a/src/agent/prompts.test.ts +++ b/src/agent/prompts.test.ts @@ -2,8 +2,11 @@ import { describe, expect, it } from "bun:test"; import { buildChatSystemPrompt, buildGrokLeafAntiThrashNote, + buildGuidelines, buildPromptDisciplineBlock, buildSubAgentSystemPrompt, + GUIDELINE_SUB_BLOCK_IDS, + KEEPSTYLE_PROMPT_SECTION_OMIT, } from "./prompts.js"; import { CORE_TOOL_NAMES, CATALOG_TOOL_NAMES } from "./tool-search.js"; @@ -138,6 +141,91 @@ describe("sub-agent report contract", () => { expect(prompt).toMatch(/emit all four headings/); expect(prompt).toContain('"None."'); }); + + it("emits the four-heading envelope exactly once (scaffold owns the shape)", () => { + const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, { + orchestrator: false, + grokAntiThrash: false, + }); + for (const heading of [ + "## Summary", + "## Findings", + "## Blockers", + "## Paths", + ]) { + expect(countOccurrences(prompt, heading)).toBe(1); + } + }); +}); + +describe("guideline sub-block omit policy (CL-7654)", () => { + it("exposes the keepstyle set as ids", () => { + expect([...GUIDELINE_SUB_BLOCK_IDS]).toEqual([ + "responseStyle", + "toolChoice", + "askVsProceed", + "scopeConventions", + "orchestration", + ]); + }); + + it("keeps the full guidelines by default", () => { + const guidelines = buildGuidelines({}); + for (const marker of [ + "Response style:", + "Tool choice:", + "Ask vs proceed:", + "Scope and conventions:", + "Orchestration:", + ]) { + expect(guidelines).toContain(marker); + } + }); + + it("keepstyle omit keeps response style, drops tool-choice / ask-vs-proceed / orchestration", () => { + expect([...KEEPSTYLE_PROMPT_SECTION_OMIT].sort()).toEqual([ + "askVsProceed", + "orchestration", + "toolChoice", + ]); + const guidelines = buildGuidelines({ omit: KEEPSTYLE_PROMPT_SECTION_OMIT }); + expect(guidelines).toContain("Response style:"); + expect(guidelines).toContain("Scope and conventions:"); + expect(guidelines).not.toContain("Tool choice:"); + expect(guidelines).not.toContain("Ask vs proceed:"); + expect(guidelines).not.toContain("Orchestration:"); + }); + + it("ignores unknown omit ids", () => { + const guidelines = buildGuidelines({ omit: ["no-such-block"] }); + expect(guidelines).toContain("Response style:"); + expect(guidelines).toContain("Tool choice:"); + expect(guidelines).toContain("Orchestration:"); + }); + + it("threads guidelineConfig through the chat system prompt", () => { + const full = buildChatSystemPrompt( + undefined, + undefined, + undefined, + [], + "orchestrator", + ); + expect(full).toContain("Tool choice:"); + expect(full).toContain("Orchestration:"); + const keepstyle = buildChatSystemPrompt( + undefined, + undefined, + undefined, + [], + "orchestrator", + undefined, + { omit: KEEPSTYLE_PROMPT_SECTION_OMIT }, + ); + expect(keepstyle).toContain("Response style:"); + expect(keepstyle).not.toContain("Tool choice:"); + expect(keepstyle).not.toContain("Orchestration:"); + }); }); describe("wait_agents mount-gated prompt copy (CL-7678)", () => { diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index 50582b56b..07b562a27 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -108,54 +108,79 @@ export function buildHarnessFacts( ].join("\n"); } -export function buildGuidelines( - opts: { - subAgent?: boolean; - sessionMode?: SessionMode; - askDirector?: boolean; - // True where createAgentToolset mounted wait_agents (exec primary). - // Picks the collection-path copy: wait_agents vs mailbox mail. - waitAgentsMounted?: boolean; - } = {}, -): string { - const subAgent = opts.subAgent ?? false; - const askDirector = opts.askDirector === true; - const waitAgentsMounted = opts.waitAgentsMounted === true; - return [ - "Guidelines:", - "", +// Guideline sub-block ids — the policy surface for `promptSectionOmit`. +// Omit drops whole named blocks; prose inside a kept block is untouched. +export const GUIDELINE_SUB_BLOCK_IDS = [ + "responseStyle", + "toolChoice", + "askVsProceed", + "scopeConventions", + "orchestration", +] as const; + +export type GuidelineSubBlockId = (typeof GUIDELINE_SUB_BLOCK_IDS)[number]; + +/** Id-based guideline policy: which sub-blocks to drop. Unknown ids are ignored. */ +export interface GuidelineConfig { + readonly omit?: readonly string[]; +} + +/** + * Omit-set for the keepstyle guideline footprint: terse response style stays, + * tool-choice / ask-vs-proceed / orchestration guidance drops. Scope and + * conventions (build gate, verification evidence) always stay. + */ +export const KEEPSTYLE_PROMPT_SECTION_OMIT: readonly string[] = [ + "toolChoice", + "askVsProceed", + "orchestration", +]; + +interface GuidelineBlockContext { + readonly subAgent: boolean; + readonly askDirector: boolean; + readonly waitAgentsMounted: boolean; +} + +const GUIDELINE_SUB_BLOCKS: Record< + GuidelineSubBlockId, + (ctx: GuidelineBlockContext) => string[] +> = { + responseStyle: () => [ "Response style:", "- Default to short, direct answers; skip preamble and filler.", "- For substantial work, lead with the outcome, then what changed and why; use bullets or short headers only when they help scanning.", "- Cite paths instead of pasting large files; fenced snippets only when essential.", "- No emojis in code or docs unless the user uses them.", - "", + ], + toolChoice: (ctx) => [ "Tool choice:", - ...(subAgent + ...(ctx.subAgent ? [] : [ "- Prefer spawn_agent(agent=…) then idle for substantial product implementation, exploration, review, and docs — " + - (waitAgentsMounted + (ctx.waitAgentsMounted ? "collect with wait_agents; do not poll list_agents." : "mailbox mail arrives as inbound; do not poll.") + " Spawn remains default for substantial work, not a tool ban.", ]), "- read_file for file contents; grep or search_files to locate code; lsp for symbols, types, references, or call flow before opening large files.", - subAgent + ctx.subAgent ? "- edit_file for targeted changes; write_file for new files or full rewrites; delete_file to remove files — never echo, heredoc, sed, or rm in the shell for those jobs." : "- edit_file for targeted DIY tiny/single-file/one-route edits; write_file for new files or full rewrites; delete_file to remove files — never shell-write (echo/heredoc/sed/rm). Spawn builder (or a docs director) for substantial/multi-file/parallel/specialist work.", "- run_shell for builds, tests, git, and one-off commands — not for shell find, head-position rg, or recursive grep -r (OOM risk), cat, or messaging the user.", - ...(subAgent + ...(ctx.subAgent ? [] : [ "- tool_search before assuming a plugin or MCP tool exists; skill_search when choosing among listed skills, use_skill to load a body.", ]), - "", - subAgent ? "Proceed vs pause:" : "Ask vs proceed:", - ...(subAgent + ], + askVsProceed: (ctx) => [ + ctx.subAgent ? "Proceed vs pause:" : "Ask vs proceed:", + ...(ctx.subAgent ? [ "- Stick to the dispatch brief; proceed autonomously on bounded work.", - askDirector + ctx.askDirector ? "- If permission denies an action, make a best-effort call and record assumptions under Blockers. If the brief is genuinely ambiguous, ask_director — you cannot reach the operator." : "- If permission denies an action or the brief is unclear, make a best-effort call and record assumptions under Blockers — you cannot ask the parent mid-run.", "- Preserve unrelated user edits; never revert changes you did not make unless the brief requires it.", @@ -167,10 +192,11 @@ export function buildGuidelines( "- Preserve unrelated user edits; never revert changes you did not make unless asked.", "- Unexpected changes in files you did not touch: stop and ask_operator.", ]), - "", + ], + scopeConventions: (ctx) => [ "Scope and conventions:", "- Touch only code required for the task; no drive-by refactors, formatting sweeps, or unrelated fixes.", - subAgent + ctx.subAgent ? "- Follow AGENTS.md and /docs for architecture." : "- Follow AGENTS.md and /docs for architecture; use_skill style and philosophy when starting repo work.", "- Match existing project patterns (functional style, arktype at boundaries, small focused diffs).", @@ -178,26 +204,55 @@ export function buildGuidelines( "- If the repository defines no typecheck command, do not invent a typecheck command: report its absence as an explicit Blocker with evidence from AGENTS.md and package scripts (or equivalent project configuration).", "- In Findings, report every exact verification command and its outcome, including exit status. A bare `pass` without command evidence is an incomplete report.", "- If a required check genuinely cannot run because of a missing runtime or dependency, sandbox restriction, or permissions, record the exact inability under Blockers; never silently skip a required check.", - ...(subAgent - ? [] - : [ - "", - "Orchestration:", - "- Break multi-step or parallel work into focused worker dispatches with distinct lenses; prefer `spawn_agent` (fire several in one turn when jobs are independent), then reply with who is running and end the turn — workers keep running while you are idle. " + - (waitAgentsMounted - ? "This surface has no mailbox delivery: collect with `wait_agents`; do not poll `list_agents`." - : "Mailbox mail arrives as inbound when a worker finishes; read it and do not poll.") + - " `list_agents` shows the fleet without blocking; after a parked ask is surfaced, answer with `send_input` and do not poll `list_agents`.", - "- Pass the typed spawn contract: `intent`, `success_criteria` (done-when; required for implement/review and their default directors), `do_not` (scope fence), and `report_focus`. Free-form `prompt` without `success_criteria` fail-closes for implement/review and their default directors.", - "- After workers return, classify fail / incomplete-report vs parent-initiated interrupt vs operator-cancel vs clean complete. Fail-path (`status: failed` or salvage `incomplete-report`): diagnose from the report or error and MAY spawn one successor with a changed brief. Parent-initiated interrupt (`interrupt_agent` / `send_input` with `interrupt:true` unblocks wait with `stop_reason: interrupted`): the worker is often still running and often has no report — `resume_agent`" + - (waitAgentsMounted - ? " or re-wait" - : ", or idle for its mailbox mail") + - "; do not `spawn_agent` a successor against a still-live worker. Successor only if that session is no longer resumable. Operator-cancel (`stop_reason` cancelled): wait for the operator; do not auto-retry. Identical brief: refuse. Merge Summary/Findings into a coherent answer for the operator; do not paste raw fleet-agent dumps.", - "- Use manage_tasks for your own coordination checklist; spawning workers is `spawn_agent`, not manage_tasks.", - "- If context is compacted automatically, do not stop tasks early due to token fear; persist progress via manage_tasks and worker reports.", - ]), - ].join("\n"); + ], + orchestration: (ctx) => { + if (ctx.subAgent) return []; + return [ + "Orchestration:", + "- Break multi-step or parallel work into focused worker dispatches with distinct lenses; prefer `spawn_agent` (fire several in one turn when jobs are independent), then reply with who is running and end the turn — workers keep running while you are idle. " + + (ctx.waitAgentsMounted + ? "This surface has no mailbox delivery: collect with `wait_agents`; do not poll `list_agents`." + : "Mailbox mail arrives as inbound when a worker finishes; read it and do not poll.") + + " `list_agents` shows the fleet without blocking; after a parked ask is surfaced, answer with `send_input` and do not poll `list_agents`.", + "- Pass the typed spawn contract: `intent`, `success_criteria` (done-when; required for implement/review and their default directors), `do_not` (scope fence), and `report_focus`. Free-form `prompt` without `success_criteria` fail-closes for implement/review and their default directors.", + "- After workers return, classify fail / incomplete-report vs parent-initiated interrupt vs operator-cancel vs clean complete. Fail-path (`status: failed` or salvage `incomplete-report`): diagnose from the report or error and MAY spawn one successor with a changed brief. Parent-initiated interrupt (`interrupt_agent` / `send_input` with `interrupt:true` unblocks wait with `stop_reason: interrupted`): the worker is often still running and often has no report — `resume_agent`" + + (ctx.waitAgentsMounted + ? " or re-wait" + : ", or idle for its mailbox mail") + + "; do not `spawn_agent` a successor against a still-live worker. Successor only if that session is no longer resumable. Operator-cancel (`stop_reason` cancelled): wait for the operator; do not auto-retry. Identical brief: refuse. Merge Summary/Findings into a coherent answer for the operator; do not paste raw fleet-agent dumps.", + "- Use manage_tasks for your own coordination checklist; spawning workers is `spawn_agent`, not manage_tasks.", + "- If context is compacted automatically, do not stop tasks early due to token fear; persist progress via manage_tasks and worker reports.", + ]; + }, +}; + +export function buildGuidelines( + opts: { + subAgent?: boolean; + sessionMode?: SessionMode; + askDirector?: boolean; + // True where createAgentToolset mounted wait_agents (exec primary). + // Picks the collection-path copy: wait_agents vs mailbox mail. + waitAgentsMounted?: boolean; + // Id-based policy: drop the named sub-blocks (see GUIDELINE_SUB_BLOCKS). + // Unknown ids are ignored; empty (default) keeps the full guidelines. + omit?: readonly string[]; + } = {}, +): string { + const ctx: GuidelineBlockContext = { + subAgent: opts.subAgent ?? false, + askDirector: opts.askDirector === true, + waitAgentsMounted: opts.waitAgentsMounted === true, + }; + const omitted = new Set(opts.omit ?? []); + const blocks = GUIDELINE_SUB_BLOCK_IDS.filter((id) => !omitted.has(id)) + .map((id) => GUIDELINE_SUB_BLOCKS[id](ctx)) + // The orchestration block is empty for workers; dropping it (rather than + // its separator) keeps default output byte-identical to before the split. + .filter((lines) => lines.length > 0); + return ["Guidelines:", ...blocks.flatMap((lines) => ["", ...lines])].join( + "\n", + ); } // Shared across every provider family and both chat/sub-agent entry points — @@ -357,6 +412,7 @@ function baseSection( baseOverride: string | undefined, sessionMode: SessionMode, waitAgentsMounted?: boolean, + guidelineConfig?: GuidelineConfig, ): string { if (baseOverride !== undefined && baseOverride.trim().length > 0) { const custom = baseOverride.trim(); @@ -368,6 +424,9 @@ function baseSection( buildGuidelines({ sessionMode: "orchestrator", ...(waitAgentsMounted !== undefined ? { waitAgentsMounted } : {}), + ...(guidelineConfig?.omit !== undefined + ? { omit: guidelineConfig.omit } + : {}), }), buildPromptDisciplineBlock(), ]); @@ -378,6 +437,9 @@ function baseSection( buildGuidelines({ sessionMode, ...(waitAgentsMounted !== undefined ? { waitAgentsMounted } : {}), + ...(guidelineConfig?.omit !== undefined + ? { omit: guidelineConfig.omit } + : {}), }), buildPromptDisciplineBlock(), ]); @@ -399,9 +461,15 @@ export function buildChatSystemPrompt( skills: readonly SkillSummary[] = [], sessionMode: SessionMode = "orchestrator", toolAvailability: ToolAvailability = DEFAULT_TOOL_AVAILABILITY, + guidelineConfig?: GuidelineConfig, ): string { const sections = [ - baseSection(baseOverride, sessionMode, toolAvailability.waitAgentsMounted), + baseSection( + baseOverride, + sessionMode, + toolAvailability.waitAgentsMounted, + guidelineConfig, + ), buildAvailableTools( coreToolNamesForSessionMode(sessionMode, toolAvailability), { advertiseArchive: true }, diff --git a/src/config/index.ts b/src/config/index.ts index 5abb8ebd4..1d9d60216 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -562,6 +562,9 @@ export interface Config { providers: ProviderCatalogEntry[]; profile?: string; systemPromptExtensions?: string[]; + // Guideline sub-block ids to drop from the chat system prompt (see + // GUIDELINE_SUB_BLOCK_IDS in agent/prompts.ts). Omitted = full guidelines. + promptSectionOmit?: string[]; // Per-call inactivity timeout in ms (default 120_000 in the harness). Tune // higher for reasoning models with long silent-thinking stretches. inactivityTimeoutMs?: number; @@ -1088,6 +1091,9 @@ export async function loadConfig( ...(profile.systemPromptExtensions !== undefined ? { systemPromptExtensions: profile.systemPromptExtensions } : {}), + ...(profile.promptSectionOmit !== undefined + ? { promptSectionOmit: profile.promptSectionOmit } + : {}), ...(profile.inactivityTimeoutMs !== undefined ? { inactivityTimeoutMs: profile.inactivityTimeoutMs } : {}), diff --git a/src/config/profiles.ts b/src/config/profiles.ts index 5cb25c29a..b11806860 100644 --- a/src/config/profiles.ts +++ b/src/config/profiles.ts @@ -8,6 +8,9 @@ const ProfileSchema = type({ "profile?": "string", "model?": "string", "systemPromptExtensions?": "string[]", + // Guideline sub-block ids to drop from the chat system prompt (see + // GUIDELINE_SUB_BLOCK_IDS in agent/prompts.ts). Omitted = full guidelines. + "promptSectionOmit?": "string[]", // Per-call inactivity timeout in milliseconds. If the provider yields no // inference event for this many ms, the call is aborted and the user sees // "Request timed out". Default in the inference harness is 120_000 (2 min). @@ -99,6 +102,9 @@ export async function resolveProfile( if (projectProfile.systemPromptExtensions !== undefined) { merged.systemPromptExtensions = projectProfile.systemPromptExtensions; } + if (projectProfile.promptSectionOmit !== undefined) { + merged.promptSectionOmit = projectProfile.promptSectionOmit; + } if (projectProfile.inactivityTimeoutMs !== undefined) merged.inactivityTimeoutMs = projectProfile.inactivityTimeoutMs; if (projectProfile.totalTimeoutMs !== undefined) diff --git a/src/exec/runner.ts b/src/exec/runner.ts index 95febb052..13993cf94 100644 --- a/src/exec/runner.ts +++ b/src/exec/runner.ts @@ -703,6 +703,9 @@ export async function runExec(config: Config): Promise { ...(config.systemPromptExtensions !== undefined ? { systemPromptExtensions: config.systemPromptExtensions } : {}), + ...(config.promptSectionOmit !== undefined + ? { promptSectionOmit: config.promptSectionOmit } + : {}), sessionMode, toolAvailability, skills: agentToolset.skills, diff --git a/src/session/runtime-assembly.ts b/src/session/runtime-assembly.ts index c1f1ad078..6099b36e8 100644 --- a/src/session/runtime-assembly.ts +++ b/src/session/runtime-assembly.ts @@ -288,6 +288,9 @@ export interface SessionChatPromptArgs { systemPromptExtensions?: readonly string[]; sessionMode: SessionMode; toolAvailability: ToolAvailability; + // Guideline sub-block ids to drop (see GUIDELINE_SUB_BLOCK_IDS). + // Omitted = full guidelines. + promptSectionOmit?: readonly string[]; // Session-start snapshot from createAgentToolset. When provided, skip // rediscovery so the prompt listing and skill_search share one catalog. skills?: readonly SkillSummary[]; @@ -323,6 +326,9 @@ export async function loadSessionChatPrompt( skills, args.sessionMode, args.toolAvailability, + args.promptSectionOmit !== undefined + ? { omit: args.promptSectionOmit } + : undefined, ), skills, }; diff --git a/src/tui/runner/session.ts b/src/tui/runner/session.ts index 05cd66d45..abca12066 100644 --- a/src/tui/runner/session.ts +++ b/src/tui/runner/session.ts @@ -431,6 +431,9 @@ export async function assembleTUISession( ...(config.systemPromptExtensions !== undefined ? { systemPromptExtensions: config.systemPromptExtensions } : {}), + ...(config.promptSectionOmit !== undefined + ? { promptSectionOmit: config.promptSectionOmit } + : {}), sessionMode: liveSessionMode, toolAvailability, skills: toolset.skills, From ecc28a8c20ddb50ea6c14b08c039444444d666a4 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 18:41:34 -0700 Subject: [PATCH 2/3] Snapshot default guidelines output as a golden test --- src/agent/prompts.test.ts | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/agent/prompts.test.ts b/src/agent/prompts.test.ts index 99cea2f38..54c93842a 100644 --- a/src/agent/prompts.test.ts +++ b/src/agent/prompts.test.ts @@ -182,6 +182,46 @@ describe("guideline sub-block omit policy (CL-7654)", () => { } }); + it("goldens the default guidelines byte-for-byte (separator shifts fail loudly)", () => { + expect(buildGuidelines({})).toBe(`Guidelines: + +Response style: +- Default to short, direct answers; skip preamble and filler. +- For substantial work, lead with the outcome, then what changed and why; use bullets or short headers only when they help scanning. +- Cite paths instead of pasting large files; fenced snippets only when essential. +- No emojis in code or docs unless the user uses them. + +Tool choice: +- Prefer spawn_agent(agent=…) then idle for substantial product implementation, exploration, review, and docs — mailbox mail arrives as inbound; do not poll. Spawn remains default for substantial work, not a tool ban. +- read_file for file contents; grep or search_files to locate code; lsp for symbols, types, references, or call flow before opening large files. +- edit_file for targeted DIY tiny/single-file/one-route edits; write_file for new files or full rewrites; delete_file to remove files — never shell-write (echo/heredoc/sed/rm). Spawn builder (or a docs director) for substantial/multi-file/parallel/specialist work. +- run_shell for builds, tests, git, and one-off commands — not for shell find, head-position rg, or recursive grep -r (OOM risk), cat, or messaging the user. +- tool_search before assuming a plugin or MCP tool exists; skill_search when choosing among listed skills, use_skill to load a body. + +Ask vs proceed: +- Clear, bounded coding requests: proceed autonomously; use ask_operator only when permission blocks you or the request is genuinely ambiguous (missing repro, conflicting instructions, destructive choice). +- Before ask_operator: put long rationale in a normal transcript reply first, then call ask_operator with a short question and short option labels only. +- Questions, reviews, and product/visual feedback: answer or diagnose first; do not edit until the user wants a change. +- Preserve unrelated user edits; never revert changes you did not make unless asked. +- Unexpected changes in files you did not touch: stop and ask_operator. + +Scope and conventions: +- Touch only code required for the task; no drive-by refactors, formatting sweeps, or unrelated fixes. +- Follow AGENTS.md and /docs for architecture; use_skill style and philosophy when starting repo work. +- Match existing project patterns (functional style, arktype at boundaries, small focused diffs). +- Before finishing implementation work, run the repository-defined typecheck command, relevant tests, and every defined full verification command; these checks are mandatory. +- If the repository defines no typecheck command, do not invent a typecheck command: report its absence as an explicit Blocker with evidence from AGENTS.md and package scripts (or equivalent project configuration). +- In Findings, report every exact verification command and its outcome, including exit status. A bare \`pass\` without command evidence is an incomplete report. +- If a required check genuinely cannot run because of a missing runtime or dependency, sandbox restriction, or permissions, record the exact inability under Blockers; never silently skip a required check. + +Orchestration: +- Break multi-step or parallel work into focused worker dispatches with distinct lenses; prefer \`spawn_agent\` (fire several in one turn when jobs are independent), then reply with who is running and end the turn — workers keep running while you are idle. Mailbox mail arrives as inbound when a worker finishes; read it and do not poll. \`list_agents\` shows the fleet without blocking; after a parked ask is surfaced, answer with \`send_input\` and do not poll \`list_agents\`. +- Pass the typed spawn contract: \`intent\`, \`success_criteria\` (done-when; required for implement/review and their default directors), \`do_not\` (scope fence), and \`report_focus\`. Free-form \`prompt\` without \`success_criteria\` fail-closes for implement/review and their default directors. +- After workers return, classify fail / incomplete-report vs parent-initiated interrupt vs operator-cancel vs clean complete. Fail-path (\`status: failed\` or salvage \`incomplete-report\`): diagnose from the report or error and MAY spawn one successor with a changed brief. Parent-initiated interrupt (\`interrupt_agent\` / \`send_input\` with \`interrupt:true\` unblocks wait with \`stop_reason: interrupted\`): the worker is often still running and often has no report — \`resume_agent\`, or idle for its mailbox mail; do not \`spawn_agent\` a successor against a still-live worker. Successor only if that session is no longer resumable. Operator-cancel (\`stop_reason\` cancelled): wait for the operator; do not auto-retry. Identical brief: refuse. Merge Summary/Findings into a coherent answer for the operator; do not paste raw fleet-agent dumps. +- Use manage_tasks for your own coordination checklist; spawning workers is \`spawn_agent\`, not manage_tasks. +- If context is compacted automatically, do not stop tasks early due to token fear; persist progress via manage_tasks and worker reports.`); + }); + it("keepstyle omit keeps response style, drops tool-choice / ask-vs-proceed / orchestration", () => { expect([...KEEPSTYLE_PROMPT_SECTION_OMIT].sort()).toEqual([ "askVsProceed", From 491da78a0ca88c5386b049c1cafb186e755ca037 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 21:53:44 -0700 Subject: [PATCH 3/3] Constrain guideline omit ids and validate profiles at load Reject unknown promptSectionOmit ids in ProfileSchema via type.enumerated over GUIDELINE_SUB_BLOCK_IDS so profile typos fail closed at load instead of silently keeping the full guidelines. Type omit as GuidelineSubBlockId end to end and drop the unwired KEEPSTYLE_PROMPT_SECTION_OMIT export. --- src/agent/prompts.test.ts | 21 +++++---------------- src/agent/prompts.ts | 19 ++++--------------- src/config/index.ts | 3 ++- src/config/profiles.ts | 5 ++++- src/profiles.test.ts | 21 +++++++++++++++++++++ src/session/runtime-assembly.ts | 3 ++- 6 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/agent/prompts.test.ts b/src/agent/prompts.test.ts index 54c93842a..f53199460 100644 --- a/src/agent/prompts.test.ts +++ b/src/agent/prompts.test.ts @@ -6,7 +6,6 @@ import { buildPromptDisciplineBlock, buildSubAgentSystemPrompt, GUIDELINE_SUB_BLOCK_IDS, - KEEPSTYLE_PROMPT_SECTION_OMIT, } from "./prompts.js"; import { CORE_TOOL_NAMES, CATALOG_TOOL_NAMES } from "./tool-search.js"; @@ -222,13 +221,10 @@ Orchestration: - If context is compacted automatically, do not stop tasks early due to token fear; persist progress via manage_tasks and worker reports.`); }); - it("keepstyle omit keeps response style, drops tool-choice / ask-vs-proceed / orchestration", () => { - expect([...KEEPSTYLE_PROMPT_SECTION_OMIT].sort()).toEqual([ - "askVsProceed", - "orchestration", - "toolChoice", - ]); - const guidelines = buildGuidelines({ omit: KEEPSTYLE_PROMPT_SECTION_OMIT }); + it("omit keeps response style, drops tool-choice / ask-vs-proceed / orchestration", () => { + const guidelines = buildGuidelines({ + omit: ["toolChoice", "askVsProceed", "orchestration"], + }); expect(guidelines).toContain("Response style:"); expect(guidelines).toContain("Scope and conventions:"); expect(guidelines).not.toContain("Tool choice:"); @@ -236,13 +232,6 @@ Orchestration: expect(guidelines).not.toContain("Orchestration:"); }); - it("ignores unknown omit ids", () => { - const guidelines = buildGuidelines({ omit: ["no-such-block"] }); - expect(guidelines).toContain("Response style:"); - expect(guidelines).toContain("Tool choice:"); - expect(guidelines).toContain("Orchestration:"); - }); - it("threads guidelineConfig through the chat system prompt", () => { const full = buildChatSystemPrompt( undefined, @@ -260,7 +249,7 @@ Orchestration: [], "orchestrator", undefined, - { omit: KEEPSTYLE_PROMPT_SECTION_OMIT }, + { omit: ["toolChoice", "askVsProceed", "orchestration"] }, ); expect(keepstyle).toContain("Response style:"); expect(keepstyle).not.toContain("Tool choice:"); diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index 07b562a27..80cfae44b 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -120,22 +120,11 @@ export const GUIDELINE_SUB_BLOCK_IDS = [ export type GuidelineSubBlockId = (typeof GUIDELINE_SUB_BLOCK_IDS)[number]; -/** Id-based guideline policy: which sub-blocks to drop. Unknown ids are ignored. */ +/** Id-based guideline policy: which sub-blocks to drop. */ export interface GuidelineConfig { - readonly omit?: readonly string[]; + readonly omit?: readonly GuidelineSubBlockId[]; } -/** - * Omit-set for the keepstyle guideline footprint: terse response style stays, - * tool-choice / ask-vs-proceed / orchestration guidance drops. Scope and - * conventions (build gate, verification evidence) always stay. - */ -export const KEEPSTYLE_PROMPT_SECTION_OMIT: readonly string[] = [ - "toolChoice", - "askVsProceed", - "orchestration", -]; - interface GuidelineBlockContext { readonly subAgent: boolean; readonly askDirector: boolean; @@ -235,8 +224,8 @@ export function buildGuidelines( // Picks the collection-path copy: wait_agents vs mailbox mail. waitAgentsMounted?: boolean; // Id-based policy: drop the named sub-blocks (see GUIDELINE_SUB_BLOCKS). - // Unknown ids are ignored; empty (default) keeps the full guidelines. - omit?: readonly string[]; + // Empty (default) keeps the full guidelines. + omit?: readonly GuidelineSubBlockId[]; } = {}, ): string { const ctx: GuidelineBlockContext = { diff --git a/src/config/index.ts b/src/config/index.ts index 1d9d60216..2d396bfd3 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -12,6 +12,7 @@ import { COMMAND_NAME } from "../branding.js"; import { isDirectorId } from "../agent/directors/registry.js"; import { DIRECTOR_IDS, type DirectorId } from "../agent/directors/types.js"; +import type { GuidelineSubBlockId } from "../agent/prompts.js"; import { validateEffort, type ReasoningEffort, @@ -564,7 +565,7 @@ export interface Config { systemPromptExtensions?: string[]; // Guideline sub-block ids to drop from the chat system prompt (see // GUIDELINE_SUB_BLOCK_IDS in agent/prompts.ts). Omitted = full guidelines. - promptSectionOmit?: string[]; + promptSectionOmit?: GuidelineSubBlockId[]; // Per-call inactivity timeout in ms (default 120_000 in the harness). Tune // higher for reasoning models with long silent-thinking stretches. inactivityTimeoutMs?: number; diff --git a/src/config/profiles.ts b/src/config/profiles.ts index b11806860..070d0bb2c 100644 --- a/src/config/profiles.ts +++ b/src/config/profiles.ts @@ -3,6 +3,7 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { type } from "arktype"; import { SETTINGS_DIR_NAME } from "../branding.js"; +import { GUIDELINE_SUB_BLOCK_IDS } from "../agent/prompts.js"; const ProfileSchema = type({ "profile?": "string", @@ -10,7 +11,9 @@ const ProfileSchema = type({ "systemPromptExtensions?": "string[]", // Guideline sub-block ids to drop from the chat system prompt (see // GUIDELINE_SUB_BLOCK_IDS in agent/prompts.ts). Omitted = full guidelines. - "promptSectionOmit?": "string[]", + // Unknown ids are rejected here so profile typos fail closed at load + // instead of silently keeping the full guidelines. + "promptSectionOmit?": type.enumerated(...GUIDELINE_SUB_BLOCK_IDS).array(), // Per-call inactivity timeout in milliseconds. If the provider yields no // inference event for this many ms, the call is aborted and the user sees // "Request timed out". Default in the inference harness is 120_000 (2 min). diff --git a/src/profiles.test.ts b/src/profiles.test.ts index bcb353206..9918ef423 100644 --- a/src/profiles.test.ts +++ b/src/profiles.test.ts @@ -57,6 +57,27 @@ test("loadProfile parses systemPromptExtensions", async () => { }); }); +test("loadProfile parses promptSectionOmit", async () => { + const dir = makeTmp(); + await mkdir(dir, { recursive: true }); + const path = join(dir, "profile.json"); + await writeJson(path, { + promptSectionOmit: ["toolChoice", "orchestration"], + }); + const result = await loadProfile(path); + expect(result).toEqual({ + promptSectionOmit: ["toolChoice", "orchestration"], + }); +}); + +test("loadProfile rejects unknown promptSectionOmit ids", async () => { + const dir = makeTmp(); + await mkdir(dir, { recursive: true }); + const path = join(dir, "profile.json"); + await writeJson(path, { promptSectionOmit: ["no-such-block"] }); + await expect(loadProfile(path)).rejects.toThrow(/promptSectionOmit/); +}); + test("loadProfile rejects unknown keys", async () => { const dir = makeTmp(); await mkdir(dir, { recursive: true }); diff --git a/src/session/runtime-assembly.ts b/src/session/runtime-assembly.ts index 6099b36e8..bf9229a37 100644 --- a/src/session/runtime-assembly.ts +++ b/src/session/runtime-assembly.ts @@ -14,6 +14,7 @@ import type { import type { Compactor } from "@intx/types/runtime"; import { buildChatSystemPrompt } from "../agent/prompts.js"; +import type { GuidelineSubBlockId } from "../agent/prompts.js"; import type { ToolAvailability } from "../agent/tool-search.js"; import { gatherEnvironment } from "../agent/environment.js"; import { @@ -290,7 +291,7 @@ export interface SessionChatPromptArgs { toolAvailability: ToolAvailability; // Guideline sub-block ids to drop (see GUIDELINE_SUB_BLOCK_IDS). // Omitted = full guidelines. - promptSectionOmit?: readonly string[]; + promptSectionOmit?: readonly GuidelineSubBlockId[]; // Session-start snapshot from createAgentToolset. When provided, skip // rediscovery so the prompt listing and skill_search share one catalog. skills?: readonly SkillSummary[];