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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, and tester. Closed ids cannot be
emil, rand, shakespeare, testsmith, tester, and gauntlet. 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
Expand Down
3 changes: 2 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 16 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 17 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**

Expand Down Expand Up @@ -281,6 +281,7 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP
| shakespeare | Docs maintain (scribe core baked into prompt); PRODUCT/ARCHITECTURE/IMPLEMENTATION lane |
| testsmith | Test design only (what/how to test) |
| tester | Runtime verification; never fix product code |
| gauntlet | Mutation/vacuity check that named tests can actually fail |

**Intent → director** (`spawn_agent(intent=…)` when `agent` is omitted). implement/review (and their default directors) fail closed without non-empty `success_criteria`.

Expand Down
4 changes: 2 additions & 2 deletions docs/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 16 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`) tell Skywalker which directors to spawn for substantial work; tiny/bounded edits may run on the primary.
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 17 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`) tell Skywalker which directors to spawn for substantial work; tiny/bounded edits may run on the primary.

| Lane | Directors |
| --------- | -------------------------------------------------------------------------------------- |
| Primary | skywalker |
| Eng | builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot |
| Design | draper, emil, rand |
| Docs / QA | shakespeare, testsmith, tester |
| Docs / QA | shakespeare, testsmith, tester, gauntlet |

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.

Expand Down
1 change: 1 addition & 0 deletions src/agent/directors/gauntlet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { gauntletPackage } from "./package.js";
107 changes: 107 additions & 0 deletions src/agent/directors/gauntlet/package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, expect, test } from "bun:test";
import { gauntletPackage } from "./package.js";

describe("gauntletPackage", () => {
test("id matches directory / registry id", () => {
expect(gauntletPackage.id).toBe("gauntlet");
});

test("systemPrompt is non-empty and not a Placeholder", () => {
expect(gauntletPackage.systemPrompt.length).toBeGreaterThan(0);
expect(gauntletPackage.systemPrompt.startsWith("Placeholder")).toBe(false);
});

test("systemPrompt identity is Gauntlet / GauntletDirector", () => {
const p = gauntletPackage.systemPrompt;
expect(p).toMatch(/GauntletDirector \(Gauntlet\)/);
expect(p).toContain("PRIMARY INTENT");
expect(p).toMatch(/mutation\/vacuity lane/i);
});

test("systemPrompt states the mutation-check lane (break, fail, restore, pass)", () => {
const p = gauntletPackage.systemPrompt;
expect(p).toMatch(/mutation-check that tests can actually fail/i);
expect(p).toMatch(/breaking mutation/i);
expect(p).toMatch(/must FAIL/);
expect(p).toMatch(/must PASS/);
expect(p).toMatch(/byte-identical/);
expect(p).toMatch(/tree clean/);
});

test("systemPrompt never leaves a breaking edit in the tree", () => {
const p = gauntletPackage.systemPrompt;
expect(p).toMatch(/never leave a breaking edit in the tree/i);
expect(p).toMatch(/git status/);
expect(p).toMatch(/git diff/);
});

test("systemPrompt names the vacuous-test verdict", () => {
const p = gauntletPackage.systemPrompt;
expect(p).toMatch(/vacuous/);
expect(p).toMatch(/passes under mutation/i);
});

test("systemPrompt does not replace tester or testsmith", () => {
const p = gauntletPackage.systemPrompt;
expect(p).toMatch(/not Tester, not Testsmith/i);
expect(p).toMatch(/do not replace tester/i);
expect(p).toMatch(/route to tester/);
expect(p).toMatch(/route to[\s\S]*testsmith/i);
});

test("systemPrompt points at the scaffold-owned worker report envelope (no re-spec)", () => {
const p = gauntletPackage.systemPrompt;
expect(p).toMatch(/Corbits report envelope/);
expect(p).toMatch(/scaffold owns its shape/i);
expect(p).not.toContain("## Summary");
expect(p).not.toContain("## Findings");
expect(p).not.toContain("## Blockers");
expect(p).not.toContain("## Paths");
expect(p).toMatch(/fail-under-mutation output/i);
expect(p).toMatch(/pass-after-restore output/i);
expect(p).toMatch(/DONE GATE/i);
expect(p).toMatch(/BLINDERS ON/i);
expect(p).toContain("success_criteria");
});

test("systemPrompt has no tool-schema restatement or fake caps", () => {
const p = gauntletPackage.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);
});

test("spawn.maySpawn is false (leaf)", () => {
expect(gauntletPackage.spawn.maySpawn).toBe(false);
});

test("tier is leaf", () => {
expect(gauntletPackage.tier).toBe("leaf");
});

test("tools.allow mounts the review surface (lane discipline in prompt)", () => {
const allow = gauntletPackage.tools?.allow ?? [];
expect(allow).toContain("read_file");
expect(allow).toContain("run_shell");
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
});

test("modelRole is test", () => {
expect(gauntletPackage.modelRole).toBe("test");
});

test("primaryIntent and outOfLane match the gauntlet lane", () => {
expect(gauntletPackage.primaryIntent).toMatch(/mutation-check/i);
expect(gauntletPackage.primaryIntent).toMatch(
/never leave a breaking edit/i,
);
const joined = gauntletPackage.outOfLane.join(" ");
expect(joined).toMatch(/shipping product code/i);
expect(joined).toMatch(/designing test cases/i);
expect(joined).toMatch(/full suite as a pass\/fail gate/i);
});
});
77 changes: 77 additions & 0 deletions src/agent/directors/gauntlet/package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { DirectorPackage } from "../types.js";
import { REVIEW_TOOLS } from "../tool-sets.js";

/**
* Gauntlet worker (CL-7658).
* Mutation/vacuity check only — proves named tests can actually fail by
* applying one temporary breaking mutation, running the named test (must
* fail), restoring the tree byte-identical, and re-running (must pass).
* Never leaves a breaking edit in the tree; never ships product code.
*/
export const gauntletPackage: DirectorPackage = {
id: "gauntlet",
primaryIntent:
"Mutation-check that tests can actually fail: break, fail, restore, pass; never leave a breaking edit in the tree",
outOfLane: [
"shipping product code",
"designing test cases",
"running the full suite as a pass/fail gate",
"fleet orchestration",
"architecture judgment without a mutation run",
],
description: "Mutation/vacuity check that named tests can actually fail",
systemPrompt: `You are GauntletDirector (Gauntlet), a specialist in Corbits Code.

PRIMARY INTENT: mutation-check that tests can actually fail. Apply one temporary breaking mutation, run the named test (it must FAIL), restore the tree byte-identical, re-run the named test (it must PASS), and leave the tree clean. A test that passes under mutation is vacuous — report it, do not fix product code to satisfy it.

You are the mutation/vacuity lane only — not Tester, not Testsmith, not an orchestrator. You do not replace tester (runs the suite / repro as a gate) or testsmith (designs permanent test cases). Do not spawn specialists. Do not ship product code, do not design new test cases, do not run the full suite as a gate.

BLINDERS ON: check what the brief's success_criteria name, nothing else. One named test and one minimal breaking mutation per run unless the brief names more.

# Protocol (in order, no shortcuts)

1. Read the named test and the code it covers. Pick ONE minimal breaking
mutation (flip a condition, drop a branch, off-by-one) that the test
should catch.
2. Apply the mutation with edit_file. Record the exact file, symbol, and
mutation so the restore is exact.
3. Run the named test with run_shell (foreground, with a timeout — never
background). It must FAIL. A pass under mutation means the test is
vacuous: stop, restore immediately, and report the vacuous test as the
finding.
4. Restore the mutation exactly (edit_file back, or git checkout the file
when the mutation is the only change). Verify with git status / git diff:
the tree must be byte-identical to before the run.
5. Re-run the named test. It must PASS on the clean tree.
6. Final verify: git status clean of mutation residue. If restore fails for
any reason, keep restoring until clean and report the struggle under
Blockers — a breaking edit left in the tree is the one unforgivable
outcome of this lane.

# Rules

- run_shell is for the named suite command only, foreground with timeouts.
- Never leave a breaking edit in the tree, not even briefly past the run.
- Findings are verdicts (vacuous or guarded), never fixes — route follow-ups
to builder (product fix) or testsmith (stronger cases).
- If the brief asks for anything other than a mutation/vacuity check, say so
under Blockers and stop. If product would rather hang this lane off a
restored Critic, say so under Blockers and stop.

# Report

When done, stop tooling and reply with ONLY the Corbits report envelope — the shared scaffold owns its shape (Summary / Findings / Blockers / Paths, in that order), so this package does not re-specify it. Findings for this lane: the mutation (file, symbol, exact change), the fail-under-mutation output, the pass-after-restore output, and follow-ups for builder/testsmith.

DONE GATE: stop when the named test has failed under mutation AND passed
after restore with the tree clean, OR when a vacuous test is restored-clean
and reported. Do not expand into fixes, new cases, suite gates, or
orchestration.

OUT OF LANE: shipping product code, designing test cases (route to
testsmith), running the full suite as a gate (route to tester), fleet
orchestration, architecture judgment without a mutation run.`,
tools: { allow: REVIEW_TOOLS },
spawn: { maySpawn: false },
tier: "leaf",
modelRole: "test",
};
29 changes: 22 additions & 7 deletions src/agent/directors/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {
} from "./registry.js";

describe("director registry", () => {
test("closed set has exactly 16 directors", () => {
expect(DIRECTOR_IDS).toHaveLength(16);
expect(listDirectors()).toHaveLength(16);
test("closed set has exactly 17 directors", () => {
expect(DIRECTOR_IDS).toHaveLength(17);
expect(listDirectors()).toHaveLength(17);
for (const id of DIRECTOR_IDS) {
expect(DIRECTOR_REGISTRY[id].id).toBe(id);
}
Expand Down Expand Up @@ -116,8 +116,8 @@ describe("director registry", () => {

test("directorProfiles is the spawn catalog (closed set minus skywalker)", () => {
const profiles = directorProfiles();
expect(profiles).toHaveLength(15);
expect(new Set(profiles.map((p) => p.id)).size).toBe(15);
expect(profiles).toHaveLength(16);
expect(new Set(profiles.map((p) => p.id)).size).toBe(16);
expect(profiles.map((p) => p.id)).not.toContain("skywalker");
});

Expand Down Expand Up @@ -151,6 +151,7 @@ describe("director registry", () => {
"bruckheimer",
"rand",
"skywalker",
"gauntlet",
] as const) {
const allow = DIRECTOR_REGISTRY[id].tools?.allow ?? [];
expect(allow).toContain("write_file");
Expand Down Expand Up @@ -194,11 +195,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(15);
expect(s.spawn.allowlist).toHaveLength(16);
});

// CL-6941: tier and spawn.maySpawn independently encode "may this package
// spawn", hand-set across 16 files. This pins their agreement so drift
// spawn", hand-set across 17 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", () => {
Expand All @@ -211,6 +212,20 @@ describe("director registry", () => {
expect(DIRECTOR_REGISTRY.greybeard.tier).toBe("nested-orchestrator");
});

test("gauntlet is a mutation-check leaf (CL-7658)", () => {
const r = resolveDirector({ agentId: "gauntlet" });
expect(r.ok).toBe(true);
if (r.ok) {
expect(r.package.id).toBe("gauntlet");
expect(r.package.tier).toBe("leaf");
expect(r.package.spawn.maySpawn).toBe(false);
expect(r.package.modelRole).toBe("test");
expect(r.package.primaryIntent).toMatch(/mutation-check/i);
}
expect(isDirectorId("gauntlet")).toBe(true);
expect(tierForDirectorId("gauntlet")).toBe("leaf");
});

test("every director profile declares matching agent id in system prompt", () => {
for (const id of DIRECTOR_IDS) {
const profile = packageToProfile(DIRECTOR_REGISTRY[id]);
Expand Down
2 changes: 2 additions & 0 deletions src/agent/directors/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { shakespearePackage } from "./shakespeare/index.js";
import { skywalkerPackage } from "./skywalker/index.js";
import { testerPackage } from "./tester/index.js";
import { testsmithPackage } from "./testsmith/index.js";
import { gauntletPackage } from "./gauntlet/index.js";
import { formatDirectorSystemPrompt } from "./identity.js";
import {
DIRECTOR_IDS,
Expand Down Expand Up @@ -58,6 +59,7 @@ export const DIRECTOR_REGISTRY: Readonly<Record<DirectorId, DirectorPackage>> =
shakespeare: shakespearePackage,
testsmith: testsmithPackage,
tester: testerPackage,
gauntlet: gauntletPackage,
};

export function isDirectorId(value: unknown): value is DirectorId {
Expand Down
10 changes: 9 additions & 1 deletion src/agent/directors/skywalker/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("skywalkerPackage", () => {

test("maySpawn true with full closed allowlist", () => {
expect(skywalkerPackage.spawn.maySpawn).toBe(true);
expect(skywalkerPackage.spawn.allowlist).toHaveLength(15);
expect(skywalkerPackage.spawn.allowlist).toHaveLength(16);
expect(skywalkerPackage.spawn.allowlist).toEqual([
"builder",
"explorer",
Expand All @@ -45,6 +45,7 @@ describe("skywalkerPackage", () => {
"shakespeare",
"testsmith",
"tester",
"gauntlet",
]);
});

Expand Down Expand Up @@ -316,4 +317,11 @@ describe("skywalkerPackage", () => {
expect(p).not.toContain("gh pr create");
expect(p).not.toContain("gh pr review");
});

test("systemPrompt routes mutation-checks to gauntlet (tiny verify-after-ship mention)", () => {
const p = skywalkerPackage.systemPrompt;
expect(p).toContain(
"gauntlet = mutation-check that tests can actually fail (tree clean)",
);
});
});
Loading
Loading