From 1340700f1b6c3e793f6f36faecd5a0757788ef5b Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Mon, 25 May 2026 01:25:33 +0200 Subject: [PATCH] fix(status): match branch-qualified cache for GitHub plugins `workspace status` looked up plugin caches by `-`, but `fetchPlugin`/`seedCacheFromClone` write to `-@` whenever the source URL pins a branch (e.g. /blob/main/...). The result was a false "not cached" right after a successful `skills add`. Plumb the parsed branch through `ParsedPluginSource` so `getPluginStatus` calls `getPluginCachePath` with the same branch the fetcher used. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/core/status.ts | 2 +- src/utils/plugin-path.ts | 2 ++ tests/unit/core/status-both-scopes.test.ts | 33 ++++++++++++++++++++++ tests/unit/utils/plugin-path.test.ts | 19 +++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/core/status.ts b/src/core/status.ts index 06d4d75e..f99d1f39 100644 --- a/src/core/status.ts +++ b/src/core/status.ts @@ -102,7 +102,7 @@ function getPluginStatus(parsed: ParsedPluginSource): PluginStatus { // Check if cached const cachePath = parsed.owner && parsed.repo - ? getPluginCachePath(parsed.owner, parsed.repo) + ? getPluginCachePath(parsed.owner, parsed.repo, parsed.branch) : ''; const available = cachePath ? existsSync(cachePath) : false; diff --git a/src/utils/plugin-path.ts b/src/utils/plugin-path.ts index bbaaba7d..3779d71b 100644 --- a/src/utils/plugin-path.ts +++ b/src/utils/plugin-path.ts @@ -48,6 +48,7 @@ export interface ParsedPluginSource { normalized: string; owner?: string; repo?: string; + branch?: string; } /** @@ -282,6 +283,7 @@ export function parsePluginSource( normalized: source, ...(parsed?.owner && { owner: parsed.owner }), ...(parsed?.repo && { repo: parsed.repo }), + ...(parsed?.branch && { branch: parsed.branch }), }; } diff --git a/tests/unit/core/status-both-scopes.test.ts b/tests/unit/core/status-both-scopes.test.ts index c27f0078..fbb442f7 100644 --- a/tests/unit/core/status-both-scopes.test.ts +++ b/tests/unit/core/status-both-scopes.test.ts @@ -142,4 +142,37 @@ describe('workspace status - both scopes', () => { await rm(separateHome, { recursive: true, force: true }); }); + + it('should mark GitHub plugin as cached when cache is branch-qualified', async () => { + // Regression: `skills add ` clones into `-@`, + // but `workspace status` looked up `-` and reported "not cached". + const homeDir = await mkdtemp(join(tmpdir(), 'allagents-status-home-')); + process.env.HOME = homeDir; + + const branchedCache = join( + homeDir, + '.allagents', + 'plugins', + 'marketplaces', + 'NousResearch-hermes-agent@main', + ); + await mkdir(branchedCache, { recursive: true }); + + await writeProjectConfig({ + repositories: [], + plugins: [ + 'https://github.com/NousResearch/hermes-agent/blob/main/skills/research/llm-wiki', + ], + clients: ['claude'], + }); + + const result = await getWorkspaceStatus(testDir); + expect(result.success).toBe(true); + expect(result.plugins.length).toBe(1); + expect(result.plugins[0]?.type).toBe('github'); + expect(result.plugins[0]?.available).toBe(true); + expect(result.plugins[0]?.path).toBe(branchedCache); + + await rm(homeDir, { recursive: true, force: true }); + }); }); diff --git a/tests/unit/utils/plugin-path.test.ts b/tests/unit/utils/plugin-path.test.ts index 1adbb70b..86199301 100644 --- a/tests/unit/utils/plugin-path.test.ts +++ b/tests/unit/utils/plugin-path.test.ts @@ -243,9 +243,28 @@ describe('parsePluginSource', () => { expect(result.type).toBe('github'); expect(result.owner).toBe('owner'); expect(result.repo).toBe('repo'); + expect(result.branch).toBeUndefined(); expect(result.original).toBe('https://github.com/owner/repo'); }); + it('should preserve branch from /blob/ URLs so the cache lookup is branch-qualified', () => { + const result = parsePluginSource( + 'https://github.com/owner/repo/blob/main/skills/research/llm-wiki', + ); + expect(result.type).toBe('github'); + expect(result.owner).toBe('owner'); + expect(result.repo).toBe('repo'); + expect(result.branch).toBe('main'); + }); + + it('should preserve branch from owner/repo@ref shorthand', () => { + const result = parsePluginSource('owner/repo@v1.2.0'); + expect(result.type).toBe('github'); + expect(result.owner).toBe('owner'); + expect(result.repo).toBe('repo'); + expect(result.branch).toBe('v1.2.0'); + }); + it('should parse local absolute paths', () => { const result = parsePluginSource('/absolute/path'); expect(result.type).toBe('local');