From 6ad60d555f2c4f91ccfbbb0f13472d345559d89f Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 11 Sep 2026 21:57:47 -0700 Subject: [PATCH] Preserve all four sub-agent report headings with None. for empty sections --- src/agent/prompts.test.ts | 12 ++++++++++++ src/agent/prompts.ts | 2 +- src/subagent/index.test.ts | 22 ++++++++++++++++++++++ src/subagent/report.ts | 18 +++++++++--------- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/agent/prompts.test.ts b/src/agent/prompts.test.ts index 05db7d7e6..76a2073dd 100644 --- a/src/agent/prompts.test.ts +++ b/src/agent/prompts.test.ts @@ -128,6 +128,18 @@ describe("shared discipline block appears exactly once per built prompt", () => }); }); +describe("sub-agent report contract", () => { + it("requires all four headings with None. instead of omitting empty sections", () => { + const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, { + orchestrator: false, + grokAntiThrash: false, + }); + expect(prompt).not.toContain("omit empty sections"); + expect(prompt).toMatch(/emit all four headings/); + expect(prompt).toContain('"None."'); + }); +}); + describe("shared verification guidance", () => { it("requires evidence-carrying verification in worker prompts", () => { const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, { diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index 8ac05ce48..792c7b328 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -441,7 +441,7 @@ export function buildSubAgentReportContract( "- Stick to the dispatch brief. Do not invent scope or wander into unrelated work.", "- If the brief lists Success criteria, treat them as the done-definition: when all are met (or you are blocked), stop calling tools and emit the report envelope. Do not keep tooling past done.", "- If the brief lists Do not, respect those constraints; do not invent scope outside Intent / Do not.", - "- When done, stop calling tools and reply with ONLY this markdown envelope (prose inside each section is fine; omit empty sections rather than inventing content):", + '- When done, stop calling tools and reply with ONLY this markdown envelope (prose inside each section is fine; emit all four headings every time in this order, writing "None." under a heading with nothing to report rather than dropping it):', "", "## Summary", "One or two sentences: what you accomplished or concluded.", diff --git a/src/subagent/index.test.ts b/src/subagent/index.test.ts index a24a1cdfa..6aac80274 100644 --- a/src/subagent/index.test.ts +++ b/src/subagent/index.test.ts @@ -692,6 +692,9 @@ describe("sub-agent stop helpers", () => { const cancelledParsedEmpty = parseSubAgentReport(emptyCancelled); expect(cancelledParsedEmpty.findings).toContain("no partial findings"); expect(emptyCancelled.toLowerCase()).not.toContain("summarize progress"); + // Empty Paths still renders its heading so the envelope stays complete. + expect(hasReportEnvelope(emptyCancelled)).toBe(true); + expect(emptyCancelled).toContain("## Paths\nNone."); // Nested agent envelope must not clobber the outer cancelled Summary when // runSubAgent re-parses the forced stop. @@ -828,6 +831,25 @@ describe("sub-agent stop helpers", () => { expect(withPathsParsed.findings).toContain("src/a.ts"); }); + test("round-trip preserves the envelope when a section body is empty", () => { + const reply = [ + "## Summary", + "Did the work.", + "", + "## Findings", + "Touched the gate.", + "", + "## Blockers", + "", + "## Paths", + "src/gate.ts", + ].join("\n"); + expect(hasReportEnvelope(reply)).toBe(true); + const roundTripped = formatSubAgentReport(parseSubAgentReport(reply)); + expect(hasReportEnvelope(roundTripped)).toBe(true); + expect(parseSubAgentReport(roundTripped).blockers).toBe("None."); + }); + test("forcedStopReport renders a Stopped line for display; classification uses the typed reason", () => { expect( forcedStopReport("cancelled", "partial", { detail: "Session closed" }), diff --git a/src/subagent/report.ts b/src/subagent/report.ts index 54bc46635..661f1e541 100644 --- a/src/subagent/report.ts +++ b/src/subagent/report.ts @@ -264,15 +264,15 @@ export function formatSubAgentReport(report: SubAgentReport): string { lines.push( "## Summary", report.summary.length > 0 ? report.summary : "(no summary)", + "", + "## Findings", + report.findings.length > 0 ? report.findings : "None.", + "", + "## Blockers", + report.blockers.length > 0 ? report.blockers : "None.", + "", + "## Paths", + report.paths.length > 0 ? report.paths : "None.", ); - if (report.findings.length > 0) { - lines.push("", "## Findings", report.findings); - } - if (report.blockers.length > 0) { - lines.push("", "## Blockers", report.blockers); - } - if (report.paths.length > 0) { - lines.push("", "## Paths", report.paths); - } return lines.join("\n"); }