Skip to content

feat(sdd): TUI model picker integration for JD agents#477

Open
danielgap wants to merge 26 commits into
Gentleman-Programming:mainfrom
danielgap:feat/jd-3-tui
Open

feat(sdd): TUI model picker integration for JD agents#477
danielgap wants to merge 26 commits into
Gentleman-Programming:mainfrom
danielgap:feat/jd-3-tui

Conversation

@danielgap
Copy link
Copy Markdown

@danielgap danielgap commented May 9, 2026

🔗 Linked Issue

Closes #208


🏷️ PR Type

  • type:feature — New feature (non-breaking change that adds functionality)

📝 Summary

Adds TUI model picker integration for judgment-day agents: JD rows with separator, profile isolation, non-selectable separator, cursor skip navigation, Diversity preset, and comprehensive test coverage.

This is PR 3 of 3 in a chained approach. Depends on #475 (foundation) and #476 (agent assets).


Cross-Agent Contract

Agent JD agents in overlay Model assignments Prompt files TUI picker
OpenCode #475 #475 N/A (uses overlay) This PR
Claude Code #476 This PR
Kiro #476 This PR

End-to-End Support Matrix

Capability OpenCode Claude Code Kiro
JD agent definitions #475 (overlay) #476 (prompt files) #476 (prompt files)
Model assignment presets #475 #476 (sentinels) #476 (sentinels)
TUI model picker rows ✅ This PR ✅ This PR ✅ This PR
Profile isolation (excluded) ✅ This PR ✅ This PR ✅ This PR
Separator + cursor skip ✅ This PR ✅ This PR ✅ This PR
Diversity preset ✅ This PR ✅ This PR ✅ This PR
Uninstall cleanup #476 #476

All three agents have complete JD model configurability after this chain.


📂 Changes

File / Area What Changed
internal/tui/screens/model_picker.go JD rows, separator guard, SeparatorRowIdx(), ModelPickerRowsForProfile(), updated title
internal/tui/screens/model_picker_test.go 9 new tests: separator no-op, JD first/last/each row, profile row count/no-separator/no-JD
internal/tui/screens/claude_model_picker.go JD phases/labels, Diversity preset, updated custom description
internal/tui/screens/profile_create.go Uses ModelPickerRowsForProfile() to exclude JD
internal/tui/model.go Separator skip in cursor navigation (up/down/j/k/enter), ForProfile flag
internal/tui/model.go SeparatorRowIdx() helper, non-selectable separator
internal/components/sdd/profiles.go Dynamic reservedProfileNames from JDPhases(), JD global permissions
internal/components/sdd/profiles_test.go Updated permission expectations for JD
internal/components/sdd/inject_test.go Agent count updated to 14
internal/components/sdd/read_assignments.go configurableAgentSet (renamed from sddPhaseSet)

🧪 Test Plan

  • Unit tests pass (go test ./...)
  • 9 new tests: separator skip, JD row mapping (first/last/each), profile isolation
  • Manually tested: TUI shows JD rows with separator, separator is not selectable
  • Profile creation excludes JD agents
  • All 4 presets (Sonnet, Opus, Haiku, Diversity) assign correctly

✅ 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 ./...)
  • My commits follow Conventional Commits format
  • My commits do not include Co-Authored-By trailers

Chain Context

Field Value
Chain JD Configurable Models (#208)
Tracker PR Not needed (stacked to main)
Position 3 of 3
Base main
Depends on #475, #476
Follow-up None
Review budget 425 / 400
Starts at #476 merged (all JD assets ready)
Ends with Full JD model configurability in TUI for all agents

Chain Overview

main
 └── #475 PR 1 (foundation — OpenCode overlay + model tables)
      └── #476 PR 2 (agent assets — Claude Code + Kiro prompt files)
           └── 📍 #477 This PR (TUI integration — validates all agents end-to-end)

Scope

Autonomy

  • This PR has one deliverable scope
  • This PR can be rolled back without unrelated changes
  • Tests cover all new behaviors

Copilot AI review requested due to automatic review settings May 9, 2026 00:03
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

This PR extends the SDD TUI model-configuration flows to incorporate Judgment Day (JD) agents, including UI row separation/skip behavior, profile isolation (JD excluded from profile-scoped pickers), and preset/policy updates so JD agents can be configured and permitted consistently.

Changes:

  • Adds JD rows + a non-actionable separator to the model picker UI, plus cursor-navigation logic to skip the separator.
  • Introduces profile-scoped row filtering (ModelPickerRowsForProfile) so JD agents are excluded during profile create/edit.
  • Expands SDD profile permissions / model-assignment reading logic to treat JD agents as configurable/global, and updates related tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/tui/screens/profile_create.go Uses profile-filtered model picker rows for correct option counts during profile creation.
internal/tui/screens/model_picker.go Adds JD rows + separator, profile filtering, separator index helper, and JD assignment mapping/rendering.
internal/tui/screens/model_picker_test.go Adds tests for separator no-op, JD row mapping, and profile row filtering.
internal/tui/screens/claude_model_picker.go Adds a Diversity preset and includes JD phases/labels in custom assignments list.
internal/tui/model.go Skips the separator during cursor navigation and blocks selecting it. Marks picker state as profile-scoped.
internal/components/sdd/read_assignments.go Broadens assignment reading to “configurable agents” (intended: SDD + JD + orchestrator) via a set.
internal/components/sdd/profiles.go Reserves JD agent names for profiles and grants JD permissions globally in generated overlays.
internal/components/sdd/profiles_test.go Adjusts expected permissions behavior depending on single vs multi overlays and JD inclusion.
internal/components/sdd/inject_test.go Updates expected agent count for multi overlay to include JD agents.
Comments suppressed due to low confidence (2)

internal/tui/screens/model_picker.go:263

  • SeparatorRowIdx() can return -1 when there are no JD phases, but the switch uses state.SelectedPhaseIdx > separatorIdx which becomes true for all rows when separatorIdx == -1. That causes SDD sub-agent selections (idx >= 2) to be treated as JD rows and no assignment will be applied. Add a guard so the JD-row branch only runs when separatorIdx >= 0 (and similarly treat the separator case only when present).
		phases := opencode.SDDPhases()
		jdPhases := opencode.JDPhases()
		separatorIdx := SeparatorRowIdx()
		switch {
		case state.SelectedPhaseIdx == 0:
			// "gentle-orchestrator" row — assign only to the base orchestrator key
			assignments[SDDOrchestratorPhase] = assignment
		case state.SelectedPhaseIdx == 1:
			// "Set all phases" — sets only the 9 sub-agents, NOT the orchestrator.
			// Also update AllPhasesModel so the label stays in sync with the last
			// "Set all" action (Issue #146: individual phase selections must NOT touch this).
			for _, phase := range phases {
				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
			}
		default:
			// SDD sub-agent rows start at idx 2; phases[idx-2] is the correct phase.
			// Individual selection intentionally does NOT update AllPhasesModel (Issue #146).
			phaseIdx := state.SelectedPhaseIdx - 2
			if phaseIdx < len(phases) {
				assignments[phases[phaseIdx]] = assignment
			}
		}

internal/components/sdd/read_assignments.go:25

  • opencode.ConfigurableAgentPhases() is called here, but the current internal/opencode package in this repo does not define it. This makes the build fail. Either add/merge that API (and its tests) or fall back to composing from existing phase lists (e.g., SDD + JD) within this package.
// configurableAgentSet is the set of valid agent names that may appear in
// opencode.json. It includes SDD phases, JD agents, and the gentle-orchestrator coordinator.
var configurableAgentSet = buildConfigurableAgentSet()

func buildConfigurableAgentSet() map[string]bool {
	phases := opencode.ConfigurableAgentPhases() // SDD + JD phases
	set := make(map[string]bool, len(phases)+1)
	for _, p := range phases {
		set[p] = true
	}
	set["gentle-orchestrator"] = true
	// Backward-compatible read alias for configs that have not been synced yet.
	set["sdd-orchestrator"] = true
	return set

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 89 to +97
func ModelPickerRows() []string {
rows := make([]string, 0, 11)
rows := make([]string, 0, 2+len(opencode.SDDPhases())+1+len(opencode.JDPhases()))
rows = append(rows, SDDOrchestratorPhase)
rows = append(rows, "Set all SDD phases")
rows = append(rows, opencode.SDDPhases()...)
if len(opencode.JDPhases()) > 0 {
rows = append(rows, "--- Judgment Day ---")
rows = append(rows, opencode.JDPhases()...)
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — JDPhases() is defined in PR #475 (foundation). Compiles when chain is applied in order.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — JDPhases() is defined in PR #475 (foundation). Compiles when chain is applied in order.

Comment thread internal/tui/screens/model_picker.go Outdated
var b strings.Builder

b.WriteString(styles.TitleStyle.Render("Assign Models to SDD Phases"))
b.WriteString(styles.TitleStyle.Render("Assign Models to SDD Phases & JD Agents"))
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e734e9d — title is now conditional on state.ForProfile.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e734e9d — title is now conditional on state.ForProfile.

Comment on lines 329 to +377
@@ -306,8 +354,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)
}
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

When JDPhases() is empty, ModelPickerRows() does not add the separator or any JD rows, so the switch never encounters separatorIdx=-1. Guarded by len(JDPhases()) > 0 in ModelPickerRows().

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Guarded by len(JDPhases()) > 0 in ModelPickerRows() — when empty, no separator or JD rows are added.

Comment on lines 22 to +33
// reservedProfileNames are names that may not be used as profile names.
var reservedProfileNames = map[string]bool{
"default": true,
"sdd-orchestrator": true,
}
// JD agent names are derived from opencode.JDPhases() to avoid drift
// when agents are renamed or added.
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
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — JDPhases() is defined in PR #475 (foundation). Compiles when chain is applied in order.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — JDPhases() is defined in PR #475 (foundation). Compiles when chain is applied in order.

ClaudePresetBalanced: model.ClaudeModelPresetBalanced,
ClaudePresetPerformance: model.ClaudeModelPresetPerformance,
ClaudePresetEconomy: model.ClaudeModelPresetEconomy,
ClaudePresetDiversity: model.ClaudeModelPresetDiversity,
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — ClaudeModelPresetDiversity is defined in PR #475. Compiles when chain is applied in order.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — ClaudeModelPresetDiversity is defined in PR #475. Compiles when chain is applied in order.

Comment on lines +1189 to +1191
// Multi overlay must contain gentle-orchestrator + 10 sub-agents + 3 JD agents = 14 agents.
if len(agentMap) != 14 {
t.Fatalf("agent count = %d, want 14", len(agentMap))
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — the overlay is updated in PR #475 to include JD agents. Agent count matches when chain is applied.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — overlay updated in PR #475 to include JD agents. Agent count matches when chain is applied.

Copy link
Copy Markdown
Contributor

@Alan-TheGentleman Alan-TheGentleman left a comment

Choose a reason for hiding this comment

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

This final slice needs to follow the collaboration guide and prove the whole chain works for both OpenCode and Claude Code.

Please add the missing type:feature label, document the final end-to-end support matrix, and include verification that the TUI/profile behavior does not only work for OpenCode while leaving Claude Code JD agents unconfigured or untestable.

@danielgap
Copy link
Copy Markdown
Author

@Alan-TheGentleman Could you add the type:feature label to this PR? I don't have permissions to add labels. Thanks!

@Alan-TheGentleman Alan-TheGentleman added the type:feature New feature label May 10, 2026
@Alan-TheGentleman
Copy link
Copy Markdown
Contributor

Alan-TheGentleman commented May 10, 2026

Added the type:feature label. Thanks for flagging the permission issue.

Copilot AI review requested due to automatic review settings May 11, 2026 12:27
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

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Comment thread internal/tui/screens/model_picker.go Outdated
Comment on lines 251 to 274
@@ -230,8 +263,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
}
default:
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

When JDPhases() is empty, ModelPickerRows() does not add the separator or any JD rows, so the switch never encounters separatorIdx=-1 in practice. The guard len(JDPhases()) > 0 in ModelPickerRows() ensures this.

Comment on lines +379 to 400
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
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same as above — when JDPhases() is empty, no separator is added to rows and the render switch never enters the JD branch with separatorIdx=-1.

Comment on lines 107 to +116
func ModelPickerRows() []string {
rows := make([]string, 0, 11)
rows := make([]string, 0, 2+len(opencode.SDDPhases())+1+len(opencode.JDPhases()))
rows = append(rows, SDDOrchestratorPhase)
rows = append(rows, "Set all SDD phases")
rows = append(rows, opencode.SDDPhases()...)
if len(opencode.JDPhases()) > 0 {
rows = append(rows, "--- Judgment Day ---")
rows = append(rows, opencode.JDPhases()...)
}
return rows
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This PR is the third slice in a stacked chain. JDPhases() is defined in PR #475 (foundation) which is the base for this branch. The code compiles when the chain is applied in order.

Comment thread internal/tui/screens/model_picker.go Outdated
var b strings.Builder

b.WriteString(styles.TitleStyle.Render("Assign Models to SDD Phases"))
b.WriteString(styles.TitleStyle.Render("Assign Models to SDD Phases & JD Agents"))
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Valid point, but low priority — ForProfile mode uses ModelPickerRowsForProfile() which excludes JD rows, so the title is technically wrong but doesn't mislead functionally. Noted for a future polish pass.

Comment on lines +12 to 20
// configurableAgentSet is the set of valid agent names that may appear in
// opencode.json. It includes SDD phases, JD agents, and the gentle-orchestrator coordinator.
var configurableAgentSet = 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)
for _, p := range phases {
set[p] = true
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — ConfigurableAgentPhases is defined in PR #475. Compiles when chain is applied in order.

Comment on lines 104 to 110
// presetConstructors maps preset IDs to their constructor functions.
var presetConstructors = map[ClaudeModelPreset]func() map[string]model.ClaudeModelAlias{
ClaudePresetBalanced: model.ClaudeModelPresetBalanced,
ClaudePresetPerformance: model.ClaudeModelPresetPerformance,
ClaudePresetEconomy: model.ClaudeModelPresetEconomy,
ClaudePresetDiversity: model.ClaudeModelPresetDiversity,
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — ClaudeModelPresetDiversity is defined in PR #475. Compiles when chain is applied in order.

Comment on lines 22 to +33
// reservedProfileNames are names that may not be used as profile names.
var reservedProfileNames = map[string]bool{
"default": true,
"sdd-orchestrator": true,
}
// JD agent names are derived from opencode.JDPhases() to avoid drift
// when agents are renamed or added.
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
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Stacked PR — JDPhases() is defined in PR #475 (foundation). Compiles when chain is applied in order.

@Alan-TheGentleman
Copy link
Copy Markdown
Contributor

This is close enough to keep as one review unit.

The PR is 425 changed lines, just over the 400-line budget, and the extra diff is mostly focused test/UI integration coverage. Please add or request the size:exception label with that rationale documented, then rebase after #475 is in the base so CI validates the actual merge state.

Copilot AI review requested due to automatic review settings May 14, 2026 22:25
@danielgap
Copy link
Copy Markdown
Author

@Alan-TheGentleman Rebased all 3 PRs onto latest main — conflicts resolved, all MERGEABLE now. CI should pick up the checks.

Regarding the 425-line budget: requesting size:exception label. The extra 25 lines are focused test coverage (9 new tests for JD row rendering, separator skip, and profile isolation) plus the JD exclusion test. The implementation diff itself is well within budget.

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

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

"sdd-verify": "Verify",
"sdd-archive": "Archive",
"default": "General delegation",
"orchestrator": "Orchestrator",
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e734e9d — updated docstring and removed the unused orchestrator entry from claudePhaseLabels.

"default",
}

// claudePhaseLabels are the human-readable labels for each SDD phase.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed in e734e9d — the orchestrator entry was dead since claudePhases never includes it.

Comment thread internal/tui/model.go Outdated
Comment on lines +987 to +990
// If wrap-around landed on the separator row, skip past it.
if m.Screen == ScreenModelPicker && !m.ModelPicker.ForProfile && m.Cursor == screens.SeparatorRowIdx() && m.Cursor > 0 {
m.Cursor--
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed in e734e9d — the separator can never be at count-1 (always followed by JD rows), so the wrap-around check was unreachable dead code.

Comment thread internal/tui/model.go
Comment on lines 999 to +1074
@@ -1012,6 +1020,12 @@ func (m Model) handleKeyPress(key tea.KeyMsg) (tea.Model, tea.Cmd) {
m.BackupScroll = m.Cursor - screens.BackupMaxVisible + 1
}
}
// Skip separator row in model picker — it is not selectable.
if m.Screen == ScreenModelPicker && !m.ModelPicker.ForProfile && m.Cursor == screens.SeparatorRowIdx() {
if m.Cursor+1 < count {
m.Cursor++
}
}
return m, nil
case "k":
count := m.optionCount()
@@ -1021,6 +1035,10 @@ func (m Model) handleKeyPress(key tea.KeyMsg) (tea.Model, tea.Cmd) {
} else if !m.isScrollableScreen() {
// Issue #150: wrap-around — Up at 0 goes to last option.
m.Cursor = count - 1
// If wrap-around landed on the separator row, skip past it.
if m.Screen == ScreenModelPicker && !m.ModelPicker.ForProfile && m.Cursor == screens.SeparatorRowIdx() && m.Cursor > 0 {
m.Cursor--
}
}
}
// Adjust scroll for the backup list.
@@ -1029,6 +1047,10 @@ func (m Model) handleKeyPress(key tea.KeyMsg) (tea.Model, tea.Cmd) {
m.BackupScroll = m.Cursor
}
}
// Skip separator row in model picker — it is not selectable.
if m.Screen == ScreenModelPicker && !m.ModelPicker.ForProfile && m.Cursor == screens.SeparatorRowIdx() && m.Cursor > 0 {
m.Cursor--
}
return m, nil
case "j":
count := m.optionCount()
@@ -1044,6 +1066,12 @@ func (m Model) handleKeyPress(key tea.KeyMsg) (tea.Model, tea.Cmd) {
m.BackupScroll = m.Cursor - screens.BackupMaxVisible + 1
}
}
// Skip separator row in model picker — it is not selectable.
if m.Screen == ScreenModelPicker && !m.ModelPicker.ForProfile && m.Cursor == screens.SeparatorRowIdx() {
if m.Cursor+1 < count {
m.Cursor++
}
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Noted — good suggestion for a future refactor. Keeping as-is for now to minimize diff.

label = fmt.Sprintf("%-20s %s / %s", row, provName, modelName)
} else {
label = fmt.Sprintf("%-20s (default)", row)
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e734e9d — title is now conditional on state.ForProfile: shows 'SDD Phases & JD Agents' normally, 'SDD Phases' in profile mode.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Theoretical — jdIdx >= len(jdPhases) can't happen since ModelPickerRows() and SeparatorRowIdx() are derived from the same opencode.JDPhases() source.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e734e9d — title now conditional on state.ForProfile.

Comment thread internal/tui/screens/model_picker.go Outdated
var b strings.Builder

b.WriteString(styles.TitleStyle.Render("Assign Models to SDD Phases"))
b.WriteString(styles.TitleStyle.Render("Assign Models to SDD Phases & JD Agents"))
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Theoretical — jdIdx >= len(jdPhases) can't happen since ModelPickerRows() and SeparatorRowIdx() are derived from the same opencode.JDPhases() source. Added as a known theoretical edge case.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Theoretical — jdIdx >= len(jdPhases) can't happen since ModelPickerRows() and SeparatorRowIdx() are derived from the same opencode.JDPhases() source.

@danielgap
Copy link
Copy Markdown
Author

@Alan-TheGentleman Noted on all points:

@Alan-TheGentleman Alan-TheGentleman added type:feature New feature and removed type:feature New feature labels May 27, 2026
@Alan-TheGentleman
Copy link
Copy Markdown
Contributor

@danielgap — heads-up on this PR (and the rest of the JD chain #475 / #476 / #477):

Unit Tests are failing on:

  • TestGoldenSDD_OpenCode_Multi
  • TestInjectOpenCodeMultiMode
  • TestDefaultOverlayTaskPermissions_ExplicitAllowlist

These are golden-file tests over the OpenCode SDD overlay. Since the JD agent definitions modify the generated overlay content, the golden fixtures need to be regenerated.

Suggested fix:

  1. Rebase against current main (we shipped v1.31.0 yesterday + a few more fixes today; some overlay infra may have shifted).
  2. Regenerate goldens: go test ./internal/components/... -update (or whatever your -update flag is — check internal/components/golden_test.go for the convention).
  3. Verify the regenerated goldens match the expected diff (your JD entries in the overlay, nothing else).
  4. Push.

Once green, I'll review the chain (PR 1 → 2 → 3 in order) for merge. Thanks for sticking with this — the JD chain is great work and I want to land it.

danielgap added 4 commits May 27, 2026 16:14
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
danielgap added 21 commits May 27, 2026 16:14
- ClaudeModelPresetDiversity now clones the base map before mutating,
  preventing corruption if ClaudeModelPresetBalanced is ever cached.
- isJDAgent uses a package-level set instead of linear scan, consistent
  with sddPhaseSet pattern in read_assignments.go.
OpenCode overlays only use {read, write, edit, bash} as tool keys.
glob and grep are not referenced anywhere in the codebase and may cause
config rejection. Judges only need read and bash for code review.
- Regenerate golden file (sdd-opencode-multi-settings.golden) to include
  jd-judge-a, jd-judge-b, and jd-fix-agent entries
- Update TestInjectOpenCodeMultiMode agent count: 11 → 14
  (gentle-orchestrator + 10 SDD phases + 3 JD agents)
- Update TestDefaultOverlayTaskPermissions_ExplicitAllowlist to expect
  JD agent permissions in the multi overlay only
- Update TUI model picker tests to expect orchestrator in balanced
  preset (now a configurable assignment in the model table)
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 flagged that delegate(prompt='...') without agent contradicts
OpenCode's delegate schema. Make explicit that the generic syntax is
for adapters that don't support named agents, and the model is
controlled by adapter-native mechanisms (e.g. model sentinels).
Instead of maintaining a manually duplicated list that can go stale
when new phases are added, derive sddSkillPhaseIDs by filtering
sddPhaseAgents for entries with the sdd- prefix excluding the
orchestrator. JD agents (jd-*) are automatically excluded.

Addresses Copilot review: keeps both lists in sync without manual
maintenance.
The slice now contains both SDD phase agents and JD sub-agents,
so the old name was misleading. configuredAgents better reflects
that it lists all agents the uninstaller should clean up from
opencode.json.

Addresses Copilot review.
The overlay now uses gentle-orchestrator (renamed from sdd-orchestrator).
Keep sdd-orchestrator for backward-compat cleanup of old user configs.
gentle-orchestrator is correctly excluded from sddSkillPhaseIDs by the
sdd- prefix filter.

Addresses Copilot review.
Update TestClaudeEmbeddedAssetLayout expected agent count from 10 to 13
to account for jd-judge-a.md, jd-judge-b.md, and jd-fix-agent.md.
…ratorRowIdx, add profile tests, update title
When navigating UP from index 0, wrap-around sets cursor to count-1.
If that row is the separator, the existing skip guard at the bottom
of the handler would run once and decrement — but this relied on a
single guard pass. The fix adds an explicit re-check immediately
after the wrap-around assignment in both 'up' and 'k' handlers,
ensuring the separator is never selected regardless of layout changes.
Adds TestInjectOpenCodeMultiModeJDAgentsExcludedFromRootModel that
asserts JD agents (jd-judge-a, jd-judge-b, jd-fix-agent) do NOT
receive the root model when no explicit assignment exists, while
SDD agents still do. This prevents regressions in the diversity
guarantee.
…date docs

- model_picker.go: title now conditional on ForProfile — shows
  'SDD Phases & JD Agents' normally, 'SDD Phases' in profile mode
- model.go: remove unreachable wrap-around separator check (separator
  is never at count-1 position)
- claude_model_picker.go: update claudePhaseLabels docstring to
  include JD agents, remove unused 'orchestrator' entry
- Update TestRenderModelPickerShowsSetAllPhasesEffort to search for
  'Set all SDD phases' (renamed from 'Set all phases')
- Fix ProfileCreate tests to use ModelPickerRowsForProfile() instead
  of ModelPickerRows() — profile creation excludes JD agents
Copilot AI review requested due to automatic review settings May 27, 2026 15:06
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

Copilot reviewed 38 out of 41 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (3)

internal/tui/screens/model_picker.go:1

  • SeparatorRowIdx() explicitly returns -1 when there are no JD phases, but the switch uses state.SelectedPhaseIdx > separatorIdx unconditionally. If separatorIdx == -1, all selectable rows with idx >= 2 will match the “JD agent rows” branch and result in no assignment (since len(jdPhases)==0), breaking SDD phase selection. Fix by guarding the separator/JD branches on separatorIdx >= 0 (e.g., case separatorIdx >= 0 && state.SelectedPhaseIdx == separatorIdx and case separatorIdx >= 0 && state.SelectedPhaseIdx > separatorIdx). The same guard should also be applied in applyAssignment() and in phase-list rendering where idx > separatorIdx is used.
    internal/components/sdd/read_assignments.go:1
  • This change removes the previously documented backward-compat behavior where legacy sdd-orchestrator agent entries were read/mapped as gentle-orchestrator (so existing configs could be loaded before a sync migrates them). Since uninstall logic still references the legacy key, it’s likely some users still have it. Consider adding "sdd-orchestrator" into configurableAgentSet and mapping it to "gentle-orchestrator" when populating result (or keep a small explicit legacy alias map) so reading current assignments remains backward compatible.
    internal/components/sdd/read_assignments.go:1
  • This change removes the previously documented backward-compat behavior where legacy sdd-orchestrator agent entries were read/mapped as gentle-orchestrator (so existing configs could be loaded before a sync migrates them). Since uninstall logic still references the legacy key, it’s likely some users still have it. Consider adding "sdd-orchestrator" into configurableAgentSet and mapping it to "gentle-orchestrator" when populating result (or keep a small explicit legacy alias map) so reading current assignments remains backward compatible.

Comment thread .pi-lens/turn-state.json Outdated
Comment on lines +1 to +6
{
"files": {},
"turnCycles": 0,
"maxCycles": 3,
"lastUpdated": "2026-05-27T14:48:56.545Z"
} No newline at end of file
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants