Skip to content

feat(vscode): add model assignment foundation#731

Open
Snakeblack wants to merge 2 commits into
Gentleman-Programming:mainfrom
Snakeblack:feat/vscode-copilot-subagents-pr2
Open

feat(vscode): add model assignment foundation#731
Snakeblack wants to merge 2 commits into
Gentleman-Programming:mainfrom
Snakeblack:feat/vscode-copilot-subagents-pr2

Conversation

@Snakeblack

@Snakeblack Snakeblack commented Jun 1, 2026

Copy link
Copy Markdown

🔗 Linked Issue

Closes #504

Related context: #677
Depends on: #708


🏷️ PR Type

What kind of change does this PR introduce?

  • type:bug — Bug fix (non-breaking change that fixes an issue)
  • type:feature — New feature (non-breaking change that adds functionality)
  • type:docs — Documentation only
  • type:refactor — Code refactoring (no functional changes)
  • type:chore — Build, CI, or tooling changes
  • type:breaking-change — Breaking change (fix or feature that changes existing behavior)

🔗 Chain Context

PR 2 of the VS Code Copilot SDD subagents stack.

#708 PR1 native VS Code `.agent.md` agents
   ↓
📍 PR2 VS Code model assignment foundation
   ↓
#738 PR3 VS Code Copilot agent install hardening
   ↓
#740 PR4 TUI + docs final

This PR depends on #708. Because #708 is still open and its branch exists only in the contributor fork, this PR targets main; once #708 lands, this PR can be rebased/updated and the GitHub diff will collapse to the PR2-only commit.

Focused review diff while #708 is open:
Snakeblack/gentle-ai@feat/vscode-copilot-subagents-pr1...feat/vscode-copilot-subagents-pr2


📝 Summary

  • Adds persisted VSCodeModelAssignments separate from OpenCode, Claude, and Kiro model assignment maps.
  • Adds an isolated VS Code resolver that reads dynamic github-copilot model cache data and safely renders optional model: frontmatter for native .agent.md files.
  • Propagates non-fatal warnings when assignments are stale, unresolved, unsupported, or cache data is missing, while preserving parent-model inheritance.

📂 Changes

File / Area What Changed
internal/model, internal/state Added VSCodeModelAssignments to selections, sync overrides, and persisted install state
internal/app, internal/cli Loads, persists, and forwards VS Code assignments through install and sync flows
internal/components/sdd/vscode_models.go New isolated resolver for Copilot model cache validation and .agent.md frontmatter rendering
internal/components/sdd/inject.go Applies VS Code model rendering during native agent sync and returns non-fatal warnings
Tests Added coverage for persistence separation, safe fallback, warning propagation, runtime key allowlist, injected-home cache paths, and idempotent rendering

🧠 Size Exception Rationale

This PR requests size:exception.

The change is larger than the 400-line budget because the behavior is intentionally cohesive: persistence, resolver validation, .agent.md rendering, warning propagation, and tests must be reviewed together to understand the safety contract. Splitting these pieces would make the implementation harder to reason about and would hide the important invariant: unresolved assignments must stay persisted but must not render unsafe model: frontmatter.

To reduce reviewer load, the new resolver files include concise behavior comments explaining why each helper exists and how failures degrade safely.


🧪 Test Plan

Focused PR2 tests

go test ./internal/model ./internal/state ./internal/app ./internal/cli ./internal/components/sdd -run "TestSelectionAndSyncOverridesHaveVSCodeModelAssignments|TestModelAssignmentsRoundTrip|TestApplyOverridesVSCodeModelAssignments|TestPersistAndLoadVSCodeModelAssignments|TestModelAssignmentsToStateSupportsVSCodeAssignments|TestVSCodeModelAssignmentKeysIncludeCoordinatorAndPhases|TestVSCodeAgentKeyRejectsUnknownNativeFiles|TestResolveVSCodeModelAssignment|TestInjectVSCodeRendersResolvedModelAssignment|TestInjectVSCodeDefaultModelCacheUsesInjectedHome|TestInjectVSCodeMissingModelCacheWarnsWithoutModelLine|TestRunSyncWithSelectionPropagatesVSCodeModelWarnings|TestRunSyncLoadsPersistedModelAssignments"

Package smoke

go test ./internal/cli

Static checks

go vet ./...
git diff --check

Full suite in CI-like Linux runner

env -i HOME=/tmp/opencode/home \
  PATH=/tmp/opencode/go/bin:/usr/bin:/bin \
  GOCACHE=/tmp/opencode/gocache \
  GOMODCACHE=/tmp/opencode/gomodcache \
  /tmp/opencode/go/bin/go test ./...

Results:

  • Focused PR2 tests pass
  • go test ./internal/cli passes
  • go vet ./... passes
  • git diff --check passes
  • go test ./... passes in clean WSL Ubuntu LF clone with Go 1.25.10
  • E2E tests pass (cd e2e && ./docker-test.sh)

Note: local Windows go test ./... still has baseline CRLF/env-sensitive failures unrelated to this PR. The full-suite gate was validated in a clean Linux/LF runner instead.


🤖 Automated Checks

Check Status Description
Check PR Cognitive Load ⚠️ Requests size:exception; behavior is cohesive and documented
Check Issue Reference PR body contains Closes #504
Check Issue Has status:approved #504 has status:approved
Check PR Has type:* Label type:feature requested (maintainer label may be needed)
Unit Tests CI should run go test ./...
E2E Tests CI should run Docker E2E if configured

✅ Contributor Checklist

  • PR is linked to an issue with status:approved
  • PR stays within 400 changed lines, or I have requested/obtained maintainer-applied size:exception with rationale documented — maintainer label needed (size:exception)
  • I have added the appropriate type:* label to this PR — maintainer label may be needed (type:feature)
  • Unit tests pass (go test ./...) in Linux/LF runner
  • E2E tests pass (cd e2e && ./docker-test.sh)
  • I have updated documentation if necessary — inline behavior comments document the new resolver surface; user-facing docs remain PR3 scope
  • My commits follow Conventional Commits format
  • My commits do not include Co-Authored-By trailers

💬 Notes for Reviewers

This is backend/model-assignment foundation only. It deliberately does not include the VS Code install hardening or the TUI/docs updates; those remain split into #738 and #740.

Key safety behavior to review:

  • unknown .agent.md files are ignored by the runtime assignment allowlist;
  • cache paths resolve from the injected home directory, not the process HOME;
  • invalid or unresolved assignments warn and omit model: instead of failing sync;
  • unresolved assignments remain persisted so a future cache refresh can resolve them.

Summary by CodeRabbit

  • New Features

    • Added support for VS Code Copilot native agents enabling spec-driven development workflows (initialize, explore, propose, design, task creation, apply, verify, archive, and onboarding phases).
    • Persisted and restored model assignments for VS Code agents across sessions.
  • Improvements

    • Surfaced sync operation warnings to users.
    • Enhanced cleanup process to remove only system-managed agent files during uninstall.

@Snakeblack

Copy link
Copy Markdown
Author

Maintainer label request: please apply exactly one type label, \ ype:feature\, and \size:exception\. The size exception is intentional: PR2's persistence + resolver + render + warning propagation must be reviewed as one cohesive safety contract, and the PR body documents the rationale.

@Snakeblack Snakeblack force-pushed the feat/vscode-copilot-subagents-pr2 branch from 763bbf0 to 3fe7acd Compare June 14, 2026 23:04
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Activates VS Code Copilot native sub-agent support: the VS Code adapter now reports sub-agent capability and targets ~/.copilot/agents; 11 SDD phase .agent.md files are embedded; a new model resolver injects a validated model: frontmatter line from the OpenCode cache; and VSCodeModelAssignments is threaded through install, sync, persist/restore, and override paths, with non-fatal cache warnings surfaced in SyncResult.

Changes

VS Code Copilot SDD Native Sub-Agent Support

Layer / File(s) Summary
VSCodeModelAssignments data model and state contracts
internal/model/selection.go, internal/state/state.go, internal/model/selection_test.go, internal/state/state_test.go
Selection and SyncOverrides gain VSCodeModelAssignments map[string]ModelAssignment; InstallState gains the JSON-persisted vscode_model_assignments field; MergeAgents carries the field through; backward-compat nil behavior tested.
VS Code adapter sub-agent activation
internal/agents/vscode/adapter.go, internal/agents/vscode/adapter_test.go
SupportsSubAgents() returns true, SubAgentsDir(homeDir) returns ~/.copilot/agents, EmbeddedSubAgentsDir() returns "vscode/agents"; new test asserts all three and the absence of workspace .github/agents.
Embedded VS Code SDD agent markdown files
internal/assets/assets.go, internal/assets/vscode/agents/*, internal/assets/assets_test.go
11 .agent.md files added (orchestrator + 10 SDD phases: init, explore, propose, spec, design, tasks, apply, verify, archive, onboard); //go:embed updated to include all:vscode; TestVSCodeNativeAgentAssetsFrontmatter validates frontmatter, tools allowlist, and body content for all assets.
VS Code model assignment resolver
internal/components/sdd/vscode_models.go, internal/components/sdd/vscode_models_test.go
New renderVSCodeAgentModelAssignment rewrites model: frontmatter by loading the OpenCode models cache, validating provider/model/tool-call support and effort variants; cache-miss and invalid-effort produce recoverable warnings with no model: emitted; dedupWarnings utility added.
SDD inject: VSCode model rendering and warnings
internal/components/sdd/inject.go, internal/components/sdd/inject_test.go, internal/components/uninstall/service_test.go
InjectOptions gains VSCodeModelAssignments, VSCodeModelCachePath, VSCodeModelVariantsPath; InjectionResult gains Warnings; VSCode Copilot native sub-agent copy loop calls renderVSCodeAgentModelAssignment; .yaml added to post-injection validity check; uninstall test verifies only managed agent files are removed.
CLI pipeline: persist/restore and warnings propagation
internal/cli/sync.go, internal/cli/run.go, internal/app/app.go, internal/cli/sync_test.go, internal/app/app_test.go, internal/cli/install_test.go
VSCodeModelAssignments threaded through RunInstall state persistence, RunSync restore from state.json, applyOverrides, loadPersistedAssignments, persistAssignments, and sdd.InjectOptions; SyncResult.Warnings added with appendSyncWarnings renderer; dedupStrings and stateModelAssignmentsToModel helpers introduced.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant RunSync
  participant loadPersistedAssignments
  participant sdd.Inject
  participant renderVSCodeAgentModelAssignment
  participant resolveVSCodeModelAssignment
  participant loadVSCodeModelCache
  participant SyncResult

  rect rgba(100, 149, 237, 0.5)
    note over RunSync,loadPersistedAssignments: Restore phase
    RunSync->>loadPersistedAssignments: read state.json VSCodeModelAssignments
    loadPersistedAssignments-->>RunSync: selection.VSCodeModelAssignments populated
  end

  rect rgba(60, 179, 113, 0.5)
    note over RunSync,sdd.Inject: Injection phase
    RunSync->>sdd.Inject: InjectOptions{VSCodeModelAssignments, VSCodeModelCachePath}
    loop for each .agent.md file
      sdd.Inject->>renderVSCodeAgentModelAssignment: filename + assignments
      renderVSCodeAgentModelAssignment->>resolveVSCodeModelAssignment: lookup key
      resolveVSCodeModelAssignment->>loadVSCodeModelCache: read models.json + variants.json
      loadVSCodeModelCache-->>resolveVSCodeModelAssignment: catalog or cache-miss warning
      resolveVSCodeModelAssignment-->>renderVSCodeAgentModelAssignment: label or warning
      renderVSCodeAgentModelAssignment-->>sdd.Inject: rewritten content + warnings
    end
    sdd.Inject-->>RunSync: InjectionResult{Files, Warnings}
  end

  rect rgba(255, 165, 0, 0.5)
    note over RunSync,SyncResult: Result phase
    RunSync->>SyncResult: dedupStrings(warnings) -> Warnings
    SyncResult-->>User: RenderSyncReport with Warnings section
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

type:feature, size:exception

Suggested reviewers

  • Alan-TheGentleman
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(vscode): add model assignment foundation' directly reflects the main objective: establishing the model assignment system for VS Code Copilot integration, which encompasses persistence, resolver validation, and frontmatter rendering.
Linked Issues check ✅ Passed The PR comprehensively implements the backend scope of issue #504: activates VS Code sub-agent support in the adapter, embeds 10 SDD .agent.md templates under vscode/agents, implements resolver validation and safe model-assignment rendering with warnings, and extends post-injection verification to accept .yaml extensions.
Out of Scope Changes check ✅ Passed All changes align with the stated backend scope from #504 and PR objectives. No TUI, external tooling, or unrelated modifications are present; changes are focused on model assignment persistence, resolver logic, and agent file generation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/app/app.go`:
- Around line 749-751: The condition in the VSCodeModelAssignments block at the
if statement is checking if the length is greater than zero, which prevents
explicit clearing when a non-nil empty map is passed. Change the condition from
checking len(selection.VSCodeModelAssignments) > 0 to instead checking if
selection.VSCodeModelAssignments is not nil. This will allow the
modelAssignmentsToState function to process empty maps that represent an
explicit clear intent, while still allowing the caller to pass nil if they want
to skip processing assignments entirely.

In `@internal/cli/run.go`:
- Around line 183-184: The mergeExplicitAgentInstallState function is not
copying the VSCodeModelAssignments field when merging state, causing newly
selected VS Code assignments to be lost when running install with an existing
state file. Add VSCodeModelAssignments to the field assignments in the
mergeExplicitAgentInstallState function to ensure the value set by RunInstall is
preserved during the state merge operation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 415afe5b-2b1d-41d7-883e-cb87ffe57903

📥 Commits

Reviewing files that changed from the base of the PR and between bcd231f and 3fe7acd.

📒 Files selected for processing (30)
  • internal/agents/vscode/adapter.go
  • internal/agents/vscode/adapter_test.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/assets/assets.go
  • internal/assets/assets_test.go
  • internal/assets/vscode/agents/sdd-apply.agent.md
  • internal/assets/vscode/agents/sdd-archive.agent.md
  • internal/assets/vscode/agents/sdd-design.agent.md
  • internal/assets/vscode/agents/sdd-explore.agent.md
  • internal/assets/vscode/agents/sdd-init.agent.md
  • internal/assets/vscode/agents/sdd-onboard.agent.md
  • internal/assets/vscode/agents/sdd-orchestrator.agent.md
  • internal/assets/vscode/agents/sdd-propose.agent.md
  • internal/assets/vscode/agents/sdd-spec.agent.md
  • internal/assets/vscode/agents/sdd-tasks.agent.md
  • internal/assets/vscode/agents/sdd-verify.agent.md
  • internal/cli/install_test.go
  • internal/cli/run.go
  • internal/cli/sync.go
  • internal/cli/sync_test.go
  • internal/components/sdd/inject.go
  • internal/components/sdd/inject_test.go
  • internal/components/sdd/vscode_models.go
  • internal/components/sdd/vscode_models_test.go
  • internal/components/uninstall/service_test.go
  • internal/model/selection.go
  • internal/model/selection_test.go
  • internal/state/state.go
  • internal/state/state_test.go

Comment thread internal/app/app.go
Comment on lines +749 to +751
if len(selection.VSCodeModelAssignments) > 0 {
current.VSCodeModelAssignments = modelAssignmentsToState(selection.VSCodeModelAssignments)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor explicit clear semantics for VSCodeModelAssignments.

persistAssignments only writes VS Code assignments when the map is non-empty. If callers pass a non-nil empty map to clear assignments, stale persisted values remain and get rehydrated on later syncs.

Suggested fix
-	if len(selection.VSCodeModelAssignments) > 0 {
-		current.VSCodeModelAssignments = modelAssignmentsToState(selection.VSCodeModelAssignments)
-	}
+	if selection.VSCodeModelAssignments != nil {
+		if len(selection.VSCodeModelAssignments) > 0 {
+			current.VSCodeModelAssignments = modelAssignmentsToState(selection.VSCodeModelAssignments)
+		} else {
+			current.VSCodeModelAssignments = nil
+		}
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(selection.VSCodeModelAssignments) > 0 {
current.VSCodeModelAssignments = modelAssignmentsToState(selection.VSCodeModelAssignments)
}
if selection.VSCodeModelAssignments != nil {
if len(selection.VSCodeModelAssignments) > 0 {
current.VSCodeModelAssignments = modelAssignmentsToState(selection.VSCodeModelAssignments)
} else {
current.VSCodeModelAssignments = nil
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/app/app.go` around lines 749 - 751, The condition in the
VSCodeModelAssignments block at the if statement is checking if the length is
greater than zero, which prevents explicit clearing when a non-nil empty map is
passed. Change the condition from checking len(selection.VSCodeModelAssignments)
> 0 to instead checking if selection.VSCodeModelAssignments is not nil. This
will allow the modelAssignmentsToState function to process empty maps that
represent an explicit clear intent, while still allowing the caller to pass nil
if they want to skip processing assignments entirely.

Comment thread internal/cli/run.go
Comment on lines +183 to 184
VSCodeModelAssignments: modelAssignmentsToState(input.Selection.VSCodeModelAssignments),
Persona: string(input.Selection.Persona),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist VS Code assignments in explicit-agent state merges.

RunInstall writes VSCodeModelAssignments into newState, but mergeExplicitAgentInstallState never copies that field. On install --agent ... with an existing state file, newly selected VS Code assignments can be lost.

Suggested fix
 func mergeExplicitAgentInstallState(homeDir string, newState state.InstallState, agentIDs []string) (state.InstallState, bool) {
@@
 	if newState.ModelAssignments != nil {
 		merged.ModelAssignments = newState.ModelAssignments
 	}
+	if newState.VSCodeModelAssignments != nil {
+		merged.VSCodeModelAssignments = newState.VSCodeModelAssignments
+	}
 	if newState.ClaudeModelAssignments != nil {
 		merged.ClaudeModelAssignments = newState.ClaudeModelAssignments
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
VSCodeModelAssignments: modelAssignmentsToState(input.Selection.VSCodeModelAssignments),
Persona: string(input.Selection.Persona),
if newState.ModelAssignments != nil {
merged.ModelAssignments = newState.ModelAssignments
}
if newState.VSCodeModelAssignments != nil {
merged.VSCodeModelAssignments = newState.VSCodeModelAssignments
}
if newState.ClaudeModelAssignments != nil {
merged.ClaudeModelAssignments = newState.ClaudeModelAssignments
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/run.go` around lines 183 - 184, The
mergeExplicitAgentInstallState function is not copying the
VSCodeModelAssignments field when merging state, causing newly selected VS Code
assignments to be lost when running install with an existing state file. Add
VSCodeModelAssignments to the field assignments in the
mergeExplicitAgentInstallState function to ensure the value set by RunInstall is
preserved during the state merge operation.

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.

feat(agents): add VS Code Copilot SDD multi-mode support (backend)

1 participant