diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index 02457e292..3ff306728 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -681,7 +681,7 @@ Agent names are drawn from a single fictional universe per assignment. Names are **Rules (always loaded):** - ONE UNIVERSE PER ASSIGNMENT. NEVER MIX. -- 31 universes available (capacity 6–25). See reference file for full list. +- 15 universes available (capacity 6–25). See reference file for full list. - Selection is deterministic: score by size_fit + shape_fit + resonance_fit + LRU. - Same inputs → same choice (unless LRU changes). diff --git a/.squad-templates/casting-policy.json b/.squad-templates/casting-policy.json index 281d75700..12a57cca8 100644 --- a/.squad-templates/casting-policy.json +++ b/.squad-templates/casting-policy.json @@ -15,7 +15,7 @@ "Lost", "Marvel Cinematic Universe", "DC Universe", - "Futurama (custom)" + "Futurama" ], "universe_capacity": { "The Usual Suspects": 6, @@ -32,6 +32,6 @@ "Lost": 18, "Marvel Cinematic Universe": 25, "DC Universe": 18, - "Futurama (custom)": 8 + "Futurama": 12 } } diff --git a/.squad-templates/casting-reference.md b/.squad-templates/casting-reference.md new file mode 100644 index 000000000..f0a72e094 --- /dev/null +++ b/.squad-templates/casting-reference.md @@ -0,0 +1,104 @@ +# Casting Reference + +On-demand reference for Squad's casting system. Loaded during Init Mode or when adding team members. + +## Universe Table + +| Universe | Capacity | Shape Tags | Resonance Signals | +|---|---|---|---| +| The Usual Suspects | 6 | small, noir, ensemble | crime, heist, mystery, deception | +| Reservoir Dogs | 8 | small, noir, ensemble | crime, heist, tension, loyalty | +| Alien | 8 | small, sci-fi, survival | space, isolation, threat, engineering | +| Ocean's Eleven | 14 | medium, heist, ensemble | planning, coordination, roles, charm | +| Arrested Development | 15 | medium, comedy, ensemble | dysfunction, business, family, satire | +| Star Wars | 12 | medium, sci-fi, epic | conflict, mentorship, legacy, rebellion | +| The Matrix | 10 | medium, sci-fi, cyberpunk | systems, reality, hacking, philosophy | +| Firefly | 10 | medium, sci-fi, western | frontier, crew, independence, smuggling | +| The Goonies | 8 | small, adventure, ensemble | exploration, treasure, kids, teamwork | +| The Simpsons | 20 | large, comedy, ensemble | satire, community, family, absurdity | +| Breaking Bad | 12 | medium, drama, tension | chemistry, transformation, consequence, power | +| Lost | 18 | large, mystery, ensemble | survival, mystery, groups, leadership | +| Marvel Cinematic Universe | 25 | large, action, ensemble | heroism, teamwork, powers, scale | +| DC Universe | 18 | large, action, ensemble | justice, duality, powers, mythology | +| Futurama | 12 | medium, sci-fi, comedy | future, robots, space, absurdity | + +**Total: 15 universes** — capacity range 6–25. + +## Selection Algorithm + +Universe selection is deterministic. Score each universe and pick the highest: + +``` +score = size_fit + shape_fit + resonance_fit + LRU +``` + +| Factor | Description | +|---|---| +| `size_fit` | How well the universe capacity matches the team size. Prefer universes where capacity ≥ agent_count with minimal waste. | +| `shape_fit` | Match universe shape tags against the assignment shape derived from the project description. | +| `resonance_fit` | Match universe resonance signals against session and repo context signals. | +| `LRU` | Least-recently-used bonus — prefer universes not used in recent assignments (from `history.json`). | + +Same inputs → same choice (unless LRU changes between assignments). + +## Casting State File Schemas + +### policy.json + +Source template: `.squad/templates/casting-policy.json` +Runtime location: `.squad/casting/policy.json` + +```json +{ + "casting_policy_version": "1.1", + "allowlist_universes": ["Universe Name", "..."], + "universe_capacity": { + "Universe Name": 10 + } +} +``` + +### registry.json + +Source template: `.squad/templates/casting-registry.json` +Runtime location: `.squad/casting/registry.json` + +```json +{ + "agents": { + "agent-role-id": { + "persistent_name": "CharacterName", + "universe": "Universe Name", + "created_at": "ISO-8601", + "legacy_named": false, + "status": "active" + } + } +} +``` + +### history.json + +Source template: `.squad/templates/casting-history.json` +Runtime location: `.squad/casting/history.json` + +```json +{ + "universe_usage_history": [ + { + "universe": "Universe Name", + "assignment_id": "unique-id", + "used_at": "ISO-8601" + } + ], + "assignment_cast_snapshots": { + "assignment-id": { + "universe": "Universe Name", + "agents": { + "role-id": "CharacterName" + }, + "created_at": "ISO-8601" + } + } +} +``` diff --git a/.squad-templates/squad.agent.md b/.squad-templates/squad.agent.md index fff5de0e3..e0957fd27 100644 --- a/.squad-templates/squad.agent.md +++ b/.squad-templates/squad.agent.md @@ -817,7 +817,7 @@ Agent names are drawn from a single fictional universe per assignment. Names are **Rules (always loaded):** - ONE UNIVERSE PER ASSIGNMENT. NEVER MIX. -- 31 universes available (capacity 6–25). See reference file for full list. +- 15 universes available (capacity 6–25). See reference file for full list. - Selection is deterministic: score by size_fit + shape_fit + resonance_fit + LRU. - Same inputs → same choice (unless LRU changes). diff --git a/templates/casting-policy.json b/templates/casting-policy.json index b3858c78d..12a57cca8 100644 --- a/templates/casting-policy.json +++ b/templates/casting-policy.json @@ -14,7 +14,8 @@ "Breaking Bad", "Lost", "Marvel Cinematic Universe", - "DC Universe" + "DC Universe", + "Futurama" ], "universe_capacity": { "The Usual Suspects": 6, @@ -30,6 +31,7 @@ "Breaking Bad": 12, "Lost": 18, "Marvel Cinematic Universe": 25, - "DC Universe": 18 + "DC Universe": 18, + "Futurama": 12 } } diff --git a/templates/casting-reference.md b/templates/casting-reference.md new file mode 100644 index 000000000..f0a72e094 --- /dev/null +++ b/templates/casting-reference.md @@ -0,0 +1,104 @@ +# Casting Reference + +On-demand reference for Squad's casting system. Loaded during Init Mode or when adding team members. + +## Universe Table + +| Universe | Capacity | Shape Tags | Resonance Signals | +|---|---|---|---| +| The Usual Suspects | 6 | small, noir, ensemble | crime, heist, mystery, deception | +| Reservoir Dogs | 8 | small, noir, ensemble | crime, heist, tension, loyalty | +| Alien | 8 | small, sci-fi, survival | space, isolation, threat, engineering | +| Ocean's Eleven | 14 | medium, heist, ensemble | planning, coordination, roles, charm | +| Arrested Development | 15 | medium, comedy, ensemble | dysfunction, business, family, satire | +| Star Wars | 12 | medium, sci-fi, epic | conflict, mentorship, legacy, rebellion | +| The Matrix | 10 | medium, sci-fi, cyberpunk | systems, reality, hacking, philosophy | +| Firefly | 10 | medium, sci-fi, western | frontier, crew, independence, smuggling | +| The Goonies | 8 | small, adventure, ensemble | exploration, treasure, kids, teamwork | +| The Simpsons | 20 | large, comedy, ensemble | satire, community, family, absurdity | +| Breaking Bad | 12 | medium, drama, tension | chemistry, transformation, consequence, power | +| Lost | 18 | large, mystery, ensemble | survival, mystery, groups, leadership | +| Marvel Cinematic Universe | 25 | large, action, ensemble | heroism, teamwork, powers, scale | +| DC Universe | 18 | large, action, ensemble | justice, duality, powers, mythology | +| Futurama | 12 | medium, sci-fi, comedy | future, robots, space, absurdity | + +**Total: 15 universes** — capacity range 6–25. + +## Selection Algorithm + +Universe selection is deterministic. Score each universe and pick the highest: + +``` +score = size_fit + shape_fit + resonance_fit + LRU +``` + +| Factor | Description | +|---|---| +| `size_fit` | How well the universe capacity matches the team size. Prefer universes where capacity ≥ agent_count with minimal waste. | +| `shape_fit` | Match universe shape tags against the assignment shape derived from the project description. | +| `resonance_fit` | Match universe resonance signals against session and repo context signals. | +| `LRU` | Least-recently-used bonus — prefer universes not used in recent assignments (from `history.json`). | + +Same inputs → same choice (unless LRU changes between assignments). + +## Casting State File Schemas + +### policy.json + +Source template: `.squad/templates/casting-policy.json` +Runtime location: `.squad/casting/policy.json` + +```json +{ + "casting_policy_version": "1.1", + "allowlist_universes": ["Universe Name", "..."], + "universe_capacity": { + "Universe Name": 10 + } +} +``` + +### registry.json + +Source template: `.squad/templates/casting-registry.json` +Runtime location: `.squad/casting/registry.json` + +```json +{ + "agents": { + "agent-role-id": { + "persistent_name": "CharacterName", + "universe": "Universe Name", + "created_at": "ISO-8601", + "legacy_named": false, + "status": "active" + } + } +} +``` + +### history.json + +Source template: `.squad/templates/casting-history.json` +Runtime location: `.squad/casting/history.json` + +```json +{ + "universe_usage_history": [ + { + "universe": "Universe Name", + "assignment_id": "unique-id", + "used_at": "ISO-8601" + } + ], + "assignment_cast_snapshots": { + "assignment-id": { + "universe": "Universe Name", + "agents": { + "role-id": "CharacterName" + }, + "created_at": "ISO-8601" + } + } +} +``` diff --git a/templates/squad.agent.md b/templates/squad.agent.md index 0bbc779c4..9378ce969 100644 --- a/templates/squad.agent.md +++ b/templates/squad.agent.md @@ -832,7 +832,7 @@ Agent names are drawn from a single fictional universe per assignment. Names are **Rules (always loaded):** - ONE UNIVERSE PER ASSIGNMENT. NEVER MIX. -- 31 universes available (capacity 6–25). See reference file for full list. +- 15 universes available (capacity 6–25). See reference file for full list. - Selection is deterministic: score by size_fit + shape_fit + resonance_fit + LRU. - Same inputs → same choice (unless LRU changes). diff --git a/test/template-sync.test.ts b/test/template-sync.test.ts new file mode 100644 index 000000000..9e66048ae --- /dev/null +++ b/test/template-sync.test.ts @@ -0,0 +1,104 @@ +/** + * Template Sync & Casting Parity Tests (Issue #459) + * + * Prevents the casting universe mismatch from recurring by verifying: + * - Universe count in squad.agent.md matches casting-policy.json + * - All 3 copies of squad.agent.md agree on universe count + * - Referenced casting-reference.md files exist + * - casting-policy.json is identical across template dirs + * - Every allowlisted universe has a capacity entry and vice versa + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; + +const ROOT = process.cwd(); + +const SQUAD_TEMPLATES = join(ROOT, '.squad-templates'); +const TEMPLATES = join(ROOT, 'templates'); +const GITHUB_AGENTS = join(ROOT, '.github', 'agents'); + +/** Extract the "N universes available" number from a squad.agent.md file. */ +function extractUniverseCount(filePath: string): number { + const content = readFileSync(filePath, 'utf-8'); + const match = content.match(/(\d+)\s+universes?\s+available/i); + if (!match) throw new Error(`No "N universes available" found in ${filePath}`); + return parseInt(match[1], 10); +} + +describe('Template sync & casting parity (issue #459)', () => { + // --- Test 1: Universe count in squad.agent.md matches casting-policy.json --- + + describe('universe count matches casting-policy.json', () => { + const policy = JSON.parse(readFileSync(join(SQUAD_TEMPLATES, 'casting-policy.json'), 'utf-8')); + const policyCount = policy.allowlist_universes.length; + + it('.squad-templates/squad.agent.md matches policy', () => { + const claimed = extractUniverseCount(join(SQUAD_TEMPLATES, 'squad.agent.md')); + expect(claimed).toBe(policyCount); + }); + + it('templates/squad.agent.md matches policy', () => { + const claimed = extractUniverseCount(join(TEMPLATES, 'squad.agent.md')); + expect(claimed).toBe(policyCount); + }); + + it('.github/agents/squad.agent.md matches policy', () => { + const claimed = extractUniverseCount(join(GITHUB_AGENTS, 'squad.agent.md')); + expect(claimed).toBe(policyCount); + }); + }); + + // --- Test 2: All 3 copies agree on universe count --- + + it('all 3 squad.agent.md copies claim the same universe count', () => { + const counts = [ + extractUniverseCount(join(SQUAD_TEMPLATES, 'squad.agent.md')), + extractUniverseCount(join(TEMPLATES, 'squad.agent.md')), + extractUniverseCount(join(GITHUB_AGENTS, 'squad.agent.md')), + ]; + expect(counts[0]).toBe(counts[1]); + expect(counts[1]).toBe(counts[2]); + }); + + // --- Test 3: casting-reference.md exists where referenced --- + + describe('casting-reference.md exists in template dirs', () => { + // casting-reference.md is a template-only file that gets copied to .squad/templates/ + // during squad init. It belongs in .squad-templates/ and templates/ (both template dirs), + // but NOT in .github/agents/ (which only contains squad.agent.md, the live governance file). + it('.squad-templates/casting-reference.md exists', () => { + expect(existsSync(join(SQUAD_TEMPLATES, 'casting-reference.md'))).toBe(true); + }); + + it('templates/casting-reference.md exists', () => { + expect(existsSync(join(TEMPLATES, 'casting-reference.md'))).toBe(true); + }); + }); + + // --- Test 4: casting-policy.json is identical across template dirs --- + + it('casting-policy.json is identical in .squad-templates and templates', () => { + const a = JSON.parse(readFileSync(join(SQUAD_TEMPLATES, 'casting-policy.json'), 'utf-8')); + const b = JSON.parse(readFileSync(join(TEMPLATES, 'casting-policy.json'), 'utf-8')); + expect(a).toEqual(b); + }); + + // --- Test 5: Every allowlisted universe has a capacity entry and vice versa --- + + it('allowlist_universes and universe_capacity are in sync', () => { + const policy = JSON.parse(readFileSync(join(SQUAD_TEMPLATES, 'casting-policy.json'), 'utf-8')); + const allowlist: string[] = policy.allowlist_universes; + const capacityKeys = Object.keys(policy.universe_capacity); + + // Every allowlisted universe has a capacity entry + for (const universe of allowlist) { + expect(capacityKeys).toContain(universe); + } + // No orphaned capacity entries + for (const key of capacityKeys) { + expect(allowlist).toContain(key); + } + }); +});