feat(config): add categories, new agents and hooks to schema

Update Zod schema with:
- CategoryConfigSchema for task delegation categories
- CategoriesConfigSchema for user category overrides
- New agents: Metis (Plan Consultant)
- New hooks: prometheus-md-only, start-work, sisyphus-orchestrator
- New commands: start-work
- Agent category and skills fields

Includes schema test coverage.

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2026-01-05 13:51:25 +09:00
parent f49d92852a
commit 7d2983fafb
2 changed files with 228 additions and 3 deletions

View File

@@ -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)
}
})
})

View File

@@ -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<typeof SkillsConfigSchema>
export type SkillDefinition = z.infer<typeof SkillDefinitionSchema>
export type RalphLoopConfig = z.infer<typeof RalphLoopConfigSchema>
export type NotificationConfig = z.infer<typeof NotificationConfigSchema>
export type CategoryConfig = z.infer<typeof CategoryConfigSchema>
export type CategoriesConfig = z.infer<typeof CategoriesConfigSchema>
export type BuiltinCategoryName = z.infer<typeof BuiltinCategoryNameSchema>
export { AnyMcpNameSchema, type AnyMcpName, McpNameSchema, type McpName } from "../mcp/types"