fix(config): allow Sisyphus-Junior agent customization via oh-my-opencode.json (#648)
* fix(config): allow Sisyphus-Junior agent customization via oh-my-opencode.json Allow users to configure Sisyphus-Junior agent via agents["Sisyphus-Junior"] in oh-my-opencode.json, removing hardcoded defaults while preserving safety constraints. Closes #623 Changes: - Add "Sisyphus-Junior" to AgentOverridesSchema and OverridableAgentNameSchema - Create createSisyphusJuniorAgentWithOverrides() helper with guardrails - Update config-handler to use override helper instead of hardcoded values - Fix README category wording (runtime presets, not separate agents) Honored override fields: - model, temperature, top_p, tools, permission, description, color, prompt_append Safety guardrails enforced post-merge: - mode forced to "subagent" (cannot change) - prompt is append-only (base discipline text preserved) - blocked tools (task, sisyphus_task, call_omo_agent) always denied - disable: true ignores override block, uses defaults Category interaction: - sisyphus_task(category=...) runs use the base Sisyphus-Junior agent config - Category model/temperature overrides take precedence at request time - To change model for a category, set categories.<cat>.model (not agent override) - Categories are runtime presets applied to Sisyphus-Junior, not separate agents Tests: 15 new tests in sisyphus-junior.test.ts, 3 new schema tests Co-Authored-By: Sisyphus <sisyphus@mengmota.com> * test(sisyphus-junior): add guard assertion for prompt anchor text Add validation that baseEndIndex is not -1 before using it for ordering assertion. Previously, if "Dense > verbose." text changed in the base prompt, indexOf would return -1 and any positive appendIndex would pass. Co-Authored-By: Sisyphus <sisyphus@mengmota.com> --------- Co-authored-by: Sisyphus <sisyphus@mengmota.com>
This commit is contained in:
committed by
GitHub
parent
c79235744b
commit
0fada4d0fc
@@ -1058,7 +1058,7 @@ Configure concurrency limits for background agent tasks. This controls how many
|
||||
|
||||
### Categories
|
||||
|
||||
Categories enable domain-specific task delegation via the `sisyphus_task` tool. Each category pre-configures a specialized `Sisyphus-Junior-{category}` agent with optimized model settings and prompts.
|
||||
Categories enable domain-specific task delegation via the `sisyphus_task` tool. Each category applies runtime presets (model, temperature, prompt additions) when calling the `Sisyphus-Junior` agent.
|
||||
|
||||
**Default Categories:**
|
||||
|
||||
|
||||
@@ -465,6 +465,129 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Sisyphus-Junior": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
},
|
||||
"top_p": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"prompt_append": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"disable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"subagent",
|
||||
"primary",
|
||||
"all"
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9A-Fa-f]{6}$"
|
||||
},
|
||||
"permission": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"edit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"bash": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"webfetch": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"doom_loop": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"external_directory": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpenCode-Builder": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
230
src/agents/sisyphus-junior.test.ts
Normal file
230
src/agents/sisyphus-junior.test.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createSisyphusJuniorAgentWithOverrides, SISYPHUS_JUNIOR_DEFAULTS } from "./sisyphus-junior"
|
||||
|
||||
describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
describe("honored fields", () => {
|
||||
test("applies model override", () => {
|
||||
// #given
|
||||
const override = { model: "openai/gpt-5.2" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.model).toBe("openai/gpt-5.2")
|
||||
})
|
||||
|
||||
test("applies temperature override", () => {
|
||||
// #given
|
||||
const override = { temperature: 0.5 }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.temperature).toBe(0.5)
|
||||
})
|
||||
|
||||
test("applies top_p override", () => {
|
||||
// #given
|
||||
const override = { top_p: 0.9 }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.top_p).toBe(0.9)
|
||||
})
|
||||
|
||||
test("applies description override", () => {
|
||||
// #given
|
||||
const override = { description: "Custom description" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.description).toBe("Custom description")
|
||||
})
|
||||
|
||||
test("applies color override", () => {
|
||||
// #given
|
||||
const override = { color: "#FF0000" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.color).toBe("#FF0000")
|
||||
})
|
||||
|
||||
test("appends prompt_append to base prompt", () => {
|
||||
// #given
|
||||
const override = { prompt_append: "Extra instructions here" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.prompt).toContain("You work ALONE")
|
||||
expect(result.prompt).toContain("Extra instructions here")
|
||||
})
|
||||
})
|
||||
|
||||
describe("defaults", () => {
|
||||
test("uses default model when no override", () => {
|
||||
// #given
|
||||
const override = {}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.model).toBe(SISYPHUS_JUNIOR_DEFAULTS.model)
|
||||
})
|
||||
|
||||
test("uses default temperature when no override", () => {
|
||||
// #given
|
||||
const override = {}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.temperature).toBe(SISYPHUS_JUNIOR_DEFAULTS.temperature)
|
||||
})
|
||||
})
|
||||
|
||||
describe("disable semantics", () => {
|
||||
test("disable: true causes override block to be ignored", () => {
|
||||
// #given
|
||||
const override = {
|
||||
disable: true,
|
||||
model: "openai/gpt-5.2",
|
||||
temperature: 0.9,
|
||||
}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then - defaults should be used, not the overrides
|
||||
expect(result.model).toBe(SISYPHUS_JUNIOR_DEFAULTS.model)
|
||||
expect(result.temperature).toBe(SISYPHUS_JUNIOR_DEFAULTS.temperature)
|
||||
})
|
||||
})
|
||||
|
||||
describe("constrained fields", () => {
|
||||
test("mode is forced to subagent", () => {
|
||||
// #given
|
||||
const override = { mode: "primary" as const }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.mode).toBe("subagent")
|
||||
})
|
||||
|
||||
test("prompt override is ignored (discipline text preserved)", () => {
|
||||
// #given
|
||||
const override = { prompt: "Completely new prompt that replaces everything" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.prompt).toContain("You work ALONE")
|
||||
expect(result.prompt).not.toBe("Completely new prompt that replaces everything")
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool safety (blocked tools enforcement)", () => {
|
||||
test("blocked tools remain blocked even if override tries to enable them via tools format", () => {
|
||||
// #given
|
||||
const override = {
|
||||
tools: {
|
||||
task: true,
|
||||
sisyphus_task: true,
|
||||
call_omo_agent: true,
|
||||
read: true,
|
||||
},
|
||||
}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
const tools = result.tools as Record<string, boolean> | undefined
|
||||
const permission = result.permission as Record<string, string> | undefined
|
||||
if (tools) {
|
||||
expect(tools.task).toBe(false)
|
||||
expect(tools.sisyphus_task).toBe(false)
|
||||
expect(tools.call_omo_agent).toBe(false)
|
||||
expect(tools.read).toBe(true)
|
||||
}
|
||||
if (permission) {
|
||||
expect(permission.task).toBe("deny")
|
||||
expect(permission.sisyphus_task).toBe("deny")
|
||||
expect(permission.call_omo_agent).toBe("deny")
|
||||
}
|
||||
})
|
||||
|
||||
test("blocked tools remain blocked when using permission format override", () => {
|
||||
// #given
|
||||
const override = {
|
||||
permission: {
|
||||
task: "allow",
|
||||
sisyphus_task: "allow",
|
||||
call_omo_agent: "allow",
|
||||
read: "allow",
|
||||
},
|
||||
} as { permission: Record<string, string> }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override as Parameters<typeof createSisyphusJuniorAgentWithOverrides>[0])
|
||||
|
||||
// #then - blocked tools should be denied regardless
|
||||
const tools = result.tools as Record<string, boolean> | undefined
|
||||
const permission = result.permission as Record<string, string> | undefined
|
||||
if (tools) {
|
||||
expect(tools.task).toBe(false)
|
||||
expect(tools.sisyphus_task).toBe(false)
|
||||
expect(tools.call_omo_agent).toBe(false)
|
||||
}
|
||||
if (permission) {
|
||||
expect(permission.task).toBe("deny")
|
||||
expect(permission.sisyphus_task).toBe("deny")
|
||||
expect(permission.call_omo_agent).toBe("deny")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("prompt composition", () => {
|
||||
test("base prompt contains discipline constraints", () => {
|
||||
// #given
|
||||
const override = {}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.prompt).toContain("Sisyphus-Junior")
|
||||
expect(result.prompt).toContain("You work ALONE")
|
||||
expect(result.prompt).toContain("BLOCKED ACTIONS")
|
||||
})
|
||||
|
||||
test("prompt_append is added after base prompt", () => {
|
||||
// #given
|
||||
const override = { prompt_append: "CUSTOM_MARKER_FOR_TEST" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
const baseEndIndex = result.prompt!.indexOf("Dense > verbose.")
|
||||
const appendIndex = result.prompt!.indexOf("CUSTOM_MARKER_FOR_TEST")
|
||||
expect(baseEndIndex).not.toBe(-1) // Guard: anchor text must exist in base prompt
|
||||
expect(appendIndex).toBeGreaterThan(baseEndIndex)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { isGptModel } from "./types"
|
||||
import type { CategoryConfig } from "../config/schema"
|
||||
import type { AgentOverrideConfig, CategoryConfig } from "../config/schema"
|
||||
import {
|
||||
createAgentToolRestrictions,
|
||||
migrateAgentConfig,
|
||||
supportsNewPermissionSystem,
|
||||
} from "../shared/permission-compat"
|
||||
|
||||
const SISYPHUS_JUNIOR_PROMPT = `<Role>
|
||||
@@ -77,6 +78,71 @@ function buildSisyphusJuniorPrompt(promptAppend?: string): string {
|
||||
// Core tools that Sisyphus-Junior must NEVER have access to
|
||||
const BLOCKED_TOOLS = ["task", "sisyphus_task", "call_omo_agent"]
|
||||
|
||||
export const SISYPHUS_JUNIOR_DEFAULTS = {
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
temperature: 0.1,
|
||||
} as const
|
||||
|
||||
export function createSisyphusJuniorAgentWithOverrides(
|
||||
override: AgentOverrideConfig | undefined
|
||||
): AgentConfig {
|
||||
if (override?.disable) {
|
||||
override = undefined
|
||||
}
|
||||
|
||||
const model = override?.model ?? SISYPHUS_JUNIOR_DEFAULTS.model
|
||||
const temperature = override?.temperature ?? SISYPHUS_JUNIOR_DEFAULTS.temperature
|
||||
|
||||
const promptAppend = override?.prompt_append
|
||||
const prompt = buildSisyphusJuniorPrompt(promptAppend)
|
||||
|
||||
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
||||
|
||||
let toolsConfig: Record<string, unknown> = {}
|
||||
if (supportsNewPermissionSystem()) {
|
||||
const userPermission = (override?.permission ?? {}) as Record<string, string>
|
||||
const basePermission = (baseRestrictions as { permission: Record<string, string> }).permission
|
||||
const merged: Record<string, string> = { ...userPermission }
|
||||
for (const tool of BLOCKED_TOOLS) {
|
||||
merged[tool] = "deny"
|
||||
}
|
||||
toolsConfig = { permission: { ...merged, ...basePermission } }
|
||||
} else {
|
||||
const userTools = override?.tools ?? {}
|
||||
const baseTools = (baseRestrictions as { tools: Record<string, boolean> }).tools
|
||||
const merged: Record<string, boolean> = { ...userTools }
|
||||
for (const tool of BLOCKED_TOOLS) {
|
||||
merged[tool] = false
|
||||
}
|
||||
toolsConfig = { tools: { ...merged, ...baseTools } }
|
||||
}
|
||||
|
||||
const base: AgentConfig = {
|
||||
description: override?.description ??
|
||||
"Sisyphus-Junior - Focused task executor. Same discipline, no delegation.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens: 64000,
|
||||
prompt,
|
||||
color: override?.color ?? "#20B2AA",
|
||||
...toolsConfig,
|
||||
}
|
||||
|
||||
if (override?.top_p !== undefined) {
|
||||
base.top_p = override.top_p
|
||||
}
|
||||
|
||||
if (isGptModel(model)) {
|
||||
return { ...base, reasoningEffort: "medium" } as AgentConfig
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
} as AgentConfig
|
||||
}
|
||||
|
||||
export function createSisyphusJuniorAgent(
|
||||
categoryConfig: CategoryConfig,
|
||||
promptAppend?: string
|
||||
|
||||
@@ -315,3 +315,76 @@ describe("BuiltinCategoryNameSchema", () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("Sisyphus-Junior agent override", () => {
|
||||
test("schema accepts agents['Sisyphus-Junior'] and retains the key after parsing", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
"Sisyphus-Junior": {
|
||||
model: "openai/gpt-5.2",
|
||||
temperature: 0.2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// #when
|
||||
const result = OhMyOpenCodeConfigSchema.safeParse(config)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.agents?.["Sisyphus-Junior"]).toBeDefined()
|
||||
expect(result.data.agents?.["Sisyphus-Junior"]?.model).toBe("openai/gpt-5.2")
|
||||
expect(result.data.agents?.["Sisyphus-Junior"]?.temperature).toBe(0.2)
|
||||
}
|
||||
})
|
||||
|
||||
test("schema accepts Sisyphus-Junior with prompt_append", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
"Sisyphus-Junior": {
|
||||
prompt_append: "Additional instructions for Sisyphus-Junior",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// #when
|
||||
const result = OhMyOpenCodeConfigSchema.safeParse(config)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.agents?.["Sisyphus-Junior"]?.prompt_append).toBe(
|
||||
"Additional instructions for Sisyphus-Junior"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test("schema accepts Sisyphus-Junior with tools override", () => {
|
||||
// #given
|
||||
const config = {
|
||||
agents: {
|
||||
"Sisyphus-Junior": {
|
||||
tools: {
|
||||
read: true,
|
||||
write: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// #when
|
||||
const result = OhMyOpenCodeConfigSchema.safeParse(config)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.agents?.["Sisyphus-Junior"]?.tools).toEqual({
|
||||
read: true,
|
||||
write: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -39,6 +39,7 @@ export const OverridableAgentNameSchema = z.enum([
|
||||
"build",
|
||||
"plan",
|
||||
"Sisyphus",
|
||||
"Sisyphus-Junior",
|
||||
"OpenCode-Builder",
|
||||
"Prometheus (Planner)",
|
||||
"Metis (Plan Consultant)",
|
||||
@@ -119,6 +120,7 @@ export const AgentOverridesSchema = z.object({
|
||||
build: AgentOverrideConfigSchema.optional(),
|
||||
plan: AgentOverrideConfigSchema.optional(),
|
||||
Sisyphus: AgentOverrideConfigSchema.optional(),
|
||||
"Sisyphus-Junior": AgentOverrideConfigSchema.optional(),
|
||||
"OpenCode-Builder": AgentOverrideConfigSchema.optional(),
|
||||
"Prometheus (Planner)": AgentOverrideConfigSchema.optional(),
|
||||
"Metis (Plan Consultant)": AgentOverrideConfigSchema.optional(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createBuiltinAgents } from "../agents";
|
||||
import { createSisyphusJuniorAgent } from "../agents/sisyphus-junior";
|
||||
import { createSisyphusJuniorAgentWithOverrides } from "../agents/sisyphus-junior";
|
||||
import {
|
||||
loadUserCommands,
|
||||
loadProjectCommands,
|
||||
@@ -152,10 +152,9 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
Sisyphus: builtinAgents.Sisyphus,
|
||||
};
|
||||
|
||||
agentConfig["Sisyphus-Junior"] = createSisyphusJuniorAgent({
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
temperature: 0.1,
|
||||
});
|
||||
agentConfig["Sisyphus-Junior"] = createSisyphusJuniorAgentWithOverrides(
|
||||
pluginConfig.agents?.["Sisyphus-Junior"]
|
||||
);
|
||||
|
||||
if (builderEnabled) {
|
||||
const { name: _buildName, ...buildConfigWithoutName } =
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { supportsNewPermissionSystem } from "./opencode-version"
|
||||
|
||||
export { supportsNewPermissionSystem }
|
||||
|
||||
export type PermissionValue = "ask" | "allow" | "deny"
|
||||
|
||||
export interface LegacyToolsFormat {
|
||||
|
||||
Reference in New Issue
Block a user