diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 10f62cb7f..6218cf8f9 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { OhMyOpenCodeConfigSchema } from "./schema" +import { AgentOverrideConfigSchema, BuiltinCategoryNameSchema, OhMyOpenCodeConfigSchema } from "./schema" describe("disabled_mcps schema", () => { test("should accept built-in MCP names", () => { @@ -134,3 +134,184 @@ describe("disabled_mcps schema", () => { } }) }) + +describe("AgentOverrideConfigSchema", () => { + describe("category field", () => { + test("accepts category as optional string", () => { + // #given + const config = { category: "visual-engineering" } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.category).toBe("visual-engineering") + } + }) + + test("accepts config without category", () => { + // #given + const config = { temperature: 0.5 } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(true) + }) + + test("rejects non-string category", () => { + // #given + const config = { category: 123 } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(false) + }) + }) + + describe("skills field", () => { + test("accepts skills as optional string array", () => { + // #given + const config = { skills: ["frontend-ui-ux", "code-reviewer"] } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.skills).toEqual(["frontend-ui-ux", "code-reviewer"]) + } + }) + + test("accepts empty skills array", () => { + // #given + const config = { skills: [] } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.skills).toEqual([]) + } + }) + + test("accepts config without skills", () => { + // #given + const config = { temperature: 0.5 } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(true) + }) + + test("rejects non-array skills", () => { + // #given + const config = { skills: "frontend-ui-ux" } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(false) + }) + }) + + describe("backward compatibility", () => { + test("still accepts model field (deprecated)", () => { + // #given + const config = { model: "openai/gpt-5.2" } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.model).toBe("openai/gpt-5.2") + } + }) + + test("accepts both model and category (deprecated usage)", () => { + // #given - category should take precedence at runtime, but both should validate + const config = { + model: "openai/gpt-5.2", + category: "high-iq" + } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.model).toBe("openai/gpt-5.2") + expect(result.data.category).toBe("high-iq") + } + }) + }) + + describe("combined fields", () => { + test("accepts category with skills", () => { + // #given + const config = { + category: "visual-engineering", + skills: ["frontend-ui-ux"] + } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.category).toBe("visual-engineering") + expect(result.data.skills).toEqual(["frontend-ui-ux"]) + } + }) + + test("accepts category with skills and other fields", () => { + // #given + const config = { + category: "high-iq", + skills: ["code-reviewer"], + temperature: 0.3, + prompt_append: "Extra instructions" + } + + // #when + const result = AgentOverrideConfigSchema.safeParse(config) + + // #then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.category).toBe("high-iq") + expect(result.data.skills).toEqual(["code-reviewer"]) + expect(result.data.temperature).toBe(0.3) + expect(result.data.prompt_append).toBe("Extra instructions") + } + }) + }) +}) + +describe("BuiltinCategoryNameSchema", () => { + test("accepts all builtin category names", () => { + // #given + const categories = ["visual-engineering", "high-iq", "artistry", "quick", "most-capable", "writing", "general"] + + // #when / #then + for (const cat of categories) { + const result = BuiltinCategoryNameSchema.safeParse(cat) + expect(result.success).toBe(true) + } + }) +}) diff --git a/src/config/schema.ts b/src/config/schema.ts index 2b09abaad..aa89582cc 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -24,10 +24,12 @@ export const BuiltinAgentNameSchema = z.enum([ "frontend-ui-ux-engineer", "document-writer", "multimodal-looker", + "Metis (Plan Consultant)", ]) export const BuiltinSkillNameSchema = z.enum([ "playwright", + "frontend-ui-ux", ]) export const OverridableAgentNameSchema = z.enum([ @@ -35,7 +37,8 @@ export const OverridableAgentNameSchema = z.enum([ "plan", "Sisyphus", "OpenCode-Builder", - "Planner-Sisyphus", + "Prometheus (Planner)", + "Metis (Plan Consultant)", "oracle", "librarian", "explore", @@ -75,14 +78,23 @@ export const HookNameSchema = z.enum([ "claude-code-hooks", "auto-slash-command", "edit-error-recovery", + "prometheus-md-only", + "start-work", + "sisyphus-orchestrator", ]) export const BuiltinCommandNameSchema = z.enum([ "init-deep", + "start-work", ]) export const AgentOverrideConfigSchema = z.object({ + /** @deprecated Use `category` instead. Model is inherited from category defaults. */ model: z.string().optional(), + /** Category name to inherit model and other settings from CategoryConfig */ + category: z.string().optional(), + /** Skill names to inject into agent prompt */ + skills: z.array(z.string()).optional(), temperature: z.number().min(0).max(2).optional(), top_p: z.number().min(0).max(1).optional(), prompt: z.string().optional(), @@ -103,7 +115,8 @@ export const AgentOverridesSchema = z.object({ plan: AgentOverrideConfigSchema.optional(), Sisyphus: AgentOverrideConfigSchema.optional(), "OpenCode-Builder": AgentOverrideConfigSchema.optional(), - "Planner-Sisyphus": AgentOverrideConfigSchema.optional(), + "Prometheus (Planner)": AgentOverrideConfigSchema.optional(), + "Metis (Plan Consultant)": AgentOverrideConfigSchema.optional(), oracle: AgentOverrideConfigSchema.optional(), librarian: AgentOverrideConfigSchema.optional(), explore: AgentOverrideConfigSchema.optional(), @@ -129,6 +142,33 @@ export const SisyphusAgentConfigSchema = z.object({ replace_plan: z.boolean().optional(), }) +export const CategoryConfigSchema = z.object({ + model: z.string(), + temperature: z.number().min(0).max(2).optional(), + top_p: z.number().min(0).max(1).optional(), + maxTokens: z.number().optional(), + thinking: z.object({ + type: z.enum(["enabled", "disabled"]), + budgetTokens: z.number().optional(), + }).optional(), + reasoningEffort: z.enum(["low", "medium", "high"]).optional(), + textVerbosity: z.enum(["low", "medium", "high"]).optional(), + tools: z.record(z.string(), z.boolean()).optional(), + prompt_append: z.string().optional(), +}) + +export const BuiltinCategoryNameSchema = z.enum([ + "visual-engineering", + "high-iq", + "artistry", + "quick", + "most-capable", + "writing", + "general", +]) + +export const CategoriesConfigSchema = z.record(z.string(), CategoryConfigSchema) + export const CommentCheckerConfigSchema = z.object({ /** Custom prompt to replace the default warning message. Use {{comments}} placeholder for detected comments XML. */ custom_prompt: z.string().optional(), @@ -251,6 +291,7 @@ export const OhMyOpenCodeConfigSchema = z.object({ disabled_hooks: z.array(HookNameSchema).optional(), disabled_commands: z.array(BuiltinCommandNameSchema).optional(), agents: AgentOverridesSchema.optional(), + categories: CategoriesConfigSchema.optional(), claude_code: ClaudeCodeConfigSchema.optional(), google_auth: z.boolean().optional(), sisyphus_agent: SisyphusAgentConfigSchema.optional(), @@ -279,5 +320,8 @@ export type SkillsConfig = z.infer export type SkillDefinition = z.infer export type RalphLoopConfig = z.infer export type NotificationConfig = z.infer +export type CategoryConfig = z.infer +export type CategoriesConfig = z.infer +export type BuiltinCategoryName = z.infer export { AnyMcpNameSchema, type AnyMcpName, McpNameSchema, type McpName } from "../mcp/types"