From 230936366c6c804ef664115faf59cf0fdc6b30cd Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 13:29:45 -0700 Subject: [PATCH 1/2] Convert greybeard to a leaf director; only Skywalker may spawn CL-7670 converts the limited spawn CL-7662 kept with a recorded reason into a leaf: greybeard loses the fleet verbs and the nested-orchestrator tier, keeping its checklists, self-read, and provenance exactly as-is. The registry test now pins Skywalker as the only closed director with spawn permission, and stale nested-orchestrator references follow suit. --- docs/ARCHITECTURE.md | 21 +++--- docs/IMPLEMENTATION.md | 2 +- docs/PRODUCT.md | 2 +- src/agent/directors/greybeard/package.test.ts | 71 +++++++++---------- src/agent/directors/greybeard/package.ts | 17 ++--- src/agent/directors/registry.test.ts | 30 ++++---- src/agent/directors/registry.ts | 2 +- src/agent/directors/tool-sets.ts | 2 +- src/agent/directors/types.ts | 5 +- src/subagent/agent-fleet.test.ts | 19 ++--- src/subagent/index.test.ts | 2 +- src/subagent/types.ts | 4 +- tests/unit/exec/runner.test.ts | 15 ++-- 13 files changed, 96 insertions(+), 96 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 91470ed90..19bc20c8f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -229,11 +229,11 @@ Built-in directors with `spawn.maySpawn` may themselves call `spawn_agent` (one Every director package carries a required `tier: SubagentTier` field (`src/agent/directors/types.ts`) — data on the package, never a prompt instruction: -| Tier | Who | Fleet surface | -| ------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| 1 — `orchestrator` | skywalker (primary) | Full fleet control over the whole tree. | -| 2 — `nested-orchestrator` | greybeard, or any package with `spawn.maySpawn` | Same fleet surface, scoped to its own subtree: may manage only its own descendants, never a sibling or ancestor. | -| 3 — worker | every other director (`tier: "leaf"` on the package) | No fleet verbs. Mounts `ask_director` and `submit_result`. | +| Tier | Who | Fleet surface | +| ------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| 1 — `orchestrator` | skywalker (primary) | Full fleet control over the whole tree. | +| 2 — `nested-orchestrator` | any package with `spawn.maySpawn` (no closed director uses this tier today) | Same fleet surface, scoped to its own subtree: may manage only its own descendants, never a sibling or ancestor. | +| 3 — worker | every other director (`tier: "leaf"` on the package) | No fleet verbs. Mounts `ask_director` and `submit_result`. | Enforcement is runtime code at the existing tool-mount point, not prompt wording — this is the fix for four prior mechanisms (`writePaths`, `report.requiredSections`, a `--config` comment, the thrash matcher) that were documented-as-enforced while enforcing nothing: @@ -261,7 +261,7 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP | counsel | Eng change plan (steps, paths, tests, risks) | Arch gate, product discovery, code | | intern | Mechanical commands only | Ambiguous or product-design work | | critic | Evidence-based code review | Fixing product code | -| greybeard | Architecture/approach review of plans/docs; limited spawn | Authoring eng plans, implementing | +| greybeard | Architecture/approach review of plans/docs | Authoring eng plans, implementing | | neckbeard | Adversarial hygiene / refactor stress | Real review substitute | | bruckheimer | Product discovery → PRODUCT/ARCHITECTURE/IMPLEMENTATION-oriented briefs | Eng plan, code | | gaasbot | Quick CTO opinion voice | Formal review gate, implement | @@ -294,11 +294,10 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP **Spawn matrix** -| Who | Spawn rights | -| --------------------------- | ----------------------------- | -| skywalker (primary session) | Full closed fleet | -| greybeard | intern, explorer, critic only | -| All other directors | no fleet delegation tools | +| Who | Spawn rights | +| --------------------------- | ------------------------- | +| skywalker (primary session) | Full closed fleet | +| All other directors | no fleet delegation tools | **Tool envelopes** prefer small `tools.allow` mounts over deny-everything. Shipped docs/design directors (shakespeare, rand, bruckheimer) mount write tools with no path-level lock. Lane routing is spawn policy (shakespeare = P/A/I docs, rand = DESIGN.md, bruckheimer = product discovery), not a file lock. There is no static per-package write-path declaration (CL-6952 removed it — no shipped director ever set one); instead spawn_agent records, without blocking, when two concurrently running dispatches land on the same cwd (see `intervention-log.ts`'s `conflict` class). diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 0442ea1cc..a0cfea8fb 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -170,7 +170,7 @@ Sixteen packages under `src/agent/directors//` register in `DIRECTOR_REGISTR 6. There is no static write-path declaration on packages or profiles (CL-6952 removed it — no shipped director ever set one). Instead, `agent-fleet.ts` tracks each running dispatch by cwd; a new mutating dispatch that lands on the same cwd as a live mutating peer (`pending_init`/`running`, and not a declared read-only `modelRole` of `explore`/`plan`/`review`/`test`) records at most one `concurrent-lane-overlap` entry per cwd wave in `intervention-log.ts` (class `conflict`). The wave flag clears when no live mutating writer remains for that cwd. Terminal-but-unsettled lanes (for example cancelled with `finishedAt` set while the run promise has not reached `finally`) are pruned from the map and do not warn. This is advisory only — it never blocks the spawn, since cwd overlap does not prove the two lanes touch the same files. 7. Spawn effort: pin > package `modelRole` default (`defaultEffortForDirector`; intern=low; plan/review/orchestrator=high; implement/explore/docs/test=medium) > orchestrator/worker binary > parent inheritance. Optional skills are listed in the identity header for awareness; workers do not mount `use_skill` (guidance is baked into package system prompts). Primary mounts `use_skill` for its own skill list. -Intent defaults: `intent=implement` → director `builder`; `explore` → `explorer`; `plan` → `counsel`; `review` → `critic`; general → error. Spawn: skywalker full fleet; greybeard intern/explorer/critic only; all other directors mount no fleet tools. Live `` injects cwd, platform, arch, runtime, date, and git status on every chat and worker prompt. +Intent defaults: `intent=implement` → director `builder`; `explore` → `explorer`; `plan` → `counsel`; `review` → `critic`; general → error. Spawn: skywalker full fleet; all other directors, including greybeard, mount no fleet tools. Live `` injects cwd, platform, arch, runtime, date, and git status on every chat and worker prompt. ### Auto Mode diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index d59ccc023..ee7c2c871 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -164,7 +164,7 @@ The primary session is always **orchestrator** (single-agent mode is gone). Its | Design | draper, emil, rand | | Docs / QA | shakespeare, testsmith, tester | -There is **no catch-all worker**. `spawn_agent` requires `agent=…` or a non-general `intent` (implement/explore/plan/review→critic); bare dispatch and `intent=general` are refused. Named `spawn_agent(agent=…)` selects a director package without requiring a plugin profile, except `skywalker` which is the primary session identity and is refused as a spawned worker. Nested spawn is runtime-enforced: only skywalker (full fleet allowlist) and greybeard (intern/explorer/critic) may spawn; other workers have no fleet tools. Primary omits an allowlist so plugin profiles remain reachable from the main session. +There is **no catch-all worker**. `spawn_agent` requires `agent=…` or a non-general `intent` (implement/explore/plan/review→critic); bare dispatch and `intent=general` are refused. Named `spawn_agent(agent=…)` selects a director package without requiring a plugin profile, except `skywalker` which is the primary session identity and is refused as a spawned worker. Nested spawn is runtime-enforced: only skywalker (full fleet allowlist) may spawn; all other workers, including greybeard, have no fleet tools. Primary omits an allowlist so plugin profiles remain reachable from the main session. Corbits Code fans work out to short-lived **fleet agents** — workers with their own loop, tools, and checklist — while the primary session stays focused. diff --git a/src/agent/directors/greybeard/package.test.ts b/src/agent/directors/greybeard/package.test.ts index 1e307885c..f57f2b63a 100644 --- a/src/agent/directors/greybeard/package.test.ts +++ b/src/agent/directors/greybeard/package.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { REVIEW_TOOLS } from "../tool-sets.js"; import { greybeardPackage } from "./package.js"; describe("greybeardPackage", () => { @@ -72,13 +73,16 @@ describe("greybeardPackage", () => { expect(p).not.toMatch(/spawn a (greybeard|reviewer)/i); }); - test("systemPrompt allows limited spawn without fake caps or scheduler language", () => { + test("systemPrompt is a leaf worker: no spawn path, no fake caps or scheduler language", () => { const p = greybeardPackage.systemPrompt; - expect(p).toMatch(/intern/); - expect(p).toMatch(/explorer/); - expect(p).toMatch(/critic/); + expect(p).toMatch(/you cannot spawn/i); + expect(p).toMatch(/leaf worker/i); + expect(p).toMatch(/no fleet verbs are mounted/i); expect(p).toMatch(/Prefer doing the review yourself/i); expect(p).toMatch(/Do not invent numeric spawn caps|not a soft ladder/i); + expect(p).not.toMatch(/Spawn only when/i); + expect(p).not.toMatch(/Package spawn rules/i); + expect(p).not.toMatch(/Spawn then idle/i); expect(p).not.toMatch(/at most \d+/i); expect(p).not.toMatch(/spawn at most one/i); expect(p).not.toMatch(/parallel diagnostic fleet/i); @@ -87,11 +91,12 @@ describe("greybeardPackage", () => { expect(p).not.toMatch(/fan-out/i); }); - test("systemPrompt has Blinders against search_agents fleet discovery", () => { + test("systemPrompt has Blinders against fleet discovery and any spawn", () => { const p = greybeardPackage.systemPrompt; expect(p).toMatch(/Blinders/i); expect(p).toMatch(/do not call search_agents/i); - expect(p).toMatch(/even when nested/i); + expect(p).toMatch(/not an orchestrator/i); + expect(p).toMatch(/Do not spawn builder/); }); test("systemPrompt guides quality without enforcement theater", () => { @@ -108,12 +113,13 @@ describe("greybeardPackage", () => { expect(p).not.toMatch(/not Build\b/); }); - test("systemPrompt requires success_criteria when spawning critic", () => { + test("systemPrompt routes blocking unknowns to Blockers/ask_director instead of spawn", () => { const p = greybeardPackage.systemPrompt; - expect(p).toContain("success_criteria"); - expect(p).toMatch(/When spawning critic/); - expect(p).toMatch(/fail-closes without it/); - expect(p).toMatch(/intern and explorer remain optional/); + expect(p).toMatch(/When a concrete unknown blocks the judgment/); + expect(p).toMatch(/name it under Blockers/); + expect(p).toContain("ask_director"); + expect(p).not.toMatch(/When spawning critic/); + expect(p).not.toMatch(/success_criteria/); }); test("systemPrompt forbids spawning builder and names off-list directors", () => { @@ -121,41 +127,36 @@ describe("greybeardPackage", () => { expect(greybeardPackage.systemPrompt).not.toMatch(/\bspawn implement\b/); }); - test("spawn.maySpawn is true with limited allowlist", () => { - expect(greybeardPackage.spawn.maySpawn).toBe(true); - expect(greybeardPackage.spawn.allowlist).toEqual([ - "intern", - "explorer", - "critic", - ]); + test("spawn.maySpawn is false (leaf)", () => { + expect(greybeardPackage.spawn.maySpawn).toBe(false); + expect(greybeardPackage.spawn.allowlist).toBeUndefined(); }); - test("allowlist is only intern, explorer, critic", () => { - const allow = greybeardPackage.spawn.allowlist ?? []; - expect(allow).toHaveLength(3); - expect(allow).toContain("intern"); - expect(allow).toContain("explorer"); - expect(allow).toContain("critic"); - expect(allow).not.toContain("implement"); - expect(allow).not.toContain("builder"); - expect(allow).not.toContain("skywalker"); - expect(allow).not.toContain("counsel"); + test("no spawn allowlist survives the leaf conversion", () => { + expect(greybeardPackage.spawn.allowlist ?? []).toHaveLength(0); }); - test("tools.allow is orchestrator surface with product writes but without fleet discovery", () => { + test("tools.allow is the review surface without fleet verbs", () => { const allow = greybeardPackage.tools?.allow ?? []; + expect([...allow]).toEqual([...REVIEW_TOOLS]); expect(allow).not.toContain("task"); - expect(allow).toContain("spawn_agent"); - // CL-7678: nested orchestrators collect through mailbox mail; wait_agents - // is exec-primary opt-in, so it stays off the Greybeard allow. + expect(allow).not.toContain("spawn_agent"); expect(allow).not.toContain("wait_agents"); - // CL-7051: search_agents is Skywalker-only — nested directors spawn from allowlist. + // CL-7051: search_agents is Skywalker-only — leaves never mount discovery. expect(allow).not.toContain("search_agents"); + expect(allow).not.toContain("list_agents"); + expect(allow).not.toContain("send_input"); + expect(allow).toContain("read_file"); + expect(allow).toContain("grep"); expect(allow).toContain("write_file"); expect(allow).toContain("edit_file"); expect(allow).toContain("delete_file"); }); + test("tier is leaf", () => { + expect(greybeardPackage.tier).toBe("leaf"); + }); + test("modelRole is review", () => { expect(greybeardPackage.modelRole).toBe("review"); }); @@ -169,9 +170,7 @@ describe("greybeardPackage", () => { }); test("primaryIntent and outOfLane match greybeard lane", () => { - expect(greybeardPackage.primaryIntent).toBe( - "Architecture judgment; limited spawn", - ); + expect(greybeardPackage.primaryIntent).toBe("Architecture judgment"); expect(greybeardPackage.outOfLane).toContain("shipping product code"); expect(greybeardPackage.outOfLane).toContain( "pedantic style-only nitpicking", diff --git a/src/agent/directors/greybeard/package.ts b/src/agent/directors/greybeard/package.ts index 9fab55ab7..dd597c60a 100644 --- a/src/agent/directors/greybeard/package.ts +++ b/src/agent/directors/greybeard/package.ts @@ -1,5 +1,5 @@ import type { DirectorPackage } from "../types.js"; -import { ORCHESTRATOR_TOOLS } from "../tool-sets.js"; +import { REVIEW_TOOLS } from "../tool-sets.js"; /** * Greybeard nested orchestrator (CL-7019). @@ -11,17 +11,14 @@ import { ORCHESTRATOR_TOOLS } from "../tool-sets.js"; */ export const greybeardPackage: DirectorPackage = { id: "greybeard", - primaryIntent: "Architecture judgment; limited spawn", + primaryIntent: "Architecture judgment", outOfLane: ["shipping product code", "pedantic style-only nitpicking"], description: "Architecture judgment", optionalSkills: ["style", "philosophy", "native-integration"], - tools: { allow: ORCHESTRATOR_TOOLS }, - spawn: { - maySpawn: true, - allowlist: ["intern", "explorer", "critic"], - }, + tools: { allow: REVIEW_TOOLS }, + spawn: { maySpawn: false }, modelRole: "review", - tier: "nested-orchestrator", + tier: "leaf", systemPrompt: `You are GreybeardDirector (Greybeard), a specialist in Corbits Code. PRIMARY INTENT: architecture judgment. Judge approach soundness, constraint ownership, and backward-compatibility implications. Teach what holds and what does not. Do not fix or ship product code. @@ -41,9 +38,9 @@ Review checklist — work the list in order: 4. Rank risks for long-term maintainability and backward compatibility. 5. Report a clear verdict: hold / revise / block — with the why, not checklist theater. -Spawn only when a concrete unknown blocks that judgment. Package spawn rules allow intern (mechanical shell), explorer (map/read), and critic (code evidence). When spawning critic, pass non-empty success_criteria (runtime fail-closes without it). intern and explorer remain optional. Prefer doing the review yourself with mounted read/search tools. Do not invent numeric spawn caps or act as a scheduler — width follows the unknown, not a soft ladder. Spawn then idle; reports arrive as mailbox mail — do not poll. +Reach the judgment yourself and report it — you cannot spawn. Prefer doing the review yourself with mounted read/search tools. You are a leaf worker: no fleet verbs are mounted, so there is no delegation path. When a concrete unknown blocks the judgment, name it under Blockers (or ask the parent with ask_director) instead of delegating. Do not invent numeric spawn caps or act as a scheduler. -Blinders: do not call search_agents to discover the fleet (even when nested). You already know the limited spawn set; stay inside it. Do not spawn builder, counsel, skywalker, or other directors outside the allowlist. +Blinders: do not call search_agents to discover the fleet. Do not spawn builder, counsel, skywalker, or any other director. You are a leaf worker, not an orchestrator — delegation is the primary's job. Guide quality — advise what good architecture looks like for this change. Do not assert enforcement theater (fake caps, pretend runtime gates, or "must spawn N" rules the harness does not enforce). diff --git a/src/agent/directors/registry.test.ts b/src/agent/directors/registry.test.ts index 475fbdc52..a956ec4e4 100644 --- a/src/agent/directors/registry.test.ts +++ b/src/agent/directors/registry.test.ts @@ -107,7 +107,7 @@ describe("director registry", () => { expect(explorer.orchestrator).toBe(false); const grey = packageToProfile(DIRECTOR_REGISTRY.greybeard); - expect(grey.orchestrator).toBe(true); + expect(grey.orchestrator).toBe(false); const shakespeare = packageToProfile(DIRECTOR_REGISTRY.shakespeare); expect(shakespeare.capabilities?.mode).toBe("allow"); @@ -121,16 +121,21 @@ describe("director registry", () => { expect(profiles.map((p) => p.id)).not.toContain("skywalker"); }); - // Phase 5 acceptance (CL-5818 / CL-5843): spawn matrix, review envelopes, primary stance. - test("greybeard spawn allowlist is intern/explorer/critic only", () => { + // Phase 5 acceptance (CL-5818 / CL-5843) as converted by CL-7670: only the + // primary spawns — greybeard is a leaf. + test("greybeard is a leaf with no spawn", () => { const g = DIRECTOR_REGISTRY.greybeard; - expect(g.spawn.maySpawn).toBe(true); - expect(g.spawn.allowlist?.slice().sort()).toEqual([ - "critic", - "explorer", - "intern", - ]); - expect(packageToProfile(g).orchestrator).toBe(true); + expect(g.spawn.maySpawn).toBe(false); + expect(g.spawn.allowlist).toBeUndefined(); + expect(g.tier).toBe("leaf"); + expect(packageToProfile(g).orchestrator).toBe(false); + }); + + test("skywalker is the only maySpawn:true closed director", () => { + const spawners = DIRECTOR_IDS.filter( + (id) => DIRECTOR_REGISTRY[id].spawn.maySpawn, + ); + expect(spawners).toEqual(["skywalker"]); }); test("closed directors mount product write tools", () => { @@ -175,7 +180,7 @@ describe("director registry", () => { expect(internAllow).toContain("delete_file"); expect(internAllow).not.toContain("apply_patch"); for (const id of DIRECTOR_IDS) { - if (id === "skywalker" || id === "greybeard") continue; + if (id === "skywalker") continue; expect(DIRECTOR_REGISTRY[id].spawn.maySpawn).toBe(false); } }); @@ -208,7 +213,8 @@ describe("director registry", () => { expect(tierForDirectorId(id)).toBe(pkg.tier); } expect(DIRECTOR_REGISTRY.skywalker.tier).toBe("orchestrator"); - expect(DIRECTOR_REGISTRY.greybeard.tier).toBe("nested-orchestrator"); + // CL-7670: greybeard converted to a leaf — only the primary spawns. + expect(DIRECTOR_REGISTRY.greybeard.tier).toBe("leaf"); }); test("every director profile declares matching agent id in system prompt", () => { diff --git a/src/agent/directors/registry.ts b/src/agent/directors/registry.ts index 6e94407be..09e4ce1f1 100644 --- a/src/agent/directors/registry.ts +++ b/src/agent/directors/registry.ts @@ -138,7 +138,7 @@ export function packageToProfile(pkg: DirectorPackage): AgentProfile { description: `${pkg.description} (agent id: ${pkg.id})`, systemPromptRole: formatDirectorSystemPrompt(pkg), // Nested spawn is still gated by allowOrchestrator on the parent fleet tools. - // Greybeard/skywalker maySpawn marks intent; leaves stay non-orchestrator. + // Skywalker maySpawn marks intent; leaves stay non-orchestrator. orchestrator: pkg.spawn.maySpawn, ...(capabilities !== undefined ? { capabilities } : {}), }; diff --git a/src/agent/directors/tool-sets.ts b/src/agent/directors/tool-sets.ts index 75f86c088..cc3cc8987 100644 --- a/src/agent/directors/tool-sets.ts +++ b/src/agent/directors/tool-sets.ts @@ -72,7 +72,7 @@ export const INTERN_TOOLS = [ ] as const; /** - * Nested orchestrator surface (greybeard / package filter): dispatch + path writes. + * Nested orchestrator surface (package filter): dispatch + path writes. * wait_agents is NOT here: TUI primary and nested orchestrators collect through * mailbox mail. Exec primary mounts it separately (mountWaitAgents) and extends * its advertised allow in resolveExecDirectorOverlay. diff --git a/src/agent/directors/types.ts b/src/agent/directors/types.ts index a77e51b33..a4a8c19fd 100644 --- a/src/agent/directors/types.ts +++ b/src/agent/directors/types.ts @@ -37,8 +37,9 @@ export type TaskIntent = * * - "orchestrator": Tier 1, primary (skywalker). Full fleet control over the * whole tree. - * - "nested-orchestrator": Tier 2, scoped to its own subtree (e.g. greybeard). - * May manage only its own descendants, never siblings or ancestors. + * - "nested-orchestrator": Tier 2, scoped to its own subtree (no closed + * director uses this tier today). May manage only its own descendants, + * never siblings or ancestors. * - "leaf": Tier 3 worker. No fleet verbs at all. */ export type SubagentTier = "orchestrator" | "nested-orchestrator" | "leaf"; diff --git a/src/subagent/agent-fleet.test.ts b/src/subagent/agent-fleet.test.ts index 57e355e81..8f1ecb56e 100644 --- a/src/subagent/agent-fleet.test.ts +++ b/src/subagent/agent-fleet.test.ts @@ -2840,7 +2840,7 @@ describe("spawn_agent dispatch contracts", () => { expect(raw.content).toContain("builder"); }); - test("a maySpawn director is launched as an orchestrator with nestedDispatch", async () => { + test("greybeard launches as a leaf worker without nestedDispatch (CL-7670)", async () => { const captured: RunSubAgentParams[] = []; const deps = makeDeps(async (params) => { captured.push(params); @@ -2854,18 +2854,13 @@ describe("spawn_agent dispatch contracts", () => { }); await new Promise((resolve) => setTimeout(resolve, 20)); expect(captured).toHaveLength(1); - expect(defined(captured[0]).orchestrator).toBe(true); - expect(defined(captured[0]).orchestratorTier).toBe("nested-orchestrator"); - expect(defined(captured[0]).tier).toBe("nested-orchestrator"); - expect(defined(captured[0]).nestedDispatch).toBeDefined(); - expect(defined(captured[0]).nestedDispatch?.spawnAllowlist).toEqual([ - "intern", - "explorer", - "critic", - ]); + expect(defined(captured[0]).orchestrator).toBeUndefined(); + expect(defined(captured[0]).orchestratorTier).toBeUndefined(); + expect(defined(captured[0]).tier).toBe("leaf"); + expect(defined(captured[0]).nestedDispatch).toBeUndefined(); }); - test("allowOrchestrator false strips nested spawn even for maySpawn directors", async () => { + test("allowOrchestrator false keeps greybeard a leaf worker", async () => { const captured: RunSubAgentParams[] = []; const deps = makeDeps(async (params) => { captured.push(params); @@ -2881,7 +2876,7 @@ describe("spawn_agent dispatch contracts", () => { await new Promise((resolve) => setTimeout(resolve, 20)); expect(defined(captured[0]).orchestrator).toBeUndefined(); expect(defined(captured[0]).nestedDispatch).toBeUndefined(); - expect(defined(captured[0]).tier).toBe("nested-orchestrator"); + expect(defined(captured[0]).tier).toBe("leaf"); }); const FAIL_CLOSED_CRITERIA = diff --git a/src/subagent/index.test.ts b/src/subagent/index.test.ts index 6aac80274..13fded09b 100644 --- a/src/subagent/index.test.ts +++ b/src/subagent/index.test.ts @@ -493,7 +493,7 @@ describe("sub-agent stop helpers", () => { ).toBe("complete"); }); - test("evaluateSubAgentStop completes greybeard spawn-only envelope when requireEvidence is off", () => { + test("evaluateSubAgentStop completes a report envelope without evidence when requireEvidence is off", () => { const thrashState = { totalToolCalls: 1, readCounts: new Map(), diff --git a/src/subagent/types.ts b/src/subagent/types.ts index 7e1c35b7c..ec2d53c00 100644 --- a/src/subagent/types.ts +++ b/src/subagent/types.ts @@ -71,8 +71,8 @@ export type NestedDispatchDeps = SubAgentSandboxDeps & { // worktree-isolation behavior as their orchestrator. useWorktree?: boolean; /** - * When set (e.g. greybeard -> intern/explorer/critic), nested `spawn_agent` - * may only spawn these director/profile ids. Omitted = no allowlist filter (primary). + * When set, nested `spawn_agent` may only spawn these director/profile ids. + * Omitted = no allowlist filter (primary). No closed director sets one today. */ spawnAllowlist?: readonly string[]; /** Same process admission queue as the parent spawn. Tests inject. */ diff --git a/tests/unit/exec/runner.test.ts b/tests/unit/exec/runner.test.ts index 06b04d7d8..057cc6e53 100644 --- a/tests/unit/exec/runner.test.ts +++ b/tests/unit/exec/runner.test.ts @@ -30,6 +30,7 @@ import { createAdvertisedToolset } from "../../../src/session/assemble-runtime.j import { createDynamicToolRunner } from "../../../src/tui/dynamic-tool-runner.js"; import { BUILD_TOOLS, + REVIEW_TOOLS, SKYWALKER_TOOLS, } from "../../../src/agent/directors/tool-sets.js"; import { @@ -660,14 +661,16 @@ describe("resolveExecDirectorOverlay", () => { expect(overlay.systemPrompt).toContain("BuilderDirector"); }); - test("greybeard exec primary keeps wait_agents advertised (CL-7678)", () => { + test("greybeard exec primary is a leaf overlay without fleet verbs (CL-7670)", () => { const overlay = resolveExecDirectorOverlay("greybeard"); - expect(overlay.mountFleet).toBe(true); + expect(overlay.mountFleet).toBe(false); expect(overlay.advertisedAllow).toBeDefined(); - // Exec mounts wait_agents beside the fleet verbs even though the package - // allow omits it for TUI/nested mailbox-mail collection. - expect(overlay.advertisedAllow).toContain("wait_agents"); - expect(overlay.advertisedAllow).toContain("spawn_agent"); + expect(overlay.advertisedAllow).toEqual([...REVIEW_TOOLS]); + expect(overlay.advertisedAllow).not.toContain("spawn_agent"); + expect(overlay.advertisedAllow).not.toContain("wait_agents"); + expect(overlay.advertisedAllow).not.toContain("search_agents"); + expect(overlay.advertisedAllow).toContain("write_file"); + expect(overlay.systemPrompt).toContain("GreybeardDirector"); }); test("skywalker default still can mount fleet", () => { From 3bf85ec7c24ae8db32ef9fc633d714212c347946 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 12 Sep 2026 13:34:48 -0700 Subject: [PATCH 2/2] Fix greybeard leaf wording coherence --- src/agent/directors/greybeard/package.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent/directors/greybeard/package.ts b/src/agent/directors/greybeard/package.ts index dd597c60a..8ae42a783 100644 --- a/src/agent/directors/greybeard/package.ts +++ b/src/agent/directors/greybeard/package.ts @@ -2,12 +2,12 @@ import type { DirectorPackage } from "../types.js"; import { REVIEW_TOOLS } from "../tool-sets.js"; /** - * Greybeard nested orchestrator (CL-7019). + * Greybeard leaf worker (CL-7019). * Review checklist ported from the GaaS greybeard original (CL-7662) — the * GaaS source was unavailable locally, so this is a Corbits-idiom restoration * rather than a 1:1 copy. Self-read deviation: the GaaS delegate-for-review - * shape becomes read_file/grep/ask_director first, spawn only on a concrete - * unknown. Architecture judgment with limited spawn — never ships product code. + * shape becomes read_file/grep/ask_director first, concluding with a verdict + * rather than a spawn. Architecture judgment as a leaf — never ships product code. */ export const greybeardPackage: DirectorPackage = { id: "greybeard", @@ -29,7 +29,7 @@ Follow style and philosophy conventions (baked into this prompt) when reviewing Your value is analysis, not delegation: reach the judgment yourself with targeted reads (read_file, grep) and pointed questions (ask_director) -before considering a spawn. +before concluding. Review checklist — work the list in order: 1. Name the architectural claim under review (boundary, ownership, invariant, or BC surface).