Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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/<name>/SKILL.md instead of .claude/skills/<name>/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:
Expand Down Expand Up @@ -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:<parent>}` facet-inheritance directive (Takt 0.39.0+)
Expand Down
6 changes: 6 additions & 0 deletions skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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/<name>/SKILL.md instead of .claude/skills/<name>/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:
Expand Down Expand Up @@ -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:<parent>}` facet-inheritance directive (Takt 0.39.0+)
Expand Down
89 changes: 89 additions & 0 deletions src/features/skills/claudecode-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 12 additions & 1 deletion src/features/skills/claudecode-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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())])),
});

Expand Down Expand Up @@ -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,
}),
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}),
Expand Down
39 changes: 39 additions & 0 deletions src/features/skills/factorydroid-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
12 changes: 11 additions & 1 deletion src/features/skills/factorydroid-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -163,13 +166,20 @@ export class FactorydroidSkill extends ToolSkill {
rootFrontmatter: rulesyncFrontmatter,
section: rulesyncFrontmatter.factorydroid,
});
const resolvedUserInvocable = resolveUserInvocable({
rootFrontmatter: rulesyncFrontmatter,
section: rulesyncFrontmatter.factorydroid,
});

const factorydroidFrontmatter: FactorydroidSkillFrontmatter = {
name: rulesyncFrontmatter.name,
description: rulesyncFrontmatter.description,
...(resolvedDisableModelInvocation !== undefined && {
"disable-model-invocation": resolvedDisableModelInvocation,
}),
...(resolvedUserInvocable !== undefined && {
"user-invocable": resolvedUserInvocable,
}),
};

return new FactorydroidSkill({
Expand Down
31 changes: 31 additions & 0 deletions src/features/skills/qwencode-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
10 changes: 7 additions & 3 deletions src/features/skills/qwencode-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading