Merge latest master into codex/migrate-to-oxlint

This commit is contained in:
Tianyi Cui
2026-07-30 00:13:30 +08:00
84 changed files with 4195 additions and 445 deletions

View File

@@ -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 .agents/notes/implemented/feature/2026-07-05-skill-system.md
2026-07-05-skill-system.md: 4fc621a9fdfa8042ebf3eb1975f0930cb1bf116c
2026-07-05-skill-system.zh.md: 0dbebd211a1fa9e434d3f0a189c936f2b1c76574
2026-07-05-skill-system.md: 242650ec8ba64fd0801a958711d5790fae07b259
2026-07-05-skill-system.zh.md: da5f4af4b2f8bc0144be0a7ed608de7edd9a9947

View File

@@ -18,13 +18,13 @@ Provider plugins register synchronously during `apply()`. Provider membership is
The local provider scans cwd-sensitive project roots, custom roots, and user roots in first-wins rank order: project `.dsh`, project `.agents`, `customSkillDirs`, user `.dsh`, then user `.agents`. The user `.dsh/skills` scan skips `.system` so a system-owned directory is not treated as normal user content. DeepSeek Harness does not ship built-in system skills; embedded or remote providers supply additional skills when configured.
Each skill is either `<name>/SKILL.md` or `<name>.md` with YAML frontmatter. `name` and `description` are required; `whenToUse`, `disableModelInvocation`, and `metadata` are optional. Names are kebab-case. YAML frontmatter is parsed with the `yaml` package instead of `js-yaml` or a hand-written parser: `yaml` is the already-declared modern parser for this package's limited frontmatter needs, and a narrow parser would either reject valid YAML users expect to work or grow into an unreviewed YAML subset.
Each skill is either `<name>/SKILL.md` or `<name>.md` with YAML frontmatter. `name` and `description` are required; `whenToUse`, `metadata`, `disable-model-invocation`, and `user-invocable` are optional. Names are kebab-case. The invocation fields project into a typed nested policy as defined by the [independent model and user invocation decision](2026-07-28-skill-invocation-policy.md); the parser rejects the old camel-case spellings. YAML frontmatter is parsed with the `yaml` package instead of `js-yaml` or a hand-written parser: `yaml` is the already-declared modern parser for this package's limited frontmatter needs, and a narrow parser would either reject valid YAML users expect to work or grow into an unreviewed YAML subset.
Local skill filesystem I/O goes through `ctx.fs` when a filesystem service is loaded: project-root lookup probes `.git` with `resolve` and `stat`, root discovery uses `listDir`, and skill reads use `readText`. The Node filesystem remains a fallback for minimal contexts that mount `dsh-skill-local` without the fs seam. Missing roots, unreadable or malformed skill files, and transient provider `list()` failures degrade to warn-and-skip so one bad source does not make every agent request fail; malformed candidates still fail fast because they are provider contract violations.
`dsh-tool-skill` injects one durable user-role `<system-reminder>` catalog as a sourced `user/message` at the session's first `agent/step`, and only when that agent's tool view resolves this plugin's exact `skill` registration. The catalog contains sorted skill name and description only; it excludes bodies, paths, sources, providers, and routing hints. Descriptions are whitespace-normalized, XML-escaped, and capped by `catalogDescriptionMaxLength`, whose default is `500` and minimum is `3`. Full skill bodies are never included in the catalog. (The catalog originally rode the request-only [session-prefix seam](../../archived/feature/2026-07-07-session-prefix.md), archived; the [unified sourced-message decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md) moved it into durable history.)
The `skill({ name })` tool loads one full skill for the current agent cwd and returns a tool result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`. `resourceBase` supplies a directory, URL, or opaque provider-managed base for explicitly referenced scripts, references, and assets; resources load only as needed, without directory enumeration. An unresolved name reports that the skill is unknown or no longer available; invalid names and skills marked `disableModelInvocation` retain distinct tool errors. The tool result is the model-visible disclosure path.
The registry's `list()` returns every winning summary, while model and user consumers apply the invocation predicates owned by the [independent invocation-policy decision](2026-07-28-skill-invocation-policy.md). The `skill({ name })` tool loads one model-invocable skill for the current agent cwd and returns a tool result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`. `resourceBase` supplies a directory, URL, or opaque provider-managed base for explicitly referenced scripts, references, and assets; resources load only as needed, without directory enumeration. An unresolved name reports that the skill is unknown or no longer available; invalid names and skills with `invocation.modelInvocable: false` retain distinct tool errors. The tool result is the model-visible disclosure path.
The data structures and catalog/tool contract are documented in [skills.md](../../../../docs/core-data-structures/skills.md), with service signatures in the generated [services catalog](../../../../docs/cordis-catalog/services.md).
@@ -52,4 +52,4 @@ The catalog is deterministic for a fixed root set and runtime registration revis
## Deferred
Forked skill contexts (`context: fork`), parameter declarations and hints (`arguments` and `argument-hint`), and per-skill tool constraints (`allowed-tools` and `disallowed-tools`) are outside the shipped contract. The registry, local provider, and model-facing tool do not parse, advertise, or enforce these fields, and the `user-invocable` frontmatter field is likewise unparsed. Direct user invocation itself ships as a consumer-side affordance instead: the TUI front door offers a manual `/skill:<name>` command over the registry's existing `list()` and `get()` methods, without a registry, provider, or tool contract change — see [the TUI skill slash command](2026-07-21-tui-skill-slash-command.md).
Forked skill contexts (`context: fork`), parameter declarations and hints (`arguments` and `argument-hint`), and per-skill tool constraints (`allowed-tools` and `disallowed-tools`) are outside the shipped contract. The registry, local provider, and model-facing tool do not parse, advertise, or enforce these fields. Direct user invocation ships as a TUI affordance over the shared invocation policy and trusted `get()` primitive; see [the TUI skill slash command](2026-07-21-tui-skill-slash-command.md).

View File

@@ -18,13 +18,13 @@ DeepSeek Harness 使用同一原语,使项目特定的评审、插件编写和
本地提供方按先到先得的排名顺序扫描 cwd 敏感的项目根目录、自定义根目录和用户根目录:项目 `.dsh`、项目 `.agents``customSkillDirs`、用户 `.dsh`,然后是用户 `.agents`。用户 `.dsh/skills` 扫描跳过 `.system`以免系统拥有的目录被当作普通用户内容处理。DeepSeek Harness 不随附内置系统 skill嵌入式或远程提供方在配置后提供额外 skill。
每个 skill 是 `<name>/SKILL.md` 或带 YAML frontmatter 的 `<name>.md``name``description` 为必填;`whenToUse``disableModelInvocation``metadata` 为可选。名称采用 kebab-case。YAML frontmatter 使用 `yaml`package解析而非 `js-yaml` 或手写解析器:`yaml` 是本包有限 frontmatter 需求已声明的现代解析器,窄解析器要么拒绝用户预期可用的合法 YAML要么膨胀为一个未经评审的 YAML 子集。
每个 skill 是 `<name>/SKILL.md` 或带 YAML frontmatter 的 `<name>.md``name``description` 为必填;`whenToUse``metadata``disable-model-invocation``user-invocable` 为可选。名称采用 kebab-case。调用字段会投影到类型化的嵌套策略中,具体由[模型与用户独立调用决策](2026-07-28-skill-invocation-policy.md)定义;解析器会拒绝旧的驼峰拼写。YAML frontmatter 使用 `yaml`package解析而非 `js-yaml` 或手写解析器:`yaml` 是本包有限 frontmatter 需求已声明的现代解析器,窄解析器要么拒绝用户预期可用的合法 YAML要么膨胀为一个未经评审的 YAML 子集。
本地 skill 的文件系统 I/O 在加载了文件系统服务时通过 `ctx.fs` 进行:项目根目录查找使用 `resolve``stat` 探测 `.git`,根目录发现使用 `listDir`skill 读取使用 `readText`。Node 文件系统作为后备,供在不挂载 fs seam 的最小上下文中加载 `dsh-skill-local` 时使用。缺失的根目录、不可读或格式错误的 skill 文件、以及提供方 `list()` 的瞬态失败均降级为警告并跳过,使一个坏源不会导致所有 agent 请求失败;格式错误的候选项仍然快速失败,因为它们违反了提供方契约。
`dsh-tool-skill` 在会话的第一个 `agent/step` 注入一个持久化的 user-role `<system-reminder>` 目录,作为带来源的 `user/message`,且仅当该 agent 的工具视图解析到本插件精确的 `skill` 注册时才注入。该目录仅包含排序后的 skill 名称与描述不包含正文、路径、来源、提供方和路由提示。描述经过空白规范化、XML 转义,并受 `catalogDescriptionMaxLength` 上限约束,其默认值为 `500`,最小值为 `3`。完整的 skill 正文从不包含在目录中。(目录最初通过仅请求的[会话前缀 seam](../../archived/feature/2026-07-07-session-prefix.md)(已归档)传递;[统一带来源消息的决策](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)将其移入持久化历史。)
`skill({ name })` 工具为当前 agent cwd 加载一个完整 skill返回包含 `<skill_content name="...">``<skill_resources>``<skill_instructions>` 的工具结果。`resourceBase` 提供一个目录、URL 或不透明的提供方管理的基路径,用于显式引用的脚本、参考资料和资产;资源仅按需加载,不进行目录枚举。无法解析的名称报告该 skill 未知或不再可用;无效名称和标记了 `disableModelInvocation` 的 skill 保留不同的工具错误。工具结果是面向模型的可见披露路径。
注册表的 `list()` 返回全部胜出摘要,而模型与用户消费方应用[独立调用策略决策](2026-07-28-skill-invocation-policy.md)定义的调用判定。`skill({ name })` 工具为当前 agent cwd 加载一个模型可调用的 skill返回包含 `<skill_content name="...">``<skill_resources>``<skill_instructions>` 的工具结果。`resourceBase` 提供一个目录、URL 或不透明的提供方管理的基路径,用于显式引用的脚本、参考资料和资产;资源仅按需加载,不进行目录枚举。无法解析的名称报告该 skill 未知或不再可用;无效名称和 `invocation.modelInvocable``false` 的 skill 保留不同的工具错误。工具结果是面向模型的可见披露路径。
数据结构与目录/工具契约记录在 [skills.md](../../../../docs/core-data-structures/skills.md) 中,服务签名见生成的[服务目录](../../../../docs/cordis-catalog/services.md)。
@@ -52,4 +52,4 @@ agent-core 主干包含一个目录贡献者、一个本地提供方和一个面
## 延后
Fork 的 skill 上下文(`context: fork`)、参数声明与提示(`arguments` 和 `argument-hint`)、以及逐 skill 的工具约束(`allowed-tools` 和 `disallowed-tools`)不在已交付的契约范围内。注册表、本地提供方和面向模型的工具不解析、不广播、也不执行这些字段`user-invocable` frontmatter 字段同样不会被解析。直接用户调用本身则作为消费方层面的能力交付TUI 前门基于注册表现有的 `list()` 与 `get()` 方法提供手动 `/skill:<name>` 命令,无需变更注册表、提供方或工具契约——见 [TUI skill 斜杠命令](2026-07-21-tui-skill-slash-command.md)。
Fork 的 skill 上下文(`context: fork`)、参数声明与提示(`arguments` 和 `argument-hint`)、以及逐 skill 的工具约束(`allowed-tools` 和 `disallowed-tools`)不在已交付的契约范围内。注册表、本地提供方和面向模型的工具不解析、不广播、也不执行这些字段。直接用户调用作为 TUI 功能交付,基于共享调用策略和受信的 `get()` 原语;见 [TUI skill 斜杠命令](2026-07-21-tui-skill-slash-command.md)。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
2026-07-21-tui-skill-slash-command.md: 8370ab61f552a6a60177b6da0b598dd142d21960
2026-07-21-tui-skill-slash-command.zh.md: 66edec6ecd5c2a974b56e08bd9e924729302cc4a
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-21-tui-skill-slash-command.md
2026-07-21-tui-skill-slash-command.md: 872e1f109728731e0d55e81a538c81e794724856
2026-07-21-tui-skill-slash-command.zh.md: 772e25745ea7ab25f715208a9c6b1d10cf0c6e65

View File

@@ -10,17 +10,17 @@ The [skill system](2026-07-05-skill-system.md) shipped with model-initiated load
## Decision
The [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) front door owns a `/skill:<name> [instructions]` command. On submit it loads the named skill and delivers one text block as a user turn — sent with `agent.send()` while idle and `agent.steer()` while running, the same rule as ordinary editor input. The block is `renderSkillInvocation(skill, instructions)`: a `<skill name="…">` element wrapping the skill body, preceded by one resource-base line when the provider exposes one, with the user's trailing text appended after a blank line. The command is a TUI-only affordance; it adds no model-facing tool and changes no skill-system package contract.
The [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) front door owns a `/skill:<name> [instructions]` command. On submit it loads the named skill and delivers one text block as a user turn — sent with `agent.send()` while idle and `agent.steer()` while running, the same rule as ordinary editor input. The block is `renderSkillInvocation(skill, instructions)`: a `<skill name="…">` element wrapping the skill body, preceded by one resource-base line when the provider exposes one, with the user's trailing text appended after a blank line. The command is a TUI-only affordance; it adds no model-facing tool. Its visibility and loading policy comes from the shared [independent model and user skill invocation policy](2026-07-28-skill-invocation-policy.md).
The TUI reads the skill service through `ctx.get('skills')`, not a declared injection, because skills mount conditionally: a deployment without the registry keeps a working front door, and `/skill:` there reports that skills are unavailable rather than failing to mount. `createTuiChat` is synchronous while `ctx.skills.list()` is async, so autocomplete seeds the static slash commands immediately and rebuilds the provider with `skill:<name>` entries once the catalog resolves; a resolution that arrives after disposal is dropped, and a rejected lookup keeps the base commands.
Autocomplete lists only model-invocable skills — it is built from `list()`, which omits `disableModelInvocation` skills — while manual submission resolves through `get()`, which the skill registry documents as the trusted-caller path that returns disabled skills too. So a person can load any skill by typing its exact name, but the completion menu never advertises a skill the model is meant not to see. Each completion entry is labeled with its winning source's scope — `(project)` for the `project-` sources, `(user)` for every other source — in the slash-command argument-hint slot, which the menu shows but selection never inserts, so trailing instructions still follow the completed name. An unknown name, an empty name after the prefix, and a lookup failure each surface as a transcript notice without sending anything.
Autocomplete filters the invocation-neutral `list()` result with `isUserInvocable`, and manual submission applies the same predicate after trusted `get()` resolves the definition. A user-only skill can therefore appear and load even when model invocation is disabled, while a user-disabled skill is neither advertised nor loadable by exact name. Each completion entry is labeled with its winning source's scope — `(project)` for the `project-` sources, `(user)` for every other source — in the slash-command argument-hint slot, which the menu shows but selection never inserts, so trailing instructions still follow the completed name. An unknown name, an empty name after the prefix, a user-disabled name, and a lookup failure each surface as a transcript notice without sending anything.
`renderSkillInvocation` and the resource-base line are the TUI's own, deliberately not reused from `dsh-tool-skill`'s `skill` tool result. The tool wraps a body in `<skill_content>`/`<skill_resources>`/`<skill_instructions>` for a *tool result*; a manual invocation is a *user turn*, and coupling the two renderers would force one model-facing shape to serve both surfaces. The cost is two renderers that both format a skill body; the benefit is that each surface's model-facing text evolves independently, and each is pinned where it is produced.
## Alternatives considered
**Add a `user-invocable` frontmatter field and enforce it in the registry.** Rejected for this change. The skill-system note defers that field, and manual invocation does not need it: the TUI is a trusted local caller, so `get()` already authorizes loading any skill, and autocomplete visibility keys off the existing `disableModelInvocation`. A new per-skill field would add a contract to the registry, local provider, and tool with no current consumer beyond visibility, which `disableModelInvocation` already covers.
**Add a `user-invocable` frontmatter field only inside the original TUI change.** Rejected there because a TUI-only field would have changed the registry, provider, and tool contract without a shared invocation model. The later [independent invocation-policy decision](2026-07-28-skill-invocation-policy.md) adds it across every relevant consumer and preserves `get()` as a trusted primitive.
**Declare `skills` as a TUI injection.** Rejected because skills mount conditionally; a declared injection would make the front door require the registry and refuse to mount without it, contradicting the package's optional-service stance. `ctx.get('skills')` reads the global store and tolerates absence.

View File

@@ -10,17 +10,17 @@ Status: implemented
## Decision
[`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 前门拥有一条 `/skill:<name> [instructions]` 命令。提交时它加载指定的 skill并投递一个文本块作为用户轮次——空闲时用 `agent.send()` 发送、运行中用 `agent.steer()` 中途引导,与普通编辑器输入遵循同一规则。该文本块由 `renderSkillInvocation(skill, instructions)` 生成:一个包裹 skill 正文的 `<skill name="…">` 元素,当提供方暴露资源基址时在其前加一行资源基址行,用户尾随的文本在空行之后追加。该命令是 TUI 独有的能;它不新增任何面向模型的工具,也不改动任何 skill 系统包的契约
[`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 前门拥有一条 `/skill:<name> [instructions]` 命令。提交时它加载指定的 skill并投递一个文本块作为用户轮次——空闲时用 `agent.send()` 发送、运行中用 `agent.steer()` 中途引导,与普通编辑器输入遵循同一规则。该文本块由 `renderSkillInvocation(skill, instructions)` 生成:一个包裹 skill 正文的 `<skill name="…">` 元素,当提供方暴露资源基址时在其前加一行资源基址行,用户尾随的文本在空行之后追加。该命令是 TUI 独有的能;它不新增任何面向模型的工具。其可见性和加载策略来自共享的[模型与用户独立 skill 调用策略](2026-07-28-skill-invocation-policy.md)
TUI 通过 `ctx.get('skills')` 读取 skill 服务,而非声明式注入,因为 skill 是条件挂载的:没有注册表的部署仍保有可用的前门,此时 `/skill:` 会报告 skill 不可用,而不是挂载失败。`createTuiChat` 是同步的,而 `ctx.skills.list()` 是异步的,所以自动补全先立即种入静态斜杠命令,待目录解析完成后再用 `skill:<name>` 条目重建 provider提供方在 dispose资源释放之后才到达的解析结果会被丢弃而被拒绝的查找会保留基础命令。
自动补全只列出模型可调用的 skill——它基于 `list()` 构建,而 `list()` 会略去 `disableModelInvocation` 的 skill——手动提交则通过 `get()` 解析skill 注册表将其记录为返回被禁用 skill 的可信调用方路径。因此用户可以通过键入 skill 的确切名称加载任意 skill但补全菜单绝不会宣传一个本不该让模型看见的 skill。每个补全条目都以其胜出来源的作用域为标签——`project-` 来源标为 `(project)`,其他一切来源标为 `(user)`——标签置于斜杠命令的参数提示位,菜单会显示它,但选中时绝不会插入,因此尾随指令仍然跟在补全后的名称之后。未知名称、前缀之后为空的名称、以及查找失败,都会各自呈现为 transcript文本记录中的一条通知且不发送任何内容。
自动补全使用 `isUserInvocable` 过滤与调用策略无关的 `list()` 结果;手动提交则在受信的 `get()` 解析定义后应用相同判定。因此,即使模型调用已禁用,仅供用户调用的 skill 仍会显示并可加载;用户禁用的 skill 既不会展示,也无法按精确名称加载。每个补全条目都以其胜出来源的作用域为标签——`project-` 来源标为 `(project)`,其他一切来源标为 `(user)`——标签置于斜杠命令的参数提示位,菜单会显示它,但选中时绝不会插入,因此尾随指令仍然跟在补全后的名称之后。未知名称、前缀之后为空的名称、用户禁用的名称以及查找失败,都会各自呈现为 transcript文本记录中的一条通知且不发送任何内容。
`renderSkillInvocation` 及资源基址行是 TUI 自有的,刻意不复用 `dsh-tool-skill``skill` 工具结果。该工具把正文包进 `<skill_content>`/`<skill_resources>`/`<skill_instructions>` 是为了一个*工具结果*;而手动调用是一个*用户轮次*,把两个渲染器耦合起来会迫使一种面向模型的形态同时服务两个界面。代价是两个都在格式化 skill 正文的渲染器;收益是各界面面向模型的文本可以独立演进,且各自在其产出处被固定。
## Alternatives considered
**新增 `user-invocable` frontmatter 字段并在注册表中强制执行。** 本次改动否决。skill 系统 note 把该字段列为待办而手动调用并不需要它TUI 是可信的本地调用方,`get()` 已经授权加载任意 skill自动补全的可见性以既有的 `disableModelInvocation` 为准。新增一个逐 skill 字段会给注册表、本地提供方和工具都加上一条契约,而除了可见性之外没有任何现有消费方,可见性又已由 `disableModelInvocation` 覆盖
**仅在最初的 TUI 变更内新增 `user-invocable` frontmatter 字段** 当时未采纳,因为 TUI 独有的字段会在没有共享调用模型的情况下改变注册表、提供方和工具契约。后续的[独立调用策略决策](2026-07-28-skill-invocation-policy.md)将其扩展到每个相关消费方,并保留 `get()` 作为受信原语
**把 `skills` 声明为 TUI 注入。** 否决,因为 skill 是条件挂载的;声明式注入会使前门必须依赖注册表,缺少它就拒绝挂载,与本包可选服务的立场相悖。`ctx.get('skills')` 读取全局存储并容忍其缺失。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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 .agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.md
2026-07-27-skill-catalog-hot-refresh.md: 8e70fb10e7da4292325b72f3a0392bef2271738c
2026-07-27-skill-catalog-hot-refresh.zh.md: 9a6b6d944baa4a9cc4dddb5158fdcbac2b05f5db

View File

@@ -0,0 +1,48 @@
# Agent Note: Skill catalog hot refresh
Status: implemented
English | [中文](2026-07-27-skill-catalog-hot-refresh.zh.md)
## Problem
Skill summaries are model routing input, but local skills can appear, disappear, or be renamed after a session starts. IDEs, Git operations, shell commands, and other processes can all mutate `.agents/skills` without going through the harness filesystem tools. A startup-only catalog leaves the model unaware of new skills and able to call deleted names. Treating every instruction-body edit as a catalog revision would instead couple progressive loading to unnecessary prompt churn.
Filesystem updates are also non-atomic from the observer's perspective. An editor or Git operation may briefly remove a file, a watched root may not exist at startup, and discovery may fail transiently. Publishing those intermediate observations as authoritative empty catalogs would be worse than retaining the last complete view.
## Decision
The skill capability separates catalog membership from instruction-body loading. `ctx.skills.snapshot()` returns summaries plus a completeness bit. `ctx.skills.registerProvider(factory)` gives the synchronous factory one registration-scoped `{ signal, invalidate }` control: `invalidate()` dirties only that exact active registration and discards completed catalog caches, while the signal aborts when registration fails or is disposed. Provider arrays are complete-discovery shorthand; an explicit incomplete observation can retain readable candidates for direct loads without becoming cacheable or authoritative for model-facing consumers. A provider or runtime generation change during discovery retries once; if the retry is also superseded, the latest candidates return as an incomplete, uncached observation. A late invalidation after disposal or replacement is a no-op because the capability has been revoked.
`@deepseek-ai/dsh-skill-local` directly depends on Chokidar and observes catalog-relevant host paths. Existing roots watch direct skill bundle directories, flat Markdown entries, and direct `SKILL.md` entry files. Additions, removals, and directory changes invalidate membership; file changes support frontmatter `name` and `description` refresh. Resource files below a bundle are ignored. Events in one microtask batch coalesce to one invalidation. Project watchers use a bounded least-recently-observed set.
A missing root is followed from its nearest existing ancestor one absent segment at a time with `fs.watchFile`, then handed to Chokidar once the real root exists. Before scanning, each discovery re-probes the retained root/ancestor mode. That independent probe re-establishes ancestor observation after deletion even when child removals invalidate and publish an authoritative empty catalog before, or without, a root `unlinkDir` event. Chokidar configuration exposes native-versus-polling mode, write stability, polling interval, symlink following, and project watcher capacity. First-party `write` and `edit` tool observations synchronously invalidate a relevant provider, so the next model step sees its own mutation without waiting for host delivery. Watch startup/runtime failures are logged and retried; discovery still returns readable candidates for direct loads but reports an incomplete observation. Teardown closes watchers and ignores late callbacks.
`@deepseek-ai/dsh-tool-skill` injects the first non-empty complete catalog as a durable sourced `user/message` on the first complete `agent/step` that observes one. At every `agent/step` it applies exact `skill` tool visibility, hashes the exact rendered text between the `<available_skills>` tags, and scans the read-only session events backwards without copying them for the newest recognizable visible catalog from this plugin. A changed digest appends a durable, complete replacement through `agent.inject()`, including an explicit empty catalog when all skills disappear. If no catalog remains visible but a recognizable one exists in historical events, compaction hid it and the next complete observation re-establishes the current catalog, including an empty tombstone. A current empty catalog with no historical publication emits nothing, while an incomplete snapshot preserves the last-good model view. The backward scan normally stops at the newest visible catalog; when compaction hides every catalog it pays an O(session-events) scan to recover that fact.
The TUI consumes the same invalidation as presentation state, not session history. `skills/change` carries no diff; the TUI refetches `snapshot()` for the active session cwd, applies only the latest complete result, and retains the previous commands across incomplete observations. A complete empty result clears stale completions. Because pi-tui closes autocomplete when its provider is replaced, a catalog that arrives while the user is typing a slash-command name also triggers a suggestion-only re-query of the current draft.
Instruction bodies keep progressive disclosure. Every `skill(name)` call asks the provider to reread and parse the current file; there is no body cache, hash, revision, or proactive notification. Previously logged tool results remain unchanged. If the loaded frontmatter name no longer matches the selected candidate, the registry rejects the stale name and invalidates that provider so a later catalog observation can publish the new name.
## Verification
Registry tests pin registration-scoped invalidation, revocation, signal abort, contained observer failures, incomplete candidates, bounded generation retries, and stale-name rejection. Local-provider tests cover bundle and flat-file creation, removal, rename, root creation/deletion/recreation including an unobserved root `unlinkDir`, description changes, body-only edits, first-party observation, symlinks, polling options, persistent watcher failures with loadable candidates, event coalescing, bounded projects, teardown, and transient reads. Tool tests pin full replacement messages, empty tombstones, digest stability for body-only edits, incomplete-state retention, visibility, and resume metadata. TUI tests pin last-complete retention, authoritative empty removal, latest-wins refresh, teardown, and the already-open slash-draft race; a real Loader/PTY smoke adds a local skill after startup and observes its completion without restarting. A keyless assembled agent-spine snapshot creates a project skill through model-facing filesystem tools, observes its replacement catalog on the next request, and loads its current body with the real `skill` tool.
## Alternatives considered
- **Put the live catalog in World State** — rejected because catalog replacements are model-visible session inputs and must be reconstructable from the event log. Durable injected history already provides replay, resume, fork, and compaction semantics without another mutable state plane.
- **Rely only on `fs/observed`** — rejected because IDEs, Git, shell commands, and external processes do not cross that seam. The event remains a latency fast path for first-party tools, while host watching supplies coverage.
- **Hash or version every `SKILL.md` body** — rejected because the model initially sees only names and descriptions, and the provider already rereads the body on each tool call. Body revisions would create catalog traffic without changing routing and would not justify rewriting historical tool results.
- **Watch every bundle resource** — rejected because references, scripts, and assets are loaded on demand and do not affect the category list. Broad recursive watching would add invalidations, descriptor pressure, and platform variability without improving routing.
- **Publish partial or failed discovery as the new catalog** — rejected because a transient read failure is not evidence of deletion. The completeness bit lets the model-facing consumer preserve its last-good catalog until a full observation succeeds.
- **Keep `invalidateProvider(provider)` public** — rejected because it exposes a registry mutation method and makes callers resupply an identity the registry already owns. The factory-issued closure binds invalidation to one registration and becomes inert on disposal, so observers need neither registry access nor provider identity.
- **Extract a generic Cordis file-watching service now** — deferred until another consumer establishes the reusable service contract. The local provider marks its Chokidar and missing-root observation boundary for that extraction; skill-path filtering and the call to the provider's invalidation closure remain skill-specific.
## Consequences
- New, deleted, and renamed local skills become visible at model-step boundaries without restarting the agent, including when the skills root did not exist at startup.
- The TUI's `/skill:` completions converge on the same complete catalog without blocking each keystroke on discovery; an open slash-name draft refreshes when the catalog arrives.
- Catalog messages are append-only, logged, whole-list snapshots. They preserve earlier reusable tokens; replacements retire stale names explicitly, at token cost proportional to the current catalog on each actual digest change.
- Body-only edits produce no catalog message. A subsequent tool call sees current content, while prior tool results remain an accurate record of what the model previously loaded.
- Missing-root polling and Chokidar add one maintained runtime dependency, host watcher resources, bounded detection latency, and deployment tunables. The bounded project set and teardown contract contain those costs.
- Remote or future mutable providers retain their own registration-scoped invalidation closure and call it from their observation mechanism; the registry does not impose a universal watcher or TTL.

View File

@@ -0,0 +1,48 @@
# Agent Note: Skill 目录热刷新
Status: implemented
[English](2026-07-27-skill-catalog-hot-refresh.md) | 中文
## 问题
skill技能摘要是模型的路由输入但本地 skill 可在会话启动后新增、消失或重命名。IDE、Git 操作、shell 命令和其他进程都可以修改 `.agents/skills`,而不经过 harness 文件系统工具。仅在启动时构建目录,会让模型无法获知新 skill并且仍能调用已删除的名称。反之如果把每次指令正文编辑都视为目录修订就会让渐进式加载与不必要的提示词频繁变化耦合。
从观察方来看,文件系统更新也不是原子完成的。编辑器或 Git 操作可能会短暂移除文件,受监视的根目录在启动时可能不存在,发现也可能暂时失败。把这些中间观察结果发布为权威空目录,比保留最后一个完整视图更糟。
## 决策
skill 服务将目录成员关系与指令正文加载分离。`ctx.skills.snapshot()` 返回摘要及一个完整性位。`ctx.skills.registerProvider(factory)` 会向同步工厂提供一项注册作用域内的 `{ signal, invalidate }` 控制能力:`invalidate()` 只会将该精确活动注册标记为脏,并丢弃已完成目录缓存;注册失败或释放时,信号会中止。提供方返回的数组是完整发现的简写形式;显式的不完整观测可以保留可读候选项供直接加载,但不能缓存,也不能作为面向模型消费方的权威结果。在发现期间,如果提供方或运行时 generation 发生变化,系统会重试一次;如果这次重试也被后续修订取代,则最新候选项会作为不完整且不缓存的观测返回。资源释放或替换后的延迟失效操作不会执行任何操作,因为该能力已被撤销。
`@deepseek-ai/dsh-skill-local` 直接依赖 Chokidar并观察与目录相关的宿主路径。已有根目录会监视其直属 skill bundle 目录、平铺的 Markdown 条目和直属 `SKILL.md` 条目文件。新增、移除和目录变更会使成员关系失效;文件变更还支持刷新 frontmatter 中的 `name``description`。bundle 内更深层的资源文件会被忽略。同一微任务批次中的事件会合并为一次失效。项目 watcher 使用有界集合,并按最久未观察顺序淘汰。
系统从缺失根目录最近的现有祖先开始,使用 `fs.watchFile` 每次跟进一层缺失路径片段;真实根目录出现后,再交给 Chokidar。每次发现操作都会在扫描前重新探测所保留的根目录祖先模式。即使子项移除在根目录 `unlinkDir` 事件之前就触发失效并发布权威空目录或者该事件根本没有到达这项独立探测也会在删除后重新建立祖先观察。Chokidar 配置公开原生事件或轮询模式、写入稳定性、轮询间隔、符号链接跟随选项和项目 watcher 容量。第一方 `write``edit` 工具观察会同步使相关提供方失效因此下一个模型步骤无需等待宿主事件投递就能看到自身改动。watcher 启动或运行失败会被记录并触发重试;发现过程仍会返回可读候选项供直接加载,但会报告不完整观测。资源销毁会关闭 watcher并忽略延迟回调。
`@deepseek-ai/dsh-tool-skill``agent/step` 首次观察到非空完整目录时,将该目录注入为一条持久且带来源的 `user/message`。每次 `agent/step`,它都会应用 `skill` 工具的精确可见性,对 `<available_skills>` 标签之间精确渲染的文本计算哈希并从后向前扫描只读会话事件且不复制以查找该插件发布的最新一条可识别且仍可见的目录。digest 变化时,插件通过 `agent.inject()` 追加一份持久的完整替换目录;所有 skill 消失时也会追加显式空目录。如果没有目录仍然可见但历史事件中存在可识别目录则说明压缩compaction已将其遮蔽下一次完整观察会重新建立当前目录包括空 tombstone。如果当前目录为空且历史上从未发布目录则不发送任何内容不完整快照则保留最后一次完整的模型视图。反向扫描通常在最新且仍可见的目录处停止当压缩遮蔽所有目录时它会以一次 O(session-events) 扫描的成本确认这一事实。
TUI 将同一失效通知作为界面状态而非会话历史来消费。`skills/change` 不携带 diffTUI 会为活动会话的 cwd 重新获取 `snapshot()`仅应用最新的完整结果并在观测不完整时保留先前命令。完整的空结果会清除陈旧补全项。pi-tui 在其提供方被替换时会关闭自动补全,因此如果目录在用户输入斜杠命令名称期间到达,还会触发一次仅用于更新建议的当前草稿重查。
指令正文继续采用渐进式披露。每次调用 `skill(name)` 时,系统都会要求提供方重新读取并解析当前文件;不存在正文缓存、哈希、修订或主动通知。先前记录的工具结果保持不变。如果加载后的 frontmatter 名称不再匹配所选候选项,注册表会拒绝这个陈旧名称,并使该提供方失效,以便后续目录观察发布新名称。
## 验证
注册表测试固定了注册作用域内的失效、能力撤销、信号中止、监听器失败隔离、不完整候选项、有界 generation 重试和陈旧名称拒绝。local-provider 测试覆盖 bundle 与平铺文件的创建、移除和重命名,以及根目录创建/删除/重建(包括未观测到根目录 `unlinkDir` 事件的情形)、描述变更、仅正文编辑、第一方观察、符号链接、轮询选项、候选项仍可加载的持续 watcher 失败、事件合并、项目 watcher 容量上限、资源销毁和暂时读取。工具测试固定了完整替换消息、空 tombstone、仅修改正文时 digest 稳定、不完整状态保留、可见性和恢复元数据。TUI 测试固定了上一份完整结果保留、权威空结果清除、刷新时以最新结果为准、资源销毁和已打开斜杠草稿的竞态;一项使用真实 Loader/PTY 的 smoke 测试会在启动后添加本地 skill并观察其补全项出现而无需重启。一个无密钥、装配完成的 agent-spine 快照测试通过面向模型的文件系统工具创建项目 skill观察下一次请求中的替换目录并使用真实 `skill` 工具加载当前正文。
## 考虑过的替代方案
- **将实时目录放入 World State**不予采纳因为目录替换是模型可见的会话输入必须能够从事件日志重建。持久注入历史已经提供回放、恢复、fork 和压缩语义,无需再引入一套可变状态层。
- **只依赖 `fs/observed`**:不予采纳,因为 IDE、Git、shell 命令和外部进程都不会经过该 seam。该事件仍作为第一方工具的低延迟快速路径宿主监视则补齐覆盖。
- **为每个 `SKILL.md` 正文计算哈希或版本**:不予采纳,因为模型最初只看到名称和描述,提供方已经在每次工具调用时重新读取正文。正文修订会产生目录流量,却不会改变路由,也不足以成为改写历史工具结果的理由。
- **监视每个 bundle 资源**:不予采纳,因为参考资料、脚本和产物都是按需加载的,不影响类别列表。宽泛的递归监视会增加失效、描述符压力和平台差异,却不能改善路由。
- **将部分发现或失败发现发布为新目录**:不予采纳,因为暂时读取失败不能证明文件已删除。完整性位让面向模型的消费方保留最后一次完整目录,直到完整观察成功。
- **保留公开的 `invalidateProvider(provider)`**:不予采纳,因为这会公开一项注册表变更方法,并要求调用方重复提供注册表已经持有的身份。发给工厂的闭包会将失效绑定到单个注册,并在释放后失去作用,因此观察方既不需要访问注册表,也不需要提供方身份。
- **现在提取通用 Cordis 文件监视服务**:暂缓,直到另一个消费方确立可复用的服务契约。本地提供方标出了其 Chokidar 和缺失根目录观测边界以便后续提取skill 路径过滤以及对提供方失效闭包的调用仍属于 skill 专用逻辑。
## 影响
- 新增、删除和重命名的本地 skill 会在模型步骤边界变得可见,无需重启 agent智能体即使 skill 根目录在启动时不存在也一样。
- TUI 的 `/skill:` 补全会收敛到同一份完整目录,而不会让每次按键都阻塞于发现;打开的斜杠命令名称草稿会在目录到达时刷新。
- 目录消息采用仅追加、日志记录和全量列表快照。它们会保留较早的可重用 token替换目录会显式停用陈旧名称每次 digest 实际变化时token 成本与当前目录大小成正比。
- 仅修改正文不会产生目录消息。后续工具调用会看到当前内容,而先前工具结果仍准确记录模型之前加载的内容。
- 缺失根目录轮询和 Chokidar 引入一个有人维护的运行时依赖、宿主 watcher 资源、有界检测延迟和部署可调参数。有界项目集合与资源销毁契约会限制这些成本。
- 远程或未来的可变提供方会保留各自注册作用域内的失效闭包,并通过自身观察机制调用它;注册表不会强制采用通用 watcher 或 TTL。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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 .agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.md
2026-07-28-skill-invocation-policy.md: f74b0bcfddb1699c48279b4d8b153cabf764b140
2026-07-28-skill-invocation-policy.zh.md: 1a7117a382be224c5371964dd4ad3e916d4e0917

View File

@@ -0,0 +1,50 @@
# Agent Note: Independent model and user skill invocation policy
Status: implemented
English | [中文](2026-07-28-skill-invocation-policy.zh.md)
## Problem
The skill registry originally treated discovery as a model catalog: `ctx.skills.list()` removed model-disabled skills, while `ctx.skills.get()` remained an unfiltered trusted loader. That was enough for model-initiated loading, but it could not represent Claude-compatible skills that are advertised only to a person, only to a model, to both, or to neither. The TUI compounded the mismatch by deriving user autocomplete from the model-filtered list and allowing every exact name through `get()`.
The local parser also exposed an internal camel-case spelling as frontmatter. Supporting the established negative `disable-model-invocation` and positive `user-invocable` fields requires a durable, symmetric domain representation without turning every possible YAML key into an untyped cross-package contract.
## Decision
`SkillSummary` carries a required typed `invocation: SkillInvocationPolicy` object whose `modelInvocable: boolean` and `userInvocable: boolean` fields are positive and symmetric. Omission exists only at explicit input seams: a runtime `SkillRegistration` without a policy and local frontmatter without either invocation key resolve to `{ modelInvocable: true, userInvocable: true }` before producing candidates or definitions. Future frontmatter keys remain outside the domain model until a consumer and enforcement contract exist; the local provider still parses frontmatter as an open `Record<string, unknown>`, then projects only recognized fields and their defaults into the normalized typed policy.
`ctx.skills.list()` returns every winning summary and no longer chooses an invocation surface. `isModelInvocable(skill)` and `isUserInvocable(skill)` read the matching positive field directly. `ctx.skills.get()` remains policy-neutral because trusted internal callers may need any definition, while a public consumer must enforce its own predicate before advertising or loading a skill. The model tool and TUI check the invocation-neutral summary before calling `get()`, then recheck the loaded definition so a denied name never reaches definition loading and a policy change between discovery and load cannot expose its body.
The local provider accepts the exact kebab-case frontmatter keys `disable-model-invocation` and `user-invocable`. It accepts YAML booleans plus case-insensitive `true`/`false`, `yes`/`no`, `on`/`off`, and `1`/`0`, matching the practical boolean forms accepted by Claude skills. It maps `disable-model-invocation` to the inverse positive field and fills both positive fields from their defaults even when neither key is present. A camel-case external spelling or non-boolean invocation value drops the entire skill from discovery with a targeted warning; this pre-release repository does not keep an on-disk compatibility alias. Invocation data fails closed because ignoring it would default to permission and could expose the skill on a disabled surface, while wrong-typed optional `whenToUse` and `metadata` values are omitted because they do not decide invocation.
The model-facing `dsh-tool-skill` catalog and loader enforce `isModelInvocable`. The TUI `/skill:` autocomplete and exact loader enforce the user field locally, so a user-only skill is visible and loadable there even when it is absent from model discovery, without turning the optional skill peer into a runtime import. The launcher-seeded initial skill used by guided `dsh migrate` and `dsh upgrade` sessions follows this same TUI path and must remain user-invocable. The browser `skill.list` RPC serves a user-selected reference that still asks the model to load the skill, so it exposes the intersection of model- and user-invocable skills; no direct browser skill-loading RPC is added.
These rules permit all four combinations:
| Policy | Model surface | User surface |
|---|---|---|
| `{ modelInvocable: true, userInvocable: true }` | included | included |
| `{ modelInvocable: true, userInvocable: false }` | included | excluded |
| `{ modelInvocable: false, userInvocable: true }` | excluded | included |
| `{ modelInvocable: false, userInvocable: false }` | excluded | excluded |
This decision extends the [skill system](2026-07-05-skill-system.md) and supersedes the invocation-policy limitation recorded by the [TUI skill slash command](2026-07-21-tui-skill-slash-command.md).
## Alternatives considered
**Store all frontmatter in a generic `Map` and read string keys in `isModelInvocable` / `isUserInvocable`.** Rejected because misspelled keys, non-boolean values, and consumer-specific coercion would cross package seams without type checking. The parser boundary remains open; the domain model is deliberately typed and narrow.
**Keep `ctx.skills.list()` model-filtered and add a second user list.** Rejected because discovery, duplicate resolution, caching, and ordering are surface-neutral work. One complete catalog plus explicit predicates prevents those mechanisms from drifting while making each consumer's policy visible at its boundary.
**Enforce invocation policy inside `ctx.skills.get()`.** Rejected because `get()` cannot know whether its caller is a model tool, a human command, or trusted orchestration. Filtering there would also make the both-disabled quadrant impossible to inspect or administer.
**Treat camel-case frontmatter as an alias.** Rejected because the external format is the kebab-case Claude skills contract and the repository has no released compatibility obligation. Failing loud avoids silently preserving a nonstandard spelling.
**Add a browser-side direct skill invocation RPC.** Rejected for this change because the existing browser flow inserts a model reference rather than a loaded instruction body. Its correct policy is therefore the intersection; a direct user-loading surface needs its own wire and logging design.
## Consequences
Providers and runtime registrations expose a small typed invocation contract, while local YAML remains extensible. Every new discovery consumer must consciously choose the model predicate, the user predicate, their intersection, or trusted unfiltered access; forgetting that choice is now review-visible rather than hidden in registry behavior.
The changed model catalog is pinned by the keyless ACP snapshot, which includes a model-only skill and excludes a user-only skill. The assembled keyless TUI snapshot discovers and loads a user-only skill by exact name, then rejects a model-only skill before loading its body; the real Loader/PTY smoke proves the same user-only path through the shipped terminal process. The real-host Chromium snapshot pins the browser intersection across all four policy quadrants. TUI unit coverage exercises those quadrants plus disposal races, while registry, local-parser, model-tool, and API-proxy tests cover defaults, supported boolean forms, malformed values, legacy-key rejection, exact-load enforcement, and the browser intersection.

View File

@@ -0,0 +1,50 @@
# Agent Note: 模型与用户彼此独立的 skill技能调用策略
Status: implemented
[English](2026-07-28-skill-invocation-policy.md) | 中文
## 问题
skill 注册表最初将发现操作视为模型目录:`ctx.skills.list()` 会移除禁止模型调用的 skill`ctx.skills.get()` 仍是不过滤内容的可信 loader。该设计足以支持由模型发起的加载却无法表示与 Claude 兼容的四类 skill仅向用户公开、仅向模型公开、同时向两者公开或者两者均不公开。TUI 从面向模型过滤后的列表中生成用户自动补全,并允许通过 `get()` 加载任意精确名称,这进一步放大了两类调用策略不匹配的问题。
本地解析器还将一种内部驼峰式拼写暴露为 frontmatter。若要支持既有的负向字段 `disable-model-invocation` 和正向字段 `user-invocable`,需要建立持久且对称的领域表示,同时避免把所有可能出现的 YAML 键都变成跨包的无类型契约。
## 决策
`SkillSummary` 包含一个必填且类型明确的 `invocation: SkillInvocationPolicy` 对象,其 `modelInvocable: boolean``userInvocable: boolean` 字段为正向且对称。只有显式输入 seam 可以省略它:未提供策略的运行时 `SkillRegistration`,以及两个调用键均未提供的本地 frontmatter都会在生成候选项或定义前解析为 `{ modelInvocable: true, userInvocable: true }`。未来的 frontmatter 键只有在具备消费方和执行契约后,才会进入领域模型;本地提供方仍将 frontmatter 解析为开放的 `Record<string, unknown>`,然后只把已识别字段及其默认值投影到规范化的类型化策略中。
`ctx.skills.list()` 返回所有胜出的摘要,不再替任何调用接口选择策略。`isModelInvocable(skill)``isUserInvocable(skill)` 分别直接读取对应的正向字段。`ctx.skills.get()` 保持策略无关,因为可信内部调用方可能需要任意定义;对外消费方则必须在展示或加载 skill 之前执行自身对应的判定函数。模型工具和 TUI 会在调用 `get()` 前检查与调用策略无关的摘要,随后再次检查已加载的定义:被拒绝的名称绝不会进入定义加载流程,发现与加载之间发生策略变更也无法暴露该 skill 的正文。
本地提供方只接受拼写完全一致的 kebab-case frontmatter 键 `disable-model-invocation``user-invocable`。它接受 YAML 布尔值,以及不区分大小写的 `true`/`false``yes`/`no``on`/`off``1`/`0`,与 Claude skills 实际支持的布尔写法一致。它将 `disable-model-invocation` 映射为相反的正向字段,即使两个键都不存在,也会根据默认值填充两个正向字段。若使用外部驼峰式拼写或提供非布尔调用值,发现流程会丢弃整个 skill并给出有针对性的警告本仓库尚处于发布前阶段因此不为磁盘格式保留兼容别名。调用数据校验遵循失败时默认拒绝原则因为忽略这类数据会默认授予权限可能使 skill 暴露在已禁用的接口上;与之不同,类型错误的可选 `whenToUse``metadata` 值会被省略,因为它们不参与调用判定。
面向模型的 `dsh-tool-skill` 目录和 loader 执行 `isModelInvocable`。TUI 的 `/skill:` 自动补全与精确名称 loader 在本地执行用户字段,因此仅允许用户调用的 skill 即使不出现在模型发现结果中,仍会在此处显示并可加载,同时不会将可选的 skill peer 变成运行时导入。由 launcher 预置、供引导式 `dsh migrate``dsh upgrade` 会话使用的初始 skill 沿用同一条 TUI 路径,因此必须保持允许用户调用。浏览器的 `skill.list` RPC 提供的是由用户选择、但仍要求模型加载的引用,因此只公开同时允许模型和用户调用的 skill本次改动不新增让浏览器直接加载 skill 的 RPC。
这些规则允许以下四种组合:
| 策略 | 模型侧接口 | 用户侧接口 |
|---|---|---|
| `{ modelInvocable: true, userInvocable: true }` | 包含 | 包含 |
| `{ modelInvocable: true, userInvocable: false }` | 包含 | 排除 |
| `{ modelInvocable: false, userInvocable: true }` | 排除 | 包含 |
| `{ modelInvocable: false, userInvocable: false }` | 排除 | 排除 |
该决策扩展了 [skill 系统](2026-07-05-skill-system.md),并取代 [TUI skill 斜杠命令](2026-07-21-tui-skill-slash-command.md)中记录的调用策略限制。
## 曾考虑的替代方案
**将所有 frontmatter 存入通用 `Map`,并在 `isModelInvocable` / `isUserInvocable` 中读取字符串键。** 不予采纳,因为拼写错误的键、非布尔值以及各消费方自行采用的类型转换都会越过包边界,且无法获得类型检查。解析器边界仍保持开放;领域模型则有意采用类型明确的窄接口。
**保持 `ctx.skills.list()` 仅返回允许模型调用的 skill并另增一份用户列表。** 不予采纳,因为发现、重复项解析、缓存和排序都是与调用接口无关的工作。采用一份完整目录和显式判定函数,可以避免这些机制逐渐分化,并在各消费方边界清楚呈现其策略。
**在 `ctx.skills.get()` 内执行调用策略。** 不予采纳,因为 `get()` 无法判断调用方是模型工具、人类命令还是可信编排逻辑。在此处过滤还会使两个接口均禁止调用的组合无法被检查或管理。
**将驼峰式 frontmatter 作为别名处理。** 不予采纳,因为外部格式遵循采用 kebab-case 的 Claude skills 契约,而本仓库尚未发布,无需承担兼容义务。快速失败可以避免暗中保留不符合标准的拼写。
**增加由浏览器端直接调用 skill 的 RPC。** 本次改动不予采纳,因为现有浏览器流程插入的是模型引用,而非已经加载的指令正文。因此,该流程应当取模型与用户调用策略的交集;直接由用户加载的接口需要单独设计协议与日志记录方式。
## 后果
提供方与运行时注册对外提供小而类型明确的调用契约,同时本地 YAML 仍可扩展。每个新的发现消费方都必须明确选择模型判定函数、用户判定函数、两者的交集,或可信且不过滤的访问方式;如果遗漏这项选择,评审时可以直接看出问题,而不会再被注册表行为掩盖。
无密钥 ACPAgent Client Protocol快照固定了模型目录的变更其中包含仅允许模型调用的 skill并排除仅允许用户调用的 skill。组装后的无密钥 TUI 快照按精确名称发现并加载一个仅允许用户调用的 skill随后在加载正文前拒绝一个仅允许模型调用的 skill真实 Loader/PTY 冒烟测试通过随产品交付的终端进程证明了同一条仅允许用户调用的路径。真实宿主上的 Chromium 快照固定了浏览器在全部四种策略组合下的交集行为。TUI 单元测试覆盖这些组合以及资源释放竞态;注册表、本地解析器、模型工具和 API 代理测试则覆盖默认值、支持的布尔写法、格式错误的值、旧键拒绝、精确名称加载时的策略执行,以及浏览器侧的策略交集。