Skip to content

Commit ece09b1

Browse files
committed
Stop cancelled salvage from inviting a parent re-dispatch
Cancelled blockers and the cancelled parent hint now ask the parent to synthesize Findings and wait for the operator. Tests pin that copy and the absence of the standing next-worker invitation on the primary chat prompt.
1 parent 7b73cc6 commit ece09b1

5 files changed

Lines changed: 18 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1717

1818
- Skywalker and primary orchestration guidance no longer invite auto-starting
1919
the next worker after an unfinished specialist.
20+
- Cancelled salvage asks the parent to synthesize Findings and wait for the
21+
operator instead of auto-starting another specialist.
2022

2123
## [0.3.14] - 2026-09-03
2224

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Two directors, selected by role:
114114

115115
- **ChatDirector** (interactive, `src/agent/director.ts`) — Extends `DefaultDirector` with task list tracking, workflow nudges, LSP auto-activation, and multi-turn chat semantics. It never terminates the session: operator declines are surfaced as replies and the reactor stays alive for the next message. Auto mode is toggled by CLI flags (`--auto` / `--no-auto`); there is currently no in-session key to toggle it (default on; constrained envelope — workspace writes and unconstrained shell auto-allow; installs, recursive rm, force/uncontained worktree changes, sensitive-path and opaque-wrapper shell still ask; contained non-force `git worktree add`/`remove`/`prune` and `list` auto-allow; shell file-mutation denied). It is not a separate edit/plan mode.
116116
- **SubAgentDirector** (delegated work, `src/subagent/index.ts`) — Drives a dispatched worker until a turn arrives with no tool calls, then replies with the final assistant text and ends the run. A tool-less turn **after tools** completes only with the four-heading envelope (Summary, Findings, Blockers, Paths); a missing envelope nudges once (**incomplete-report**) and a second tool-less turn still without the envelope salvages as **incomplete-report-stop**. Explore/read-only workers that used tools then replied with findings remain normal completes; `requireEvidence` (off by default, set per director) additionally requires at least one read before a tool-less spawn-only reply can complete. Reads done through `run_shell` count as evidence too — `src/subagent/shell-evidence.ts` classifies shell reads (`cat`, `grep`, `sed` without `-i`, …) over the same subject expansion the auto-shell policy uses — but there is no corresponding shell-write evidence or file-write requirement: a run that never touches a file still completes normally once it replies with the envelope. There is no turn budget. Operator/parent cancel after any progress returns a **cancelled** salvage report (partial findings + tool activity) instead of a bare cancel string; cancel before progress still surfaces as cancelled-by-operator. There is no repetition/no-progress/never-acted/never-edited hard stop and no fingerprint-based re-dispatch block — a genuinely stuck leaf runs until it completes, stalls, hits an opt-in wall-clock deadline, or is cancelled.
117-
`spawn_agent` starts each worker and records it in the caller's fleet mailbox; `wait_agents` collects terminal reports from that mailbox. Deadline salvage prepends an advisory parent hint suggesting continuation plus a longer deadline if more wall-clock time is warranted; cancelled salvage suggests continuing from Findings rather than redoing completed work. Either way the hint is advisory only — an identical re-dispatch is still admitted.
117+
`spawn_agent` starts each worker and records it in the caller's fleet mailbox; `wait_agents` collects terminal reports from that mailbox. Deadline salvage prepends an advisory parent hint suggesting continuation plus a longer deadline if more wall-clock time is warranted. Cancelled salvage asks the parent to synthesize Findings and Paths and wait for the operator instead of auto-starting another specialist. Deadline hints are advisory only — an identical re-dispatch is still admitted.
118118

119119
#### Model-family policy (`src/agent/model-family-policy.ts`)
120120

src/prompts.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,15 @@ test("orchestrator guidelines teach the typed task spawn contract", () => {
111111
expect(guidelines).toContain("intent");
112112
expect(guidelines).toContain("spawn_agent");
113113
expect(guidelines).toContain("wait_agents");
114+
});
115+
116+
test("primary chat prompt does not invite auto-starting the next worker after unfinished specialists", () => {
117+
const prompt = buildChatSystemPrompt();
118+
const guidelines = buildGuidelines({ sessionMode: "orchestrator" });
114119
expect(guidelines).not.toContain("change the brief rather than repeating it");
115120
expect(guidelines).not.toContain("start the next worker");
121+
expect(prompt).not.toContain("Then start the next worker");
122+
expect(prompt).not.toContain("if the job still needs doing");
116123
});
117124

118125
test("primary guidelines advise against early-stop from compaction token fear", () => {

src/subagent/index.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ describe("sub-agent stop helpers", () => {
334334
const reparsed = formatSubAgentReport(parseSubAgentReport(salvaged));
335335
const reparsedFields = parseSubAgentReport(reparsed);
336336
expect(reparsedFields.summary).toContain("cancelled");
337-
expect(reparsedFields.blockers).toContain("re-dispatch");
337+
expect(reparsedFields.blockers).toContain("wait for the operator");
338+
expect(reparsedFields.blockers).not.toContain("parent may re-dispatch");
338339
expect(reparsedFields.findings).toContain("Reviewed the auth gate");
339340
expect(reparsedFields.findings).toContain("src/gate.ts");
340341
expect(reparsedFields.findings).toContain("### Summary");
@@ -352,7 +353,8 @@ describe("sub-agent stop helpers", () => {
352353
const cancelledParsed = parseSubAgentReport(cancelled);
353354
expect(cancelledParsed.summary).toContain("cancelled");
354355
expect(cancelledParsed.findings).toContain("Partial findings");
355-
expect(cancelledParsed.blockers).toContain("re-dispatch");
356+
expect(cancelledParsed.blockers).toContain("wait for the operator");
357+
expect(cancelledParsed.blockers).not.toContain("parent may re-dispatch");
356358

357359
// Nested agent envelope in partial text must not clobber cancel Summary.
358360
const cancelledNested = [
@@ -388,6 +390,8 @@ describe("sub-agent stop helpers", () => {
388390
expect(cancelledWithHint).not.toContain("wall-clock deadline");
389391
expect(cancelledWithHint).toContain("was cancelled before finishing");
390392
expect(cancelledWithHint).toContain("Findings and Paths");
393+
expect(cancelledWithHint).toContain("wait for the operator");
394+
expect(cancelledWithHint).not.toContain("re-dispatch only if");
391395

392396
// Paths section carries thrash salvage; empty prose with paths still informs Findings.
393397
const withPaths = forcedStopReport("cancelled", "", {

src/subagent/stop-policy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export function forcedStopReport(
229229
const summary = FORCED_STOP_SUMMARIES[reason];
230230
const blockers =
231231
reason === "cancelled"
232-
? "Operator or parent cancelled the worker mid-run; parent may re-dispatch with the partial findings below."
232+
? "Operator or parent cancelled the worker mid-run; synthesize the partial findings below, report Blockers, and wait for the operator."
233233
: reason === "deadline"
234234
? "Worker wall-clock deadline elapsed mid-run; parent may re-dispatch with a longer deadline or a narrower scope for the remaining work."
235235
: reason === "stalled"
@@ -258,7 +258,7 @@ const DEADLINE_PARENT_HINT =
258258
"[Sub-agent hit an explicit wall-clock deadline before finishing. Continue from Findings rather than redoing completed work; re-dispatch with continuation context and a longer deadline only if more wall-clock time is warranted.]";
259259

260260
const CANCELLED_PARENT_HINT =
261-
"[Sub-agent was cancelled before finishing. Continue from Findings and Paths rather than redoing completed work; re-dispatch only if remaining work is still needed.]";
261+
"[Sub-agent was cancelled before finishing. Synthesize Findings and Paths rather than redoing completed work; wait for the operator instead of auto-starting another specialist.]";
262262

263263
/** Options for parent-hint stacking (session re-dispatch ledger state). */
264264
export interface SubAgentParentHintOptions {

0 commit comments

Comments
 (0)