diff --git a/src/core/registry.mjs b/src/core/registry.mjs index ba05595..e64abe0 100644 --- a/src/core/registry.mjs +++ b/src/core/registry.mjs @@ -481,8 +481,14 @@ export function buildRegistry(customHome = null) { Object.entries(agentDirs) .filter(([agentKey, cfg]) => isAgentVisible(cfg, agentKey, overrides)) // An Agent that cannot read the Skill has no trigger word for it. - // Listing one told the user to type a command that does nothing. - .filter(([agentKey]) => !agentOnly || agentKey === agentOnly) + // Listing one tells the user to type a command that does nothing. + // + // `agents` above already answers "can this Agent read it": the link + // exists, or the Agent scans the SSOT natively. Asking that map is + // what makes this true after an unlink or a trash restore too — both + // leave the Skill in place with its links gone, and both used to keep + // advertising the trigger word for Agents that had just lost it. + .filter(([agentKey]) => agents[agentKey]) .map(([agentKey, cfg]) => [ agentKey, `${cfg.triggerPrefix || "/"}${cfg.type === "native" ? fmName || name : name}`, diff --git a/test/registry.test.mjs b/test/registry.test.mjs index 00d1020..561134b 100644 --- a/test/registry.test.mjs +++ b/test/registry.test.mjs @@ -93,6 +93,12 @@ test("registry uses configured agent triggers and metadata overrides", (t) => { writeFileSync(join(skillDir, "SKILL.md"), "---\nname: demo\ndescription: demo skill\n---\n"); // cursor is switched on explicitly; gemini is left to the default, and its // directory does not exist in this sandbox. + // The link has to exist for cursor to have a trigger word at all: a trigger + // is what an Agent that can reach the Skill would answer to, and cursor can + // only reach it through this link. + const cursorSkills = join(tmp, ".cursor", "skills"); + mkdirSync(cursorSkills, { recursive: true }); + symlinkSync(skillDir, join(cursorSkills, "demo"), "dir"); saveUserOverrides(join(tmp, ".skillhub", "overrides.json"), { notesOverrides: { demo: "portable note" }, managedSkills: { demo: "external-manager" }, @@ -108,6 +114,16 @@ test("registry uses configured agent triggers and metadata overrides", (t) => { assert.equal(reg.skills.demo.triggers.codex, "$demo"); assert.equal(reg.skills.demo.triggers.cursor, "/demo"); + // Take the link away and the trigger word goes with it. An Agent that cannot + // reach the Skill has no command to offer, and printing one sends the user to + // type something that does nothing — which is what `unlink` and a restore + // from the trash both used to leave behind. + rmSync(join(cursorSkills, "demo")); + const afterUnlink = buildRegistry(tmp); + assert.equal(afterUnlink.skills.demo.agents.cursor, false); + assert.equal(afterUnlink.skills.demo.triggers.cursor, undefined); + assert.equal(afterUnlink.skills.demo.triggers.codex, "$demo", "the native Agent still reaches it"); + // An Agent that is not in use is left out of the registry entirely, so the // switch means the same thing to `scan`, to the sync planner and to the // dashboard.