Skip to content

Commit 74e3f31

Browse files
committed
Add mutation-check gauntlet leaf director
1 parent bf79cc0 commit 74e3f31

12 files changed

Lines changed: 245 additions & 14 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ Corbits Code keeps repository guidance and the closed director fleet separate:
198198
- `src/agent/directors/` — closed spawn catalog (`directorProfiles()`). Skywalker
199199
is the primary orchestrator; spawnable directors include builder, explorer,
200200
counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot, draper,
201-
emil, rand, shakespeare, testsmith, and tester. Closed ids cannot be
201+
emil, rand, shakespeare, testsmith, tester, and gauntlet. Closed ids cannot be
202202
overridden by plugins or local files.
203203
- `.agents/agents/` — optional local profile additions; this directory is not
204204
required and may be absent

‎docs/ARCHITECTURE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ Enforcement is runtime code at the existing tool-mount point, not prompt wording
244244

245245
#### Closed director fleet (`src/agent/directors/`)
246246

247-
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.
247+
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.
248248

249249
**Primary**
250250

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

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

‎docs/PRODUCT.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,14 @@ Capabilities beyond the core toolset are opt-in plugins, enabled per workspace t
155155

156156
## Multi-agent (fleet agents)
157157

158-
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.
158+
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.
159159

160160
| Lane | Directors |
161161
| --------- | -------------------------------------------------------------------------------------- |
162162
| Primary | skywalker |
163163
| Eng | builder, explorer, counsel, intern, critic, greybeard, neckbeard, bruckheimer, gaasbot |
164164
| Design | draper, emil, rand |
165-
| Docs / QA | shakespeare, testsmith, tester |
165+
| Docs / QA | shakespeare, testsmith, tester, gauntlet |
166166

167167
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.
168168

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { gauntletPackage } from "./package.js";
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { gauntletPackage } from "./package.js";
3+
4+
describe("gauntletPackage", () => {
5+
test("id matches directory / registry id", () => {
6+
expect(gauntletPackage.id).toBe("gauntlet");
7+
});
8+
9+
test("systemPrompt is non-empty and not a Placeholder", () => {
10+
expect(gauntletPackage.systemPrompt.length).toBeGreaterThan(0);
11+
expect(gauntletPackage.systemPrompt.startsWith("Placeholder")).toBe(false);
12+
});
13+
14+
test("systemPrompt identity is Gauntlet / GauntletDirector", () => {
15+
const p = gauntletPackage.systemPrompt;
16+
expect(p).toMatch(/GauntletDirector \(Gauntlet\)/);
17+
expect(p).toContain("PRIMARY INTENT");
18+
expect(p).toMatch(/mutation\/vacuity lane/i);
19+
});
20+
21+
test("systemPrompt states the mutation-check lane (break, fail, restore, pass)", () => {
22+
const p = gauntletPackage.systemPrompt;
23+
expect(p).toMatch(/mutation-check that tests can actually fail/i);
24+
expect(p).toMatch(/breaking mutation/i);
25+
expect(p).toMatch(/must FAIL/);
26+
expect(p).toMatch(/must PASS/);
27+
expect(p).toMatch(/byte-identical/);
28+
expect(p).toMatch(/tree clean/);
29+
});
30+
31+
test("systemPrompt never leaves a breaking edit in the tree", () => {
32+
const p = gauntletPackage.systemPrompt;
33+
expect(p).toMatch(/never leave a breaking edit in the tree/i);
34+
expect(p).toMatch(/git status/);
35+
expect(p).toMatch(/git diff/);
36+
});
37+
38+
test("systemPrompt names the vacuous-test verdict", () => {
39+
const p = gauntletPackage.systemPrompt;
40+
expect(p).toMatch(/vacuous/);
41+
expect(p).toMatch(/passes under mutation/i);
42+
});
43+
44+
test("systemPrompt does not replace tester or testsmith", () => {
45+
const p = gauntletPackage.systemPrompt;
46+
expect(p).toMatch(/not Tester, not Testsmith/i);
47+
expect(p).toMatch(/do not replace tester/i);
48+
expect(p).toMatch(/route to tester/);
49+
expect(p).toMatch(/route to[\s\S]*testsmith/i);
50+
});
51+
52+
test("systemPrompt states Corbits report shape and done gate", () => {
53+
const p = gauntletPackage.systemPrompt;
54+
expect(p).toContain("## Summary");
55+
expect(p).toContain("## Findings");
56+
expect(p).toContain("## Blockers");
57+
expect(p).toContain("## Paths");
58+
expect(p).toMatch(/DONE GATE/i);
59+
expect(p).toMatch(/BLINDERS ON/i);
60+
expect(p).toContain("success_criteria");
61+
});
62+
63+
test("systemPrompt has no tool-schema restatement or fake caps", () => {
64+
const p = gauntletPackage.systemPrompt;
65+
expect(p).not.toMatch(/parameters?:/i);
66+
expect(p).not.toMatch(/fan-out/i);
67+
expect(p).not.toMatch(/at most \d+/i);
68+
expect(p).not.toMatch(/turn budget/i);
69+
expect(p).not.toMatch(/scheduler/i);
70+
});
71+
72+
test("spawn.maySpawn is false (leaf)", () => {
73+
expect(gauntletPackage.spawn.maySpawn).toBe(false);
74+
});
75+
76+
test("tier is leaf", () => {
77+
expect(gauntletPackage.tier).toBe("leaf");
78+
});
79+
80+
test("tools.allow mounts the review surface (lane discipline in prompt)", () => {
81+
const allow = gauntletPackage.tools?.allow ?? [];
82+
expect(allow).toContain("read_file");
83+
expect(allow).toContain("run_shell");
84+
expect(allow).toContain("write_file");
85+
expect(allow).toContain("edit_file");
86+
expect(allow).toContain("delete_file");
87+
});
88+
89+
test("modelRole is test", () => {
90+
expect(gauntletPackage.modelRole).toBe("test");
91+
});
92+
93+
test("primaryIntent and outOfLane match the gauntlet lane", () => {
94+
expect(gauntletPackage.primaryIntent).toMatch(/mutation-check/i);
95+
expect(gauntletPackage.primaryIntent).toMatch(
96+
/never leave a breaking edit/i,
97+
);
98+
const joined = gauntletPackage.outOfLane.join(" ");
99+
expect(joined).toMatch(/shipping product code/i);
100+
expect(joined).toMatch(/designing test cases/i);
101+
expect(joined).toMatch(/full suite as a pass\/fail gate/i);
102+
});
103+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { DirectorPackage } from "../types.js";
2+
import { REVIEW_TOOLS } from "../tool-sets.js";
3+
4+
/**
5+
* Gauntlet worker (CL-7658).
6+
* Mutation/vacuity check only — proves named tests can actually fail by
7+
* applying one temporary breaking mutation, running the named test (must
8+
* fail), restoring the tree byte-identical, and re-running (must pass).
9+
* Never leaves a breaking edit in the tree; never ships product code.
10+
*/
11+
export const gauntletPackage: DirectorPackage = {
12+
id: "gauntlet",
13+
primaryIntent:
14+
"Mutation-check that tests can actually fail: break, fail, restore, pass; never leave a breaking edit in the tree",
15+
outOfLane: [
16+
"shipping product code",
17+
"designing test cases",
18+
"running the full suite as a pass/fail gate",
19+
"fleet orchestration",
20+
"architecture judgment without a mutation run",
21+
],
22+
description: "Mutation/vacuity check that named tests can actually fail",
23+
systemPrompt: `You are GauntletDirector (Gauntlet), a specialist in Corbits Code.
24+
25+
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.
26+
27+
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.
28+
29+
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.
30+
31+
# Protocol (in order, no shortcuts)
32+
33+
1. Read the named test and the code it covers. Pick ONE minimal breaking
34+
mutation (flip a condition, drop a branch, off-by-one) that the test
35+
should catch.
36+
2. Apply the mutation with edit_file. Record the exact file, symbol, and
37+
mutation so the restore is exact.
38+
3. Run the named test with run_shell (foreground, with a timeout — never
39+
background). It must FAIL. A pass under mutation means the test is
40+
vacuous: stop, restore immediately, and report the vacuous test as the
41+
finding.
42+
4. Restore the mutation exactly (edit_file back, or git checkout the file
43+
when the mutation is the only change). Verify with git status / git diff:
44+
the tree must be byte-identical to before the run.
45+
5. Re-run the named test. It must PASS on the clean tree.
46+
6. Final verify: git status clean of mutation residue. If restore fails for
47+
any reason, keep restoring until clean and report the struggle under
48+
Blockers — a breaking edit left in the tree is the one unforgivable
49+
outcome of this lane.
50+
51+
# Rules
52+
53+
- run_shell is for the named suite command only, foreground with timeouts.
54+
- Never leave a breaking edit in the tree, not even briefly past the run.
55+
- Findings are verdicts (vacuous or guarded), never fixes — route follow-ups
56+
to builder (product fix) or testsmith (stronger cases).
57+
- If the brief asks for anything other than a mutation/vacuity check, say so
58+
under Blockers and stop. If product would rather hang this lane off a
59+
restored Critic, say so under Blockers and stop.
60+
61+
# Corbits report shape
62+
63+
When done, stop tooling and reply with ONLY this envelope:
64+
65+
## Summary
66+
One or two sentences: the named test, the mutation, and the verdict
67+
(guarded or vacuous).
68+
69+
## Findings
70+
The mutation (file, symbol, exact change), the fail-under-mutation output,
71+
the pass-after-restore output, and follow-ups for builder/testsmith.
72+
73+
## Blockers
74+
Open questions, restore struggles, or assumptions. Write "None." if clear.
75+
76+
## Paths
77+
Files you mutated, tests you ran, and outputs you produced (one per line).
78+
Write "None." if none.
79+
80+
DONE GATE: stop when the named test has failed under mutation AND passed
81+
after restore with the tree clean, OR when a vacuous test is restored-clean
82+
and reported. Do not expand into fixes, new cases, suite gates, or
83+
orchestration.
84+
85+
OUT OF LANE: shipping product code, designing test cases (route to
86+
testsmith), running the full suite as a gate (route to tester), fleet
87+
orchestration, architecture judgment without a mutation run.`,
88+
tools: { allow: REVIEW_TOOLS },
89+
spawn: { maySpawn: false },
90+
tier: "leaf",
91+
modelRole: "test",
92+
};

‎src/agent/directors/registry.test.ts‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import {
1313
} from "./registry.js";
1414

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

117117
test("directorProfiles is the spawn catalog (closed set minus skywalker)", () => {
118118
const profiles = directorProfiles();
119-
expect(profiles).toHaveLength(15);
120-
expect(new Set(profiles.map((p) => p.id)).size).toBe(15);
119+
expect(profiles).toHaveLength(16);
120+
expect(new Set(profiles.map((p) => p.id)).size).toBe(16);
121121
expect(profiles.map((p) => p.id)).not.toContain("skywalker");
122122
});
123123

@@ -151,6 +151,7 @@ describe("director registry", () => {
151151
"bruckheimer",
152152
"rand",
153153
"skywalker",
154+
"gauntlet",
154155
] as const) {
155156
const allow = DIRECTOR_REGISTRY[id].tools?.allow ?? [];
156157
expect(allow).toContain("write_file");
@@ -194,11 +195,11 @@ describe("director registry", () => {
194195
expect(s.tools?.allow).toContain("write_file");
195196
expect(s.tools?.allow).toContain("edit_file");
196197
expect(s.tools?.allow).toContain("delete_file");
197-
expect(s.spawn.allowlist).toHaveLength(15);
198+
expect(s.spawn.allowlist).toHaveLength(16);
198199
});
199200

200201
// CL-6941: tier and spawn.maySpawn independently encode "may this package
201-
// spawn", hand-set across 16 files. This pins their agreement so drift
202+
// spawn", hand-set across 17 files. This pins their agreement so drift
202203
// (adding maySpawn: true without bumping tier, or vice versa) fails a test
203204
// instead of surfacing as an unexplained FleetAuthorityError at dispatch.
204205
test("tier agrees with spawn.maySpawn for every director", () => {
@@ -211,6 +212,20 @@ describe("director registry", () => {
211212
expect(DIRECTOR_REGISTRY.greybeard.tier).toBe("nested-orchestrator");
212213
});
213214

215+
test("gauntlet is a mutation-check leaf (CL-7658)", () => {
216+
const r = resolveDirector({ agentId: "gauntlet" });
217+
expect(r.ok).toBe(true);
218+
if (r.ok) {
219+
expect(r.package.id).toBe("gauntlet");
220+
expect(r.package.tier).toBe("leaf");
221+
expect(r.package.spawn.maySpawn).toBe(false);
222+
expect(r.package.modelRole).toBe("test");
223+
expect(r.package.primaryIntent).toMatch(/mutation-check/i);
224+
}
225+
expect(isDirectorId("gauntlet")).toBe(true);
226+
expect(tierForDirectorId("gauntlet")).toBe("leaf");
227+
});
228+
214229
test("every director profile declares matching agent id in system prompt", () => {
215230
for (const id of DIRECTOR_IDS) {
216231
const profile = packageToProfile(DIRECTOR_REGISTRY[id]);

‎src/agent/directors/registry.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { shakespearePackage } from "./shakespeare/index.js";
1515
import { skywalkerPackage } from "./skywalker/index.js";
1616
import { testerPackage } from "./tester/index.js";
1717
import { testsmithPackage } from "./testsmith/index.js";
18+
import { gauntletPackage } from "./gauntlet/index.js";
1819
import { formatDirectorSystemPrompt } from "./identity.js";
1920
import {
2021
DIRECTOR_IDS,
@@ -58,6 +59,7 @@ export const DIRECTOR_REGISTRY: Readonly<Record<DirectorId, DirectorPackage>> =
5859
shakespeare: shakespearePackage,
5960
testsmith: testsmithPackage,
6061
tester: testerPackage,
62+
gauntlet: gauntletPackage,
6163
};
6264

6365
export function isDirectorId(value: unknown): value is DirectorId {

‎src/agent/directors/skywalker/package.test.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe("skywalkerPackage", () => {
2828

2929
test("maySpawn true with full closed allowlist", () => {
3030
expect(skywalkerPackage.spawn.maySpawn).toBe(true);
31-
expect(skywalkerPackage.spawn.allowlist).toHaveLength(15);
31+
expect(skywalkerPackage.spawn.allowlist).toHaveLength(16);
3232
expect(skywalkerPackage.spawn.allowlist).toEqual([
3333
"builder",
3434
"explorer",
@@ -45,6 +45,7 @@ describe("skywalkerPackage", () => {
4545
"shakespeare",
4646
"testsmith",
4747
"tester",
48+
"gauntlet",
4849
]);
4950
});
5051

@@ -316,4 +317,11 @@ describe("skywalkerPackage", () => {
316317
expect(p).not.toContain("gh pr create");
317318
expect(p).not.toContain("gh pr review");
318319
});
320+
321+
test("systemPrompt routes mutation-checks to gauntlet (tiny verify-after-ship mention)", () => {
322+
const p = skywalkerPackage.systemPrompt;
323+
expect(p).toContain(
324+
"gauntlet = mutation-check that tests can actually fail (tree clean)",
325+
);
326+
});
319327
});

0 commit comments

Comments
 (0)