From db6ab720bc422345cc8159b5876e64997ebcf3cb Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:23:40 -0400 Subject: [PATCH] Install shared skills for 11 more agents and stop Qoder listing skills twice (#31657) * Install shared skills for 11 more agents Add skill install provider entries for OpenClaude (own .openclaude folders, OPENCLAUDE_CONFIG_DIR), and OpenCode 2, MiMo Code, OMP, Prime Agent, Qoder CN, Codebuff, Freebuff, Ante, Jcode and DeepSeek Harness, which read the shared .agents/skills folder at both scopes. A coverage test now requires every Orca agent to have an entry or a recorded reason it has none. * Use the shared skills folder for Qoder Qoder's CLI loads .agents/skills at both scopes by default, so its own .qoder/skills placement made every skill appear twice. * Keep scanning Qoder's own skill folders Qoder still reads .qoder/skills, so discovery keeps both folders as discovery-only roots now that installs go to the canonical root. --- .../skill-discovery-concurrency.test.ts | 2 +- src/main/skills/skill-discovery-sources.ts | 18 ++++- .../skill-provider-destinations.test.ts | 2 +- .../skill-provider-discovery-coverage.test.ts | 31 +++++++ src/shared/skill-install-providers.ts | 80 +++++++++++++++++-- 5 files changed, 124 insertions(+), 9 deletions(-) diff --git a/src/main/skills/skill-discovery-concurrency.test.ts b/src/main/skills/skill-discovery-concurrency.test.ts index 3213c919043..dcf0f59235d 100644 --- a/src/main/skills/skill-discovery-concurrency.test.ts +++ b/src/main/skills/skill-discovery-concurrency.test.ts @@ -185,7 +185,7 @@ describe('bounded concurrent skill discovery', () => { const line = String(info.mock.calls.at(0)?.at(0)) // `present` is the signal that separates "big tree" from "big root set", and // is not derivable from the other counts. - expect(line).toContain('[skills] scan roots=33 present=3 walked=33 skills=3') + expect(line).toContain('[skills] scan roots=35 present=3 walked=35 skills=3') expect(line).toContain('home-claude') expect(line).not.toContain(home) expect(line).not.toContain(tmpdir()) diff --git a/src/main/skills/skill-discovery-sources.ts b/src/main/skills/skill-discovery-sources.ts index f02e7e7de76..c8b68b7fb48 100644 --- a/src/main/skills/skill-discovery-sources.ts +++ b/src/main/skills/skill-discovery-sources.ts @@ -165,6 +165,14 @@ export function buildSkillDiscoverySources( ['agent-skills'], 'omp' ), + source( + 'home-qoder', + 'Qoder home', + pathApi.join(home, '.qoder', 'skills'), + 'home', + ['agent-skills'], + 'qoder' + ), source('home-hermes', 'Hermes home', hermesSkillsRoot, 'home', ['agent-skills'], 'hermes'), source( 'home-prime-agent', @@ -210,7 +218,15 @@ export function buildSkillDiscoverySources( null ), ...providerRepoSources(repoPath, label, pathApi), - // OpenCode also reads its own project folder; it installs into the canonical root above. + // OpenCode and Qoder also read their own project folders; they install into the canonical root above. + source( + `repo-qoder-${stablePathId(repoPath)}`, + `${label} .qoder`, + pathApi.join(repoPath, '.qoder', 'skills'), + 'repo', + ['agent-skills'], + 'qoder' + ), source( `repo-opencode-${stablePathId(repoPath)}`, `${label} .opencode`, diff --git a/src/main/skills/skill-provider-destinations.test.ts b/src/main/skills/skill-provider-destinations.test.ts index 4254a6bd07f..55254420218 100644 --- a/src/main/skills/skill-provider-destinations.test.ts +++ b/src/main/skills/skill-provider-destinations.test.ts @@ -128,7 +128,7 @@ describe('resolveSkillProviderDestinations', () => { resolveSkillProviderDestinations({ scope: 'global', homeDirectory: HOME, - detectedProviders: ['aider', 'ante', 'not-an-agent'] + detectedProviders: ['aider', 'hermes', 'not-an-agent'] }) ).toEqual([]) }) diff --git a/src/main/skills/skill-provider-discovery-coverage.test.ts b/src/main/skills/skill-provider-discovery-coverage.test.ts index ed6181a5be1..6192922c463 100644 --- a/src/main/skills/skill-provider-discovery-coverage.test.ts +++ b/src/main/skills/skill-provider-discovery-coverage.test.ts @@ -1,10 +1,19 @@ import { join, posix, resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { SKILL_INSTALL_PROVIDERS } from '../../shared/skill-install-providers' +import type { TuiAgent } from '../../shared/tui-agent' +import { TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' import { buildSkillDiscoverySources } from './skill-discovery-sources' import { resolveEnvironmentSkillProviderRoots } from './skill-provider-runtime-roots' const HOME = '/home/test' + +// Why: a new agent must get an entry or a recorded reason, never a silent gap. +const AGENTS_WITHOUT_INSTALL_PROVIDER: Partial> = { + 'claude-agent-teams': 'detected only alongside claude, whose entry covers .claude/skills', + aider: 'no skills support', + hermes: 'Windows home under %LOCALAPPDATA% and opt-in project skills fit no table shape' +} const WORKSPACE = '/workspace/current' function discover(providerRootOverrides = {}) { @@ -75,4 +84,26 @@ describe('skill install provider table drives discovery', () => { } } }) + + it('gives every Orca agent an entry or a recorded reason it has none', () => { + const covered = new Set(SKILL_INSTALL_PROVIDERS.map((provider) => provider.id)) + for (const agent of Object.keys(TUI_AGENT_CONFIG)) { + expect( + covered.has(agent) !== Object.hasOwn(AGENTS_WITHOUT_INSTALL_PROVIDER, agent), + agent + ).toBe(true) + } + }) + + it('keeps scanning Qoder folders it still reads after installs moved to the canonical root', () => { + const qoderPaths = discover() + .filter((source) => source.owner === 'qoder') + .map((source) => source.path) + expect(qoderPaths).toEqual( + expect.arrayContaining([ + posix.join(HOME, '.qoder', 'skills'), + posix.join(WORKSPACE, '.qoder', 'skills') + ]) + ) + }) }) diff --git a/src/shared/skill-install-providers.ts b/src/shared/skill-install-providers.ts index 64a544da788..d1b44c0e27b 100644 --- a/src/shared/skill-install-providers.ts +++ b/src/shared/skill-install-providers.ts @@ -97,12 +97,6 @@ export const SKILL_INSTALL_PROVIDERS = Object.freeze([ globalSegments: ['.codebuddy', 'skills'], workspaceSegments: ['.codebuddy', 'skills'] }, - { - id: 'qoder', - displayName: 'Qoder', - globalSegments: ['.qoder', 'skills'], - workspaceSegments: ['.qoder', 'skills'] - }, { id: 'kiro', displayName: 'Kiro', @@ -118,6 +112,14 @@ export const SKILL_INSTALL_PROVIDERS = Object.freeze([ globalSegments: ['.gemini', 'config', 'skills'], workspaceSegments: null }, + // Why: OpenClaude keeps its own `.openclaude` home; it does not read Claude's folders by default. + { + id: 'openclaude', + displayName: 'OpenClaude', + globalSegments: ['.openclaude', 'skills'], + workspaceSegments: ['.openclaude', 'skills'], + homeEnv: 'OPENCLAUDE_CONFIG_DIR' + }, // Why: every agent below reads the canonical .agents/skills root at both scopes. { id: 'autohand', @@ -214,6 +216,72 @@ export const SKILL_INSTALL_PROVIDERS = Object.freeze([ displayName: 'GitHub Copilot', globalSegments: null, workspaceSegments: null + }, + { + id: 'opencode2', + displayName: 'OpenCode 2', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'mimo-code', + displayName: 'MiMo Code', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'omp', + displayName: 'OMP', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'prime-agent', + displayName: 'Prime Agent', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'qoder', + displayName: 'Qoder', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'qoder-cn', + displayName: 'Qoder CN', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'codebuff', + displayName: 'Codebuff', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'freebuff', + displayName: 'Freebuff', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'ante', + displayName: 'Ante', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'jcode', + displayName: 'Jcode', + globalSegments: null, + workspaceSegments: null + }, + { + id: 'dsh', + displayName: 'DeepSeek Harness', + globalSegments: null, + workspaceSegments: null } ] satisfies SkillInstallProviderEntry[])