fix: install agent skills project-scoped so the PromptScript target lands

The in-app skill install/update commands hardcoded --global. The npx skills
CLI cannot install the PromptScript target (the one Orca consumes) in global
mode: it skips that single target, still prints a success banner, and exits 0,
so the skill silently never installs for Orca.

Drop --global and add -y to the install command (and drop --global from the
update command). The setup terminal runs in the home dir, so a project-scoped
install lands in ~/.agents/skills, which discovery already classifies as a
'home' source the feature-setup surfaces detect — no detection change needed.
Also update the native-Windows update-normalization regex to match the new
project-scoped update form.

Fixes #4642

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-06-27 23:29:22 -07:00
co-authored by Orca
parent b45cb545c5
commit d4134c5569
14 changed files with 113 additions and 36 deletions
+34
View File
@@ -3,6 +3,10 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { buildSkillDiscoverySources, discoverSkills } from './discovery'
import {
buildAgentFeatureSkillInstallCommand,
ORCHESTRATION_SKILL_NAME
} from '../../shared/agent-feature-install-commands'
import type { Repo } from '../../shared/types'
function makeRepo(path: string, connectionId: string | null = null): Repo {
@@ -102,6 +106,36 @@ describe('skill discovery', () => {
])
})
it('classifies a project-scoped install run from home as a home source (regression for #4642)', async () => {
// Why: the in-app setup terminal runs in the home dir, so a non-global
// `npx skills add ... -y` install lands in ~/.agents/skills. That must be
// discovered as a 'home' source so the feature-setup surfaces (which filter
// on 'home') flip to "Installed". `--global` skipped this target silently.
const installCommand = buildAgentFeatureSkillInstallCommand([ORCHESTRATION_SKILL_NAME])
expect(installCommand).not.toContain('--global')
const root = await mkdtemp(join(tmpdir(), 'orca-skills-'))
const home = join(root, 'home')
const skillDir = join(home, '.agents', 'skills', ORCHESTRATION_SKILL_NAME)
await mkdir(skillDir, { recursive: true })
await writeFile(
join(skillDir, 'SKILL.md'),
[
'---',
`name: ${ORCHESTRATION_SKILL_NAME}`,
'description: Coordinate agents.',
'---',
''
].join('\n')
)
const result = await discoverSkills({ homeDir: home, cwd: home, repos: [] })
expect(result.skills).toMatchObject([
{ name: ORCHESTRATION_SKILL_NAME, sourceKind: 'home', directoryPath: skillDir }
])
})
it('does not loop through recursive symlinked skill directories', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skills-'))
const home = join(root, 'home')
@@ -49,7 +49,7 @@ describe('onboarding feature setup telemetry validation', () => {
linear_tickets: false,
orchestration: true,
selected_count: 2,
command: 'npx skills add https://github.com/stablyai/orca --global'
command: 'npx skills add https://github.com/stablyai/orca --skill orchestration -y'
} as never).ok
).toBe(false)
expect(
@@ -119,7 +119,7 @@ describe('onboarding feature setup runner', () => {
expect(text).toBe(ALL_SKILL_INSTALL_COMMAND)
expect(text).toBe(
'npx skills add https://github.com/stablyai/orca --skill orca-cli computer-use orchestration orca-linear --global'
'npx skills add https://github.com/stablyai/orca --skill orca-cli computer-use orchestration orca-linear -y'
)
})
@@ -7,8 +7,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
import { TooltipProvider } from '../ui/tooltip'
const INSTALL_COMMAND = 'npx skills add https://github.com/stablyai/orca --skill orca-cli --global'
const UPDATE_COMMAND = 'npx skills update orca-cli --global'
const INSTALL_COMMAND = 'npx skills add https://github.com/stablyai/orca --skill orca-cli -y'
const UPDATE_COMMAND = 'npx skills update orca-cli'
const mocks = vi.hoisted(() => ({
clipboardWrite: vi.fn(),
@@ -16,8 +16,8 @@ vi.mock('./AgentSkillSetupPanel', () => ({
describe('BrowserUseSkillStep', () => {
it('forwards a single-skill installed command even when setup installs a bundle', () => {
const bundleInstallCommand =
'npx skills add https://github.com/stablyai/orca --skill orca-cli orchestration --global'
const updateCommand = 'npx skills update orca-cli --global'
'npx skills add https://github.com/stablyai/orca --skill orca-cli orchestration -y'
const updateCommand = 'npx skills update orca-cli'
renderToStaticMarkup(
<BrowserUseSkillStep
@@ -92,7 +92,7 @@ describe('CliSection project runtime defaults', () => {
expect(capturedPanel.props?.command).toContain("wsl.exe -d 'Ubuntu' -- sh -c")
expect(capturedPanel.props?.command).toContain('npx skills add')
expect(capturedPanel.props?.installedCommand).toContain("wsl.exe -d 'Ubuntu' -- sh -c")
expect(capturedPanel.props?.installedCommand).toContain('npx skills update orca-cli --global')
expect(capturedPanel.props?.installedCommand).toContain('npx skills update orca-cli')
expect(getWslInstallStatus).toHaveBeenCalledWith({ distro: 'Ubuntu' })
expect(getWslInstallStatus).toHaveBeenCalledTimes(2)
})
@@ -10,7 +10,7 @@ import {
describe('CliSkillRuntimeSetup runtime helpers', () => {
it('wraps WSL skill installs in the selected distro login shell', () => {
const command = buildSkillInstallCommandForRuntime('npx skills add orchestration --global', {
const command = buildSkillInstallCommandForRuntime('npx skills add orchestration -y', {
runtime: 'wsl',
wslDistro: 'Ubuntu',
label: 'WSL Ubuntu'
@@ -18,11 +18,11 @@ describe('CliSkillRuntimeSetup runtime helpers', () => {
expect(command).toContain("wsl.exe -d 'Ubuntu' -- sh -c")
expect(command).toContain('getent passwd')
expect(command).toContain('npx skills add orchestration --global')
expect(command).toContain('npx skills add orchestration -y')
})
it('wraps WSL skill updates with the same selected distro login shell', () => {
const command = buildSkillCommandForRuntime('npx skills update orchestration --global', {
const command = buildSkillCommandForRuntime('npx skills update orchestration', {
runtime: 'wsl',
wslDistro: 'Fedora Remix',
label: 'WSL Fedora Remix'
@@ -30,13 +30,13 @@ describe('CliSkillRuntimeSetup runtime helpers', () => {
expect(command).toContain("wsl.exe -d 'Fedora Remix' -- sh -c")
expect(command).toContain('getent passwd')
expect(command).toContain('npx skills update orchestration --global')
expect(command).toContain('npx skills update orchestration')
})
it('reinstalls Windows-host skill updates through the add path', () => {
expect(
buildSkillCommandForRuntime(
'npx skills update orchestration --global',
'npx skills update orchestration',
{
runtime: 'host',
label: 'Windows'
@@ -47,22 +47,22 @@ describe('CliSkillRuntimeSetup runtime helpers', () => {
})
it('treats missing runtime as a Windows host fallback for skill updates', () => {
expect(
buildSkillCommandForRuntime('npx skills update orca-cli --global', undefined, 'win32')
).toBe(buildAgentFeatureSkillInstallCommand(['orca-cli']))
expect(buildSkillCommandForRuntime('npx skills update orca-cli', undefined, 'win32')).toBe(
buildAgentFeatureSkillInstallCommand(['orca-cli'])
)
})
it('keeps non-Windows host skill updates on the update path', () => {
expect(
buildSkillCommandForRuntime(
'npx skills update orchestration --global',
'npx skills update orchestration',
{
runtime: 'host',
label: 'This device'
},
'linux'
)
).toBe('npx skills update orchestration --global')
).toBe('npx skills update orchestration')
})
it('preserves the selected WSL distro for skill discovery', () => {
@@ -100,7 +100,7 @@ function normalizeWindowsSkillUpdateCommand(
}
const trimmedCommand = command.trim()
const updateMatch = /^npx\s+skills\s+update\s+([A-Za-z0-9_-]+)\s+--global$/i.exec(trimmedCommand)
const updateMatch = /^npx\s+skills\s+update\s+([A-Za-z0-9_-]+)$/i.exec(trimmedCommand)
if (!updateMatch) {
return command
}
@@ -7,8 +7,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getOrchestrationUsageExamples } from '@/lib/orchestration-usage-examples'
import { OrchestrationPane } from './OrchestrationPane'
const INSTALL_COMMAND =
'npx skills add https://github.com/stablyai/orca --skill orchestration --global'
const INSTALL_COMMAND = 'npx skills add https://github.com/stablyai/orca --skill orchestration -y'
// Why: on the win32 host this pane runs against, the update command is
// normalized back into the install command, so both match the install form.
const UPDATE_COMMAND = INSTALL_COMMAND
const mocks = vi.hoisted(() => ({
@@ -347,8 +347,7 @@ describe('LinearAgentSkillSetupPrompt', () => {
expect(document.body.textContent).toContain("wsl.exe -d 'Fedora' -- bash -lc 'npx skills add")
expect(mocks.panelProps.at(-1)).toEqual(
expect.objectContaining({
installedCommand:
"wsl.exe -d 'Fedora' -- bash -lc 'npx skills update orca-linear --global'",
installedCommand: "wsl.exe -d 'Fedora' -- bash -lc 'npx skills update orca-linear'",
terminalShellOverride: 'powershell.exe',
getPrerequisiteStatus: expect.any(Function)
})
@@ -156,7 +156,7 @@ describe('LinearAgentSkillSetupPrompt update command', () => {
await renderPrompt()
expect(mocks.panelProps.at(-1)).toEqual(
expect.objectContaining({ installedCommand: 'npx skills update orca-linear --global' })
expect.objectContaining({ installedCommand: 'npx skills update orca-linear' })
)
})
@@ -166,7 +166,7 @@ describe('LinearAgentSkillSetupPrompt update command', () => {
await renderPrompt()
expect(mocks.panelProps.at(-1)).toEqual(
expect.objectContaining({ installedCommand: 'npx skills update linear-tickets --global' })
expect.objectContaining({ installedCommand: 'npx skills update linear-tickets' })
)
})
@@ -176,7 +176,7 @@ describe('LinearAgentSkillSetupPrompt update command', () => {
await renderPrompt()
expect(mocks.panelProps.at(-1)).toEqual(
expect.objectContaining({ installedCommand: 'npx skills update orca-linear --global' })
expect.objectContaining({ installedCommand: 'npx skills update orca-linear' })
)
})
})
@@ -115,6 +115,28 @@ describe('hasInstalledAgentSkill', () => {
).toBe(true)
})
it('detects a project-scoped install that lands in the home dir (regression for #4642)', () => {
// Why: the setup terminal runs in the home dir, so a non-global install
// writes to ~/.agents/skills with sourceKind 'home' — the feature-setup
// surfaces' filter must still report it installed.
expect(
hasInstalledAgentSkill(
[
skill({
name: 'orchestration',
sourceKind: 'home',
sourceLabel: 'Agent skills home',
rootPath: '/Users/test/.agents/skills',
directoryPath: '/Users/test/.agents/skills/orchestration',
skillFilePath: '/Users/test/.agents/skills/orchestration/SKILL.md'
})
],
'orchestration',
{ sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS }
)
).toBe(true)
})
it('matches installed skills by any accepted name', () => {
expect(
hasInstalledAgentSkillNamed(
@@ -11,25 +11,39 @@ import {
} from './agent-feature-install-commands'
describe('agent feature skill commands', () => {
// Why: `--global` silently skips the PromptScript target Orca consumes while
// exiting 0, so the install/update commands must stay project-scoped.
it('builds project-scoped single-skill install commands', () => {
expect(buildAgentFeatureSkillInstallCommand(['orchestration'])).toBe(
'npx skills add https://github.com/stablyai/orca --skill orchestration -y'
)
expect(buildAgentFeatureSkillInstallCommand(['orca-cli', 'orchestration'])).toBe(
'npx skills add https://github.com/stablyai/orca --skill orca-cli orchestration -y'
)
})
it('never emits the --global flag that masks the PromptScript failure', () => {
expect(buildAgentFeatureSkillInstallCommand(['orchestration'])).not.toContain('--global')
expect(buildAgentFeatureSkillUpdateCommand('orchestration')).not.toContain('--global')
})
it('builds single-skill update commands', () => {
expect(buildAgentFeatureSkillUpdateCommand('orchestration')).toBe(
'npx skills update orchestration --global'
'npx skills update orchestration'
)
})
it('trims and rejects blank update skill names', () => {
expect(buildAgentFeatureSkillUpdateCommand(' orca-cli ')).toBe(
'npx skills update orca-cli --global'
)
expect(buildAgentFeatureSkillUpdateCommand(' orca-cli ')).toBe('npx skills update orca-cli')
expect(() => buildAgentFeatureSkillUpdateCommand(' ')).toThrow('A skill name is required.')
})
it('exports single-skill update constants without changing install bundles', () => {
expect(ORCA_CLI_SKILL_UPDATE_COMMAND).toBe('npx skills update orca-cli --global')
expect(COMPUTER_USE_SKILL_UPDATE_COMMAND).toBe('npx skills update computer-use --global')
expect(ORCHESTRATION_SKILL_UPDATE_COMMAND).toBe('npx skills update orchestration --global')
expect(ORCA_LINEAR_SKILL_UPDATE_COMMAND).toBe('npx skills update orca-linear --global')
expect(LINEAR_TICKETS_SKILL_UPDATE_COMMAND).toBe('npx skills update linear-tickets --global')
expect(ORCA_CLI_SKILL_UPDATE_COMMAND).toBe('npx skills update orca-cli')
expect(COMPUTER_USE_SKILL_UPDATE_COMMAND).toBe('npx skills update computer-use')
expect(ORCHESTRATION_SKILL_UPDATE_COMMAND).toBe('npx skills update orchestration')
expect(ORCA_LINEAR_SKILL_UPDATE_COMMAND).toBe('npx skills update orca-linear')
expect(LINEAR_TICKETS_SKILL_UPDATE_COMMAND).toBe('npx skills update linear-tickets')
expect(ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND).toBe(
buildAgentFeatureSkillInstallCommand(['orca-cli', 'orchestration'])
)
+9 -2
View File
@@ -11,7 +11,12 @@ export function buildAgentFeatureSkillInstallCommand(skillNames: readonly string
if (skillNames.length === 0) {
throw new Error('At least one skill name is required.')
}
return `npx skills add ${ORCA_SKILLS_REPOSITORY_URL} --skill ${skillNames.join(' ')} --global`
// Why: `--global` skips the PromptScript target (the one Orca consumes) yet
// still exits 0, so installs silently never land. Project-scoped installs to
// <cwd>/.agents/skills succeed for every target; the setup terminal runs in
// the home dir, so this writes to ~/.agents/skills (a scanned 'home' source).
// `-y` keeps the auto-run terminal non-interactive.
return `npx skills add ${ORCA_SKILLS_REPOSITORY_URL} --skill ${skillNames.join(' ')} -y`
}
export function buildAgentFeatureSkillUpdateCommand(skillName: string): string {
@@ -19,7 +24,9 @@ export function buildAgentFeatureSkillUpdateCommand(skillName: string): string {
if (!trimmedSkillName) {
throw new Error('A skill name is required.')
}
return `npx skills update ${trimmedSkillName} --global`
// Why: must stay project-scoped to match the install above; a `--global`
// update would target an install that never happened.
return `npx skills update ${trimmedSkillName}`
}
export const ORCA_CLI_SKILL_INSTALL_COMMAND = buildAgentFeatureSkillInstallCommand([