diff --git a/src/features/team-mode/team-registry/validator.test.ts b/src/features/team-mode/team-registry/validator.test.ts
new file mode 100644
index 000000000..b101ded2c
--- /dev/null
+++ b/src/features/team-mode/team-registry/validator.test.ts
@@ -0,0 +1,166 @@
+///
+
+import { describe, expect, test } from "bun:test"
+
+import { TeamSpecSchema } from "../types"
+
+import type { Member, TeamSpec } from "../types"
+import {
+ TeamSpecValidationError,
+ validateDualSupport,
+ validateMemberEligibility,
+ validateSpec,
+} from "./validator"
+
+const PROMETHEUS_REJECTION_MESSAGE =
+ "Agent 'prometheus' is plan-mode-only; can only write to .sisyphus/*.md (enforced by prometheusMdOnly hook). Cannot write to team mailbox. Use category: 'plan' instead."
+
+function createCategoryMember(name: string): Member {
+ return {
+ kind: "category",
+ name,
+ category: "deep",
+ prompt: `implement the assigned work for ${name}`,
+ backendType: "in-process",
+ isActive: true,
+ }
+}
+
+function createBaseTeamSpec(): TeamSpec {
+ return {
+ version: 1,
+ name: "validator-team",
+ createdAt: 1,
+ leadAgentId: "lead",
+ members: [createCategoryMember("lead"), createCategoryMember("reviewer")],
+ }
+}
+
+describe("team-registry validator", () => {
+ test("rejects members that specify both category and subagent_type", () => {
+ // given
+ const teamSpec = {
+ ...createBaseTeamSpec(),
+ members: [
+ {
+ kind: "category",
+ name: "lead",
+ category: "deep",
+ prompt: "implement the assigned work for lead",
+ subagent_type: "sisyphus",
+ },
+ ],
+ }
+
+ // when
+ const result = TeamSpecSchema.safeParse(teamSpec)
+
+ // then
+ expect(result.success).toBe(false)
+ })
+
+ test("rejects members that omit the kind discriminator", () => {
+ // given
+ const teamSpec = {
+ ...createBaseTeamSpec(),
+ members: [{ name: "lead", category: "deep", prompt: "implement the assigned work for lead" }],
+ }
+
+ // when
+ const result = TeamSpecSchema.safeParse(teamSpec)
+
+ // then
+ expect(result.success).toBe(false)
+ })
+
+ test("rejects prometheus subagent members with the exact plan message", () => {
+ // given
+ const member: Member = {
+ kind: "subagent_type",
+ name: "planner",
+ subagent_type: "prometheus",
+ backendType: "in-process",
+ isActive: true,
+ }
+
+ // when
+ const act = () => validateMemberEligibility(member)
+
+ // then
+ expect(act).toThrow(PROMETHEUS_REJECTION_MESSAGE)
+ expect(act).toThrow(TeamSpecValidationError)
+ })
+
+ test("accepts hephaestus subagent members after the D-36 eligibility change", () => {
+ // given
+ const member: Member = {
+ kind: "subagent_type",
+ name: "craftsman",
+ subagent_type: "hephaestus",
+ backendType: "in-process",
+ isActive: true,
+ }
+
+ // when
+ const act = () => validateMemberEligibility(member)
+
+ // then
+ expect(act).not.toThrow()
+ })
+
+ test("rejects leadAgentId values that do not match a member name", () => {
+ // given
+ const teamSpec = { ...createBaseTeamSpec(), leadAgentId: "ghost" }
+
+ // when
+ const act = () => validateSpec(teamSpec)
+
+ // then
+ expect(act).toThrow("Team 'validator-team' leadAgentId 'ghost' must match exactly one member.name.")
+ })
+
+ test("rejects duplicate member names within a team", () => {
+ // given
+ const duplicateMember = createCategoryMember("lead")
+ const teamSpec = { ...createBaseTeamSpec(), members: [createCategoryMember("lead"), duplicateMember] }
+
+ // when
+ const act = () => validateSpec(teamSpec)
+
+ // then
+ expect(act).toThrow("Member name 'lead' is duplicated within team 'validator-team'. Member names must be unique.")
+ })
+
+ test("rejects teams that exceed the 8-member cap", () => {
+ // given
+ const teamSpec = {
+ ...createBaseTeamSpec(),
+ members: Array.from({ length: 9 }, (_, index) => createCategoryMember(`member-${index}`)),
+ leadAgentId: "member-0",
+ }
+
+ // when
+ const act = () => validateSpec(teamSpec)
+
+ // then
+ expect(act).toThrow("Team 'validator-team' exceeds max 8 members.")
+ })
+
+ test("rejects category prompts that collapse to empty text", () => {
+ // given
+ const member: Member = {
+ kind: "category",
+ name: "lead",
+ category: "deep",
+ prompt: " ",
+ backendType: "in-process",
+ isActive: true,
+ }
+
+ // when
+ const act = () => validateDualSupport(member)
+
+ // then
+ expect(act).toThrow("Member 'lead' prompt must not be empty after trimming whitespace.")
+ })
+})
diff --git a/src/features/team-mode/team-registry/validator.ts b/src/features/team-mode/team-registry/validator.ts
new file mode 100644
index 000000000..ceea0a1f0
--- /dev/null
+++ b/src/features/team-mode/team-registry/validator.ts
@@ -0,0 +1,106 @@
+import { AGENT_ELIGIBILITY_REGISTRY } from "../types"
+
+import type { Member, TeamSpec } from "../types"
+
+const MAX_TEAM_MEMBERS = 8
+const UNKNOWN_SUBAGENT_MESSAGE =
+ "Unknown subagent_type ''. Available ELIGIBLE agents: sisyphus, atlas, sisyphus-junior, hephaestus (if D-36 applied). Use delegate-task for read-only agents like oracle, librarian, explore, metis, momus, multimodal-looker."
+
+export class TeamSpecValidationError extends Error {
+ constructor(
+ message: string,
+ public readonly code: string,
+ public readonly field?: string,
+ public readonly memberName?: string,
+ ) {
+ super(message)
+ this.name = "TeamSpecValidationError"
+ }
+}
+
+export function validateSpec(spec: TeamSpec): void {
+ if (spec.members.length > MAX_TEAM_MEMBERS) {
+ throw new TeamSpecValidationError(
+ `Team '${spec.name}' exceeds max 8 members.`,
+ "TEAM_MEMBER_LIMIT_EXCEEDED",
+ "members",
+ )
+ }
+
+ const seenMemberNames = new Set()
+ let leadMatchCount = 0
+
+ for (const member of spec.members) {
+ if (seenMemberNames.has(member.name)) {
+ throw new TeamSpecValidationError(
+ `Member name '${member.name}' is duplicated within team '${spec.name}'. Member names must be unique.`,
+ "DUPLICATE_MEMBER_NAME",
+ "members",
+ member.name,
+ )
+ }
+
+ seenMemberNames.add(member.name)
+ validateMemberEligibility(member)
+ validateDualSupport(member)
+
+ if (member.name === spec.leadAgentId) {
+ leadMatchCount += 1
+ }
+ }
+
+ if (leadMatchCount !== 1) {
+ throw new TeamSpecValidationError(
+ `Team '${spec.name}' leadAgentId '${spec.leadAgentId}' must match exactly one member.name.`,
+ "INVALID_LEAD_AGENT_ID",
+ "leadAgentId",
+ )
+ }
+}
+
+export function validateMemberEligibility(member: Member): void {
+ if (member.kind !== "subagent_type") {
+ return
+ }
+
+ const eligibility = AGENT_ELIGIBILITY_REGISTRY[member.subagent_type]
+ if (!eligibility) {
+ throw new TeamSpecValidationError(
+ UNKNOWN_SUBAGENT_MESSAGE.replace("", member.subagent_type),
+ "UNKNOWN_SUBAGENT_TYPE",
+ "subagent_type",
+ member.name,
+ )
+ }
+
+ if (eligibility.verdict === "hard-reject") {
+ throw new TeamSpecValidationError(
+ eligibility.rejectionMessage ?? `Agent '${member.subagent_type}' is not eligible as a team member.`,
+ "INELIGIBLE_AGENT",
+ "subagent_type",
+ member.name,
+ )
+ }
+}
+
+export function validateDualSupport(member: Member): void {
+ const trimmedPrompt = member.prompt?.trim()
+
+ if (trimmedPrompt === "") {
+ throw new TeamSpecValidationError(
+ `Member '${member.name}' prompt must not be empty after trimming whitespace.`,
+ "EMPTY_PROMPT",
+ "prompt",
+ member.name,
+ )
+ }
+
+ if (member.kind === "category" && member.prompt.trim().length < 8) {
+ throw new TeamSpecValidationError(
+ `Member '${member.name}' category prompt must be at least 8 characters long.`,
+ "CATEGORY_PROMPT_TOO_SHORT",
+ "prompt",
+ member.name,
+ )
+ }
+}