_Kerman 4 недель назад
Родитель
Сommit
999b677e18
100 измененных файлов с 863 добавлено и 212 удалено
  1. 2 2
      .agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.i18n.yaml
  2. 3 2
      .agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md
  3. 3 2
      .agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.zh.md
  4. 6 0
      .agents/notes/implemented/process/2026-09-08-comment-only-review-routing.i18n.yaml
  5. 41 0
      .agents/notes/implemented/process/2026-09-08-comment-only-review-routing.md
  6. 41 0
      .agents/notes/implemented/process/2026-09-08-comment-only-review-routing.zh.md
  7. 2 2
      .agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.i18n.yaml
  8. 9 9
      .agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md
  9. 9 9
      .agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.zh.md
  10. 2 2
      .agents/notes/proposed/process/2026-08-20-audience-first-documentation-quality.i18n.yaml
  11. 6 5
      .agents/notes/proposed/process/2026-08-20-audience-first-documentation-quality.md
  12. 6 5
      .agents/notes/proposed/process/2026-08-20-audience-first-documentation-quality.zh.md
  13. 2 2
      .agents/skills/dsh-doc/SKILL.md
  14. 2 2
      .agents/skills/dsh-doc/references/review.md
  15. 1 1
      .agents/skills/dsh-doc/references/structure-hierarchy.md
  16. 1 1
      .agents/skills/dsh-doc/references/style.md
  17. 1 1
      .agents/skills/dsh-doc/templates/package-bundle.md
  18. 1 1
      .agents/skills/dsh-doc/templates/package-group.md
  19. 1 1
      .agents/skills/dsh-doc/templates/package-library.md
  20. 1 1
      .agents/skills/dsh-doc/templates/package-reference.md
  21. 2 2
      .github/review-ownership/CODEOWNERS
  22. 2 2
      .github/review-ownership/README.i18n.yaml
  23. 13 7
      .github/review-ownership/README.md
  24. 13 7
      .github/review-ownership/README.zh.md
  25. 258 22
      .github/review-ownership/request-review.mjs
  26. 201 26
      .github/review-ownership/request-review.test.mjs
  27. 1 2
      .github/workflows/request-review.yml
  28. 24 2
      apps/web/tests/lifecycle-chrome.e2e.ts
  29. 1 0
      package.json
  30. 2 2
      packages/acp/acp/README.i18n.yaml
  31. 1 1
      packages/acp/acp/README.md
  32. 1 1
      packages/acp/acp/README.zh.md
  33. 2 2
      packages/api/workspace-files/README.i18n.yaml
  34. 1 1
      packages/api/workspace-files/README.md
  35. 1 1
      packages/api/workspace-files/README.zh.md
  36. 2 2
      packages/attachment/attachment-local/README.i18n.yaml
  37. 1 1
      packages/attachment/attachment-local/README.md
  38. 1 1
      packages/attachment/attachment-local/README.zh.md
  39. 2 2
      packages/attachment/attachment/README.i18n.yaml
  40. 1 1
      packages/attachment/attachment/README.md
  41. 1 1
      packages/attachment/attachment/README.zh.md
  42. 2 2
      packages/boot/cmdline/README.i18n.yaml
  43. 1 1
      packages/boot/cmdline/README.md
  44. 1 1
      packages/boot/cmdline/README.zh.md
  45. 2 2
      packages/bundle/web-app/README.i18n.yaml
  46. 1 1
      packages/bundle/web-app/README.md
  47. 1 1
      packages/bundle/web-app/README.zh.md
  48. 2 2
      packages/client/README.i18n.yaml
  49. 1 1
      packages/client/README.md
  50. 1 1
      packages/client/README.zh.md
  51. 2 2
      packages/client/locale/README.i18n.yaml
  52. 1 1
      packages/client/locale/README.md
  53. 1 1
      packages/client/locale/README.zh.md
  54. 2 2
      packages/client/resources/README.i18n.yaml
  55. 1 1
      packages/client/resources/README.md
  56. 1 1
      packages/client/resources/README.zh.md
  57. 2 2
      packages/client/ui-agent-preset/README.i18n.yaml
  58. 1 1
      packages/client/ui-agent-preset/README.md
  59. 1 1
      packages/client/ui-agent-preset/README.zh.md
  60. 2 2
      packages/client/ui-brand-official/README.i18n.yaml
  61. 1 1
      packages/client/ui-brand-official/README.md
  62. 1 1
      packages/client/ui-brand-official/README.zh.md
  63. 2 2
      packages/client/ui-chat/README.i18n.yaml
  64. 1 1
      packages/client/ui-chat/README.md
  65. 1 1
      packages/client/ui-chat/README.zh.md
  66. 2 2
      packages/client/ui-commands/src/client/contract.ts
  67. 12 0
      packages/client/ui-commands/src/client/locales.ts
  68. 23 2
      packages/client/ui-commands/src/client/service.ts
  69. 33 2
      packages/client/ui-commands/tests/service.client.spec.ts
  70. 2 2
      packages/client/ui-goal/README.i18n.yaml
  71. 1 1
      packages/client/ui-goal/README.md
  72. 1 1
      packages/client/ui-goal/README.zh.md
  73. 2 2
      packages/client/ui-input-trigger/README.i18n.yaml
  74. 1 1
      packages/client/ui-input-trigger/README.md
  75. 1 1
      packages/client/ui-input-trigger/README.zh.md
  76. 10 0
      packages/client/ui-input-trigger/src/client/controller.ts
  77. 4 0
      packages/client/ui-input-trigger/src/client/service.ts
  78. 38 0
      packages/client/ui-input-trigger/tests/service.client.spec.ts
  79. 2 2
      packages/client/ui-model-selection/README.i18n.yaml
  80. 1 1
      packages/client/ui-model-selection/README.md
  81. 1 1
      packages/client/ui-model-selection/README.zh.md
  82. 2 4
      packages/client/ui-model-selection/src/client/index.ts
  83. 2 2
      packages/client/ui-permission-presets/README.i18n.yaml
  84. 1 1
      packages/client/ui-permission-presets/README.md
  85. 1 1
      packages/client/ui-permission-presets/README.zh.md
  86. 2 2
      packages/client/ui-primitives/README.i18n.yaml
  87. 1 1
      packages/client/ui-primitives/README.md
  88. 1 1
      packages/client/ui-primitives/README.zh.md
  89. 2 2
      packages/client/ui-reference/README.i18n.yaml
  90. 1 1
      packages/client/ui-reference/README.md
  91. 1 1
      packages/client/ui-reference/README.zh.md
  92. 2 2
      packages/client/ui-settings-general/README.i18n.yaml
  93. 1 1
      packages/client/ui-settings-general/README.md
  94. 1 1
      packages/client/ui-settings-general/README.zh.md
  95. 2 2
      packages/client/ui-settings-plugin-inventory/README.i18n.yaml
  96. 1 1
      packages/client/ui-settings-plugin-inventory/README.md
  97. 1 1
      packages/client/ui-settings-plugin-inventory/README.zh.md
  98. 2 2
      packages/client/ui-settings-plugins/README.i18n.yaml
  99. 1 1
      packages/client/ui-settings-plugins/README.md
  100. 1 1
      packages/client/ui-settings-plugins/README.zh.md

+ 2 - 2
.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.i18n.yaml

@@ -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/process/2026-07-04-doc-tiers-and-budgets.md
-2026-07-04-doc-tiers-and-budgets.md: 378da8f8fddafa32dc7450bfac1c5376f2c7a065
-2026-07-04-doc-tiers-and-budgets.zh.md: 1d92ed7fbbec8a9a15bf94a2d320ee88f65a9fa8
+2026-07-04-doc-tiers-and-budgets.md: 209504218d18e97ae6da65bed9a22da40d2a7681
+2026-07-04-doc-tiers-and-budgets.zh.md: 9c866424d84b4fefa5ffe95efa21a3cf7d3c321a

+ 3 - 2
.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md

@@ -13,14 +13,14 @@ Standing docs accumulated repeated rules, retold incidents, duplicated package m
 - **Structure follows the documentation tree.** [docs/AGENTS.md](../../../../docs/AGENTS.md) is the documentation standard: a document owns detail about its subject, summarizes only the purpose, responsibility, and high-level behavior of direct children, and links to deeper owners. [Agent Notes](../../README.md) remain outside this structural contract. Every human-facing document is a tutorial with an ordered outcome or a reference with an explicit lookup scope; a [postmortem](../../../../docs/postmortem/README.md) is an incident-scoped reference whose chronology records evidence. Tutorials introduce concepts in prerequisite order for the reader's starting knowledge.
 - **A tier taxonomy with one home per fact.** The standard assigns every Markdown tier one job, forbids restating a fact outside its home tier, and carries the slop checklist used when writing or reviewing any doc.
 - **One product onboarding path.** The root README owns the recommended package-run path, the source-run alternative, and compact `dsh plugin --profile` usage. The published user guide starts with tasks inside the running Web UI, then links to distinct tutorials or reference owners for other interfaces, plugin development, and advanced configuration instead of repeating Web startup.
-- **A narrow, hard budget gate.** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) joins `doc-sync`: every doc listed in [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) must stay under its word ceiling (`wc -w` semantics, whole file), and a budgeted file that is missing fails the gate so a rename cannot silently orphan its budget. Scope is deliberately only the accretion-prone standing docs — the root and subtree `AGENTS.md` files, `architecture.md`, `packages/README.md`, and the standing policy docs they evict content into (`docs/testing.md`, `docs/defensive-patterns.md`). Reference docs, Agent Notes, and package READMEs are unbudgeted: length is legitimate there when every row is a fact, and review plus the slop checklist govern them.
+- **Narrow, hard budget gates.** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) joins `doc-sync`: every doc listed in [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) must stay under its word ceiling (`wc -w` semantics, whole file), and a budgeted file that is missing fails the gate so a rename cannot silently orphan its budget. Its scope is deliberately only the accretion-prone standing docs — the root and subtree `AGENTS.md` files, `architecture.md`, `packages/README.md`, and the standing policy docs they evict content into (`docs/testing.md`, `docs/defensive-patterns.md`). Reference docs, Agent Notes, and complete package READMEs remain unbudgeted because exhaustive facts can be long. The separate [package Summary gate](../../../../scripts/verify-package-readme-summaries.ts) caps only each English package entry paragraph at 100 words and directs failures to `dsh-doc` and the selected kind template.
 - **Ceilings are an enforcement frontier that ratchets.** A doc at or below its target keeps at least 5% headroom as its ceiling ratchets down; a doc above target keeps a frozen ceiling that prevents growth until it reaches the target (root `AGENTS.md` ≤ 1,600 words; `architecture.md` ≤ 1,800; subtree `AGENTS.md` ≤ 600 except `packages/AGENTS.md` ≤ 650 and `docs/AGENTS.md` ≤ 1,250; `packages/README.md` ≤ 600). When the gate goes red, relocate or condense; raise a ceiling only with explicit PR justification.
 - **A thin workflow skill, contracts in docs.** [.agents/skills/dsh-doc](../../../skills/dsh-doc/SKILL.md) carries the placement, audit, budget, and website workflow and defers to the standard as its source of truth, the same split as [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) over the i18n contract.
 
 ## Alternatives considered
 
 - **Skill and review discipline without a gate** — rejected: the accretion above happened while the current-state rule and reviewer attention already existed; a prose rule with no mechanical backstop demonstrably does not hold here, and this repo's own [quality-gates stance](2026-06-11-quality-gates.md) says invariants worth keeping are worth encoding.
-- **A broad gate over every doc tier** — rejected: a blanket ceiling punishes exactly the right kind of long doc (a feature matrix or type catalog where every row is a fact) and generates per-file override churn that trains contributors to rubber-stamp raises.
+- **A broad gate over every complete doc** — rejected: a blanket ceiling punishes exactly the right kind of long doc (a feature matrix or type catalog where every row is a fact) and generates per-file override churn that trains contributors to rubber-stamp raises. The package Summary limit instead bounds one common entry paragraph without constraining its owning reference sections.
 - **Independent onboarding tutorials for each documentation entry point** — rejected: duplicated setup steps drift in command order, first outcome, and product identity. A short README path followed by task-focused guides keeps the transition explicit without maintaining competing tutorials.
 - **Housing the standard inside the skill** — rejected: contracts live in docs and workflows in skills; a standard packed into SKILL.md is invisible to an agent that edits docs without invoking the skill, and `docs/AGENTS.md` already loads as subtree instructions for anyone working under `docs/`.
 
@@ -30,4 +30,5 @@ Standing docs accumulated repeated rules, retold incidents, duplicated package m
 - Structural review starts with ownership and document form before sentence-level editing, so lower-level detail moves to its owner instead of being polished in the wrong place.
 - Readers reach a running Web UI before encountering headless execution, SDK embedding, custom profiles, or direct settings files; those interfaces remain available from their reference owners.
 - Budgeted docs that remain above target cannot grow; reaching the target restores the 5% working headroom.
+- Package references retain exhaustive owned facts below their entry paragraph, while every package Summary stays within the same 100-word retrieval budget.
 - Word count is a crude proxy accepted deliberately: it cannot judge quality, but it forces the relocation decision at exactly the moment content is being added, which is when the author has the context to place it correctly.

+ 3 - 2
.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.zh.md

@@ -13,14 +13,14 @@ Status: implemented
 - **结构遵循文档树。**[docs/AGENTS.md](../../../../docs/AGENTS.md) 是文档标准:文档负责承载其主题的详细内容,仅概述直接子项的目的、职责和高层行为,并链接到更深层内容的归属文档。[Agent Note](../../README.zh.md) 仍不受这一结构约定约束。每份面向人的文档要么是按顺序引导读者达成结果的教程(tutorial),要么是查阅范围明确的参考文档(reference);[事故复盘(postmortem)](../../../../docs/postmortem/README.zh.md) 是范围限定于单起事故的参考文档,其时间线记录证据。教程结合读者的起始知识,按前置依赖顺序介绍概念。
 - **每项事实只归属一处的层级分类。**文档标准为每种 Markdown 层级分配单一职责,禁止在事实归属层级之外重复陈述,并包含编写或评审任何文档时使用的赘余检查清单。
 - **单一产品入门路径。**根 README 负责推荐的包运行路径、从源码运行的备选路径和简要的 `dsh plugin --profile` 用法。已发布的用户指南从运行中的 Web UI 内部任务开始,再链接到其他界面的独立教程或插件开发与进阶配置的参考文档归属处,而不会重复介绍 Web 启动步骤。
-- **范围窄且严格的预算门禁。**[scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) 接入 `doc-sync`:[scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) 列出的每份文档都必须低于其词数上限(采用 `wc -w` 语义,统计整个文件);预算内文件缺失也会使门禁失败,使重命名无法悄然遗落其预算。范围刻意只涵盖容易膨胀的常设文档——根目录和子树中的 `AGENTS.md` 文件、`architecture.md`、`packages/README.md`,以及它们将内容移入的常设策略文档(`docs/testing.md`、`docs/defensive-patterns.md`)。参考文档、Agent Note 和包 README 不设预算:只要每一行都是事实,长度在这些位置就是合理的;评审和赘余检查清单负责约束它们。
+- **范围窄且严格的预算门禁。**[scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) 接入 `doc-sync`:[scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) 列出的每份文档都必须低于其词数上限(采用 `wc -w` 语义,统计整个文件);预算内文件缺失也会使门禁失败,使重命名无法悄然遗落其预算。该门禁的范围刻意只涵盖容易膨胀的常设文档——根目录和子树中的 `AGENTS.md` 文件、`architecture.md`、`packages/README.md`,以及它们将内容移入的常设策略文档(`docs/testing.md`、`docs/defensive-patterns.md`)。参考文档、Agent Note 和完整的包 README 仍不设预算,因为穷尽式事实可能很长。单独的[包 Summary 门禁](../../../../scripts/verify-package-readme-summaries.ts)只把每个英文包入口段落限制为 100 词,并引导失败项阅读 `dsh-doc` 和所选 kind 模板。
 - **上限是只进不退的执行红线。** 达到或低于目标的文档在上限逐步下调时保留至少 5% 的余量;高于目标的文档则维持冻结的上限,在达到目标之前不得增长(根 `AGENTS.md` ≤ 1,600 词;`architecture.md` ≤ 1,800;子树 `AGENTS.md` ≤ 600,但 `packages/AGENTS.md` ≤ 650、`docs/AGENTS.md` ≤ 1,250;`packages/README.md` ≤ 600)。门禁变红时,迁移或压缩内容;只有在 PR(Pull Request)描述中给出明确理由时才提高上限。
 - **精简的工作流 skill(技能),约定归文档。**[.agents/skills/dsh-doc](../../../skills/dsh-doc/SKILL.md) 承载文档放置、审计、预算与站点发布工作流,并以文档标准为真源,与 [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) 和 i18n 约定之间的分工相同。
 
 ## 曾考虑的替代方案
 
 - **仅靠 skill 和评审纪律,不设门禁**:否决。上述膨胀正是在现行规则和评审注意力已经存在的情况下发生的;一条没有自动化保障的行文规则在此处已被证明无法维持,而本仓库自身的[质量门禁立场](2026-06-11-quality-gates.zh.md)认为值得保持的不变式就值得编码。
-- **对所有文档层级全面设限**:否决。一刀切的上限恰好惩罚了那些正当的长文档(如功能矩阵或类型目录,每一行都是事实),并产生逐文件的例外变更,训练贡献者机械地批准提限。
+- **对每份完整文档全面设限**:否决。一刀切的上限恰好惩罚了那些正当的长文档(如功能矩阵或类型目录,每一行都是事实),并产生逐文件的例外变更,训练贡献者机械地批准提限。包 Summary 上限只约束共同的入口段落,不限制其归属参考章节。
 - **为每个文档入口维护独立入门教程**:否决。重复的设置步骤会在命令顺序、首个结果和产品定位上产生分歧。简短的 README 路径接上面向任务的指南,可明确衔接两者,且不需要维护相互竞争的教程。
 - **将标准放在 skill 内部**:否决。约定归文档,工作流归 skill;如果标准被塞进 SKILL.md,那些不调用该 skill 而直接编辑文档的 agent(智能体)就看不到它,而 `docs/AGENTS.md` 已经作为子树指令被任何在 `docs/` 下工作的人加载。
 
@@ -30,4 +30,5 @@ Status: implemented
 - 结构评审先检查归属关系和文档形式,再进行句子层面的编辑,使较低层级的细节迁移到其归属文档,而不是在错误的位置加以润色。
 - 读者会先进入可运行的 Web UI,再遇到 headless 执行、SDK 嵌入、自定义 profile 或直接 settings 文件;这些入口仍可从各自的参考文档归属处访问。
 - 仍高于目标的受预算约束文档不得增长;达到目标后,将恢复 5% 的工作余量。
+- 包参考可在入口段落之后保留穷尽式归属事实,而每个包 Summary 都遵守相同的 100 词检索预算。
 - 词数是一个粗糙的代理指标,这是有意接受的:它无法判断质量,但它在内容被添加的那一刻强制触发迁移决策,而那正是作者拥有足够上下文来正确放置内容的时刻。

+ 6 - 0
.agents/notes/implemented/process/2026-09-08-comment-only-review-routing.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/process/2026-09-08-comment-only-review-routing.md
+2026-09-08-comment-only-review-routing.md: 050905285b2291b34da9873d19c2f122c088a9e5
+2026-09-08-comment-only-review-routing.zh.md: b98f5d70b4d5c0fd27df1c393238b0802e420e09

+ 41 - 0
.agents/notes/implemented/process/2026-09-08-comment-only-review-routing.md

@@ -0,0 +1,41 @@
+# Agent Note: Exclude documentation and comment-only changes from review routing
+
+Status: implemented
+
+English | [中文](2026-09-08-comment-only-review-routing.zh.md)
+
+## Problem
+
+Directory ownership alone treats documentation and comment edits like executable changes. These edits do not require the automatic code-owner request that protects behavior changes.
+
+GitHub may omit or truncate a file patch. A scanner that assumes every patch is complete can miss executable changes that occur outside the supplied hunks.
+
+## Decision
+
+Review routing classifies every old and new path in this order: test, documentation, comment-only, then reviewable code. Test classification wins when a test path also has a documentation extension. Every filename ending in `.md` or `.yaml`, matched without case sensitivity, is documentation. A `.yml` file is not documentation under this rule.
+
+Comment-only classification applies only to files with `status: modified` and a declared source-comment syntax. The scanner reconstructs the before and after text for each patch hunk, removes comments outside quoted strings, removes empty lines left by comments, and requires the remaining text to be identical.
+
+The scanner counts added and deleted patch lines and compares them with GitHub's file record before accepting a comment-only result. A missing patch, a count mismatch, a rename, an unsupported extension, or a comment form that remains visible to the lexer keeps the file reviewable. This fail-safe result can request an unnecessary review but cannot suppress a known code change.
+
+The supported lexical rules cover C-style line and block comments, hash comments, SQL comments, CSS block comments, and HTML comments for an explicit extension set in the scanner. Comment directives such as JSDoc tags, lint controls, compiler controls, and coverage controls are comments for routing purposes.
+
+## Verification
+
+[Scanner tests](../../../../.github/review-ownership/request-review.test.mjs) cover documentation extensions, supported comment forms, quoted comment markers, executable token changes, incomplete patches, renames, unsupported extensions, exclusion precedence, and the no-request result when every file is excluded.
+
+## Alternatives considered
+
+**Keep every non-test file reviewable.** This requests code owners for documentation and comment maintenance even though the routing policy is intended to identify executable changes.
+
+**Infer arbitrary semantic equivalence.** Proving behavior equivalence across the repository's languages requires language toolchains and still cannot assign one stable meaning to generated files, configuration, or build directives. The scanner performs only lexical comment removal.
+
+**Trust every patch returned by GitHub.** GitHub can omit or truncate patches. Matching the patch's added and deleted line counts to the file record prevents a partial patch from producing a comment-only verdict.
+
+**Fetch and parse every complete file revision.** Per-file content requests multiply API traffic for large pull requests and still require the same language-specific parsing. The changed-file response already carries enough evidence for complete ordinary patches.
+
+## Consequences
+
+Documentation and proven comment-only changes request nobody. The workflow logs them separately from tests so maintainers can audit why owner matching ignored a file.
+
+Unsupported or incomplete inputs remain reviewable. Comment directives do not request owners even when another tool interprets them, because this policy classifies their lexical form rather than downstream tool behavior.

+ 41 - 0
.agents/notes/implemented/process/2026-09-08-comment-only-review-routing.zh.md

@@ -0,0 +1,41 @@
+# Agent Note: 从评审路由中排除文档和纯注释变更
+
+Status: implemented
+
+[English](2026-09-08-comment-only-review-routing.md) | 中文
+
+## 问题
+
+只按目录分配 owner 会把文档和注释编辑视为可执行变更。这些编辑不需要用于保护行为变更的自动代码 owner 请求。
+
+GitHub 可能省略或截断文件 patch。如果扫描器假定每个 patch 都完整,就可能漏掉位于已提供 hunk 之外的可执行变更。
+
+## 决策
+
+评审路由按测试、文档、纯注释、可评审代码的顺序对每个新旧路径分类。当测试路径同时具有文档扩展名时,测试分类优先。所有以 `.md` 或 `.yaml` 结尾的文件均视为文档,扩展名匹配不区分大小写;此规则不把 `.yml` 文件视为文档。
+
+纯注释分类只适用于 `status: modified` 且已声明源码注释语法的文件。扫描器重建每个 patch hunk 的变更前后文本,移除引号字符串外的注释和注释留下的空行,并要求其余文本完全相同。
+
+扫描器会统计 patch 的新增行和删除行,并在接受纯注释结果前与 GitHub 文件记录比较。缺失 patch、计数不符、重命名、不受支持的扩展名,或词法分析器仍能看到的注释形式都会使文件保持可评审状态。该保守结果可能产生不必要的评审请求,但不会隐藏已知代码变更。
+
+受支持的词法规则按扫描器中显式的扩展名集合覆盖 C 风格行注释和块注释、井号注释、SQL 注释、CSS 块注释及 HTML 注释。JSDoc 标签、lint 控制、编译器控制和覆盖率控制等注释指令在评审路由中仍属于注释。
+
+## 验证
+
+[扫描器测试](../../../../.github/review-ownership/request-review.test.mjs)覆盖文档扩展名、受支持的注释形式、引号内的注释标记、可执行 token 变更、不完整 patch、重命名、不受支持的扩展名、排除优先级,以及所有文件均被排除时不发出请求的结果。
+
+## 考虑过的替代方案
+
+**让每个非测试文件都保持可评审。** 这会为文档和注释维护请求代码 owner,但该路由策略的目标是识别可执行变更。
+
+**推断任意语义等价。** 证明仓库中多种语言的行为等价需要各语言工具链,而且仍然无法为生成文件、配置或构建指令提供一种稳定含义。扫描器只执行词法注释移除。
+
+**信任 GitHub 返回的每个 patch。** GitHub 可能省略或截断 patch。将 patch 的新增和删除行数与文件记录匹配,可以防止不完整 patch 产生纯注释结论。
+
+**获取并解析每个文件的完整修订版本。** 对于大型 PR,逐文件内容请求会增加多倍 API 流量,而且仍需相同的语言专用解析。普通完整 patch 所需的证据已包含在变更文件响应中。
+
+## 后果
+
+文档和确认的纯注释变更不会请求任何人。Workflow 会将它们与测试分开记录,以便维护者检查 owner 匹配忽略文件的原因。
+
+不受支持或不完整的输入仍需评审。即使其他工具会解释注释指令,这些指令也不会请求 owner,因为该策略按词法形式分类,而不是按下游工具行为分类。

+ 2 - 2
.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.i18n.yaml

@@ -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/process/2026-09-08-trusted-changed-file-review-routing.md
-2026-09-08-trusted-changed-file-review-routing.md: 246c223d976e312e5712d7baa150bd3e84f1363e
-2026-09-08-trusted-changed-file-review-routing.zh.md: 397d4174340fc9c6002419b21436b5c33542f7e4
+2026-09-08-trusted-changed-file-review-routing.md: 9f59f336fb1f8d2fdd1e13f6751258a47518b4dc
+2026-09-08-trusted-changed-file-review-routing.zh.md: e845c79051858c11053597516f486cf29fa2805a

+ 9 - 9
.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md

@@ -12,21 +12,21 @@ Review routing needs an observable changed-file input, explicit owner rules, com
 
 ## Decision
 
-The repository keeps a CODEOWNERS-compatible map at [`.github/review-ownership/CODEOWNERS`](../../../../.github/review-ownership/CODEOWNERS), outside GitHub's native CODEOWNERS locations. The map accepts only explicit absolute directory patterns and individual GitHub users. It rejects wildcards, hidden-directory patterns, teams, duplicate patterns, and duplicate owners. Later matching patterns replace earlier matches.
+The repository keeps a CODEOWNERS-compatible map at [`.github/review-ownership/CODEOWNERS`](../../../../.github/review-ownership/CODEOWNERS), outside GitHub's native CODEOWNERS locations. The map accepts only explicit absolute directory patterns with one or two individual GitHub users. It rejects wildcards, hidden-directory patterns, teams, more than two owners, duplicate patterns, and duplicate owners. Later matching patterns replace earlier matches.
 
 The policy test counts non-test tracked lines in directories that match an ownership rule. It rejects a map in which `@turtle1999` owns more than one third of that eligible owned codebase.
 
-The [`request-review` workflow](../../../../.github/workflows/request-review.yml) runs on non-draft `pull_request_target` events for opened, synchronized, reopened, and ready-for-review pull requests. Its write-capable job checks out the default branch and executes only the default branch's scanner and ownership map. It does not check out pull-request code or read repository secrets.
+The [`request-review` workflow](../../../../.github/workflows/request-review.yml) runs on `pull_request_target` events for opened, synchronized, reopened, ready-for-review, and converted-to-draft pull requests. Its write-capable job checks out the default branch and executes only the default branch's scanner and ownership map. It does not check out pull-request code or read repository secrets.
 
 The scanner fetches every changed-file record before deciding. It fails if the pull request reports more than GitHub's 3,000-file API limit or if pagination returns an incomplete list. It normalizes repository paths, evaluates old and new paths of a rename independently, and escapes filenames before logging them.
 
-The scanner excludes test-only paths before owner matching. Excluded paths comprise directories named `test`, `tests`, `__tests__`, `__snapshots__`, `benches`, or `stress-tests`; the top-level `benchmarks` and `snapshots` trees; `packages/test-support`; `scripts/fixtures` and `scripts/snapshots`; filenames ending in `.bench.<ext>`, `.corpus.<ext>`, `.e2e.<ext>`, `.perf.<ext>`, `.snapshot.<ext>`, `.spec.<ext>`, `.stress.<ext>`, or `.test.<ext>`; and Python `test_*.py`, `*_test.py`, or `*_tests.py` files. Test infrastructure such as `vitest*.config.ts` and gate implementations remains reviewable because it changes how repository evidence is produced.
+The scanner excludes test-only paths before owner matching. Excluded paths comprise directories named `test`, `tests`, `__tests__`, `__snapshots__`, `benches`, or `stress-tests`; the top-level `benchmarks` and `snapshots` trees; `packages/test-support`; `scripts/fixtures` and `scripts/snapshots`; filenames ending in `.bench.<ext>`, `.corpus.<ext>`, `.e2e.<ext>`, `.perf.<ext>`, `.snapshot.<ext>`, `.spec.<ext>`, `.stress.<ext>`, or `.test.<ext>`; and Python `test_*.py`, `*_test.py`, or `*_tests.py` files. Test infrastructure such as `vitest*.config.ts` and gate implementations remains reviewable because it changes how repository evidence is produced. The [comment-only routing decision](2026-09-08-comment-only-review-routing.md) owns the additional documentation and comment exclusions.
 
-The workflow prints the changed non-test paths, excluded test paths, per-file owner matches, and final reviewer list before any review-request mutation. It requests the union of matched individual owners after removing the pull-request author and users who are already requested. A test-only or wholly unmatched change requests nobody.
+The workflow prints the changed code paths, each exclusion class, per-file owner matches, and final reviewer action before any review-request mutation. For a non-draft pull request, it removes the pull-request author and users who are already requested from the login-sorted union of matched individual owners, then fills at most two current individual review-request slots. Existing individual requests consume slots even when they do not match the ownership map. Login order deterministically selects candidates when the available slots cannot cover the remaining union. The workflow does not remove requests from a non-draft pull request. For a draft, it reads the complete review-request timeline and cancels current requests whose latest requester is `github-actions[bot]`; requests made by people remain unchanged.
 
 ## Verification
 
-[Scanner tests](../../../../.github/review-ownership/request-review.test.mjs) cover admitted ownership syntax, rejected syntax, each test convention, production-name negative controls, renames, last-match behavior, unmatched files, complete pagination, the 3,000-file limit, log-before-request ordering, author and existing-reviewer filtering, test-only changes, drafts, and API failures. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the event set, least permissions, trusted default-branch checkout, absence of pull-request-head references and secrets, and executed command. The gate graph includes both suites in static CI and `check-all`.
+[Scanner tests](../../../../.github/review-ownership/request-review.test.mjs) cover admitted ownership syntax, rejected syntax, each exclusion class, production-name negative controls, renames, last-match behavior, unmatched files, complete pagination, the 3,000-file limits, log-before-mutation ordering, author and existing-reviewer filtering, draft cancellation provenance, and API failures. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the event set, least permissions, trusted default-branch checkout, absence of pull-request-head references and secrets, and executed command. The gate graph includes both suites in static CI and `check-all`.
 
 ## Alternatives considered
 
@@ -36,12 +36,12 @@ The workflow prints the changed non-test paths, excluded test paths, per-file ow
 
 **Execute the pull request's scanner or owner map under `pull_request_target`.** This lets an untrusted pull request choose its own write-capable behavior or owners.
 
-**Infer semantic source changes from patches or language parsers.** GitHub can truncate patches, and the repository spans TypeScript, JavaScript, Python, Rust, YAML, Markdown, and generated evidence. A cross-language semantic classifier would add ambiguous rules without providing a complete input. The scanner therefore uses the complete non-test changed-file list and does not claim to distinguish formatting, comments, or documentation-only edits inside an eligible file.
+**Infer arbitrary semantic source changes from patches or language parsers.** GitHub can omit or truncate patches, and the repository spans many languages. The scanner does not try to prove that two programs behave identically. The later [comment-only routing decision](2026-09-08-comment-only-review-routing.md) adds a narrow lexical comparison only when changed-line counts prove that GitHub supplied the complete patch.
 
 ## Consequences
 
-Reviewer requests are reproducible from a trusted policy and the file list printed in the workflow log. Test-only changes do not request owners. Ownership changes become effective only after merge, so the pull request that changes policy cannot apply its untrusted policy to itself.
+Reviewer mutations are reproducible from a trusted policy, the file classifications printed in the workflow log, and review-request provenance in the pull-request timeline. Excluded changes do not request owners, and draft pull requests do not retain workflow-authored requests. Ownership changes become effective only after merge, so the pull request that changes policy cannot apply its untrusted policy to itself.
 
-The workflow requests every matched owner rather than choosing one owner nondeterministically. Shared ownership on large directories therefore produces multiple requests. GitHub-generated review-request events may not start other workflows that depend on recursively triggered events from `GITHUB_TOKEN`; those workflows must not rely on this request as their only trigger.
+The workflow requests every matched owner rather than choosing one owner nondeterministically. Shared ownership therefore produces at most two requests for each changed module. GitHub-generated review-request events may not start other workflows that depend on recursively triggered events from `GITHUB_TOKEN`; those workflows must not rely on this request as their only trigger.
 
-Any non-test change under an owned directory remains eligible, including comment-only or formatting-only edits and documentation changes. Unmatched paths are logged and request nobody. Pull requests above the API file limit fail without requesting a partial owner set.
+Any change that does not match an explicit exclusion remains eligible under an owned directory. Unmatched paths are logged and request nobody. Pull requests above the file or timeline API limit fail without applying a partial reviewer mutation.

+ 9 - 9
.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.zh.md

@@ -12,21 +12,21 @@ Status: implemented
 
 ## 决策
 
-仓库在 GitHub 原生 CODEOWNERS 路径之外的 [`.github/review-ownership/CODEOWNERS`](../../../../.github/review-ownership/CODEOWNERS) 中保存兼容 CODEOWNERS 格式的映射。该映射只接受显式绝对目录模式和 GitHub 个人用户。通配符、隐藏目录模式、团队、重复模式和重复 owner 都会被拒绝。靠后的匹配模式会替换靠前的匹配结果。
+仓库在 GitHub 原生 CODEOWNERS 路径之外的 [`.github/review-ownership/CODEOWNERS`](../../../../.github/review-ownership/CODEOWNERS) 中保存兼容 CODEOWNERS 格式的映射。该映射只接受显式绝对目录模式,每条模式配置一至两名 GitHub 个人用户。通配符、隐藏目录模式、团队、超过两名 owner、重复模式和重复 owner 都会被拒绝。靠后的匹配模式会替换靠前的匹配结果。
 
 策略测试会统计匹配所有权规则的目录中的非测试跟踪文件行数。如果 `@turtle1999` 拥有的有效代码库超过三分之一,测试就会拒绝该映射。
 
-[`request-review` workflow](../../../../.github/workflows/request-review.yml) 在非草稿 PR 的 `pull_request_target` 事件上运行,订阅创建、同步、重新打开和标记为可评审操作。具备写权限的 job 检出默认分支,只执行默认分支上的扫描器和所有权映射。它不会检出 PR 代码,也不会读取仓库 secret。
+[`request-review` workflow](../../../../.github/workflows/request-review.yml) 在 PR 创建、同步、重新打开、标记为可评审和转为草稿时运行 `pull_request_target` 事件。具备写权限的 job 检出默认分支,只执行默认分支上的扫描器和所有权映射。它不会检出 PR 代码,也不会读取仓库 secret。
 
 扫描器在决策之前获取所有变更文件记录。如果 PR 报告的文件数超过 GitHub API 的 3,000 个文件上限,或者分页只返回了部分列表,扫描器就会失败。它会规范化仓库路径,分别检查重命名前后的路径,并在记录文件名之前进行转义。
 
-扫描器会在匹配 owner 之前排除纯测试路径。排除范围包括名为 `test`、`tests`、`__tests__`、`__snapshots__`、`benches` 或 `stress-tests` 的目录,顶层 `benchmarks` 和 `snapshots` 目录树,`packages/test-support`、`scripts/fixtures` 和 `scripts/snapshots`,以 `.bench.<ext>`、`.corpus.<ext>`、`.e2e.<ext>`、`.perf.<ext>`、`.snapshot.<ext>`、`.spec.<ext>`、`.stress.<ext>` 或 `.test.<ext>` 结尾的文件名,以及 Python 的 `test_*.py`、`*_test.py` 或 `*_tests.py` 文件。`vitest*.config.ts` 和门禁实现等测试基础设施仍需评审,因为它们会改变仓库证据的生成方式。
+扫描器会在匹配 owner 之前排除纯测试路径。排除范围包括名为 `test`、`tests`、`__tests__`、`__snapshots__`、`benches` 或 `stress-tests` 的目录,顶层 `benchmarks` 和 `snapshots` 目录树,`packages/test-support`、`scripts/fixtures` 和 `scripts/snapshots`,以 `.bench.<ext>`、`.corpus.<ext>`、`.e2e.<ext>`、`.perf.<ext>`、`.snapshot.<ext>`、`.spec.<ext>`、`.stress.<ext>` 或 `.test.<ext>` 结尾的文件名,以及 Python 的 `test_*.py`、`*_test.py` 或 `*_tests.py` 文件。`vitest*.config.ts` 和门禁实现等测试基础设施仍需评审,因为它们会改变仓库证据的生成方式。[纯注释路由决策](2026-09-08-comment-only-review-routing.zh.md)记录额外的文档和注释排除规则。
 
-Workflow 会在发出任何评审请求变更之前,依次打印变更的非测试路径、排除的测试路径、逐文件 owner 匹配结果和最终评审者列表。它合并匹配到的个人 owner,并排除 PR 作者和已经收到评审请求的用户。纯测试变更或全部未匹配的变更不会请求任何人。
+Workflow 会在发出任何评审请求变更之前,依次打印变更代码路径、每类排除项、逐文件 owner 匹配结果和最终评审操作。对于非草稿 PR,它会从按登录名排序并合并的个人 owner 中排除 PR 作者和已经收到评审请求的用户,然后填充最多两个当前个人评审请求名额。现有个人请求即使不匹配所有权映射,也会占用名额。当可用名额无法覆盖剩余候选集合时,登录名顺序会确定性地选择候选人。Workflow 不会从非草稿 PR 移除请求。对于草稿,它会读取完整的评审请求时间线,并取消最近一次请求者为 `github-actions[bot]` 的当前请求;由人员发出的请求保持不变。
 
 ## 验证
 
-[扫描器测试](../../../../.github/review-ownership/request-review.test.mjs)覆盖允许的所有权语法、拒绝的语法、每种测试约定、生产文件名负向对照、重命名、最后匹配规则、未匹配文件、完整分页、3,000 个文件上限、先记录后请求的顺序、作者与现有评审者过滤、纯测试变更、草稿和 API 失败。[Workflow 测试](../../../../scripts/ci-workflow.spec.ts)固定事件集合、最小权限、受信任的默认分支检出、不引用 PR head 和 secret,以及执行的命令。门禁图在静态 CI 和 `check-all` 中包含这两组测试。
+[扫描器测试](../../../../.github/review-ownership/request-review.test.mjs)覆盖允许的所有权语法、拒绝的语法、每类排除项、生产文件名负向对照、重命名、最后匹配规则、未匹配文件、完整分页、两个 3,000 项上限、先记录后修改的顺序、作者与现有评审者过滤、草稿取消来源和 API 失败。[Workflow 测试](../../../../scripts/ci-workflow.spec.ts)固定事件集合、最小权限、受信任的默认分支检出、不引用 PR head 和 secret,以及执行的命令。门禁图在静态 CI 和 `check-all` 中包含这两组测试。
 
 ## 考虑过的替代方案
 
@@ -36,12 +36,12 @@ Workflow 会在发出任何评审请求变更之前,依次打印变更的非
 
 **在 `pull_request_target` 下执行 PR 中的扫描器或 owner 映射。** 这会让不受信任的 PR 选择自己的写权限行为或 owner。
 
-**根据补丁或语言解析器推断语义源码变更。** GitHub 可能截断补丁,而且仓库包含 TypeScript、JavaScript、Python、Rust、YAML、Markdown 和生成的证据。跨语言语义分类器会增加含义不明确的规则,却无法提供完整输入。因此,扫描器使用完整的非测试变更文件列表,并且不会声称能够区分合格文件中的纯格式、注释或仅文档编辑。
+**根据 patch 或语言解析器推断任意语义源码变更。** GitHub 可能省略或截断 patch,而且仓库包含多种语言。扫描器不会尝试证明两个程序行为相同。后续的[纯注释路由决策](2026-09-08-comment-only-review-routing.zh.md)只在变更行计数能够证明 GitHub 提供了完整 patch 时执行有限的词法比较。
 
 ## 后果
 
-评审请求可以根据受信任的策略和 workflow 日志中打印的文件列表复现。纯测试变更不会请求 owner。所有权变更只有合并后才会生效,因此修改策略的 PR 无法对自身应用其中不受信任的策略。
+评审请求修改可以根据受信任的策略、workflow 日志中打印的文件分类,以及 PR 时间线中的请求来源复现。被排除的变更不会请求 owner,草稿 PR 不会保留 workflow 发出的请求。所有权变更只有合并后才会生效,因此修改策略的 PR 无法对自身应用其中不受信任的策略。
 
-Workflow 会请求所有匹配的 owner,不会随机选择一人。因此,大目录上的共享所有权会产生多个请求。GitHub 使用 `GITHUB_TOKEN` 生成的评审请求事件可能不会启动依赖递归触发事件的其他 workflow;这些 workflow 不得把此请求作为唯一触发条件。
+Workflow 会请求所有匹配的 owner,不会随机选择一人。因此,共享所有权对每个变更模块最多产生两个请求。GitHub 使用 `GITHUB_TOKEN` 生成的评审请求事件可能不会启动依赖递归触发事件的其他 workflow;这些 workflow 不得把此请求作为唯一触发条件。
 
-所有已分配目录下的非测试变更仍符合请求条件,其中包括纯注释、纯格式调整和文档变更。未匹配的路径会被记录,但不会请求任何人。超过 API 文件上限的 PR 会失败,并且不会请求不完整的 owner 集合。
+已分配目录下不符合任何显式排除规则的变更仍符合请求条件。未匹配的路径会被记录,但不会请求任何人。超过文件或时间线 API 上限的 PR 会失败,并且不会执行不完整的评审者修改。

+ 2 - 2
.agents/notes/proposed/process/2026-08-20-audience-first-documentation-quality.i18n.yaml

@@ -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/proposed/process/2026-08-20-audience-first-documentation-quality.md
-2026-08-20-audience-first-documentation-quality.md: d44e9508959232397b90ad8a22a5e8b6040e0748
-2026-08-20-audience-first-documentation-quality.zh.md: 88c0c64266eed9a0744b43185362342638e4a6b9
+2026-08-20-audience-first-documentation-quality.md: 9e0c4a61408449100b79148a571cd740e4044040
+2026-08-20-audience-first-documentation-quality.zh.md: 0b6f1da54380f1d2d44afbe948131233def513cd

+ 6 - 5
.agents/notes/proposed/process/2026-08-20-audience-first-documentation-quality.md

@@ -51,7 +51,7 @@ Adopt one audience-first quality contract with five definitions:
 The [dsh-doc skill](../../../skills/dsh-doc/SKILL.md) owns the first executable version of these rules. The `session-persistence-jsonl` README pair uses the shipped append, recovery, and encoding behavior as evidence rather than treating its prior prose as authority.
 
 - Every authored package README starts with searchable YAML. A Skill-style `description` and mechanically derived `kind` are required. Four kinds map one-to-one to four skill templates: `package-group` (group map), `package-reference` (plugin or service package), `package-library` (plain module entry), and `package-bundle` (`dsh.bundle.patch`). The counterpart path, hashes, and physical line alignment belong to the merge-safe sidecar and its gate, so README frontmatter contains no `i18n` block. The title or package manifest already owns the name, the document job expresses its audience, and tags remain absent until a governed taxonomy and search consumer proves value beyond full-text search.
-- Authored pages start with a three-to-five-sentence `Summary`, then a linked `Table of Contents`. Format-owned Agent Notes, postmortems, generated fragments, and machine files keep their required skeletons.
+- Authored pages start with a three-to-five-sentence `Summary`, then a linked `Table of Contents`. An English package README Summary stays within 100 `wc -w`-style words. It describes reader-visible capability instead of Cordis roles, registrations, or internal components, and omits source identifiers unless readers use them directly in configuration, commands, or a public API. Format-owned Agent Notes, postmortems, generated fragments, and machine files keep their required skeletons.
 - Each substantive section starts with a short orientation before subsections, tables, or code, and the page progresses from basic user use to advanced developer and maintainer detail.
 - English technical prose uses an ASD-STE100-inspired, non-certified clarity review: explicit actors and actions, stable terms, direct verbs, separated instructions and conditions, and preserved modality, exceptions, timing, and numbers. The 20-word instruction and 25-word description limits are review prompts. Precision overrides them.
 - Package contracts remain beside code. Cross-package material moves deliberately toward `docs/learn/overview/`, `docs/learn/cordis/`, `docs/learn/practices/`, `docs/user/`, `docs/developer/`, `docs/developer/discussion/`, `docs/scratch/`, and the parallel `docs/subsystems/` tier.
@@ -87,11 +87,11 @@ The first prototype should use one large catalog and one mixed subsystem page. I
 
 1. Create and validate `dsh-doc`, then rewrite one package README pair as a line-aligned, metadata-bearing prototype without changing runtime claims.
 2. Review the rendered prototype with newcomer, user, developer, and agent tasks; revise the skill before enforcing the format elsewhere.
-3. Add narrow metadata, section-order, line-alignment, link-resolution, and pairing fixtures. Keep sidecars until every merge and recovery consumer has replacement support.
+3. Add narrow metadata, Summary-length, section-order, line-alignment, link-resolution, and pairing fixtures. Migrate every existing package Summary that violates the accepted entry limit, and keep sidecars until every merge and recovery consumer has replacement support.
 4. Extract accepted standing rules into one canonical quality reference, condense `docs/AGENTS.md` below its target, and organize one coherent `docs/` topic at a time with atomic link/navigation repair.
 5. Prototype generated-reference entry/detail separation on `config-catalog.md` and `docs/subsystems/core.md`; apply confirmed patterns elsewhere only after measured lookup cost falls without lost facts or route churn.
 
-This sequence keeps each change independently reviewable. The first three slices improve criteria and correctness without rewriting the corpus; the generated-doc prototype supplies evidence before a broader information-architecture change.
+This sequence keeps each change independently reviewable. The first three slices improve criteria and package entry points without changing the broader information architecture; the generated-doc prototype supplies evidence before a broader structural change.
 
 Slices 1–3 have shipped in this form: `dsh-doc` is the consolidated standard (`dsh-doc-standards` and `dsh-doc-site-sync` are folded into it, and the site workflow carries the corrected sidebar values), the `session-persistence-jsonl` README pair is the reference example, and `pnpm run test:docs` enforces the metadata, pairing, and quick documentation checks. Slices 4–5 remain open.
 
@@ -107,7 +107,7 @@ This proposal does not shorten exhaustive facts, merge audience tiers, publish i
 
 **Use readability scores as the quality gate.** Rejected because formulas penalize exact technical terms and cannot detect wrong ownership, missing failure behavior, stale commands, or a broken reader journey.
 
-**Rewrite or split the full corpus immediately.** Rejected because the current system is mechanically healthy and many long references are appropriately exhaustive. A prototype should prove a retrieval improvement before route and translation churn spreads.
+**Rewrite or split the full documentation corpus immediately.** Rejected because the current system is mechanically healthy and many long references are appropriately exhaustive. The bounded package-Summary migration does not alter routes or exhaustive reference content; larger structural changes still require measured evidence.
 
 **Keep the existing gates and rely on review for friendliness.** Rejected because the stale workflow values and budget-policy mismatch show that review alone does not preserve copied semantic claims, and the current gates do not ask whether a reader can complete a task.
 
@@ -116,6 +116,7 @@ This proposal does not shorten exhaustive facts, merge audience tiers, publish i
 - One canonical quality reference defines brief, intuitive, friendly, accurate, and agent-readable documentation by document job.
 - `.agents/skills/dsh-doc` validates and directly links its metadata, structure/hierarchy, and review/prototype references without duplicating their detailed rules in `SKILL.md`.
 - The `session-persistence-jsonl` README pair demonstrates searchable YAML, Summary, Table of Contents, user-to-developer progression, Further Exploration, final Dev Note, structural parity, and exact line-count equality while preserving verified package contracts.
+- Every English package README Summary stays within 100 `wc -w`-style words; the focused gate reports the measured count and directs failures to `dsh-doc` and the selected kind template.
 - `docs/AGENTS.md` links that reference, remains sufficient as standing instruction, and is below its target with at least 5% headroom.
 - The root user path, Web quick start, first-plugin tutorial, contributor setup, and architecture overview each name an observable outcome and a verification owner without duplicating implementation detail.
 - The budget manifest records both target and temporary ceiling, and its check reports or rejects a violated headroom/ratchet state.
@@ -128,7 +129,7 @@ This proposal does not shorten exhaustive facts, merge audience tiers, publish i
 ## Risks
 
 - Metadata can become boilerplate; the package README check therefore permits only fields with current retrieval, template-selection, or bilingual-consistency consumers.
-- Hard sentence limits can fragment explanations or separate a condition from its consequence. The controlled-English word counts remain review prompts, and exact contracts override them.
+- Hard sentence limits can fragment explanations or separate a condition from its consequence. The controlled-English sentence counts remain review prompts, while the separate 100-word package-Summary ceiling bounds only the entry paragraph and leaves exact contracts in the owning sections.
 - Exact line alignment can pressure translators into unnatural prose; review must protect meaning and may revise both sides together rather than weaken one.
 - Splitting generated references can increase routes and link maintenance; prototypes must preserve aliases and measure the trade-off.
 - A semantic check can become a repository-topology scanner that blocks legitimate changes; checks should cover high-risk copied values and representative journeys, while review owns prose meaning.

+ 6 - 5
.agents/notes/proposed/process/2026-08-20-audience-first-documentation-quality.zh.md

@@ -51,7 +51,7 @@ Status: proposed
 [dsh-doc skill](../../../skills/dsh-doc/SKILL.md) 负责这些规则的首个可执行版本。`session-persistence-jsonl` README 对以已交付的追加、恢复与编码行为为证据,而不把其旧版正文当作权威。
 
 - 每个撰写型包 README 都以可搜索 YAML 开头。Skill 风格的 `description` 与按机制推导的 `kind` 为必填字段。四种 kind 与四个技能模板一一对应:`package-group`(组地图)、`package-reference`(插件或服务包)、`package-library`(纯模块入口)与 `package-bundle`(`dsh.bundle.patch`)。对照文件路径、哈希与物理行对齐由支持自动合并的 sidecar 及其门禁负责,因此 README frontmatter 不包含 `i18n` 块。名称已由标题或包 manifest 归属,受众已由文档职责表达;在受治理的标签分类与搜索消费方证明其价值超过全文检索之前,不加入标签。
-- 撰写型页面先写三至五句的 `Summary`,再写带链接的 `Table of Contents`。由格式约束的 Agent Note、事故复盘、生成片段和机器文件保留其必需骨架。
+- 撰写型页面先写三至五句的 `Summary`,再写带链接的 `Table of Contents`。英文包 README 的 Summary 不超过 100 个按 `wc -w` 语义统计的词。它描述读者可见能力,而不是 Cordis 角色、注册项或内部组件;除非读者会在配置、命令或公开 API 中直接使用某个源码标识符,否则不得写入该标识符。由格式约束的 Agent Note、事故复盘、生成片段和机器文件保留其必需骨架。
 - 每个实质章节在子章节、表格或代码之前先给出简短引导,页面则从基础用户用法逐步进入高级开发者与维护者细节。
 - 英文技术正文采用受 ASD-STE100 启发但不宣称认证的清晰度评审:明确行动者与动作,稳定使用术语,使用直接动词,拆分指令与条件,并完整保留情态、例外、时序与数值。指令 20 词和描述 25 词的限制仅作评审提示。准确性高于句长。
 - 包约定留在代码旁。跨包材料有计划地向 `docs/learn/overview/`、`docs/learn/cordis/`、`docs/learn/practices/`、`docs/user/`、`docs/developer/`、`docs/developer/discussion/`、`docs/scratch/` 和平行的 `docs/subsystems/` 层级迁移。
@@ -87,11 +87,11 @@ Status: proposed
 
 1. 创建并验证 `dsh-doc`,再把一组 package README 对改写为行对齐、带元数据的原型,同时不改变运行时事实。
 2. 用新人、用户、开发者和 agent 任务评审渲染后的原型;先修订 skill,再在其他位置强制执行该格式。
-3. 添加聚焦的元数据、章节顺序、行对齐、链接解析和配对 fixture。在每个合并与恢复消费方都有替代支持前,保留伴随文件。
+3. 添加聚焦的元数据、Summary 长度、章节顺序、行对齐、链接解析和配对 fixture。迁移所有违反已接受入口上限的既有包 Summary;在每个合并与恢复消费方都有替代支持前,保留伴随文件。
 4. 把已接受的常驻规则提取到一份规范质量参考,将 `docs/AGENTS.md` 精简到目标以下,并且一次只组织一个内聚的 `docs/` 主题,同时原子地修复链接与导航。
 5. 在 `config-catalog.md` 和 `docs/subsystems/core.md` 上制作生成参考入口层与细节层分离的原型;只有实测查询成本下降且没有丢失事实或造成路由扰动,才把确认后的模式应用到其他位置。
 
-该顺序使每项变更都能独立评审。前三个切片在不重写语料的情况下改进标准与正确性;生成文档原型则在更广的信息架构变更前提供证据。
+该顺序使每项变更都能独立评审。前三个切片改进标准与包入口,而不改变更广的信息架构;生成文档原型则在更广的结构变更前提供证据。
 
 切片 1–3 已按此形式交付:`dsh-doc` 成为合并后的标准(`dsh-doc-standards` 与 `dsh-doc-site-sync` 已并入其中,站点工作流携带修正后的侧边栏值),`session-persistence-jsonl` README 对是参考示例,`pnpm run test:docs` 强制执行元数据、配对与快速文档检查。切片 4–5 仍待完成。
 
@@ -107,7 +107,7 @@ Status: proposed
 
 **把可读性分数作为质量门禁。**不予采纳,因为公式会惩罚精确技术术语,却无法发现错误所有权、遗漏失败行为、陈旧命令或破损的读者路径。
 
-**立即重写或拆分全部语料。**不予采纳,因为现有系统在机制上健康,许多长参考也确实应保持穷尽。原型应先证明检索有所改善,再扩散路由和翻译扰动。
+**立即重写或拆分全部文档语料。**不予采纳,因为现有系统在机制上健康,许多长参考也确实应保持穷尽。范围受限的包 Summary 迁移不会改变路由或穷尽式参考内容;更大的结构变更仍需实测证据。
 
 **保留现有门禁,让评审负责友好程度。**不予采纳,因为陈旧工作流值和预算策略不一致说明,仅凭评审无法保留复制的语义事实,而现有门禁也不询问读者是否能完成任务。
 
@@ -116,6 +116,7 @@ Status: proposed
 - 一份规范质量参考按文档职责定义简短、直观、友好、准确和便于 agent 阅读的文档。
 - `.agents/skills/dsh-doc` 通过验证,并直接链接其元数据、结构或层级及评审或原型参考,而不在 `SKILL.md` 中复制这些参考的详细规则。
 - `session-persistence-jsonl` README 对展示可搜索 YAML、Summary、Table of Contents、从用户到开发者的渐进结构、Further Exploration、结尾 Dev Note、结构一致性和精确行数相等,同时保留已验证的包约定。
+- 每个英文包 README Summary 都不超过 100 个按 `wc -w` 语义统计的词;聚焦门禁报告实测词数,并引导失败项阅读 `dsh-doc` 与所选 kind 模板。
 - `docs/AGENTS.md` 链接该参考,仍足以充当常驻指令,并低于其目标且至少保留 5% 余量。
 - 根级用户路径、Web 快速开始、第一个插件教程、贡献者设置和架构概览各自给出一个可观察结果与验证归属者,同时不复制实现细节。
 - 预算 manifest 同时记录目标与临时上限,其检查会报告或拒绝违反余量或棘轮规则的状态。
@@ -128,7 +129,7 @@ Status: proposed
 ## 风险
 
 - 元数据可能沦为样板;因此包 README 检查只允许具有现行检索、模板选择或双语一致性消费方的字段。
-- 硬性句长限制可能割裂说明,或把条件与后果分开。受控英语的词数限制仅作评审提示,精确约定优先于句长。
+- 硬性句长限制可能割裂说明,或把条件与后果分开。受控英语的句长仅作评审提示;单独的 100 词包 Summary 上限只约束入口段落,精确约定仍保留在其归属章节。
 - 精确行对齐可能迫使译者写出不自然的正文;评审必须保护含义,并可同时修订两侧,而不是削弱其中一侧。
 - 拆分生成参考可能增加路由与链接维护;原型必须保留别名并衡量取舍。
 - 语义检查可能膨胀成阻塞正当变更的仓库拓扑扫描器;检查应覆盖高风险复制值和代表性路径,而正文含义仍由评审负责。

+ 2 - 2
.agents/skills/dsh-doc/SKILL.md

@@ -61,7 +61,7 @@ Open the template before writing and follow its skeleton and rules; it states wh
 
 These rules decide what a section may say. They apply to every authored human-facing page, and to package READMEs with particular force.
 
-- **Summary says what the subject does.** The opening `Summary` and the user-facing sections describe what a user or agent can DO with the subject — outcomes, benefits, when to choose it, main cost — never its role, type, or internal identity. "The seam registers `ctx.x` and appends `x/event` records" is identity narration; "you can save a note per message and it survives restarts" is what it does.
+- **Summary says what the subject does.** The opening `Summary` and the user-facing sections describe what a user or agent can DO with the subject — outcomes, benefits, when to choose it, main cost — never its role, type, or internal identity. In a package Summary, “what it is” means only its reader-visible capability, not its Cordis role, registrations, or internal components. Omit source identifiers unless the reader directly uses them in configuration, a command, or a public API. "The seam registers `ctx.x` and appends `x/event` records" is identity narration; "you can save a note per message and it survives restarts" is what it does.
 - **Developer sections explain, never enumerate.** Folded implementation content covers the overall design concept, architecture, and hand-waving dataflow — enough to understand how the package works — and links code for exact detail. No full API catalogs, exhaustive column lists, event-payload enumerations, or JSDoc restatement inside the folds.
 - **Dev Note is the only slop zone.** Partial ideas, scratches, undecided directions, measured artifacts, and working hypotheses live only in the final Dev Note, marked explicitly non-authoritative. Every other section is polished, current-state prose.
 - **Current state only.** No compatibility shims, migration talk, or history ("previously", "now", "no longer", renamed) outside the Dev Note; the codebase as it is today is the only subject.
@@ -118,7 +118,7 @@ Validate the affected format, not merely Markdown syntax. A strong promise needs
 - Bilingual pages: verify structure, exact line count, terminology, link parity, and the sidecar record.
 - Tutorials: exercise the documented entry path or name an explicit manual verification owner.
 - Generated references: run the deterministic freshness check and report retrieval-size measures.
-- Package READMEs: run model-experience and limitation checks, then package-focused tests when behavior claims changed; re-run every command the README instructs before merging a claim about it.
+- Package READMEs: run the Summary gate, which limits each English Summary to 100 `wc -w`-style words and directs failures back to this skill and the kind template; run model-experience and limitation checks, then package-focused tests when behavior claims changed; re-run every command the README instructs before merging a claim about it.
 - Skills: run the repository's skill-invocation metadata check.
 
 Run `pnpm run test:docs` for the quick comprehensive documentation checks (pairing, wrap, links, README gates, budgets, skill metadata, Agent Note gates) before the full `pnpm run doc-sync`.

+ 2 - 2
.agents/skills/dsh-doc/references/review.md

@@ -32,7 +32,7 @@ Retain a statement only when it helps the target reader act, reason, or avoid mi
 Require the following without forcing one universal internal heading set:
 
 - searchable YAML metadata with a precise `description` and the mechanically derived `kind` (`package-group`, `package-reference`, `package-library`, or `package-bundle`);
-- a three-to-five-sentence Summary that says what the subject DOES for its user or agent reader, with a linked Table of Contents;
+- a three-to-five-sentence English Summary of at most 100 `wc -w`-style words that says what the subject DOES for its user or agent reader, with a linked Table of Contents;
 - controlled English with explicit actors, stable terms, direct verbs, separated instructions and conditions, and unchanged modality;
 - when to choose or avoid the package;
 - a smallest safe configuration or usage path when one exists — for a bundle, the verified `dsh plugin` install path; for a library, the consumer entry point; never profile-install guidance for a shape that does not take it;
@@ -42,7 +42,7 @@ Require the following without forcing one universal internal heading set:
 - newcomer-facing Further Exploration where adjacent docs materially help;
 - a final non-authoritative Dev Note as the only home for partial ideas, scratches, and undecided directions.
 
-Do not restate JSDoc or generated catalogs. Link the owner and explain only the decision or relationship needed locally. Reject any user-facing section that narrates internals (function subjects, event streams, data flow) and any fold that enumerates APIs instead of explaining the concept.
+Do not restate JSDoc or generated catalogs. Link the owner and explain only the decision or relationship needed locally. A package Summary describes reader-visible capability rather than Cordis roles, registrations, or internal components, and it omits source identifiers unless readers directly use them in configuration, commands, or a public API. Reject any user-facing section that narrates internals (function subjects, event streams, data flow) and any fold that enumerates APIs instead of explaining the concept.
 
 ## Reference example
 

+ 1 - 1
.agents/skills/dsh-doc/references/structure-hierarchy.md

@@ -21,7 +21,7 @@ Use this order for authored human-facing pages when the format owner permits it.
 1. YAML metadata.
 2. H1 title.
 3. Language switcher for a bilingual page.
-4. `## Summary`: three to five explanatory sentences stating what the subject is, why a reader would care, the main operating model, and the most important boundary.
+4. `## Summary`: three to five explanatory sentences stating what the reader can do or observe, why a reader would care, the main operating model, and the most important boundary. English package README Summaries stay within the gate-owned 100-word limit.
 5. `## Table of Contents`: links to the page's H2 sections; keep it navigational rather than descriptive.
 6. Stable content, ordered from user-facing use to developer-facing design and operational detail.
 7. Optional `## Further Exploration` for newcomer-oriented links to adjacent subjects.

+ 1 - 1
.agents/skills/dsh-doc/references/style.md

@@ -15,7 +15,7 @@ Page-level style preferences that make DSH pages scannable and difficult to misr
 
 ## Short summary
 
-Open every authored page with a short `Summary`: three to five sentences in one paragraph stating what the subject is, why the reader cares, the operating model, and the most important boundary. The Table of Contents and the sections carry the detail; placement and section order live in [structure-hierarchy.md](structure-hierarchy.md).
+Open every authored page with a short `Summary`: three to five sentences in one paragraph stating what the reader can do or observe, why the reader cares, the operating model, and the most important boundary. The Table of Contents and the sections carry the detail; placement and section order live in [structure-hierarchy.md](structure-hierarchy.md). An English package README Summary is additionally limited to 100 `wc -w`-style words by `verify-package-readme-summaries`.
 
 ## Controlled technical English
 

+ 1 - 1
.agents/skills/dsh-doc/templates/package-bundle.md

@@ -22,7 +22,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Three to five sentences: what a profile gains from this layer, which profiles already include it, how a user adds or removes it, and the main boundary.
+Three to five sentences and at most 100 `wc -w`-style words: what a profile gains from this layer, which profiles already include it, how a user adds or removes it, and the main boundary. Apply the [Summary voice rules](../SKILL.md#voice-rules).
 
 ## Table of Contents
 

+ 1 - 1
.agents/skills/dsh-doc/templates/package-group.md

@@ -20,7 +20,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Three to five sentences: what the family provides, what a reader can DO with it, which package owns which half, and the main boundary.
+Three to five sentences and at most 100 `wc -w`-style words: what the family provides, what a reader can DO with it, which package owns which half, and the main boundary. Apply the [Summary voice rules](../SKILL.md#voice-rules).
 
 ## Table of Contents
 

+ 1 - 1
.agents/skills/dsh-doc/templates/package-library.md

@@ -22,7 +22,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Three to five sentences: what a caller can DO with the library, who consumes it, the smallest entry point, and the main boundary.
+Three to five sentences and at most 100 `wc -w`-style words: what a caller can DO with the library, who consumes it, the smallest entry point, and the main boundary. Apply the [Summary voice rules](../SKILL.md#voice-rules).
 
 ## Table of Contents
 

+ 1 - 1
.agents/skills/dsh-doc/templates/package-reference.md

@@ -20,7 +20,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Three to five sentences on what a user or agent can DO with the package: outcomes, when to choose it, main cost, most important boundary. Never its role, type, or internal identity.
+Three to five sentences and at most 100 `wc -w`-style words on what a user or agent can DO with the package: outcomes, when to choose it, main cost, most important boundary. Apply the [Summary voice rules](../SKILL.md#voice-rules); never describe its role, type, or internal identity.
 
 ## Table of Contents
 

+ 2 - 2
.github/review-ownership/CODEOWNERS

@@ -18,7 +18,7 @@
 /packages/code-runtime/ @Chinesezjc
 /packages/compaction/ @imccyu
 /packages/context/ @turtle1999
-/packages/core/ @tianyicui @turtle1999 @mektpoy
+/packages/core/ @turtle1999 @mektpoy
 /packages/credentials/ @mektpoy
 /packages/e2b/ @mektpoy
 /packages/experimental/ @mektpoy
@@ -41,7 +41,7 @@
 /packages/sandbox/ @mektpoy
 /packages/schedule/ @imccyu
 /packages/sdk/ @mektpoy
-/packages/session/ @tianyicui @turtle1999 @mektpoy
+/packages/session/ @turtle1999 @mektpoy
 /packages/session-query/ @mektpoy
 /packages/settings/ @mektpoy
 /packages/shell/ @mektpoy

+ 2 - 2
.github/review-ownership/README.i18n.yaml

@@ -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 .github/review-ownership/README.md
-README.md: 95dd52595bacb9b2b00bc475966f328b06098a14
-README.zh.md: a20a5f4ae41b5d0418601afa853ac2fbc141cc96
+README.md: 4c412a7c47f114e0aa6aa9a3048fd7f833f0b0e3
+README.zh.md: 1db7d1b56d4927a83c502557f393cb6518dc09f1

+ 13 - 7
.github/review-ownership/README.md

@@ -4,12 +4,12 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The [`request-review` workflow](../workflows/request-review.yml) reads the CODEOWNERS-compatible [ownership map](CODEOWNERS) from the trusted default branch. It prints the complete changed non-test file list, matches those files to owners, and then requests the missing reviewers. The ownership map is outside GitHub's native CODEOWNERS locations, so GitHub does not apply it directly.
+The [`request-review` workflow](../workflows/request-review.yml) reads the CODEOWNERS-compatible [ownership map](CODEOWNERS) from the trusted default branch. It classifies changed files, requests missing owners for reviewable code, and cancels its outstanding requests when a pull request becomes a draft. The ownership map is outside GitHub's native CODEOWNERS locations, so GitHub does not apply it directly.
 
 ## Table of Contents
 
 - [Routing](#routing)
-- [Test exclusion](#test-exclusion)
+- [Review exclusions](#review-exclusions)
 - [Security](#security)
 - [Verification](#verification)
 - [Dev Note](#dev-note)
@@ -18,20 +18,26 @@ The [`request-review` workflow](../workflows/request-review.yml) reads the CODEO
 
 ## Routing
 
-Non-draft pull requests run the workflow when opened, synchronized, reopened, or marked ready for review. The scanner fetches the complete pull-request file list, evaluates both paths of a rename, and fails instead of routing from a partial list. GitHub exposes at most 3,000 files for this API.
+Pull requests run the workflow when opened, synchronized, reopened, marked ready for review, or converted to a draft. The scanner fetches the complete pull-request file list, evaluates both paths of a rename, and fails instead of routing from a partial list. GitHub exposes at most 3,000 files for this API.
 
-The ownership map accepts explicit absolute directory patterns and individual GitHub users. It rejects wildcards, hidden-directory patterns, teams, and duplicate patterns or owners. Matching follows CODEOWNERS last-match semantics. The scanner prints `Changed code files`, `Excluded test files`, `Owners by changed file`, and `Reviewers to request` before it sends the review request. Unmatched files remain visible in the log. The pull-request author and users who are already requested are omitted.
+For a non-draft pull request, the workflow requests missing matched owners while keeping the total number of current individual review requests at two or fewer. Existing individual requests consume those slots, including requests made by people outside the ownership map. When more candidates remain than available slots, login order selects the reviewers deterministically. The workflow does not remove requests from a non-draft pull request. For a draft, it reads the current requested reviewers and review-request timeline, then cancels each current request whose latest requester is `github-actions[bot]`. Current requests made by people remain unchanged. The workflow fails without cancellation when the timeline exceeds 3,000 events or contains invalid request provenance.
+
+The ownership map accepts explicit absolute directory patterns and one or two individual GitHub users per pattern. It rejects wildcards, hidden-directory patterns, teams, more than two owners, and duplicate patterns or owners. Matching follows CODEOWNERS last-match semantics. The scanner prints the changed code, excluded test, documentation, and comment-only files; per-file owner matches; and the reviewers it will request or cancel before it mutates review requests. Unmatched files remain visible in the log. The pull-request author and users who are already requested are omitted from new requests.
 
 The policy test measures non-test tracked lines under matched directories and requires `@turtle1999` to own no more than one third of that eligible owned codebase.
 
-<a id="test-exclusion"></a>
+<a id="review-exclusions"></a>
 
-## Test exclusion
+## Review exclusions
 
 Review routing excludes the repository's unit, end-to-end, expected-output, snapshot, benchmark, performance, stress, corpus, native, and Python test conventions. This includes `test`, `tests`, `__tests__`, `__snapshots__`, `benches`, and `stress-tests` directories; the top-level `benchmarks` and `snapshots` trees; `packages/test-support`; `scripts/fixtures` and `scripts/snapshots`; recognized test filename suffixes; and Python `test_*.py` or `*_test.py` files.
 
 Test infrastructure that can alter how evidence is produced remains reviewable, including `vitest*.config.ts` and gate implementations under `scripts`. A production file named `test.ts`, `spec.ts`, or `snapshot.ts` is not excluded solely by that name.
 
+Files ending in `.md` or `.yaml`, with case-insensitive extension matching, are documentation and never contribute owners. A `.yml` file remains reviewable unless another exclusion applies.
+
+For a modified file with a supported source extension, the scanner compares the pre-change and post-change text after removing parsed comments. It excludes the file only when GitHub supplies a patch whose counted additions and deletions prove that the patch is complete and the remaining code is identical. The parser recognizes C-style line and block comments, hash comments, SQL comments, CSS block comments, and HTML comments for their declared extensions. Renames, unsupported languages, missing or partial patches, and uncertain comment forms remain reviewable.
+
 <a id="security"></a>
 
 ## Security
@@ -44,7 +50,7 @@ Ownership changes take effect only after they merge into the default branch. Thi
 
 ## Verification
 
-Run `pnpm run test:request-review` for ownership parsing, test classification, pagination, logging order, reviewer filtering, and API behavior. [Workflow tests](../../scripts/ci-workflow.spec.ts) pin the trusted checkout, permissions, events, and command. The repository gate graph runs both checks in CI.
+Run `pnpm run test:request-review` for ownership parsing, file classification, complete-patch checks, comment parsing, pagination, logging order, draft cancellation, reviewer filtering, and API behavior. [Workflow tests](../../scripts/ci-workflow.spec.ts) pin the trusted checkout, permissions, events, and command. The repository gate graph runs both checks in CI.
 
 <a id="dev-note"></a>
 

+ 13 - 7
.github/review-ownership/README.zh.md

@@ -4,12 +4,12 @@
 
 ## 概要
 
-[`request-review` workflow](../workflows/request-review.yml) 从受信任的默认分支读取兼容 CODEOWNERS 格式的[所有权映射](CODEOWNERS)。它先打印完整的非测试变更文件列表,再将这些文件与 owner 匹配,最后请求尚未加入的评审者。所有权映射不在 GitHub 原生 CODEOWNERS 路径中,因此 GitHub 不会直接应用它。
+[`request-review` workflow](../workflows/request-review.yml) 从受信任的默认分支读取兼容 CODEOWNERS 格式的[所有权映射](CODEOWNERS)。它会对变更文件分类,为可评审代码请求尚未加入的 owner,并在 PR 转为草稿时取消自己尚未完成的请求。所有权映射不在 GitHub 原生 CODEOWNERS 路径中,因此 GitHub 不会直接应用它。
 
 ## 目录
 
 - [路由](#routing)
-- [排除测试](#test-exclusion)
+- [评审排除规则](#review-exclusions)
 - [安全性](#security)
 - [验证](#verification)
 - [开发说明](#dev-note)
@@ -18,20 +18,26 @@
 
 ## 路由
 
-非草稿 PR 在创建、同步、重新打开或标记为可评审时运行该 workflow。扫描器获取完整的 PR 文件列表,分别检查重命名前后的路径;如果只能取得部分列表,则停止执行,不发出评审请求。GitHub 对此 API 最多公开 3,000 个文件。
+PR 在创建、同步、重新打开、标记为可评审或转为草稿时运行该 workflow。扫描器获取完整的 PR 文件列表,分别检查重命名前后的路径;如果只能取得部分列表,则停止执行,不发出评审请求。GitHub 对此 API 最多公开 3,000 个文件。
 
-所有权映射只接受显式绝对目录模式和 GitHub 个人用户。通配符、隐藏目录模式、团队,以及重复的模式或 owner 都会被拒绝。匹配遵循 CODEOWNERS 的最后一条匹配规则。扫描器先打印 `Changed code files`、`Excluded test files`、`Owners by changed file` 和 `Reviewers to request`,再发送评审请求。未匹配的文件仍显示在日志中。PR 作者和已经收到评审请求的用户会被排除。
+对于非草稿 PR,workflow 会请求尚未加入的匹配 owner,同时确保当前个人评审请求总数不超过两个。现有个人请求会占用名额,包括由人员向所有权映射之外用户发出的请求。当剩余候选人数超过可用名额时,workflow 会按登录名顺序确定评审者。Workflow 不会从非草稿 PR 移除请求。对于草稿,workflow 会读取当前评审请求和评审请求时间线,然后取消最近一次请求者为 `github-actions[bot]` 的当前请求。由人员发出的当前请求保持不变。如果时间线超过 3,000 个事件或包含无效的请求来源,workflow 会失败且不执行取消操作。
+
+所有权映射只接受显式绝对目录模式,并允许每条模式配置一至两名 GitHub 个人用户。通配符、隐藏目录模式、团队、超过两名 owner,以及重复的模式或 owner 都会被拒绝。匹配遵循 CODEOWNERS 的最后一条匹配规则。扫描器会在修改评审请求前,打印变更代码文件、排除的测试文件、文档文件和纯注释文件,逐文件 owner 匹配结果,以及将要请求或取消的评审者。未匹配的文件仍显示在日志中。PR 作者和已经收到评审请求的用户不会收到新请求。
 
 策略测试会统计已匹配目录下的非测试跟踪文件行数,并要求 `@turtle1999` 拥有的有效代码库不超过三分之一。
 
-<a id="test-exclusion"></a>
+<a id="review-exclusions"></a>
 
-## 排除测试
+## 评审排除规则
 
 评审路由会排除仓库中的单元测试、端到端测试、预期输出、快照、基准测试、性能测试、压力测试、语料测试、原生测试和 Python 测试约定。其中包括 `test`、`tests`、`__tests__`、`__snapshots__`、`benches` 和 `stress-tests` 目录,顶层 `benchmarks` 和 `snapshots` 目录树,`packages/test-support`、`scripts/fixtures` 和 `scripts/snapshots`,可识别的测试文件名后缀,以及 Python 的 `test_*.py` 或 `*_test.py` 文件。
 
 能够改变证据生成方式的测试基础设施仍需评审,包括 `vitest*.config.ts` 和 `scripts` 下的门禁实现。生产文件不会仅因名称为 `test.ts`、`spec.ts` 或 `snapshot.ts` 而被排除。
 
+扩展名以不区分大小写方式匹配。所有以 `.md` 或 `.yaml` 结尾的文件均视为文档,不会贡献 owner;除非符合其他排除规则,否则 `.yml` 文件仍需评审。
+
+对于具有受支持源码扩展名的修改文件,扫描器会移除解析出的注释,再比较变更前后的文本。只有 GitHub 提供的 patch 中增删行数与文件记录一致、能够证明 patch 完整,且其余代码完全相同时,扫描器才会排除该文件。解析器会按声明的扩展名识别 C 风格行注释和块注释、井号注释、SQL 注释、CSS 块注释与 HTML 注释。重命名、不受支持的语言、缺失或不完整的 patch,以及无法确定的注释形式仍需评审。
+
 <a id="security"></a>
 
 ## 安全性
@@ -44,7 +50,7 @@
 
 ## 验证
 
-运行 `pnpm run test:request-review` 可检查所有权解析、测试分类、分页、日志顺序、评审者过滤和 API 行为。[Workflow 测试](../../scripts/ci-workflow.spec.ts)固定受信任检出、权限、事件和命令。仓库门禁图会在 CI 中运行这两类检查。
+运行 `pnpm run test:request-review` 可检查所有权解析、文件分类、完整 patch 检查、注释解析、分页、日志顺序、草稿取消、评审者过滤和 API 行为。[Workflow 测试](../../scripts/ci-workflow.spec.ts)固定受信任检出、权限、事件和命令。仓库门禁图会在 CI 中运行这两类检查。
 
 <a id="dev-note"></a>
 

+ 258 - 22
.github/review-ownership/request-review.mjs

@@ -5,11 +5,23 @@ import process from 'node:process'
 import { pathToFileURL } from 'node:url'
 
 const API_VERSION = '2026-03-10'
+const MAX_OWNERS_PER_RULE = 2
 const MAX_PULL_REQUEST_FILES = 3_000
+const MAX_REQUESTED_REVIEWERS = 2
+const MAX_TIMELINE_EVENTS = 3_000
 const PAGE_SIZE = 100
+const WORKFLOW_REVIEW_REQUESTER = 'github-actions[bot]'
 const TEST_DIRECTORY_NAMES = new Set(['__snapshots__', '__tests__', 'benches', 'stress-tests', 'test', 'tests'])
 const TEST_FILE_MARKER = /\.(?:bench|corpus|e2e|perf|snapshot|spec|stress|test)\.[^./]+$/u
 const PYTHON_TEST_FILE = /^(?:test_.+|.+_tests?)\.py$/u
+const DOCUMENTATION_FILE = /\.(?:md|yaml)$/iu
+const C_STYLE_EXTENSIONS = new Set([
+  'c', 'cc', 'cjs', 'cpp', 'cts', 'cxx', 'go', 'h', 'hpp', 'java', 'js', 'jsx',
+  'kt', 'kts', 'less', 'mjs', 'mts', 'rs', 'scss', 'swift', 'ts', 'tsx',
+])
+const BLOCK_COMMENT_EXTENSIONS = new Set(['css'])
+const HASH_COMMENT_EXTENSIONS = new Set(['bash', 'ps1', 'py', 'pyi', 'r', 'rb', 'sh', 'toml', 'yml', 'zsh'])
+const HTML_COMMENT_EXTENSIONS = new Set(['htm', 'html'])
 
 /**
  * Parse the explicit directory subset accepted from the review ownership file.
@@ -30,6 +42,9 @@ export function parseOwnership(source) {
     if (pattern.startsWith('/.')) throw new Error(`${location}: hidden-directory patterns are not allowed`)
     if (patterns.has(pattern)) throw new Error(`${location}: duplicate pattern ${JSON.stringify(pattern)}`)
     if (owners.length === 0) throw new Error(`${location}: expected at least one owner`)
+    if (owners.length > MAX_OWNERS_PER_RULE) {
+      throw new Error(`${location}: expected at most ${MAX_OWNERS_PER_RULE} owners`)
+    }
     const normalizedOwners = []
     const seenOwners = new Set()
     for (const owner of owners) {
@@ -83,27 +98,174 @@ export function isTestPath(value) {
 }
 
 /**
- * Expand changed-file records into reviewable and excluded repository paths.
+ * Decide whether a repository path is documentation excluded from review routing.
+ * @param {string} value Repository-relative path.
+ * @returns {boolean} Whether the path has an excluded documentation extension.
+ */
+export function isDocumentationPath(value) {
+  return DOCUMENTATION_FILE.test(normalizeRepositoryPath(value))
+}
+
+/**
+ * Decide whether a complete modified-file patch changes comments only.
+ * @param {unknown} value GitHub changed-file record.
+ * @returns {boolean} Whether supported comment parsing removes every changed token.
+ */
+export function isCommentOnlyChange(value) {
+  if (!isRecord(value) || value.status !== 'modified' || typeof value.filename !== 'string'
+    || typeof value.patch !== 'string' || !Number.isSafeInteger(value.additions)
+    || value.additions < 0 || !Number.isSafeInteger(value.deletions) || value.deletions < 0) return false
+  const syntax = commentSyntax(value.filename)
+  if (syntax === undefined) return false
+  if (value.filename.toLowerCase().endsWith('.rs') && /\b(?:br|r)#{0,255}"/u.test(value.patch)) return false
+  const hunks = parsePatchHunks(value.patch)
+  if (hunks === undefined || hunks.additions !== value.additions || hunks.deletions !== value.deletions) {
+    return false
+  }
+  return hunks.values.every(({ before, after }) =>
+    normalizedCode(before, syntax) === normalizedCode(after, syntax))
+}
+
+function commentSyntax(filename) {
+  const normalized = normalizeRepositoryPath(filename)
+  const basename = normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase()
+  const extension = basename.includes('.') ? basename.slice(basename.lastIndexOf('.') + 1) : ''
+  const line = []
+  const block = []
+  if (C_STYLE_EXTENSIONS.has(extension)) {
+    line.push('//')
+    block.push(['/*', '*/'])
+  }
+  if (BLOCK_COMMENT_EXTENSIONS.has(extension)) block.push(['/*', '*/'])
+  if (HASH_COMMENT_EXTENSIONS.has(extension) || basename === 'dockerfile' || basename.startsWith('dockerfile.')
+    || basename === 'makefile' || basename.startsWith('makefile.')) line.push('#')
+  if (extension === 'sql') {
+    line.push('--')
+    block.push(['/*', '*/'])
+  }
+  if (HTML_COMMENT_EXTENSIONS.has(extension)) block.push(['<!--', '-->'])
+  return line.length === 0 && block.length === 0 ? undefined : { line, block }
+}
+
+function parsePatchHunks(patch) {
+  const values = []
+  let current
+  let additions = 0
+  let deletions = 0
+  for (const line of patch.split('\n')) {
+    if (line.startsWith('@@')) {
+      current = { before: [], after: [] }
+      values.push(current)
+      continue
+    }
+    if (current === undefined || line.startsWith('\\ No newline at end of file')) continue
+    const prefix = line[0]
+    const content = line.slice(1)
+    if (prefix === ' ') {
+      current.before.push(content)
+      current.after.push(content)
+    } else if (prefix === '-') {
+      current.before.push(content)
+      deletions++
+    } else if (prefix === '+') {
+      current.after.push(content)
+      additions++
+    }
+  }
+  return values.length === 0 ? undefined : { values, additions, deletions }
+}
+
+function normalizedCode(lines, syntax) {
+  return stripComments(lines.join('\n'), syntax)
+    .split('\n')
+    .map(line => line.trimEnd())
+    .filter(line => line.trim().length > 0)
+    .join('\n')
+}
+
+function stripComments(source, syntax) {
+  let result = ''
+  let quote
+  let blockEnd
+  for (let index = 0; index < source.length;) {
+    if (blockEnd !== undefined) {
+      if (source.startsWith(blockEnd, index)) {
+        index += blockEnd.length
+        blockEnd = undefined
+      } else {
+        index++
+      }
+      continue
+    }
+    const character = source[index]
+    if (quote !== undefined) {
+      result += character
+      index++
+      if (character === '\\' && index < source.length) {
+        result += source[index]
+        index++
+      } else if (character === quote) {
+        quote = undefined
+      }
+      continue
+    }
+    if (character === '\'' || character === '"' || character === '`') {
+      quote = character
+      result += character
+      index++
+      continue
+    }
+    const block = syntax.block.find(([start]) => source.startsWith(start, index))
+    if (block !== undefined) {
+      index += block[0].length
+      blockEnd = block[1]
+      continue
+    }
+    const line = syntax.line.find(marker => source.startsWith(marker, index))
+    const lineStart = index === 0 || source[index - 1] === '\n'
+    const hashStartsComment = line !== '#' || lineStart || /\s/u.test(source[index - 1] ?? '')
+    if (line !== undefined && hashStartsComment && !(line === '#' && lineStart && source[index + 1] === '!')) {
+      const newline = source.indexOf('\n', index + line.length)
+      if (newline === -1) break
+      result += '\n'
+      index = newline + 1
+      continue
+    }
+    result += character
+    index++
+  }
+  return result
+}
+
+/**
+ * Expand changed-file records into reviewable, test, documentation, and comment-only paths.
  * @param {unknown[]} files Pull-request file records from GitHub.
- * @returns {{changedCodeFiles: string[], excludedTestFiles: string[]}} Classified paths.
+ * @returns {{changedCodeFiles: string[], excludedTestFiles: string[], excludedDocumentationFiles: string[], excludedCommentOnlyFiles: string[]}} Classified paths.
  */
 export function classifyChangedFiles(files) {
   const changedCodeFiles = new Set()
   const excludedTestFiles = new Set()
+  const excludedDocumentationFiles = new Set()
+  const excludedCommentOnlyFiles = new Set()
   for (const entry of files) {
     if (!isRecord(entry)) throw new Error('changed-file response contains a non-object entry')
     const paths = [normalizeRepositoryPath(entry.filename)]
+    const commentOnly = isCommentOnlyChange(entry)
     if (entry.previous_filename !== undefined) {
       paths.unshift(normalizeRepositoryPath(entry.previous_filename))
     }
     for (const file of paths) {
       if (isTestPath(file)) excludedTestFiles.add(file)
+      else if (isDocumentationPath(file)) excludedDocumentationFiles.add(file)
+      else if (commentOnly) excludedCommentOnlyFiles.add(file)
       else changedCodeFiles.add(file)
     }
   }
   return {
     changedCodeFiles: [...changedCodeFiles].sort(),
     excludedTestFiles: [...excludedTestFiles].sort(),
+    excludedDocumentationFiles: [...excludedDocumentationFiles].sort(),
+    excludedCommentOnlyFiles: [...excludedCommentOnlyFiles].sort(),
   }
 }
 
@@ -190,23 +352,81 @@ export async function listPullRequestFiles(api, repository, pullNumber, expected
 }
 
 /**
- * Print changed paths, route owners, and request every missing eligible reviewer.
+ * Fetch the pull request timeline used to identify workflow-authored review requests.
+ * @param {(path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>} api GitHub API caller.
+ * @param {string} repository Owner/name repository identifier.
+ * @param {number} pullNumber Pull-request number.
+ * @returns {Promise<unknown[]>} Complete timeline event list within the supported limit.
+ */
+export async function listPullRequestTimeline(api, repository, pullNumber) {
+  const events = []
+  for (let page = 1; ; page++) {
+    const response = await api(`/repos/${repository}/issues/${pullNumber}/timeline?per_page=${PAGE_SIZE}&page=${page}`)
+    if (!Array.isArray(response)) throw new Error('pull-request timeline response is not an array')
+    events.push(...response)
+    if (response.length < PAGE_SIZE) return events
+    if (events.length >= MAX_TIMELINE_EVENTS) {
+      throw new Error(`pull-request timeline exceeds ${MAX_TIMELINE_EVENTS} events`)
+    }
+  }
+}
+
+/** Return current requested reviewers whose latest request came from this workflow identity. */
+function workflowRequestedReviewers(events, requestedReviewers) {
+  const requested = new Map(requestedReviewers.map(login => [login.toLowerCase(), login]))
+  const latestRequester = new Map()
+  for (const event of events) {
+    if (!isRecord(event) || event.event !== 'review_requested') continue
+    if (!isRecord(event.requested_reviewer) || typeof event.requested_reviewer.login !== 'string') continue
+    const key = event.requested_reviewer.login.toLowerCase()
+    if (!requested.has(key)) continue
+    if (!isRecord(event.review_requester) || typeof event.review_requester.login !== 'string') {
+      throw new Error('review-request timeline event has no requester login')
+    }
+    latestRequester.set(key, event.review_requester.login.toLowerCase())
+  }
+  return [...requested]
+    .filter(([key]) => latestRequester.get(key) === WORKFLOW_REVIEW_REQUESTER)
+    .map(([, login]) => login)
+}
+
+/** Extract and validate individual logins from GitHub's requested-reviewer response. */
+function requestedReviewerLogins(response) {
+  if (!isRecord(response) || !Array.isArray(response.users)) {
+    throw new Error('requested-reviewers response has no users array')
+  }
+  return response.users.map((user) => {
+    if (!isRecord(user) || typeof user.login !== 'string') {
+      throw new Error('requested-reviewers response contains an invalid user')
+    }
+    return user.login
+  })
+}
+
+/**
+ * Print changed paths, request missing owners on reviewable pull requests, and
+ * cancel workflow-authored requests on drafts.
  * @param {{event: unknown, ownershipSource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, write?: (line: string) => void}} options Runtime inputs.
- * @returns {Promise<{changedCodeFiles: string[], excludedTestFiles: string[], requestedReviewers: string[]}>} Applied routing result.
+ * @returns {Promise<{changedCodeFiles: string[], excludedTestFiles: string[], excludedDocumentationFiles: string[], excludedCommentOnlyFiles: string[], requestedReviewers: string[], cancelledReviewers: string[]}>} Applied routing result.
  */
 export async function requestReviews({ event, ownershipSource, api, write = line => process.stdout.write(`${line}\n`) }) {
   const pull = pullRequestFromEvent(event)
   write('This is by automated Angry Turtle Cyborg, not a human')
-  if (pull.draft) {
-    write('Draft pull request; reviewer routing is deferred until ready_for_review.')
-    return { changedCodeFiles: [], excludedTestFiles: [], requestedReviewers: [] }
-  }
-
   const files = await listPullRequestFiles(api, pull.repository, pull.number, pull.changedFileCount)
   const classified = classifyChangedFiles(files)
   const plan = planReviewers(parseOwnership(ownershipSource), classified.changedCodeFiles)
   writeList(write, 'Changed code files', classified.changedCodeFiles.map(file => JSON.stringify(file)))
   writeList(write, 'Excluded test files', classified.excludedTestFiles.map(file => JSON.stringify(file)))
+  writeList(
+    write,
+    'Excluded documentation files',
+    classified.excludedDocumentationFiles.map(file => JSON.stringify(file)),
+  )
+  writeList(
+    write,
+    'Excluded comment-only files',
+    classified.excludedCommentOnlyFiles.map(file => JSON.stringify(file)),
+  )
   writeList(
     write,
     'Owners by changed file',
@@ -214,30 +434,46 @@ export async function requestReviews({ event, ownershipSource, api, write = line
   )
 
   const candidates = plan.reviewers.filter(login => login.toLowerCase() !== pull.author.toLowerCase())
+  if (pull.draft) {
+    const existing = await api(`/repos/${pull.repository}/pulls/${pull.number}/requested_reviewers`)
+    const requestedReviewers = requestedReviewerLogins(existing)
+    const reviewers = requestedReviewers.length === 0
+      ? []
+      : workflowRequestedReviewers(
+          await listPullRequestTimeline(api, pull.repository, pull.number),
+          requestedReviewers,
+        )
+    writeList(write, 'Review requests to cancel', reviewers.map(login => `@${login}`))
+    if (reviewers.length === 0) return { ...classified, requestedReviewers: [], cancelledReviewers: [] }
+
+    await api(`/repos/${pull.repository}/pulls/${pull.number}/requested_reviewers`, {
+      method: 'DELETE',
+      body: { reviewers },
+    })
+    const requestLabel = reviewers.length === 1 ? 'request' : 'requests'
+    write(`Cancelled review ${requestLabel} for ${reviewers.map(login => `@${login}`).join(' ')}.`)
+    return { ...classified, requestedReviewers: [], cancelledReviewers: reviewers }
+  }
+
   if (candidates.length === 0) {
     writeList(write, 'Reviewers to request', [])
-    return { ...classified, requestedReviewers: [] }
+    return { ...classified, requestedReviewers: [], cancelledReviewers: [] }
   }
   const existing = await api(`/repos/${pull.repository}/pulls/${pull.number}/requested_reviewers`)
-  if (!isRecord(existing) || !Array.isArray(existing.users)) {
-    throw new Error('requested-reviewers response has no users array')
-  }
-  const alreadyRequested = new Set(existing.users.map((user) => {
-    if (!isRecord(user) || typeof user.login !== 'string') {
-      throw new Error('requested-reviewers response contains an invalid user')
-    }
-    return user.login.toLowerCase()
-  }))
-  const reviewers = candidates.filter(login => !alreadyRequested.has(login.toLowerCase()))
+  const alreadyRequested = new Set(requestedReviewerLogins(existing).map(login => login.toLowerCase()))
+  const availableSlots = Math.max(0, MAX_REQUESTED_REVIEWERS - alreadyRequested.size)
+  const reviewers = candidates
+    .filter(login => !alreadyRequested.has(login.toLowerCase()))
+    .slice(0, availableSlots)
   writeList(write, 'Reviewers to request', reviewers.map(login => `@${login}`))
-  if (reviewers.length === 0) return { ...classified, requestedReviewers: [] }
+  if (reviewers.length === 0) return { ...classified, requestedReviewers: [], cancelledReviewers: [] }
 
   await api(`/repos/${pull.repository}/pulls/${pull.number}/requested_reviewers`, {
     method: 'POST',
     body: { reviewers },
   })
   write(`Requested ${reviewers.map(login => `@${login}`).join(' ')}.`)
-  return { ...classified, requestedReviewers: reviewers }
+  return { ...classified, requestedReviewers: reviewers, cancelledReviewers: [] }
 }
 
 function pullRequestFromEvent(event) {

+ 201 - 26
.github/review-ownership/request-review.test.mjs

@@ -1,13 +1,16 @@
 import assert from 'node:assert/strict'
 import { execFileSync } from 'node:child_process'
-import { readFileSync } from 'node:fs'
+import { existsSync, readFileSync } from 'node:fs'
 import test from 'node:test'
 
 import {
   classifyChangedFiles,
   createGitHubApi,
+  isCommentOnlyChange,
+  isDocumentationPath,
   isTestPath,
   listPullRequestFiles,
+  listPullRequestTimeline,
   normalizeRepositoryPath,
   parseOwnership,
   planReviewers,
@@ -35,30 +38,29 @@ test('loads the repository ownership policy without test-only directory rules',
   assert.equal(rules.some(rule => rule.pattern === '/packages/test-support/'), false)
   assert.deepEqual(ownersByPattern.get('/apps/cli/'), ['@turtle1999'])
   assert.deepEqual(ownersByPattern.get('/docs/'), ['@turtle1999'])
-  assert.deepEqual(ownersByPattern.get('/packages/core/'), ['@tianyicui', '@turtle1999', '@mektpoy'])
+  assert.deepEqual(ownersByPattern.get('/packages/core/'), ['@turtle1999', '@mektpoy'])
   assert.deepEqual(ownersByPattern.get('/packages/llm/'), ['@LegGasai'])
   assert.deepEqual(ownersByPattern.get('/packages/preset/'), ['@LegGasai', '@turtle1999'])
-  assert.deepEqual(ownersByPattern.get('/packages/session/'), ['@tianyicui', '@turtle1999', '@mektpoy'])
+  assert.deepEqual(ownersByPattern.get('/packages/session/'), ['@turtle1999', '@mektpoy'])
   assert.deepEqual(ownersByPattern.get('/packages/subagent/'), ['@Dudu-0223'])
   assert.deepEqual(ownersByPattern.get('/packages/web/'), ['@imccyu'])
   assert.deepEqual(ownersByPattern.get('/python/'), ['@LegGasai'])
   assert.deepEqual(ownersByPattern.get('/website/'), ['@LegGasai'])
-  assert.deepEqual(
-    rules.filter(rule => rule.owners.includes('@tianyicui')).map(rule => rule.pattern),
-    ['/packages/core/', '/packages/session/'],
-  )
-  for (const excludedOwner of ['@kermeanx', '@pkh-xht']) {
+  assert.equal(rules.every(rule => rule.owners.length <= 2), true)
+  for (const excludedOwner of ['@tianyicui', '@kermeanx', '@pkh-xht']) {
     assert.equal(rules.some(rule => rule.owners.some(owner => owner.toLowerCase() === excludedOwner)), false)
   }
 })
 
 test('keeps turtle below one third of the eligible owned codebase', () => {
   const rules = parseOwnership(ownershipSource)
-  const trackedFiles = execFileSync('git', ['ls-files', '-z'], { encoding: 'utf8' }).split('\0').filter(Boolean)
+  const trackedFiles = execFileSync('git', ['ls-files', '-z'], { encoding: 'utf8' })
+    .split('\0')
+    .filter(file => file && existsSync(file))
   let ownedLines = 0
   let turtleLines = 0
   for (const file of trackedFiles) {
-    if (isTestPath(file)) continue
+    if (isTestPath(file) || isDocumentationPath(file)) continue
     const owners = planReviewers(rules, [file]).matches[0]?.owners ?? []
     if (owners.length === 0) continue
     const content = readFileSync(file)
@@ -82,6 +84,7 @@ test('rejects ownership forms the requester cannot apply safely', () => {
     ['/packages/*/ @owner\n', /explicit absolute directory/u],
     ['/packages/core/\n', /at least one owner/u],
     ['/packages/core/ @org/team\n', /individual GitHub users/u],
+    ['/packages/core/ @one @two @three\n', /at most 2 owners/u],
     ['/packages/core/ @owner @OWNER\n', /duplicate owner/u],
     ['/packages/core/ @owner\n/packages/core/ @other\n', /duplicate pattern/u],
   ]) {
@@ -132,6 +135,76 @@ test('does not confuse production names with tests', () => {
   }
 })
 
+test('excludes Markdown and YAML documentation extensions', () => {
+  for (const file of [
+    'README.md',
+    'docs/architecture.MD',
+    'packages/subagent/subagent/guide.yaml',
+    'profiles/example.YAML',
+  ]) {
+    assert.equal(isDocumentationPath(file), true, file)
+  }
+  for (const file of [
+    '.github/workflows/request-review.yml',
+    'packages/subagent/subagent/src/index.ts',
+    'website/docs.ts',
+  ]) {
+    assert.equal(isDocumentationPath(file), false, file)
+  }
+})
+
+test('detects comment-only changes only from complete supported patches', () => {
+  for (const file of [
+    {
+      filename: 'packages/core/agent/src/index.ts',
+      status: 'modified', additions: 1, deletions: 1,
+      patch: '@@ -1,2 +1,2 @@\n-// old note\n+// new note\n const value = "https://example.com"',
+    },
+    {
+      filename: 'python/sdk/src/client.py',
+      status: 'modified', additions: 1, deletions: 1,
+      patch: '@@ -1 +1 @@\n-value = 1  # old note\n+value = 1  # new note',
+    },
+    {
+      filename: 'native/landlock-run/src/main.rs',
+      status: 'modified', additions: 1, deletions: 1,
+      patch: '@@ -1 +1 @@\n-let value = 1; /* old note */\n+let value = 1; /* new note */',
+    },
+  ]) {
+    assert.equal(isCommentOnlyChange(file), true, file.filename)
+  }
+
+  for (const file of [
+    {
+      filename: 'packages/core/agent/src/index.ts',
+      status: 'modified', additions: 1, deletions: 1,
+      patch: '@@ -1 +1 @@\n-const value = 1 // note\n+const value = 2 // note',
+    },
+    {
+      filename: 'packages/core/agent/src/index.ts',
+      status: 'modified', additions: 2, deletions: 1,
+      patch: '@@ -1 +1 @@\n-// old note\n+// new note',
+    },
+    {
+      filename: 'packages/core/agent/src/data.json',
+      status: 'modified', additions: 1, deletions: 1,
+      patch: '@@ -1 +1 @@\n-{"value":1}\n+{"value":2}',
+    },
+    {
+      filename: 'native/landlock-run/src/main.rs',
+      status: 'modified', additions: 1, deletions: 1,
+      patch: '@@ -1 +1 @@\n-let value = r#"https://old.example"#;\n+let value = r#"https://new.example"#;',
+    },
+    {
+      filename: 'packages/core/agent/src/index.ts',
+      status: 'renamed', additions: 1, deletions: 1,
+      patch: '@@ -1 +1 @@\n-// old note\n+// new note',
+    },
+  ]) {
+    assert.equal(isCommentOnlyChange(file), false, file.filename)
+  }
+})
+
 test('normalizes separators and rejects paths that are not repository-relative', () => {
   assert.equal(normalizeRepositoryPath('./packages\\core\\agent\\src\\index.ts'), 'packages/core/agent/src/index.ts')
   for (const file of ['', '/absolute.ts', '../escape.ts', 'packages//empty.ts', 'packages/./same.ts']) {
@@ -150,6 +223,12 @@ test('classifies both sides of a rename independently', () => {
         filename: 'packages/client/store/src/restored.ts',
         previous_filename: 'packages/client/store/tests/restored.spec.ts',
       },
+      { filename: 'packages/core/agent/README.md' },
+      {
+        filename: 'packages/core/agent/src/commented.ts',
+        status: 'modified', additions: 1, deletions: 1,
+        patch: '@@ -1 +1 @@\n-// old note\n+// new note',
+      },
     ]),
     {
       changedCodeFiles: [
@@ -160,6 +239,8 @@ test('classifies both sides of a rename independently', () => {
         'packages/client/store/tests/restored.spec.ts',
         'packages/core/agent/tests/moved.spec.ts',
       ],
+      excludedDocumentationFiles: ['packages/core/agent/README.md'],
+      excludedCommentOnlyFiles: ['packages/core/agent/src/commented.ts'],
     },
   )
 })
@@ -210,7 +291,19 @@ test('fails closed when GitHub cannot provide the complete file list', async ()
   )
 })
 
-test('prints changed code files before requesting missing owners', async () => {
+test('fails closed when the review-request timeline exceeds its limit', async () => {
+  let calls = 0
+  await assert.rejects(
+    listPullRequestTimeline(async () => {
+      calls++
+      return Array.from({ length: 100 }, () => ({ event: 'commented' }))
+    }, 'owner/repo', 42),
+    /exceeds 3000 events/u,
+  )
+  assert.equal(calls, 30)
+})
+
+test('prints changed code files and limits current review requests to two people', async () => {
   const trace = []
   const files = [
     { filename: 'packages/core/agent/src/index.ts' },
@@ -239,14 +332,16 @@ test('prints changed code files before requesting missing owners', async () => {
 
   assert.deepEqual(result, {
     changedCodeFiles: [
-      'AGENTS.md',
       'packages/client/store/src/index.ts',
       'packages/core/agent/src/index.ts',
       'packages/preset/agent-presets/src/index.ts',
       'packages/subagent/subagent/src/index.ts',
     ],
     excludedTestFiles: ['packages/core/agent/tests/index.spec.ts'],
-    requestedReviewers: ['Dudu-0223', 'LegGasai', 'mektpoy', 'tianyicui'],
+    excludedDocumentationFiles: ['AGENTS.md'],
+    excludedCommentOnlyFiles: [],
+    requestedReviewers: ['Dudu-0223'],
+    cancelledReviewers: [],
   })
   assert.equal(trace[0].type, 'log')
   assert.equal(trace[0].line, 'This is by automated Angry Turtle Cyborg, not a human')
@@ -258,17 +353,46 @@ test('prints changed code files before requesting missing owners', async () => {
     path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
     options: {
       method: 'POST',
-      body: { reviewers: ['Dudu-0223', 'LegGasai', 'mektpoy', 'tianyicui'] },
+      body: { reviewers: ['Dudu-0223'] },
     },
   })
 })
 
-test('does not request reviewers for a test-only change', async () => {
+test('does not add an owner when two people are already requested', async () => {
+  const calls = []
+  const result = await requestReviews({
+    event: pullRequestEvent(),
+    ownershipSource: '/packages/core/ @turtle1999 @mektpoy\n',
+    api: async (path, options = {}) => {
+      calls.push({ path, options })
+      if (path.endsWith('/files?per_page=100&page=1')) {
+        return [{ filename: 'packages/core/agent/src/index.ts' }]
+      }
+      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
+        return { users: [{ login: 'first' }, { login: 'second' }], teams: [] }
+      }
+      throw new Error(`unexpected API path ${path}`)
+    },
+    write: () => {},
+  })
+
+  assert.deepEqual(result.requestedReviewers, [])
+  assert.equal(calls.some(call => call.options.method === 'POST'), false)
+})
+
+test('does not request reviewers for test, documentation, or comment-only changes', async () => {
   const calls = []
   const output = []
   const files = [
     { filename: 'apps/web/tests/chat.e2e.ts' },
     { filename: 'packages/core/agent/tests/agent.spec.ts' },
+    { filename: 'packages/core/agent/README.md' },
+    { filename: 'packages/core/agent/examples.yaml' },
+    {
+      filename: 'packages/core/agent/src/index.ts',
+      status: 'modified', additions: 1, deletions: 1,
+      patch: '@@ -1 +1 @@\n-// old note\n+// new note',
+    },
   ]
   const result = await requestReviews({
     event: pullRequestEvent({ changedFiles: files.length }),
@@ -281,8 +405,11 @@ test('does not request reviewers for a test-only change', async () => {
   })
   assert.deepEqual(result, {
     changedCodeFiles: [],
-    excludedTestFiles: files.map(file => file.filename),
+    excludedTestFiles: files.slice(0, 2).map(file => file.filename),
+    excludedDocumentationFiles: files.slice(2, 4).map(file => file.filename),
+    excludedCommentOnlyFiles: ['packages/core/agent/src/index.ts'],
     requestedReviewers: [],
+    cancelledReviewers: [],
   })
   assert.equal(calls.length, 1)
   assert.deepEqual(output.slice(0, 4), [
@@ -293,19 +420,67 @@ test('does not request reviewers for a test-only change', async () => {
   ])
 })
 
-test('defers draft pull requests without reading changed files', async () => {
-  const output = []
+test('cancels workflow-authored review requests on draft pull requests', async () => {
+  const trace = []
+  const files = [
+    { filename: 'packages/subagent/subagent/src/index.ts' },
+    { filename: 'packages/subagent/subagent/tests/index.spec.ts' },
+    { filename: 'packages/subagent/subagent/README.md' },
+  ]
   const result = await requestReviews({
-    event: pullRequestEvent({ draft: true }),
+    event: pullRequestEvent({ draft: true, changedFiles: files.length }),
     ownershipSource,
-    api: async () => assert.fail('draft routing must not call GitHub'),
-    write: line => output.push(line),
+    api: async (path, options = {}) => {
+      trace.push({ type: 'api', path, options })
+      if (path.endsWith('/files?per_page=100&page=1')) return files
+      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
+        return { users: [{ login: 'Dudu-0223' }, { login: 'manual-reviewer' }], teams: [] }
+      }
+      if (path.endsWith('/timeline?per_page=100&page=1')) {
+        return [
+          {
+            event: 'review_requested',
+            requested_reviewer: { login: 'Dudu-0223' },
+            review_requester: { login: 'maintainer' },
+          },
+          {
+            event: 'review_requested',
+            requested_reviewer: { login: 'Dudu-0223' },
+            review_requester: { login: 'github-actions[bot]' },
+          },
+          {
+            event: 'review_requested',
+            requested_reviewer: { login: 'manual-reviewer' },
+            review_requester: { login: 'github-actions[bot]' },
+          },
+          {
+            event: 'review_requested',
+            requested_reviewer: { login: 'manual-reviewer' },
+            review_requester: { login: 'maintainer' },
+          },
+        ]
+      }
+      if (path.endsWith('/requested_reviewers') && options.method === 'DELETE') return {}
+      throw new Error(`unexpected API path ${path}`)
+    },
+    write: line => trace.push({ type: 'log', line }),
   })
-  assert.deepEqual(result, { changedCodeFiles: [], excludedTestFiles: [], requestedReviewers: [] })
-  assert.deepEqual(output, [
-    'This is by automated Angry Turtle Cyborg, not a human',
-    'Draft pull request; reviewer routing is deferred until ready_for_review.',
-  ])
+  assert.deepEqual(result, {
+    changedCodeFiles: ['packages/subagent/subagent/src/index.ts'],
+    excludedTestFiles: ['packages/subagent/subagent/tests/index.spec.ts'],
+    excludedDocumentationFiles: ['packages/subagent/subagent/README.md'],
+    excludedCommentOnlyFiles: [],
+    requestedReviewers: [],
+    cancelledReviewers: ['Dudu-0223'],
+  })
+  const remove = trace.find(item => item.type === 'api' && item.options.method === 'DELETE')
+  assert.deepEqual(remove, {
+    type: 'api',
+    path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
+    options: { method: 'DELETE', body: { reviewers: ['Dudu-0223'] } },
+  })
+  assert.equal(trace.some(item => item.type === 'log' && item.line === '- @manual-reviewer'), false)
+  assert.equal(trace.at(-1).line, 'Cancelled review request for @Dudu-0223.')
 })
 
 test('sends authenticated JSON and escapes an API error body', async () => {

+ 1 - 2
.github/workflows/request-review.yml

@@ -2,7 +2,7 @@ name: request-review
 
 on:
   pull_request_target:
-    types: [opened, synchronize, reopened, ready_for_review]
+    types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
 
 permissions:
   contents: read
@@ -15,7 +15,6 @@ concurrency:
 jobs:
   request-review:
     name: request-review
-    if: ${{ !github.event.pull_request.draft }}
     runs-on: ubuntu-latest
     timeout-minutes: 5
     steps:

+ 24 - 2
apps/web/tests/lifecycle-chrome.e2e.ts

@@ -23,7 +23,7 @@ import {
   launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
 } from './scaffold.ts'
 import {
-  connectFreshWorkspace, newEnglishPage, saveFailureShot, writeComposerDraft,
+  connectFreshWorkspace, newEnglishPage, saveFailureShot, writeComposerDraft, ZH_BROWSER_LOCALE,
 } from './support.ts'
 
 const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/lifecycle-chrome', import.meta.url))
@@ -31,6 +31,7 @@ const FIXTURE = join(SNAPSHOT_DIR, 'session.v2.jsonl')
 const REPLAY_OVERRIDE = join(SNAPSHOT_DIR, 'replay.override.json')
 const HERO_EXPECTED = join(SNAPSHOT_DIR, 'hero.expected.md')
 const COMMAND_MENU_EXPECTED = join(SNAPSHOT_DIR, 'command-menu.expected.md')
+const COMMAND_MENU_ZH_EXPECTED = join(SNAPSHOT_DIR, 'command-menu-zh.expected.md')
 const FUZZY_COMMAND_MENU_EXPECTED = join(SNAPSHOT_DIR, 'command-menu-fuzzy.expected.md')
 const PLAN_ACTIVE_EXPECTED = join(SNAPSHOT_DIR, 'plan-active.expected.md')
 const CONNECTION_ERROR_EXPECTED = join(SNAPSHOT_DIR, 'connection-error.expected.md')
@@ -103,6 +104,26 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
     await expect.poll(() => menu.count()).toBe(0)
   })
 
+  it.skipIf(MODE === 'record')('localizes slash-command descriptions from the browser language', async () => {
+    const zhPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
+    const zhTripwire = watchConsole(zhPage)
+    onTestFailed(() => saveFailureShot(zhPage, 'web-e2e-command-menu-zh'))
+    try {
+      await zhPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
+      await zhPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+      const launcher = zhPage.getByRole('button', { name: '指令' })
+      await launcher.click()
+      const menu = zhPage.getByRole('listbox', { name: '触发候选建议' })
+      await menu.waitFor({ timeout: 10_000 })
+      const snapshot = await captureStableAria(zhPage, '[role="listbox"]', scaffold.workspaceCwd)
+      await compareOrRefreshGolden(COMMAND_MENU_ZH_EXPECTED, snapshot, MODE)
+      expect(zhTripwire.pageErrors).toEqual([])
+      expect(zhTripwire.warnings).toEqual([])
+    } finally {
+      await zhPage.close()
+    }
+  })
+
   it.skipIf(MODE === 'record')('shows active Plan as the warn-state status action', async () => {
     const activeScaffold = await launchWebScaffold()
     const activePage = await newEnglishPage(browser)
@@ -415,7 +436,8 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
     expect(tripwire.warnings).toEqual([])
     await assertFixtureInventory(SNAPSHOT_DIR, [
       'session.v2.jsonl', 'replay.override.json', 'command-menu.expected.md',
-      'command-menu-fuzzy.expected.md', 'connection-error.expected.md', 'hero.expected.md', 'plan-active.expected.md',
+      'command-menu-fuzzy.expected.md', 'command-menu-zh.expected.md', 'connection-error.expected.md',
+      'hero.expected.md', 'plan-active.expected.md',
       'reloaded.expected.md', 'reloaded-expanded.expected.md',
     ])
   })

+ 1 - 0
package.json

@@ -102,6 +102,7 @@
     "verify-package-invariants": "tsx scripts/verify-package-invariants.ts",
     "verify-built-package-invariants": "node scripts/verify-built-package-invariants.mjs",
     "verify-package-readme-model-experience": "tsx scripts/verify-package-readme-model-experience.ts",
+    "verify-package-readme-summaries": "tsx scripts/verify-package-readme-summaries.ts",
     "verify-mermaid": "tsx scripts/verify-mermaid.ts",
     "verify-agent-note-classification": "tsx scripts/verify-agent-note-classification.ts",
     "verify-agent-note-format": "tsx scripts/verify-agent-note-format.ts",

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

@@ -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/acp/acp/README.md
-README.md: 6f0411d993f67f96d599a811ce583f8e166b76b6
-README.zh.md: 641e0801ce85caefa90c0d9fdd32c3886ada82b8
+README.md: 635e0e6993a65f403a5cc89f2c9d147b48308a04
+README.zh.md: 65d7d3b2f9f66701c801c910048a501f565b0a46

+ 1 - 1
packages/acp/acp/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-acp` lets trusted programs drive persistent DeepSeek Harness agents over the standard [Agent Client Protocol](https://agentclientprotocol.com): create or resume sessions, list resumable sessions, attach standard MCP servers, select a model and reasoning effort, prompt or cancel work, receive semantic execution updates, and close one session without affecting others. It is built for automation — out-of-process subagents, test runners, and scripted controllers — rather than the DSH user interface: it emits standard ACP messages, thoughts, generic tool lifecycle, configuration, and context usage, never private DSH presentation data or methods. Session persistence enables list, resume, and close across process restarts, while deletion, fork, transcript replay, additional directories, and interactive UI surfaces remain unsupported. The repository's own ACP client is `dsh-subagent-acp`, and `pnpm dsh --profile acp` starts a ready-to-use server. Setup and usage come first; the implementation details live in a collapsible developer section below.
+`dsh-acp` lets trusted programs automate persistent DeepSeek Harness agents through the standard [Agent Client Protocol](https://agentclientprotocol.com): create or resume sessions, select a model and reasoning effort, attach MCP servers, submit or cancel work, receive semantic updates, and close sessions independently. Choose it for out-of-process subagents, test runners, and scripted controllers; it intentionally omits DSH-specific presentation data and interactive UI features. Persistence supports listing, resuming, and closing sessions across process restarts, but deletion, forks, transcript replay, and additional directories are unsupported. Run `pnpm dsh --profile acp` to start the server; use `dsh-subagent-acp` as the repository client.
 
 ## Table of Contents
 

+ 1 - 1
packages/acp/acp/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-acp` 让受信程序可以通过标准 [Agent Client Protocol(ACP)](https://agentclientprotocol.com) 驱动持久 DeepSeek Harness agent:创建或恢复会话、列出可恢复会话、挂载标准 MCP 服务器、选择模型与推理强度、发送或取消工作、接收语义执行更新,并关闭一个会话而不影响其他会话。它是为自动化而生的——进程外 subagent、测试运行器与脚本化控制器——而不是 DSH 用户界面:它发送标准 ACP 消息、thought、通用工具生命周期、配置与上下文用量,绝不发送 DSH 私有呈现数据或方法。会话持久化支持跨进程重启的列出、恢复与关闭,而删除、fork、转录回放、附加目录与交互式 UI 界面仍不支持。仓库自带的 ACP 客户端是 `dsh-subagent-acp`,`pnpm dsh --profile acp` 会启动一个开箱即用的服务器。设置与用法在前;实现细节放在下方可折叠的开发者章节中。
+`dsh-acp` 让受信程序通过标准 [Agent Client Protocol(ACP)](https://agentclientprotocol.com) 自动操作持久 DeepSeek Harness agent:创建或恢复会话、选择模型与推理强度、挂载 MCP 服务器、提交或取消工作、接收语义更新,并独立关闭会话。进程外 subagent、测试运行器与脚本化控制器适合选择它;它刻意不提供 DSH 专用呈现数据与交互式 UI 功能。持久化支持跨进程重启列出、恢复与关闭会话,但不支持删除、fork、转录回放与附加目录。运行 `pnpm dsh --profile acp` 可启动服务器;仓库客户端使用 `dsh-subagent-acp`。
 
 ## 目录
 

+ 2 - 2
packages/api/workspace-files/README.i18n.yaml

@@ -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/api/workspace-files/README.md
-README.md: f7442845bc3c592bee0c59817a72ad07c8c91a2a
-README.zh.md: 4acd022335ee7c276aca00d66e177c19b060df0a
+README.md: ec15f52bf17fffca2b225fa427a7405922aaf2b3
+README.zh.md: 7e45d5c9a6073435e26e1237791a328db654d839

+ 1 - 1
packages/api/workspace-files/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`@deepseek-ai/dsh-api-workspace-files` owns the Host `ctx.workspaceFiles` service and the generated Client `workspaceFiles` Remote namespace: `read` returns one page of lines from a UTF-8 text file, `readBytes` returns one window of raw bytes from any regular file, `stat` returns a file's version and size without its content, `list` returns one directory's direct children, and `changes` streams every filesystem observation an Agent makes inside the Session's workspace root. All five run over the composed `ctx.fs` and confine themselves to the workspace root the sandbox policy resolves for the addressed Session; the filesystem backend's own cwd never decides. Client packages reach the namespace through the [`api-remotes`](../../api/remotes/README.md) assembly. The package's `./client` export registers the `file` resource provider that turns `stat` and `changes` into live file metadata for `useResource<'file'>`; the Sidebar's file tree tab lists directories through `list`.
+Use this package to browse and inspect files within a Session's workspace from the web client. It reads UTF-8 text one page of lines at a time, reads raw bytes in bounded windows, reports file versions and sizes, lists direct directory children, and streams changes caused by Agent file operations. Every operation stays within the workspace root selected for the addressed Session, independent of the filesystem backend's working directory. Client components can also follow live file metadata and build the Sidebar file tree through the shared Remote API.
 
 ## Table of Contents
 

+ 1 - 1
packages/api/workspace-files/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`@deepseek-ai/dsh-api-workspace-files` 拥有 Host 侧 `ctx.workspaceFiles` 服务与生成的 Client 侧 `workspaceFiles` Remote 命名空间:`read` 返回一个 UTF-8 文本文件的一页行,`readBytes` 返回任意普通文件的一个原始字节窗口,`stat` 返回文件的版本与大小而不带内容,`list` 返回一个目录的直接子项,`changes` 流式推送 Agent 在 Session 工作区根内做出的每一次文件系统观察。五者都经组合后的 `ctx.fs` 运行,并把自己限定在沙箱策略为被寻址 Session 解析出的工作区根内;文件系统后端自己的 cwd 从不参与判定。Client 包经 [`api-remotes`](../../api/remotes/README.zh.md) 装配触达该命名空间。本包的 `./client` 导出注册 `file` 资源提供者,把 `stat` 与 `changes` 变成 `useResource<'file'>` 的实时文件元数据;Sidebar 的文件树 tab 经 `list` 列举目录。
+使用本包可从 Web Client 浏览和检查 Session 工作区内的文件。它按行分页读取 UTF-8 文本、按有界窗口读取原始字节、报告文件版本与大小、列举目录的直接子项,并流式推送 Agent 文件操作造成的变更。每项操作都限定在为被寻址 Session 选择的工作区根内,不受文件系统后端工作目录影响。Client 组件还可经共享 Remote API 跟随实时文件元数据并构建 Sidebar 文件树。
 
 ## 目录
 

+ 2 - 2
packages/attachment/attachment-local/README.i18n.yaml

@@ -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/attachment/attachment-local/README.md
-README.md: 364153b7b56daa725003178b6cfad90e3f94bc04
-README.zh.md: 6ca5c6df8289c9e16bfe608b5b9ae200adf18a6b
+README.md: d4b8037c5e5cdcd9cd39302422d74ef854fb0890
+README.zh.md: 37d03f5edda1311f51968fe66f928cb19886514c

+ 1 - 1
packages/attachment/attachment-local/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This package provides the local storage and image-processing backend for attachments: source images are validated, oriented, stripped of metadata and color profiles, normalized to 8-bit sRGB/sRGBA, and saved below `DSH_HOME`; route-specific request versions are derived and cached separately, and generic files are saved byte-for-byte with no admission limits. Streamed file writes and reads use bounded chunks; writes hash into a private staging object before atomic publication, and reads verify the recorded byte length and digest without a whole-file memory copy. It is what the shipped `dsh` composition uses, so durable attachments work without configuration. Identical bytes occupy one canonical object even when uploads use different display names; each model-facing name is a hard link to that object. Concurrent reads of one request variant share work, and stored images stay readable after later admission-limit changes. Storage is local to this machine; other hosts cannot read these objects, and objects are never deleted automatically.
+Store images and generic file attachments durably below `DSH_HOME` on the machine running DSH. Images are validated, normalized for model requests, and cached per route; generic files are preserved byte-for-byte without admission limits. Identical bytes are stored once even when uploads use different display names, reads verify file length and content, and admitted images remain readable if limits later tighten. The shipped `dsh` composition uses this package without configuration. Objects remain local to one machine and are never deleted automatically.
 
 ## Table of Contents
 

+ 1 - 1
packages/attachment/attachment-local/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-本包提供附件的本地存储与图片处理后端:源图经过校验、方向修正、元数据与色彩配置移除,并规范化为 8-bit sRGB/sRGBA 后保存在 `DSH_HOME` 下;路由专用请求版本另行派生并缓存,通用文件则不设准入限制,按字节原样保存。流式文件写入与读取都使用有界分块;写入会在私有暂存对象中计算摘要后原子发布,读取会校验记录的字节长度与摘要,两者都不产生整文件内存副本。随附的 `dsh` 组合使用的就是它,因此持久附件无需配置即可工作。即使使用不同显示名称上传,相同字节也只占用一个规范对象;每条模型可见路径都是指向该对象的硬链接。同一请求变体的并发读取共享工作,即使后来收紧准入限制,已存图片仍然可读。存储仅限本机,其他主机无法读取这些对象,对象也永远不会自动删除。
+在运行 DSH 的机器上,把图片与通用文件附件持久存储到 `DSH_HOME` 下。图片经过校验、针对模型请求完成规范化并按路由缓存;通用文件不设准入限制,按字节原样保存。即使上传时使用不同显示名称,相同字节也只存储一次;读取会校验文件长度与内容,之后收紧限制也不会让已接纳的图片不可读。随附的 `dsh` 组合无需配置即可使用本包。对象仅限本机,并且永远不会自动删除。
 
 ## 目录
 

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

@@ -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/attachment/attachment/README.md
-README.md: a812182aeff6d09506a1ea2d4fa8d9a44a175936
-README.zh.md: c487f8204c86d8f0bbdfd85280e8fbab6ea14dec
+README.md: fc3903cb1ab4ed4a1249ad2ec62c0df633f4a7c4
+README.zh.md: 9e84a5ed8889d541b3cb87fb5e5d5560d36a2bb5

+ 1 - 1
packages/attachment/attachment/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-You can attach images and generic files to prompts, and the harness keeps them durably: each source image is admitted and normalized before your message is processed, while any other file is stored byte-for-byte with no format or size limits, and both reappear in conversation history across restarts of the same session. The shipped `dsh` composition enables this with no setup. Browser paths, provider URLs, local storage paths, and base64 never enter durable session events. Images accept raster formats (PNG, JPEG, WebP, GIF) under deployment limits; files accept anything, and the model reads a stored file on demand from its saved read-only path instead of receiving its bytes. Stored objects are never deleted automatically, and audio and video have no dedicated handling yet.
+Attach images and generic files to prompts and commands, then reuse them after restarting the same session, without extra setup in the shipped `dsh` composition. Images are validated and normalized before the message is accepted; PNG, JPEG, WebP, and GIF are supported within deployment limits. Other files are stored byte-for-byte without format or size limits, and models read them on demand through saved read-only paths instead of receiving their bytes. Durable session events exclude browser paths, provider URLs, local storage paths, and base64. Stored attachments are never deleted automatically; audio and video have no dedicated handling.
 
 ## Table of Contents
 

+ 1 - 1
packages/attachment/attachment/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-你可以把图片和通用文件附加到提示词中,harness 会持久保存它们:每张源图都会在你的消息被处理前准入并规范化,而其他任何文件都按字节原样保存、不设格式与大小限制,两者都会在同一会话重启后重新出现在对话历史中。随附的 `dsh` 组合无需任何配置即可支持这一点。浏览器路径、提供方 URL、本地存储路径与 base64 绝不会进入持久会话事件。图片接受部署限额内的光栅格式(PNG、JPEG、WebP、GIF);文件接受任何内容,模型不接收文件字节,而是在需要时从保存的只读路径按需读取。已存储对象永远不会被自动删除,音频和视频暂无专门处理。
+把图片与通用文件附加到提示词和命令中,同一会话重启后仍可复用;随附的 `dsh` 组合无需额外配置。图片会在消息被接受前完成校验与规范化;部署限额内支持 PNG、JPEG、WebP 和 GIF。其他文件按字节原样保存,不设格式与大小限制;模型通过保存的只读路径按需读取,而不接收文件字节。持久会话事件不包含浏览器路径、提供方 URL、本地存储路径和 base64。已存储附件不会被自动删除;音频和视频暂无专门处理。
 
 ## 目录
 

+ 2 - 2
packages/boot/cmdline/README.i18n.yaml

@@ -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/boot/cmdline/README.md
-README.md: fff0ba4df85b7ea834a79087ecbfe9f1e27f7714
-README.zh.md: 345db4a86ed2088a998c1723c3f906c614a171f3
+README.md: f0c6636b342d856b44c9d5eaffbbd4657f3e88ae
+README.zh.md: 31246ada165bad830bf2f5808fe9c6e1ad91d7cb

+ 1 - 1
packages/boot/cmdline/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-cmdline` lets your app own its command line: the launcher keeps only its own flags (`--profile`, `--patch`, the config dumps) and passes everything after them to your app verbatim, so your app decides its flags, its `--help` text, and its parse errors. Values you parse from those arguments win over any default written in the config, without writing anything back. Your app also gets a bounded way to ask for process exit, wired to the launcher's shutdown. Use it when you write an app bin that accepts its own flags; it adds no prompt, schema, or model-facing surface of its own.
+`dsh-cmdline` lets an app parse its own flags, `--help`, and errors from the arguments left unchanged after launcher flags. Parsed values can override configuration defaults without rewriting configuration. The app can also request process exit through the launcher's shutdown path. Use this package for app bins with their own command-line interface. It adds no prompt, schema, or model-visible content.
 
 ## Table of Contents
 

+ 1 - 1
packages/boot/cmdline/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-library"
 
 ## 概述
 
-`dsh-cmdline` 让你的应用持有自己的命令行:启动器只保留属于自己的 flag(`--profile`、`--patch`、配置 dump),并把**其后的一切**原样交给你的应用,因此 flag、`--help` 文本与解析错误都由你的应用决定。你从这些参数解析出的值会胜过配置中写下的任何默认值,且无需写回任何内容。你的应用还获得一个有边界的进程退出请求,接到启动器的关停上。当你编写接受自有 flag 的应用 bin 时使用它;它本身不增加任何提示词、schema 或面向模型的表面。
+`dsh-cmdline` 让应用从启动器 flag 之后原样留下的参数中解析自己的 flag、`--help` 与错误。解析值可以覆盖配置默认值,而无需改写配置。应用还可以通过启动器的关停路径请求进程退出。适用于拥有自有命令行界面的应用 bin。它不增加提示词、schema 或模型可见内容。
 
 ## 目录
 

+ 2 - 2
packages/bundle/web-app/README.i18n.yaml

@@ -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/bundle/web-app/README.md
-README.md: 0f71be178c25c0e6687a6e51ff777a9d6ac76a5a
-README.zh.md: ea7747c0b814dc36d222d0d7445732159f589d7b
+README.md: aea694942173a856861d00a69f15b451cc930976
+README.zh.md: f1b402c985ec2c21afdd67f8cb5477196aafeb44

+ 1 - 1
packages/bundle/web-app/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Run `dsh --profile web` and the interface opens in your default browser, ready for interactive chat with the agent. You get the conversation view, model and settings management, and session history, backed by the same model access, tools, and safety defaults as every other surface. The command prints a tokenized startup URL; the browser exchanges that token for a signed session cookie and redirects to the clean root URL. You can change the port, suppress the browser handoff, and allow extra hosts from the command line; binding all network interfaces is intentionally not supported. Choose it for interactive work in the browser; `dsh-headless` is the one-shot command-line sibling.
+Run `dsh --profile web` to open an interactive browser GUI with chat, model and settings management, and session history. It uses the same model access, tools, and safety defaults as other dsh surfaces. Startup prints an authenticated URL and normally opens it in the default browser; SSH sessions and `--no-open` leave the URL for manual opening. You can change the port and allow extra hosts, but cannot bind all network interfaces. Choose this package for interactive browser work; use `dsh-headless` for one-shot command-line tasks.
 
 ## Table of Contents
 

+ 1 - 1
packages/bundle/web-app/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-bundle"
 
 ## 概述
 
-运行 `dsh --profile web`,界面会在你的默认浏览器中打开,即可与 agent(智能体)交互式聊天。你会获得会话视图、模型与设置管理以及会话历史,背后与其他表层相同的模型访问、工具与安全默认值。该命令会打印带 token 的启动 URL;浏览器用该 token 换取签名会话 cookie,再重定向到干净的根 URL。你可以从命令行更改端口、关闭浏览器交接并允许额外主机;有意不支持绑定所有网络接口。需要浏览器中的交互式工作时选择它;`dsh-headless` 是一次性的命令行兄弟表层。
+运行 `dsh --profile web`,打开提供聊天、模型与设置管理以及会话历史的交互式浏览器 GUI。它使用与其他 dsh 表层相同的模型访问、工具与安全默认值。启动时会打印经过认证的 URL,通常还会在默认浏览器中打开;SSH 会话和 `--no-open` 会保留该 URL,供你手动打开。你可以更改端口并允许额外主机,但不能绑定所有网络接口。需要在浏览器中交互式工作时选择本包;一次性的命令行任务应使用 `dsh-headless`。
 
 ## 目录
 

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

@@ -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/client/README.md
-README.md: aec7edcb1e15d174544a9abaf99a4dc784034f2a
-README.zh.md: e4f069e1afef4973ebc8fdcc507a720c7a02be79
+README.md: 6bb433b8411fa9db3d6de981a24895e3c7b674c4
+README.zh.md: c04292b86becba404e5dbb58ca924833ee88f58b

+ 1 - 1
packages/client/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The `client/` group runs the browser half of the dsh web GUI: it boots the web shell, loads browser-side plugin modules, keeps browser-to-host RPC and event delivery alive, and provides the shared client services and UI feature plugins that render the application. UI features compose through the slot system — each plugin fills declared extension slots with typed props and stores, and the shell renders the assembled tree. All packages here are product packages named `@deepseek-ai/dsh-client-<name>`; the host half that serves the page lives in [`host/`](../host/README.md). Authoring rules live in [AGENTS.md](AGENTS.md), and the module graph, slot model, and object layer are documented in the related notes below.
+The `client/` group provides the browser experience for the dsh web GUI, including conversation, navigation, settings, approvals, file access, and other interactive features. Choose packages from this family when adding browser-visible behavior; use [`host/`](../host/README.md) for server-side page delivery and host integration. Packages cover both the shared browser foundation and focused UI features, while each child README owns its configuration and behavior. Authoring rules live in [AGENTS.md](AGENTS.md), and the related documentation below explains cross-package composition.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-group"
 
 ## 概述
 
-`client/` 组运行 dsh web GUI 的浏览器侧:它启动 web 外壳、加载浏览器侧插件模块、维持浏览器与宿主之间的 RPC 与事件投递,并提供渲染应用所需的共享客户端服务与 UI 功能插件。UI 功能通过 slot 系统组合——每个插件填充已声明的扩展 slot,携带类型化 props 与 store,由外壳渲染组装后的整棵树。本组所有包均为产品包,名为 `@deepseek-ai/dsh-client-<name>`;服务于页面的宿主半侧位于 [`host/`](../host/README.zh.md)。编写规则见 [AGENTS.md](AGENTS.md),模块图、slot 模型与对象层的说明见下方相关文档。
+`client/` 组提供 dsh web GUI 的浏览器体验,包括对话、导航、设置、批准、文件访问及其他交互功能。添加浏览器中可见的行为时,请选择本系列中的包;服务端页面交付与宿主集成则使用 [`host/`](../host/README.zh.md)。本系列同时涵盖共享浏览器基础与专门的 UI 功能,各子包 README 拥有其配置与行为说明。编写规则见 [AGENTS.md](AGENTS.md),下方相关文档解释跨包组合方式。
 
 ## 目录
 

+ 2 - 2
packages/client/locale/README.i18n.yaml

@@ -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/client/locale/README.md
-README.md: da0931ff5cf78b16d57354a8ac6abe6bf1878e50
-README.zh.md: 18eb233e80ba8a68621b2fa34442cdda3c4329a0
+README.md: 56a9cff9c3ec18dcc395378fcd1691207c022284
+README.zh.md: b23a1f2da59d0d83213d9c38e7cee05843881274

+ 1 - 1
packages/client/locale/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-client-locale` localizes the web GUI: users choose from the registered languages in Settings → General, and the UI copy switches immediately. The package ships `zh` and `en`, while external client plugins can add languages and their namespace dictionaries. On a loopback page, the choice persists as `locale.preference` in `$DSH_HOME/settings.yaml`; a non-loopback page keeps its selection process-local even though Connection authenticates every API method. A fresh browser starts provisionally in the first registered language requested by `navigator` until an allowed Host preference arrives and replaces it live. Plugin authors receive full type checking for the built-in dictionary form and translate through the framework `t` seat; copy rendered through slots follows language switches without a reload.
+Use `dsh-client-locale` to switch the web GUI between the shipped English and Chinese locales or languages added by client plugins. User selections take effect immediately; loopback pages persist them in `$DSH_HOME/settings.yaml`, while non-loopback pages keep them only for the current process. New browsers use the first supported language requested by the browser until an allowed stored preference arrives. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/locale/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-client-locale` 为 web GUI 提供本地化:用户在“设置 → 常规”中从已注册语言中选择,UI 文案会立即切换。本包内置 `zh` 与 `en`,外部 client 插件可以增加语言及其命名空间字典。在 loopback 页面上,该选择以 `locale.preference` 存储在 `$DSH_HOME/settings.yaml` 中;非 loopback 页面即使由 Connection 认证所有 API 方法,也只在进程内保留选择。全新浏览器会先临时使用 `navigator` 请求的第一个已注册语言,直到允许读取的 Host 偏好到达并实时替换。插件作者使用内置字典形式时会获得完整类型检查,并通过框架 `t` 席位翻译;经 slot 渲染的文案会随语言切换即时更新。
+使用 `dsh-client-locale` 可在 web GUI 中切换内置的 English、中文 locale,或 client 插件添加的语言。用户选择会立即生效;loopback 页面把选择持久化到 `$DSH_HOME/settings.yaml`,非 loopback 页面则只为当前进程保留选择。全新浏览器会使用浏览器请求的第一个受支持语言,直到允许读取的已存储偏好到达。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。
 
 ## 目录
 

+ 2 - 2
packages/client/resources/README.i18n.yaml

@@ -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/client/resources/README.md
-README.md: 2bc3d03bc5c58d45f9a0955aa73185be87b2bc6c
-README.zh.md: 43238fd5ff3794204f8d6d989e5d771131b8c489
+README.md: 7c32293c8d4250b11f8beaaf473a62145c915bce
+README.zh.md: b53089a2449f379f43156f610b587656a998ece3

+ 1 - 1
packages/client/resources/README.md

@@ -8,7 +8,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The resource model of the web client. A resource is one address, and a resource address is a `dsh-resource://<type>/…` URL whose host is the protocol key; the protocol's owning client package registers a provider that turns an address into a value stream, and any slot component reads that stream through the `useResource` global standard hook. A protocol that needs a scope encodes it in the path (`dsh-resource://file/session/<sessionId>/<absolute path>`); the model knows only addresses, and an address under any other scheme (`sidebar://guide`) names no resource. Use it when a component needs live data it only knows by address (a tab record, a link, a mention) and the data's owner is another client plugin.
+Use client resources when a component knows live data only by URL address, such as a tab record, link, or mention, while another client package owns the data. Resource addresses use `dsh-resource://<type>/…`; protocols that need a scope encode it in the path. Components receive the current value and later updates through the public `useResource` hook. Unsupported protocols and non-resource schemes, such as `sidebar://guide`, resolve to no resource.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/resources/README.zh.md

@@ -8,7 +8,7 @@ kind: "package-reference"
 
 ## 概述
 
-Web 客户端的资源模型。一份资源是一个地址,资源地址是 `dsh-resource://<type>/…` 形式的 URL,host 即协议键;协议所属的客户端包注册一个提供方把地址变成值的流,任何 slot 组件通过 `useResource` 全局标准 hook 读取这条流。需要作用域的协议把它编进路径(`dsh-resource://file/session/<sessionId>/<绝对路径>`);模型本身只认地址,其它 scheme 的地址(`sidebar://guide`)不指向资源。当组件需要的活数据只以地址形式可知(tab 记录、链接、提及),而数据的拥有者是另一个客户端插件时,请使用它。
+当组件只知道活数据的 URL 地址,而数据由另一个客户端包拥有时,请使用客户端资源;例如 tab 记录、链接或提及。资源地址使用 `dsh-resource://<type>/…`;需要作用域的协议把作用域编进路径。组件通过公开的 `useResource` hook 接收当前值与后续更新。不支持的协议与非资源 scheme(例如 `sidebar://guide`)不指向任何资源。
 
 ## 目录
 

+ 2 - 2
packages/client/ui-agent-preset/README.i18n.yaml

@@ -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/client/ui-agent-preset/README.md
-README.md: 06f2bc703633069a40b4677d4d84c12f4cc2444c
-README.zh.md: a9fecb68fd4cddd192a02667e58133e696e345c8
+README.md: df5a46ae7d1668483b60538cffc4fc1b851163bc
+README.zh.md: b83957ea7d1e8e79ec72070505ed24cacfb01864

+ 1 - 1
packages/client/ui-agent-preset/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This package provides the agent-preset surfaces of the Web GUI: a chip on the new-session screen choosing the next session's preset, a read-only label in the session header, and a settings section that manages the roster — copy, delete, default, and the way into a preset's own files. A session's preset is fixed at creation, so the choice applies to sessions started afterwards while running sessions keep the composition they began with; the default preset is edited in the settings section, where the roster is visible, so General settings carries no duplicate control for the same field. When a deployment composes no presets, all three surfaces render nothing and every session shares the host composition.
+Use this package to choose the agent preset for a new Web GUI session, see the active preset in the session header, and manage available presets in Settings. A preset is fixed when a session is created, so changing the selection or default affects only later sessions. If the deployment provides no presets, these controls stay hidden and every session uses the host composition.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-agent-preset/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-本包提供 Web GUI 的 agent preset 表面:新建会话界面的一枚 chip,选择下一个会话的 preset;会话标题旁的一个只读标签;以及一个设置分区,用于管理名单——复制、删除、默认值,以及通往 preset 自身文件的入口。会话的 preset 在创建时即固定,因此选择作用于此后开启的会话,运行中的会话保持它们开始时的组装;默认 preset 在能看到名单的设置分区里编辑,通用设置不再为同一字段保留重复控件。当部署未组装任何 preset 时,三个表面都不渲染任何内容,每个会话共用宿主组装。
+使用本包可以为新的 Web GUI 会话选择 agent preset、在会话标题中查看当前 preset,并在设置中管理可用 preset。preset 在会话创建时即固定,因此更改选择或默认值只影响此后创建的会话。如果部署未提供任何 preset,这些控件保持隐藏,每个会话都使用宿主组装。
 
 ## 目录
 

+ 2 - 2
packages/client/ui-brand-official/README.i18n.yaml

@@ -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/client/ui-brand-official/README.md
-README.md: 0176d78feac7eafa3a99a570a515ad1d753fd686
-README.zh.md: 0879e25fffce4973c4b741ddcdb5fa0e6a6ebbdb
+README.md: f8687047ca3b2a88d4fb2ae36a27819852df5ee9
+README.zh.md: 94477d966defce6ec4c3f4536ecdb7ae96389a31

+ 1 - 1
packages/client/ui-brand-official/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This package fills the sidebar brand slots — `sidebar.brand.mark` and `sidebar.brand.name` — with the official DeepSeek Harness mark and name. It registers these occupants only when the client bundle builds with the `official` profile; every other build loads the plugin but registers nothing, so the shell fallbacks stay visible. The conversation hero slot (`conversation.hero.brand.mark`) stays unoccupied in every build: its declaring package renders the animated hero fish (hover swim morph) as the fallback, and the official brand is that fish. Choose this package when the deployed identity is DeepSeek's own; a deployment with its own brand composes a different package into the same slots instead. It retains no runtime state and contributes nothing to model requests.
+This package gives an `official` client build the DeepSeek Harness mark and name in the sidebar. Other build profiles keep the shell's fish mark and local-build label, while the conversation hero always uses the animated fish. Choose it for deployments branded as DeepSeek Harness; deployments with another identity should provide a replacement brand package. It has no runtime state and does not affect model requests.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-brand-official/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-本包向侧栏品牌槽位——`sidebar.brand.mark` 与 `sidebar.brand.name`——填充官方 DeepSeek Harness 标志与名称。它只在客户端以 `official` profile 构建时注册这些填充;其余构建同样加载插件但不注册任何内容,因此外壳回退保持可见。会话首屏槽位(`conversation.hero.brand.mark`)在所有构建中都保持无填充:其声明包以动画首屏鱼(悬停游动形变)作为回退渲染,而官方品牌正是这条鱼。当部署身份就是 DeepSeek 自身时选择本包;自有品牌的部署改为在相同槽位中组合另一个包。它不保留任何运行时状态,也不向模型请求贡献任何内容。
+本包让以 `official` profile 构建的客户端在侧栏显示 DeepSeek Harness 标志与名称。其他构建 profile 保留外壳的鱼形标志与本地构建标签,会话首屏则始终使用动画鱼。品牌为 DeepSeek Harness 的部署应选择本包;使用其他品牌的部署应提供替代品牌包。本包不保留运行时状态,也不影响模型请求。
 
 ## 目录
 

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

@@ -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/client/ui-chat/README.md
-README.md: 5dd3a1c2a52789522629a822c8d522365fe93054
-README.zh.md: 07e8c2a9d41fa3a5d4f1a40e4570b9864822547f
+README.md: 91618e226dd25c8629ff103ac5a958e688aeed26
+README.zh.md: 4f9629c8974fbee9e56ed003ebd57bbc3b029577

+ 1 - 1
packages/client/ui-chat/README.md

@@ -8,7 +8,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The browser Chat target for Conversation assembly. It registers Chat event definitions and snapshot construction, supplies `useChat`, renders transcript nodes, and owns Chat-specific stores, actions, localization, and scroll restoration; historical image URLs resolve through the Conversation-owned per-session cache (`ctx.uiConversation.imageUrl`). Its Assistant and Turn Tail definitions fold packed historical Assistant runs without expanding their members. Steering classification retains only next-step Inbox IDs through persistent splice state; next-turn splices create no Chat Context. Local submission echoes (`SessionSnapshot.pendingSubmissions`) retain the surface selected when the submit begins: transcript echoes render at the flow tail, steering echoes render with the pending-steering marker, and queued echoes stay out of Chat. Each echo is hidden per render once a user/steering node or queue occurrence carries its prompt `rpcId`, so the handoff is atomic.
+Use this package to render a browser chat from recorded Session conversations, including historical images, localized actions, and restored scroll position. Compact display folds completed-turn process rows while keeping the final answer and independently useful context visible; packed historical Assistant runs remain collapsed. Local transcript and steering submissions appear immediately, remain in their original surface, and disappear atomically when authoritative Session records arrive, while queued submissions stay outside Chat. The package does not assemble or modify model requests.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-chat/README.zh.md

@@ -8,7 +8,7 @@ kind: "package-reference"
 
 ## 概述
 
-Conversation 组装的浏览器 Chat target。本包注册 Chat event definition 与 snapshot 构造、提供 `useChat`、渲染 transcript node,并拥有 Chat 专属 store、action、本地化与滚动位置恢复;历史图片 URL 通过 Conversation 持有的按会话缓存(`ctx.uiConversation.imageUrl`)解析。其中 Assistant 与 Turn Tail definition 会直接 fold packed Assistant 历史 run,不展开其成员。steering 分类通过持久 splice state 只保留 next-step Inbox ID;next-turn splice 不创建 Chat Context。本地提交回显(`SessionSnapshot.pendingSubmissions`)保留提交开始时选定的区域:transcript 回显位于消息流末尾,steering 回显带 pending-steering 标记,queued 回显不进入 Chat。一旦 user/steering 节点或 queue occurrence 携带回显的 prompt `rpcId`,该回显即在同一渲染中隐藏,因此交接是原子的。
+使用本包可在浏览器中渲染已记录的 Session 对话,包括历史图片、本地化操作和滚动位置恢复。紧凑显示会收起已完成轮次的过程行,同时保持最终答案和独立有用的上下文可见;已打包的历史 Assistant 连续消息保持收起。本地 transcript 与 steering 提交会立即显示并保留在原区域,在权威 Session 记录到达时原子地消失,而 queued 提交始终不进入 Chat。本包不组装或修改模型请求。
 
 ## 目录
 

+ 2 - 2
packages/client/ui-commands/src/client/contract.ts

@@ -46,8 +46,8 @@ export type CommandUiSpec = {
 export interface CommandContribution {
   /** Command name without the leading slash (unique across contributions). */
   readonly name: string
-  /** Menu row description. */
-  readonly description: string
+  /** Resolve the localized menu row description when candidates are requested. */
+  readonly description: () => string
   /** Capability filter, called with a fresh projection per candidate pass. */
   available(session: ClientSessionContext): boolean
   /** The command's UI behavior (this phase: popupSelect only). */

+ 12 - 0
packages/client/ui-commands/src/client/locales.ts

@@ -2,6 +2,12 @@
 
 /** Simplified Chinese dictionary (the key-set source of truth). */
 export const zh = {
+  'description.compact': '压缩以上对话内容',
+  'description.export': '将当前会话内容导出为 ZIP',
+  'description.feedback': '发送关于当前会话的反馈',
+  'description.goal': '设置或查看长期任务目标',
+  'description.permission': '切换权限预设(沙箱模式与审批策略)',
+  'description.plan': '进入或退出计划模式',
   'search.placeholder': '搜索…',
   'search.aria': '筛选选项',
   'status.loading': '正在加载选项…',
@@ -17,6 +23,12 @@ export type CommandKey = keyof typeof zh
 
 /** English dictionary, checked complete against the zh key set. */
 export const en = {
+  'description.compact': 'Compact older conversation history',
+  'description.export': 'Download this Session log as a ZIP archive',
+  'description.feedback': 'record feedback about this session',
+  'description.goal': 'set or view the goal for a long-running task',
+  'description.permission': 'Switch the permission preset (sandbox mode + approval policy)',
+  'description.plan': 'Enter or leave plan mode',
   'search.placeholder': 'Search…',
   'search.aria': 'Filter options',
   'status.loading': 'Loading options…',

+ 23 - 2
packages/client/ui-commands/src/client/service.ts

@@ -26,6 +26,7 @@ import type {
 import type { CommandContribution, CommandDecoration, CommandUiContract } from './contract.ts'
 import type { CommandDescriptor } from './directory.ts'
 import { CommandDirectory } from './directory.ts'
+import { en, type CommandKey } from './locales.ts'
 import { PopupSelectController } from './popup.ts'
 import type { TokenSegment } from './popup.ts'
 
@@ -58,6 +59,16 @@ interface LiveState {
   readonly popups: Map<SessionId, PopupSelectController<ClientSessionContext>>
 }
 
+/** Locale keys for the canonical first-party Host command descriptions. */
+const HOST_DESCRIPTION_KEYS = new Map<string, CommandKey>([
+  ['compact', 'description.compact'],
+  ['export', 'description.export'],
+  ['feedback', 'description.feedback'],
+  ['goal', 'description.goal'],
+  ['permission', 'description.permission'],
+  ['plan', 'description.plan'],
+])
+
 /** Command surface: session-keyed directory + '/' source + contribution registry + per-session popups. */
 export class CommandUiRuntime extends Service implements CommandUiContract {
   static inject = ['inputTriggers', 'sessions', 'remote', 'remote.commands']
@@ -192,14 +203,18 @@ export class CommandUiRuntime extends Service implements CommandUiContract {
     const seen = new Set<string>()
     for (const c of list) {
       seen.add(c.name)
-      rows.push({ name: c.name, description: c.description, ...(c.input !== undefined ? { hint: c.input.hint } : {}) })
+      rows.push({
+        name: c.name,
+        description: this.hostDescription(c),
+        ...(c.input !== undefined ? { hint: c.input.hint } : {}),
+      })
     }
     for (const contribution of this.live.contributions.values()) {
       if (!contribution.available(session)) continue
       if (seen.has(contribution.name)) {
         throw new Error(`ui-commands: contribution /${contribution.name} collides with a host command`)
       }
-      rows.push({ name: contribution.name, description: contribution.description })
+      rows.push({ name: contribution.name, description: contribution.description() })
     }
     return rankByName(
       rows.filter(c => req.position === 'leading' || c.hint === undefined),
@@ -207,6 +222,12 @@ export class CommandUiRuntime extends Service implements CommandUiContract {
     )
   }
 
+  /** Translate exact built-in Host copy while preserving scoped or third-party descriptors verbatim. */
+  private hostDescription(command: CommandDescriptor): string {
+    const key = HOST_DESCRIPTION_KEYS.get(command.name)
+    return key !== undefined && command.description === en[key] ? this.t(key) : command.description
+  }
+
   /** Decision table, menu column: contribution/decorated-host → popup; host input → claim; host bare → detached execute. */
   private dispatch(pick: InputTriggerPick): PickOutcome {
     const name = pick.candidate.name

+ 33 - 2
packages/client/ui-commands/tests/service.client.spec.ts

@@ -39,6 +39,7 @@ interface BenchOptions {
   /** Scripted catalog per list payload; default serves the fixed catalogs by session. */
   commands?: (payload: { sessionId: SessionId }) => Promise<{ commands: CommandDescriptor[] }>
   execute?: (payload: { sessionId: SessionId; line: string }) => Promise<ExecuteValue>
+  translate?: (namespace: string, key: string, params?: Record<string, unknown>) => string
   addressed?: SessionId
 }
 
@@ -98,7 +99,8 @@ async function bench(opts: BenchOptions = {}) {
   // Deterministic key-echo translator: notice assertions read `key{json}`.
   ctx.provide('locale', {
     bind: (ns: string) => (key: string, params?: Record<string, unknown>) =>
-      `${ns}:${key}${params === undefined ? '' : JSON.stringify(params)}`,
+      opts.translate?.(ns, key, params)
+      ?? `${ns}:${key}${params === undefined ? '' : JSON.stringify(params)}`,
   })
   // Real scope tags behind a fake sessions face.
   const scopes = new Map<SessionId, { ctx: Context; fiber: { dispose(): Promise<void> } }>()
@@ -164,7 +166,7 @@ const themeUi = (over: Partial<CommandUiSpec> = {}): CommandUiSpec => ({
 
 const themeContribution = (over: Partial<CommandContribution> = {}): CommandContribution => ({
   name: 'theme',
-  description: 'client popup kind',
+  description: () => 'client popup kind',
   available: () => true,
   ui: themeUi(),
   ...over,
@@ -251,6 +253,35 @@ describe('candidates', () => {
     expect(names).toEqual(['theme'])
   })
 
+  it('localizes canonical built-in and contribution descriptions on every candidate request', async () => {
+    let locale = 'zh'
+    const commands: CommandDescriptor[] = [
+      { name: 'compact', description: 'Compact older conversation history' },
+      { name: 'goal', description: 'scoped goal override' },
+      { name: 'custom', description: 'plugin-authored copy' },
+    ]
+    const { command, source } = await bench({
+      commands: () => Promise.resolve({ commands }),
+      translate: (namespace, key) => `${locale}:${namespace}:${key}`,
+    })
+    command.register(themeContribution({ description: () => `${locale}:theme` }))
+
+    await expect(source.candidates(proj('s1'), req(''))).resolves.toEqual([
+      { name: 'compact', description: 'zh:command:description.compact' },
+      { name: 'goal', description: 'scoped goal override' },
+      { name: 'custom', description: 'plugin-authored copy' },
+      { name: 'theme', description: 'zh:theme' },
+    ])
+
+    locale = 'en'
+    await expect(source.candidates(proj('s1'), req(''))).resolves.toEqual([
+      { name: 'compact', description: 'en:command:description.compact' },
+      { name: 'goal', description: 'scoped goal override' },
+      { name: 'custom', description: 'plugin-authored copy' },
+      { name: 'theme', description: 'en:theme' },
+    ])
+  })
+
   it('a contribution/host name collision fails loud', async () => {
     const { command, source } = await bench()
     command.register(themeContribution({ name: 'plan' }))

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

@@ -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/client/ui-goal/README.md
-README.md: bec1e1dfed96731d70a10404c23230f9b7fe6e0a
-README.zh.md: 31bf10735ad46ff8344e2ced7b975c3807a18d42
+README.md: 1fbc1c7df944f1106fedcff79f6b9df35bda480d
+README.zh.md: 01f8902377c98dc39d64b3bfe26108b68cb29fe0

+ 1 - 1
packages/client/ui-goal/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This package renders the goal surface in the Web GUI: a strip in the composer-context stack that shows the current goal of the session and offers edit, pause, resume, and clear actions. It reads the durable goal from the host-computed projection, overlays process-local `activation` from a registrant-private observable hook, and routes every mutation through the goal service, surfacing rejections inline. It also projects each durable `/goal` command run as a `Command input` bubble in the chat, so a goal command entered by the user or the model appears in the transcript. Goal creation is outside this plugin. The shipped Web presets other than `minimal` mount `/goal` in their agent scope.
+The Web GUI goal surface shows both the durable goal state and its current process-local activation, and lets users edit, pause, resume, or clear the goal; rejected changes appear inline. It displays durable `/goal` runs as `Command input` bubbles so commands from users or the model remain visible after reload. Goal creation remains outside this package. Shipped Web presets other than `minimal` make `/goal` available to agents.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-goal/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-本包在 Web GUI 中渲染 goal 表面:composer 上下文堆栈里的一条条带,显示会话的当前目标,并提供编辑、暂停、恢复与清除动作。它从宿主计算的投影读取持久 goal,通过 registrant-private 的可观察 hook 叠加进程本地 `activation`,把每次变更都经 goal 服务路由,并把拒绝内联呈现。它还把每条持久的 `/goal` 命令运行投影为聊天中的 `Command input` 气泡,让用户或模型输入的 goal 命令出现在文本记录中。goal 创建不归本插件。除 `minimal` 外,随附的 Web preset 都会在其 agent scope 中挂载 `/goal`。
+Web GUI 的 goal 表面同时显示持久 goal 状态及当前的进程本地激活状态,供用户编辑、暂停、恢复或清除 goal;被拒绝的变更会内联显示。它把持久的 `/goal` 运行显示为 `Command input` 气泡,让用户或模型发出的命令在重新加载后仍然可见。goal 创建仍不归本包。除 `minimal` 外,随附的 Web preset 都会向 agent 提供 `/goal`。
 
 ## 目录
 

+ 2 - 2
packages/client/ui-input-trigger/README.i18n.yaml

@@ -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/client/ui-input-trigger/README.md
-README.md: 9ad568027e4be8f2e9cc641270cbf6b2f870c59c
-README.zh.md: 2926eb87d4738387cf6dc56d38a02f579ed267c4
+README.md: 883be41f07e05a4c27a0b31d186c41fa5fa96fb9
+README.zh.md: c29b3e526a3624930a0f4f6b9dafa325763c42bb

+ 1 - 1
packages/client/ui-input-trigger/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This package powers the input trigger pipeline of the Web GUI: it detects `/` and `@` typed under the caret, shows a grouped candidate menu, and routes a pick to the registered source. Sources register through `ctx.inputTriggers` — the `/` command source (ui-commands), the `@` file and session reference sources (ui-reference), and any business package — and the conversation wiring drives the pipeline per session. Typing a trigger seeds every source registered for it; a chrome launcher can also open exactly one source over the current selection. The pipeline is presentation-only: picks produce command claims or reference inserts whose consequences belong to the consuming host and input packages.
+When users type `/` or `@` at the caret in the Web GUI, this package opens a grouped menu for slash commands, file references, and session references. It supports keyboard and pointer selection, including drill-down choices and launchers that open a single candidate group over the current selection. A pick either invokes a command flow or inserts a reference for the consuming input surface to handle. The package affects browser presentation only; it does not assemble or send model requests.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-input-trigger/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-本包为 Web GUI 提供输入触发流水线:检测光标处键入的 `/` 与 `@`,显示分组候选菜单,并把 pick 路由到已注册 source。source 经 `ctx.inputTriggers` 注册——`/` 命令 source(ui-commands)、`@` 文件与会话引用 source(ui-reference),以及任何业务包——对话接线层按会话驱动这条流水线。键入触发器会 seed 为该触发器注册的所有 source;chrome launcher 也可以在当前选区上只打开一个 source。流水线仅做呈现:pick 产出命令声明或引用插入,其后果属于消费它们的宿主与输入包。
+当用户在 Web GUI 的光标处键入 `/` 或 `@` 时,本包会为斜杠命令、文件引用和会话引用打开分组菜单。它支持键盘和指针选择,包括下钻候选项,以及在当前选区上打开单个候选分组的 launcher。pick 会触发命令流程或插入引用,具体结果由消费它的输入表面处理。本包只影响浏览器呈现;它既不组装也不发送模型请求。
 
 ## 目录
 

+ 10 - 0
packages/client/ui-input-trigger/src/client/controller.ts

@@ -364,6 +364,16 @@ export class InputTriggerController {
     this.reduce({ type: 'close' })
   }
 
+  /** Re-fetch the currently open menu without changing its hit or visible rows. */
+  refreshOpenMenu(): void {
+    if (this.disposed || !this.menu.getSnapshot().open || this.hit === null) return
+    const launched = this.launcher.getSnapshot()
+    const roster = this.deps.roster.sources(this.hit.trigger)
+      .filter(source => launched === null || source.name === launched)
+    if (roster.length === 0) return
+    this.fetchCandidates(this.hit, roster)
+  }
+
   /** Scope teardown: close and abort (the service deletes the map entry). */
   dispose(): void {
     this.disposed = true

+ 4 - 0
packages/client/ui-input-trigger/src/client/service.ts

@@ -9,6 +9,7 @@ import { Service } from '@deepseek-ai/cordis'
 import type { Context } from '@deepseek-ai/cordis'
 import type { Context as ClientContext } from '@deepseek-ai/cordis'
 import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
+import type {} from '@deepseek-ai/dsh-client-locale/client'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import type { InputTriggerSource } from '../types.ts'
 import { InputTriggerController } from './controller.ts'
@@ -37,6 +38,9 @@ export class InputTriggerService extends Service implements InputTriggerServiceC
    */
   constructor(ctx: Context) {
     super(ctx, 'inputTriggers')
+    ctx.on('locale/change', () => {
+      for (const controller of this.live.controllers.values()) controller.refreshOpenMenu()
+    })
   }
 
   /**

+ 38 - 0
packages/client/ui-input-trigger/tests/service.client.spec.ts

@@ -10,6 +10,7 @@
 import { Context } from '@deepseek-ai/cordis'
 import { describe, expect, it, vi } from 'vitest'
 import { createScope, scopeOf } from '@deepseek-ai/dsh-api-session-controller/client'
+import type { LocaleSnapshot } from '@deepseek-ai/dsh-client-locale/client'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import { InputTriggerController, InputTriggerService } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
 import type {
@@ -225,6 +226,43 @@ describe('sessionOf', () => {
     expect(ca.menu.getSnapshot().groups[0]!.items).toEqual([{ name: 'goal' }])
     expect(cb.menu.getSnapshot().open).toBe(false)
   })
+
+  it('re-fetches every open menu when the active locale changes', async () => {
+    const { root, inputTriggers, mint } = await serviceBench()
+    let locale = 'en'
+    const candidates = vi.fn(() => Promise.resolve([{ name: 'compact', description: locale }]))
+    inputTriggers.registerSource({
+      trigger: '/',
+      name: 'command',
+      candidates,
+      onPick: () => undefined,
+    })
+    const first = inputTriggers.sessionOf(mint('a').actx)
+    const second = inputTriggers.sessionOf(mint('b').actx)
+    const closed = inputTriggers.sessionOf(mint('c').actx)
+    first.track('/c', 2, { tier: 'plain' }, 1)
+    second.track('/c', 2, { tier: 'plain' }, 1)
+    await tick()
+    expect(first.menu.getSnapshot()).toMatchObject({
+      open: true,
+      hit: { query: 'c' },
+      groups: [{ source: 'command', status: 'ready', items: [{ name: 'compact', description: 'en' }] }],
+    })
+
+    locale = 'zh'
+    root.emit('locale/change', { active: 'zh', locales: [], revision: 1 } as LocaleSnapshot)
+    expect(first.menu.getSnapshot().open).toBe(true)
+    expect(second.menu.getSnapshot().open).toBe(true)
+    await tick()
+    expect(candidates).toHaveBeenCalledTimes(4)
+    expect(first.menu.getSnapshot()).toMatchObject({
+      open: true,
+      hit: { query: 'c' },
+      groups: [{ source: 'command', status: 'ready', items: [{ name: 'compact', description: 'zh' }] }],
+    })
+    expect(second.menu.getSnapshot().groups[0]!.items).toEqual([{ name: 'compact', description: 'zh' }])
+    expect(closed.menu.getSnapshot().open).toBe(false)
+  })
 })
 
 describe('track', () => {

+ 2 - 2
packages/client/ui-model-selection/README.i18n.yaml

@@ -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/client/ui-model-selection/README.md
-README.md: 0ad5d6ee21dd47757247617d0ca2579144ac9611
-README.zh.md: 9c0b72a1e83785fc0bdaa62ec9bd35df10353ce1
+README.md: b050f1fa7e680b7669e208d88851754aa72f09e2
+README.zh.md: 85b4d44a1beb45a5a11fa946d2f36d83f9201e29

+ 1 - 1
packages/client/ui-model-selection/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This package provides model selection in the Web GUI: the `/model` popup command and the composer's model seat, both over one per-session directory of provider-grouped models. Choosing a model submits the complete selection — provider, model, and reasoning effort — which the Host snapshots at the next prompt-assembly boundary, so the following request uses it while a running step keeps its assembled selection. The composer seat shows a two-level Model/Effort menu: models stay provider-grouped, and the selected exact model supplies its adapter-owned effort names and default. When the Host reports that no adapter serves the session's route, the composer input goes inert until a route becomes available.
+The Web GUI lets users switch the model and reasoning effort for an existing session through either the `/model` popup or the composer's model control. Both surfaces present the same provider-grouped choices, and the selected model determines the available effort names and default. A complete selection applies to the next request; a running step keeps the model and effort it started with. If no adapter can serve the session's route, the composer remains disabled until routing becomes available.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-model-selection/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-本包提供 Web GUI 的模型选择:`/model` 弹窗命令与 composer 模型位,两者共用一份按提供方分组的会话级目录。选择模型会提交完整选择——提供方、模型与推理强度——宿主在下一次提示词组装边界对其快照,因此后续请求采用该选择,而运行中的步骤保留已组装选择。composer 位显示两级 Model/Effort 菜单:模型按提供方分组,所选具体模型提供其适配器持有的推理强度名称与默认值。当宿主报告没有适配器服务该会话的路由时,composer 输入停用,直到路由恢复可用。
+Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有会话使用的模型与推理强度。两个界面呈现同一组按提供方分组的选择;所选模型决定可用的推理强度名称与默认值。完整选择从下一次请求开始生效;运行中的步骤保留其启动时的模型与推理强度。如果没有适配器可以服务会话路由,composer 会保持停用,直至路由恢复可用。
 
 ## 目录
 

+ 2 - 4
packages/client/ui-model-selection/src/client/index.ts

@@ -120,16 +120,14 @@ export function apply(ctx: ClientContext): void {
   // a locale change reaches the next publish.
   ctx.plugin(ModelDirectoryResolver, { blockReason: () => t('blocked.composer') })
 
-  // Entry 1: the /model popupSelect over the shared directory. The command
-  // description is registry-held text: it reads t() once at registration and
-  // refreshes only on re-registration, not on locale change.
+  // Entry 1: the /model popupSelect over the shared directory.
   ctx.inject(['commandUi', 'modelDirectories'], (scope: ClientContext) => {
     const command = scope.get('commandUi') as CommandUiContract
     const models = scope.modelDirectories
     const sessions = scope.sessions
     scope.effect(() => command.register({
       name: 'model',
-      description: t('command.description'),
+      description: () => t('command.description'),
       available: session => sessions.subagentAddress(session.sessionId) === undefined,
       ui: {
         kind: 'popupSelect',

+ 2 - 2
packages/client/ui-permission-presets/README.i18n.yaml

@@ -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/client/ui-permission-presets/README.md
-README.md: 9fa1d42f90478c00c06b23bc89ef212195b01dc2
-README.zh.md: dee60ba44d7997a535d51f544993ecca9947e3eb
+README.md: d8169e5173d4bfbeef0542a57a97c5450927225c
+README.zh.md: f9b27963eeab4b4dceeb57a99d2f10c05b700db9

+ 1 - 1
packages/client/ui-permission-presets/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This package provides permission preset surfaces for two lifetimes in the Web GUI: a General-settings row chooses the default for later sessions without switching the current session. A picker on the host `/permission` command switches the current session through one flat preset list with the active value marked. Canonical built-in names render as locale-owned product labels, explicit host labels remain unchanged, and unknown kebab-case names render in title case. Choosing full access requires an explicit risk acknowledgement before either surface writes it. Both surfaces read one host-computed projection and write through one path, so the pushed projection frame is the single confirmation both follow.
+Use this package to choose Web GUI permission presets for future sessions or switch the current session. The General settings row changes only the default for sessions created later, while the `/permission` picker changes only the current session and marks its active preset. Built-in presets use localized labels; explicit host labels remain unchanged, and unknown kebab-case names appear in title case. Full access always requires explicit risk acknowledgement. Both surfaces confirm changes only after the host pushes the resulting permission state.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-permission-presets/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-本包为 Web GUI 中两种生命周期提供权限预设表面:通用设置中的一行选择之后创建会话所用的默认值,但不会切换当前会话。挂在宿主 `/permission` 命令上的选择器通过一张扁平预设列表切换当前会话,并标记 active 值。规范内置名称渲染为 locale 所有的产品标签,显式 host 标签保持原样,未知 kebab-case 名称渲染为 Title Case。选择完全权限时,该行或选择器写入前必须先显式确认风险。两个表面读取同一份宿主计算的投影、经同一条路径写入,因此推送的投影帧是两者共同跟随的唯一确认。
+使用本包可在 Web GUI 中为未来会话选择权限预设,或切换当前会话的权限预设。通用设置行只更改之后创建会话所用的默认值;`/permission` 选择器只更改当前会话,并标记其当前预设。内置预设使用本地化标签;显式宿主标签保持原样,未知的 kebab-case 名称显示为 Title Case。完全权限始终需要显式确认风险。两个表面都只在宿主推送更改后的权限状态后确认变更。
 
 ## 目录
 

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

@@ -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/client/ui-primitives/README.md
-README.md: 2a64ac8614ecac9005e85fbea08ffb0c291faa60
-README.zh.md: ae142907be4ff091d9481b8b098f6760c40b39a3
+README.md: 5bb645b196ab6c3d73f9efdfc87217a62bc78c4a
+README.zh.md: b534868574d35e9c0794752a286bd11c8abcc00b

+ 1 - 1
packages/client/ui-primitives/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-client-ui-primitives` is the web client's shared React component library: every feature plugin composes its UI from these atoms, and nothing here depends on Cordis or the slot system. It provides the control set (buttons, pills, inputs, menus, modals, toast banners, disclosure rows, hover cards, connection indicators), the icon glyphs and brand marks, positioning hooks for anchored overlays, and the content renderers for agent output: markdown with TeX math, terminal output, file reads, diffs, search results, web retrieval, and JSON inspection. The renderers are built for untrusted model output — raw HTML is dropped, links are neutralized or opened safely, and ANSI escape sequences are parsed rather than passed through. User-facing copy is supplied through label props; the feature plugin that composes an atom owns localization.
+Use `dsh-client-ui-primitives` to build web-client controls and render agent output with shared React UI. It includes standard controls, icons, anchored overlays, and renderers for Markdown with TeX, terminal output, file reads, diffs, search, web retrieval, and JSON. The renderers handle untrusted model output by dropping raw HTML, restricting links, and parsing ANSI escape sequences. Callers must supply localized labels, and the components rely only on React and `--dsw-*` design tokens.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-primitives/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-library"
 
 ## 概述
 
-`dsh-client-ui-primitives` 是 Web 客户端共享的 React 组件库:每个功能插件都用这些原子组件拼装自己的 UI,而这里没有任何内容依赖 Cordis 或 slot 系统。它提供控件集(按钮、胶囊、输入框、菜单、模态框、Toast 横幅、折叠行、悬浮卡片、连接指示器)、图标字形与品牌标记、锚定浮层用的定位钩子,以及 agent 输出的内容渲染器:带 TeX 公式的 markdown、终端输出、文件读取、差异、搜索结果、网页检索与 JSON 检查。这些渲染器为不受信任的模型输出而设计——原始 HTML 会被丢弃、链接会被失效或安全打开、ANSI 转义序列会被解析而非透传。面向用户的文案通过 label prop 提供;拼装某个原子组件的功能插件负责本地化。
+使用 `dsh-client-ui-primitives`,通过共享 React UI 构建 Web 客户端控件并渲染 agent 输出。它提供标准控件、图标、锚定浮层,以及用于带 TeX 公式的 Markdown、终端输出、文件读取、差异、搜索、网页检索和 JSON 的渲染器。这些渲染器会丢弃原始 HTML、限制链接并解析 ANSI 转义序列,以处理不受信任的模型输出。调用方必须提供本地化 label;这些组件仅依赖 React 和 `--dsw-*` 设计 token。
 
 ## 目录
 

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

@@ -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/client/ui-reference/README.md
-README.md: ecabff50060ac80c21c223fa64a898184a619aba
-README.zh.md: f8c5ec7a664aa28efa3f836106af46049480fe56
+README.md: 7d09db53e2ef1fec35837b60f2822559e513d3e7
+README.zh.md: 3f4623bc594baee7e895a711359c974b51a410fc

+ 1 - 1
packages/client/ui-reference/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-client-ui-reference` is the unified Web `@file` and `@session` reference source: it registers the `reference` entry in the composer's inline-suggestion machinery so a user typing `@` sees file and session candidates in one list. Files order before sessions, sections are labelled with locale-registered terms, and either candidate domain can fail independently without blocking the other. Each row carries only what distinguishes it: a file names its parent directory and nothing at the workspace root, a session names its workspace only when that workspace is not the current one, and a drilled directory listing names none because its breadcrumb already does. A pick inserts an atomic inline reference — file, folder, and session alike — whose hidden serialized and clipboard form is the natural text the shared `@path` grammar defines; a directory row additionally carries a drill verb (Tab or the row's chevron) that keeps plain editable path text and the menu active at its trailing slash so the user can descend another level. Selecting a session routes through the session-reference service, which validates the mention and captures model context at the pre-step boundary; this package itself registers no prompt or tool.
+Use `dsh-client-ui-reference` when Web users need to mention files, folders, or sessions from one `@` completion menu. It lists files before sessions and keeps either group available when the other cannot load. Picking a file, folder, or session inserts an atomic reference with a stable clipboard form; folder rows also let users descend without closing completion. File rows omit redundant root locations, and session rows show a workspace only when it differs from the current one. Session mentions are validated before model context is captured, while browsing candidates has no model effect.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-reference/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-client-ui-reference` 是统一的 Web `@file` 与 `@session` 引用 source:它把 `reference` 条目注册进编辑器的行内建议机制,让用户在输入 `@` 时于同一个列表中看到文件与会话候选。文件排在会话之前,分组标题使用注册在 locale 字典中的标签,任一候选领域失败都会独立降级、不阻塞另一领域。每一行只承载能区分它的信息:文件显示其父目录、位于工作区根目录时不显示;会话仅在其工作区不是当前工作区时显示该工作区;下钻后的目录列表不显示位置,因为面包屑已经承载了它。选择一项会插入原子行内引用——文件、文件夹与会话皆然——其隐藏的序列化与剪贴板形式就是共享 `@path` 语法所定义的自然文本;目录行额外携带一个钻取动词(Tab 或行尾 chevron),保持可编辑的路径纯文本并让菜单在尾部斜杠处保持活跃,用户可以继续进入下一层。选择会话会经 session-reference 服务路由,该服务校验 mention 并在 pre-step 边界捕获模型上下文;本包自身不注册任何提示词或工具。
+Web 用户需要从同一个 `@` 补全菜单提及文件、文件夹或会话时,可以使用 `dsh-client-ui-reference`。菜单先列出文件,再列出会话;其中一组无法加载时,另一组仍然可用。选择文件、文件夹或会话会插入带稳定剪贴板形式的原子引用;文件夹行还允许用户在不关闭补全的情况下继续下钻。文件行省略多余的根目录位置,会话行仅在工作区与当前工作区不同时显示该工作区。会话 mention 会在捕获模型上下文前接受校验,而浏览候选项不会影响模型。
 
 ## 目录
 

+ 2 - 2
packages/client/ui-settings-general/README.i18n.yaml

@@ -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/client/ui-settings-general/README.md
-README.md: 3c81dbf2560e0cbddd9e77e75befdbfda192cb70
-README.zh.md: 68580116fc809c796fb100c1847c2007b151ece1
+README.md: c5bf1c1ccf0215e5902e65e66aea465a94350b9c
+README.zh.md: 8fa4b48640374f414162a4f129d2ef177efb6287

+ 1 - 1
packages/client/ui-settings-general/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-client-ui-settings-general` is the settings shell of the dsh web client: the Settings panel opens from the sidebar's bottom control, a connection-failure indicator beside that control offers immediate recovery, the navigation is built from the sections features contribute, and first-run users are walked through one onboarding step at a time. It also registers everything on the Settings pages that belongs to no single feature: the trigger/header/close chrome content, the local configuration-file action, the General section and its `settings.general.item` slot, and the `settings` dictionaries. Feature-owned rows (Permission, Language, Appearance), sections (Models), and conditional onboarding steps stay with their feature packages; the shell itself ships no onboarding copy of its own.
+Use this package to give the dsh web client a Settings panel, connection-recovery control, feature-contributed navigation, and sequential first-run onboarding. Users can open it from the sidebar, retry a failed connection immediately, and access a local configuration file when the Host makes one available on a loopback browser. Feature packages supply their own settings rows, sections, and onboarding steps; this package supplies their shared presentation and does not add onboarding copy or built-in General rows.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-settings-general/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-client-ui-settings-general` 是 dsh Web 客户端的设置外壳:Settings 面板从侧边栏底部的控件打开,该控件旁的连接故障指示器提供即时恢复操作;导航由各功能贡献的分区构建;首次运行的用户一次只走一个引导步骤。它还注册设置页面上所有不属于单一功能的内容:触发器、标题栏与关闭控件界面框架、「本地配置文件」操作、「通用」分区及其 `settings.general.item` slot,以及 `settings` 字典。归具体功能所有的行(「权限」、「语言」、「外观」)、分区(「模型」)与条件式首次使用引导步骤仍由各自的功能包提供;外壳本身不自带任何引导文案。
+使用本包可为 dsh Web 客户端提供 Settings 面板、连接恢复控件、由功能包贡献的导航,以及依次进行的首次运行引导。用户可以从侧边栏打开面板、立即重试失败的连接,并在宿主为回环浏览器提供本地配置文件时访问该文件。各功能包提供自己的设置行、分区和引导步骤;本包提供共享的界面展示,但不添加引导文案或「通用」分区的内置行。
 
 ## 目录
 

+ 2 - 2
packages/client/ui-settings-plugin-inventory/README.i18n.yaml

@@ -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/client/ui-settings-plugin-inventory/README.md
-README.md: a9e4108848794bf9168c513a4223dd6769601972
-README.zh.md: ccca8826387f7a97978190eec3175df1b3122a55
+README.md: 9a7ea91521c08dd1855925af59521d3af6e47b25
+README.zh.md: 2427d667d92b7a4c620fc6222507ecd8c2fabcef

+ 1 - 1
packages/client/ui-settings-plugin-inventory/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-client-ui-settings-plugin-inventory` contributes the read-only **Plugin list** tab to the Web Settings Plugins section. The tab lazily calls `ctx.remote.pluginInventory.list()` the first time it is selected and renders the inventory in two collapsible groups. The agent-preset group comes first, open by default: a display-only switcher pill over the roster opens on the default preset, and each composition row is a compact disclosure card carrying its enablement — including `conditional` for a disabled gate the Host could not evaluate — with provenance facts behind the disclosure. The global group follows collapsed, its header carrying the entry count and a failure count; expanded, failures float first, and an entry disabled globally but enabled by at least one preset is marked as preset-provided in place — its details name the enabling presets — instead of reading as plainly disabled. Search filters both groups, forces the collapsed groups open, and points at matches sitting in unselected presets. Loading, empty, no-match, and generic failure states stay local to the mounted component, and a failed read can be retried without exposing transport details; without a roster the tab renders the global plane alone, expanded.
+The **Plugin list** tab lets Web users inspect plugins without changing their configuration. It presents agent-preset compositions first and keeps the global inventory collapsed until needed. Cards expose enablement, provenance, runtime status, disabled conditions, and discovery failures; global entries supplied by presets are identified with the enabling presets. Search covers both groups and points to matches in other presets. The tab supports loading, empty, no-match, failure, and retry states without exposing transport details, and it still shows the global inventory when no preset roster is available.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-settings-plugin-inventory/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-client-ui-settings-plugin-inventory` 向 Web 设置的「插件」分区贡献只读的**插件列表**标签页。该标签页在首次被选择时懒调用 `ctx.remote.pluginInventory.list()`,并把清单分成两个可折叠分组渲染。Agent 预设组在前、默认展开:一个只改显示的切换器胶囊覆盖 roster、初始停在默认预设,每个组合行是一张紧凑折叠卡片,携带其启停状态——含宿主无法求值的 disabled 门对应的 `conditional`——出处事实收在折叠里。全局组随后且默认收起,组头带条目计数与失败计数;展开后失败行浮在最前,全局停用但被至少一个预设启用的条目就地标记为预设提供——详情列出启用它的预设——而不是读作单纯的已停用。搜索同时过滤两组、强制撑开收起的分组,并指出未选中预设里的匹配。加载、空结果、无匹配与通用失败状态只属于已挂载组件,读取失败后可以重试,且不会暴露传输细节;没有 roster 时标签页只渲染全局平面并保持展开。
+**插件列表**标签页让 Web 用户查看插件,而不改变其配置。它优先展示 Agent 预设组合,并在需要前收起全局清单。卡片展示启停状态、出处、运行状态、禁用条件与发现失败;由预设提供的全局条目会标明启用它的预设。搜索覆盖两个分组,并指出其他预设中的匹配。标签页支持加载、空结果、无匹配、失败与重试状态,且不暴露传输细节;没有预设 roster 时仍会展示全局清单。
 
 ## 目录
 

+ 2 - 2
packages/client/ui-settings-plugins/README.i18n.yaml

@@ -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/client/ui-settings-plugins/README.md
-README.md: 444b05b16b79a3a660a71984c92969d367fe22a7
-README.zh.md: c05859fc9f12ce0fe2013dc9f3549e156bd7234f
+README.md: cea5dd292634fe033ba9f12c9e8fe5daedc1d2ab
+README.zh.md: cc3bd4b3fcb35ce9f39fd978e3424919c66b2edc

+ 1 - 1
packages/client/ui-settings-plugins/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-client-ui-settings-plugins` is the **Plugins** settings section of the dsh web client: users edit host-plane plugin configuration on its **Plugin configuration** tab, and feature plugins contribute their own pages through `settings.plugins.tab`. This package's own tab shows one expandable card per Host plugin whose configuration a user owns: a card shows the plugin's name and what it governs, and expanding it reveals hand-written controls bound to that plugin's settings namespace, each field marking whether the user overrode it and offering a reset back to the value the deployment composed. Cards stage edits locally and write only on save, with every write fenced by the namespace revision the form read.
+Use the **Plugins** settings section to configure the plugins exposed by the current deployment and to open feature-specific plugin pages. The **Plugin configuration** tab presents one expandable card for each supported plugin, shows which values the user overrode, and lets the user reset them to deployment defaults. Cards keep edits local until save. If the configuration changed after the card loaded, the save is rejected instead of overwriting the newer values.
 
 ## Table of Contents
 

+ 1 - 1
packages/client/ui-settings-plugins/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-client-ui-settings-plugins` 是 dsh Web 客户端的**插件**设置分区:用户在其**插件配置**标签页上编辑宿主平面插件配置,功能插件则通过 `settings.plugins.tab` 贡献自己的页面。本包自己的标签页为每个配置由用户拥有的 Host 插件展示一张可展开卡片:卡片展示插件名称及其管辖范围,展开后是绑定到该插件 settings 命名空间的手写控件,每个字段标注用户是否覆盖过它,并提供重置回部署组装值的入口。卡片暂存用户输入,只有用户保存时才写入,且每次写入都以表单读取时的命名空间 revision 设栅。
+使用**插件**设置分区可以配置当前部署公开的插件,也可以打开插件功能自己的页面。**插件配置**标签页会为每个受支持的插件展示一张可展开卡片,标明用户覆盖过哪些值,并允许用户将它们重置为部署默认值。卡片会在本地保留修改,直到用户保存。如果配置在卡片加载后发生变化,保存会被拒绝,而不会覆盖较新的值。
 
 ## 目录
 

Некоторые файлы не были показаны из-за большого количества измененных файлов