feat(core): support disable-model-invocation in skill frontmatter (#52747)

This commit is contained in:
Filip
2026-10-02 15:07:30 +02:00
committed by GitHub
Unverified
parent dbe08d2ac3
commit f3a23e52b3
3 changed files with 25 additions and 5 deletions
+11 -4
View File
@@ -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<string, unknown>)[key]
return booleanValue((metadata as Record<string, unknown>)[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: {
+12
View File
@@ -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(
+2 -1
View File
@@ -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