-
Notifications
You must be signed in to change notification settings - Fork 0
feat: support Pi Desktop subagents and Selesai skills #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e05b895
6337627
0e747b7
65aeb28
51e6b58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -439,6 +439,12 @@ func renderClaudeAgentRole(role agentRole) string { | |
| return b.String() | ||
| } | ||
|
|
||
| // renderPiDesktopAgentRole emits the Markdown frontmatter consumed by | ||
| // Pi Desktop's global ~/.agents/subagents directory. | ||
| func renderPiDesktopAgentRole(role agentRole) string { | ||
| return renderClaudeAgentRole(role) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a canonical role defines AGENTS.md reference: AGENTS.md:L21-L21 Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| func renderCodexAgentRole(role agentRole) string { | ||
| model := strings.TrimSpace(role.Codex.Model) | ||
| if model == "" { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package main | |
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "sort" | ||
|
|
@@ -245,6 +246,13 @@ func initHarnesses() { | |
| TrailerExample: "Co-authored-by: pi[bot] <pi[bot]@users.noreply.github.com>", | ||
| }, | ||
|
|
||
| agentPiDesktop: { | ||
| Detect: detectPiDesktop, | ||
| Skills: SkillsSymlink, | ||
| Roles: &RolesCapability{Extension: ".md", Render: renderPiDesktopAgentRole}, | ||
| IntegrationNote: "uses Pi Desktop's supported global ~/.agents/skills and ~/.agents/subagents roots; no app internals or plugin package", | ||
| }, | ||
|
|
||
| agentOMP: { | ||
| Skills: SkillsSymlink, | ||
| MCP: mcpTargetPtr(mcpTarget{ | ||
|
|
@@ -258,6 +266,13 @@ func initHarnesses() { | |
| Roles: &RolesCapability{Extension: ".md", Render: renderOMPAgentRole}, | ||
| }, | ||
|
|
||
| agentSelesai: { | ||
| Detect: detectSelesai, | ||
| Skills: SkillsSymlink, | ||
| TrailerExample: "Co-authored-by: selesai[bot] <selesai[bot]@users.noreply.github.com>", | ||
| IntegrationNote: "syncs only non-bundled skills to avoid conflicts with Selesai's installed bundled skills", | ||
| }, | ||
|
|
||
| agentQwenCode: { | ||
| Skills: SkillsConfigDriven, | ||
| InspectSkills: func(agent agentConfig, expected map[string]string, agentsSkillRoot string, cfg config, home string) (agentReport, error) { | ||
|
|
@@ -348,3 +363,16 @@ func detectVanillaPi(executable string) bool { | |
| version, _ := exec.Command(executable, "--version").CombinedOutput() // nosemgrep: go.lang.security.audit.dangerous-exec-command | ||
| return !bytes.HasPrefix(bytes.TrimSpace(version), []byte("omp/")) | ||
| } | ||
|
|
||
| func detectPiDesktop(executable string) bool { | ||
| // Pi Desktop is a GUI application. Detect by checking if the app bundle exists. | ||
| // The executable might be 'pi' from the PATH, but we check for the desktop app. | ||
| info, err := os.Stat("/Applications/PI-Desktop.app") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On macOS, applications may be installed under AGENTS.md reference: AGENTS.md:L17-L17 Useful? React with 👍 / 👎. |
||
| return err == nil && info.IsDir() | ||
| } | ||
|
|
||
| func detectSelesai(executable string) bool { | ||
| // Verify this is actually the selesai command, not something else named 'selesai'. | ||
| version, _ := exec.Command(executable, "--version").CombinedOutput() // nosemgrep: go.lang.security.audit.dangerous-exec-command | ||
| return bytes.Contains(bytes.ToLower(version), []byte("selesai")) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "gopkg.in/yaml.v3" | ||
| ) | ||
|
|
||
| func TestPiDesktopHarnessCapabilities(t *testing.T) { | ||
| piDesktop := harnessFor(agentPiDesktop) | ||
| if piDesktop == nil { | ||
| t.Fatal("Pi Desktop harness is not registered") | ||
| } | ||
| if piDesktop.Skills != SkillsSymlink { | ||
| t.Fatalf("Pi Desktop skills capability = %v, want symlink", piDesktop.Skills) | ||
| } | ||
| if piDesktop.Roles == nil || piDesktop.Roles.Extension != ".md" { | ||
| t.Fatalf("Pi Desktop roles capability = %#v, want Markdown roles", piDesktop.Roles) | ||
| } | ||
| if piDesktop.MCP != nil || piDesktop.Hooks != nil || piDesktop.RootInstructions != nil { | ||
| t.Fatal("Pi Desktop must expose only its verified skills and subagents surfaces") | ||
| } | ||
| if piDesktop.IntegrationNote == "" { | ||
| t.Fatal("Pi Desktop should document its native global roots") | ||
| } | ||
| } | ||
|
|
||
| func TestPiDesktopDefaultConfig(t *testing.T) { | ||
| for _, cfg := range defaultAgentConfigs() { | ||
| if cfg.Name != agentPiDesktop { | ||
| continue | ||
| } | ||
| if cfg.SkillRoot != "~/.agents/skills" || cfg.AgentRoot != "~/.agents/subagents" || cfg.Detect != "" { | ||
| t.Fatalf("Pi Desktop default config = %#v", cfg) | ||
| } | ||
| return | ||
| } | ||
| t.Fatal("Pi Desktop not found in default agent configs") | ||
| } | ||
|
|
||
| func TestPiDesktopRendersSupportedSubagentRole(t *testing.T) { | ||
| role := agentRole{ | ||
| Name: "researcher", | ||
| Description: "Find reliable evidence", | ||
| Model: "gpt-5.6-luna", | ||
| Tools: []string{"read", "grep"}, | ||
| Instructions: "Compare the sources.", | ||
| } | ||
| root := filepath.Join(t.TempDir(), ".agents", "subagents") | ||
| path, content, ok := renderAgentRole(role, agentConfig{Name: agentPiDesktop, AgentRoot: root}) | ||
| if !ok { | ||
| t.Fatal("Pi Desktop role was not rendered") | ||
| } | ||
| if want := filepath.Join(root, "researcher.md"); path != want { | ||
| t.Fatalf("Pi Desktop role path = %q, want %q", path, want) | ||
| } | ||
| parts := strings.SplitN(content, "---\n", 3) | ||
| if len(parts) != 3 { | ||
| t.Fatalf("Pi Desktop role lacks YAML frontmatter:\n%s", content) | ||
| } | ||
| var frontmatter struct { | ||
| Name string `yaml:"name"` | ||
| Description string `yaml:"description"` | ||
| Model string `yaml:"model"` | ||
| Tools string `yaml:"tools"` | ||
| } | ||
| if err := yaml.Unmarshal([]byte(parts[1]), &frontmatter); err != nil { | ||
| t.Fatalf("parse Pi Desktop role frontmatter: %v", err) | ||
| } | ||
| if frontmatter.Name != role.Name || frontmatter.Description != role.Description || frontmatter.Model != role.Model || frontmatter.Tools != "read, grep" { | ||
| t.Fatalf("Pi Desktop role frontmatter = %#v", frontmatter) | ||
| } | ||
| if !strings.Contains(parts[2], role.Instructions) { | ||
| t.Fatalf("Pi Desktop role dropped instructions:\n%s", content) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "errors" | ||
| "io/fs" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| ) | ||
|
|
||
| // getSelesaiBundledSkills discovers Selesai's bundled skills by inspecting | ||
| // the installed npm package. Returns a set of bundled skill names to exclude | ||
| // from dotagents sync. | ||
| func getSelesaiBundledSkills() (map[string]struct{}, error) { | ||
| // Find selesai executable | ||
| selesaiPath, err := exec.LookPath("selesai") | ||
| if err != nil { | ||
| // Selesai not installed, return empty set | ||
| return make(map[string]struct{}), nil | ||
|
Comment on lines
+16
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a Selesai agent uses a custom Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| // Resolve symlink if the executable is a symlink (common with npm global installs) | ||
| selesaiPath, err = filepath.EvalSymlinks(selesaiPath) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // npm global installs typically have structure: | ||
| // /path/to/npm/prefix/lib/node_modules/@selesai/code/bin/selesai.js | ||
| // We need to find the package directory: /path/to/npm/prefix/lib/node_modules/@selesai/code | ||
| packageDir := selesaiPath | ||
| for { | ||
| parent := filepath.Dir(packageDir) | ||
| if parent == packageDir { | ||
| // Reached root without finding package.json | ||
| return nil, errors.New("could not find @selesai/code package directory") | ||
| } | ||
| packageJSON := filepath.Join(parent, "package.json") | ||
| if hasFile(packageJSON) { | ||
| packageDir = parent | ||
| break | ||
| } | ||
| packageDir = parent | ||
| } | ||
|
|
||
| // Look for bundled skills in dist/skills/ or src/skills/ | ||
| bundled := make(map[string]struct{}) | ||
| for _, skillsDir := range []string{ | ||
| filepath.Join(packageDir, "dist", "skills"), | ||
| filepath.Join(packageDir, "src", "skills"), | ||
| } { | ||
| entries, err := os.ReadDir(skillsDir) | ||
| if errors.Is(err, fs.ErrNotExist) { | ||
| continue | ||
| } | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| for _, entry := range entries { | ||
| if entry.IsDir() && hasFile(filepath.Join(skillsDir, entry.Name(), "SKILL.md")) { | ||
| bundled[entry.Name()] = struct{}{} | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return bundled, nil | ||
| } | ||
|
|
||
| // filterSelesaiExpectedSkills removes bundled skills from the expected skills map | ||
| // for Selesai agent to avoid conflicts with Selesai's built-in skills. | ||
| func filterSelesaiExpectedSkills(expected map[string]string) (map[string]string, error) { | ||
| bundled, err := getSelesaiBundledSkills() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if len(bundled) == 0 { | ||
| // No bundled skills found or Selesai not installed, sync all skills | ||
| return expected, nil | ||
| } | ||
|
|
||
| filtered := make(map[string]string) | ||
| for name, path := range expected { | ||
| if _, isBundled := bundled[name]; !isBundled { | ||
| filtered[name] = path | ||
| } | ||
| } | ||
|
|
||
| return filtered, nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These new capability rows leave
skills/dotagents/SKILL.mdinconsistent: its description and capability matrix still enumerate only the previous harnesses, its role targets omit Pi Desktop, and it still states that Pi has skills only without distinguishing Desktop. Agents using the bundled dotagents skill will consequently give stale guidance about the newly advertised integrations.AGENTS.md reference: AGENTS.md:L49-L51
Useful? React with 👍 / 👎.