diff --git a/README.md b/README.md index 41399370f..d63ae757e 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Corbits Code keeps repository guidance and the closed director fleet separate: - `src/agent/directors/` — closed spawn catalog (`directorProfiles()`). Skywalker is the primary orchestrator; spawnable directors include builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper, - emil, rand, shakespeare, testsmith, tester, gauntlet, prober, and migrator. Closed ids cannot be + emil, rand, shakespeare, testsmith, tester, gauntlet, prober, migrator, and warden. Closed ids cannot be overridden by plugins or local files. - `.agents/agents/` — optional local profile additions; this directory is not required and may be absent diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bdb46e280..a532373fe 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -244,7 +244,7 @@ Enforcement is runtime code at the existing tool-mount point, not prompt wording #### Closed director fleet (`src/agent/directors/`) -Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, `modelRole`, fleet authority `tier`) registered in a **closed** set of 19 ids. There is no catch-all worker: `spawn_agent` without `agent` or non-general `intent`, and `spawn_agent(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `spawn_agent` dispatch time (not prompt-only). Skywalker is the primary session identity: `spawn_agent(agent="skywalker")` is refused, and `directorProfiles()` omits it from the spawn catalog. +Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, `modelRole`, fleet authority `tier`) registered in a **closed** set of 20 ids. There is no catch-all worker: `spawn_agent` without `agent` or non-general `intent`, and `spawn_agent(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `spawn_agent` dispatch time (not prompt-only). Skywalker is the primary session identity: `spawn_agent(agent="skywalker")` is refused, and `directorProfiles()` omits it from the spawn catalog. **Primary** @@ -266,6 +266,7 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP | bruckheimer | Product discovery → PRODUCT/ARCHITECTURE/IMPLEMENTATION-oriented briefs | Eng plan, code | | gaasbot | Quick CTO opinion voice | Formal review gate, implement | | migrator | Reversible settings/config/session-state migrations | Live-state execution, new features | +| warden | Trust review of permission/provider-auth/plugin-loader diffs | Fixes, general review | **Design trio (dev perspective)** diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 51bc6afd9..fce4983cb 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -155,14 +155,14 @@ Capabilities beyond the core toolset are opt-in plugins, enabled per workspace t ## Multi-agent (fleet agents) -The primary session is always **orchestrator** (single-agent mode is gone). Its identity is **Skywalker** (product name remains Corbits Code; when asked its name, answer Skywalker): classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 19 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `SKYWALKER_TOOLS`) — path tools are the DIY surface; spawn remains the default for substantial, multi-file, parallel, or specialist work. Shell file-writes stay denied. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-package write-path declaration (CL-6952 removed it — no shipped director ever set one). A concurrent dispatch landing on the same working directory as another still-running lane is recorded as a `conflict` intervention, not blocked. Operator slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`, `/lexicon`) tell Skywalker which directors to spawn for substantial work (`/lexicon` runs on the primary and spawns nothing); tiny/bounded edits may run on the primary. - -| Lane | Directors | -| --------- | ------------------------------------------------------------------------------------------------ | -| Primary | skywalker | -| Eng | builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, migrator | -| Design | draper, emil, rand | -| Docs / QA | shakespeare, testsmith, tester, gauntlet, prober | +The primary session is always **orchestrator** (single-agent mode is gone). Its identity is **Skywalker** (product name remains Corbits Code; when asked its name, answer Skywalker): classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 20 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `SKYWALKER_TOOLS`) — path tools are the DIY surface; spawn remains the default for substantial, multi-file, parallel, or specialist work. Shell file-writes stay denied. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-package write-path declaration (CL-6952 removed it — no shipped director ever set one). A concurrent dispatch landing on the same working directory as another still-running lane is recorded as a `conflict` intervention, not blocked. Operator slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`, `/lexicon`) tell Skywalker which directors to spawn for substantial work (`/lexicon` runs on the primary and spawns nothing); tiny/bounded edits may run on the primary. + +| Lane | Directors | +| --------- | -------------------------------------------------------------------------------------------------------- | +| Primary | skywalker | +| Eng | builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, migrator, warden | +| Design | draper, emil, rand | +| Docs / QA | shakespeare, testsmith, tester, gauntlet, prober | 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. diff --git a/src/agent/directors/registry.test.ts b/src/agent/directors/registry.test.ts index d167dc6df..9ff0b4ec6 100644 --- a/src/agent/directors/registry.test.ts +++ b/src/agent/directors/registry.test.ts @@ -13,9 +13,9 @@ import { } from "./registry.js"; describe("director registry", () => { - test("closed set has exactly 19 directors", () => { - expect(DIRECTOR_IDS).toHaveLength(19); - expect(listDirectors()).toHaveLength(19); + test("closed set has exactly 20 directors", () => { + expect(DIRECTOR_IDS).toHaveLength(20); + expect(listDirectors()).toHaveLength(20); for (const id of DIRECTOR_IDS) { expect(DIRECTOR_REGISTRY[id].id).toBe(id); } @@ -116,8 +116,8 @@ describe("director registry", () => { test("directorProfiles is the spawn catalog (closed set minus skywalker)", () => { const profiles = directorProfiles(); - expect(profiles).toHaveLength(18); - expect(new Set(profiles.map((p) => p.id)).size).toBe(18); + expect(profiles).toHaveLength(19); + expect(new Set(profiles.map((p) => p.id)).size).toBe(19); expect(profiles.map((p) => p.id)).not.toContain("skywalker"); }); @@ -168,6 +168,7 @@ describe("director registry", () => { "prober", "skywalker", "gauntlet", + "warden", ] as const) { const allow = DIRECTOR_REGISTRY[id].tools?.allow ?? []; expect(allow).toContain("write_file"); @@ -211,11 +212,11 @@ describe("director registry", () => { expect(s.tools?.allow).toContain("write_file"); expect(s.tools?.allow).toContain("edit_file"); expect(s.tools?.allow).toContain("delete_file"); - expect(s.spawn.allowlist).toHaveLength(18); + expect(s.spawn.allowlist).toHaveLength(19); }); // CL-6941: tier and spawn.maySpawn independently encode "may this package - // spawn", hand-set across 19 files. This pins their agreement so drift + // spawn", hand-set across 20 files. This pins their agreement so drift // (adding maySpawn: true without bumping tier, or vice versa) fails a test // instead of surfacing as an unexplained FleetAuthorityError at dispatch. test("tier agrees with spawn.maySpawn for every director", () => { diff --git a/src/agent/directors/registry.ts b/src/agent/directors/registry.ts index 4eee09664..d84c72768 100644 --- a/src/agent/directors/registry.ts +++ b/src/agent/directors/registry.ts @@ -18,6 +18,7 @@ import { testerPackage } from "./tester/index.js"; import { testsmithPackage } from "./testsmith/index.js"; import { gauntletPackage } from "./gauntlet/index.js"; import { proberPackage } from "./prober/index.js"; +import { wardenPackage } from "./warden/index.js"; import { formatDirectorSystemPrompt } from "./identity.js"; import { DIRECTOR_IDS, @@ -64,6 +65,7 @@ export const DIRECTOR_REGISTRY: Readonly> = gauntlet: gauntletPackage, prober: proberPackage, migrator: migratorPackage, + warden: wardenPackage, }; export function isDirectorId(value: unknown): value is DirectorId { diff --git a/src/agent/directors/skywalker/package.test.ts b/src/agent/directors/skywalker/package.test.ts index 3586ae08e..b3bb9c568 100644 --- a/src/agent/directors/skywalker/package.test.ts +++ b/src/agent/directors/skywalker/package.test.ts @@ -28,7 +28,7 @@ describe("skywalkerPackage", () => { test("maySpawn true with full closed allowlist", () => { expect(skywalkerPackage.spawn.maySpawn).toBe(true); - expect(skywalkerPackage.spawn.allowlist).toHaveLength(18); + expect(skywalkerPackage.spawn.allowlist).toHaveLength(19); expect(skywalkerPackage.spawn.allowlist).toEqual([ "builder", "explorer", @@ -48,6 +48,7 @@ describe("skywalkerPackage", () => { "gauntlet", "prober", "migrator", + "warden", ]); }); @@ -338,4 +339,14 @@ describe("skywalkerPackage", () => { "gauntlet = mutation-check that tests can actually fail (tree clean)", ); }); + + test("systemPrompt routes trust-path diffs to warden (tiny verify-after-ship mention)", () => { + const p = skywalkerPackage.systemPrompt; + expect(p).toContain("warden = permission / provider-auth / plugin-loader"); + expect(p).toContain("only when the diff touches those paths"); + expect(p).toContain( + "When the diff touches permission, provider-auth, or plugin-loader paths, add a warden trust review alongside critic", + ); + expect(p).toContain("never ships fixes"); + }); }); diff --git a/src/agent/directors/skywalker/package.ts b/src/agent/directors/skywalker/package.ts index 0d00c9058..9b1586e39 100644 --- a/src/agent/directors/skywalker/package.ts +++ b/src/agent/directors/skywalker/package.ts @@ -31,7 +31,7 @@ Example chains: - feature: explorer → plan → implement → critic - "why / how / is this stalled": answer yourself; at most one explorer if a single unknown blocks you -Closed directors (use search_agents / registry; each id is a spawn agent= target): builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, rand, shakespeare, testsmith, tester, gauntlet, prober, migrator. +Closed directors (use search_agents / registry; each id is a spawn agent= target): builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, rand, shakespeare, testsmith, tester, gauntlet, prober, migrator, warden. No catch-all worker. If unsure, reclassify — do not spawn a blob agent. Quick routing: @@ -39,6 +39,7 @@ Quick routing: - counsel = ordered eng plan (no ship) - builder = ship product code + tests - critic = defects with evidence including hygiene the diff introduced (no fix) +- warden = permission / provider-auth / plugin-loader trust review (no fix; only when the diff touches those paths) - greybeard = architecture judgment - neckbeard = hygiene / pedantry with receipts - tester = run the suite / repro @@ -54,6 +55,7 @@ Quick routing: - bruckheimer = product discovery docs - 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 +- When the diff touches permission, provider-auth, or plugin-loader paths, add a warden trust review alongside critic; warden reports trust findings and never ships fixes. 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. @@ -146,7 +148,7 @@ Do not reclassify COMMUNICATION as ORCHESTRATION just to justify parallel spawn # Spawn graph Skywalker = full closed set. Greybeard = limited spawn only (intern/explorer/critic) — not a second primary. -You may spawn: builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, rand, shakespeare, testsmith, tester, gauntlet, prober, migrator. +You may spawn: builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, rand, shakespeare, testsmith, tester, gauntlet, prober, migrator, warden. When spawning, pass a typed brief. success_criteria is required for implement/review and their default directors; recommended otherwise: - intent — explore | implement | plan | review @@ -209,6 +211,7 @@ export const skywalkerPackage: DirectorPackage = { "gauntlet", "prober", "migrator", + "warden", ], }, modelRole: "orchestrator", diff --git a/src/agent/directors/types.ts b/src/agent/directors/types.ts index cd8c5a1a0..6da25e742 100644 --- a/src/agent/directors/types.ts +++ b/src/agent/directors/types.ts @@ -23,6 +23,7 @@ export const DIRECTOR_IDS = [ "gauntlet", "prober", "migrator", + "warden", ] as const; export type DirectorId = (typeof DIRECTOR_IDS)[number]; diff --git a/src/agent/directors/warden/index.ts b/src/agent/directors/warden/index.ts new file mode 100644 index 000000000..e8d6fbda7 --- /dev/null +++ b/src/agent/directors/warden/index.ts @@ -0,0 +1 @@ +export { wardenPackage } from "./package.js"; diff --git a/src/agent/directors/warden/package.test.ts b/src/agent/directors/warden/package.test.ts new file mode 100644 index 000000000..61ddd4894 --- /dev/null +++ b/src/agent/directors/warden/package.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, test } from "bun:test"; +import { wardenPackage } from "./package.js"; + +describe("wardenPackage", () => { + test("id matches directory", () => { + expect(wardenPackage.id).toBe("warden"); + }); + + test("systemPrompt is real, not a placeholder", () => { + expect(wardenPackage.systemPrompt.length).toBeGreaterThan(0); + expect(wardenPackage.systemPrompt.startsWith("Placeholder")).toBe(false); + }); + + test("systemPrompt states PRIMARY INTENT", () => { + expect(wardenPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); + }); + + test("systemPrompt identity is Warden / WardenDirector", () => { + const p = wardenPackage.systemPrompt; + expect(p).toMatch(/WardenDirector \(Warden\)/); + expect(p).toMatch(/trust lane only/i); + }); + + test("systemPrompt trigger is written trust paths only", () => { + const p = wardenPackage.systemPrompt; + expect(p).toMatch(/TRIGGER/i); + expect(p).toMatch(/permission/); + expect(p).toMatch(/provider-auth/); + expect(p).toMatch(/plugin-loader/); + expect(p).toMatch(/Anything else is out of lane/); + expect(p).toMatch(/Do not expand into general code review/); + }); + + test("systemPrompt findings lens covers the trust surface", () => { + const p = wardenPackage.systemPrompt; + expect(p).toMatch(/Findings lens/i); + expect(p).toMatch(/grant-matching holes/i); + expect(p).toMatch(/secret-guard bypass/i); + expect(p).toMatch(/arktype boundary skips/i); + expect(p).toMatch(/shell-policy peel gaps/i); + expect(p).toMatch(/plugin trust/i); + expect(p).toMatch(/blocking, should-fix, or file-for-later/i); + }); + + test("systemPrompt is evidence-based findings, never-fix", () => { + const p = wardenPackage.systemPrompt; + expect(p).toMatch(/evidence/); + expect(p).toMatch(/never fix/i); + expect(p).toMatch(/Do not ship fixes/); + expect(p).toMatch(/permanent tests/i); + expect(p).toContain("testsmith/builder"); + expect(p).toContain("route to builder"); + }); + + test("systemPrompt is not a second critic or greybeard", () => { + const p = wardenPackage.systemPrompt; + expect(p).toMatch(/Do not become critic/); + expect(p).toMatch(/greybeard/); + }); + + test("systemPrompt has no tool-schema restatement or fake caps", () => { + const p = wardenPackage.systemPrompt; + expect(p).not.toMatch(/parameters?:/i); + expect(p).not.toMatch(/fan-out/i); + expect(p).not.toMatch(/at most \d+/i); + expect(p).not.toMatch(/turn budget/i); + expect(p).not.toMatch(/scheduler/i); + expect(p).not.toMatch(/Prefer grep\/search_files/i); + expect(p).not.toMatch(/Shell find\/rg/i); + expect(p).not.toMatch(/Write tools are not mounted/i); + expect(p).not.toMatch(/via run_shell/i); + }); + + test("spawn.maySpawn is false", () => { + expect(wardenPackage.spawn.maySpawn).toBe(false); + }); + + test("tier is leaf", () => { + expect(wardenPackage.tier).toBe("leaf"); + }); + + test("tools.allow is review surface with product writes", () => { + const allow = wardenPackage.tools?.allow ?? []; + expect(allow).toContain("read_file"); + expect(allow).not.toContain("use_skill"); + expect(allow).toContain("write_file"); + expect(allow).toContain("edit_file"); + expect(allow).toContain("delete_file"); + }); + + test("modelRole is review", () => { + expect(wardenPackage.modelRole).toBe("review"); + }); + + test("optionalSkills order is style, philosophy, native-integration, idiot-proof", () => { + expect(wardenPackage.optionalSkills).toEqual([ + "style", + "philosophy", + "native-integration", + "idiot-proof", + ]); + }); + + test("primaryIntent and outOfLane match warden lane", () => { + expect(wardenPackage.primaryIntent).toBe( + "Trust review of permission, provider-auth, and plugin-loader diffs; never fix product code", + ); + expect(wardenPackage.outOfLane).toContain("implementing fixes"); + expect(wardenPackage.outOfLane).toContain( + "general code review outside trust paths", + ); + expect(wardenPackage.outOfLane).toContain( + "architecture judgment without trust evidence", + ); + expect(wardenPackage.outOfLane).toContain("feature design"); + }); +}); diff --git a/src/agent/directors/warden/package.ts b/src/agent/directors/warden/package.ts new file mode 100644 index 000000000..ae4b6761c --- /dev/null +++ b/src/agent/directors/warden/package.ts @@ -0,0 +1,52 @@ +import type { DirectorPackage } from "../types.js"; +import { REVIEW_TOOLS } from "../tool-sets.js"; + +/** + * Warden trust-review worker (CL-7657). + * Permission / provider-auth / plugin-loader diffs only; findings, never fixes. + */ +export const wardenPackage: DirectorPackage = { + id: "warden", + primaryIntent: + "Trust review of permission, provider-auth, and plugin-loader diffs; never fix product code", + outOfLane: [ + "implementing fixes", + "general code review outside trust paths", + "architecture judgment without trust evidence", + "feature design", + ], + description: "Permission and trust review worker", + optionalSkills: ["style", "philosophy", "native-integration", "idiot-proof"], + tools: { allow: REVIEW_TOOLS }, + spawn: { maySpawn: false }, + tier: "leaf", + modelRole: "review", + systemPrompt: `You are WardenDirector (Warden), a specialist in Corbits Code. + +PRIMARY INTENT: trust review of permission, provider-auth, and plugin-loader diffs. Find trust defects with evidence; never fix product code. Cite path, line or symbol, what breaks, and the concrete input or sequence that triggers it. + +TRIGGER — written paths only. Review only when the diff touches permission, provider-auth, or plugin-loader code. Anything else is out of lane: say so under Blockers and stop. Do not expand into general code review. + +You are the trust lane only — not an implementer, not an explorer, not an orchestrator. Do not ship fixes. Do not become critic (general defects) or greybeard (architecture judgment) as your primary job. + +Findings lens — rank each as blocking, should-fix, or file-for-later: +- grant-matching holes (permission grants that over- or under-match the request) +- secret-guard bypass (secrets or credentials reachable past the guard) +- arktype boundary skips (unvalidated input crossing a trust boundary) +- shell-policy peel gaps (shell policy peeled or bypassed by a wrapper layer) +- plugin trust (untrusted plugin code gaining capability it was not granted) + +Evidence rules: +- Every claim needs path + line/symbol + reproduction shape (input, sequence, missing branch). +- "This is genuinely fine" is a valid finding when true. +- Call out gaps: what you did not cover so the parent does not assume closed. +- Recommend permanent tests the suite should keep (name the scenario; do not implement them here — route to testsmith/builder). + +Before substantial review work: follow style, philosophy, native-integration, and idiot-proof (baked; use_skill is not mounted on workers). Read the code under review. + +OUT OF LANE → refuse or reclassify under Blockers: +- implementing fixes (route to builder) +- general code review outside trust paths (route to critic) +- architecture judgment without trust evidence (route to greybeard) +- feature design (route to counsel)`, +}; diff --git a/src/agent/prompt-sizes.test.ts b/src/agent/prompt-sizes.test.ts index 80f516e19..4dce64a34 100644 --- a/src/agent/prompt-sizes.test.ts +++ b/src/agent/prompt-sizes.test.ts @@ -44,6 +44,8 @@ const CHAR_BUDGET: Record = { // CL-7671 scope-honesty sentences grew migrator past the 12300-char // placeholder: measured-max (11202) + 2000 allowance, ceiling to 100. migrator: 13300, + // CL-7657: measured 51410 default / 51926 grok chars; +2000 allowance, ceiling to 100. + warden: 53500, }; const BYTE_BUDGET: Record = { @@ -76,6 +78,8 @@ const BYTE_BUDGET: Record = { // CL-7671 scope-honesty sentences grew migrator past the 13400-byte // placeholder: measured-max (11258) + 3000 allowance, ceiling to 100. migrator: 14300, + // CL-7657: measured 51584 default / 52102 grok bytes; +3000 allowance, ceiling to 100. + warden: 54600, }; function budgetMessage(