소스 검색

docs(skill): document the user-explicit invocation path

Bilingual README updates for the four touched packages (ui-skill's claim
flow and deterministic-injection model experience, the apiproxy skills
domain, the shared renderSkillContent seam export, the catalog stitch
sentence), the implemented Agent Note triplet recording the decision and
its peer-product evidence, and the regenerated catalogs/graphs.
Yichen Jiang 2 달 전
부모
커밋
3584d8e088

+ 6 - 0
.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.i18n.yaml

@@ -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-08-08-user-explicit-skill-invocation.md
+2026-08-08-user-explicit-skill-invocation.md: 9249ee5c9c712e9c6aa827e97178f352728ed927
+2026-08-08-user-explicit-skill-invocation.zh.md: f15975c3b13fbf76e036fcece30253e78e7b417d

+ 36 - 0
.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.md

@@ -0,0 +1,36 @@
+# Agent Note: User-explicit skill invocation over skill.invoke
+
+Status: implemented
+
+English | [中文](2026-08-08-user-explicit-skill-invocation.zh.md)
+
+## Problem
+
+A `disable-model-invocation: true` skill is user-only by design: it never enters the model-facing catalog and the `skill` tool refuses to load it. Its only legitimate entry point is an explicit user gesture — yet the web client had none. `skill.list` filtered to the model-and-user intersection (hiding user-only skills from the menu), an entered `/name` line rode into the default prompt sink as plain text, and the model it reached was forbidden to load the skill — so it degraded to `read`-ing the SKILL.md file or ignoring the gesture (issue #1470). Even for ordinary skills, the decision-21 plain-text reference made user invocation a collaboration cue the model could ignore, not a guarantee.
+
+## Decision
+
+User-explicit invocation is a deterministic host-side injection, uniform for every user-invocable skill:
+
+- `skill.invoke { sessionId, name, text? }` (host apiproxy) enforces user-invocation policy at the operation boundary (`skill-not-found` / `skill-not-invocable`), renders the skill with the shared `renderSkillContent`, appends the optional trailing text after a blank line, and injects the whole as one user-role message carrying the new `skill-invocation` `MessageSource` kind (`{ name, args? }`) before starting a turn through the same route-served gate as `session.prompt`.
+- `renderSkillContent` moved from `dsh-tool-skill` to the `dsh-skill` seam: the `skill` tool result and the injection share one verbatim `<skill_content>` shape, and the catalog text gained the seam rule — an inline-injected skill must be followed, not re-loaded through the tool.
+- `skill.list` serves every user-invocable skill and carries `modelInvocable`, so the browser menu lists user-only skills with a marker (description prefix — the `hint` field is claim-state ghost text the menu never renders).
+- ui-skill claims a menu pick or an entered `/name [args]` into the invoke transaction (`matchEnter` strong-waits the catalog; unknown names stay plain prompts). The unreached legacy `<skill>name</skill>` reference codec is removed.
+- The transcript materializes the injection as a dedicated `skill-invocation` node from source metadata (never re-parsed from the body) and renders a right-aligned bubble: `/name` chip, trailing text, and the injected block collapsed behind a disclosure.
+
+Peer-product survey (Pi, OpenCode, Claude Code, Kimi Code, Codex, DeepSeek-Reasonix — local checkouts) was unanimous: user-explicit triggering is programmatic injection as a user-role message with zero model participation on every product, prompt-guided tool loading exists only on the model-autonomous track, and the disable-model-invocation equivalents gate only the model-side surfaces. Kimi's origin-metadata rendering and the Claude Code/Kimi no-reload prompt rule translate directly onto `MessageSource` and the catalog sentence.
+
+## Alternatives considered
+
+- **`agent.inject()` context injection** — no peer precedent; the gesture is a user turn, not an environment notice, and context-row presentation, compaction, and attribution all mismatch. Rejected.
+- **A host `/skill <name>` command** (command registry, plan-mode precedent) — two-token UX, no name completion, and user-only skills stay undiscoverable in the menu; the per-cwd skill catalog also fits the static command registry poorly. Rejected.
+- **Client-side expansion** (fetch body, splice into the prompt) — authorization becomes bypassable client courtesy, the log loses the invocation semantics, and Codex deleted its equivalent mechanism (custom prompts) in favor of core injection. Rejected.
+- **Host prompt-pipeline scanning for `/name`** (Codex `$name` core mentions) — duplicates the adjudication layer and risks swallowing literal slashes in prose; the claim path already covers the need. Rejected.
+- **Per-injection preamble line** (Kimi's `User activated the skill …`) — dropped in favor of a one-time catalog sentence: same context, paid once, and the injected block stays byte-identical with the tool result.
+
+## Consequences
+
+- Decision 21's plain-text reference path is superseded at submission: the draft still carries plain text and lexicon-derived chip visuals, but submit claims into a deterministic injection instead of shipping the literal and hoping. The model-autonomous track (catalog + `skill` tool) is unchanged.
+- Every user-invocable skill invocation now costs its full rendered body unconditionally — the price of determinism the peer survey showed everyone pays.
+- The `skill-invocation` source rides `user/message`, so Model-visible ⟺ logged holds with no new event type, and replay/UI read metadata rather than text markers.
+- TUI and ACP can adopt `skill.invoke` later for the same semantics; until then the TUI's client-side expansion remains its own path.

+ 36 - 0
.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.zh.md

@@ -0,0 +1,36 @@
+# Agent Note: 经 skill.invoke 的用户显式 skill 调用
+
+Status: implemented
+
+[English](2026-08-08-user-explicit-skill-invocation.md) | 中文
+
+## 问题
+
+`disable-model-invocation: true` 的 skill(技能)在设计上就是仅限用户的:它绝不进入面向模型的目录,`skill` 工具也拒绝加载它。它唯一正当的入口是一次显式的用户手势——而 web 客户端此前没有这个入口。`skill.list` 过滤到模型与用户的交集(把仅限用户的 skill 挡在菜单之外),回车提交的 `/name` 一行以纯文本落入默认提示词 sink,而这行文本到达的模型又被禁止加载该 skill——于是退化为模型去 `read` 那份 SKILL.md 文件,或者干脆无视这次手势(issue #1470)。即使对普通 skill,决策 21 的纯文本引用也让用户调用只是模型可以忽略的协作线索,而不是保证。
+
+## 决策
+
+用户显式调用是一次确定性的宿主侧注入,对每一个用户可调用的 skill 一致:
+
+- `skill.invoke { sessionId, name, text? }`(宿主 apiproxy)在操作边界强制执行用户调用策略(`skill-not-found`/`skill-not-invocable`),用共享的 `renderSkillContent` 渲染该 skill,在一个空行之后追加可选的尾随文本,并把整体作为一条携带新增 `skill-invocation` `MessageSource` kind(`{ name, args? }`)的 user 角色消息注入,随后经由与 `session.prompt` 相同的「路由是否有适配器在服务」闸门开启一个轮次。
+- `renderSkillContent` 从 `dsh-tool-skill` 移入 `dsh-skill` seam:`skill` 工具结果与注入共享同一份逐字一致的 `<skill_content>` 形态,目录文本则新增了这条 seam 规则——已内联注入的 skill 必须被遵循,而不是再经工具重新加载。
+- `skill.list` 提供每一个用户可调用的 skill 并携带 `modelInvocable`,因此浏览器菜单会带标记地列出仅限用户的 skill(描述前缀——`hint` 字段是认领态的 ghost text,菜单从不渲染它)。
+- ui-skill 把菜单 pick 或回车提交的 `/name [args]` 认领进 invoke 事务(`matchEnter` 强等目录;未知名称保持为普通提示词)。已不可达的旧 `<skill>name</skill>` 引用 codec 被移除。
+- transcript(文本记录)依据来源元数据把这次注入物化为专用的 `skill-invocation` 节点(绝不从正文重新解析),并渲染为一个右对齐气泡:`/name` chip、尾随文本,以及收在 disclosure 之后的注入块。
+
+同类产品调研(Pi、OpenCode、Claude Code、Kimi Code、Codex、DeepSeek-Reasonix——本地检出)结论一致:在每个产品上,用户显式触发都是以 user 角色消息做程序化注入、模型零参与;提示词引导的工具加载只存在于模型自主轨道上;disable-model-invocation 的对应物只把关模型侧表层。Kimi 的来源元数据渲染与 Claude Code/Kimi 的禁止重载提示词规则,可直接平移到 `MessageSource` 与目录那句话上。
+
+## 考虑过的替代方案
+
+- **`agent.inject()` 上下文注入**——没有同类产品先例;这次手势是一个用户轮次,不是环境通知,而且上下文行呈现、压缩(compaction)与归属全都不匹配。否决。
+- **宿主 `/skill <name>` 命令**(命令注册表,plan 模式先例)——两 token 的 UX、没有名称补全、仅限用户的 skill 在菜单里仍不可发现;按 cwd 的 skill 目录也与静态命令注册表格格不入。否决。
+- **客户端展开**(拉取正文、拼进提示词)——授权沦为可被绕过的客户端善意,日志失去调用语义,而且 Codex 已删除其等价机制(custom prompts)转向核心注入。否决。
+- **宿主提示词流水线扫描 `/name`**(Codex 的 `$name` core mentions)——重复了裁决层,还有吞掉普通行文中字面斜杠的风险;认领路径已经覆盖了这一需求。否决。
+- **每次注入一条前导语**(Kimi 的 `User activated the skill …`)——弃用,改为一次性的目录句子:同样的上下文、只支付一次,且注入块与工具结果保持逐字节一致。
+
+## 后果
+
+- 决策 21 的纯文本引用路径在提交处被取代:草稿仍承载纯文本与 lexicon 派生的 chip 视觉,但提交会认领进一次确定性注入,而不是把字面文本发出去再碰运气。模型自主轨道(目录 + `skill` 工具)不变。
+- 每一次用户可调用 skill 的调用现在都无条件付出其完整渲染正文的成本——这是确定性的代价,同类调研表明所有产品都在支付。
+- `skill-invocation` 来源搭乘 `user/message`,因此「模型可见 ⟺ 已记录」在不新增事件类型的情况下继续成立,回放与 UI 读取的是元数据而非文本标记。
+- TUI 与 ACP 之后可以为同样的语义采用 `skill.invoke`;在那之前,TUI 的客户端展开仍是它自己的路径。

+ 2 - 2
docs/config-catalog.md

@@ -1471,7 +1471,7 @@ export interface Config {
 }
 }
 ```
 ```
 
 
-Source: [`packages/skill/skill/src/index.ts:170`](../packages/skill/skill/src/index.ts)
+Source: [`packages/skill/skill/src/index.ts:261`](../packages/skill/skill/src/index.ts)
 
 
 ## `@deepseek-ai/dsh-skill-local`
 ## `@deepseek-ai/dsh-skill-local`
 
 
@@ -2063,7 +2063,7 @@ export interface Config {
 }
 }
 ```
 ```
 
 
-Source: [`packages/skill/tool-skill/src/index.ts:58`](../packages/skill/tool-skill/src/index.ts)
+Source: [`packages/skill/tool-skill/src/index.ts:59`](../packages/skill/tool-skill/src/index.ts)
 
 
 ## `@deepseek-ai/dsh-tool-str-replace-editor`
 ## `@deepseek-ai/dsh-tool-str-replace-editor`
 
 

+ 1 - 1
docs/cordis-catalog/events.md

@@ -677,7 +677,7 @@ A skill provider, runtime contribution, or provider-backed catalog may have chan
 'skills/change'(): void
 'skills/change'(): void
 ```
 ```
 
 
-Source: [`packages/skill/skill/src/index.ts:188`](../../packages/skill/skill/src/index.ts)
+Source: [`packages/skill/skill/src/index.ts:279`](../../packages/skill/skill/src/index.ts)
 
 
 ## `subagent/*`
 ## `subagent/*`
 
 

+ 1 - 1
docs/cordis-catalog/services.md

@@ -1946,7 +1946,7 @@ async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefiniti
 
 
 Types: [SkillCatalogSnapshot](../core-data-structures/skills.md) · [SkillDefinition](../core-data-structures/skills.md) · [SkillLookupOptions](../core-data-structures/skills.md) · [SkillProvider](../core-data-structures/skills.md) · [SkillProviderControl](../core-data-structures/skills.md) · [SkillRegistration](../core-data-structures/skills.md) · [SkillSummary](../core-data-structures/skills.md)
 Types: [SkillCatalogSnapshot](../core-data-structures/skills.md) · [SkillDefinition](../core-data-structures/skills.md) · [SkillLookupOptions](../core-data-structures/skills.md) · [SkillProvider](../core-data-structures/skills.md) · [SkillProviderControl](../core-data-structures/skills.md) · [SkillRegistration](../core-data-structures/skills.md) · [SkillSummary](../core-data-structures/skills.md)
 
 
-Source: [`packages/skill/skill/src/index.ts:209`](../../packages/skill/skill/src/index.ts)
+Source: [`packages/skill/skill/src/index.ts:300`](../../packages/skill/skill/src/index.ts)
 
 
 ## `ctx.spillStore` — `SpillStore` (abstract seam)
 ## `ctx.spillStore` — `SpillStore` (abstract seam)
 
 

+ 1 - 1
docs/event-producer-consumer.md

@@ -36,7 +36,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry) |
 | `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
 | `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
 | `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
 | `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
-| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:188`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
+| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:279`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
 | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:160`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) |
 | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:160`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) |
 | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:134`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:134`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |

+ 2 - 2
packages/client/ui-skill/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-skill/README.md
 #   pnpm run verify-translation-pairing --write packages/client/ui-skill/README.md
-README.md: f70bd2780f255cd8e0c64acb3da3863e10c4fa9d
-README.zh.md: 6eb6cbd3ae196a540e161a3a23f9df2136824f2e
+README.md: c888622bc92b038413c7d0ebf63abb61b483f6f5
+README.zh.md: 3bbbc90186726356c53f375bb664d678c4926988

+ 8 - 7
packages/client/ui-skill/README.md

@@ -2,7 +2,9 @@
 
 
 English | [中文](README.zh.md)
 English | [中文](README.zh.md)
 
 
-Skill reference source, browser half: registers the `/`-trigger `skill` source into `ctx.slash`. Ordinary-session candidates come from the `skill.list` RPC addressed by the per-call `ClientSessionContext` projection's `{sessionId}`, with the host resolving `cwd` from the session header. The host returns the intersection of model-invocable and user-invocable skills because this browser path inserts a model reference rather than loading the body directly. Catalog-addressed continuable children resolve no skill candidates locally because the existing skill RPC requires an attached session; viewing their persisted history must not activate them. Catalogs cache per ordinary session with a single-flight fetch; the scope-birth `warm` hook prewarms the session's entry and `connection/reset` clears everything. Results filter by `startsWith(query)`; picking a candidate lands the literal `/name ` text through the slash pipeline (decision 21 plain-text reference), and the source `codec` owns the reference's two projections: `clipboardText` → `/name`, `serialize` → the model form `<skill>name</skill>` invoked at submit time. The RPC rides the plugin's root-context connection captured at registration — the source never reads services off a per-call argument. The source implements no `matchSpace`/`matchEnter` hooks — skill references never enter command adjudication and ride ordinary prompts into the default sink.
+Skill invocation source, browser half: registers the `/`-trigger `skill` source into `ctx.slash`. Ordinary-session candidates come from the `skill.list` RPC addressed by the per-call `ClientSessionContext` projection's `{sessionId}`, with the host resolving `cwd` from the session header. The host serves every user-invocable skill; a `modelInvocable: false` entry (a `disable-model-invocation` skill, whose only entry point is this path) wears the user-only marker as a description prefix in the active language. Catalog-addressed continuable children resolve no skill candidates locally because the existing skill RPC requires an attached session; viewing their persisted history must not activate them. Catalogs cache per ordinary session with a single-flight fetch; the scope-birth `warm` hook prewarms the session's entry and `connection/reset` clears everything. Results filter by `startsWith(query)`.
+
+A menu pick or an entered `/name [args]` line claims the composer into an args-tolerant `skill.invoke` transaction (`matchEnter` strong-waits the catalog; an unknown name answers undefined and stays a plain prompt). Submit trims the args, keeps blank args off the wire, and folds an RPC refusal into the composer's error outcome; the host renders the skill body and injects it as a user message before starting the turn, so invocation is deterministic for every user-invocable skill. The RPC rides the plugin's root-context connection captured at registration — the source never reads services off a per-call argument. Draft chip visuals still derive from the `lexicon` scan; the legacy `<skill>name</skill>` reference codec is gone (decision 21 removal cut) and `matchSpace` stays unimplemented — menu and enter own the skill flows.
 
 
 A failed `skill.list` throws from `candidates`, which the slash shell logs and folds into a silent menu-group drop — the menu shows only pending/ready states.
 A failed `skill.list` throws from `candidates`, which the slash shell logs and folds into a silent menu-group drop — the menu shows only pending/ready states.
 
 
@@ -14,23 +16,22 @@ The browser plugin also registers a keyed `skill` toolview in `conversation.chat
 
 
 ## Model Experience
 ## Model Experience
 
 
-### Skill reference text in the user prompt
+### User-explicit skill invocation
 
 
 #### What the model sees
 #### What the model sees
 
 
-A picked candidate lands the literal `/name ` in the draft (decision 21: plain text, no `<skill>` tag); the text reaches the model verbatim inside the ordinary user message (`session.prompt`), with no dedicated content block, prompt section, or host-side expansion. The association with the actual skill is model-side and non-deterministic: the session prefix already carries the skill catalog (rendered by `dsh-tool-skill`), and the reference's name matching a catalog entry is what invites the model to load it.
+A claimed invocation never ships the `/name` literal. The host (`skill.invoke`) renders the canonical `<skill_content>` block — the same `renderSkillContent` output the `skill` tool returns — appends the user's trailing text after a blank line, and injects the whole as one user-role message carrying the `skill-invocation` source, immediately starting a turn. Loading is deterministic: the model receives the full body without being asked to call the `skill` tool, and the catalog (rendered by `dsh-tool-skill`) tells it not to re-load an inline-injected skill.
 
 
 #### Token effect
 #### Token effect
 
 
-Conditional and tiny: only a pick (or hand-typing the same text) adds the reference's characters to that one user message. Menu browsing and the candidate fetch add zero model tokens.
+One invocation adds the rendered skill body plus the trailing text to that turn's user message — the same cost as the model loading the skill through the tool, paid unconditionally instead of at the model's discretion. Menu browsing and the candidate fetch add zero model tokens.
 
 
 #### KV Cache effect
 #### KV Cache effect
 
 
-Append-only: the reference is part of a new user message appended after the reusable history prefix. This package never edits earlier request tokens.
+Append-only: the injected message lands after the reusable history prefix. This package never edits earlier request tokens.
 
 
 ## Known Limitations and Deferred Work
 ## Known Limitations and Deferred Work
 
 
 - **Result-only history pages use the generic row** — keyed dispatch needs the paired call in the runtime window; pagination that leaves the call outside has no tool identity. This client presentation feature does not extend the history wire contract to recover it.
 - **Result-only history pages use the generic row** — keyed dispatch needs the paired call in the runtime window; pagination that leaves the call outside has no tool identity. This client presentation feature does not extend the history wire contract to recover it.
-- **Non-deterministic skill loading** — the reference is a collaboration cue, not a guarantee; the model may ignore it. The rework path when hit rate proves insufficient (a host-side `context/skill-reference` guidance package, or full-text injection) sits in the design ledger; the wire text shape would not change.
-- **First keystroke may race the prewarm** — the scope-birth warm launches the catalog fetch, but a menu opened before it settles shows no skill candidates for that keystroke. Accepted by design: skill references do not participate in enter adjudication, so nothing correctness-bearing waits on the catalog.
+- **Enter waits on the catalog once** — `matchEnter` strong-waits the session's first catalog fetch before answering, so an enter racing a cold cache resolves against the settled catalog rather than silently missing. A menu opened before the prewarm settles still shows no skill candidates for that keystroke.
 - **Text is the truth** — the reference is plain draft text; a hand-typed identical token is the same reference. Chip visuals derive from the lexicon scan; no occurrence identity or position tracking (componentized chips are a ledger item).
 - **Text is the truth** — the reference is plain draft text; a hand-typed identical token is the same reference. Chip visuals derive from the lexicon scan; no occurrence identity or position tracking (componentized chips are a ledger item).

+ 8 - 7
packages/client/ui-skill/README.zh.md

@@ -2,7 +2,9 @@
 
 
 [English](README.md) | 中文
 [English](README.md) | 中文
 
 
-skill(技能)引用 source 的浏览器端:把 `/` 触发的 `skill` source 注册进 `ctx.slash`。普通会话的候选来自 `skill.list` RPC,以每次调用的 `ClientSessionContext` 投影中的 `{sessionId}` 寻址,host 从会话 header 解析 `cwd`。宿主返回模型可调用与用户可调用 skill 的交集,因为该浏览器路径插入的是模型引用,而不是直接加载正文。由目录寻址的可继续 subagent 在客户端解析为没有 skill 候选,因为现有 skill RPC 要求会话已挂载;查看其持久化历史不得激活它。目录按普通会话缓存,拉取走 single-flight;scope 创建时的 `warm` 钩子预热该会话的缓存项,`connection/reset` 清空全部缓存。结果按 `startsWith(query)` 过滤;pick 一个候选会把字面文本 `/name ` 经 slash 流水线落进草稿(决策 21 的纯文本引用),source 的 `codec` 拥有该引用的两种投影:`clipboardText` → `/name`,`serialize` → 提交时生成的模型形式 `<skill>name</skill>`。RPC 使用插件注册时捕获的根上下文连接——source 绝不从每次调用的参数上读取服务。source 不实现 `matchSpace`/`matchEnter` 钩子——skill 引用永不进入命令裁决,随普通提示词落入 default sink。
+skill(技能)调用 source 的浏览器端:把 `/` 触发的 `skill` source 注册进 `ctx.slash`。普通会话的候选来自 `skill.list` RPC,以每次调用的 `ClientSessionContext` 投影中的 `{sessionId}` 寻址,host 从会话 header 解析 `cwd`。宿主提供每一个用户可调用的 skill;`modelInvocable: false` 的条目(即 `disable-model-invocation` skill,此路径是其唯一入口)会以当前语言把仅限用户标记作为描述前缀带上。由目录寻址的可继续 subagent 在客户端解析为没有 skill 候选,因为现有 skill RPC 要求会话已挂载;查看其持久化历史不得激活它。目录按普通会话缓存,拉取走 single-flight;scope 创建时的 `warm` 钩子预热该会话的缓存项,`connection/reset` 清空全部缓存。结果按 `startsWith(query)` 过滤。
+
+菜单 pick 或回车提交的一行 `/name [args]` 会把 composer 认领进一个容忍参数的 `skill.invoke` 事务(`matchEnter` 强等目录;未知名称应答 undefined,保持为普通提示词)。提交时会修剪参数、让空白参数不上协议,并把 RPC 拒绝折叠进 composer 的错误结局;宿主在开启轮次之前渲染 skill 正文并将其作为用户消息注入,因此对每一个用户可调用的 skill,调用都是确定性的。RPC 使用插件注册时捕获的根上下文连接——source 绝不从每次调用的参数上读取服务。草稿 chip 视觉仍由 `lexicon` 扫描派生;旧的 `<skill>name</skill>` 引用 codec 已经移除(决策 21 的移除裁定),`matchSpace` 保持不实现——skill 流程归菜单与回车所有。
 
 
 `skill.list` 失败时 `candidates` 抛出异常,slash 壳层记录日志并折叠为静默的菜单组丢弃——菜单只显示 pending/ready 状态。
 `skill.list` 失败时 `candidates` 抛出异常,slash 壳层记录日志并折叠为静默的菜单组丢弃——菜单只显示 pending/ready 状态。
 
 
@@ -14,23 +16,22 @@ skill(技能)引用 source 的浏览器端:把 `/` 触发的 `skill` sourc
 
 
 ## 模型体验
 ## 模型体验
 
 
-### 用户提示词中的 skill 引用文本
+### 用户显式 skill 调用
 
 
 #### 模型看到的内容
 #### 模型看到的内容
 
 
-被 pick 的候选会把字面文本 `/name ` 落进草稿(决策 21:纯文本,无 `<skill>` 标签);该文本原样进入普通用户消息(`session.prompt`)到达模型,没有专用内容块、提示词 section 或 host 侧展开。与实际 skill 的关联在模型侧建立且具有非确定性:会话前缀已携带 skill 目录(由 `dsh-tool-skill` 渲染),引用名称与目录条目匹配,正是这一点引导模型去加载它。
+被认领的调用绝不会把字面文本 `/name` 发出去。宿主(`skill.invoke`)渲染规范的 `<skill_content>` 块——与 `skill` 工具返回的 `renderSkillContent` 输出相同——在一个空行之后追加用户的尾随文本,并把整体作为一条携带 `skill-invocation` 来源的 user 角色消息注入,随即开启一个轮次。加载是确定性的:模型无需被要求调用 `skill` 工具就能收到完整正文,目录(由 `dsh-tool-skill` 渲染)也会告诉它不要重新加载已内联注入的 skill。
 
 
 #### Token 影响
 #### Token 影响
 
 
-有条件且极小:只有 pick(或手动键入相同文本)会把引用的字符加进那一条用户消息。浏览菜单和拉取候选不会增加任何模型 token。
+一次调用会把渲染后的 skill 正文连同尾随文本加进该轮次的用户消息——成本与模型经由工具加载该 skill 相同,只是无条件支付,而非由模型自行裁量。浏览菜单和拉取候选不会增加任何模型 token。
 
 
 #### KV Cache 影响
 #### KV Cache 影响
 
 
-仅追加:引用是追加在可复用历史前缀之后的新用户消息的一部分。该包绝不改写较早的请求 token。
+仅追加:注入的消息落在可复用历史前缀之后。该包绝不改写较早的请求 token。
 
 
 ## 已知限制与暂缓事项
 ## 已知限制与暂缓事项
 
 
 - **仅含结果的 history 页使用通用行**:键控分派要求配对调用位于 runtime 窗口内;分页将调用留在窗口外时,结果没有工具身份。这项客户端呈现功能不会为了恢复该身份而扩展 history 协议契约。
 - **仅含结果的 history 页使用通用行**:键控分派要求配对调用位于 runtime 窗口内;分页将调用留在窗口外时,结果没有工具身份。这项客户端呈现功能不会为了恢复该身份而扩展 history 协议契约。
-- **skill 加载具有非确定性**:引用是协作线索,不是保证;模型可能忽略它。针对命中率不足情况的返工路径(host 侧 `context/skill-reference` 引导包,或全文注入)记录在设计台账中;协议中的文本形态不会改变。
-- **首次击键可能与预热竞速**:scope 创建时的预热会启动目录拉取,但目录落定之前打开的菜单,在那次击键下不会显示 skill 候选。这是设计上接受的取舍:skill 引用不参与回车裁决,因此没有任何攸关正确性的环节等待目录。
+- **回车对目录只等待一次**:`matchEnter` 在应答之前强等该会话的首次目录拉取,因此与冷缓存竞速的回车会对照已落定的目录解析,而不是静默错过。预热落定之前打开的菜单,在那次击键下仍不会显示 skill 候选。
 - **文本是唯一依据**:引用是普通的草稿文本;手动键入的相同 token 就是同一个引用。chip 视觉由 lexicon 扫描派生;没有 occurrence 身份或位置跟踪(组件化 chip 是台账事项)。
 - **文本是唯一依据**:引用是普通的草稿文本;手动键入的相同 token 就是同一个引用。chip 视觉由 lexicon 扫描派生;没有 occurrence 身份或位置跟踪(组件化 chip 是台账事项)。

+ 2 - 2
packages/host/apiproxy/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md
 #   pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md
-README.md: 7ac7bdc6db2e2abbc60d1a8813e229c21ed39fe7
-README.zh.md: d6ece5caed752cf0cc59cc97017549ec2b1e66cb
+README.md: 8d7a24b0b8b897d94ed29d5dc9ed6e9efb250fc6
+README.zh.md: c988b7540ba719d02e50d6da9595353c93766835

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
packages/host/apiproxy/README.md


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
packages/host/apiproxy/README.zh.md


+ 2 - 2
packages/skill/skill/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/skill/skill/README.md
 #   pnpm run verify-translation-pairing --write packages/skill/skill/README.md
-README.md: f538ae668ccff291be86348627d5547150f460df
-README.zh.md: d61a242d01df1e22270c1cb049b922536654bbd6
+README.md: 0c1b2249d8c46ad9ce8097ceeda2bd988c92eb21
+README.zh.md: 8fed350d00433206aecdb32819adc81c82745869

+ 4 - 0
packages/skill/skill/README.md

@@ -37,6 +37,10 @@ This package owns the `ctx.skills` interface. It does not know whether skills co
 | `{ modelInvocable: false, userInvocable: true }` | excluded | included |
 | `{ modelInvocable: false, userInvocable: true }` | excluded | included |
 | `{ modelInvocable: false, userInvocable: false }` | excluded | excluded |
 | `{ modelInvocable: false, userInvocable: false }` | excluded | excluded |
 
 
+### Shared model-facing rendering
+
+`renderSkillContent(skill)` renders one loaded skill as the canonical `<skill_content>` block (escaped `name` attribute, resource hints, verbatim body). It is the single truth for both loading paths: `dsh-tool-skill` returns it as the `skill` tool result, and the host's user-explicit `skill.invoke` injects it as a user message, so the model sees one shape regardless of who initiated the load. `escapeText` is exported beside it for consumers embedding prose in the same markup frame. The package also declares the `skill-invocation` `MessageSource` kind ({ name, args? }) that user-explicit injection stamps on its messages — transcript consumers present the invocation from this metadata instead of re-parsing the body.
+
 `isModelInvocable(skill)` and `isUserInvocable(skill)` read the matching positive field directly. `ctx.skills.get()` remains the trusted, policy-neutral loading primitive, so every user- or model-facing consumer must enforce the predicate that matches its surface before exposing or loading a skill.
 `isModelInvocable(skill)` and `isUserInvocable(skill)` read the matching positive field directly. `ctx.skills.get()` remains the trusted, policy-neutral loading primitive, so every user- or model-facing consumer must enforce the predicate that matches its surface before exposing or loading a skill.
 
 
 ## Provider Contract
 ## Provider Contract

+ 4 - 0
packages/skill/skill/README.zh.md

@@ -37,6 +37,10 @@
 | `{ modelInvocable: false, userInvocable: true }` | 排除 | 包含 |
 | `{ modelInvocable: false, userInvocable: true }` | 排除 | 包含 |
 | `{ modelInvocable: false, userInvocable: false }` | 排除 | 排除 |
 | `{ modelInvocable: false, userInvocable: false }` | 排除 | 排除 |
 
 
+### 共享的面向模型渲染
+
+`renderSkillContent(skill)` 把一个已加载 skill 渲染为规范的 `<skill_content>` 块(转义后的 `name` 属性、资源提示、原样正文)。它是两条加载路径的唯一真源:`dsh-tool-skill` 将其作为 `skill` 工具结果返回,宿主的用户显式 `skill.invoke` 将其作为用户消息注入,因此无论加载由谁发起,模型看到的都是同一种形态。`escapeText` 随之一并导出,供要在同一标记框架中嵌入文案的消费方使用。该包还声明 `skill-invocation` 这个 `MessageSource` kind({ name, args? }),用户显式注入会把它打在自己的消息上——transcript(文本记录)消费方依据这份元数据呈现该次调用,而不是重新解析正文。
+
 `isModelInvocable(skill)` 和 `isUserInvocable(skill)` 分别直接读取对应的正向字段。`ctx.skills.get()` 仍是受信且与策略无关的加载原语,因此每个面向用户或模型的消费方都必须先执行与自身接口匹配的判定,再暴露或加载 skill。
 `isModelInvocable(skill)` 和 `isUserInvocable(skill)` 分别直接读取对应的正向字段。`ctx.skills.get()` 仍是受信且与策略无关的加载原语,因此每个面向用户或模型的消费方都必须先执行与自身接口匹配的判定,再暴露或加载 skill。
 
 
 ## 提供方契约
 ## 提供方契约

+ 2 - 2
packages/skill/tool-skill/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/skill/tool-skill/README.md
 #   pnpm run verify-translation-pairing --write packages/skill/tool-skill/README.md
-README.md: 8e0bff5d1c4853092d412b8f7f9528d4b00d9626
-README.zh.md: c6b815bef59eb1f14be0892078694f129366d004
+README.md: 5c6e592c670f324eb660dbe1fec168fd77e5b368
+README.zh.md: 202a621b1d4047c7d763de3b98c1a69c8c1ee1f7

+ 2 - 1
packages/skill/tool-skill/README.md

@@ -36,7 +36,7 @@ Tool execution does not add a synthetic context message. Its freshly loaded resu
 
 
 #### What the model sees
 #### What the model sees
 
 
-If model-invocable skills exist and this exact `skill` tool is visible, the agent receives the catalog template below as a durable user-role message before the first request, with one data-dependent entry per sorted skill. Later membership, description, or visibility changes append a complete replacement using the same `<available_skills>` envelope; deleting every skill appends an empty envelope with an explicit instruction not to use older names.
+If model-invocable skills exist and this exact `skill` tool is visible, the agent receives the catalog template below as a durable user-role message before the first request, with one data-dependent entry per sorted skill. Later membership, description, or visibility changes append a complete replacement using the same `<available_skills>` envelope; deleting every skill appends an empty envelope with an explicit instruction not to use older names. The template's closing sentence is the seam rule against double-loading: the host's user-explicit `skill.invoke` injects the same `renderSkillContent` output (shared from `@deepseek-ai/dsh-skill`) inline, and the catalog tells the model to follow that block instead of re-loading the skill through the tool; the replacement-catalog template carries the same sentence.
 
 
 ##### Skill catalog template
 ##### Skill catalog template
 
 
@@ -49,6 +49,7 @@ A skill is a reusable set of task-specific instructions. The following skills ar
 </available_skills>
 </available_skills>
 
 
 If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.
 If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.
+A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.
 </system-reminder>
 </system-reminder>
 ```
 ```
 
 

+ 2 - 1
packages/skill/tool-skill/README.zh.md

@@ -36,7 +36,7 @@
 
 
 #### 模型看到的内容
 #### 模型看到的内容
 
 
-如果存在模型可调用 skill,且可见的正是这个 `skill` 工具,agent 会在第一个请求之前收到下方目录模板,其中包含每个已排序 skill 的一条随数据而定的条目。该目录是一条持久的用户角色消息。后续成员关系、描述或可见性的变化会使用同一个 `<available_skills>` 信封追加完整替换;删除所有 skill 时,会追加一个空信封,并明确指示不得使用旧名称。
+如果存在模型可调用 skill,且可见的正是这个 `skill` 工具,agent 会在第一个请求之前收到下方目录模板,其中包含每个已排序 skill 的一条随数据而定的条目。该目录是一条持久的用户角色消息。后续成员关系、描述或可见性的变化会使用同一个 `<available_skills>` 信封追加完整替换;删除所有 skill 时,会追加一个空信封,并明确指示不得使用旧名称。模板的结尾一句是防止双重加载的 seam 规则:宿主的用户显式 `skill.invoke` 会把同一份 `renderSkillContent` 输出(共享自 `@deepseek-ai/dsh-skill`)内联注入,目录则告诉模型遵循该块,而不是再经工具重新加载该 skill;替换目录模板携带同一句话。
 
 
 ##### Skill 目录模板
 ##### Skill 目录模板
 
 
@@ -49,6 +49,7 @@ A skill is a reusable set of task-specific instructions. The following skills ar
 </available_skills>
 </available_skills>
 
 
 If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.
 If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.
+A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.
 </system-reminder>
 </system-reminder>
 ```
 ```
 
 

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.