diff --git a/src/agents/athena/agent.ts b/src/agents/athena/agent.ts index 59f97f6d..c81c667c 100644 --- a/src/agents/athena/agent.ts +++ b/src/agents/athena/agent.ts @@ -1,6 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentMode, AgentPromptMetadata } from "../types" -import { createAgentToolRestrictions, type PermissionValue } from "../../shared/permission-compat" +import { createAgentToolRestrictions } from "../../shared/permission-compat" import { applyModelThinkingConfig } from "./model-thinking-config" const MODE: AgentMode = "primary" @@ -211,10 +211,10 @@ The switch_agent tool switches the active agent. After you call it, end your res export function createAthenaAgent(model: string): AgentConfig { const restrictions = createAgentToolRestrictions(["write", "edit", "call_omo_agent"]) - const permission: Record = { + const permission = { ...restrictions.permission, question: "allow", - } + } as AgentConfig["permission"] const base = { description: diff --git a/src/agents/builtin-agents/athena-council-guard.ts b/src/agents/builtin-agents/athena-council-guard.ts index 9272c07d..149ebf3b 100644 --- a/src/agents/builtin-agents/athena-council-guard.ts +++ b/src/agents/builtin-agents/athena-council-guard.ts @@ -1,6 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk" -const MISSING_COUNCIL_PROMPT = ` +const MISSING_COUNCIL_PROMPT_HEADER = ` ## CRITICAL: No Council Members Configured @@ -32,7 +32,9 @@ You have no council members registered. This means the Athena council config is } \`\`\` -Each member requires \`model\` (\`"provider/model-id"\` format) and \`name\` (display name). Minimum 2 members required. Optional fields: \`variant\`, \`temperature\`. +Each member requires \`model\` (\`"provider/model-id"\` format) and \`name\` (display name). Minimum 2 members required. Optional fields: \`variant\`, \`temperature\`.` + +const MISSING_COUNCIL_PROMPT_FOOTER = ` --- @@ -47,12 +49,14 @@ export function appendMissingCouncilPrompt( athenaConfig: AgentConfig, skippedMembers?: Array<{ name: string; reason: string }>, ): AgentConfig { - let prompt = MISSING_COUNCIL_PROMPT + let prompt = MISSING_COUNCIL_PROMPT_HEADER if (skippedMembers && skippedMembers.length > 0) { const skipDetails = skippedMembers.map((m) => `- **${m.name}**: ${m.reason}`).join("\n") prompt += `\n\n### Why Council Failed\n\nThe following members were skipped:\n${skipDetails}` } + prompt += MISSING_COUNCIL_PROMPT_FOOTER + return { ...athenaConfig, prompt } } diff --git a/src/agents/builtin-agents/council-member-agents.test.ts b/src/agents/builtin-agents/council-member-agents.test.ts index eead3048..5d49f618 100644 --- a/src/agents/builtin-agents/council-member-agents.test.ts +++ b/src/agents/builtin-agents/council-member-agents.test.ts @@ -2,12 +2,12 @@ import { describe, expect, test } from "bun:test" import { registerCouncilMemberAgents } from "./council-member-agents" describe("council-member-agents", () => { - test("skips duplicate names and disables council when below minimum", () => { + test("skips case-insensitive duplicate names and disables council when below minimum", () => { //#given const config = { members: [ { model: "openai/gpt-5.3-codex", name: "GPT" }, - { model: "anthropic/claude-opus-4-6", name: "GPT" }, + { model: "anthropic/claude-opus-4-6", name: "gpt" }, ], } //#when diff --git a/src/config/schema/athena.test.ts b/src/config/schema/athena.test.ts index e116acb6..ed0edac0 100644 --- a/src/config/schema/athena.test.ts +++ b/src/config/schema/athena.test.ts @@ -330,8 +330,9 @@ describe("CouncilConfigSchema", () => { expect(result.success).toBe(false) }) - test("rejects council with duplicate member names", () => { - //#given + test("accepts council with duplicate member names for graceful runtime handling", () => { + //#given - duplicate detection is handled at runtime by registerCouncilMemberAgents, + // not at schema level, to allow graceful fallback instead of hard parse failure const config = { members: [ { model: "anthropic/claude-opus-4-6", name: "analyst" }, @@ -343,11 +344,11 @@ describe("CouncilConfigSchema", () => { const result = CouncilConfigSchema.safeParse(config) //#then - expect(result.success).toBe(false) + expect(result.success).toBe(true) }) - test("rejects council with case-insensitive duplicate names", () => { - //#given + test("accepts council with case-insensitive duplicate names for graceful runtime handling", () => { + //#given - case-insensitive dedup is handled at runtime by registerCouncilMemberAgents const config = { members: [ { model: "anthropic/claude-opus-4-6", name: "Claude" }, @@ -359,7 +360,7 @@ describe("CouncilConfigSchema", () => { const result = CouncilConfigSchema.safeParse(config) //#then - expect(result.success).toBe(false) + expect(result.success).toBe(true) }) test("accepts council with unique member names", () => { diff --git a/src/config/schema/athena.ts b/src/config/schema/athena.ts index ebc7e022..32e778a9 100644 --- a/src/config/schema/athena.ts +++ b/src/config/schema/athena.ts @@ -20,13 +20,7 @@ export const CouncilMemberSchema = z.object({ }).strict() export const CouncilConfigSchema = z.object({ - members: z.array(CouncilMemberSchema).min(2).refine( - (members) => { - const names = members.map(m => m.name.toLowerCase()) - return new Set(names).size === names.length - }, - { message: "Council member names must be unique (case-insensitive)" }, - ), + members: z.array(CouncilMemberSchema).min(2), }).strict() export type CouncilMemberConfig = z.infer