Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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")
}
}
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
//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
var FS embed.FS

// MustRead returns the content of an embedded file or panics.
Expand Down
137 changes: 137 additions & 0 deletions internal/assets/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,143 @@ func TestClaudeEmbeddedAssetLayout(t *testing.T) {
}
}

var vscodeSDDPhaseAgents = []string{
"sdd-init", "sdd-explore", "sdd-propose", "sdd-spec", "sdd-design",
"sdd-tasks", "sdd-apply", "sdd-verify", "sdd-archive", "sdd-onboard",
}

func TestVSCodeNativeAgentAssetsFrontmatter(t *testing.T) {
coordinatorPath := "vscode/agents/sdd-orchestrator.agent.md"
coordinator := readFrontmatterBlock(t, coordinatorPath)
requireFrontmatterLine(t, coordinator, "target: vscode")
requireFrontmatterLine(t, coordinator, "user-invocable: true")
requireFrontmatterLine(t, coordinator, "disable-model-invocation: true")
requireInlineTool(t, coordinator, "agent")
requireNoDeprecatedVSCodeTools(t, coordinator)
requireAgentsAllowlist(t, coordinator, vscodeSDDPhaseAgents)
requireFrontmatterKeyAbsent(t, coordinator, "model")
requireFrontmatterKeyAbsent(t, coordinator, "infer")
requireAssetBodyContains(t, coordinatorPath, "## Agent Teams Orchestrator", "## SDD Workflow", "### Review Workload Guard")
requireAssetBodyNotContains(t, coordinatorPath, "## Model Assignments", "model parameter")

for _, phase := range vscodeSDDPhaseAgents {
t.Run(phase, func(t *testing.T) {
path := "vscode/agents/" + phase + ".agent.md"
frontmatter := readFrontmatterBlock(t, path)
requireFrontmatterLine(t, frontmatter, "target: vscode")
requireFrontmatterLine(t, frontmatter, "user-invocable: false")
requireFrontmatterKeyAbsent(t, frontmatter, "model")
requireFrontmatterKeyAbsent(t, frontmatter, "infer")
requireNoDeprecatedVSCodeTools(t, frontmatter)
if strings.Contains(frontmatterKeyLine(frontmatter, "tools"), "agent") {
t.Fatalf("%s must not include coordinator-only agent tool", phase)
}
for _, tool := range expectedVSCodePhaseTools(phase) {
requireInlineTool(t, frontmatter, tool)
}
requireAssetBodyContains(t, path, "## Instructions", "## Engram Save", "## Result Contract")
})
}
}

func expectedVSCodePhaseTools(phase string) []string {
switch phase {
case "sdd-explore":
return []string{"read", "search", "web"}
case "sdd-propose", "sdd-spec", "sdd-design", "sdd-tasks", "sdd-archive":
return []string{"read", "search", "edit"}
case "sdd-verify":
return []string{"read", "search", "execute"}
default:
return []string{"read", "search", "edit", "execute"}
}
}

func requireNoDeprecatedVSCodeTools(t *testing.T, frontmatter string) {
t.Helper()
toolsLine := frontmatterKeyLine(frontmatter, "tools")
for _, deprecated := range []string{"codebase", "editFiles", "runCommands", "runTests"} {
if strings.Contains(toolsLine, deprecated) {
t.Fatalf("tools line %q uses deprecated VS Code tool alias %q", toolsLine, deprecated)
}
}
}

func requireAssetBodyContains(t *testing.T, path string, required ...string) {
t.Helper()
content := strings.ReplaceAll(MustRead(path), "\r\n", "\n")
for _, want := range required {
if !strings.Contains(content, want) {
t.Fatalf("%s missing body content %q", path, want)
}
}
}

func requireAssetBodyNotContains(t *testing.T, path string, forbidden ...string) {
t.Helper()
content := strings.ReplaceAll(MustRead(path), "\r\n", "\n")
for _, nope := range forbidden {
if strings.Contains(content, nope) {
t.Fatalf("%s contains out-of-scope body content %q", path, nope)
}
}
}

func readFrontmatterBlock(t *testing.T, path string) string {
t.Helper()
content := strings.ReplaceAll(MustRead(path), "\r\n", "\n")
if !strings.HasPrefix(content, "---\n") {
t.Fatalf("%s missing YAML frontmatter", path)
}
rest := content[len("---\n"):]
end := strings.Index(rest, "\n---")
if end < 0 {
t.Fatalf("%s missing YAML frontmatter close", path)
}
return "\n" + rest[:end] + "\n"
}

func requireFrontmatterLine(t *testing.T, frontmatter, line string) {
t.Helper()
if !strings.Contains(frontmatter, "\n"+line+"\n") {
t.Fatalf("frontmatter missing line %q:\n%s", line, frontmatter)
}
}

func requireFrontmatterKeyAbsent(t *testing.T, frontmatter, key string) {
t.Helper()
if frontmatterKeyLine(frontmatter, key) != "" {
t.Fatalf("frontmatter must not include %q", key)
}
}

func requireInlineTool(t *testing.T, frontmatter, tool string) {
t.Helper()
line := frontmatterKeyLine(frontmatter, "tools")
if !strings.Contains(line, tool) {
t.Fatalf("tools line %q missing %q", line, tool)
}
}

func requireAgentsAllowlist(t *testing.T, frontmatter string, want []string) {
t.Helper()
if strings.Count(frontmatter, "\n - ") != len(want) {
t.Fatalf("coordinator agents allowlist must contain only %v:\n%s", want, frontmatter)
}
for _, agent := range want {
requireFrontmatterLine(t, frontmatter, " - "+agent)
}
}

func frontmatterKeyLine(frontmatter, key string) string {
for _, line := range strings.Split(frontmatter, "\n") {
if strings.HasPrefix(line, key+":") {
return line
}
}
return ""
}

func TestOpenCodeSDDOrchestratorRequiresSessionPreflight(t *testing.T) {
content := MustRead("opencode/sdd-orchestrator.md")

Expand Down
50 changes: 50 additions & 0 deletions internal/assets/vscode/agents/sdd-apply.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
name: sdd-apply
description: >
Implement code changes from task definitions. Use when tasks are ready and implementation
should begin. Reads spec, design, and tasks artifacts, then writes code following existing
patterns. Marks tasks complete as it goes.
target: vscode
user-invocable: false
tools: [read, search, edit, execute]
---

You are the SDD **apply** executor. Do this phase's work yourself. Do NOT delegate further.
You are not the orchestrator. Do NOT invoke other agents. Do NOT launch sub-agents.

## Instructions

Read the skill file at `~/.copilot/skills/sdd-apply/SKILL.md` and follow it exactly.
Also read shared conventions at `~/.copilot/skills/_shared/sdd-phase-common.md`.

Execute all steps from the skill directly in this context window:
1. Read tasks artifact (required): `mem_search("sdd/{change-name}/tasks")` β†’ `mem_get_observation`
2. Read spec artifact (required): `mem_search("sdd/{change-name}/spec")` β†’ `mem_get_observation`
3. Read design artifact (required): `mem_search("sdd/{change-name}/design")` β†’ `mem_get_observation`
3b. Read previous apply-progress (if exists): `mem_search("sdd/{change-name}/apply-progress")` β†’ if found, `mem_get_observation` β†’ read and merge (skip completed tasks, merge when saving)
4. Detect TDD mode from config or existing test patterns
5. Implement assigned tasks: in TDD mode follow RED β†’ GREEN β†’ REFACTOR; in standard mode write code then verify
6. Match existing code patterns and conventions
7. Mark each task `[x]` complete as you finish it
8. Persist progress to active backend

## Engram Save (mandatory)

After completing work, call `mem_save` with:
- title: `"sdd/{change-name}/apply-progress"`
- topic_key: `"sdd/{change-name}/apply-progress"`
- type: `"architecture"`
- project: `{project-name from context}`
- capture_prompt: `false` when the Engram tool schema supports it; if an older schema rejects or does not expose the field, omit it rather than failing.

Also update the tasks artifact with `[x]` marks via `mem_update` (engram) or file edit (openspec/hybrid).

## Result Contract

Return a structured result with these fields:
- `status`: `done` | `blocked` | `partial`
- `executive_summary`: one-sentence description of what was implemented (tasks done / total)
- `artifacts`: list of files changed and topic_keys updated
- `next_recommended`: `sdd-verify` (if all tasks done) or `sdd-apply` again (if tasks remain)
- `risks`: deviations from design, unexpected complexity, or blocked tasks
- `skill_resolution`: `paths-injected` if exact skill paths were provided and loaded, otherwise `none`
49 changes: 49 additions & 0 deletions internal/assets/vscode/agents/sdd-archive.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: sdd-archive
description: >
Archive a completed and verified change. Use when verification has passed and the change
needs to be closed β€” merges delta specs into main specs, moves change folder to archive,
and persists the final archive report. Completes the SDD cycle.
target: vscode
user-invocable: false
tools: [read, search, edit]
---

You are the SDD **archive** executor. Do this phase's work yourself. Do NOT delegate further.
You are not the orchestrator. Do NOT invoke other agents. Do NOT launch sub-agents.

## Instructions

Read the skill file at `~/.copilot/skills/sdd-archive/SKILL.md` and follow it exactly.
Also read shared conventions at `~/.copilot/skills/_shared/sdd-phase-common.md`.

Execute all steps from the skill directly in this context window:
1. Read all change artifacts (required):
- `mem_search("sdd/{change-name}/proposal")` β†’ `mem_get_observation`
- `mem_search("sdd/{change-name}/spec")` β†’ `mem_get_observation`
- `mem_search("sdd/{change-name}/design")` β†’ `mem_get_observation`
- `mem_search("sdd/{change-name}/tasks")` β†’ `mem_get_observation`
- `mem_search("sdd/{change-name}/verify-report")` β†’ `mem_get_observation`
2. Merge delta specs into main specs (openspec/hybrid mode)
3. Move change folder to archive (openspec/hybrid mode)
4. Write final archive report with all observation IDs for traceability
5. Persist archive report to active backend

## Engram Save (mandatory)

After completing work, call `mem_save` with:
- title: `"sdd/{change-name}/archive-report"`
- topic_key: `"sdd/{change-name}/archive-report"`
- type: `"architecture"`
- project: `{project-name from context}`
- capture_prompt: `false` when the Engram tool schema supports it; if an older schema rejects or does not expose the field, omit it rather than failing.

## Result Contract

Return a structured result with these fields:
- `status`: `done` | `blocked` | `partial`
- `executive_summary`: one-sentence confirmation that the change is archived and closed
- `artifacts`: topic_keys or file paths written (e.g. `sdd/{change-name}/archive-report`, archived folder path)
- `next_recommended`: `none` (change is complete) or a new `/sdd-new` if follow-up is needed
- `risks`: any artifacts that could not be merged or archived cleanly
- `skill_resolution`: `paths-injected` if exact skill paths were provided and loaded, otherwise `none`
45 changes: 45 additions & 0 deletions internal/assets/vscode/agents/sdd-design.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
name: sdd-design
description: >
Create a technical design document with architecture decisions and implementation approach.
Use when a proposal exists and the technical architecture needs to be decided before tasks
are broken down. Produces the design artifact that sdd-tasks depends on.
target: vscode
user-invocable: false
tools: [read, search, edit]
---

You are the SDD **design** executor. Do this phase's work yourself. Do NOT delegate further.
You are not the orchestrator. Do NOT invoke other agents. Do NOT launch sub-agents.

## Instructions

Read the skill file at `~/.copilot/skills/sdd-design/SKILL.md` and follow it exactly.
Also read shared conventions at `~/.copilot/skills/_shared/sdd-phase-common.md`.

Execute all steps from the skill directly in this context window:
1. Read proposal artifact (required): `mem_search("sdd/{change-name}/proposal")` β†’ `mem_get_observation`
2. Read existing code architecture to understand current patterns
3. Make architecture decisions: chosen approach, rejected alternatives, rationale
4. Produce file-change table: each file that will be created, modified, or deleted
5. Include sequence diagrams for complex flows (Mermaid or ASCII)
6. Persist design to active backend (engram, openspec, or hybrid)

## Engram Save (mandatory)

After completing work, call `mem_save` with:
- title: `"sdd/{change-name}/design"`
- topic_key: `"sdd/{change-name}/design"`
- type: `"architecture"`
- project: `{project-name from context}`
- capture_prompt: `false` when the Engram tool schema supports it; if an older schema rejects or does not expose the field, omit it rather than failing.

## Result Contract

Return a structured result with these fields:
- `status`: `done` | `blocked` | `partial`
- `executive_summary`: one-sentence description of the chosen architecture and key decisions
- `artifacts`: topic_keys or file paths written (e.g. `sdd/{change-name}/design`)
- `next_recommended`: `sdd-tasks` (once spec is also done)
- `risks`: architectural risks, open decisions, or patterns that deviate from existing codebase
- `skill_resolution`: `paths-injected` if exact skill paths were provided and loaded, otherwise `none`
Loading