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.
This commit is contained in:
Jinwoo Hong
2026-10-22 15:23:40 -04:00
committed by GitHub
parent 8fc1698ebb
commit db6ab720bc
5 changed files with 124 additions and 9 deletions
@@ -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())
+17 -1
View File
@@ -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`,
@@ -128,7 +128,7 @@ describe('resolveSkillProviderDestinations', () => {
resolveSkillProviderDestinations({
scope: 'global',
homeDirectory: HOME,
detectedProviders: ['aider', 'ante', 'not-an-agent']
detectedProviders: ['aider', 'hermes', 'not-an-agent']
})
).toEqual([])
})
@@ -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<Record<TuiAgent, string>> = {
'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<string>(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')
])
)
})
})
+74 -6
View File
@@ -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[])