Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/cli/src/utils/skillsMirror.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)", () => {
Expand Down
22 changes: 21 additions & 1 deletion packages/cli/src/utils/skillsMirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand All @@ -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;
Expand Down Expand Up @@ -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 });
}
Expand Down
Loading