From 334e8b89131e7ba3d33c9e7627715588faf07cbd Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 20:32:25 -0700 Subject: [PATCH 1/2] Strip scheduler restatements from the Skywalker prompt --- src/agent/directors/skywalker/package.test.ts | 19 +++++++++++++++---- src/agent/directors/skywalker/package.ts | 11 +++++------ src/agent/prompts.test.ts | 7 +++++-- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/agent/directors/skywalker/package.test.ts b/src/agent/directors/skywalker/package.test.ts index d917eb682..9468fafcf 100644 --- a/src/agent/directors/skywalker/package.test.ts +++ b/src/agent/directors/skywalker/package.test.ts @@ -115,19 +115,25 @@ describe("skywalkerPackage", () => { expect(p).not.toContain("2–4 workers"); expect(p).not.toContain("at most 4"); expect(p).not.toContain("Prefer synthesizing early returns"); - expect(p).toContain("queues excess"); + // CL-6953: admission queue owns queueing (src/subagent/admission.ts) — + // the prompt keeps the fan-out judgment, not the mechanism restatement. + expect(p).not.toContain("queues excess"); expect(p).toContain("Do not invent a numeric cap"); }); test("systemPrompt prefers spawn_agent then idle (idle-orchestrator)", () => { const p = skywalkerPackage.systemPrompt; expect(p).toContain("spawn_agent"); - expect(p).toContain("wait_agents"); + // CL-6953: collection path (wait_agents vs mailbox) lives in the runtime + // mount + spawn_agent tool description — not the static prompt. + expect(p).not.toContain("wait_agents"); expect(p).toContain("Idle-orchestrator"); expect(p).not.toContain("task()"); expect(p).toContain("Spawn then idle; do not poll"); expect(p).not.toContain("do not poll wait_agents"); - expect(p).toContain("wait_agents is mounted on exec-primary runs only"); + // CL-6953: wait_agents mounting lives in the runtime (exec/runner.ts) and + // tool descriptions — the prompt keeps spawn-then-idle, not the mount fact. + expect(p).not.toContain("wait_agents is mounted on exec-primary runs only"); expect(p).toContain("mailbox mail arrives as inbound"); expect(p).toContain( "When the fleet goes dry the runtime re-enters with collected reports", @@ -218,6 +224,9 @@ describe("skywalkerPackage", () => { expect(p).toContain("clean-room"); expect(p).toContain("no fork"); expect(p).toContain("required for implement/review"); + // CL-6953 / CL-6807: single contract statement (spawn graph); the routing + // and handoff restatements are gone. + expect(p).not.toContain("Runtime requires success_criteria"); expect(p).not.toContain("Brief completeness"); expect(p).not.toContain("Prefer typed spawn"); expect(p.indexOf("Critic stays clean-room")).toBeGreaterThan( @@ -303,7 +312,9 @@ describe("skywalkerPackage", () => { expect(p).toContain("re-dispatch **builder**"); expect(p).toMatch(/narrowed or changed follow-up brief/i); expect(p).toContain("ship → verify → fix → re-verify"); - expect(p).toContain("Cap re-fix rounds"); + // CL-6953: no runtime retry budget enforces a re-fix cap — keep the loop + // judgment, not the number. + expect(p).not.toContain("Cap re-fix rounds"); }); test("systemPrompt Linear three-state: In Review at PR-open, never Done at PR-open", () => { diff --git a/src/agent/directors/skywalker/package.ts b/src/agent/directors/skywalker/package.ts index 562f9b2c8..401761613 100644 --- a/src/agent/directors/skywalker/package.ts +++ b/src/agent/directors/skywalker/package.ts @@ -16,7 +16,7 @@ You do not do the specialists' jobs by default. For tiny bounded product edits, Do not run long-blocking jobs on the parent (evals, full test suites, long installs, long-running implementation). Dispatch intern (mechanical shell), tester (suite / repro), or builder (substantial code). Path tools (write_file/edit_file/delete_file) are the DIY surface; shell file-writes stay denied. -Idle-orchestrator: fire one or more spawn_agent calls in a turn — each returns immediately with an agent_id and does not hold the parent. Then **reply to the operator** with who is running and **end the turn**. Workers keep running while you are idle; mailbox mail arrives as inbound when a worker finishes or fails — read it and decide the next action. Spawn then idle; do not poll. wait_agents is mounted on exec-primary runs only. list_agents shows the fleet without blocking; do not poll list_agents. interrupt_agent unblocks an in-flight wait immediately. Enter mid-run delivers at the next parent tool.boundary — a long parent foreground run_shell holds those steers (start long commands with run_shell background:true instead). A bare spawn_agent does not. When the fleet goes dry the runtime re-enters with collected reports. +Idle-orchestrator: fire one or more spawn_agent calls in a turn — each returns immediately with an agent_id and does not hold the parent. Then **reply to the operator** with who is running and **end the turn**. Workers keep running while you are idle; mailbox mail arrives as inbound when a worker finishes or fails — read it and decide the next action. Spawn then idle; do not poll. list_agents shows the fleet without blocking; do not poll list_agents. Enter mid-run delivers at the next parent tool.boundary — a long parent foreground run_shell holds those steers (start long commands with run_shell background:true instead). A bare spawn_agent does not. When the fleet goes dry the runtime re-enters with collected reports. # Operator updates (mandatory while fleet is live) @@ -52,7 +52,7 @@ Quick routing: - intern = exact shell / mechanical ops - After every delegated builder landing → run a critic on the diff/criteria in a fresh context; when architecture is in play, add greybeard for architecture judgment -success_criteria is required for implement/review and their default directors; recommended otherwise. Pass intent, do_not, report_focus, and agent when specialist. +Pass intent, do_not, report_focus, and agent when specialist. Parallelize independent lanes with spawn_agent, then idle. manage_tasks for your checklist. ask_operator when blocked or ambiguous — put long rationale in a normal transcript reply first, then call ask_operator with a short question and short option labels only. # Fetch URLs (primary-mounted) @@ -64,7 +64,7 @@ When the operator (or brief) gives an http(s) URL to read: # Effort scaling (IMPLEMENTATION / ORCHESTRATION) -Scale fan-out to the ask — the runtime queues excess rather than refusing: +Scale fan-out to the ask: - Simple (answer, one-path lookup, tiny fix): 0–1 worker, few tools; often answer without fleet - Tiny single-file / one-route asks: **DIY on the parent** with write_file/edit_file; skip spawn, skip explorer, skip plan, skip critic. Do not always explorer→plan→implement→critic for simple work — that burns wall clock. - Multi-lane work: spawn only named, non-overlapping lanes (distinct path/package/ownership). Width follows independent lanes. Do not invent a numeric cap. @@ -76,7 +76,7 @@ Do **not** turn a "why is this stalled / why no thinking / spawn looks broken" d - 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 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. \`incomplete-report\` from plan/counsel is not an attachable plan; do not auto-dispatch the same brief. +- 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). 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. - 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. @@ -85,7 +85,6 @@ Do **not** turn a "why is this stalled / why no thinking / spawn looks broken" d # Spawn handoff 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. @@ -97,7 +96,7 @@ When the operator brief states a function signature or return shape, put that ** Critic stays clean-room: brief + diff + public API; no fork. After every delegated **builder** implementation, run **critic** in a fresh context focused on the brief, resulting diff, and relevant public API contracts (sync/async, signatures). A substantial implementation limited to one internal file still requires Critic review. Builder self-report, even a green report with claimed test passes, is never sufficient to skip this independent critique. Skip a new Critic dispatch only for parent-DIY work or when existing independent review evidence already covers both the resulting diff and its success criteria. Use **tester** when you need independent suite evidence. If critic (or tester) reports **blocking** findings, re-dispatch **builder** with a narrowed or changed follow-up brief that carries those findings in success_criteria/do_not — do not declare done on a "ready" that ignored blockers. -Close the loop: ship → verify → fix → re-verify. Cap re-fix rounds (e.g. 1–2) then report Blockers. +Close the loop: ship → verify → fix → re-verify, then report Blockers. Critic flags correctness/brief gaps and hygiene the diff introduced — still evidence-based, still never fixing. That hygiene lens is not over-engineering theater. # Mandatory workflow for every request diff --git a/src/agent/prompts.test.ts b/src/agent/prompts.test.ts index f6eecd431..5fe2a6da4 100644 --- a/src/agent/prompts.test.ts +++ b/src/agent/prompts.test.ts @@ -163,10 +163,13 @@ describe("wait_agents mount-gated prompt copy (CL-7678)", () => { it("tells an unmounted primary to spawn then idle on mailbox mail", () => { const prompt = chatPrompt(TUI_AVAILABILITY); expect(prompt).toContain("mailbox mail arrives as inbound"); - // No wait_agents tool ad on an unmounted primary — only the exec-only note. + // No wait_agents tool ad on an unmounted primary — and no mount-fact + // restatement either (CL-6953: the mount lives in the runtime toolset + + // mount-gated guidelines copy, not the static prompt; naming an unmounted + // tool is an impossible-tool ref per CL-6807 hygiene). expect(prompt).not.toContain("- wait_agents:"); expect(prompt).not.toContain("collect with wait_agents"); - expect(prompt).toContain( + expect(prompt).not.toContain( "wait_agents is mounted on exec-primary runs only", ); }); From 1c93834307d955d6a1be3a18dfea83ac186c5aac Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 21:49:00 -0700 Subject: [PATCH 2/2] Restore one-successor-per-stall cap in Skywalker prompt --- src/agent/directors/skywalker/package.test.ts | 1 + src/agent/directors/skywalker/package.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agent/directors/skywalker/package.test.ts b/src/agent/directors/skywalker/package.test.ts index 9468fafcf..18713718d 100644 --- a/src/agent/directors/skywalker/package.test.ts +++ b/src/agent/directors/skywalker/package.test.ts @@ -177,6 +177,7 @@ describe("skywalkerPackage", () => { const p = skywalkerPackage.systemPrompt; expect(p).toContain("incomplete-report"); expect(p).toContain("MAY `spawn_agent` **one** successor"); + expect(p).toContain("Cap is one successor for that stall"); expect(p).toContain("changed** brief"); expect(p).toContain("wait for the operator"); expect(p).toContain("Do not auto-retry"); diff --git a/src/agent/directors/skywalker/package.ts b/src/agent/directors/skywalker/package.ts index 401761613..ac46beaa0 100644 --- a/src/agent/directors/skywalker/package.ts +++ b/src/agent/directors/skywalker/package.ts @@ -76,7 +76,7 @@ Do **not** turn a "why is this stalled / why no thinking / spawn looks broken" d - 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 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). 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. +- 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: if the successor also stalls, synthesize what returned, report Blockers, and stop — do not chain a further successor. 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. - 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.