From d2a9efb90fd7b9375851c1fb43c2de16c31db225 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Fri, 21 Aug 2026 09:37:53 -0700 Subject: [PATCH] Preserve inferred agent update scope Prepared with AI assistance from Codex under explicit maintainer authorization. --- cli/bin/commands/skills.mjs | 30 +++++++++++++++--------------- tests/skills-cli.test.js | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs index a314e239a..7923a0989 100644 --- a/cli/bin/commands/skills.mjs +++ b/cli/bin/commands/skills.mjs @@ -699,7 +699,7 @@ function deduplicateProviders(root, providers, scope) { * SKILL.md, so script-only fixes and removed files are detected. * Returns true if every bundle skill matches the local copy. */ -function isUpToDate(root, providers, bundleDir, scope) { +function isUpToDate(root, providers, bundleDir, scope, agentScope = scope) { const unique = deduplicateProviders(root, providers, scope); if (unique.length === 0) return false; @@ -725,7 +725,7 @@ function isUpToDate(root, providers, bundleDir, scope) { } } - if (!providerAgentsUpToDate(bundleDir, root, provider, scope)) return false; + if (!providerAgentsUpToDate(bundleDir, root, provider, agentScope)) return false; } return true; } @@ -747,7 +747,8 @@ async function check() { console.log('Checking for updates...\n'); try { const bundleDir = await downloadAndExtractBundle(); - const upToDate = isUpToDate(root, providers, bundleDir); + const agentScope = isHomeDir(root) ? 'user' : undefined; + const upToDate = isUpToDate(root, providers, bundleDir, undefined, agentScope); rmSync(bundleDir, { recursive: true, force: true }); if (upToDate) { @@ -1288,18 +1289,15 @@ function providerAgentsUpToDate(bundleDir, root, provider, scope) { const srcDir = join(bundleDir, provider, 'agents'); if (!existsSync(srcDir)) return true; - const projectDir = join(root, provider, 'agents'); - const destDirs = scope === 'user' - ? [artifact.userDir(root)] - : scope === 'project' || !isHomeDir(root) - ? [projectDir] - : [...new Set([artifact.userDir(root), projectDir])]; + const destDir = scope === 'user' + ? artifact.userDir(root) + : join(root, provider, 'agents'); const agentFiles = readdirSync(srcDir).filter(name => name.endsWith(artifact.ext)); - return agentFiles.every(name => destDirs.some(destDir => { + return agentFiles.every(name => { const localPath = join(destDir, name); return existsSync(localPath) && hashSkillFile(join(srcDir, name)) === hashSkillFile(localPath); - })); + }); } function copyProviderAgents(bundleDir, root, providers, { scope, home = homedir() } = {}) { @@ -2101,7 +2099,9 @@ function resolveUpdateTarget({ projectRoot, home, explicitScope }) { const homeRooted = isHomeDir(projectRoot); if (homeRooted && !explicitScope) { const providers = findInstalledProviders(home); - return providers.length ? { root: home, scope: undefined, providers, scopeLabel: 'user level' } : null; + return providers.length + ? { root: home, scope: undefined, agentScope: 'user', providers, scopeLabel: 'user level' } + : null; } const projectProviders = homeRooted ? [] : findImpeccableProviders(projectRoot, 'project'); @@ -2232,7 +2232,7 @@ async function update(flags = []) { : { root: projectRoot, scope: 'project', providers: target.projectProviders, scopeLabel: 'this project' }; } - const { root, scope } = target; + const { root, scope, agentScope = scope } = target; console.log(`Updating the ${target.scopeLabel} install: ${formatPathForDisplay(root)} (${target.providers.join(', ')})`); const providers = target.providers; const linkedProviders = findLinkedProviders(root, providers, scope); @@ -2256,7 +2256,7 @@ async function update(flags = []) { } // Compare local vs remote -- skip if already up to date - if (isUpToDate(root, copyProviders, tmpDir, scope)) { + if (isUpToDate(root, copyProviders, tmpDir, scope, agentScope)) { try { const wantHooks = installHooks && await decideHookInstall(root, copyProviders, { yes }); const hookTargets = wantHooks ? copyProviderHooks(tmpDir, root, copyProviders, { force }) : []; @@ -2292,7 +2292,7 @@ async function update(flags = []) { if (migrated > 0) console.log('Migrated a prefixed install back to /impeccable (the i- prefix is no longer used).'); const updated = refreshProviderSkills(tmpDir, root, copyProviders, scope); - reportProviderAgents(copyProviderAgents(tmpDir, root, copyProviders, { scope })); + reportProviderAgents(copyProviderAgents(tmpDir, root, copyProviders, { scope: agentScope })); const wantHooks = installHooks && await decideHookInstall(root, providers, { yes }); const hookTargets = wantHooks ? copyProviderHooks(tmpDir, root, providers, { force }) : []; diff --git a/tests/skills-cli.test.js b/tests/skills-cli.test.js index fde99abac..d01b7b83c 100644 --- a/tests/skills-cli.test.js +++ b/tests/skills-cli.test.js @@ -325,6 +325,29 @@ describe('copyProviderAgents: Claude, Copilot, and Cursor subagents', () => { rmSync(home, { recursive: true, force: true }); }, 15000); + test('inferred home-rooted updates refresh stale or missing Copilot user agents', () => { + const home = mkdtempSync(join(tmpdir(), 'imp-agents-update-home-')); + execSync('git init', { cwd: home }); + const bundleRoot = createFakeUniversalBundle(home, ['.github']); + const env = { ...process.env, HOME: home, IMPECCABLE_BUNDLE_PATH: bundleRoot }; + const userAgent = join(home, '.copilot', 'agents', 'impeccable-finish-reviewer.agent.md'); + const projectAgent = join(home, '.github', 'agents', 'impeccable-finish-reviewer.agent.md'); + + run('skills install -y --scope=global --no-hooks --providers=github', { cwd: home, env }); + writeFileSync(userAgent, 'stale copy\n'); + + run('skills update -y --no-hooks', { cwd: home, env }); + expect(readFileSync(userAgent, 'utf8')).toContain('Copilot reviewer body.'); + expect(existsSync(projectAgent)).toBe(false); + + rmSync(userAgent); + run('skills update -y --no-hooks', { cwd: home, env }); + expect(readFileSync(userAgent, 'utf8')).toContain('Copilot reviewer body.'); + expect(existsSync(projectAgent)).toBe(false); + + rmSync(home, { recursive: true, force: true }); + }, 20000); + test('project scope reports user-level Copilot agents that shadow the installed ones; Cursor never does (project wins there)', () => { const tmp = mkdtempSync(join(tmpdir(), 'imp-agents-shadow-')); const home = mkdtempSync(join(tmpdir(), 'imp-agents-shadow-home-'));