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
2 changes: 2 additions & 0 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ cursor: # for Cursor-specific parameters (optional)
disable-model-invocation: true # (optional) only include the skill when invoked via /skill-name
metadata: # (optional) free-form metadata
author: rulesync
factorydroid: # for Factory Droid-specific parameters (optional)
disable-model-invocation: true # (optional) prevent the model from auto-invoking this skill
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
2 changes: 2 additions & 0 deletions skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ cursor: # for Cursor-specific parameters (optional)
disable-model-invocation: true # (optional) only include the skill when invoked via /skill-name
metadata: # (optional) free-form metadata
author: rulesync
factorydroid: # for Factory Droid-specific parameters (optional)
disable-model-invocation: true # (optional) prevent the model from auto-invoking this skill
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
8 changes: 5 additions & 3 deletions src/features/skills/claudecode-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +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 {
ToolSkill,
ToolSkillForDeletionParams,
Expand Down Expand Up @@ -164,9 +165,10 @@ export class ClaudecodeSkill extends ToolSkill {
}: ToolSkillFromRulesyncSkillParams): ClaudecodeSkill {
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();

const resolvedDisableModelInvocation =
rulesyncFrontmatter.claudecode?.["disable-model-invocation"] ??
rulesyncFrontmatter["disable-model-invocation"];
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
rootFrontmatter: rulesyncFrontmatter,
section: rulesyncFrontmatter.claudecode,
});

const claudecodeFrontmatter: ClaudecodeSkillFrontmatter = {
name: rulesyncFrontmatter.name,
Expand Down
8 changes: 5 additions & 3 deletions src/features/skills/cursor-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +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 {
ToolSkill,
ToolSkillForDeletionParams,
Expand Down Expand Up @@ -151,9 +152,10 @@ 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 resolvedDisableModelInvocation = resolveDisableModelInvocation({
rootFrontmatter: rulesyncFrontmatter,
section: cursorSection,
});

const cursorFrontmatter: CursorSkillFrontmatter = {
name: rulesyncFrontmatter.name,
Expand Down
68 changes: 68 additions & 0 deletions src/features/skills/factorydroid-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,48 @@ This is a test factorydroid skill content.`;
const factorydroidSkill = FactorydroidSkill.fromRulesyncSkill({ rulesyncSkill });
expect(factorydroidSkill.getFrontmatter()["disable-model-invocation"]).toBeUndefined();
});

it("should let the factorydroid section override the root disable-model-invocation", () => {
const rulesyncSkill = new RulesyncSkill({
outputRoot: testDir,
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
dirName: "section-override",
frontmatter: {
name: "Section Override",
description: "Section flag",
targets: ["factorydroid"],
"disable-model-invocation": false,
factorydroid: {
"disable-model-invocation": true,
},
},
body: "Body",
});

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

it("should let a false factorydroid section override a true root value", () => {
const rulesyncSkill = new RulesyncSkill({
outputRoot: testDir,
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
dirName: "section-false-override",
frontmatter: {
name: "Section False Override",
description: "Section flag",
targets: ["factorydroid"],
"disable-model-invocation": true,
factorydroid: {
"disable-model-invocation": false,
},
},
body: "Body",
});

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

describe("toRulesyncSkill", () => {
Expand All @@ -184,6 +226,32 @@ This is a test factorydroid skill content.`;
});
expect(rulesyncSkill.getBody()).toBe("Test body");
});

it("should round-trip disable-model-invocation into the factorydroid section", () => {
const skill = new FactorydroidSkill({
outputRoot: testDir,
relativeDirPath: join(".factory", "skills"),
dirName: "dmi-skill",
frontmatter: {
name: "DMI Skill",
description: "DMI description",
"disable-model-invocation": true,
},
body: "Test body",
validate: true,
});

const rulesyncSkill = skill.toRulesyncSkill();

expect(rulesyncSkill.getFrontmatter()).toEqual({
name: "DMI Skill",
description: "DMI description",
targets: ["*"],
factorydroid: {
"disable-model-invocation": true,
},
});
});
});

describe("fromDir", () => {
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,6 +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 {
ToolSkill,
ToolSkillForDeletionParams,
Expand Down Expand Up @@ -126,10 +127,16 @@ export class FactorydroidSkill extends ToolSkill {

toRulesyncSkill(): RulesyncSkill {
const frontmatter = this.getFrontmatter();
const factorydroidBlock = {
...(frontmatter["disable-model-invocation"] !== undefined && {
"disable-model-invocation": frontmatter["disable-model-invocation"],
}),
};
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
name: frontmatter.name,
description: frontmatter.description,
targets: ["*"],
...(Object.keys(factorydroidBlock).length > 0 && { factorydroid: factorydroidBlock }),
};

return new RulesyncSkill({
Expand All @@ -152,7 +159,10 @@ export class FactorydroidSkill extends ToolSkill {
}: ToolSkillFromRulesyncSkillParams): FactorydroidSkill {
const settablePaths = FactorydroidSkill.getSettablePaths({ global });
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
const resolvedDisableModelInvocation = rulesyncFrontmatter["disable-model-invocation"];
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
rootFrontmatter: rulesyncFrontmatter,
section: rulesyncFrontmatter.factorydroid,
});

const factorydroidFrontmatter: FactorydroidSkillFrontmatter = {
name: rulesyncFrontmatter.name,
Expand Down
10 changes: 8 additions & 2 deletions src/features/skills/pi-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +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 {
ToolSkill,
ToolSkillForDeletionParams,
Expand Down Expand Up @@ -169,12 +170,17 @@ export class PiSkill extends ToolSkill {
const settablePaths = PiSkill.getSettablePaths({ global });
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
const piSection = rulesyncFrontmatter.pi;
const resolvedDisableModelInvocation =
piSection?.["disable-model-invocation"] ?? rulesyncFrontmatter["disable-model-invocation"];
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
rootFrontmatter: rulesyncFrontmatter,
section: piSection,
});

const piFrontmatter: PiSkillFrontmatter = {
name: rulesyncFrontmatter.name,
description: rulesyncFrontmatter.description,
// Spread the section first to carry over any tool-specific keys, then
// re-apply the resolved `disable-model-invocation` so the root default is
// honored when the section omits the key.
...piSection,
...(resolvedDisableModelInvocation !== undefined && {
"disable-model-invocation": resolvedDisableModelInvocation,
Expand Down
8 changes: 5 additions & 3 deletions src/features/skills/qwencode-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +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 {
ToolSkill,
ToolSkillForDeletionParams,
Expand Down Expand Up @@ -167,9 +168,10 @@ 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 resolvedDisableModelInvocation = resolveDisableModelInvocation({
rootFrontmatter: rulesyncFrontmatter,
section: qwencodeSection,
});

const qwencodeFrontmatter: QwencodeSkillFrontmatter = {
name: rulesyncFrontmatter.name,
Expand Down
8 changes: 8 additions & 0 deletions src/features/skills/rulesync-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ const RulesyncSkillFrontmatterSchemaInternal = z.looseObject({
metadata: z.optional(z.looseObject({})),
}),
),
factorydroid: z.optional(
z.looseObject({
"disable-model-invocation": z.optional(z.boolean()),
}),
),
agentsskills: z.optional(
z.looseObject({
license: z.optional(z.string()),
Expand Down Expand Up @@ -255,6 +260,9 @@ export type RulesyncSkillFrontmatterInput = {
"disable-model-invocation"?: boolean;
metadata?: Record<string, unknown>;
};
factorydroid?: {
"disable-model-invocation"?: boolean;
};
agentsskills?: {
license?: string;
compatibility?: string | Record<string, unknown>;
Expand Down
50 changes: 50 additions & 0 deletions src/features/skills/skills-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";

import { resolveDisableModelInvocation } from "./skills-utils.js";

describe("resolveDisableModelInvocation", () => {
it("returns the section value when it is set", () => {
expect(
resolveDisableModelInvocation({
rootFrontmatter: { "disable-model-invocation": false },
section: { "disable-model-invocation": true },
}),
).toBe(true);
});

it("lets a false section value override a true root value", () => {
expect(
resolveDisableModelInvocation({
rootFrontmatter: { "disable-model-invocation": true },
section: { "disable-model-invocation": false },
}),
).toBe(false);
});

it("falls back to the root value when the section omits the key", () => {
expect(
resolveDisableModelInvocation({
rootFrontmatter: { "disable-model-invocation": true },
section: {},
}),
).toBe(true);
});

it("falls back to the root value when the section is undefined", () => {
expect(
resolveDisableModelInvocation({
rootFrontmatter: { "disable-model-invocation": true },
section: undefined,
}),
).toBe(true);
});

it("returns undefined when neither value is set", () => {
expect(
resolveDisableModelInvocation({
rootFrontmatter: {},
section: undefined,
}),
).toBeUndefined();
});
});
21 changes: 21 additions & 0 deletions src/features/skills/skills-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,24 @@ export async function getLocalSkillDirNames(outputRoot: string): Promise<Set<str

return names;
}

/**
* Resolve the effective `disable-model-invocation` value for a tool skill.
*
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
* default that applies to every tool supporting the flag (claudecode, cursor,
* zed, pi, qwencode, 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 resolveDisableModelInvocation({
rootFrontmatter,
section,
}: {
rootFrontmatter: { "disable-model-invocation"?: boolean };
section: { "disable-model-invocation"?: boolean } | undefined;
}): boolean | undefined {
return section?.["disable-model-invocation"] ?? rootFrontmatter["disable-model-invocation"];
}
10 changes: 8 additions & 2 deletions src/features/skills/zed-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ZED_SKILLS_DIR_PATH } from "../../constants/zed-paths.js";
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 {
ToolSkill,
ToolSkillForDeletionParams,
Expand Down Expand Up @@ -146,12 +147,17 @@ export class ZedSkill extends ToolSkill {
const settablePaths = ZedSkill.getSettablePaths({ global });
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
const zedSection = rulesyncFrontmatter.zed;
const resolvedDisableModelInvocation =
zedSection?.["disable-model-invocation"] ?? rulesyncFrontmatter["disable-model-invocation"];
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
rootFrontmatter: rulesyncFrontmatter,
section: zedSection,
});

const zedFrontmatter: ZedSkillFrontmatter = {
name: rulesyncFrontmatter.name,
description: rulesyncFrontmatter.description,
// Spread the section first to carry over any tool-specific keys, then
// re-apply the resolved `disable-model-invocation` so the root default is
// honored when the section omits the key.
...zedSection,
...(resolvedDisableModelInvocation !== undefined && {
"disable-model-invocation": resolvedDisableModelInvocation,
Expand Down