Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5450d27
feat(vscode): add native SDD agents
Snakeblack May 29, 2026
b78bd1d
test(assets): register vscode agent files in expected list
Snakeblack Jun 29, 2026
f7ed70b
fix(uninstall): filter by opRemoveFile in VS Code uninstall test
Snakeblack Jul 1, 2026
41f4f50
style(vscode): add language identifier to dependency-graph code block
Snakeblack Jul 1, 2026
034c7a4
feat(vscode): agrega asignaciones de modelo
Snakeblack Jun 1, 2026
d51c5e0
fix(app,cli): add VSCodeModelAssignments merge and empty override checks
Snakeblack Jun 29, 2026
2dab40b
fix(vscode): endurece instalaciΓ³n de agentes
Snakeblack Jun 2, 2026
a247af2
fix(sync,sdd): resolve CodeRabbit review comments on PR 731
Snakeblack Jun 30, 2026
8ff7569
fix(sdd): require both delimiters before removing legacy preflight lines
Snakeblack Jul 1, 2026
47ff860
fix(sdd): validate every legacy preflight block independently
Snakeblack Jul 1, 2026
e3eba36
fix(sdd): remove duplicate containsString test helper
Snakeblack Jul 1, 2026
90553ef
fix(sdd,uninstall): add backup and restore support for Claude interna…
Snakeblack Jun 29, 2026
cabc090
fix(vscode): resolve rebase conflicts on hardening branch and fix eng…
Snakeblack Jun 30, 2026
b198fae
fix(vscode,tui): resolve all CodeRabbit review comments on backup/res…
Snakeblack Jun 30, 2026
d220e9c
fix(vscode,uninstall,tui): address coderabbit review on atomic backup…
Snakeblack Jun 30, 2026
5d70857
fix(vscode,cli,sdd): resolve remaining CodeRabbit comments on backup …
Snakeblack Jun 30, 2026
51763da
fix(vscode): resolve CodeRabbit review comments on backup restore and…
Snakeblack Jul 1, 2026
28d0885
fix(uninstall): abort on unexpected backup or read failures
Snakeblack Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ After creating a profile, open OpenCode and press **Tab** to switch between `gen

**Full guide**: [OpenCode SDD Profiles](docs/opencode-profiles.md)

### VS Code Copilot SDD Agents

Gentle-AI installs three VS Code layers: global instructions/rules in the VS Code user prompts folder (e.g. `%APPDATA%/Code/User/prompts/gentle-ai.instructions.md` on Windows, `~/Library/Application Support/Code/User/prompts/gentle-ai.instructions.md` on macOS, or `~/.config/Code/User/prompts/gentle-ai.instructions.md` on Linux), native custom agents in `~/.copilot/agents`, and SDD skills/shared conventions in `~/.copilot/skills`.

When a VS Code model assignment is missing, invalid, or unvalidated, generated agents omit `model` and inherit the parent chat model instead of failing sync.

If VS Code also discovers Gentle-AI's Claude-format internal agents, sync marks those managed `sdd-*` and `jd-*` files `user-invocable: false` so the visible picker stays focused on `sdd-orchestrator`.

### Engram (Persistent Memory)

Your AI agent automatically remembers decisions, bugs, and context across sessions. You don't need to do anything -- but when you do:
Expand Down
9 changes: 6 additions & 3 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,13 @@ Kiro uses native custom agents in `~/.kiro/agents/`. `gentle-ai` writes phase ag

### VS Code Copilot

- Uses the `runSubagent` tool with support for parallel execution
- Skills at `~/.copilot/skills/`
- System prompt at `Code/User/prompts/gentle-ai.instructions.md`
- Native custom agents at `~/.copilot/agents/*.agent.md`: one user-invocable `sdd-orchestrator` coordinator plus hidden SDD phase agents
- Delegates through VS Code Copilot's native sub-agent / `runSubagent` flow
- Skills and shared SDD conventions at `~/.copilot/skills/`
- Global instructions/rules at `Code/User/prompts/gentle-ai.instructions.md`
- MCP config at `Code/User/mcp.json`
- Missing, invalid, or unvalidated model assignments omit `model` and inherit the parent chat model
- VS Code may also discover Claude-format agents in `~/.claude/agents`; Gentle-AI marks its managed internal `sdd-*` and `jd-*` Claude agents `user-invocable: false` so the picker stays focused on `sdd-orchestrator`

### Codex

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/mattn/go-isatty v0.0.20
github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.38.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🌐 Web query:

What is the first patched version for GO-2026-5024 in golang.org/x/sys, and is v0.38.0 affected?

πŸ’‘ Result:

The first patched version for GO-2026-5024 (also tracked as CVE-2026-39824) in the golang.org/x/sys module is v0.44.0 [1][2][3]. Yes, v0.38.0 is affected by this vulnerability because it is below the patched version of v0.44.0 [1][4][5]. The vulnerability involves an integer overflow in the NewNTUnicodeString function within the golang.org/x/sys/windows package [1][2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== go.mod ==\n'
cat -n go.mod | sed -n '1,40p'

printf '\n== replace_windows.go ==\n'
cat -n internal/components/filemerge/replace_windows.go | sed -n '1,120p'

Repository: Gentleman-Programming/gentle-ai

Length of output: 2390


Bump golang.org/x/sys to v0.44.0 or newer. v0.38.0 is affected by GO-2026-5024/CVE-2026-39824, and internal/components/filemerge/replace_windows.go imports golang.org/x/sys/windows directly.

🧰 Tools
πŸͺ› OSV Scanner (2.4.0)

[LOW] 11-11: golang.org/x/sys 0.38.0: Invoking integer overflow in NewNTUnicodeString in golang.org/x/sys/windows

(GO-2026-5024)

πŸ€– 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 `@go.mod` at line 11, Update the golang.org/x/sys dependency in go.mod from
v0.38.0 to v0.44.0 or newer to remove the vulnerable version. After bumping the
module, ensure any code that depends on golang.org/x/sys/windows, especially
internal/components/filemerge/replace_windows.go, still builds and resolves
against the updated dependency.

Source: Linters/SAST tools

)

require (
Expand All @@ -28,6 +29,5 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.3.8 // indirect
)
9 changes: 5 additions & 4 deletions internal/agents/vscode/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (a *Adapter) InstallCommand(_ system.PlatformProfile) ([][]string, error) {
// VS Code Copilot reads .instructions.md files from the VS Code User prompts folder.
// Skills are loaded from ~/.copilot/skills/ (global), .github/skills/ (workspace),
// ~/.claude/skills/, and .claude/skills/. We target ~/.copilot/skills/ for global reach.
// Native agent files are installed globally under ~/.copilot/agents/.

func (a *Adapter) GlobalConfigDir(homeDir string) string {
return filepath.Join(homeDir, ".copilot")
Expand Down Expand Up @@ -133,15 +134,15 @@ func (a *Adapter) CommandsDir(_ string) string {
}

func (a *Adapter) SupportsSubAgents() bool {
return false
return true
}

func (a *Adapter) SubAgentsDir(_ string) string {
return ""
func (a *Adapter) SubAgentsDir(homeDir string) string {
return filepath.Join(homeDir, ".copilot", "agents")
}

func (a *Adapter) EmbeddedSubAgentsDir() string {
return ""
return "vscode/agents"
}

func (a *Adapter) SupportsSkills() bool {
Expand Down
23 changes: 23 additions & 0 deletions internal/agents/vscode/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package vscode
import (
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/gentleman-programming/gentle-ai/internal/model"
Expand Down Expand Up @@ -91,3 +92,25 @@ func TestMCPConfigPathUsesVSCodeUserProfile(t *testing.T) {
}
}
}

func TestSubAgentSupportUsesCopilotUserAgentsDir(t *testing.T) {
a := NewAdapter()
home := "/tmp/home"

if !a.SupportsSubAgents() {
t.Fatal("VS Code adapter must advertise native sub-agent support")
}

got := a.SubAgentsDir(home)
want := filepath.Join(home, ".copilot", "agents")
if got != want {
t.Fatalf("SubAgentsDir() = %q, want %q", got, want)
}
if got == filepath.Join(home, ".github", "agents") || strings.Contains(got, string(filepath.Separator)+".github"+string(filepath.Separator)) {
t.Fatalf("SubAgentsDir() must not target workspace .github/agents, got %q", got)
}

if got := a.EmbeddedSubAgentsDir(); got != "vscode/agents" {
t.Fatalf("EmbeddedSubAgentsDir() = %q, want %q", got, "vscode/agents")
}
}
17 changes: 16 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ func tuiExecute(
CodexCarrilModelAssignments: selection.CodexCarrilModelAssignments,
CodexPhaseModelAssignments: selection.CodexPhaseModelAssignments,
ModelAssignments: modelAssignmentsToState(selection.ModelAssignments),
VSCodeModelAssignments: modelAssignmentsToState(selection.VSCodeModelAssignments),
Persona: string(selection.Persona),
})
}
Expand Down Expand Up @@ -549,6 +550,9 @@ func applyOverrides(selection *model.Selection, overrides *model.SyncOverrides)
if overrides.ModelAssignments != nil {
selection.ModelAssignments = overrides.ModelAssignments
}
if overrides.VSCodeModelAssignments != nil {
selection.VSCodeModelAssignments = overrides.VSCodeModelAssignments
}
if overrides.ClaudeModelAssignments != nil {
selection.ClaudeModelAssignments = overrides.ClaudeModelAssignments
}
Expand Down Expand Up @@ -657,6 +661,9 @@ func loadPersistedAssignments(homeDir string, selection *model.Selection) {
}
selection.ModelAssignments = m
}
if len(selection.VSCodeModelAssignments) == 0 && len(s.VSCodeModelAssignments) > 0 {
selection.VSCodeModelAssignments = cli.StateModelAssignmentsToModel(s.VSCodeModelAssignments)
}
}

// persistAssignments writes the model assignments from selection back to
Expand All @@ -672,10 +679,11 @@ func persistAssignments(homeDir string, selection model.Selection) {
selection.ClaudePhaseAssignments != nil ||
selection.KiroModelAssignments != nil ||
selection.ModelAssignments != nil ||
selection.VSCodeModelAssignments != nil ||
selection.CodexModelAssignments != nil ||
selection.CodexCarrilModelAssignments != nil ||
selection.CodexPhaseModelAssignments != nil
if len(selection.ClaudeModelAssignments) == 0 && len(selection.ClaudePhaseAssignments) == 0 && len(selection.KiroModelAssignments) == 0 && len(selection.ModelAssignments) == 0 && len(selection.CodexModelAssignments) == 0 && len(selection.CodexCarrilModelAssignments) == 0 && len(selection.CodexPhaseModelAssignments) == 0 && !hasAssignmentSignal {
if len(selection.ClaudeModelAssignments) == 0 && len(selection.ClaudePhaseAssignments) == 0 && len(selection.KiroModelAssignments) == 0 && len(selection.ModelAssignments) == 0 && len(selection.VSCodeModelAssignments) == 0 && len(selection.CodexModelAssignments) == 0 && len(selection.CodexCarrilModelAssignments) == 0 && len(selection.CodexPhaseModelAssignments) == 0 && !hasAssignmentSignal {
return
}
current, err := state.Read(homeDir)
Expand Down Expand Up @@ -738,6 +746,13 @@ func persistAssignments(homeDir string, selection model.Selection) {
current.ModelAssignments = nil
}
}
if selection.VSCodeModelAssignments != nil {
if len(selection.VSCodeModelAssignments) > 0 {
current.VSCodeModelAssignments = modelAssignmentsToState(selection.VSCodeModelAssignments)
} else {
current.VSCodeModelAssignments = nil
}
}
_ = state.Write(homeDir, current)
}

Expand Down
66 changes: 66 additions & 0 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,34 @@ func TestTuiSyncSDDProfileStrategyEmptyOverrideNoChange(t *testing.T) {
}
}

func TestApplyOverridesVSCodeModelAssignments(t *testing.T) {
selection := model.Selection{
VSCodeModelAssignments: map[string]model.ModelAssignment{
"sdd-apply": {ProviderID: "github-copilot", ModelID: "gpt-4.1"},
},
ModelAssignments: map[string]model.ModelAssignment{
"sdd-apply": {ProviderID: "anthropic", ModelID: "claude-opus-4"},
},
}
overrides := &model.SyncOverrides{
VSCodeModelAssignments: map[string]model.ModelAssignment{
"sdd-design": {ProviderID: "github-copilot", ModelID: "claude-sonnet-4"},
},
}

applyOverrides(&selection, overrides)

if _, exists := selection.VSCodeModelAssignments["sdd-apply"]; exists {
t.Fatal("VSCodeModelAssignments should be replaced as a whole map, not merged key-by-key")
}
if got := selection.VSCodeModelAssignments["sdd-design"].ProviderID; got != "github-copilot" {
t.Fatalf("VSCodeModelAssignments[sdd-design].ProviderID = %q, want github-copilot", got)
}
if got := selection.ModelAssignments["sdd-apply"].ProviderID; got != "anthropic" {
t.Fatalf("OpenCode ModelAssignments changed after VS Code override: provider = %q", got)
}
}

func boolPtr(b bool) *bool { return &b }

func TestTuiSyncTargetAgentsOverridePersistedInstallState(t *testing.T) {
Expand Down Expand Up @@ -666,6 +694,9 @@ func TestLoadPersistedAssignmentsPopulatesEmptySelection(t *testing.T) {
ModelAssignments: map[string]state.ModelAssignmentState{
"sdd-init": {ProviderID: "anthropic", ModelID: "claude-sonnet-4"},
},
VSCodeModelAssignments: map[string]state.ModelAssignmentState{
"sdd-apply": {ProviderID: "github-copilot", ModelID: "gpt-4.1"},
},
})
if err != nil {
t.Fatalf("state.Write: %v", err)
Expand All @@ -690,6 +721,10 @@ func TestLoadPersistedAssignmentsPopulatesEmptySelection(t *testing.T) {
if ma.ProviderID != "anthropic" || ma.ModelID != "claude-sonnet-4" {
t.Errorf("ModelAssignments[sdd-init] = %+v, want anthropic/claude-sonnet-4", ma)
}
vs := selection.VSCodeModelAssignments["sdd-apply"]
if vs.ProviderID != "github-copilot" || vs.ModelID != "gpt-4.1" {
t.Errorf("VSCodeModelAssignments[sdd-apply] = %+v, want github-copilot/gpt-4.1", vs)
}
}

// TestLoadPersistedAssignmentsDoesNotOverrideExisting verifies that when the
Expand All @@ -704,6 +739,9 @@ func TestLoadPersistedAssignmentsDoesNotOverrideExisting(t *testing.T) {
ModelAssignments: map[string]state.ModelAssignmentState{
"sdd-init": {ProviderID: "google", ModelID: "gemini-pro"},
},
VSCodeModelAssignments: map[string]state.ModelAssignmentState{
"sdd-apply": {ProviderID: "github-copilot", ModelID: "old-model"},
},
})
if err != nil {
t.Fatalf("state.Write: %v", err)
Expand All @@ -717,6 +755,9 @@ func TestLoadPersistedAssignmentsDoesNotOverrideExisting(t *testing.T) {
ModelAssignments: map[string]model.ModelAssignment{
"sdd-init": {ProviderID: "anthropic", ModelID: "claude-sonnet-4"},
},
VSCodeModelAssignments: map[string]model.ModelAssignment{
"sdd-apply": {ProviderID: "github-copilot", ModelID: "new-model"},
},
}
loadPersistedAssignments(home, &selection)

Expand All @@ -728,6 +769,9 @@ func TestLoadPersistedAssignmentsDoesNotOverrideExisting(t *testing.T) {
if ma.ProviderID != "anthropic" {
t.Errorf("ModelAssignments[sdd-init].ProviderID = %q, want %q (should not be overwritten)", ma.ProviderID, "anthropic")
}
if got := selection.VSCodeModelAssignments["sdd-apply"].ModelID; got != "new-model" {
t.Errorf("VSCodeModelAssignments[sdd-apply].ModelID = %q, want new-model (should not be overwritten)", got)
}
}

// TestPersistAssignmentsPreservesInstalledAgents verifies the read-merge-write
Expand Down Expand Up @@ -927,6 +971,28 @@ func TestPersistAndLoadKiroModelAssignments(t *testing.T) {
}
}

func TestPersistAndLoadVSCodeModelAssignments(t *testing.T) {
home := t.TempDir()

selection := model.Selection{
VSCodeModelAssignments: map[string]model.ModelAssignment{
"sdd-orchestrator": {ProviderID: "github-copilot", ModelID: "gpt-4.1"},
"sdd-apply": {ProviderID: "github-copilot", ModelID: "claude-sonnet-4", Effort: "low"},
},
}
persistAssignments(home, selection)

loaded := model.Selection{}
loadPersistedAssignments(home, &loaded)

if got := loaded.VSCodeModelAssignments["sdd-orchestrator"].ModelID; got != "gpt-4.1" {
t.Fatalf("VSCodeModelAssignments[sdd-orchestrator].ModelID = %q, want gpt-4.1", got)
}
if got := loaded.VSCodeModelAssignments["sdd-apply"].Effort; got != "low" {
t.Fatalf("VSCodeModelAssignments[sdd-apply].Effort = %q, want low", got)
}
}

// TestPersistAssignmentsNoOpWhenEmpty verifies that persistAssignments does
// not write to state.json when the selection has no assignments.
func TestPersistAssignmentsNoOpWhenEmpty(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package assets

import "embed"

//go:embed all:claude all:opencode all:generic all:skills all:gga all:gemini all:codex all:antigravity all:windsurf all:cursor all:kimi all:qwen all:kiro all:hermes
//go:embed all:claude all:opencode all:generic all:skills all:gga all:gemini all:codex all:antigravity all:windsurf all:cursor all:vscode all:kimi all:qwen all:kiro all:hermes
var FS embed.FS

// MustRead returns the content of an embedded file or panics.
Expand Down
Loading