From f67b30dc83e496e677ed128542244deb03bf8384 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sun, 4 Oct 2026 22:13:35 -0700 Subject: [PATCH] feat(dispatch): distinguish mailbox turn yield from task completion --- src/agent/directors/dispatch/package.test.ts | 51 ++++++++++++++++++++ src/agent/directors/dispatch/package.ts | 4 ++ 2 files changed, 55 insertions(+) create mode 100644 src/agent/directors/dispatch/package.test.ts diff --git a/src/agent/directors/dispatch/package.test.ts b/src/agent/directors/dispatch/package.test.ts new file mode 100644 index 000000000..bd1a90296 --- /dev/null +++ b/src/agent/directors/dispatch/package.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from "bun:test"; + +import { DIRECTOR_REGISTRY } from "../registry.js"; + +const card = (): string => DIRECTOR_REGISTRY.dispatch.systemPrompt; + +/** + * CL-9920: mailbox turn yield vs task completion, plus midflight steering. + * Instruction-only change, so the existing harness is the prompt contract: + * each test scripts a staggered-worker scenario and asserts the dispatch + * card carries the guidance that scenario requires. All fail before the + * guidance lands. + */ +describe("dispatch yield/steering guidance (CL-9920)", () => { + test("staggered completion: yield with pending workers is not task completion", () => { + // Two workers spawned; one reports while the other still runs. The + // resume turn must end as a yield, never as a completion claim. + expect(card()).toMatch(/yield ends this turn/i); + expect(card()).toMatch(/never .* completion .* pending/i); + }); + + test("completion requires acceptance assessment and blocker disposition", () => { + // All workers reported but a check is unassessed and a blocker is open: + // completion still needs report-vs-brief acceptance plus disposition. + expect(card()).toMatch(/acceptance/i); + expect(card()).toMatch(/blocker/i); + }); + + test("resumed steering reaches active workers through existing messaging only", () => { + // Operator steers midflight; a worker is still running. The resumed + // turn must incorporate the steering and forward it via send_input — + // the existing channel — with no new scheduling/messaging invented. + expect(card()).toMatch(/steering/i); + expect(card()).toMatch(/send_input/); + expect(card()).not.toMatch(/new channel|new tool|new messag/i); + }); + + test("superseded work is named; stale results are never approval", () => { + // Steering supersedes one lane while its stale report lands anyway. + // Dispatch must name the superseded work and refuse the stale result. + expect(card()).toMatch(/superseded/i); + expect(card()).toMatch(/stale/i); + }); + + test("unreachable steering is disclosed, acceptance withheld, stop preserved", () => { + // Steering arrives but cannot reach active work: dispatch must say so, + // withhold acceptance, and keep stop/cancel semantics unchanged. + expect(card()).toMatch(/withhold acceptance/i); + expect(card()).toMatch(/stop/i); + }); +}); diff --git a/src/agent/directors/dispatch/package.ts b/src/agent/directors/dispatch/package.ts index 0313df46d..cea64284a 100644 --- a/src/agent/directors/dispatch/package.ts +++ b/src/agent/directors/dispatch/package.ts @@ -36,6 +36,10 @@ You are Dispatch, the coordinator for Corbits Code. Specialists own substantive - Spawn independent lanes in the same turn. There is no parallelization cap. Duplicate = the same live job, not the same repo. - Split by path, ownership, or lens. One worker = one outcome. Independent tickets/files/reviews go out together. Do not serialize explorer-then-coder unless the coder brief needs the map. - After spawn, yield. Mailbox (TUI/nested) or wait_agents (exec) delivers. Spawn the next independent wave before the first finishes. Do not poll. +- A yield ends this turn while work stays active: never claim completion while workers or checks are still pending. +- Completion requires acceptance: assess each worker report against its brief, disposition blockers, and only then report done. +- On resumption, incorporate newly received steering first. Forward relevant changes to still-active workers with send_input only — the existing messaging surface; invent no additional scheduling or delivery capabilities. Name superseded work, never adopt its stale results as approval, and never present them as acceptance. +- When steering cannot reach active work, disclose the limitation and withhold acceptance until it can be re-driven. Stop/cancel semantics are unchanged: stopping ends the run, it never completes it. - Use reports as the working record. Resolve gaps with the same worker instead of repeating its investigation. Route unfinished implementation back to coder, not yourself. - After coder finishes non-trivial or risky code changes, including single-file changes, run reviewer on the diff. Skip review only for mechanical or docs-only diffs and say so.