diff --git a/docs/reference/file-formats.md b/docs/reference/file-formats.md index 07d8541cd..a5d29180b 100644 --- a/docs/reference/file-formats.md +++ b/docs/reference/file-formats.md @@ -385,6 +385,10 @@ targets: ["*"] # * = all, or specific tools # zed, pi, qwencode, and factorydroid. Any of those tool sections can override it # by setting their own `disable-model-invocation` value below. disable-model-invocation: true +# (optional) shared default for tools that support the flag — claudecode, qwencode, +# vibe, and factorydroid. Any of those tool sections can override it by setting +# their own `user-invocable` value below. +user-invocable: false claudecode: # for claudecode-specific parameters model: sonnet # opus, sonnet, haiku, or any string allowed-tools: @@ -395,6 +399,7 @@ claudecode: # for claudecode-specific parameters disallowed-tools: # (optional) removes these tools while the skill is active (string or list) - "WebFetch" disable-model-invocation: true # (optional) disable model invocation for this skill + user-invocable: false # (optional) hide from the / menu while keeping model access scheduled-task: true # (optional) emit to .claude/scheduled-tasks//SKILL.md instead of .claude/skills//SKILL.md # paths (optional) limits auto-activation to matching globs. Accepts a # comma-separated string, e.g. paths: "src/**/*.ts,test/**/*.ts", or a list: @@ -491,6 +496,7 @@ cursor: # for Cursor-specific parameters (optional) author: rulesync factorydroid: # for Factory Droid-specific parameters (optional) disable-model-invocation: true # (optional) prevent the model from auto-invoking this skill + user-invocable: false # (optional) hide from the slash-command menu, keep model access takt: # takt specific parameters (optional; emitted under .takt/facets/knowledge/ — frontmatter is dropped on emit) name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..") extends: "base" # (optional) emit a leading `{extends:}` facet-inheritance directive (Takt 0.39.0+) diff --git a/skills/rulesync/file-formats.md b/skills/rulesync/file-formats.md index 07d8541cd..a5d29180b 100644 --- a/skills/rulesync/file-formats.md +++ b/skills/rulesync/file-formats.md @@ -385,6 +385,10 @@ targets: ["*"] # * = all, or specific tools # zed, pi, qwencode, and factorydroid. Any of those tool sections can override it # by setting their own `disable-model-invocation` value below. disable-model-invocation: true +# (optional) shared default for tools that support the flag — claudecode, qwencode, +# vibe, and factorydroid. Any of those tool sections can override it by setting +# their own `user-invocable` value below. +user-invocable: false claudecode: # for claudecode-specific parameters model: sonnet # opus, sonnet, haiku, or any string allowed-tools: @@ -395,6 +399,7 @@ claudecode: # for claudecode-specific parameters disallowed-tools: # (optional) removes these tools while the skill is active (string or list) - "WebFetch" disable-model-invocation: true # (optional) disable model invocation for this skill + user-invocable: false # (optional) hide from the / menu while keeping model access scheduled-task: true # (optional) emit to .claude/scheduled-tasks//SKILL.md instead of .claude/skills//SKILL.md # paths (optional) limits auto-activation to matching globs. Accepts a # comma-separated string, e.g. paths: "src/**/*.ts,test/**/*.ts", or a list: @@ -491,6 +496,7 @@ cursor: # for Cursor-specific parameters (optional) author: rulesync factorydroid: # for Factory Droid-specific parameters (optional) disable-model-invocation: true # (optional) prevent the model from auto-invoking this skill + user-invocable: false # (optional) hide from the slash-command menu, keep model access takt: # takt specific parameters (optional; emitted under .takt/facets/knowledge/ — frontmatter is dropped on emit) name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..") extends: "base" # (optional) emit a leading `{extends:}` facet-inheritance directive (Takt 0.39.0+) diff --git a/src/features/skills/claudecode-skill.test.ts b/src/features/skills/claudecode-skill.test.ts index 46f2129ec..ea644da6a 100644 --- a/src/features/skills/claudecode-skill.test.ts +++ b/src/features/skills/claudecode-skill.test.ts @@ -373,6 +373,27 @@ describe("ClaudecodeSkill", () => { }); }); + it("should convert to RulesyncSkill with user-invocable false", () => { + const frontmatter: ClaudecodeSkillFrontmatter = { + name: "hidden-skill", + description: "Skill hidden from the slash menu", + "user-invocable": false, + }; + + const skill = new ClaudecodeSkill({ + dirName: "hidden-skill", + frontmatter, + body: "Hidden body", + }); + + const rulesyncSkill = skill.toRulesyncSkill(); + const rulesyncFrontmatter = rulesyncSkill.getFrontmatter(); + + expect(rulesyncFrontmatter.claudecode).toEqual({ + "user-invocable": false, + }); + }); + it("should convert to RulesyncSkill with paths as string", () => { const frontmatter: ClaudecodeSkillFrontmatter = { name: "paths-string-skill", @@ -658,6 +679,74 @@ describe("ClaudecodeSkill", () => { expect(claudecodeSkill.getFrontmatter()["disable-model-invocation"]).toBeUndefined(); }); + it("should convert from RulesyncSkill with user-invocable false", () => { + const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = { + name: "hidden-skill", + description: "Skill hidden from the slash menu", + claudecode: { "user-invocable": false }, + }; + + const rulesyncSkill = new RulesyncSkill({ + dirName: "hidden-skill", + frontmatter: rulesyncFrontmatter, + body: "Hidden body", + }); + + const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(claudecodeSkill.getFrontmatter()["user-invocable"]).toBe(false); + }); + + it("should omit user-invocable when claudecode section does not set it", () => { + const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = { + name: "no-user-invocable-skill", + description: "Skill without user-invocable", + }; + + const rulesyncSkill = new RulesyncSkill({ + dirName: "no-user-invocable-skill", + frontmatter: rulesyncFrontmatter, + body: "Body", + }); + + const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(claudecodeSkill.getFrontmatter()["user-invocable"]).toBeUndefined(); + }); + + it("should pick up root-level user-invocable when claudecode section omits it", () => { + const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = { + name: "root-user-invocable-skill", + description: "Skill with root-level user-invocable", + "user-invocable": false, + }; + + const rulesyncSkill = new RulesyncSkill({ + dirName: "root-user-invocable-skill", + frontmatter: rulesyncFrontmatter, + body: "Body", + }); + + const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(claudecodeSkill.getFrontmatter()["user-invocable"]).toBe(false); + }); + + it("should let claudecode user-invocable override the root-level value", () => { + const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = { + name: "user-invocable-override-skill", + description: "Skill where the claudecode section overrides the root default", + "user-invocable": true, + claudecode: { "user-invocable": false }, + }; + + const rulesyncSkill = new RulesyncSkill({ + dirName: "user-invocable-override-skill", + frontmatter: rulesyncFrontmatter, + body: "Body", + }); + + const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(claudecodeSkill.getFrontmatter()["user-invocable"]).toBe(false); + }); + it("should convert from RulesyncSkill with paths as string", () => { const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = { name: "paths-string-skill", diff --git a/src/features/skills/claudecode-skill.ts b/src/features/skills/claudecode-skill.ts index 33a9a3d7f..8aaf694da 100644 --- a/src/features/skills/claudecode-skill.ts +++ b/src/features/skills/claudecode-skill.ts @@ -11,7 +11,7 @@ import { RULESYNC_SKILLS_RELATIVE_DIR_PATH } from "../../constants/rulesync-path import { ValidationResult } from "../../types/ai-dir.js"; import { formatError } from "../../utils/error.js"; import { RulesyncSkill, RulesyncSkillFrontmatterInput, SkillFile } from "./rulesync-skill.js"; -import { resolveDisableModelInvocation } from "./skills-utils.js"; +import { resolveDisableModelInvocation, resolveUserInvocable } from "./skills-utils.js"; import { ToolSkill, ToolSkillForDeletionParams, @@ -29,6 +29,7 @@ export const ClaudecodeSkillFrontmatterSchema = z.looseObject({ "disallowed-tools": z.optional(z.union([z.string(), z.array(z.string())])), model: z.optional(z.string()), "disable-model-invocation": z.optional(z.boolean()), + "user-invocable": z.optional(z.boolean()), paths: z.optional(z.union([z.string(), z.array(z.string())])), }); @@ -133,6 +134,9 @@ export class ClaudecodeSkill extends ToolSkill { ...(frontmatter["disable-model-invocation"] !== undefined && { "disable-model-invocation": frontmatter["disable-model-invocation"], }), + ...(frontmatter["user-invocable"] !== undefined && { + "user-invocable": frontmatter["user-invocable"], + }), ...(this.relativeDirPath === CLAUDECODE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true, }), @@ -169,6 +173,10 @@ export class ClaudecodeSkill extends ToolSkill { rootFrontmatter: rulesyncFrontmatter, section: rulesyncFrontmatter.claudecode, }); + const resolvedUserInvocable = resolveUserInvocable({ + rootFrontmatter: rulesyncFrontmatter, + section: rulesyncFrontmatter.claudecode, + }); const claudecodeFrontmatter: ClaudecodeSkillFrontmatter = { name: rulesyncFrontmatter.name, @@ -185,6 +193,9 @@ export class ClaudecodeSkill extends ToolSkill { ...(resolvedDisableModelInvocation !== undefined && { "disable-model-invocation": resolvedDisableModelInvocation, }), + ...(resolvedUserInvocable !== undefined && { + "user-invocable": resolvedUserInvocable, + }), ...(rulesyncFrontmatter.claudecode?.paths !== undefined && { paths: rulesyncFrontmatter.claudecode.paths, }), diff --git a/src/features/skills/factorydroid-skill.test.ts b/src/features/skills/factorydroid-skill.test.ts index c079ade5f..1c3b3e2e6 100644 --- a/src/features/skills/factorydroid-skill.test.ts +++ b/src/features/skills/factorydroid-skill.test.ts @@ -200,6 +200,45 @@ This is a test factorydroid skill content.`; const factorydroidSkill = FactorydroidSkill.fromRulesyncSkill({ rulesyncSkill }); expect(factorydroidSkill.getFrontmatter()["disable-model-invocation"]).toBe(false); }); + + it("should pick up root-level user-invocable when factorydroid section omits it", () => { + const rulesyncSkill = new RulesyncSkill({ + outputRoot: testDir, + relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH, + dirName: "root-user-invocable", + frontmatter: { + name: "Root User Invocable", + description: "Root user-invocable", + targets: ["factorydroid"], + "user-invocable": false, + }, + body: "Body", + }); + + const factorydroidSkill = FactorydroidSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(factorydroidSkill.getFrontmatter()["user-invocable"]).toBe(false); + }); + + it("should let the factorydroid section override the root-level user-invocable value", () => { + const rulesyncSkill = new RulesyncSkill({ + outputRoot: testDir, + relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH, + dirName: "user-invocable-override", + frontmatter: { + name: "User Invocable Override", + description: "Factorydroid overrides user-invocable", + targets: ["factorydroid"], + "user-invocable": true, + factorydroid: { + "user-invocable": false, + }, + }, + body: "Body", + }); + + const factorydroidSkill = FactorydroidSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(factorydroidSkill.getFrontmatter()["user-invocable"]).toBe(false); + }); }); describe("toRulesyncSkill", () => { diff --git a/src/features/skills/factorydroid-skill.ts b/src/features/skills/factorydroid-skill.ts index 8c9d87d9d..da0941ea0 100644 --- a/src/features/skills/factorydroid-skill.ts +++ b/src/features/skills/factorydroid-skill.ts @@ -8,7 +8,7 @@ import { RULESYNC_SKILLS_RELATIVE_DIR_PATH } from "../../constants/rulesync-path import { ValidationResult } from "../../types/ai-dir.js"; import { formatError } from "../../utils/error.js"; import { RulesyncSkill, RulesyncSkillFrontmatterInput, SkillFile } from "./rulesync-skill.js"; -import { resolveDisableModelInvocation } from "./skills-utils.js"; +import { resolveDisableModelInvocation, resolveUserInvocable } from "./skills-utils.js"; import { ToolSkill, ToolSkillForDeletionParams, @@ -131,6 +131,9 @@ export class FactorydroidSkill extends ToolSkill { ...(frontmatter["disable-model-invocation"] !== undefined && { "disable-model-invocation": frontmatter["disable-model-invocation"], }), + ...(frontmatter["user-invocable"] !== undefined && { + "user-invocable": frontmatter["user-invocable"], + }), }; const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = { name: frontmatter.name, @@ -163,6 +166,10 @@ export class FactorydroidSkill extends ToolSkill { rootFrontmatter: rulesyncFrontmatter, section: rulesyncFrontmatter.factorydroid, }); + const resolvedUserInvocable = resolveUserInvocable({ + rootFrontmatter: rulesyncFrontmatter, + section: rulesyncFrontmatter.factorydroid, + }); const factorydroidFrontmatter: FactorydroidSkillFrontmatter = { name: rulesyncFrontmatter.name, @@ -170,6 +177,9 @@ export class FactorydroidSkill extends ToolSkill { ...(resolvedDisableModelInvocation !== undefined && { "disable-model-invocation": resolvedDisableModelInvocation, }), + ...(resolvedUserInvocable !== undefined && { + "user-invocable": resolvedUserInvocable, + }), }; return new FactorydroidSkill({ diff --git a/src/features/skills/qwencode-skill.test.ts b/src/features/skills/qwencode-skill.test.ts index c80f31be9..f520f857f 100644 --- a/src/features/skills/qwencode-skill.test.ts +++ b/src/features/skills/qwencode-skill.test.ts @@ -272,6 +272,37 @@ describe("QwencodeSkill", () => { const qwencodeSkill = QwencodeSkill.fromRulesyncSkill({ rulesyncSkill }); expect(qwencodeSkill.getFrontmatter()["disable-model-invocation"]).toBeUndefined(); }); + + it("should pick up root-level user-invocable when qwencode section omits it", () => { + const rulesyncSkill = new RulesyncSkill({ + dirName: "root-user-invocable", + frontmatter: { + name: "root-user-invocable", + description: "Root user-invocable", + "user-invocable": false, + }, + body: "Body", + }); + + const qwencodeSkill = QwencodeSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(qwencodeSkill.getFrontmatter()["user-invocable"]).toBe(false); + }); + + it("should let qwencode user-invocable override the root-level value", () => { + const rulesyncSkill = new RulesyncSkill({ + dirName: "user-invocable-override", + frontmatter: { + name: "user-invocable-override", + description: "Qwencode overrides user-invocable", + "user-invocable": true, + qwencode: { "user-invocable": false }, + } as RulesyncSkillFrontmatterInput, + body: "Body", + }); + + const qwencodeSkill = QwencodeSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(qwencodeSkill.getFrontmatter()["user-invocable"]).toBe(false); + }); }); describe("toRulesyncSkill", () => { diff --git a/src/features/skills/qwencode-skill.ts b/src/features/skills/qwencode-skill.ts index b09f871f6..95b4a5c68 100644 --- a/src/features/skills/qwencode-skill.ts +++ b/src/features/skills/qwencode-skill.ts @@ -8,7 +8,7 @@ import { RULESYNC_SKILLS_RELATIVE_DIR_PATH } from "../../constants/rulesync-path import { ValidationResult } from "../../types/ai-dir.js"; import { formatError } from "../../utils/error.js"; import { RulesyncSkill, RulesyncSkillFrontmatterInput, SkillFile } from "./rulesync-skill.js"; -import { resolveDisableModelInvocation } from "./skills-utils.js"; +import { resolveDisableModelInvocation, resolveUserInvocable } from "./skills-utils.js"; import { ToolSkill, ToolSkillForDeletionParams, @@ -172,14 +172,18 @@ export class QwencodeSkill extends ToolSkill { rootFrontmatter: rulesyncFrontmatter, section: qwencodeSection, }); + const resolvedUserInvocable = resolveUserInvocable({ + rootFrontmatter: rulesyncFrontmatter, + section: qwencodeSection, + }); const qwencodeFrontmatter: QwencodeSkillFrontmatter = { name: rulesyncFrontmatter.name, description: rulesyncFrontmatter.description, ...(qwencodeSection?.priority !== undefined && { priority: qwencodeSection.priority }), ...(qwencodeSection?.paths !== undefined && { paths: qwencodeSection.paths }), - ...(qwencodeSection?.["user-invocable"] !== undefined && { - "user-invocable": qwencodeSection["user-invocable"], + ...(resolvedUserInvocable !== undefined && { + "user-invocable": resolvedUserInvocable, }), ...(resolvedDisableModelInvocation !== undefined && { "disable-model-invocation": resolvedDisableModelInvocation, diff --git a/src/features/skills/rulesync-skill.ts b/src/features/skills/rulesync-skill.ts index 8ce3dddb8..f0e250835 100644 --- a/src/features/skills/rulesync-skill.ts +++ b/src/features/skills/rulesync-skill.ts @@ -17,12 +17,16 @@ const RulesyncSkillFrontmatterSchemaInternal = z.looseObject({ // Default for tools that support the flag (claudecode, cursor, zed, pi, qwencode, factorydroid). // A target-section value of the same key overrides this default. "disable-model-invocation": z.optional(z.boolean()), + // Default for tools that support the flag (claudecode, qwencode, vibe, factorydroid). + // A target-section value of the same key overrides this default. + "user-invocable": z.optional(z.boolean()), claudecode: z.optional( z.looseObject({ "allowed-tools": z.optional(z.array(z.string())), "disallowed-tools": z.optional(z.union([z.string(), z.array(z.string())])), model: z.optional(z.string()), "disable-model-invocation": z.optional(z.boolean()), + "user-invocable": z.optional(z.boolean()), "scheduled-task": z.optional(z.boolean()), paths: z.optional(z.union([z.string(), z.array(z.string())])), }), @@ -145,6 +149,7 @@ const RulesyncSkillFrontmatterSchemaInternal = z.looseObject({ factorydroid: z.optional( z.looseObject({ "disable-model-invocation": z.optional(z.boolean()), + "user-invocable": z.optional(z.boolean()), }), ), agentsskills: z.optional( @@ -186,11 +191,13 @@ export type RulesyncSkillFrontmatterInput = { description: string; targets?: ("*" | string)[]; "disable-model-invocation"?: boolean; + "user-invocable"?: boolean; claudecode?: { "allowed-tools"?: string[]; "disallowed-tools"?: string | string[]; model?: string; "disable-model-invocation"?: boolean; + "user-invocable"?: boolean; "scheduled-task"?: boolean; paths?: string | string[]; }; @@ -270,6 +277,7 @@ export type RulesyncSkillFrontmatterInput = { }; factorydroid?: { "disable-model-invocation"?: boolean; + "user-invocable"?: boolean; }; agentsskills?: { license?: string; diff --git a/src/features/skills/skills-utils.test.ts b/src/features/skills/skills-utils.test.ts index 49fbbc99f..a0791520e 100644 --- a/src/features/skills/skills-utils.test.ts +++ b/src/features/skills/skills-utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { resolveDisableModelInvocation } from "./skills-utils.js"; +import { resolveDisableModelInvocation, resolveUserInvocable } from "./skills-utils.js"; describe("resolveDisableModelInvocation", () => { it("returns the section value when it is set", () => { @@ -48,3 +48,50 @@ describe("resolveDisableModelInvocation", () => { ).toBeUndefined(); }); }); + +describe("resolveUserInvocable", () => { + it("returns the section value when it is set", () => { + expect( + resolveUserInvocable({ + rootFrontmatter: { "user-invocable": true }, + section: { "user-invocable": false }, + }), + ).toBe(false); + }); + + it("lets a false section value override a true root value", () => { + expect( + resolveUserInvocable({ + rootFrontmatter: { "user-invocable": true }, + section: { "user-invocable": false }, + }), + ).toBe(false); + }); + + it("falls back to the root value when the section omits the key", () => { + expect( + resolveUserInvocable({ + rootFrontmatter: { "user-invocable": false }, + section: {}, + }), + ).toBe(false); + }); + + it("falls back to the root value when the section is undefined", () => { + expect( + resolveUserInvocable({ + rootFrontmatter: { "user-invocable": false }, + section: undefined, + }), + ).toBe(false); + }); + + it("returns undefined when neither value is set", () => { + expect( + resolveUserInvocable({ + rootFrontmatter: {}, + section: undefined, + }), + ).toBeUndefined(); + }); +}); diff --git a/src/features/skills/skills-utils.ts b/src/features/skills/skills-utils.ts index cc90c97ee..917684b63 100644 --- a/src/features/skills/skills-utils.ts +++ b/src/features/skills/skills-utils.ts @@ -48,3 +48,24 @@ export function resolveDisableModelInvocation({ }): boolean | undefined { return section?.["disable-model-invocation"] ?? rootFrontmatter["disable-model-invocation"]; } + +/** + * Resolve the effective `user-invocable` value for a tool skill. + * + * The rulesync skill frontmatter exposes a root-level `user-invocable` default + * that applies to every tool supporting the flag (claudecode, qwencode, vibe, + * factorydroid). Each tool's own section may override that default with a + * per-target value. A defined section value (including `false`) always wins + * over the root default. + * + * @returns The resolved boolean, or `undefined` when neither value is set. + */ +export function resolveUserInvocable({ + rootFrontmatter, + section, +}: { + rootFrontmatter: { "user-invocable"?: boolean }; + section: { "user-invocable"?: boolean } | undefined; +}): boolean | undefined { + return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"]; +} diff --git a/src/features/skills/vibe-skill.test.ts b/src/features/skills/vibe-skill.test.ts index 89fc07fc5..35195ddc7 100644 --- a/src/features/skills/vibe-skill.test.ts +++ b/src/features/skills/vibe-skill.test.ts @@ -114,6 +114,53 @@ describe("VibeSkill", () => { }); }); + it("should pick up root-level user-invocable when vibe section omits it", () => { + const rulesyncSkill = new RulesyncSkill({ + outputRoot: testDir, + relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH, + dirName: "root-user-invocable", + frontmatter: { + name: "root-user-invocable", + description: "Root user-invocable", + targets: ["vibe"], + "user-invocable": false, + }, + body: "Body", + }); + + const vibeSkill = VibeSkill.fromRulesyncSkill({ + outputRoot: testDir, + rulesyncSkill, + }); + + expect(vibeSkill.getFrontmatter()["user-invocable"]).toBe(false); + }); + + it("should let the vibe section override the root-level user-invocable value", () => { + const rulesyncSkill = new RulesyncSkill({ + outputRoot: testDir, + relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH, + dirName: "user-invocable-override", + frontmatter: { + name: "user-invocable-override", + description: "Vibe overrides user-invocable", + targets: ["vibe"], + "user-invocable": true, + vibe: { + "user-invocable": false, + }, + }, + body: "Body", + }); + + const vibeSkill = VibeSkill.fromRulesyncSkill({ + outputRoot: testDir, + rulesyncSkill, + }); + + expect(vibeSkill.getFrontmatter()["user-invocable"]).toBe(false); + }); + it("should load from .agents/skills import fallback when requested by the processor", async () => { const skillDir = join(testDir, ".agents", "skills", "fallback"); await ensureDir(skillDir); diff --git a/src/features/skills/vibe-skill.ts b/src/features/skills/vibe-skill.ts index 9c11ffebe..07ff0df19 100644 --- a/src/features/skills/vibe-skill.ts +++ b/src/features/skills/vibe-skill.ts @@ -7,6 +7,7 @@ import { RULESYNC_SKILLS_RELATIVE_DIR_PATH } from "../../constants/rulesync-path import { ValidationResult } from "../../types/ai-dir.js"; import { formatError } from "../../utils/error.js"; import { RulesyncSkill, RulesyncSkillFrontmatterInput, SkillFile } from "./rulesync-skill.js"; +import { resolveUserInvocable } from "./skills-utils.js"; import { ToolSkill, ToolSkillForDeletionParams, @@ -161,6 +162,11 @@ export class VibeSkill extends ToolSkill { ? (looseTopLevel.metadata as Record) : undefined; + const resolvedUserInvocable = resolveUserInvocable({ + rootFrontmatter: rulesyncFrontmatter, + section: vibeSection, + }); + const vibeFrontmatter: VibeSkillFrontmatter = { name: rulesyncFrontmatter.name, description: rulesyncFrontmatter.description, @@ -173,8 +179,8 @@ export class VibeSkill extends ToolSkill { ...(vibeSection?.metadata !== undefined || topLevelMetadata !== undefined ? { metadata: vibeSection?.metadata ?? topLevelMetadata } : {}), - ...(vibeSection?.["user-invocable"] !== undefined && { - "user-invocable": vibeSection["user-invocable"], + ...(resolvedUserInvocable !== undefined && { + "user-invocable": resolvedUserInvocable, }), ...(vibeSection?.["allowed-tools"] !== undefined && { "allowed-tools": vibeSection["allowed-tools"],