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
4 changes: 4 additions & 0 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ name: example-skill # skill name
description: >- # skill description
A sample skill that demonstrates the skill format
targets: ["*"] # * = all, or specific tools
# (optional) shared default for tools that support the flag — claudecode, cursor,
# 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
claudecode: # for claudecode-specific parameters
model: sonnet # opus, sonnet, haiku, or any string
allowed-tools:
Expand Down
4 changes: 4 additions & 0 deletions skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ name: example-skill # skill name
description: >- # skill description
A sample skill that demonstrates the skill format
targets: ["*"] # * = all, or specific tools
# (optional) shared default for tools that support the flag — claudecode, cursor,
# 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
claudecode: # for claudecode-specific parameters
model: sonnet # opus, sonnet, haiku, or any string
allowed-tools:
Expand Down
51 changes: 51 additions & 0 deletions src/features/skills/claudecode-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,57 @@ describe("ClaudecodeSkill", () => {
expect(claudecodeSkill.getFrontmatter()["disable-model-invocation"]).toBe(false);
});

it("should pick up root-level disable-model-invocation when claudecode section omits it", () => {
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
name: "root-default-skill",
description: "Skill with root-level disable-model-invocation",
"disable-model-invocation": true,
};

const rulesyncSkill = new RulesyncSkill({
dirName: "root-default-skill",
frontmatter: rulesyncFrontmatter,
body: "Body",
});

const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
expect(claudecodeSkill.getFrontmatter()["disable-model-invocation"]).toBe(true);
});

it("should let claudecode disable-model-invocation override the root-level value", () => {
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
name: "override-skill",
description: "Skill where the claudecode section overrides the root default",
"disable-model-invocation": true,
claudecode: { "disable-model-invocation": false },
};

const rulesyncSkill = new RulesyncSkill({
dirName: "override-skill",
frontmatter: rulesyncFrontmatter,
body: "Body",
});

const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
expect(claudecodeSkill.getFrontmatter()["disable-model-invocation"]).toBe(false);
});

it("should omit disable-model-invocation when neither root nor claudecode set it", () => {
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
name: "no-flag-skill",
description: "Skill without the flag",
};

const rulesyncSkill = new RulesyncSkill({
dirName: "no-flag-skill",
frontmatter: rulesyncFrontmatter,
body: "Body",
});

const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
expect(claudecodeSkill.getFrontmatter()["disable-model-invocation"]).toBeUndefined();
});

it("should convert from RulesyncSkill with paths as string", () => {
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
name: "paths-string-skill",
Expand Down
8 changes: 6 additions & 2 deletions src/features/skills/claudecode-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ export class ClaudecodeSkill extends ToolSkill {
}: ToolSkillFromRulesyncSkillParams): ClaudecodeSkill {
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

const resolvedDisableModelInvocation =
rulesyncFrontmatter.claudecode?.["disable-model-invocation"] ??
rulesyncFrontmatter["disable-model-invocation"];

const claudecodeFrontmatter: ClaudecodeSkillFrontmatter = {
name: rulesyncFrontmatter.name,
description: rulesyncFrontmatter.description,
Expand All @@ -176,8 +180,8 @@ export class ClaudecodeSkill extends ToolSkill {
...(rulesyncFrontmatter.claudecode?.model && {
model: rulesyncFrontmatter.claudecode.model,
}),
...(rulesyncFrontmatter.claudecode?.["disable-model-invocation"] !== undefined && {
"disable-model-invocation": rulesyncFrontmatter.claudecode["disable-model-invocation"],
...(resolvedDisableModelInvocation !== undefined && {
"disable-model-invocation": resolvedDisableModelInvocation,
}),
...(rulesyncFrontmatter.claudecode?.paths !== undefined && {
paths: rulesyncFrontmatter.claudecode.paths,
Expand Down
42 changes: 42 additions & 0 deletions src/features/skills/cursor-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,48 @@ This is the body of the cursor skill.`;
expect(fm["disable-model-invocation"]).toBe(true);
expect(fm.metadata).toEqual({ author: "rulesync" });
});

it("should pick up root-level disable-model-invocation when cursor section omits it", () => {
const rulesyncSkill = new RulesyncSkill({
dirName: "root-default",
frontmatter: {
name: "Root Default",
description: "Root-level flag",
"disable-model-invocation": true,
},
body: "Body",
});

const cursorSkill = CursorSkill.fromRulesyncSkill({ rulesyncSkill });
expect(cursorSkill.getFrontmatter()["disable-model-invocation"]).toBe(true);
});

it("should let cursor disable-model-invocation override the root-level value", () => {
const rulesyncSkill = new RulesyncSkill({
dirName: "override",
frontmatter: {
name: "Override",
description: "Cursor opts out of root default",
"disable-model-invocation": true,
cursor: { "disable-model-invocation": false },
},
body: "Body",
});

const cursorSkill = CursorSkill.fromRulesyncSkill({ rulesyncSkill });
expect(cursorSkill.getFrontmatter()["disable-model-invocation"]).toBe(false);
});

it("should omit disable-model-invocation when neither root nor cursor set it", () => {
const rulesyncSkill = new RulesyncSkill({
dirName: "no-flag",
frontmatter: { name: "No Flag", description: "No flag" },
body: "Body",
});

const cursorSkill = CursorSkill.fromRulesyncSkill({ rulesyncSkill });
expect(cursorSkill.getFrontmatter()["disable-model-invocation"]).toBeUndefined();
});
});

describe("forDeletion", () => {
Expand Down
7 changes: 5 additions & 2 deletions src/features/skills/cursor-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,16 @@ export class CursorSkill extends ToolSkill {
const settablePaths = CursorSkill.getSettablePaths({ global });
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
const cursorSection = rulesyncFrontmatter.cursor;
const resolvedDisableModelInvocation =
cursorSection?.["disable-model-invocation"] ??
rulesyncFrontmatter["disable-model-invocation"];

const cursorFrontmatter: CursorSkillFrontmatter = {
name: rulesyncFrontmatter.name,
description: rulesyncFrontmatter.description,
...(cursorSection?.paths !== undefined && { paths: cursorSection.paths }),
...(cursorSection?.["disable-model-invocation"] !== undefined && {
"disable-model-invocation": cursorSection["disable-model-invocation"],
...(resolvedDisableModelInvocation !== undefined && {
"disable-model-invocation": resolvedDisableModelInvocation,
}),
...(cursorSection?.metadata !== undefined && { metadata: cursorSection.metadata }),
};
Expand Down
35 changes: 35 additions & 0 deletions src/features/skills/factorydroid-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,41 @@ This is a test factorydroid skill content.`;
expect(factorydroidSkill.getGlobal()).toBe(true);
expect(factorydroidSkill.getRelativeDirPath()).toBe(join(".factory", "skills"));
});

it("should pick up root-level disable-model-invocation", () => {
const rulesyncSkill = new RulesyncSkill({
outputRoot: testDir,
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
dirName: "root-default",
frontmatter: {
name: "Root Default",
description: "Root flag",
targets: ["factorydroid"],
"disable-model-invocation": true,
},
body: "Body",
});

const factorydroidSkill = FactorydroidSkill.fromRulesyncSkill({ rulesyncSkill });
expect(factorydroidSkill.getFrontmatter()["disable-model-invocation"]).toBe(true);
});

it("should omit disable-model-invocation when the root value is not set", () => {
const rulesyncSkill = new RulesyncSkill({
outputRoot: testDir,
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
dirName: "no-flag",
frontmatter: {
name: "No Flag",
description: "No flag",
targets: ["factorydroid"],
},
body: "Body",
});

const factorydroidSkill = FactorydroidSkill.fromRulesyncSkill({ rulesyncSkill });
expect(factorydroidSkill.getFrontmatter()["disable-model-invocation"]).toBeUndefined();
});
});

describe("toRulesyncSkill", () => {
Expand Down
4 changes: 4 additions & 0 deletions src/features/skills/factorydroid-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,14 @@ export class FactorydroidSkill extends ToolSkill {
}: ToolSkillFromRulesyncSkillParams): FactorydroidSkill {
const settablePaths = FactorydroidSkill.getSettablePaths({ global });
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
const resolvedDisableModelInvocation = rulesyncFrontmatter["disable-model-invocation"];

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

return new FactorydroidSkill({
Expand Down
51 changes: 51 additions & 0 deletions src/features/skills/pi-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,57 @@ Body`,
metadata: { author: "rulesync" },
});
});

it("should pick up root-level disable-model-invocation when pi section omits it", () => {
const rulesyncSkill = new RulesyncSkill({
outputRoot: testDir,
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
dirName: "root-default",
frontmatter: {
name: "root-default",
description: "Root flag",
"disable-model-invocation": true,
},
body: "Body",
validate: true,
});

const skill = PiSkill.fromRulesyncSkill({ outputRoot: testDir, rulesyncSkill });
expect(skill.getFrontmatter()["disable-model-invocation"]).toBe(true);
});

it("should let pi disable-model-invocation override the root-level value", () => {
const rulesyncSkill = new RulesyncSkill({
outputRoot: testDir,
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
dirName: "override",
frontmatter: {
name: "override",
description: "Pi opts out of root default",
"disable-model-invocation": true,
pi: { "disable-model-invocation": false },
},
body: "Body",
validate: true,
});

const skill = PiSkill.fromRulesyncSkill({ outputRoot: testDir, rulesyncSkill });
expect(skill.getFrontmatter()["disable-model-invocation"]).toBe(false);
});

it("should omit disable-model-invocation when neither root nor pi set it", () => {
const rulesyncSkill = new RulesyncSkill({
outputRoot: testDir,
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
dirName: "no-flag",
frontmatter: { name: "no-flag", description: "No flag" },
body: "Body",
validate: true,
});

const skill = PiSkill.fromRulesyncSkill({ outputRoot: testDir, rulesyncSkill });
expect(skill.getFrontmatter()["disable-model-invocation"]).toBeUndefined();
});
});

describe("toRulesyncSkill", () => {
Expand Down
8 changes: 7 additions & 1 deletion src/features/skills/pi-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,17 @@ export class PiSkill extends ToolSkill {
}: ToolSkillFromRulesyncSkillParams): PiSkill {
const settablePaths = PiSkill.getSettablePaths({ global });
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
const piSection = rulesyncFrontmatter.pi;
const resolvedDisableModelInvocation =
piSection?.["disable-model-invocation"] ?? rulesyncFrontmatter["disable-model-invocation"];

const piFrontmatter: PiSkillFrontmatter = {
name: rulesyncFrontmatter.name,
description: rulesyncFrontmatter.description,
...rulesyncFrontmatter.pi,
...piSection,
...(resolvedDisableModelInvocation !== undefined && {
"disable-model-invocation": resolvedDisableModelInvocation,
}),
};

return new PiSkill({
Expand Down
42 changes: 42 additions & 0 deletions src/features/skills/qwencode-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,48 @@ describe("QwencodeSkill", () => {

expect(qwencodeSkill.getGlobal()).toBe(true);
});

it("should pick up root-level disable-model-invocation when qwencode section omits it", () => {
const rulesyncSkill = new RulesyncSkill({
dirName: "root-default",
frontmatter: {
name: "root-default",
description: "Root flag",
"disable-model-invocation": true,
},
body: "Body",
});

const qwencodeSkill = QwencodeSkill.fromRulesyncSkill({ rulesyncSkill });
expect(qwencodeSkill.getFrontmatter()["disable-model-invocation"]).toBe(true);
});

it("should let qwencode disable-model-invocation override the root-level value", () => {
const rulesyncSkill = new RulesyncSkill({
dirName: "override",
frontmatter: {
name: "override",
description: "Qwencode opts out",
"disable-model-invocation": true,
qwencode: { "disable-model-invocation": false },
} as RulesyncSkillFrontmatterInput,
body: "Body",
});

const qwencodeSkill = QwencodeSkill.fromRulesyncSkill({ rulesyncSkill });
expect(qwencodeSkill.getFrontmatter()["disable-model-invocation"]).toBe(false);
});

it("should omit disable-model-invocation when neither root nor qwencode set it", () => {
const rulesyncSkill = new RulesyncSkill({
dirName: "no-flag",
frontmatter: { name: "no-flag", description: "No flag" },
body: "Body",
});

const qwencodeSkill = QwencodeSkill.fromRulesyncSkill({ rulesyncSkill });
expect(qwencodeSkill.getFrontmatter()["disable-model-invocation"]).toBeUndefined();
});
});

describe("toRulesyncSkill", () => {
Expand Down
7 changes: 5 additions & 2 deletions src/features/skills/qwencode-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ export class QwencodeSkill extends ToolSkill {
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
const qwencodeSection = (rulesyncFrontmatter as { qwencode?: QwencodeRulesyncSection })
.qwencode;
const resolvedDisableModelInvocation =
qwencodeSection?.["disable-model-invocation"] ??
rulesyncFrontmatter["disable-model-invocation"];

const qwencodeFrontmatter: QwencodeSkillFrontmatter = {
name: rulesyncFrontmatter.name,
Expand All @@ -176,8 +179,8 @@ export class QwencodeSkill extends ToolSkill {
...(qwencodeSection?.["user-invocable"] !== undefined && {
"user-invocable": qwencodeSection["user-invocable"],
}),
...(qwencodeSection?.["disable-model-invocation"] !== undefined && {
"disable-model-invocation": qwencodeSection["disable-model-invocation"],
...(resolvedDisableModelInvocation !== undefined && {
"disable-model-invocation": resolvedDisableModelInvocation,
}),
};

Expand Down
Loading