feat(sdd): configurable models for judgment-day agents#383
feat(sdd): configurable models for judgment-day agents#383danielgap wants to merge 10 commits intoGentleman-Programming:mainfrom
Conversation
Add jd-judge-a, jd-judge-b, and jd-fix-agent as hidden sub-agents in the multi-mode overlay. Update orchestrator permissions to allow delegation to these agents. Refs: Gentleman-Programming#208
JDPhases returns the judgment-day sub-agent names (jd-judge-a, jd-judge-b, jd-fix-agent). ConfigurableAgentPhases combines SDD phases and JD agents for the TUI model picker. Refs: Gentleman-Programming#208
Add jd-judge-a, jd-judge-b, jd-fix-agent to claudeModelAssignmentRowOrder and claudeModelAssignmentReasons in inject.go. Add JD entries to all three existing presets (balanced, performance, economy) in claude_model.go. Add new ClaudeModelPresetDiversity() preset for perspective diversity between judges (opus/haiku/sonnet split). Refs: Gentleman-Programming#208
Add isJDAgent helper and modify injectModelAssignments case 3 to skip root model fallback for judgment-day agents. This preserves independent model configuration for diversity of perspective between judges. Refs: Gentleman-Programming#208
Pattern 1 now documents both OpenCode named-agent delegation (jd-judge-a, jd-judge-b, jd-fix-agent) and generic delegate() for agents without named sub-agent support. Pattern 4 and Rules section updated to reference both delegation modes.
Add jd-judge-a, jd-judge-b, and jd-fix-agent to sddPhaseAgents so they are properly cleaned up during uninstall.
…filtering, diversity preset
There was a problem hiding this comment.
Pull request overview
Adds configurable Judgment Day (JD) workflow agents to the OpenCode SDD multi-mode overlay and exposes per-JD-agent model selection in the TUI (including a new “Diversity” preset), while keeping JD agents global (excluded from profile-scoped model assignment).
Changes:
- Introduces 3 named JD agents (
jd-judge-a,jd-judge-b,jd-fix-agent) in the OpenCode multi overlay with orchestrator delegation permissions. - Extends model discovery/configuration to include JD agents (new
JDPhases()/ConfigurableAgentPhases()), updates injection/assignment reading, and adds root-model propagation exclusion for JD agents. - Updates TUI model pickers and presets (Claude) to support configuring JD agent models; updates JD skill docs and adds agent definition assets (Claude/Kiro).
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/tui/screens/profile_create.go | Uses profile-filtered model-picker rows to exclude JD agents from profile flows. |
| internal/tui/screens/model_picker_test.go | Updates expected row count to include separator + JD rows. |
| internal/tui/screens/model_picker.go | Adds JD rows + separator, ForProfile filtering, and JD assignment mapping/rendering. |
| internal/tui/screens/claude_model_picker.go | Adds “Diversity” preset and includes JD phases/labels in Claude custom mode. |
| internal/tui/model.go | Flags model picker as profile-scoped and uses filtered rows during profile creation/edit. |
| internal/opencode/models.go | Adds JDPhases() and ConfigurableAgentPhases() for shared agent discovery. |
| internal/model/claude_model.go | Extends presets with JD entries and adds ClaudeModelPresetDiversity(). |
| internal/components/uninstall/service.go | Includes JD agents in SDD-related cleanup list. |
| internal/components/sdd/read_assignments.go | Expands valid assignment set to include JD agents (via ConfigurableAgentPhases()). |
| internal/components/sdd/profiles_test.go | Updates permission expectations to account for JD permissions only in multi overlay. |
| internal/components/sdd/profiles.go | Reserves JD names from profile naming and grants JD permissions in generated profile overlays. |
| internal/components/sdd/inject_test.go | Updates multi-overlay agent count expectations to include JD agents. |
| internal/components/sdd/inject.go | Adds JD rows to model assignment tables and excludes JD agents from root model propagation. |
| internal/assets/skills/judgment-day/SKILL.md | Documents named-agent delegation when supported; fallback guidance for other agents. |
| internal/assets/opencode/sdd-overlay-multi.json | Adds JD agent definitions and allows orchestrator delegation to them. |
| internal/assets/kiro/agents/jd-judge-b.md | Adds Kiro JD Judge B agent definition. |
| internal/assets/kiro/agents/jd-judge-a.md | Adds Kiro JD Judge A agent definition. |
| internal/assets/kiro/agents/jd-fix-agent.md | Adds Kiro JD Fix Agent definition. |
| internal/assets/claude/agents/jd-judge-b.md | Adds Claude JD Judge B agent definition. |
| internal/assets/claude/agents/jd-judge-a.md | Adds Claude JD Judge A agent definition. |
| internal/assets/claude/agents/jd-fix-agent.md | Adds Claude JD Fix Agent definition. |
Comments suppressed due to low confidence (1)
internal/tui/screens/model_picker.go:106
- ModelPickerRows/ModelPickerRowsForProfile hard-code slice capacities (15/11). Since the row list length depends on opencode.SDDPhases()/JDPhases(), the fixed numbers will drift as phases change. Prefer computing capacity from the phase slice lengths (or just omit the capacity) to avoid future off-by-one assumptions.
func ModelPickerRows() []string {
rows := make([]string, 0, 15)
rows = append(rows, SDDOrchestratorPhase)
rows = append(rows, "Set all phases")
rows = append(rows, opencode.SDDPhases()...)
rows = append(rows, "--- Judgment Day ---")
rows = append(rows, opencode.JDPhases()...)
return rows
}
// ModelPickerRowsForProfile returns model picker rows for profile creation.
// JD agents are excluded because they are global (not profile-scoped).
func ModelPickerRowsForProfile() []string {
rows := make([]string, 0, 11)
rows = append(rows, SDDOrchestratorPhase)
rows = append(rows, "Set all phases")
rows = append(rows, opencode.SDDPhases()...)
return rows
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // sddPhaseSet is the set of valid agent names that may appear in | ||
| // opencode.json. It includes SDD phases, JD agents, and the orchestrator. | ||
| var sddPhaseSet = buildConfigurableAgentSet() | ||
|
|
||
| func buildSDDPhaseSet() map[string]bool { | ||
| phases := opencode.SDDPhases() | ||
| func buildConfigurableAgentSet() map[string]bool { | ||
| phases := opencode.ConfigurableAgentPhases() // SDD + JD phases | ||
| set := make(map[string]bool, len(phases)+1) |
There was a problem hiding this comment.
The variable name sddPhaseSet is now misleading: it includes JD agents and the orchestrator (i.e., more than SDD phases). Renaming it (e.g., configurableAgentSet / agentNameSet) would reduce confusion when reading the filtering logic in ReadCurrentModelAssignments.
| ClaudePresetPerformance: "Maximum quality: opus for architecture, planning & verification phases", | ||
| ClaudePresetEconomy: "Cost-optimised: sonnet for all phases, haiku for archiving", | ||
| ClaudePresetDiversity: "Diversity: Opus for Judge A, Haiku for Judge B, Sonnet for fixes", | ||
| ClaudePresetCustom: "Pick the model alias for each SDD phase individually", |
There was a problem hiding this comment.
The "custom" preset description says it lets users pick a model for each SDD phase, but the custom phase list now also includes JD agents (and "default"). Update the description so it matches what the UI actually configures.
| ClaudePresetCustom: "Pick the model alias for each SDD phase individually", | |
| ClaudePresetCustom: "Pick the model alias for each SDD phase, JD agent, and general delegation entry individually", |
| var reservedProfileNames = map[string]bool{ | ||
| "default": true, | ||
| "sdd-orchestrator": true, | ||
| "jd-judge-a": true, | ||
| "jd-judge-b": true, | ||
| "jd-fix-agent": true, | ||
| } |
There was a problem hiding this comment.
JD agent names are duplicated as string literals in reservedProfileNames. Since there is already a central list in opencode.JDPhases(), consider building this map using that list (plus the existing reserved names) to avoid future drift if JD agents are renamed/added.
| var reservedProfileNames = map[string]bool{ | |
| "default": true, | |
| "sdd-orchestrator": true, | |
| "jd-judge-a": true, | |
| "jd-judge-b": true, | |
| "jd-fix-agent": true, | |
| } | |
| // Keep explicitly reserved non-JD names here, and derive JD agent names from | |
| // the canonical opencode.JDPhases() list to avoid drift. | |
| var reservedProfileNames = func() map[string]bool { | |
| names := map[string]bool{ | |
| "default": true, | |
| "sdd-orchestrator": true, | |
| } | |
| for _, name := range opencode.JDPhases() { | |
| names[name] = true | |
| } | |
| return names | |
| }() |
| @@ -212,8 +232,16 @@ func handleModelNav( | |||
| assignments[phase] = assignment | |||
| } | |||
| state.AllPhasesModel = assignment | |||
| case state.SelectedPhaseIdx == separatorIdx: | |||
| // Separator row ("--- Judgment Day ---") — no action, skip. | |||
| case state.SelectedPhaseIdx > separatorIdx: | |||
| // JD agent rows: map to JDPhases() after separator. | |||
| jdIdx := state.SelectedPhaseIdx - separatorIdx - 1 | |||
| if jdIdx < len(jdPhases) { | |||
| assignments[jdPhases[jdIdx]] = assignment | |||
| } | |||
There was a problem hiding this comment.
The "--- Judgment Day ---" separator row is treated as a selectable phase (it appears in ModelPickerRows), but selecting a model for it becomes a no-op in handleModelNav. In the current flow, pressing Enter on that row will still open provider/model selection and then silently apply nothing, which is confusing UX and looks like a broken selection.
Consider making the separator row non-actionable (e.g., skip it in the ScreenModelPicker enter logic / cursor navigation), or represent rows with metadata so the UI can render separators without allowing selection.
| @@ -279,8 +307,15 @@ func renderPhaseList( | |||
| b.WriteString(styles.SubtextStyle.Render("Current assignments:")) | |||
| b.WriteString("\n\n") | |||
|
|
|||
| rows := ModelPickerRows() | |||
| var rows []string | |||
| if state.ForProfile { | |||
| rows = ModelPickerRowsForProfile() | |||
| } else { | |||
| rows = ModelPickerRows() | |||
| } | |||
| phases := opencode.SDDPhases() | |||
| jdPhases := opencode.JDPhases() | |||
| separatorIdx := 2 + len(phases) | |||
|
|
|||
| for idx, row := range rows { | |||
| focused := idx == cursor | |||
| @@ -306,8 +341,29 @@ func renderPhaseList( | |||
| } else { | |||
| label = fmt.Sprintf("%-20s (not set)", row) | |||
| } | |||
| case idx == separatorIdx: | |||
| // Separator row — render as a visual divider with subtle indicator when focused. | |||
| if focused { | |||
| b.WriteString(styles.SubtextStyle.Render("▸ " + row) + "\n") | |||
| } else { | |||
| b.WriteString(styles.SubtextStyle.Render(" " + row) + "\n") | |||
| } | |||
| continue | |||
| case idx > separatorIdx: | |||
| // JD agent rows | |||
| jdIdx := idx - separatorIdx - 1 | |||
| if jdIdx < len(jdPhases) { | |||
| phase := jdPhases[jdIdx] | |||
| assignment, ok := assignments[phase] | |||
| if ok && assignment.ProviderID != "" { | |||
| provName, modelName := resolveNames(assignment, state) | |||
| label = fmt.Sprintf("%-20s %s / %s", row, provName, modelName) | |||
| } else { | |||
| label = fmt.Sprintf("%-20s (default)", row) | |||
| } | |||
| } | |||
| default: | |||
There was a problem hiding this comment.
New JD-row mapping and separator behavior in handleModelNav/renderPhaseList isn’t covered by tests (e.g., selecting the separator should not change assignments, and selecting each JD row should set the expected jd-* key). Since this logic is index-based, a small row-order change could silently mis-assign models.
Add unit tests that exercise the separator index and the JD indices (first/last JD row) to lock in the mapping.
… separator, tests
🔗 Linked Issue
Closes #208
🏷️ PR Type
type:feature— New feature (non-breaking change that adds functionality)📝 Summary
Adds 3 configurable judgment-day agents (
jd-judge-a,jd-judge-b,jd-fix-agent) to the OpenCode overlay system, enabling users to assign different models per JD agent through the TUI model picker.Key design decisions:
sdd-overlay-multi.jsonalongside SDD phases, since the JD skill is distributed with SDD📂 Changes
internal/assets/opencode/sdd-overlay-multi.jsoninternal/opencode/models.goJDPhases()andConfigurableAgentPhases()for dynamic agent discoveryinternal/components/sdd/inject.goisJDAgent()helper, root model exclusioninternal/model/claude_model.goClaudeModelPresetDiversity()internal/components/sdd/read_assignments.gobuildConfigurableAgentSet()usingConfigurableAgentPhases()internal/components/sdd/profiles.goGenerateProfileOverlay(), reserved profile namesinternal/tui/screens/model_picker.goModelPickerRowsForProfile()internal/tui/screens/claude_model_picker.goclaudePhases/labels, Diversity preset wired ininternal/tui/screens/profile_create.goModelPickerRowsForProfile()to exclude JD from profilesinternal/tui/model.goForProfileflag for profile create/edit flowsinternal/assets/skills/judgment-day/SKILL.mdinternal/assets/claude/agents/jd-*.md{{CLAUDE_MODEL}}sentinelinternal/assets/kiro/agents/jd-*.md{{KIRO_MODEL}}sentinelinternal/components/uninstall/service.gosddPhaseAgentscleanup list🧪 Test Plan
Unit Tests
go test ./...E2E Tests (Docker required)
go test ./...)cd e2e && ./docker-test.sh)sync --sdd-mode multiagainst temp configmode=subagent,hidden=true, no model inherited from rootjd-judge-a: allow,jd-judge-b: allow,jd-fix-agent: allow🤖 Automated Checks
The following checks run automatically on this PR:
Closes/Fixes/Resolves #Nstatus:approvedtype:*Labeltype:*label must be appliedgo test ./...must passcd e2e && ./docker-test.shmust pass✅ Contributor Checklist
status:approvedtype:*label to this PRgo test ./...)cd e2e && ./docker-test.sh)Co-Authored-Bytrailers💬 Notes for Reviewers
Diversitypreset (Opus for deep reasoning on Judge A, Haiku for fast pattern matching on Judge B, Sonnet for Fix) is the recommended default for perspective diversity