Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
workflow rails. Frozen-prefix matching ignores model-emitted copies of the
marker.

### Changed

- Skywalker may spawn one successor with a changed brief after a failed or
incomplete-report fleet worker. A parent-initiated interrupt
(`stop_reason: interrupted`) is a resumable pause — `resume_agent` or
re-wait, not a successor — unless the session is no longer resumable.
Operator-cancelled salvage still waits for the operator. Identical briefs
stay refused at the prompt / spawn-handoff layer. `wait_agents` JSON
includes `stop_reason` so interrupt vs cancelled vs incomplete-report stay
distinct. This reverses the 0.3.15 fail-path idle, not operator-cancel.

## [0.3.18] - 2026-09-08

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Two directors, selected by role:

- **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.
- **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 worker runs until it completes, stalls, hits an opt-in wall-clock deadline, or is cancelled.
`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.
`spawn_agent` starts each worker and records it in the caller's fleet mailbox; `wait_agents` collects terminal reports from that mailbox. 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.

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

Expand Down
4 changes: 2 additions & 2 deletions docs/TELEMETRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ a truthy value (`1`, `true`, …) to restore per-call spans for debugging.
worker ending during an active parent turn carries that turn's `parent_trace_id`.
Pre-progress operator aborts settle with `status=cancelled` and
`stop_reason=cancelled` even when the worker promise rejects. An interrupt that
keeps a worker resumable settles with `status=interrupted` and the same
`stop_reason=cancelled`; terminal events never report a still-running status.
keeps a worker resumable settles with `status=interrupted` and
`stop_reason=interrupted`; terminal events never report a still-running status.

A deterministic synthetic fixture captures 10 parent generations, 80 parent
tool spans, and 4 worker start/end pairs. The comparable former shape is 98
Expand Down
24 changes: 24 additions & 0 deletions src/agent/directors/skywalker/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,30 @@ describe("skywalkerPackage", () => {
expect(p).not.toContain("if the job still needs doing");
});

test("systemPrompt fail-then-successor is distinct from operator-cancel wait", () => {
const p = skywalkerPackage.systemPrompt;
expect(p).toContain("incomplete-report");
expect(p).toContain("MAY `spawn_agent` **one** successor");
expect(p).toContain("changed** brief");
expect(p).toContain("wait for the operator");
expect(p).toContain("Do not auto-retry");
expect(p).toContain("Identical re-dispatch of the same brief stays refused");
expect(p).toContain("Operator-cancel is not a re-dispatch");
expect(p).not.toContain("Then start the next worker");
expect(p).not.toContain("if the job still needs doing");
expect(p).not.toContain("interrupted-incomplete");
});

test("systemPrompt treats parent interrupt as resume, not successor spawn", () => {
const p = skywalkerPackage.systemPrompt;
expect(p).toContain("Parent-initiated interrupt");
expect(p).toContain("resume_agent");
expect(p).toContain("still-live worker");
expect(p).toContain("no longer resumable");
expect(p).toContain("interrupt_agent");
expect(p).toContain("stop_reason: interrupted");
});

test("systemPrompt simple path skips explorer+critic for tiny work", () => {
const p = skywalkerPackage.systemPrompt;
expect(p).toContain("DIY on the parent");
Expand Down
8 changes: 7 additions & 1 deletion src/agent/directors/skywalker/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ Do **not** turn a "why is this stalled / why no thinking / spawn looks broken" d
- Classify digs, screenshots of worker rows, and "why/how does X work" as COMMUNICATION first.
- Answer from mounted tools + known architecture; at most **one** explorer worker if a single unknown path blocks the answer.
- Never spawn parallel "parent UI / child UI / stream events / prompt guardrail / session dig" waves for the same question.
- When workers stall, loop, or come back unfinished: synthesize what returned, report Blockers, and change approach — do **not** re-fan-out another diagnostic wave on the same topic.
- 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.
- 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.
- 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.
- 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.
- Do **not** search the repo yourself after a worker stops without finishing.
- Permission asks and long run_shell clocks on worker rows are not a signal to spawn more diggers.

Expand All @@ -85,6 +88,9 @@ Do **not** turn a "why is this stalled / why no thinking / spawn looks broken" d
Child starts blank. Parent writes a complete packet: Goal, contracts copied verbatim, Scope/do_not, Done-when/success_criteria, What to report.
Runtime requires success_criteria for implement/review and their default directors; recommended otherwise.
Re-dispatch after a blocker is a new handoff (new criteria / new do_not), not a retry of the old one-liner.
Identical re-dispatch of the same brief stays refused.
Operator-cancel is not a re-dispatch — wait for the operator.
Parent-initiated interrupt is not a re-dispatch — resume_agent (or re-wait). Successor only if the session is no longer resumable.
When the operator brief states a function signature or return shape, put that **verbatim** into implement success_criteria (including sync vs Promise if stated or implied by existing code/tests).

# Verify after ship
Expand Down
2 changes: 1 addition & 1 deletion src/agent/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function buildGuidelines(
"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, and `wait_agents` / `list_agents` on a later turn collect their reports without holding this conversation blocked.",
"- 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, merge their Summary/Findings into a coherent answer for the operator; do not paste raw fleet-agent dumps.",
"- 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 re-wait; 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` / `wait_agents`, 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.",
]),
Expand Down
13 changes: 11 additions & 2 deletions src/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,20 @@ test("orchestrator guidelines teach the typed task spawn contract", () => {
expect(guidelines).not.toContain("weaker");
});

test("primary chat prompt does not invite auto-starting the next worker after unfinished specialists", () => {
test("primary chat prompt classifies fail-path successor vs interrupt resume vs operator-cancel wait", () => {
const prompt = buildChatSystemPrompt();
const guidelines = buildGuidelines({ sessionMode: "orchestrator" });
expect(guidelines).not.toContain("change the brief rather than repeating it");
expect(guidelines).toContain("MAY spawn one successor with a changed brief");
expect(guidelines).toContain("wait for the operator");
expect(guidelines).toContain("do not auto-retry");
expect(guidelines).toContain("Identical brief: refuse");
expect(guidelines).toContain("resume_agent");
expect(guidelines).toContain("still-live worker");
expect(guidelines).not.toContain("interrupted-incomplete");
expect(guidelines).not.toContain("start the next worker");
expect(prompt).toContain("MAY `spawn_agent` **one** successor");
expect(prompt).toContain("wait for the operator");
expect(prompt).toContain("Identical re-dispatch of the same brief stays refused");
expect(prompt).not.toContain("Then start the next worker");
expect(prompt).not.toContain("if the job still needs doing");
});
Expand Down
Loading
Loading