From d4134c55691c097dfd48c0326b72b38780a8d8d5 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:29:22 -0700 Subject: [PATCH] fix: install agent skills project-scoped so the PromptScript target lands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/main/skills/discovery.test.ts | 34 +++++++++++++++++++ ...onboarding-feature-setup-validator.test.ts | 2 +- .../onboarding-feature-setup.test.ts | 2 +- .../settings/AgentSkillSetupPanel.test.tsx | 4 +-- .../settings/BrowserUseSkillStep.test.tsx | 4 +-- .../components/settings/CliSection.test.tsx | 2 +- .../settings/CliSkillRuntimeSetup.test.tsx | 20 +++++------ .../settings/CliSkillRuntimeSetup.tsx | 2 +- .../settings/OrchestrationPane.test.tsx | 5 +-- .../LinearAgentSkillSetupPrompt.test.tsx | 3 +- ...ntSkillSetupPrompt.update-command.test.tsx | 6 ++-- .../src/hooks/useInstalledAgentSkills.test.ts | 22 ++++++++++++ .../agent-feature-install-commands.test.ts | 32 ++++++++++++----- src/shared/agent-feature-install-commands.ts | 11 ++++-- 14 files changed, 113 insertions(+), 36 deletions(-) diff --git a/src/main/skills/discovery.test.ts b/src/main/skills/discovery.test.ts index 93a8e59aafc..580ed6e74bc 100644 --- a/src/main/skills/discovery.test.ts +++ b/src/main/skills/discovery.test.ts @@ -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') diff --git a/src/main/telemetry/onboarding-feature-setup-validator.test.ts b/src/main/telemetry/onboarding-feature-setup-validator.test.ts index 60b2358b6de..babc242c169 100644 --- a/src/main/telemetry/onboarding-feature-setup-validator.test.ts +++ b/src/main/telemetry/onboarding-feature-setup-validator.test.ts @@ -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( diff --git a/src/renderer/src/components/onboarding/onboarding-feature-setup.test.ts b/src/renderer/src/components/onboarding/onboarding-feature-setup.test.ts index ab1d1a43f91..42cd02447e0 100644 --- a/src/renderer/src/components/onboarding/onboarding-feature-setup.test.ts +++ b/src/renderer/src/components/onboarding/onboarding-feature-setup.test.ts @@ -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' ) }) diff --git a/src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx b/src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx index 69d2360b7e7..776ffec0517 100644 --- a/src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx +++ b/src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx @@ -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(), diff --git a/src/renderer/src/components/settings/BrowserUseSkillStep.test.tsx b/src/renderer/src/components/settings/BrowserUseSkillStep.test.tsx index 0b4dad98baf..5331907fe5f 100644 --- a/src/renderer/src/components/settings/BrowserUseSkillStep.test.tsx +++ b/src/renderer/src/components/settings/BrowserUseSkillStep.test.tsx @@ -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( { 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) }) diff --git a/src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx b/src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx index 64b7bb3dd1c..c4f3e0dc883 100644 --- a/src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx +++ b/src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx @@ -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', () => { diff --git a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx index 895caa1b80a..d5d223fcd72 100644 --- a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx +++ b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx @@ -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 } diff --git a/src/renderer/src/components/settings/OrchestrationPane.test.tsx b/src/renderer/src/components/settings/OrchestrationPane.test.tsx index 02da9913403..01d1496a1b8 100644 --- a/src/renderer/src/components/settings/OrchestrationPane.test.tsx +++ b/src/renderer/src/components/settings/OrchestrationPane.test.tsx @@ -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(() => ({ diff --git a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx index 420467d1aef..dd918f40d57 100644 --- a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx +++ b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx @@ -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) }) diff --git a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.update-command.test.tsx b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.update-command.test.tsx index 7ff473b49da..6e87a3f3bab 100644 --- a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.update-command.test.tsx +++ b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.update-command.test.tsx @@ -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' }) ) }) }) diff --git a/src/renderer/src/hooks/useInstalledAgentSkills.test.ts b/src/renderer/src/hooks/useInstalledAgentSkills.test.ts index 28453f00489..b8f41d7012e 100644 --- a/src/renderer/src/hooks/useInstalledAgentSkills.test.ts +++ b/src/renderer/src/hooks/useInstalledAgentSkills.test.ts @@ -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( diff --git a/src/shared/agent-feature-install-commands.test.ts b/src/shared/agent-feature-install-commands.test.ts index 97d253436a9..77f1c398317 100644 --- a/src/shared/agent-feature-install-commands.test.ts +++ b/src/shared/agent-feature-install-commands.test.ts @@ -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']) ) diff --git a/src/shared/agent-feature-install-commands.ts b/src/shared/agent-feature-install-commands.ts index c5272a84299..4a91f61f0f5 100644 --- a/src/shared/agent-feature-install-commands.ts +++ b/src/shared/agent-feature-install-commands.ts @@ -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 + // /.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([