Skip to content

feat(sdd): configurable models for judgment-day agents#383

Open
danielgap wants to merge 10 commits intoGentleman-Programming:mainfrom
danielgap:feat/issue-208-judgment-day-models
Open

feat(sdd): configurable models for judgment-day agents#383
danielgap wants to merge 10 commits intoGentleman-Programming:mainfrom
danielgap:feat/issue-208-judgment-day-models

Conversation

@danielgap
Copy link
Copy Markdown

🔗 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:

  • Named agents in SDD overlay — JD agents live in sdd-overlay-multi.json alongside SDD phases, since the JD skill is distributed with SDD
  • Root model exclusion — JD agents are intentionally excluded from inheriting the root model, enabling diversity of perspective between judges (different models = different viewpoints)
  • 4 presets: Sonnet, Opus, Haiku, and Diversity (Judge A=Opus, Judge B=Haiku, Fix=Sonnet)
  • Profile-safe — JD agents are excluded from profile flows (they're global workflow agents, not SDD phases)

📂 Changes

File / Area What Changed
internal/assets/opencode/sdd-overlay-multi.json 3 JD agent definitions + orchestrator delegation permissions
internal/opencode/models.go JDPhases() and ConfigurableAgentPhases() for dynamic agent discovery
internal/components/sdd/inject.go Model assignment rows, reasons, isJDAgent() helper, root model exclusion
internal/model/claude_model.go JD entries in 3 presets + ClaudeModelPresetDiversity()
internal/components/sdd/read_assignments.go buildConfigurableAgentSet() using ConfigurableAgentPhases()
internal/components/sdd/profiles.go JD global permissions in GenerateProfileOverlay(), reserved profile names
internal/tui/screens/model_picker.go JD rows, separator with focus indicator, ModelPickerRowsForProfile()
internal/tui/screens/claude_model_picker.go JD in claudePhases/labels, Diversity preset wired in
internal/tui/screens/profile_create.go Uses ModelPickerRowsForProfile() to exclude JD from profiles
internal/tui/model.go ForProfile flag for profile create/edit flows
internal/assets/skills/judgment-day/SKILL.md Conditional delegation (named agents for OpenCode, ad-hoc for others)
internal/assets/claude/agents/jd-*.md 3 Claude agent files with {{CLAUDE_MODEL}} sentinel
internal/assets/kiro/agents/jd-*.md 3 Kiro agent files with {{KIRO_MODEL}} sentinel
internal/components/uninstall/service.go JD agents in sddPhaseAgents cleanup list

🧪 Test Plan

Unit Tests

go test ./...

E2E Tests (Docker required)

cd e2e && ./docker-test.sh
  • Unit tests pass (go test ./...)
  • E2E tests pass (cd e2e && ./docker-test.sh)
  • Manually tested locally — full integration test with temp HOME:
    • Built binary from branch, ran sync --sdd-mode multi against temp config
    • Verified 15 agents in output (build + 11 SDD + 3 JD)
    • Verified JD agents: mode=subagent, hidden=true, no model inherited from root
    • Verified orchestrator permissions: jd-judge-a: allow, jd-judge-b: allow, jd-fix-agent: allow
    • Verified TUI model picker shows JD rows with separator
    • Verified presets (Sonnet, Opus, Haiku, Diversity) assign models correctly

🤖 Automated Checks

The following checks run automatically on this PR:

Check Status Description
Check Issue Reference PR body must contain Closes/Fixes/Resolves #N
Check Issue Has status:approved Linked issue must have been approved before work began
Check PR Has type:* Label Exactly one type:* label must be applied
Unit Tests go test ./... must pass
E2E Tests cd e2e && ./docker-test.sh must pass

✅ Contributor Checklist

  • PR is linked to an issue with status:approved
  • I have added the appropriate type:* label to this PR
  • Unit tests pass (go test ./...)
  • E2E tests pass (cd e2e && ./docker-test.sh)
  • I have updated documentation if necessary
  • My commits follow Conventional Commits format
  • My commits do not include Co-Authored-By trailers

💬 Notes for Reviewers

  • The 9 commits are organized in dependency order — each builds on the previous one
  • A judgment-day adversarial review was run on both the proposal AND the implementation (0 criticals)
  • The Diversity preset (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
  • No changes to single-mode overlay — JD delegation contradicts single-mode's "Do NOT delegate" instruction

danielgap added 9 commits April 26, 2026 17:30
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.
Create jd-judge-a, jd-judge-b, and jd-fix-agent agent files for
both Claude and Kiro backends. Claude agents use {{CLAUDE_MODEL}}
placeholder with explicit tool allowlists. Kiro agents use
{{KIRO_MODEL}} with @Builtin and @engram tool references.
Add jd-judge-a, jd-judge-b, and jd-fix-agent to sddPhaseAgents
so they are properly cleaned up during uninstall.
Copilot AI review requested due to automatic review settings April 26, 2026 18:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 12 to 18
// 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)
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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",
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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",

Copilot uses AI. Check for mistakes.
Comment thread internal/components/sdd/profiles.go Outdated
Comment on lines 23 to 29
var reservedProfileNames = map[string]bool{
"default": true,
"sdd-orchestrator": true,
"jd-judge-a": true,
"jd-judge-b": true,
"jd-fix-agent": true,
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
}()

Copilot uses AI. Check for mistakes.
Comment on lines 219 to +242
@@ -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
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 235 to 365
@@ -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:
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Configurable models for judgment-day in the TUI

2 participants