Skip to content

Commit 0fada4d

Browse files
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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Sisyphus <[email protected]>
1 parent c792357 commit 0fada4d

File tree

8 files changed

+502
-7
lines changed

8 files changed

+502
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1058,7 +1058,7 @@ Configure concurrency limits for background agent tasks. This controls how many
10581058

10591059
### Categories
10601060

1061-
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.
1061+
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.
10621062

10631063
**Default Categories:**
10641064

assets/oh-my-opencode.schema.json

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,129 @@
465465
}
466466
}
467467
},
468+
"Sisyphus-Junior": {
469+
"type": "object",
470+
"properties": {
471+
"model": {
472+
"type": "string"
473+
},
474+
"category": {
475+
"type": "string"
476+
},
477+
"skills": {
478+
"type": "array",
479+
"items": {
480+
"type": "string"
481+
}
482+
},
483+
"temperature": {
484+
"type": "number",
485+
"minimum": 0,
486+
"maximum": 2
487+
},
488+
"top_p": {
489+
"type": "number",
490+
"minimum": 0,
491+
"maximum": 1
492+
},
493+
"prompt": {
494+
"type": "string"
495+
},
496+
"prompt_append": {
497+
"type": "string"
498+
},
499+
"tools": {
500+
"type": "object",
501+
"propertyNames": {
502+
"type": "string"
503+
},
504+
"additionalProperties": {
505+
"type": "boolean"
506+
}
507+
},
508+
"disable": {
509+
"type": "boolean"
510+
},
511+
"description": {
512+
"type": "string"
513+
},
514+
"mode": {
515+
"type": "string",
516+
"enum": [
517+
"subagent",
518+
"primary",
519+
"all"
520+
]
521+
},
522+
"color": {
523+
"type": "string",
524+
"pattern": "^#[0-9A-Fa-f]{6}$"
525+
},
526+
"permission": {
527+
"type": "object",
528+
"properties": {
529+
"edit": {
530+
"type": "string",
531+
"enum": [
532+
"ask",
533+
"allow",
534+
"deny"
535+
]
536+
},
537+
"bash": {
538+
"anyOf": [
539+
{
540+
"type": "string",
541+
"enum": [
542+
"ask",
543+
"allow",
544+
"deny"
545+
]
546+
},
547+
{
548+
"type": "object",
549+
"propertyNames": {
550+
"type": "string"
551+
},
552+
"additionalProperties": {
553+
"type": "string",
554+
"enum": [
555+
"ask",
556+
"allow",
557+
"deny"
558+
]
559+
}
560+
}
561+
]
562+
},
563+
"webfetch": {
564+
"type": "string",
565+
"enum": [
566+
"ask",
567+
"allow",
568+
"deny"
569+
]
570+
},
571+
"doom_loop": {
572+
"type": "string",
573+
"enum": [
574+
"ask",
575+
"allow",
576+
"deny"
577+
]
578+
},
579+
"external_directory": {
580+
"type": "string",
581+
"enum": [
582+
"ask",
583+
"allow",
584+
"deny"
585+
]
586+
}
587+
}
588+
}
589+
}
590+
},
468591
"OpenCode-Builder": {
469592
"type": "object",
470593
"properties": {

src/agents/sisyphus-junior.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { createSisyphusJuniorAgentWithOverrides, SISYPHUS_JUNIOR_DEFAULTS } from "./sisyphus-junior"
3+
4+
describe("createSisyphusJuniorAgentWithOverrides", () => {
5+
describe("honored fields", () => {
6+
test("applies model override", () => {
7+
// #given
8+
const override = { model: "openai/gpt-5.2" }
9+
10+
// #when
11+
const result = createSisyphusJuniorAgentWithOverrides(override)
12+
13+
// #then
14+
expect(result.model).toBe("openai/gpt-5.2")
15+
})
16+
17+
test("applies temperature override", () => {
18+
// #given
19+
const override = { temperature: 0.5 }
20+
21+
// #when
22+
const result = createSisyphusJuniorAgentWithOverrides(override)
23+
24+
// #then
25+
expect(result.temperature).toBe(0.5)
26+
})
27+
28+
test("applies top_p override", () => {
29+
// #given
30+
const override = { top_p: 0.9 }
31+
32+
// #when
33+
const result = createSisyphusJuniorAgentWithOverrides(override)
34+
35+
// #then
36+
expect(result.top_p).toBe(0.9)
37+
})
38+
39+
test("applies description override", () => {
40+
// #given
41+
const override = { description: "Custom description" }
42+
43+
// #when
44+
const result = createSisyphusJuniorAgentWithOverrides(override)
45+
46+
// #then
47+
expect(result.description).toBe("Custom description")
48+
})
49+
50+
test("applies color override", () => {
51+
// #given
52+
const override = { color: "#FF0000" }
53+
54+
// #when
55+
const result = createSisyphusJuniorAgentWithOverrides(override)
56+
57+
// #then
58+
expect(result.color).toBe("#FF0000")
59+
})
60+
61+
test("appends prompt_append to base prompt", () => {
62+
// #given
63+
const override = { prompt_append: "Extra instructions here" }
64+
65+
// #when
66+
const result = createSisyphusJuniorAgentWithOverrides(override)
67+
68+
// #then
69+
expect(result.prompt).toContain("You work ALONE")
70+
expect(result.prompt).toContain("Extra instructions here")
71+
})
72+
})
73+
74+
describe("defaults", () => {
75+
test("uses default model when no override", () => {
76+
// #given
77+
const override = {}
78+
79+
// #when
80+
const result = createSisyphusJuniorAgentWithOverrides(override)
81+
82+
// #then
83+
expect(result.model).toBe(SISYPHUS_JUNIOR_DEFAULTS.model)
84+
})
85+
86+
test("uses default temperature when no override", () => {
87+
// #given
88+
const override = {}
89+
90+
// #when
91+
const result = createSisyphusJuniorAgentWithOverrides(override)
92+
93+
// #then
94+
expect(result.temperature).toBe(SISYPHUS_JUNIOR_DEFAULTS.temperature)
95+
})
96+
})
97+
98+
describe("disable semantics", () => {
99+
test("disable: true causes override block to be ignored", () => {
100+
// #given
101+
const override = {
102+
disable: true,
103+
model: "openai/gpt-5.2",
104+
temperature: 0.9,
105+
}
106+
107+
// #when
108+
const result = createSisyphusJuniorAgentWithOverrides(override)
109+
110+
// #then - defaults should be used, not the overrides
111+
expect(result.model).toBe(SISYPHUS_JUNIOR_DEFAULTS.model)
112+
expect(result.temperature).toBe(SISYPHUS_JUNIOR_DEFAULTS.temperature)
113+
})
114+
})
115+
116+
describe("constrained fields", () => {
117+
test("mode is forced to subagent", () => {
118+
// #given
119+
const override = { mode: "primary" as const }
120+
121+
// #when
122+
const result = createSisyphusJuniorAgentWithOverrides(override)
123+
124+
// #then
125+
expect(result.mode).toBe("subagent")
126+
})
127+
128+
test("prompt override is ignored (discipline text preserved)", () => {
129+
// #given
130+
const override = { prompt: "Completely new prompt that replaces everything" }
131+
132+
// #when
133+
const result = createSisyphusJuniorAgentWithOverrides(override)
134+
135+
// #then
136+
expect(result.prompt).toContain("You work ALONE")
137+
expect(result.prompt).not.toBe("Completely new prompt that replaces everything")
138+
})
139+
})
140+
141+
describe("tool safety (blocked tools enforcement)", () => {
142+
test("blocked tools remain blocked even if override tries to enable them via tools format", () => {
143+
// #given
144+
const override = {
145+
tools: {
146+
task: true,
147+
sisyphus_task: true,
148+
call_omo_agent: true,
149+
read: true,
150+
},
151+
}
152+
153+
// #when
154+
const result = createSisyphusJuniorAgentWithOverrides(override)
155+
156+
// #then
157+
const tools = result.tools as Record<string, boolean> | undefined
158+
const permission = result.permission as Record<string, string> | undefined
159+
if (tools) {
160+
expect(tools.task).toBe(false)
161+
expect(tools.sisyphus_task).toBe(false)
162+
expect(tools.call_omo_agent).toBe(false)
163+
expect(tools.read).toBe(true)
164+
}
165+
if (permission) {
166+
expect(permission.task).toBe("deny")
167+
expect(permission.sisyphus_task).toBe("deny")
168+
expect(permission.call_omo_agent).toBe("deny")
169+
}
170+
})
171+
172+
test("blocked tools remain blocked when using permission format override", () => {
173+
// #given
174+
const override = {
175+
permission: {
176+
task: "allow",
177+
sisyphus_task: "allow",
178+
call_omo_agent: "allow",
179+
read: "allow",
180+
},
181+
} as { permission: Record<string, string> }
182+
183+
// #when
184+
const result = createSisyphusJuniorAgentWithOverrides(override as Parameters<typeof createSisyphusJuniorAgentWithOverrides>[0])
185+
186+
// #then - blocked tools should be denied regardless
187+
const tools = result.tools as Record<string, boolean> | undefined
188+
const permission = result.permission as Record<string, string> | undefined
189+
if (tools) {
190+
expect(tools.task).toBe(false)
191+
expect(tools.sisyphus_task).toBe(false)
192+
expect(tools.call_omo_agent).toBe(false)
193+
}
194+
if (permission) {
195+
expect(permission.task).toBe("deny")
196+
expect(permission.sisyphus_task).toBe("deny")
197+
expect(permission.call_omo_agent).toBe("deny")
198+
}
199+
})
200+
})
201+
202+
describe("prompt composition", () => {
203+
test("base prompt contains discipline constraints", () => {
204+
// #given
205+
const override = {}
206+
207+
// #when
208+
const result = createSisyphusJuniorAgentWithOverrides(override)
209+
210+
// #then
211+
expect(result.prompt).toContain("Sisyphus-Junior")
212+
expect(result.prompt).toContain("You work ALONE")
213+
expect(result.prompt).toContain("BLOCKED ACTIONS")
214+
})
215+
216+
test("prompt_append is added after base prompt", () => {
217+
// #given
218+
const override = { prompt_append: "CUSTOM_MARKER_FOR_TEST" }
219+
220+
// #when
221+
const result = createSisyphusJuniorAgentWithOverrides(override)
222+
223+
// #then
224+
const baseEndIndex = result.prompt!.indexOf("Dense > verbose.")
225+
const appendIndex = result.prompt!.indexOf("CUSTOM_MARKER_FOR_TEST")
226+
expect(baseEndIndex).not.toBe(-1) // Guard: anchor text must exist in base prompt
227+
expect(appendIndex).toBeGreaterThan(baseEndIndex)
228+
})
229+
})
230+
})

0 commit comments

Comments
 (0)