fix(skills): skip mirror fan-out to agents that read the universal store (#3325)

This commit is contained in:
Santhi Prakash
2026-08-24 09:41:19 -04:00
committed by GitHub
parent 2ca578f945
commit 95e1ac9f04
2 changed files with 45 additions and 1 deletions
@@ -177,6 +177,30 @@ describe("mirrorGlobalSkills", () => {
const link = join(home, ".cursor", "skills", "hyperframes");
expect(realpathSync(link)).toBe(realpathSync(join(home, ".claude", "skills", "hyperframes")));
});
// Pi natively discovers BOTH ~/.pi/agent/skills and the universal
// ~/.agents/skills (pi's packages/coding-agent/docs/skills.md#locations).
// A mirrored per-agent copy collides with the universal one and Pi skips
// the universal entry on name conflict (#3294), so the mirror must not fan
// out to it.
it("skips agents that natively read the universal store (pi, #3294)", () => {
const home = makeHome();
seedStore(home, ["hyperframes"]);
installMarker(home, ".pi/agent"); // Pi present
installMarker(home, ".cursor"); // a regular per-dir agent, for contrast
const { mirrored } = mirrorGlobalSkills({
skills: ["hyperframes"],
home,
platform: "linux",
env: ENV,
});
const agents = mirrored.map((m) => m.agent);
expect(agents).not.toContain("pi");
expect(agents).toContain("cursor");
// no per-agent copy created where the universal store already serves Pi
expect(existsSync(join(home, ".pi", "agent", "skills", "hyperframes"))).toBe(false);
});
});
describe("AGENT_GLOBAL_DIRS (generated table)", () => {
+21 -1
View File
@@ -9,7 +9,10 @@
// populate.
//
// So we mirror the canonical Claude store into each of those per-agent dirs, but
// only for agents the machine actually has (their marker dir exists). On Unix
// only for agents the machine actually has (their marker dir exists). Agents
// that already consume the universal ~/.agents/skills store globally (Pi) are
// skipped: their universal copy is authoritative and a per-agent copy would
// collide with it (#3294). On Unix
// each skill is a relative symlink back into the store (one source of truth,
// near-zero size, auto-fresh on update); on Windows it's a copy, because
// symlinks there need admin / Developer Mode and otherwise silently dangle —
@@ -24,6 +27,22 @@ import { homedir } from "node:os";
import { dirname, isAbsolute, join, relative } from "node:path";
import { AGENT_GLOBAL_DIRS, type AgentDirBase } from "./agentDirs.generated.js";
/**
* Agents that natively discover the universal `~/.agents/skills` store globally
* in ADDITION to their own agent-specific directory. Mirroring into their own
* dir makes every skill discoverable twice.
*
* Pi is the known case (earendil-works/pi): it reads both `~/.pi/agent/skills/`
* and `~/.agents/skills/` as global locations (pi's packages/coding-agent/docs/
* skills.md#locations), so a mirrored entry collides with the universal copy
* and Pi skips the universal one on name conflict (#3294).
*
* The generated table cannot carry this capability — it is a plain
* (agent, base, sub) list synced from vercel-labs/skills — so the set lives
* here next to the mirror logic that needs it.
*/
const UNIVERSAL_STORE_READERS = new Set(["pi"]);
export interface MirrorResult {
/** The store mirrored from, or null when no global Claude store was found. */
source: string | null;
@@ -128,6 +147,7 @@ export function mirrorGlobalSkills(opts: {
for (const { agent, base, sub } of AGENT_GLOBAL_DIRS) {
const targetDir = join(bases[base], ...sub.split("/").filter(Boolean));
if (targetDir === source || targetDir === universalStore) continue; // install-owned
if (UNIVERSAL_STORE_READERS.has(agent)) continue; // already reads the universal store (#3294)
if (!existsSync(dirname(targetDir))) continue; // agent not installed (no marker)
if (mirrorInto(targetDir, source, skills, platform)) mirrored.push({ agent, dir: targetDir });
}