diff --git a/packages/core/src/config/plugin/skill-file.ts b/packages/core/src/config/plugin/skill-file.ts index 13b4f97da5c..95782512b9f 100644 --- a/packages/core/src/config/plugin/skill-file.ts +++ b/packages/core/src/config/plugin/skill-file.ts @@ -10,6 +10,7 @@ const Frontmatter = Schema.Struct({ name: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional), metadata: Schema.Unknown.pipe(Schema.optional), + "disable-model-invocation": Schema.Unknown.pipe(Schema.optional), }) const decodeFrontmatter = SchemaParser.decodeUnknownResult(Frontmatter) @@ -22,12 +23,16 @@ const metadataBoolean = (metadata: unknown, key: string) => { if (metadata === undefined || metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) { return undefined } - const value = (metadata as Record)[key] + return booleanValue((metadata as Record)[key]) +} + +const booleanValue = (value: unknown) => { if (typeof value === "boolean") return value + if (typeof value === "number") return value === 1 ? true : value === 0 ? false : undefined if (typeof value !== "string") return undefined const normalized = value.trim().toLowerCase() - if (normalized === "true") return true - if (normalized === "false") return false + if (["true", "yes", "on", "1"].includes(normalized)) return true + if (["false", "no", "off", "0"].includes(normalized)) return false return undefined } @@ -41,7 +46,9 @@ export function parse(directory: string, filepath: string, content: string): Par path.dirname(filepath) === directory && path.basename(filepath) !== "SKILL.md" ? path.basename(filepath, ".md") : path.basename(path.dirname(filepath)) - const autoinvoke = metadataBoolean(frontmatter.metadata, "opencode/autoinvoke") + const opencodeAutoinvoke = metadataBoolean(frontmatter.metadata, "opencode/autoinvoke") + const disableModelInvocation = booleanValue(frontmatter["disable-model-invocation"]) + const autoinvoke = opencodeAutoinvoke ?? (disableModelInvocation ? false : undefined) return { _tag: "Parsed", skill: { diff --git a/packages/core/test/config/skill.test.ts b/packages/core/test/config/skill.test.ts index d226842e946..b172222e697 100644 --- a/packages/core/test/config/skill.test.ts +++ b/packages/core/test/config/skill.test.ts @@ -123,6 +123,18 @@ function emitAndWait(update: Watcher.Update) { } describe("SkillFile.parse", () => { + test("reads disable-model-invocation as autoinvoke", () => { + const parse = (frontmatter: string) => + SkillFile.parse("/repo/skills", "/repo/skills/deploy/SKILL.md", `---\n${frontmatter}\n---\n# deploy`) + expect(parse("disable-model-invocation: true")).toMatchObject({ skill: { autoinvoke: false } }) + expect(parse("disable-model-invocation: yes")).toMatchObject({ skill: { autoinvoke: false } }) + expect(parse("disable-model-invocation: false")).not.toMatchObject({ skill: { autoinvoke: expect.anything() } }) + expect(parse("disable-model-invocation: maybe")).not.toMatchObject({ skill: { autoinvoke: expect.anything() } }) + expect( + parse("disable-model-invocation: true\nmetadata:\n opencode/autoinvoke: true"), + ).toMatchObject({ skill: { autoinvoke: true } }) + }) + test("parses root and nested skill ids and metadata flags", () => { const directory = "/repo/skills" expect( diff --git a/services/www/src/docs/content/skills.mdx b/services/www/src/docs/content/skills.mdx index 8afac5b01de..56fec46c624 100644 --- a/services/www/src/docs/content/skills.mdx +++ b/services/www/src/docs/content/skills.mdx @@ -135,10 +135,11 @@ Prepare the changelog, version bump, tag, and release notes. | `slash` | Set to `false` to hide the skill from interactive command catalogs | | `metadata.opencode/slash` | Boolean or `"true"`/`"false"`; overrides `slash` | | `metadata.opencode/autoinvoke` | Set to `false` to omit the skill from the model's available list | +| `disable-model-invocation` | Set to `true` for the same effect as `opencode/autoinvoke: false` | All frontmatter is optional at runtime. Add a clear `description` when the model should discover the skill; skills without one are not advertised. -`opencode/autoinvoke: false` only hides the skill from the model's available list. The skill remains registered and can still be loaded explicitly by ID. V2 accepts portability fields such as `license` and `compatibility` but does not interpret them. +`opencode/autoinvoke: false` only hides the skill from the model's available list. The skill remains registered and can still be loaded explicitly by ID. When both `disable-model-invocation` and `opencode/autoinvoke` are set, `opencode/autoinvoke` wins. V2 accepts portability fields such as `license` and `compatibility` but does not interpret them. ## IDs