Skip to content

Commit 8947ee7

Browse files
Merge pull request #919 from corbitsdev/cl-7756-salvage-plancounsel-stub-findings-as-incomplete-not-a
Salvage stub plan findings as an incomplete report
2 parents 276f383 + 68ea6e0 commit 8947ee7

16 files changed

Lines changed: 612 additions & 9 deletions

CHANGELOG.md

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

1414
## [Unreleased]
1515

16+
### Added
17+
18+
- Plan and counsel workers require substance in Findings (files/paths,
19+
acceptance criteria, non-goals, risks, ordered steps). Four headings
20+
with stub Findings salvage as `incomplete-report`, not an attachable
21+
plan. Implement and review envelope completeness is unchanged.
22+
1623
### Fixed
1724

1825
- System-originated inbound (`message.received` without the operator flag —
@@ -24,6 +31,7 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
2431
ask is dropped. Do not poll `list_agents`.
2532

2633

34+
2735
## [0.3.21] - 2026-09-11
2836

2937
### Added

docs/ARCHITECTURE.md

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

121121
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.
122122

123-
- **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). Assistant text that prints explicit `<tool_call>` markup is treated as attempted tool use, not narration: one **verbatim-tool-call** nudge asks the worker to re-issue a real `tool_call` and does not count toward the tool-less spiral. A missing envelope otherwise 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 worker runs until it completes, stalls, hits an opt-in wall-clock deadline, or is cancelled.
123+
- **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). Assistant text that prints explicit `<tool_call>` markup is treated as attempted tool use, not narration: one **verbatim-tool-call** nudge asks the worker to re-issue a real `tool_call` and does not count toward the tool-less spiral. A missing envelope otherwise 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. `requirePlanSubstance` (counsel or `intent=plan`, not `modelRole === "plan"`) additionally requires Findings to contain files/paths, acceptance criteria, non-goals, risks, and ordered steps with a non-placeholder line each — four headings with stub Findings are incomplete-report, not an attachable plan. 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 worker runs until it completes, stalls, hits an opt-in wall-clock deadline, or is cancelled.
124124
`spawn_agent` starts each worker and records it in the caller's fleet mailbox. On the TUI primary, mailbox mail is the collect path: occupancy takes uncollected terminals and re-enters the parent as system inbound. Nested orchestrators still collect with `wait_agents`. TUI-primary `wait_agents` may yield as a timeout (workers untouched, no take) so occupancy can deliver mail or a queued Enter steer. Already-collected waits return status without a second report or error body. Wait JSON includes `stop_reason` from the session when present so a salvage that is wait-`done` is not mistaken for a clean complete, and so parent-initiated interrupt (`interrupted`) is not mistaken for operator-cancel (`cancelled`). Deadline salvage prepends an advisory parent hint suggesting continuation plus a longer deadline if more wall-clock time is warranted. Failed and incomplete-report salvage tell the parent to diagnose from the report or error and MAY spawn one successor with a changed brief. A parent-initiated interrupt is a resumable pause: wait unblocks with `stop_reason: interrupted` (often while the session is still running and has no report); the parent should `resume_agent` or re-wait, and must not spawn a successor against a still-live worker. Successor only if that session is no longer resumable. Operator-cancelled salvage asks the parent to synthesize Findings and Paths and wait for the operator instead of auto-starting another specialist. Identical re-dispatch of the same brief stays refused at the prompt / spawn-handoff layer; there is no fingerprint-based re-dispatch hard-block. Deadline hints are advisory only — an identical re-dispatch is still admitted at runtime. Parent hints are prepended on salvage reports returned to the parent. The runtime does not auto-spawn successors.
125125

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

docs/PRODUCT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Corbits Code fans work out to short-lived **fleet agents** — workers with thei
172172
- **Tasks** are checklist items owned by one agent via `manage_tasks`.
173173
- **Fleet agents** are spawned with `spawn_agent`. On the TUI primary, mailbox mail arrives as inbound when a worker finishes or fails — do not poll `wait_agents`. Nested orchestrators still collect with `wait_agents`. Workers ask the parent with `ask_director`. That parks a question while the worker stays `running`. Nested `wait_agents` returns `awaiting_director` with a question payload — that is not terminal. Once a parked ask is surfaced, `list_agents` fails closed until `send_input` answers or the ask is dropped — do not poll `list_agents`. The parent answers with `send_input` (`target` = the worker's session id). When the parent TUI is not blocked in `wait_agents`, a parked question arrives as a synthetic idle-send wake. Escalate to the human only with `ask_operator`.
174174

175-
Dispatch uses a structured brief (context / goal / optional goals seed) and returns a structured report. The TUI Agents strip and fleet board show who is running; live tool progress updates the status bar without dumping the child transcript into the parent chat. There is no turn budget. A tool-less final turn completes only with the four-heading report envelope. Printed `<tool_call>` markup in assistant text gets one corrective nudge to issue a real tool call and does not count as the wrap-up; without the envelope, one incomplete-report nudge is given and a second tool-less turn without the envelope salvages as `incomplete-report-stop`. A silent worker (no activity for `stallTimeoutMs`, opt-in) gets one continuation nudge, then salvages as `stalled` only after a full `stallTimeoutMs` grace with still no activity — queued checks inside that window wait, they do not salvage. An opt-in `deadlineMs`, or an operator cancel, can also end a run early. Each of these returns a salvage report so a runaway or idle child cannot quietly burn a large token budget or look done after prose alone.
175+
Dispatch uses a structured brief (context / goal / optional goals seed) and returns a structured report. The TUI Agents strip and fleet board show who is running; live tool progress updates the status bar without dumping the child transcript into the parent chat. There is no turn budget. A tool-less final turn completes only with the four-heading report envelope. Printed `<tool_call>` markup in assistant text gets one corrective nudge to issue a real tool call and does not count as the wrap-up; without the envelope, one incomplete-report nudge is given and a second tool-less turn without the envelope salvages as `incomplete-report-stop`. Counsel and `intent=plan` also require plan substance in Findings (files/paths, acceptance criteria, non-goals, risks, ordered steps); headings-only or stub Findings salvage as `incomplete-report`, not an attachable plan. A silent worker (no activity for `stallTimeoutMs`, opt-in) gets one continuation nudge, then salvages as `stalled` only after a full `stallTimeoutMs` grace with still no activity — queued checks inside that window wait, they do not salvage. An opt-in `deadlineMs`, or an operator cancel, can also end a run early. Each of these returns a salvage report so a runaway or idle child cannot quietly burn a large token budget or look done after prose alone.
176176

177177
## Roadmap (planned, not yet shipped)
178178

src/agent/directors/counsel/package.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe("counselPackage", () => {
6060
expect(p).toContain("success_criteria");
6161
expect(p).toMatch(/[Ss]top when/);
6262
expect(p).toContain("Blockers");
63+
expect(p).toContain("Headings-only Findings is not done");
6364
});
6465

6566
test("spawn.maySpawn is false", () => {

src/agent/directors/counsel/package.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Author an agent-proof plan:
3636
3737
When requirements are fuzzy, ask_director instead of guessing — after the cap, note remaining questions under Blockers. Do not invent scope.
3838
39-
DONE GATE: Stop when the plan covers every success_criteria item from the brief OR blockers are explicit. Do not expand into implementation, architecture essays, or review theater after the plan is complete.
39+
DONE GATE: Stop when the plan covers every success_criteria item from the brief OR blockers are explicit. Headings-only Findings is not done. Do not expand into implementation, architecture essays, or review theater after the plan is complete.
4040
4141
OUT OF LANE: shipping code, architecture gate sign-off, fleet orchestration, pure code review, becoming Builder/Critic/Greybeard/Explorer as primary.
4242

src/agent/directors/skywalker/package.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ describe("skywalkerPackage", () => {
156156
expect(p).toContain("Do not reclassify COMMUNICATION as ORCHESTRATION");
157157
expect(p).toContain("synthesize what returned");
158158
expect(p).toContain("do **not** re-fan-out another diagnostic wave");
159+
expect(p).toContain(
160+
"`incomplete-report` from plan/counsel is not an attachable plan",
161+
);
159162
expect(p).not.toContain("Then start the next worker");
160163
expect(p).not.toContain("if the job still needs doing");
161164
});

src/agent/directors/skywalker/package.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Do **not** turn a "why is this stalled / why no thinking / spawn looks broken" d
7676
- Answer from mounted tools + known architecture; at most **one** explorer worker if a single unknown path blocks the answer.
7777
- Never spawn parallel "parent UI / child UI / stream events / prompt guardrail / session dig" waves for the same question.
7878
- When workers stall or loop: synthesize what returned, report Blockers, and change approach — do **not** re-fan-out another diagnostic wave on the same topic.
79-
- Failed wait (\`status: failed\` plus \`error\`) or salvage \`incomplete-report\`: diagnose from the wait report or error; MAY \`spawn_agent\` **one** successor with a **changed** brief (new \`success_criteria\` / \`do_not\` / continuation from Findings). Cap is one successor for that stall. Spawn the successor — do not search the repo as a substitute.
79+
- Failed wait (\`status: failed\` plus \`error\`) or salvage \`incomplete-report\`: diagnose from the wait report or error; MAY \`spawn_agent\` **one** successor with a **changed** brief (new \`success_criteria\` / \`do_not\` / continuation from Findings). Cap is one successor for that stall. Spawn the successor — do not search the repo as a substitute. \`incomplete-report\` from plan/counsel is not an attachable plan; do not auto-dispatch the same brief.
8080
- Parent-initiated interrupt (\`interrupt_agent\` / \`send_input\` with \`interrupt:true\`): wait unblocks with \`status: interrupted\` and \`stop_reason: interrupted\`. That is a resumable pause, not fail or incomplete-report. The worker is often still running and often has no report. Call \`resume_agent\` (changed follow-up into retained context) or re-wait. Do **not** \`spawn_agent\` a successor against a still-live worker. Successor only if the session is no longer resumable.
8181
- Operator-cancel (\`stop_reason\` cancelled, or Blockers that say wait for the operator): synthesize Findings and Paths, report Blockers, and **wait for the operator**. Do not auto-retry. Do not spawn a successor because the worker was cancelled.
8282
- Do **not** search the repo yourself after a worker stops without finishing.

src/prompts.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from "./agent/director.js";
77
import { manageTasksDefinition } from "./agent/tasks.js";
88
import { CHAT_PROMPT_QUALITY_MARKERS } from "./agent/prompt-contract.js";
9-
import { hasReportEnvelope } from "./subagent/report.js";
9+
import { hasPlanFindings, hasReportEnvelope } from "./subagent/report.js";
1010
import {
1111
buildActiveContext,
1212
buildAvailableTools,
@@ -424,6 +424,7 @@ test("sub-agent report contract's headings satisfy hasReportEnvelope", () => {
424424
.filter((line) => line.startsWith("## "))
425425
.join("\n");
426426
expect(hasReportEnvelope(headingsOnly)).toBe(true);
427+
expect(hasPlanFindings(headingsOnly)).toBe(false);
427428
});
428429

429430
test("sub-agent prompt does not advertise tool_search (it gets the full toolset)", () => {

src/subagent/agent-fleet.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,39 @@ describe("spawn_agent + wait_agents", () => {
617617
expect(defined(results[0]).error).toBeUndefined();
618618
});
619619

620+
test("plan-lane incomplete-report salvage is wait done with stop_reason, not a clean complete", async () => {
621+
const deps = makeDeps(async () => ({
622+
report: forcedStopReport(
623+
"incomplete-report",
624+
"Stub plan Findings (missing files/paths, acceptance criteria, non-goals, risks, or ordered steps). This is not an attachable plan.\n\nPlan ready.",
625+
),
626+
stopReason: "incomplete-report",
627+
}));
628+
const spawn = createSpawnAgentTool(deps);
629+
const wait = createWaitAgentsTool({
630+
sessions: deps.sessions,
631+
fleetRecords: deps.fleetRecords,
632+
});
633+
634+
const spawned = await callTool(spawn, {
635+
description: "stub plan",
636+
prompt: "outline it",
637+
intent: "plan",
638+
});
639+
const id = spawned.agent_id as string;
640+
const waited = await callTool(wait, { targets: [id], timeout_ms: 5000 });
641+
const results = waited.results as {
642+
status: string;
643+
report?: string;
644+
error?: string;
645+
stop_reason?: string;
646+
}[];
647+
expect(defined(results[0]).status).toBe("done");
648+
expect(defined(results[0]).stop_reason).toBe("incomplete-report");
649+
expect(defined(results[0]).report).toContain("not an attachable plan");
650+
expect(defined(results[0]).error).toBeUndefined();
651+
});
652+
620653
test("failed spawn_agent wait_agents returns error not report", async () => {
621654
const deps = makeDeps(async () => {
622655
throw new Error("provider blew up");

0 commit comments

Comments
 (0)