refactor(skill): canonicalize invocation policy
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/skill/skill-local/README.md
|
||||
README.md: cbef5e489e4340e10f3337ffd1bdb64490af4bc1
|
||||
README.zh.md: ae1e5658c5820ecf5a9a89f76f7aba4d6ad3e742
|
||||
README.md: addabda98490b49736c1aa5053f5734978079a20
|
||||
README.zh.md: 1ab1a43d70d8393ba8c837f384e3a422cfdc1d3c
|
||||
|
||||
@@ -38,7 +38,7 @@ When `ctx.fs` is available, discovery lists roots through `ctx.fs.listDir`, read
|
||||
|
||||
Skills can be single-level directory bundles (`<name>/SKILL.md`) or flat Markdown files (`<name>.md`). Nested `**/SKILL.md` discovery is intentionally not part of v1. Frontmatter is parsed as an open YAML object with the `yaml` package; this provider currently interprets required `name` and `description`, plus optional `whenToUse`, `metadata`, `disable-model-invocation`, and `user-invocable`. Names must be kebab-case.
|
||||
|
||||
The two invocation fields accept YAML booleans and the case-insensitive forms `true`/`false`, `yes`/`no`, `on`/`off`, and `1`/`0`. `disable-model-invocation: true` excludes the skill from model-facing catalogs and loaders; `user-invocable: false` excludes it from human-facing commands. If either field is present, the provider fills both positive internal policy values from these external defaults. The camel-case spellings `disableModelInvocation`, `modelInvocable`, and `userInvocable` are rejected with a warning instead of acting as compatibility aliases.
|
||||
The two invocation fields accept YAML booleans and the case-insensitive forms `true`/`false`, `yes`/`no`, `on`/`off`, and `1`/`0`. `disable-model-invocation: true` excludes the skill from model-facing catalogs and loaders; `user-invocable: false` excludes it from human-facing commands. Each omitted field defaults to permitting its surface, and the provider always emits both positive internal policy values, including when both keys are absent. The camel-case spellings `disableModelInvocation`, `modelInvocable`, and `userInvocable` are rejected with a warning instead of acting as compatibility aliases.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
Skill 可以是单层目录 bundle(`<name>/SKILL.md`),也可以是平铺 Markdown 文件(`<name>.md`)。v1 刻意不支持发现嵌套的 `**/SKILL.md`。Frontmatter 使用 `yaml` 包解析为开放的 YAML 对象;该提供方目前解析必填的 `name` 和 `description`,以及可选的 `whenToUse`、`metadata`、`disable-model-invocation` 和 `user-invocable`。名称必须使用 kebab-case。
|
||||
|
||||
这两个调用字段接受 YAML 布尔值,以及不区分大小写的 `true`/`false`、`yes`/`no`、`on`/`off` 和 `1`/`0`。`disable-model-invocation: true` 会从面向模型的目录和 loader 中排除该 skill;`user-invocable: false` 会从面向用户的命令中排除该 skill。任一字段存在时,提供方都会按照这些外部默认值填充两个正向内部策略值。系统会拒绝驼峰形式的 `disableModelInvocation`、`modelInvocable` 和 `userInvocable` 并记录警告,而不会将其作为兼容别名。
|
||||
这两个调用字段接受 YAML 布尔值,以及不区分大小写的 `true`/`false`、`yes`/`no`、`on`/`off` 和 `1`/`0`。`disable-model-invocation: true` 会从面向模型的目录和 loader 中排除该 skill;`user-invocable: false` 会从面向用户的命令中排除该 skill。每个省略的字段都默认为允许对应接口调用;提供方始终输出两个正向内部策略值,即使两个键都不存在也不例外。系统会拒绝驼峰形式的 `disableModelInvocation`、`modelInvocable` 和 `userInvocable` 并记录警告,而不会将其作为兼容别名。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ interface ParsedSkill {
|
||||
name: string
|
||||
description: string
|
||||
whenToUse?: string
|
||||
invocation?: SkillInvocationPolicy
|
||||
invocation: SkillInvocationPolicy
|
||||
metadata?: Record<string, unknown>
|
||||
content: string
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export class LocalSkillProvider implements SkillProvider {
|
||||
name: parsed.name,
|
||||
description: parsed.description,
|
||||
...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},
|
||||
...parsed.invocation !== undefined ? { invocation: parsed.invocation } : {},
|
||||
invocation: parsed.invocation,
|
||||
source: candidate.source,
|
||||
provider: this.name,
|
||||
resourceBase: { kind: 'directory', path: locator.directory },
|
||||
@@ -185,7 +185,7 @@ async function discoverRoot(root: SkillRoot, ctx: Context): Promise<SkillCandida
|
||||
name: parsed.name,
|
||||
description: parsed.description,
|
||||
...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},
|
||||
...parsed.invocation !== undefined ? { invocation: parsed.invocation } : {},
|
||||
invocation: parsed.invocation,
|
||||
provider: 'local',
|
||||
source: root.source,
|
||||
rank: root.rank,
|
||||
@@ -275,7 +275,7 @@ async function parseSkillFile(path: string, ctx: Context, signal?: AbortSignal,
|
||||
name,
|
||||
description,
|
||||
...optionalString(parsed.data, 'whenToUse'),
|
||||
...invocation === undefined ? {} : { invocation },
|
||||
invocation,
|
||||
...optionalMetadata(parsed.data),
|
||||
content: parsed.body.trim(),
|
||||
}
|
||||
@@ -428,13 +428,12 @@ function optionalString(data: Record<string, unknown>, key: string): { [K in typ
|
||||
return typeof value === 'string' && value.length > 0 ? { [key]: value } : {}
|
||||
}
|
||||
|
||||
function parseInvocationPolicy(data: Record<string, unknown>): SkillInvocationPolicy | undefined {
|
||||
function parseInvocationPolicy(data: Record<string, unknown>): SkillInvocationPolicy {
|
||||
rejectLegacyInvocationKey(data, 'disableModelInvocation', 'disable-model-invocation')
|
||||
rejectLegacyInvocationKey(data, 'modelInvocable', 'disable-model-invocation')
|
||||
rejectLegacyInvocationKey(data, 'userInvocable', 'user-invocable')
|
||||
const disableModelInvocation = frontmatterBoolean(data, 'disable-model-invocation')
|
||||
const userInvocable = frontmatterBoolean(data, 'user-invocable')
|
||||
if (disableModelInvocation === undefined && userInvocable === undefined) return undefined
|
||||
return {
|
||||
modelInvocable: disableModelInvocation !== true,
|
||||
userInvocable: userInvocable !== false,
|
||||
|
||||
@@ -244,7 +244,11 @@ describe('LocalSkillProvider', () => {
|
||||
'rich-skill',
|
||||
'user-only-skill',
|
||||
])
|
||||
expect(flatSummary.invocation).toEqual({ modelInvocable: true, userInvocable: true })
|
||||
expect(await ctx.skills.get('flat-skill')).toBeUndefined()
|
||||
expect(await ctx.skills.get('no-trailing-body')).toMatchObject({
|
||||
invocation: { modelInvocable: true, userInvocable: true },
|
||||
})
|
||||
expect(await ctx.skills.get('user-only-skill')).toMatchObject({
|
||||
invocation: { modelInvocable: false, userInvocable: true },
|
||||
content: 'User-only.',
|
||||
|
||||
Reference in New Issue
Block a user