From b6ab079ab9423f281937e5a84872a47daef5a752 Mon Sep 17 00:00:00 2001 From: Vie <125633564+Vieeeeeee@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:43:21 +0800 Subject: [PATCH] fix: drop the trigger word for an Agent that cannot read the Skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard listed "/bravo (Claude·Gemini)" for a Skill neither Agent could load. Restoring from the trash puts the Skill back without its links — the modal says so — and `unlink` removes one link on purpose. Both left the trigger column advertising a command that does nothing. registry.mjs already had the rule written down, and applied it only to the agent-only case. The `agents` map computed a few lines above is the real answer to "can this Agent reach it": the link exists, or the Agent scans the SSOT natively. Asking that map covers unlink and restore as well, and subsumes the agent-only special case. Found by walking the dashboard by hand: uninstall bravo, restore it from the trash, then read what the table claims against what is on disk. Co-Authored-By: Claude Opus 5 --- src/core/registry.mjs | 10 ++++++++-- test/registry.test.mjs | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) 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.