ソースを参照

refactor(plugins): limit manager operations to the global profile

Yichen Jiang 1 週間 前
親
コミット
07f9daa55a
56 ファイル変更、320 行追加、1320 行削除
  1. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.i18n.yaml
  2. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.zh.md
  4. 0 35
      .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.md
  5. 0 35
      .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md
  6. 3 3
      .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.i18n.yaml
  7. 25 0
      .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.md
  8. 25 0
      .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.zh.md
  9. 2 2
      docs/config-catalog.md
  10. 1 1
      docs/event-producer-consumer.i18n.yaml
  11. 2 2
      docs/event-producer-consumer.md
  12. 2 2
      docs/module-graph.i18n.yaml
  13. 7 9
      docs/module-graph.md
  14. 7 9
      docs/module-graph.zh.md
  15. 2 2
      docs/subsystems/core.i18n.yaml
  16. 5 30
      docs/subsystems/core.md
  17. 5 30
      docs/subsystems/core.zh.md
  18. 2 2
      packages/boot/app-boot/README.i18n.yaml
  19. 1 1
      packages/boot/app-boot/README.md
  20. 1 1
      packages/boot/app-boot/README.zh.md
  21. 2 2
      packages/boot/plugin-manager/README.i18n.yaml
  22. 5 9
      packages/boot/plugin-manager/README.md
  23. 5 9
      packages/boot/plugin-manager/README.zh.md
  24. 1 1
      packages/boot/plugin-manager/src/index.ts
  25. 38 96
      packages/boot/plugin-manager/src/manager.ts
  26. 3 10
      packages/boot/plugin-manager/src/types.ts
  27. 23 57
      packages/boot/plugin-manager/tests/plugin-manager.spec.ts
  28. 3 3
      packages/client/ui-settings-plugin-inventory/tests/components.client.spec.tsx
  29. 12 38
      packages/extensions/tool-cordis/src/api-catalog.ts
  30. 2 2
      packages/host/plugin-inventory/README.i18n.yaml
  31. 1 1
      packages/host/plugin-inventory/README.md
  32. 1 1
      packages/host/plugin-inventory/README.zh.md
  33. 0 4
      packages/host/plugin-inventory/src/types.ts
  34. 2 2
      packages/host/plugin-manager/README.i18n.yaml
  35. 3 3
      packages/host/plugin-manager/README.md
  36. 3 3
      packages/host/plugin-manager/README.zh.md
  37. 1 7
      packages/host/plugin-manager/package.json
  38. 8 16
      packages/host/plugin-manager/src/index.ts
  39. 11 12
      packages/host/plugin-manager/tests/plugin-manager.spec.ts
  40. 0 3
      packages/host/plugin-manager/tsconfig.json
  41. 2 2
      packages/preset/agent-presets/README.i18n.yaml
  42. 6 22
      packages/preset/agent-presets/README.md
  43. 6 22
      packages/preset/agent-presets/README.zh.md
  44. 6 8
      packages/preset/agent-presets/package.json
  45. 0 35
      packages/preset/agent-presets/src/authoring.ts
  46. 11 83
      packages/preset/agent-presets/src/composition-inventory.ts
  47. 13 128
      packages/preset/agent-presets/src/discovery.ts
  48. 32 141
      packages/preset/agent-presets/src/index.ts
  49. 3 17
      packages/preset/agent-presets/src/mount.ts
  50. 0 8
      packages/preset/agent-presets/src/preset.ts
  51. 19 23
      packages/preset/agent-presets/tests/composition-inventory.spec.ts
  52. 2 2
      packages/preset/agent-presets/tests/mount.spec.ts
  53. 0 370
      packages/preset/agent-presets/tests/overlay.spec.ts
  54. 0 3
      packages/preset/agent-presets/tsconfig.json
  55. 0 6
      pnpm-lock.yaml
  56. 0 1
      scripts/gen-cordis-catalog.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.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/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
-2026-09-04-plugin-manager-over-the-profile-runtime.md: e5b3e697e56ffe008556bb112565cd0f83941ee7
-2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: 4661241c23210b09e483c558348054fa81e84677
+2026-09-04-plugin-manager-over-the-profile-runtime.md: c68d7aedcd93ca4f179ab9df97e157ff5388b407
+2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: e01301a5f9e1f35e07471e86fb0df0e223c7f965

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md

@@ -10,7 +10,7 @@ The CLI and Web need the same installation checks, while only a running applicat
 
 ## Decision
 
-**One business manager, one Remote adapter.** `dsh-plugin-manager` belongs beside app-boot. `PluginInstaller` operates on profile files and pnpm without a running Loader; `PluginManager` adds bundle and row operations over the live profile. The CLI shares the installer. `dsh-host-plugin-manager` supplies per-call readers for the profile runtime, preset layers and running-agent count, relays methods, and maps `PluginOperationError` codes to Remote errors. Missing profile runtime reports `plugins/unavailable` when called. The manager depends on the preset-layer interface, not the preset or agent implementations.
+**One business manager, one Remote adapter.** `dsh-plugin-manager` belongs beside app-boot. `PluginInstaller` operates on profile files and pnpm without a running Loader; `PluginManager` adds bundle and row operations over the live profile. The CLI shares the installer. `dsh-host-plugin-manager` supplies per-call readers for the profile runtime and running-agent count, relays methods, and maps `PluginOperationError` codes to Remote errors. Missing profile runtime reports `plugins/unavailable` when called. The manager reads running-agent counts without depending on the agent implementation.
 
 **One mutation at a time.** An overlapping mutation receives `plugins/busy` rather than entering a queue with stale assumptions. Installation and removal reject with `plugins/agents-running` while an agent is running because pnpm rewrites modules those agents import. Bundle and row edits leave `node_modules` alone and do not use that guard. Every operation reads the profile manifest afresh: dependencies record installation, and `dsh.profile.bundles` records enablement.
 
@@ -18,7 +18,7 @@ The CLI and Web need the same installation checks, while only a running applicat
 
 **Enablement selects a layer; diagnostics describe actual rows.** Bundle enablement, nontransactional recomposition and explicit `dsh.plugins` discovery follow [native entry diagnostics and static declarations](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md). The manager keeps per-row issues distinct from the enabled choice. `retry` removes the complete layer, awaits cleanup, and adds it again; unchanged row options alone do not restart a failed plugin. `list` derives each package view from the manifest, static declarations and current Loader state.
 
-**Rows go through the patch-file writer.** `addRow` inserts the declared module and config into the profile user patch or the preset overlay returned by its roster. `setRowDisabled` writes or removes `disabled: true`, restoring the author's original gate when the user withdraws a denial. Global changes recompose the live tree; preset changes affect subsequent standing generations.
+**Rows go through the patch-file writer.** `addRow` inserts the declared module and config into the profile user patch. `setRowDisabled` writes or removes `disabled: true`, restoring the author's original gate when the user withdraws a denial. Global changes recompose the live tree.
 
 **Runtime notifications follow diagnostics.** Native entry and fiber events share one pending reader. It waits for Loader and profile recomposition, then compares row identity, module, fiber phase, failure stage and message. Only a changed diagnostic set emits `plugins/changed` with reason `runtime`; healthy status churn and repeated identical failures stay silent. Management operations retain their own completion notifications. Events arriving during a read request another pass, and disposing the adapter cancels publication.
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.zh.md

@@ -10,7 +10,7 @@ CLI 与 Web 需要相同的安装检查,而只有运行中的应用才能应
 
 ## Decision
 
-**一个业务管理器,一个 Remote 适配器。** `dsh-plugin-manager` 位于 app-boot 旁。`PluginInstaller` 操作 profile 文件和 pnpm,不需要运行中的 Loader;`PluginManager` 在其上增加面向运行中 profile 的组合包与行操作。CLI 共用安装器。`dsh-host-plugin-manager` 提供逐次调用的 profile runtime、预设层与运行中 agent 数读取器,转接方法并将 `PluginOperationError` 错误码映射为 Remote 错误。缺少 profile runtime 时在调用处报告 `plugins/unavailable`。管理器依赖预设层接口,不依赖预设或 agent 的实现。
+**一个业务管理器,一个 Remote 适配器。** `dsh-plugin-manager` 位于 app-boot 旁。`PluginInstaller` 操作 profile 文件和 pnpm,不需要运行中的 Loader;`PluginManager` 在其上增加面向运行中 profile 的组合包与行操作。CLI 共用安装器。`dsh-host-plugin-manager` 提供逐次调用的 profile runtime 与运行中 agent 数读取器,转接方法并将 `PluginOperationError` 错误码映射为 Remote 错误。缺少 profile runtime 时在调用处报告 `plugins/unavailable`。管理器读取运行中 agent 的数量,不依赖 agent 实现。
 
 **同时只允许一个变更。** 重叠变更收到 `plugins/busy`,不进入可能携带过期假设的队列。有 agent 运行时,安装与移除以 `plugins/agents-running` 拒绝,因为 pnpm 会重写这些 agent 导入的模块。组合包与行编辑不改 `node_modules`,不使用这一限制。每次操作重新读取 profile 清单:依赖记录安装,`dsh.profile.bundles` 记录启用。
 
@@ -18,7 +18,7 @@ CLI 与 Web 需要相同的安装检查,而只有运行中的应用才能应
 
 **启用选择层,诊断描述实际行。** 组合包启用、非事务重组和显式 `dsh.plugins` 发现遵循[原生条目诊断与静态声明](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md)。管理器区分逐行问题与启用选择。`retry` 移除完整层,等待清理,再将其加入;仅保留不变的行选项不会重启失败插件。`list` 从清单、静态声明和当前 Loader 状态派生每个包的视图。
 
-**行通过 patch-file 写入器修改。** `addRow` 将声明的模块与配置插入 profile 用户 patch 或 roster 返回的预设覆盖层。`setRowDisabled` 写入或移除 `disabled: true`,用户撤销禁用时恢复作者原来的条件。全局修改重组运行中的树;预设修改影响后续常驻代际。
+**行通过 patch-file 写入器修改。** `addRow` 将声明的模块与配置插入 profile 用户 patch。`setRowDisabled` 写入或移除 `disabled: true`,用户撤销禁用时恢复作者原来的条件。全局修改重组运行中的树。
 
 **运行时通知跟随诊断。** 原生条目与 fiber 事件共用一次待完成的读取。读取等待 Loader 和 profile 重组完成,再比较行身份、模块、fiber 阶段、失败阶段与消息。只有诊断集合变化才发送原因是 `runtime` 的 `plugins/changed`;健康状态波动和重复的相同失败保持安静。管理操作仍保留自己的完成通知。读取期间到来的事件请求再次读取,适配器销毁时取消发布。
 

+ 0 - 35
.agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.md

@@ -1,35 +0,0 @@
-# Agent Note: A preset takes a user patch layer, and patch files get one home
-
-Status: implemented
-
-English | [中文](2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md)
-
-## Problem
-
-A shipped agent preset was read-only in every sense: to switch one of its rows off or add a tool to it, a person copied the whole preset and edited the copy, which then drifted from the shipped one on every upgrade. The profile already had the shape that solves this — a base composition plus a `cordis.patch.yml` user layer applied over it — and the preset had nothing of the kind. Separately, that file format had one parser in `dsh-app-boot` and no writer: a program that wanted to switch a row off had to rewrite the whole file, losing the author's comments and any `!!js` gate it did not understand.
-
-## Decision
-
-**A preset's user layer is `cordis.patch.yml` in its slot.** Beside the composition for a locally authored preset; alone in the user root's directory of the same id (`$DSH_HOME/.agent-presets/<id>/cordis.patch.yml`) for a shipped one, whose install stays untouched. Discovery attaches the layer to the preset that wins the id, judges it with the composition (an unparsable layer, a malformed insert, or an inserted row naming a module that cannot resolve makes the preset broken with that reason), and reports a layer whose id no root supplies as a broken slot rather than hiding it. The mount hands the parsed layer to the include as its runtime patches, so it applies with the Loader's own patch semantics. The composition inventory applies it before flattening and marks each row `source: 'preset' | 'user'` and, when off, `disabledBy: 'composition' | 'user'`. A copy carries the layer in beside the new composition; `removeOverlay` deletes it and an emptied slot.
-
-**Generations follow the layer's content.** The standing mount's stamp now covers the composition file's stat and a digest of the layer's text. A changed layer starts the next generation for sessions created afterwards, as an edited composition always did; a layer edited back to a content an earlier generation composed returns to that generation, so switching a row off and on again does not stack a third live subtree.
-
-**`dsh-app-boot/patch-file` owns the format.** The parser and the writer are one module of `dsh-app-boot`, exported as `./patch-file` so the roster and the plugin manager load them without the boot entry; `parsePatchList` is the one parser (js-yaml with the include's `!!js` dialect, relative names anchored to the file); `PatchDocument` edits a file at the key level through the `yaml` package's comment-preserving document, which keeps an unresolved `!!js` tag on its scalar and prints it back verbatim; `mutatePatchFile` takes the `dsh-atomic-write` lock, reads, edits, replaces atomically when the document is dirty, and parses the written text back. A removed row's comment block moves to its neighbour or the document's trailing comment instead of vanishing.
-
-## Alternatives considered
-
-**A settings namespace per preset for enablement.** Rejected earlier in the design: enablement is composition, not preference, and the profile's own user layer is already a patch file; a preset's should be the same file in the same format.
-
-**Editing the shipped composition in place.** Rejected: an upgrade overwrites it, and a session already running would be the one reading a half-written file.
-
-**A second parser in `dsh-agent-presets` to avoid a dependency on `dsh-app-boot`.** Rejected: two parsers of one format drift. A separate `dsh-patch-file` util package was tried first and folded back into `dsh-app-boot` as its `./patch-file` export: the format is the Loader's and the include's, not a mechanical primitive, and a dozen plugin packages already depend on `dsh-app-boot`, so the roster joining them costs nothing.
-
-**A `dsh-global-tool-mask` row: `tools.restrict()` as a composition row, so a preset's layer could hide a host tool.** Built, then removed before merge: no shipped preset, the plugin manager, or the settings UI composes one, so it was a package with a README and no consumer. A layer adds rows and switches the composition's rows off; hiding a host tool from one preset is a per-preset setting for the scope-resolved settings of the next layer, not a hand-written row.
-
-## Consequences
-
-A person hides a tool from `standard` with three lines under `.agent-presets/standard/` and keeps the shipped composition; the plugin manager writes the same file for them. A layer applies to the sessions created after it changes, never to running ones. The layer addresses rows by the composition's own ids, so an id the composition leaves anonymous cannot be targeted.
-
-## Testing
-
-`packages/preset/agent-presets/tests/overlay.spec.ts` pins discovery (attach, own layer, orphan slot, unparsable, malformed, unresolvable insert, composition verdict first), the mount applying the layer, generations across edits with the retired generation reused, the inventory's `source` and `disabledBy` from the file and from a mount, the layer path for shipped and authored presets, copy carrying the layer, and removal. `packages/boot/app-boot/tests/patch-file.spec.ts` pins the parser, the document edits with comments and `!!js` intact, and the locked atomic mutation with readback.

+ 0 - 35
.agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md

@@ -1,35 +0,0 @@
-# Agent Note:preset 获得用户补丁层,补丁文件有了唯一的家
-
-Status: implemented
-
-[English](2026-09-04-preset-user-patch-layer-and-patch-file-writer.md) | 中文
-
-## 问题
-
-随附的 agent preset 在任何意义上都是只读的:想关掉它的一行或给它加一个工具,只能整个复制 preset 再改副本,而副本从此在每次升级时与随附版本渐行渐远。profile 早已具备解决这件事的形状——一份基底组合加一层施加其上的 `cordis.patch.yml` 用户层——而 preset 没有任何对应物。另一方面,这种文件格式在 `dsh-app-boot` 里只有一个解析器而没有写入器:想关掉一行的程序只能整个改写文件,丢掉作者的注释和它看不懂的 `!!js` 门。
-
-## 决定
-
-**preset 的用户层就是其槽位里的 `cordis.patch.yml`。** 本地创作的 preset 放在组合旁边;随附 preset 则单独放在用户根目录里同 id 的目录(`$DSH_HOME/.agent-presets/<id>/cordis.patch.yml`),其安装目录不动。discovery 把这一层挂到赢得该 id 的 preset 上,与组合一起判定健康(不可解析的层、畸形的 insert、或插入了无法解析模块的行,都以该原因让 preset 变为 broken),并把没有任何根提供其 id 的层报告为损坏的槽位而不是藏起来。mount 把解析后的层作为 include 的运行时 patches 交上去,于是它以 Loader 自己的补丁语义施加。组合清单在展平前先施加它,并给每一行标上 `source: 'preset' | 'user'`,关闭时再标上 `disabledBy: 'composition' | 'user'`。复制会把该层带到新组合旁边;`removeOverlay` 删除它以及被清空的槽位。
-
-**代际跟随层的内容。** 常驻挂载的印记现在覆盖组合文件的 stat 与层文本的摘要。层变化即为之后创建的会话开启下一代,一如组合被编辑时的既有行为;层被改回某个早先代际组合过的内容时,回到那一代,因此一行关掉再打开不会堆出第三棵在线子树。
-
-**`dsh-app-boot/patch-file` 拥有这种格式。** 解析器与写入器是 `dsh-app-boot` 的同一个模块,以 `./patch-file` 导出,roster 与插件管理器加载它时不经过启动入口;`parsePatchList` 是唯一的解析器(js-yaml 配 include 的 `!!js` 方言,相对名字锚定到文件);`PatchDocument` 通过 `yaml` 包保留注释的 document 在键级编辑文件,它把未解析的 `!!js` 标签留在标量上并原样打印;`mutatePatchFile` 拿 `dsh-atomic-write` 的锁,读取、编辑、在 document 变脏时原子替换,并把写出的文本回读解析。被移除行上方的注释块移到邻居或文档尾注释,而不是消失。
-
-## 考虑过的替代方案
-
-**为每个 preset 用一个 settings 命名空间做启停。** 在设计阶段已否决:启停是组合而非偏好,profile 自己的用户层本就是补丁文件;preset 的应当是同样格式的同一种文件。
-
-**原地编辑随附组合。** 否决:升级会覆盖它,而正在运行的会话会读到写了一半的文件。
-
-**在 `dsh-agent-presets` 里再写一个解析器以避免依赖 `dsh-app-boot`。** 否决:同一格式的两个解析器会漂移。曾先做成独立的 `dsh-patch-file` util 包,后折回 `dsh-app-boot` 作为它的 `./patch-file` 导出:这种格式属于 Loader 与 include,不是机械原语,而且已有十几个插件包依赖 `dsh-app-boot`,roster 加入其中不增加任何代价。
-
-**一个 `dsh-global-tool-mask` 行:把 `tools.restrict()` 写成组合行,让 preset 的层能遮蔽宿主工具。** 做出来后在合并前移除:随附 preset、插件管理器与设置 UI 都不会组合它,它是一个只有 README 没有消费者的包。层负责加行和关掉组装自己的行;对某个 preset 隐藏宿主工具属于下一层按 scope 解析的设置里的一条 preset 级设置,不是手写的行。
-
-## 后果
-
-一个人用 `.agent-presets/standard/` 下的三行就能对 `standard` 隐藏一个工具并保留随附组合;插件管理器替他们写同一个文件。层作用于其变化之后创建的会话,从不作用于运行中的会话。层按组合自己的 id 寻址行,因此组合留作匿名的行无法被定位。
-
-## 测试
-
-`packages/preset/agent-presets/tests/overlay.spec.ts` 钉住 discovery(附着、自有层、孤儿槽位、不可解析、畸形、不可解析的插入、组合判定优先)、施加层的挂载、编辑之间的代际及退役代际的复用、清单从文件与从挂载得到的 `source` 与 `disabledBy`、随附与自作 preset 的层路径、复制携带层,以及移除。`packages/boot/app-boot/tests/patch-file.spec.ts` 钉住解析器、保留注释与 `!!js` 的文档编辑,以及带回读的加锁原子变更。

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.i18n.yaml → .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.md
-2026-09-04-preset-user-patch-layer-and-patch-file-writer.md: 78dbea52069d50c5772d47e72ab7b0a963ea21a5
-2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md: 8129ebeb7a6c01feb2cbdd5872199460db8b44a5
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.md
+2026-09-04-profile-patch-file-writer.md: a7c8e5f10b33b0471e6d9fc225a7432a1ea14164
+2026-09-04-profile-patch-file-writer.zh.md: 57d73bdee6ce77afd0fd1a1551c1a39001891be9

+ 25 - 0
.agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.md

@@ -0,0 +1,25 @@
+# Agent Note: Profile patch files share one parser and writer
+
+Status: implemented
+
+English | [中文](2026-09-04-profile-patch-file-writer.zh.md)
+
+## Problem
+
+The launcher and plugin manager consume the same profile patch format. Separate parsers can disagree about accepted rows or relative module paths, and rewriting a YAML document as plain objects loses comments and unresolved `!!js` expressions.
+
+## Decision
+
+`dsh-app-boot/patch-file` owns patch parsing and editing. The boot entry delegates to its parser; management operations use its comment-preserving YAML document and locked atomic writer. Relative plugin names resolve from the patch file's directory. Editing a row preserves unrelated fields, comments and expression text; deleting a row preserves its comments on a neighbour or the document.
+
+## Alternatives considered
+
+**Independent parsers or whole-document serialization.** Both let a file accepted by one consumer change meaning in another. A shared parser defines accepted input, while the YAML document editor retains syntax that plain object serialization discards.
+
+## Consequences
+
+Global row operations edit the profile user layer without modifying bundle files. A write holds the file lock across read, mutation and atomic replacement, then parses the written result. Unchanged documents are not rewritten.
+
+## Testing
+
+`packages/boot/app-boot/tests/patch-file.spec.ts` covers parsing, relative module paths, comments, `!!js`, row edits, locking and write failures. Plugin-manager tests exercise global row changes through real profile files.

+ 25 - 0
.agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.zh.md

@@ -0,0 +1,25 @@
+# Agent Note: Profile patch 文件共用一个解析器与写入器
+
+Status: implemented
+
+[English](2026-09-04-profile-patch-file-writer.md) | 中文
+
+## Problem
+
+启动器与插件管理器消费相同的 profile patch 格式。独立解析器可能对可接受的行或相对模块路径得出不同结果,而将 YAML 文档转成普通对象重写会丢失注释和未求值的 `!!js` 表达式。
+
+## Decision
+
+`dsh-app-boot/patch-file` 负责 patch 解析与编辑。启动入口委托它解析;管理操作使用它保留注释的 YAML 文档和持锁的原子写入器。相对插件名以 patch 文件所在目录解析。编辑行时保留无关字段、注释与表达式文本;删除行时将其注释保留到相邻行或文档上。
+
+## Alternatives considered
+
+**独立解析器或整份文档序列化。** 两者都可能让一个消费方接受的文件在另一个消费方中改变含义。共享解析器定义可接受的输入,YAML 文档编辑器则保留普通对象序列化会丢弃的语法。
+
+## Consequences
+
+全局行操作编辑 profile 用户层,不修改组合包文件。一次写入在读取、修改和原子替换期间持有文件锁,然后解析写入结果。未改变的文档不会重写。
+
+## Testing
+
+`packages/boot/app-boot/tests/patch-file.spec.ts` 覆盖解析、相对模块路径、注释、`!!js`、行编辑、锁和写入失败。插件管理器测试通过真实 profile 文件验证全局行修改。

+ 2 - 2
docs/config-catalog.md

@@ -157,7 +157,7 @@ export interface PresetRoot {
 export type PresetTrust = 'system' | 'user'
 ```
 
-Source: [`packages/preset/agent-presets/src/preset.ts:60`](../packages/preset/agent-presets/src/preset.ts)
+Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/agent-presets/src/preset.ts)
 
 <a id="deepseek-aidsh-agent-tool-presentation"></a>
 
@@ -942,7 +942,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/host/plugin-manager/src/index.ts:42`](../packages/host/plugin-manager/src/index.ts)
+Source: [`packages/host/plugin-manager/src/index.ts:40`](../packages/host/plugin-manager/src/index.ts)
 
 <a id="deepseek-aidsh-host-webserver"></a>
 

+ 1 - 1
docs/event-producer-consumer.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 docs/event-producer-consumer.md
-event-producer-consumer.md: 53415610df183d86abf2eb93153d5ffade3fb8a7
+event-producer-consumer.md: f30ae258881eee0cf3b220aaf760e17ac1a900dd
 event-producer-consumer.zh.md: 02645d1b5ba78a23f75a3d4c7e81fea2c2809f3a

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

@@ -48,8 +48,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:72`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:44`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
-| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:240`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:246`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:233`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:239`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |

+ 2 - 2
docs/module-graph.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 docs/module-graph.md
-module-graph.md: 74319fd35422752e2803c7605bfff0fdf6e30e38
-module-graph.zh.md: a0adbe2e119487f6b1cf44ab7440fee7a3216397
+module-graph.md: 5a30446d3962a56e0273153ed5d85644c6817948
+module-graph.zh.md: 87b971b11686ce06b2fa4eeef3b006cc3a843dda

+ 7 - 9
docs/module-graph.md

@@ -580,6 +580,11 @@ flowchart TD
   pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime
   pkg_experimental_ptc_runtime_python --> pkg_timeout
   pkg_experimental_ptc_runtime_python --> pkg_util_values
+  pkg_host_plugin_manager --> pkg_agent
+  pkg_host_plugin_manager --> pkg_app_boot
+  pkg_host_plugin_manager --> pkg_plugin_manager
+  pkg_host_plugin_manager --> pkg_typert_protocol
+  pkg_host_plugin_manager --> pkg_util_values
   pkg_commands --> pkg_agent
   pkg_commands --> pkg_attachment
   pkg_commands --> pkg_brand
@@ -848,7 +853,6 @@ flowchart TD
   pkg_mcp_resources --> pkg_system_prompt
   pkg_mcp_resources --> pkg_tools
   pkg_agent_presets --> pkg_agent
-  pkg_agent_presets --> pkg_app_boot
   pkg_agent_presets --> pkg_atomic_write
   pkg_agent_presets --> pkg_home_paths
   pkg_agent_presets --> pkg_invariants
@@ -960,12 +964,6 @@ flowchart TD
   pkg_host_plugin_inventory --> pkg_app_boot
   pkg_host_plugin_inventory --> pkg_brand
   pkg_host_plugin_inventory --> pkg_typert_protocol
-  pkg_host_plugin_manager --> pkg_agent
-  pkg_host_plugin_manager --> pkg_agent_presets
-  pkg_host_plugin_manager --> pkg_app_boot
-  pkg_host_plugin_manager --> pkg_plugin_manager
-  pkg_host_plugin_manager --> pkg_typert_protocol
-  pkg_host_plugin_manager --> pkg_util_values
   pkg_mcp_client --> pkg_attachment
   pkg_mcp_client --> pkg_llm
   pkg_mcp_client --> pkg_mcp_resources
@@ -1420,6 +1418,7 @@ flowchart TD
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
 | [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
+| [`host-plugin-manager`](../packages/host/plugin-manager) | `host` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`plugin-manager`](../packages/boot/plugin-manager), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
 | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
@@ -1472,7 +1471,7 @@ flowchart TD
 | [`tool-jobs`](../packages/jobs/tool-jobs) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`mcp-resources`](../packages/mcp/mcp-resources) | `mcp` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
-| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
 | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
 | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
 | [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
@@ -1492,7 +1491,6 @@ flowchart TD
 | [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`app-boot`](../packages/boot/app-boot), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) |
-| [`host-plugin-manager`](../packages/host/plugin-manager) | `host` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`app-boot`](../packages/boot/app-boot), [`plugin-manager`](../packages/boot/plugin-manager), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
 | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |

+ 7 - 9
docs/module-graph.zh.md

@@ -582,6 +582,11 @@ flowchart TD
   pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime
   pkg_experimental_ptc_runtime_python --> pkg_timeout
   pkg_experimental_ptc_runtime_python --> pkg_util_values
+  pkg_host_plugin_manager --> pkg_agent
+  pkg_host_plugin_manager --> pkg_app_boot
+  pkg_host_plugin_manager --> pkg_plugin_manager
+  pkg_host_plugin_manager --> pkg_typert_protocol
+  pkg_host_plugin_manager --> pkg_util_values
   pkg_commands --> pkg_agent
   pkg_commands --> pkg_attachment
   pkg_commands --> pkg_brand
@@ -850,7 +855,6 @@ flowchart TD
   pkg_mcp_resources --> pkg_system_prompt
   pkg_mcp_resources --> pkg_tools
   pkg_agent_presets --> pkg_agent
-  pkg_agent_presets --> pkg_app_boot
   pkg_agent_presets --> pkg_atomic_write
   pkg_agent_presets --> pkg_home_paths
   pkg_agent_presets --> pkg_invariants
@@ -962,12 +966,6 @@ flowchart TD
   pkg_host_plugin_inventory --> pkg_app_boot
   pkg_host_plugin_inventory --> pkg_brand
   pkg_host_plugin_inventory --> pkg_typert_protocol
-  pkg_host_plugin_manager --> pkg_agent
-  pkg_host_plugin_manager --> pkg_agent_presets
-  pkg_host_plugin_manager --> pkg_app_boot
-  pkg_host_plugin_manager --> pkg_plugin_manager
-  pkg_host_plugin_manager --> pkg_typert_protocol
-  pkg_host_plugin_manager --> pkg_util_values
   pkg_mcp_client --> pkg_attachment
   pkg_mcp_client --> pkg_llm
   pkg_mcp_client --> pkg_mcp_resources
@@ -1422,6 +1420,7 @@ flowchart TD
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
 | [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
+| [`host-plugin-manager`](../packages/host/plugin-manager) | `host` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`plugin-manager`](../packages/boot/plugin-manager), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
 | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
@@ -1474,7 +1473,7 @@ flowchart TD
 | [`tool-jobs`](../packages/jobs/tool-jobs) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`mcp-resources`](../packages/mcp/mcp-resources) | `mcp` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
-| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
 | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
 | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
 | [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
@@ -1494,7 +1493,6 @@ flowchart TD
 | [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`app-boot`](../packages/boot/app-boot), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) |
-| [`host-plugin-manager`](../packages/host/plugin-manager) | `host` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`app-boot`](../packages/boot/app-boot), [`plugin-manager`](../packages/boot/plugin-manager), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
 | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |

+ 2 - 2
docs/subsystems/core.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 docs/subsystems/core.md
-core.md: 8058918f8cf1fd53cf424d2ddd09ea735eab63cc
-core.zh.md: e552d25f37ff9c2ba59c4f331693f5b5c88060e3
+core.md: 9ffdfdac6bc4262a541eed214386d8649ff041c1
+core.zh.md: 45298191ecaf3d03894638e3167712bf6fe39a2a

+ 5 - 30
docs/subsystems/core.md

@@ -600,28 +600,6 @@ composedPreset(agentCtx: Context): string | undefined
  */
 async read(id: string): Promise<string>
 
-/**
- * Where one preset's user patch layer is, or would be written: the layer
- * discovery attached, else the writable root's slot of the same id — beside
- * the composition for a locally authored preset, alone in the slot for a
- * shipped one. The file need not exist yet.
- * @param id - the preset id.
- * @returns the absolute path of the layer file.
- * @throws when the preset is unknown, or it has no layer and the
- * deployment configures no writable root.
- */
-async overlayPathFor(id: string): Promise<string>
-
-/**
- * Delete one preset's user patch layer, so the next generation composes the
- * preset exactly as its root supplies it. Sessions already joined keep the
- * generation they run on.
- * @param id - the preset id.
- * @returns true when a layer was removed; false when the preset had none.
- * @throws when the preset is unknown or its layer lies outside the writable root.
- */
-async removeOverlay(id: string): Promise<boolean>
-
 /**
  * One preset's composition text with the roster row it belongs to.
  * @param agentPreset - the preset id.
@@ -926,7 +904,7 @@ Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index
 
 The `pluginManager` service and the `plugins` Remote.
 
-The row injects only the Loader; the profile runtime, the preset roster, and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.
+The row injects only the Loader; the profile runtime and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.
 
 ```ts cordis-catalog
 /**
@@ -971,29 +949,26 @@ The row injects only the Loader; the profile runtime, the preset roster, and the
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 
 /**
- * Add a row naming one of the package's modules to a user layer.
+ * Add a row naming one of the package's modules to the profile user layer.
  * @param packageName - the installed package.
- * @param target - which layer.
  * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
  * `id` overrides the derived row id, `config` overrides the declared default.
  * @returns where the row landed.
  */
-@Remote('addRow') async addRow( packageName: string, target: PluginRowTarget, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
+@Remote('addRow') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
 
 /**
  * Remove a row a user layer inserted.
- * @param target - which layer.
  * @param rowId - the inserted row's id.
  */
-@Remote('removeRow') async removeRow(target: PluginRowTarget, rowId: string): Promise<void>
+@Remote('removeRow') async removeRow(rowId: string): Promise<void>
 
 /**
  * Switch one row off or on in a user layer; deny-only.
- * @param target - which layer.
  * @param rowId - the row's id as the composition declares it.
  * @param disabled - whether the layer should switch the row off.
  */
-@Remote('setRowDisabled') async setRowDisabled(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void>
+@Remote('setRowDisabled') async setRowDisabled(rowId: string, disabled: boolean): Promise<void>
 
 /**
  * What disabling or removing a package would strand.

+ 5 - 30
docs/subsystems/core.zh.md

@@ -610,28 +610,6 @@ composedPreset(agentCtx: Context): string | undefined
  */
 async read(id: string): Promise<string>
 
-/**
- * Where one preset's user patch layer is, or would be written: the layer
- * discovery attached, else the writable root's slot of the same id — beside
- * the composition for a locally authored preset, alone in the slot for a
- * shipped one. The file need not exist yet.
- * @param id - the preset id.
- * @returns the absolute path of the layer file.
- * @throws when the preset is unknown, or it has no layer and the
- * deployment configures no writable root.
- */
-async overlayPathFor(id: string): Promise<string>
-
-/**
- * Delete one preset's user patch layer, so the next generation composes the
- * preset exactly as its root supplies it. Sessions already joined keep the
- * generation they run on.
- * @param id - the preset id.
- * @returns true when a layer was removed; false when the preset had none.
- * @throws when the preset is unknown or its layer lies outside the writable root.
- */
-async removeOverlay(id: string): Promise<boolean>
-
 /**
  * One preset's composition text with the roster row it belongs to.
  * @param agentPreset - the preset id.
@@ -936,7 +914,7 @@ Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index
 
 The `pluginManager` service and the `plugins` Remote.
 
-The row injects only the Loader; the profile runtime, the preset roster, and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.
+The row injects only the Loader; the profile runtime and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.
 
 ```ts cordis-catalog
 /**
@@ -981,29 +959,26 @@ The row injects only the Loader; the profile runtime, the preset roster, and the
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 
 /**
- * Add a row naming one of the package's modules to a user layer.
+ * Add a row naming one of the package's modules to the profile user layer.
  * @param packageName - the installed package.
- * @param target - which layer.
  * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
  * `id` overrides the derived row id, `config` overrides the declared default.
  * @returns where the row landed.
  */
-@Remote('addRow') async addRow( packageName: string, target: PluginRowTarget, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
+@Remote('addRow') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
 
 /**
  * Remove a row a user layer inserted.
- * @param target - which layer.
  * @param rowId - the inserted row's id.
  */
-@Remote('removeRow') async removeRow(target: PluginRowTarget, rowId: string): Promise<void>
+@Remote('removeRow') async removeRow(rowId: string): Promise<void>
 
 /**
  * Switch one row off or on in a user layer; deny-only.
- * @param target - which layer.
  * @param rowId - the row's id as the composition declares it.
  * @param disabled - whether the layer should switch the row off.
  */
-@Remote('setRowDisabled') async setRowDisabled(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void>
+@Remote('setRowDisabled') async setRowDisabled(rowId: string, disabled: boolean): Promise<void>
 
 /**
  * What disabling or removing a package would strand.

+ 2 - 2
packages/boot/app-boot/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/app-boot/README.md
-README.md: 94e46a03f5bd7603e8a5444e2663b937f028e65f
-README.zh.md: 516128a34ab8df8a96a4a9b7bfa5b12dba1b9112
+README.md: e070f187379638dff75e1bcfc3bfe835195d08c0
+README.zh.md: 9d56700ca22743c7fc484bba113b718751f935e0

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

@@ -65,7 +65,7 @@ The launcher provides `ctx.profileRuntime` before any configuration entry mounts
 <a id="patch-files"></a>
 ### Patch files
 
-Every layer above is a `cordis.patch.yml`: a top-level YAML sequence of the include plugin's `PatchOptions` — id-targeted overrides and `insert` lists — in the Loader's dialect, where `!!js` marks an expression the row's fiber evaluates. The `./patch-file` export is the one place that reads and writes such a file, so a file the boot accepts is a file the agent-preset roster and the plugin manager accept.
+Every layer above is a `cordis.patch.yml`: a top-level YAML sequence of the include plugin's `PatchOptions` — id-targeted overrides and `insert` lists — in the Loader's dialect, where `!!js` marks an expression the row's fiber evaluates. The `./patch-file` export is the one place that reads and writes such a file, so a file the boot accepts is a file the plugin manager accepts.
 
 Read a layer with `parsePatchList` (text in hand) or `readPatchListFile` (an absent file reads as `undefined`). Both anchor a relative `insert` row name such as `./plugin.js` to the file's own directory and fail loud on anything that is not a sequence of mappings, because a patch file that cannot be applied at all is a misconfiguration; a patch whose target row is absent stays a per-entry Loader warning.
 

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

@@ -65,7 +65,7 @@ launcher 在任何配置行挂载前提供 `ctx.profileRuntime`。该运行时
 <a id="patch-files"></a>
 ### 补丁文件
 
-上面的每一层都是一个 `cordis.patch.yml`:一个顶层 YAML 序列,元素是 include 插件的 `PatchOptions`——按 id 定位的覆盖与 `insert` 列表——采用 Loader 的方言,其中 `!!js` 标记一个由该行 fiber 求值的表达式。`./patch-file` 导出是读写这种文件的唯一地方,因此启动接受的文件就是 agent preset roster 与插件管理器接受的文件。
+上面的每一层都是一个 `cordis.patch.yml`:一个顶层 YAML 序列,元素是 include 插件的 `PatchOptions`——按 id 定位的覆盖与 `insert` 列表——采用 Loader 的方言,其中 `!!js` 标记一个由该行 fiber 求值的表达式。`./patch-file` 导出是读写这种文件的唯一地方,因此启动接受的文件就是 插件管理器接受的文件。
 
 用 `parsePatchList`(已有文本)或 `readPatchListFile`(文件不存在时读到 `undefined`)读取一层。两者都把 `insert` 行中相对的名字(如 `./plugin.js`)锚定到文件自己的目录,并对任何不是"映射序列"的内容直接报错,因为一个完全无法施加的补丁文件就是配置错误;而目标行不存在的单条补丁仍然只是 Loader 的逐条警告。
 

+ 2 - 2
packages/boot/plugin-manager/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/plugin-manager/README.md
-README.md: 2ea3709d441ca9327048ffe0e001acf08020c33d
-README.zh.md: 6c0cc3d17f67f96ab41cb6e3625f1cfa536671a9
+README.md: 538f837dbaaec0b9b89d0ba2a70a44d5f67cf32f
+README.zh.md: 803dbd572b735f95f828260290fd7d39dd4e6fbe

+ 5 - 9
packages/boot/plugin-manager/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-plugin-manager` installs packages and manages their declared plugin rows through the CLI and Web host. Installation reads metadata without executing modules. The manager enables whole bundle layers, edits global or preset user patches, retries failures, and reports current runtime issues. Failures carry `plugins/*` codes; the [Host adapter](../../host/plugin-manager/README.md) exposes the operations through Remote.
+`dsh-plugin-manager` installs packages and manages their declared plugin rows through the CLI and Web host. Installation reads metadata without executing modules. The manager enables whole bundle layers, edits the profile user patch, retries failures, and reports current runtime issues. Failures carry `plugins/*` codes; the [Host adapter](../../host/plugin-manager/README.md) exposes the operations through Remote.
 
 ## Table of Contents
 
@@ -51,12 +51,11 @@ console.log(outcome.installed, outcome.removed)
 
 ### Managing the booted profile
 
-Build a `PluginManager` over the Cordis context and readers for what it needs per call — the profile runtime, the preset roster's layers, and the running-agent count — so a composition that gains or lacks one of them is answered at call time rather than at mount:
+Build a `PluginManager` over the Cordis context and readers for what it needs per call — the profile runtime and the running-agent count — so a composition that gains or lacks one of them is answered at call time rather than at mount:
 
 ```ts
 import type { Context } from '@deepseek-ai/cordis'
 import type {} from '@deepseek-ai/dsh-agent'
-import type {} from '@deepseek-ai/dsh-agent-presets'
 import type {} from '@deepseek-ai/dsh-app-boot'
 import { PluginManager, type PluginToolingConfig } from '@deepseek-ai/dsh-plugin-manager'
 
@@ -66,7 +65,6 @@ declare const config: PluginToolingConfig
 const manager = new PluginManager(ctx, {
   config,
   runtime: () => ctx.get('profileRuntime'),
-  presets: () => ctx.get('agentPresets'),
   runningAgents: () => (ctx.get('agents')?.list() ?? []).filter(agent => agent.status === 'running').length,
 })
 console.log(await manager.list())
@@ -76,13 +74,13 @@ console.log(await manager.list())
 
 `enable` selects the whole bundle layer and recomposes live profiles. Per-row failures retain the enabled choice and successful siblings; results report `issues`, and the list can show `partial` or `failed`. Preparation failures revert the enable selection and raise `plugins/enable-failed`. `disable` removes the whole layer, including overrides. `retry` disables, awaits cleanup, and enables again. Startup-only profiles report `effect: restart`. Unapplied selections on live profiles report `failed`, preserving the rows actually running; `restart-required` is reserved for startup-only profiles. `uninstall` disables the bundle, removes user-inserted references, and runs pnpm remove.
 
-`addRow` writes an explicitly declared `dsh.plugins` module to the profile’s global `cordis.patch.yml` or a preset user layer. It preserves declared defaults, checks the target row id, and performs no pre-mount import. `removeRow` removes a user insert. `setRowDisabled` writes or removes `disabled: true`, preserving the bundle’s own condition. Global edits recompose immediately on live profiles; preset edits apply to subsequent generations. `dependents` reports injection dependents and user-layer module references.
+`addRow` writes an explicitly declared `dsh.plugins` module to the profile’s global `cordis.patch.yml`. It preserves declared defaults, checks the target row id, and performs no pre-mount import. `removeRow` removes a user insert. `setRowDisabled` writes or removes `disabled: true`, preserving the bundle’s own condition. Global edits recompose immediately on live profiles. `dependents` reports injection dependents and user-layer module references.
 
 The manager runs one mutation at a time — a second call while one runs fails with `plugins/busy` naming the operation in flight — and `add` and `uninstall` refuse to change `node_modules` while a session is running, with `plugins/agents-running`. Every change is followed by a `plugins/changed` event on the context, and an install run emits pnpm's output as `plugins/install-log` chunks, each naming the command line it ran and the profile directory it ran in, and, with colour on, carrying pnpm's SGR escapes.
 
 ### Failures
 
-Every refusal or failure is a `PluginOperationError` with a stable `code` and `details` typed by it: `plugins/unavailable` (no profile runtime, or no roster for a preset target), `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/row-conflict`, `plugins/busy`, `plugins/agents-running`, and `plugins/bad-request` for a request that names nothing the profile has. `pluginOperationFailureOf` narrows a caught value to the code-discriminated union.
+Every refusal or failure is a `PluginOperationError` with a stable `code` and `details` typed by it: `plugins/unavailable` (no profile runtime), `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/row-conflict`, `plugins/busy`, `plugins/agents-running`, and `plugins/bad-request` for a request that names nothing the profile has. `pluginOperationFailureOf` narrows a caught value to the code-discriminated union.
 
 -----
 
@@ -106,7 +104,7 @@ Retry removes the entire layer and awaits removed-fiber cleanup before adding it
 
 ### What the manager reads and what it is handed
 
-The manager reads Loader entries, the reflect store and entry-owned diagnostics. Profile, preset and agent facts arrive through per-call readers. The [Host adapter](../../host/plugin-manager/README.md) converts Loader lifecycle changes into `plugins/changed` notifications after settlement, including a pending row whose provider becomes available.
+The manager reads Loader entries, the reflect store and entry-owned diagnostics. Profile and agent facts arrive through per-call readers. The [Host adapter](../../host/plugin-manager/README.md) converts Loader lifecycle changes into `plugins/changed` notifications after settlement, including a pending row whose provider becomes available.
 
 ### Source map
 
@@ -133,7 +131,6 @@ Read these when the manager's contract is not enough: the runtime it drives, the
 
 - [App boot](../app-boot/README.md) — the profile runtime, external bundle isolation, and static package declarations.
 - [Patch files](../app-boot/README.md#patch-files) — how user-layer rows are read and written.
-- [Agent presets](../../preset/agent-presets/README.md) — the per-preset user layer a preset target writes.
 - [Host plugin manager](../../host/plugin-manager/README.md) — the `plugins` Remote over this manager.
 - [dsh app](../../../apps/cli/README.md) — the `dsh plugin` command over the installer.
 
@@ -157,7 +154,6 @@ These limits define what the manager will not do for a caller. They are current
 
 - **Updating a loaded package needs a restart** — Node caches ESM modules by URL and a hoisted install keeps the path; `pnpm update` through `add` rewrites the files but the running tree keeps the old modules until the process restarts.
 - **Dependents stop at injection** — a registry-type dependency (a tool, an LLM adapter) has no `inject` edge, so `dependents` cannot name a row that only reads what the package registered.
-- **A preset row is not composed live** — the manager writes the preset's layer; sessions created afterwards compose it, sessions already running keep their generation.
 - **No `engines.dsh` check yet** — the range is reported, not enforced against the running harness version.
 - **One process at a time** — the mutex is in-process and the patch-file writer takes a file lock, but the profile manifest has no lock: the CLI and a running Web host editing the same profile at once is not supported.
 

+ 5 - 9
packages/boot/plugin-manager/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-plugin-manager` 通过 CLI 与 Web 宿主安装包并管理它们声明的插件行。安装读取元信息,不执行模块。管理器启停整份组合包层,编辑全局或预设用户 patch,重试失败并报告当前运行问题。失败携带 `plugins/*` 错误码;[Host 适配器](../../host/plugin-manager/README.zh.md) 通过 Remote 暴露这些操作。
+`dsh-plugin-manager` 通过 CLI 与 Web 宿主安装包并管理它们声明的插件行。安装读取元信息,不执行模块。管理器启停整份组合包层,编辑profile 用户 patch,重试失败并报告当前运行问题。失败携带 `plugins/*` 错误码;[Host 适配器](../../host/plugin-manager/README.zh.md) 通过 Remote 暴露这些操作。
 
 ## 目录
 
@@ -51,12 +51,11 @@ console.log(outcome.installed, outcome.removed)
 
 ### 管理已启动的 profile
 
-在 Cordis 上下文之上构造 `PluginManager`,并交给它按调用读取所需之物的读取器——profile runtime、preset roster 的层、运行中的 agent 数——这样一个后来才有或始终没有其中之一的组合在调用时得到回答,而不是在挂载时:
+在 Cordis 上下文之上构造 `PluginManager`,并交给它按调用读取所需之物的读取器——profile runtime 和运行中的 agent 数——这样一个后来才有或始终没有其中之一的组合在调用时得到回答,而不是在挂载时:
 
 ```ts
 import type { Context } from '@deepseek-ai/cordis'
 import type {} from '@deepseek-ai/dsh-agent'
-import type {} from '@deepseek-ai/dsh-agent-presets'
 import type {} from '@deepseek-ai/dsh-app-boot'
 import { PluginManager, type PluginToolingConfig } from '@deepseek-ai/dsh-plugin-manager'
 
@@ -66,7 +65,6 @@ declare const config: PluginToolingConfig
 const manager = new PluginManager(ctx, {
   config,
   runtime: () => ctx.get('profileRuntime'),
-  presets: () => ctx.get('agentPresets'),
   runningAgents: () => (ctx.get('agents')?.list() ?? []).filter(agent => agent.status === 'running').length,
 })
 console.log(await manager.list())
@@ -76,13 +74,13 @@ console.log(await manager.list())
 
 `enable` 选择整份组合包层,并在实时 profile 中重组。逐行失败保留启用选择与成功的其他行;结果报告 `issues`,列表可显示 `partial` 或 `failed`。准备失败会撤销启用选择,并抛出 `plugins/enable-failed`。`disable` 移除整层,包括覆盖。`retry` 先禁用并等待清理,再启用。仅启动时生效的 profile 报告 `effect: restart`。实时 profile 中尚未应用的选择报告 `failed`,并保留实际运行的行;`restart-required` 只用于仅启动时生效的 profile。`uninstall` 禁用组合包、删除用户插入的引用、执行 pnpm remove。
 
-`addRow` 将显式声明的 `dsh.plugins` 模块写入 profile 的全局 `cordis.patch.yml` 或预设用户层。它保留声明的默认配置,检查目标行 id,不在挂载前 import。`removeRow` 删除用户插入。`setRowDisabled` 写入或删除 `disabled: true`,保留组合包自己的条件。全局编辑在实时 profile 中立即重组;预设编辑应用于后续代际。`dependents` 报告注入依赖方与用户层模块引用。
+`addRow` 将显式声明的 `dsh.plugins` 模块写入 profile 的全局 `cordis.patch.yml`。它保留声明的默认配置,检查目标行 id,不在挂载前 import。`removeRow` 删除用户插入。`setRowDisabled` 写入或删除 `disabled: true`,保留组合包自己的条件。全局编辑在实时 profile 中立即重组。`dependents` 报告注入依赖方与用户层模块引用。
 
 管理器一次只跑一个变更——上一个还在跑时再调用会以 `plugins/busy` 失败并点名正在进行的操作——`add` 与 `uninstall` 在有会话运行时拒绝改动 `node_modules`,报 `plugins/agents-running`。每次变更之后在上下文上发出 `plugins/changed` 事件,安装运行把 pnpm 的输出以 `plugins/install-log` 分块发出,每块都写明所跑的命令行与所在的 profile 目录,开了颜色时还带着 pnpm 的 SGR 转义。
 
 ### 失败
 
-每次拒绝或失败都是一个 `PluginOperationError`,带稳定的 `code` 与按码定型的 `details`:`plugins/unavailable`(没有 profile runtime,或 preset 目标没有 roster)、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/row-conflict`、`plugins/busy`、`plugins/agents-running`,以及请求点名了 profile 没有的东西时的 `plugins/bad-request`。`pluginOperationFailureOf` 把捕获到的值收窄为按码区分的联合。
+每次拒绝或失败都是一个 `PluginOperationError`,带稳定的 `code` 与按码定型的 `details`:`plugins/unavailable`(没有 profile runtime)、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/row-conflict`、`plugins/busy`、`plugins/agents-running`,以及请求点名了 profile 没有的东西时的 `plugins/bad-request`。`pluginOperationFailureOf` 把捕获到的值收窄为按码区分的联合。
 
 -----
 
@@ -106,7 +104,7 @@ subprocess seam 会清洗形似密钥的变量且没有 shell 模式,而 pnpm
 
 ### 管理器自己读什么,别人交给它什么
 
-管理器读取 Loader 条目、reflect 存储与条目自身诊断。profile、预设和 agent 信息通过逐次调用的读取器提供。[Host 适配器](../../host/plugin-manager/README.zh.md) 在状态稳定后将 Loader 生命周期变化转换为 `plugins/changed` 通知,包括等待中的行在提供方出现后恢复运行。
+管理器读取 Loader 条目、reflect 存储与条目自身诊断。profile 和 agent 信息通过逐次调用的读取器提供。[Host 适配器](../../host/plugin-manager/README.zh.md) 在状态稳定后将 Loader 生命周期变化转换为 `plugins/changed` 通知,包括等待中的行在提供方出现后恢复运行。
 
 ### 源码地图
 
@@ -133,7 +131,6 @@ subprocess seam 会清洗形似密钥的变量且没有 shell 模式,而 pnpm
 
 - [App boot](../app-boot/README.zh.md)——profile runtime、外部组合包隔离与静态包声明。
 - [补丁文件](../app-boot/README.zh.md#patch-files)——用户层的行如何读写。
-- [Agent presets](../../preset/agent-presets/README.zh.md)——preset 目标所写的每预设用户层。
 - [宿主插件管理器](../../host/plugin-manager/README.zh.md)——本管理器之上的 `plugins` Remote。
 - [dsh 应用](../../../apps/cli/README.zh.md)——安装器之上的 `dsh plugin` 命令。
 
@@ -157,7 +154,6 @@ subprocess seam 会清洗形似密钥的变量且没有 shell 模式,而 pnpm
 
 - **更新已加载的包需要重启**——Node 按 URL 缓存 ESM 模块,hoisted 安装下路径不变;经 `add` 做的 `pnpm update` 改写了文件,但运行中的树在进程重启前一直用旧模块。
 - **依赖检测止于注入**——注册型依赖(工具、LLM 适配器)没有 `inject` 边,因此 `dependents` 无法点名只读取该包所注册内容的行。
-- **preset 的行不在线组合**——管理器写入 preset 的层;之后创建的会话组合它,已在运行的会话保持其代际。
 - **尚无 `engines.dsh` 检查**——该范围只被报告,不对运行中的 harness 版本强制执行。
 - **一次只有一个进程**——互斥在进程内,补丁文件写入器持文件锁,但 profile manifest 没有锁:CLI 与运行中的 Web 宿主同时编辑同一个 profile 不受支持。
 

+ 1 - 1
packages/boot/plugin-manager/src/index.ts

@@ -20,4 +20,4 @@ export * from './errors.ts'
 export type * from './types.ts'
 export type { PluginToolingConfig, SpawnLike } from './helpers.ts'
 export { PluginInstaller, type PluginInstallerOptions, type PluginInstallOutcome } from './installer.ts'
-export { PluginManager, type PluginManagerOptions, type PresetLayers } from './manager.ts'
+export { PluginManager, type PluginManagerOptions } from './manager.ts'

+ 38 - 96
packages/boot/plugin-manager/src/manager.ts

@@ -1,7 +1,7 @@
 /**
  * Plugin management over the booted profile: every operation the `plugins`
  * Remote exposes, one mutation at a time, with the tree read through the
- * Cordis context and the profile runtime, the preset roster, and the
+ * Cordis context and the profile runtime and the
  * running-agent count read through per-call readers.
  * @module @deepseek-ai/dsh-plugin-manager/manager
  */
@@ -34,43 +34,15 @@ import type {
   PluginRowAddition,
   PluginRowIssue,
   PluginRowReference,
-  PluginRowTarget,
   PluginServiceDependent,
 } from './types.ts'
 import { ownedEntries, packageView } from './view.ts'
 
-/**
- * The agent-preset roster as row operations see it: where each preset's user
- * patch layer lives and which row ids its composition already carries. The
- * roster service satisfies this structurally; a host passes it in so this
- * package never imports the roster.
- */
-export interface PresetLayers {
-  /**
-   * The user patch layer file of one preset, created on the first write.
-   * @param presetId - the preset id.
-   * @returns the absolute path.
-   */
-  overlayPathFor(presetId: string): Promise<string>
-  /**
-   * Every preset with the path of its user patch layer when it has one.
-   * @returns the presets.
-   */
-  list(): Promise<readonly { readonly id: string; readonly overlayPath?: string }[]>
-  /**
-   * Every preset's composition as rows, under the ids the composition declares; an anonymous row has none.
-   * @returns one composition per preset.
-   */
-  compositionInventory(): Promise<readonly { readonly id: string; readonly rows: readonly { readonly entryId: string | null }[] }[]>
-}
-
 /** What {@link PluginManager} needs beyond the Cordis context it reads the tree through. */
 export interface PluginManagerOptions {
   readonly config: PluginToolingConfig
   /** The booted profile, read per call; `undefined` means no profile is composed in this process. */
   readonly runtime: () => ProfileRuntime | undefined
-  /** The preset roster's layers, read per call; `undefined` when no roster is composed. */
-  readonly presets: () => PresetLayers | undefined
   /** How many agents are running; `add` and `uninstall` refuse while any is. */
   readonly runningAgents: () => number
   /** Test seam: the child spawner; defaults to `node:child_process`. */
@@ -82,7 +54,7 @@ export interface PluginManagerOptions {
 /**
  * Plugin management over the booted profile: every operation the `plugins`
  * Remote exposes, with the tree read through the Cordis context and the
- * profile runtime, the preset roster, and the running-agent count read
+ * profile runtime and the running-agent count read
  * through {@link PluginManagerOptions} per call.
  *
  * Every write touches the profile manifest, the user layers, or
@@ -96,7 +68,7 @@ export class PluginManager {
 
   /**
    * @param ctx - the context whose Loader tree and services the operations read.
-   * @param options - the profile runtime, the roster, the agent count, and the tooling bounds.
+   * @param options - the profile runtime, the agent count, and the tooling bounds.
    */
   constructor(private readonly ctx: Context, private readonly options: PluginManagerOptions) {}
 
@@ -238,10 +210,10 @@ export class PluginManager {
       await this.disableNow(packageName)
     }
     for (const reference of references) {
-      await this.editLayer(runtime, reference.target, (document) => { document.removeInsert(reference.rowId) })
+      await this.editLayer(runtime, (document) => { document.removeInsert(reference.rowId) })
     }
     await installer.remove(packageName)
-    if (references.some(reference => reference.target.kind === 'global') && runtime.patchReload === 'live') {
+    if (references.length > 0 && runtime.patchReload === 'live') {
       await runtime.recompose()
     }
     this.changed('uninstall', packageName)
@@ -357,28 +329,23 @@ export class PluginManager {
   }
 
   /**
-   * Add a row naming one of the package's modules to a user layer: the
-   * profile's global `cordis.patch.yml`, or an agent preset's.
+   * Add a row naming one of the package's modules to the profile's `cordis.patch.yml`.
    * @param packageName - the installed package.
-   * @param target - which layer.
    * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
    * `id` overrides the derived row id, `config` overrides the declared default.
    * @returns where the row landed.
    * @throws {PluginOperationError} `plugins/not-installed`, `plugins/not-enableable`
-   * when the module is absent from `dsh.plugins`, `plugins/row-conflict`,
-   * or `plugins/unavailable` for a preset target without a roster.
+   * when the module is absent from `dsh.plugins`, or `plugins/row-conflict`.
    */
   async addRow(
     packageName: string,
-    target: PluginRowTarget,
     options?: { module?: string; id?: string; config?: JsonValue },
   ): Promise<PluginRowAddition> {
-    return this.exclusive('addRow', packageName, () => this.addRowNow(packageName, target, options))
+    return this.exclusive('addRow', packageName, () => this.addRowNow(packageName, options))
   }
 
   private async addRowNow(
     packageName: string,
-    target: PluginRowTarget,
     options?: { module?: string; id?: string; config?: JsonValue },
   ): Promise<PluginRowAddition> {
     const runtime = this.runtime()
@@ -398,30 +365,29 @@ export class PluginManager {
     }
     const config = options?.config !== undefined ? options.config : addable.config !== undefined ? addable.config : {}
     const row: PatchRow = { id: rowId, name: moduleName, config }
-    if (await this.rowExists(runtime, target, rowId)) {
-      throw new PluginOperationError('plugins/row-conflict', `${NAME}: a row ${JSON.stringify(rowId)} already exists`, { rowId, target })
+    if (await this.rowExists(runtime, rowId)) {
+      throw new PluginOperationError('plugins/row-conflict', `${NAME}: a row ${JSON.stringify(rowId)} already exists`, { rowId })
     }
-    const file = await this.editLayer(runtime, target, (document) => { document.appendInsert(row) })
+    const file = await this.editLayer(runtime, (document) => { document.appendInsert(row) })
     this.changed('row', packageName)
-    return { target, rowId, file }
+    return { rowId, file }
   }
 
   /**
    * Remove a row a user layer inserted.
-   * @param target - which layer.
    * @param rowId - the inserted row's id.
    * @throws {PluginOperationError} `plugins/bad-request` when the layer inserts no such row.
    */
-  async removeRow(target: PluginRowTarget, rowId: string): Promise<void> {
-    return this.exclusive('removeRow', rowId, () => this.removeRowNow(target, rowId))
+  async removeRow(rowId: string): Promise<void> {
+    return this.exclusive('removeRow', rowId, () => this.removeRowNow(rowId))
   }
 
-  private async removeRowNow(target: PluginRowTarget, rowId: string): Promise<void> {
+  private async removeRowNow(rowId: string): Promise<void> {
     const runtime = this.runtime()
     // A holder rather than a `let`: the assignment happens inside the edit
     // callback, which control-flow narrowing does not see.
     const outcome = { removed: false }
-    await this.editLayer(runtime, target, (document) => { outcome.removed = document.removeInsert(rowId) })
+    await this.editLayer(runtime, (document) => { outcome.removed = document.removeInsert(rowId) })
     if (!outcome.removed) {
       throw new PluginOperationError('plugins/bad-request', `${NAME}: the layer inserts no row ${JSON.stringify(rowId)}`, {})
     }
@@ -432,17 +398,16 @@ export class PluginManager {
    * Switch one row off or on in a user layer. Deny-only: `true` writes
    * `disabled: true` for the row, `false` removes that key, so a bundle's
    * own `!!js` gate is restored rather than overridden.
-   * @param target - which layer.
    * @param rowId - the row's id as the composition declares it.
    * @param disabled - whether the layer should switch the row off.
    */
-  async setRowDisabled(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void> {
-    return this.exclusive('setRowDisabled', rowId, () => this.setRowDisabledNow(target, rowId, disabled))
+  async setRowDisabled(rowId: string, disabled: boolean): Promise<void> {
+    return this.exclusive('setRowDisabled', rowId, () => this.setRowDisabledNow(rowId, disabled))
   }
 
-  private async setRowDisabledNow(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void> {
+  private async setRowDisabledNow(rowId: string, disabled: boolean): Promise<void> {
     const runtime = this.runtime()
-    await this.editLayer(runtime, target, (document) => {
+    await this.editLayer(runtime, (document) => {
       if (disabled) document.setRowField(rowId, 'disabled', true)
       else document.deleteRowField(rowId, 'disabled')
     })
@@ -482,70 +447,47 @@ export class PluginManager {
   /** Every user-layer row naming the package or one of its subpaths. */
   private async rowReferences(runtime: ProfileRuntime, packageName: string): Promise<PluginRowReference[]> {
     const found: PluginRowReference[] = []
-    const names = (target: PluginRowTarget, rows: readonly PatchRow[]): void => {
+    const names = (rows: readonly PatchRow[]): void => {
       for (const row of rows) {
         // A group's config is a row list once the patch parser accepted the file.
         /* v8 ignore next */
-        if (row.group === true) names(target, (row.config ?? []) as PatchRow[])
+        if (row.group === true) names((row.config ?? []) as PatchRow[])
         if (typeof row.id !== 'string') continue
         if (row.name === packageName || row.name.startsWith(`${packageName}/`)) {
-          found.push({ target, rowId: row.id, moduleName: row.name })
+          found.push({ rowId: row.id, moduleName: row.name })
         }
       }
     }
-    const scan = async (target: PluginRowTarget, file: string): Promise<void> => {
-      let patches
-      try {
-        patches = await readPatchListFile(NAME, file, 'patches')
-      } catch {
-        // An unreadable layer names nothing this call can act on; the
-        // launcher and the roster report it as their own failure.
-        return
-      }
-      for (const patch of patches ?? []) if (patch.insert !== undefined) names(target, patch.insert)
-    }
-    await scan({ kind: 'global' }, runtime.patchPath)
-    const presets = this.options.presets()
-    if (presets !== undefined) {
-      for (const preset of await presets.list()) {
-        if (preset.overlayPath !== undefined) await scan({ kind: 'preset', preset: preset.id }, preset.overlayPath)
-      }
+    let patches
+    try {
+      patches = await readPatchListFile(NAME, runtime.patchPath, 'patches')
+    } catch {
+      // An unreadable layer names nothing this call can act on; the
+      // launcher reports it as a composition failure.
+      return found
     }
+    for (const patch of patches ?? []) if (patch.insert !== undefined) names(patch.insert)
     return found
   }
 
-  /** Whether the target layer's composition already carries a row with `rowId`. */
-  private async rowExists(runtime: ProfileRuntime, target: PluginRowTarget, rowId: string): Promise<boolean> {
-    if (target.kind === 'global') {
-      const tree = rootIncludeEntry(this.ctx.root)?.subtree
-      return [...this.ctx.loader.entries()].some(entry => entry.parent.tree === tree && entry.options.id === rowId)
-        || (await readPatchListFile(NAME, runtime.patchPath, 'patches') ?? []).some(patch => patch.insert?.some(row => row.id === rowId))
-    }
-    const composition = (await this.presets().compositionInventory()).find(candidate => candidate.id === target.preset)
-    return composition?.rows.some(row => row.entryId === rowId) ?? false
-  }
-
-  /** The roster's layers, or the failure a preset target without a roster receives. */
-  private presets(): PresetLayers {
-    const presets = this.options.presets()
-    if (presets === undefined) {
-      throw new PluginOperationError('plugins/unavailable', `${NAME}: no agent-preset roster is composed`, { reason: 'no roster' })
-    }
-    return presets
+  /** Whether the profile's composition already carries a row with `rowId`. */
+  private async rowExists(runtime: ProfileRuntime, rowId: string): Promise<boolean> {
+    const tree = rootIncludeEntry(this.ctx.root)?.subtree
+    return [...this.ctx.loader.entries()].some(entry => entry.parent.tree === tree && entry.options.id === rowId)
+      || (await readPatchListFile(NAME, runtime.patchPath, 'patches') ?? []).some(patch => patch.insert?.some(row => row.id === rowId))
   }
 
   /** Edit one user layer file and, for the live global layer, recompose. */
   private async editLayer(
     runtime: ProfileRuntime,
-    target: PluginRowTarget,
     mutate: Parameters<typeof mutatePatchFile>[1],
   ): Promise<string> {
-    const file = target.kind === 'global' ? runtime.patchPath : await this.presets().overlayPathFor(target.preset)
+    const file = runtime.patchPath
     await mutatePatchFile(file, mutate, { binName: NAME, mode: 0o600, dirMode: 0o700 })
     // The profile launcher's watcher reapplies the global file on its own;
     // recomposing here makes the change visible to this call's caller before
     // it returns, and the watcher's later pass composes the same text.
-    if (target.kind === 'global' && runtime.patchReload === 'live') await runtime.recompose()
+    if (runtime.patchReload === 'live') await runtime.recompose()
     return file
   }
 

+ 3 - 10
packages/boot/plugin-manager/src/types.ts

@@ -51,7 +51,7 @@ export interface PluginPackageRowView {
   readonly failure?: { readonly stage: string; readonly message: string }
 }
 
-/** One agent-plane module a package declares addable to a composition. */
+/** One module a package declares addable to the profile. */
 export interface PluginPackageAddableView {
   /** The module as it is named in a row: the bare package for `.`, else `<package>/<subpath>`. */
   readonly moduleName: string
@@ -92,17 +92,12 @@ export interface PluginPackageView {
   readonly issues?: readonly PluginRowIssue[]
   /** Ids of built-in rows the bundle's patch overrides. */
   readonly overrides: readonly string[]
-  /** Agent-plane modules the package declares addable. */
+  /** Modules the package declares addable. */
   readonly addable: readonly PluginPackageAddableView[]
   /** Whether the profile applies user patch files while running; false means changes wait for a restart. */
   readonly liveReload: boolean
 }
 
-/** Where a composition row is added or edited: the profile's global user layer, or one preset's. */
-export type PluginRowTarget =
-  | { readonly kind: 'global' }
-  | { readonly kind: 'preset'; readonly preset: string }
-
 /** A package pnpm installed that the run removed again, with the check it failed. */
 export interface PluginInstallRejection {
   /** The package name. */
@@ -157,7 +152,6 @@ export interface PluginServiceDependent {
 
 /** One user-layer row that names a module of the package. */
 export interface PluginRowReference {
-  readonly target: PluginRowTarget
   readonly rowId: string
   readonly moduleName: string
 }
@@ -170,7 +164,6 @@ export interface PluginDependents {
 
 /** Where a row was added. */
 export interface PluginRowAddition {
-  readonly target: PluginRowTarget
   /** The id the row was given. */
   readonly rowId: string
   /** The user layer file the row was written to. */
@@ -214,7 +207,7 @@ export interface PluginOperationDetailsMap {
   /** pnpm exited non-zero, could not be spawned, or timed out. */
   'plugins/install-failed': { readonly spec: string; readonly exitCode: number | null; readonly log: string }
   /** The row id is already taken in the target user layer. */
-  'plugins/row-conflict': { readonly rowId: string; readonly target: PluginRowTarget }
+  'plugins/row-conflict': { readonly rowId: string }
   /** Another mutation is still running; the manager runs one at a time and refuses rather than queues. */
   'plugins/busy': {
     readonly operation: string

+ 23 - 57
packages/boot/plugin-manager/tests/plugin-manager.spec.ts

@@ -4,7 +4,7 @@
  * through `boot()` with the profile runtime the launcher provides, and a
  * fake pnpm that edits the profile the way the real one does. The manager
  * is built the way the Web host's adapter builds it, reading the runtime,
- * the roster, and the agent registry off the context per call.
+ * the agent registry off the context per call.
  */
 
 import { EventEmitter } from 'node:events'
@@ -25,7 +25,6 @@ import {
   PluginManager, type PluginInstallLogChunk, type PluginToolingConfig, type SpawnLike,
 } from '@deepseek-ai/dsh-plugin-manager'
 import type {} from '@deepseek-ai/dsh-agent'
-import type {} from '@deepseek-ai/dsh-agent-presets'
 
 const NAME = 'dsh-test'
 
@@ -232,12 +231,11 @@ async function bootProfile(staged: StagedHome, internals: Internals = {}, config
   return { ctx, manager, runtime: ctx.profileRuntime, changes, log }
 }
 
-/** The manager as the Web host's adapter builds it: runtime, roster, and agent count read off the context per call. */
+/** The manager as the Web host's adapter builds it: runtime and agent count read off the context per call. */
 function managerOver(ctx: Context, internals: Internals = {}, config: Partial<PluginToolingConfig> = {}): PluginManager {
   return new PluginManager(ctx, {
     config: managerConfig(config),
     runtime: () => ctx.get('profileRuntime'),
-    presets: () => ctx.get('agentPresets'),
     runningAgents: () => (ctx.get('agents')?.list() ?? []).filter(agent => agent.status === 'running').length,
     ...internals,
   })
@@ -347,12 +345,12 @@ describe('PluginManager', () => {
     expect(views.find(view => view.name === 'declared')?.addable[0]).toMatchObject({ moduleName: 'declared', config: { custom: 1 } })
     expect(existsSync(marker)).toBe(false)
     for (const id of ['', 'nested:child']) {
-      await expect(manager.addRow('declared', { kind: 'global' }, { id })).rejects.toMatchObject({ code: 'plugins/bad-request' })
+      await expect(manager.addRow('declared', { id })).rejects.toMatchObject({ code: 'plugins/bad-request' })
     }
-    const added = await manager.addRow('declared', { kind: 'global' }, { config: null })
+    const added = await manager.addRow('declared', { config: null })
     expect(readFileSync(added.file, 'utf8')).toContain('config: null')
     expect(views.find(view => view.name === 'unknown')?.kind).toBe('unknown')
-    await expect(manager.addRow('unknown', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/not-enableable' })
+    await expect(manager.addRow('unknown')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
   })
 
   it('reports plugins/unavailable without a profile runtime', async () => {
@@ -522,7 +520,7 @@ describe('PluginManager', () => {
       const { manager } = await bootProfile(staged)
       await manager.enable('ext-off')
       await manager.enable('ext-waiting')
-      await manager.setRowDisabled({ kind: 'global' }, 'b', true)
+      await manager.setRowDisabled('b', true)
 
       const views = await manager.list()
 
@@ -532,7 +530,7 @@ describe('PluginManager', () => {
         ['a', false, 'composition', null],
         ['b', false, 'user', null],
       ])
-      await manager.setRowDisabled({ kind: 'global' }, 'b', true)
+      await manager.setRowDisabled('b', true)
       const waiting = views.find(view => view.name === 'ext-waiting')
       expect(waiting).toMatchObject({ status: 'failed', reason: expect.stringContaining('fixtureSvc') as string })
       expect(waiting?.rows).toEqual([expect.objectContaining({ entryId: 'include:w', phase: 'pending', failure: expect.objectContaining({ stage: 'inject-pending' }) as object })])
@@ -885,24 +883,24 @@ describe('PluginManager', () => {
       addDependency(staged.profileDir, '@acme/ext-plugin')
       const { ctx, manager, changes } = await bootProfile(staged)
 
-      const added = await manager.addRow('@acme/ext-plugin', { kind: 'global' })
+      const added = await manager.addRow('@acme/ext-plugin')
 
-      expect(added).toEqual({ target: { kind: 'global' }, rowId: 'acme/ext-plugin', file: join(staged.profileDir, 'cordis.patch.yml') })
+      expect(added).toEqual({ rowId: 'acme/ext-plugin', file: join(staged.profileDir, 'cordis.patch.yml') })
       expect(readFileSync(added.file, 'utf8')).toBe('- insert:\n    - id: acme/ext-plugin\n      name: "@acme/ext-plugin"\n      config: {}\n')
       // Composed live: the row is in the tree, though its module cannot import from the temp home.
       expect(entryIds(ctx)).toContain('include:acme/ext-plugin')
-      await expect(manager.addRow('@acme/ext-plugin', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/row-conflict' })
+      await expect(manager.addRow('@acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/row-conflict' })
 
-      await manager.setRowDisabled({ kind: 'global' }, 'acme/ext-plugin', true)
+      await manager.setRowDisabled('acme/ext-plugin', true)
       expect(ctx.loader.resolve('include:acme/ext-plugin')?.disabled).toBe(true)
       expect(readFileSync(added.file, 'utf8')).toContain('- id: acme/ext-plugin\n  disabled: true\n')
-      await manager.setRowDisabled({ kind: 'global' }, 'acme/ext-plugin', false)
+      await manager.setRowDisabled('acme/ext-plugin', false)
       expect(ctx.loader.resolve('include:acme/ext-plugin')?.disabled).toBe(false)
       expect(readFileSync(added.file, 'utf8')).not.toContain('disabled')
 
-      await manager.removeRow({ kind: 'global' }, 'acme/ext-plugin')
+      await manager.removeRow('acme/ext-plugin')
       expect(entryIds(ctx)).not.toContain('include:acme/ext-plugin')
-      await expect(manager.removeRow({ kind: 'global' }, 'acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/bad-request' })
+      await expect(manager.removeRow('acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/bad-request' })
       expect(changes.map(change => change.reason)).toEqual(['row', 'row', 'row', 'row'])
     })
 
@@ -912,10 +910,10 @@ describe('PluginManager', () => {
       addDependency(staged.profileDir, 'ext-plugin')
       const { ctx, manager } = await bootProfile(staged)
 
-      await manager.addRow('ext-plugin', { kind: 'global' })
+      await manager.addRow('ext-plugin')
 
       expect(entryIds(ctx)).not.toContain('include:ext-plugin')
-      await expect(manager.addRow('ext-plugin', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/row-conflict' })
+      await expect(manager.addRow('ext-plugin')).rejects.toMatchObject({ code: 'plugins/row-conflict' })
     })
 
     it('adds a declared addable module with its default config and an explicit id, and refuses the rest', async () => {
@@ -931,47 +929,15 @@ describe('PluginManager', () => {
       addDependency(staged.profileDir, 'ext-bundle')
       const { manager } = await bootProfile(staged)
 
-      const added = await manager.addRow('ext-bundle', { kind: 'global' }, { module: './tools/sql.js', id: 'sql' })
+      const added = await manager.addRow('ext-bundle', { module: './tools/sql.js', id: 'sql' })
       expect(added.rowId).toBe('sql')
       expect(readFileSync(added.file, 'utf8')).toContain('- id: sql\n      name: ext-bundle/tools/sql.js\n      config:\n        dsn: sqlite://\n')
-      await expect(manager.addRow('ext-bundle', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/not-enableable' })
-      await expect(manager.addRow('ext-bundle', { kind: 'global' }, { module: './missing.js' })).resolves.toMatchObject({ rowId: 'ext-bundle/missing.js' })
-      await expect(manager.addRow('absent', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/not-installed' })
+      await expect(manager.addRow('ext-bundle')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
+      await expect(manager.addRow('ext-bundle', { module: './missing.js' })).resolves.toMatchObject({ rowId: 'ext-bundle/missing.js' })
+      await expect(manager.addRow('absent')).rejects.toMatchObject({ code: 'plugins/not-installed' })
     })
 
-    it('writes a preset\'s layer through the roster, and refuses a preset target without one', async () => {
-      const staged = await stageHome()
-      stagePackage(staged.profileDir, 'ext-plugin', { plugins: [{ name: '.' }], main: 'export const name = "p"\nexport function apply() {}\n' })
-      addDependency(staged.profileDir, 'ext-plugin')
-      const { ctx, manager } = await bootProfile(staged)
-      await expect(manager.addRow('ext-plugin', { kind: 'preset', preset: 'standard' })).rejects.toMatchObject({ code: 'plugins/unavailable' })
 
-      const overlay = join(staged.home, '.agent-presets', 'standard', 'cordis.patch.yml')
-      const roster = {
-        overlayPathFor: (id: string) => Promise.resolve(join(staged.home, '.agent-presets', id, 'cordis.patch.yml')),
-        compositionInventory: () => Promise.resolve([{ id: 'standard', rows: [{ entryId: 'tool-web' }] }]),
-        list: () => Promise.resolve([{ id: 'standard', overlayPath: existsSync(overlay) ? overlay : undefined }]),
-      }
-      ctx.provide('agentPresets', roster)
-      // No layer exists yet: nothing references the package.
-      expect((await manager.dependents('ext-plugin')).references).toEqual([])
-      // A preset the inventory does not know has no rows to conflict with.
-      const elsewhere = await manager.addRow('ext-plugin', { kind: 'preset', preset: 'other' })
-      expect(elsewhere.file).toBe(join(staged.home, '.agent-presets', 'other', 'cordis.patch.yml'))
-
-      const added = await manager.addRow('ext-plugin', { kind: 'preset', preset: 'standard' })
-      expect(added).toEqual({ target: { kind: 'preset', preset: 'standard' }, rowId: 'ext-plugin', file: overlay })
-      await expect(manager.addRow('ext-plugin', { kind: 'preset', preset: 'standard' }, { id: 'tool-web' })).rejects.toMatchObject({ code: 'plugins/row-conflict' })
-      await manager.setRowDisabled({ kind: 'preset', preset: 'standard' }, 'tool-web', true)
-      expect(readFileSync(overlay, 'utf8')).toBe('- insert:\n    - id: ext-plugin\n      name: ext-plugin\n      config: {}\n- id: tool-web\n  disabled: true\n')
-      // The preset's layer is not part of the host tree.
-      expect(entryIds(ctx)).not.toContain('include:ext-plugin')
-
-      const dependents = await manager.dependents('ext-plugin')
-      expect(dependents.references).toEqual([{ target: { kind: 'preset', preset: 'standard' }, rowId: 'ext-plugin', moduleName: 'ext-plugin' }])
-      await manager.removeRow({ kind: 'preset', preset: 'standard' }, 'ext-plugin')
-      expect(readFileSync(overlay, 'utf8')).toBe('- id: tool-web\n  disabled: true\n')
-    })
   })
 
   describe('dependents and uninstall', () => {
@@ -1000,14 +966,14 @@ describe('PluginManager', () => {
       await manager.enable('ext-provider')
       // A built-in row injecting the bundle's service, composed once the
       // provider is up (a boot would refuse a row left waiting).
-      writeFileSync(join(staged.profileDir, 'cordis.patch.yml'), `${readFileSync(join(staged.profileDir, 'cordis.patch.yml'), 'utf8')}- insert:\n    - id: needs-svc\n      name: cordis:consumer\n`)
+      writeFileSync(join(staged.profileDir, 'cordis.patch.yml'), `${readFileSync(join(staged.profileDir, 'cordis.patch.yml'), 'utf8')}- id: svc\n  disabled: false\n- insert:\n    - id: needs-svc\n      name: cordis:consumer\n`)
       await runtime.recompose()
 
       const dependents = await manager.dependents('ext-provider')
 
       expect(dependents.services).toEqual([{ service: 'fixtureSvc', providedBy: 'include:svc', injectedBy: ['include:needs-svc'] }])
       expect(dependents.references.map(reference => reference.rowId)).toEqual(['ref', 'nested-ref'])
-      expect(dependents.references[0]).toEqual({ target: { kind: 'global' }, rowId: 'ref', moduleName: 'ext-provider/tools/x.js' })
+      expect(dependents.references[0]).toEqual({ rowId: 'ref', moduleName: 'ext-provider/tools/x.js' })
     })
 
     it('ignores an unreadable user layer while collecting references', async () => {
@@ -1027,7 +993,7 @@ describe('PluginManager', () => {
       const calls: string[][] = []
       const { ctx, manager, changes } = await bootProfile(staged, { spawn: recordingPnpm(staged.profileDir, calls) })
       await manager.enable('ext-bundle')
-      await manager.addRow('ext-bundle', { kind: 'global' }, { module: './extra.js' })
+      await manager.addRow('ext-bundle', { module: './extra.js' })
       expect(entryIds(ctx)).toEqual(expect.arrayContaining(['include:hello', 'include:ext-bundle/extra.js']))
 
       await manager.uninstall('ext-bundle')

+ 3 - 3
packages/client/ui-settings-plugin-inventory/tests/components.client.spec.tsx

@@ -144,8 +144,8 @@ describe('PluginInventorySettingsTab', () => {
         trust: 'user',
         isDefault: true,
         rows: [
-          { entryId: 'tool-subagent-primary', moduleName: '@deepseek-ai/dsh-tool-subagent', enabled: true, fiberPhase: null, source: 'preset' },
-          { entryId: longId, moduleName: '@deepseek-ai/dsh-tool-subagent', enabled: false, fiberPhase: null, source: 'preset' },
+          { entryId: 'tool-subagent-primary', moduleName: '@deepseek-ai/dsh-tool-subagent', enabled: true, fiberPhase: null },
+          { entryId: longId, moduleName: '@deepseek-ai/dsh-tool-subagent', enabled: false, fiberPhase: null },
         ],
       }],
     })
@@ -338,7 +338,7 @@ describe('PluginInventorySettingsTab', () => {
         id: 'solo',
         trust: 'user',
         isDefault: false,
-        rows: [{ entryId: 'one', moduleName: '@fixture/one', enabled: true, fiberPhase: null, source: 'preset' }],
+        rows: [{ entryId: 'one', moduleName: '@fixture/one', enabled: true, fiberPhase: null }],
       }],
     })
 

+ 12 - 38
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -187,20 +187,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         returns: 'the composition exactly as stored.',
         throws: ['when no configured root supplies that id.'],
       },
-      {
-        signature: 'async overlayPathFor(id: string): Promise<string>',
-        description: 'Where one preset\'s user patch layer is, or would be written: the layer discovery attached, else the writable root\'s slot of the same id — beside the composition for a locally authored preset, alone in the slot for a shipped one. The file need not exist yet.',
-        parameters: [{ name: 'id', description: 'the preset id.' }],
-        returns: 'the absolute path of the layer file.',
-        throws: ['when the preset is unknown, or it has no layer and the deployment configures no writable root.'],
-      },
-      {
-        signature: 'async removeOverlay(id: string): Promise<boolean>',
-        description: 'Delete one preset\'s user patch layer, so the next generation composes the preset exactly as its root supplies it. Sessions already joined keep the generation they run on.',
-        parameters: [{ name: 'id', description: 'the preset id.' }],
-        returns: 'true when a layer was removed; false when the preset had none.',
-        throws: ['when the preset is unknown or its layer lies outside the writable root.'],
-      },
       {
         signature: '@Remote(\'read\') async readDocument(agentPreset: string): Promise<AgentPresetDocument>',
         description: 'One preset\'s composition text with the roster row it belongs to.',
@@ -1400,7 +1386,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
   {
     key: 'pluginManager',
     summary: 'The `pluginManager` service and the `plugins` Remote.',
-    description: 'The `pluginManager` service and the `plugins` Remote.\n\nThe row injects only the Loader; the profile runtime, the preset roster, and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.',
+    description: 'The `pluginManager` service and the `plugins` Remote.\n\nThe row injects only the Loader; the profile runtime and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.',
     methods: [
       {
         signature: '@Remote(\'list\') async list(): Promise<PluginPackageView[]>',
@@ -1438,20 +1424,20 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         returns: 'the enable outcome of the second step.',
       },
       {
-        signature: '@Remote(\'addRow\') async addRow( packageName: string, target: PluginRowTarget, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>',
-        description: 'Add a row naming one of the package\'s modules to a user layer.',
-        parameters: [{ name: 'packageName', description: 'the installed package.' }, { name: 'target', description: 'which layer.' }, { name: 'options', description: '`module` selects a declared `dsh.plugins[]` name (default `.`), `id` overrides the derived row id, `config` overrides the declared default.' }],
+        signature: '@Remote(\'addRow\') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>',
+        description: 'Add a row naming one of the package\'s modules to the profile user layer.',
+        parameters: [{ name: 'packageName', description: 'the installed package.' }, { name: 'options', description: '`module` selects a declared `dsh.plugins[]` name (default `.`), `id` overrides the derived row id, `config` overrides the declared default.' }],
         returns: 'where the row landed.',
       },
       {
-        signature: '@Remote(\'removeRow\') async removeRow(target: PluginRowTarget, rowId: string): Promise<void>',
+        signature: '@Remote(\'removeRow\') async removeRow(rowId: string): Promise<void>',
         description: 'Remove a row a user layer inserted.',
-        parameters: [{ name: 'target', description: 'which layer.' }, { name: 'rowId', description: 'the inserted row\'s id.' }],
+        parameters: [{ name: 'rowId', description: 'the inserted row\'s id.' }],
       },
       {
-        signature: '@Remote(\'setRowDisabled\') async setRowDisabled(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void>',
+        signature: '@Remote(\'setRowDisabled\') async setRowDisabled(rowId: string, disabled: boolean): Promise<void>',
         description: 'Switch one row off or on in a user layer; deny-only.',
-        parameters: [{ name: 'target', description: 'which layer.' }, { name: 'rowId', description: 'the row\'s id as the composition declares it.' }, { name: 'disabled', description: 'whether the layer should switch the row off.' }],
+        parameters: [{ name: 'rowId', description: 'the row\'s id as the composition declares it.' }, { name: 'disabled', description: 'whether the layer should switch the row off.' }],
       },
       {
         signature: '@Remote(\'dependents\') async dependents(packageName: string): Promise<PluginDependents>',
@@ -3799,7 +3785,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'AgentPreset',
-    declaration: 'export interface AgentPreset {\n    readonly id: string;\n    readonly trust: PresetTrust;\n    readonly path: string;\n    readonly overlayPath?: string;\n    readonly name?: string;\n    readonly description?: string;\n    readonly order?: number;\n    readonly broken?: string;\n}',
+    declaration: 'export interface AgentPreset {\n    readonly id: string;\n    readonly trust: PresetTrust;\n    readonly path: string;\n    readonly name?: string;\n    readonly description?: string;\n    readonly order?: number;\n    readonly broken?: string;\n}',
   },
   {
     name: 'AgentPresetComposition',
@@ -3807,7 +3793,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'AgentPresetCompositionRow',
-    declaration: 'export interface AgentPresetCompositionRow {\n    readonly entryId: string | null;\n    readonly moduleName: string;\n    readonly enabled: CompositionRowEnablement;\n    readonly condition?: string;\n    readonly fiberState?: FiberState;\n    readonly source: CompositionRowSource;\n    readonly disabledBy?: CompositionRowDisabledBy;\n}',
+    declaration: 'export interface AgentPresetCompositionRow {\n    readonly entryId: string | null;\n    readonly moduleName: string;\n    readonly enabled: CompositionRowEnablement;\n    readonly condition?: string;\n    readonly fiberState?: FiberState;\n}',
   },
   {
     name: 'AgentPresetDirectoryOpenValue',
@@ -4077,18 +4063,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'CompactionTrigger',
     declaration: 'export type CompactionTrigger = \'pressure\' | \'context-overflow\';',
   },
-  {
-    name: 'CompositionRowDisabledBy',
-    declaration: 'export type CompositionRowDisabledBy = \'composition\' | \'user\';',
-  },
   {
     name: 'CompositionRowEnablement',
     declaration: 'export type CompositionRowEnablement = boolean | \'conditional\';',
   },
-  {
-    name: 'CompositionRowSource',
-    declaration: 'export type CompositionRowSource = \'preset\' | \'user\';',
-  },
   {
     name: 'ComputerUseProviderName',
     declaration: 'export type ComputerUseProviderName = Branded<\'ComputerUseProviderName\'>;',
@@ -4995,7 +4973,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'PluginRowAddition',
-    declaration: 'export interface PluginRowAddition {\n    readonly target: PluginRowTarget;\n    readonly rowId: string;\n    readonly file: string;\n}',
+    declaration: 'export interface PluginRowAddition {\n    readonly rowId: string;\n    readonly file: string;\n}',
   },
   {
     name: 'PluginRowIssue',
@@ -5007,11 +4985,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'PluginRowReference',
-    declaration: 'export interface PluginRowReference {\n    readonly target: PluginRowTarget;\n    readonly rowId: string;\n    readonly moduleName: string;\n}',
-  },
-  {
-    name: 'PluginRowTarget',
-    declaration: 'export type PluginRowTarget = {\n    readonly kind: \'global\';\n} | {\n    readonly kind: \'preset\';\n    readonly preset: string;\n};',
+    declaration: 'export interface PluginRowReference {\n    readonly rowId: string;\n    readonly moduleName: string;\n}',
   },
   {
     name: 'PluginServiceDependent',

+ 2 - 2
packages/host/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/host/plugin-inventory/README.md
-README.md: f789dae7f7a61130c27d185ceae768aba810e741
-README.zh.md: 3bbd871c262e334871738769a1e67fc2c0b01c3b
+README.md: e8272b020cfcfd48343e1c2df2a7c44760b845ce
+README.zh.md: 052b26ac2cc51f38921c5296ed52333086d355ad

+ 1 - 1
packages/host/plugin-inventory/README.md

@@ -35,7 +35,7 @@ Each row is one non-group Loader entry: its entry id, the exact module specifier
 
 ### Per-preset compositions
 
-With a roster composed, `agentPresets` carries one group per preset in roster order: its id, whether the deployment ships it or the user owns it (`trust`, which clients use to localize shipped names), published display name, whether a session naming no preset composes it, and flattened plugin rows — entry id (null when the file row declares none), module specifier, effective enablement, the row's own `!!js` disabled expression when it carries one, a root-fiber phase when the composition is live, whether the preset's composition file or its user patch layer supplied the row (`source`), and for a row that is off whether the composition or the layer switched it off (`disabledBy`). A preset some session already composed answers from its newest standing generation — even when its file has since broken, because the mount is what those sessions run; one never composed since boot answers from its composition file with disabled gates evaluated against the Loader context, and reading never mounts a preset. `conditional` enablement marks a gate the Host could not evaluate, and a broken preset nothing composed stays listed with its reason and no rows. Without a roster the field is absent.
+With a roster composed, `agentPresets` carries one group per preset in roster order: its id, whether the deployment ships it or the user owns it (`trust`, which clients use to localize shipped names), published display name, whether a session naming no preset composes it, and flattened plugin rows — entry id (null when the file row declares none), module specifier, effective enablement, the row's own `!!js` disabled expression when it carries one, and a root-fiber phase when the composition is live. A preset some session already composed answers from its newest standing generation — even when its file has since broken, because the mount is what those sessions run; one never composed since boot answers from its composition file with disabled gates evaluated against the Loader context, and reading never mounts a preset. `conditional` enablement marks a gate the Host could not evaluate, and a broken preset nothing composed stays listed with its reason and no rows. Without a roster the field is absent.
 
 ### What you can and cannot do with it
 

+ 1 - 1
packages/host/plugin-inventory/README.zh.md

@@ -35,7 +35,7 @@ kind: "package-reference"
 
 ### 每个预设的组合
 
-组合了 roster 时,`agentPresets` 按 roster 顺序携带每个预设一组:其 id、随部署内置还是用户自建(`trust`,客户端据此本地化内置预设名)、发布的显示名、未指名预设的会话是否组合它,以及压平后的插件行——条目 id(文件行未声明时为 null)、模块标识、有效启用状态、行自带的 `!!js` disabled 表达式(如有)、组合存活时的根 Fiber 阶段、该行来自预设的组合文件还是其用户补丁层(`source`),以及被关掉的行是组合还是补丁层关掉的(`disabledBy`)。已有会话组合过的预设由其最新仍存续的世代作答——即使其文件事后损坏也是如此,因为挂载才是这些会话实际运行的组合;开机以来从未被组合的预设由其组合文件作答,disabled 门用 Loader 上下文求值,且读取从不挂载预设。`conditional` 表示宿主无法求值的门;无人组合的坏预设保留在列表中,携带原因且没有行。没有 roster 时该字段缺席。
+组合了 roster 时,`agentPresets` 按 roster 顺序携带每个预设一组:其 id、随部署内置还是用户自建(`trust`,客户端据此本地化内置预设名)、发布的显示名、未指名预设的会话是否组合它,以及压平后的插件行——条目 id(文件行未声明时为 null)、模块标识、有效启用状态、行自带的 `!!js` disabled 表达式(如有),以及组合存活时的根 Fiber 阶段。已有会话组合过的预设由其最新仍存续的世代作答——即使其文件事后损坏也是如此,因为挂载才是这些会话实际运行的组合;开机以来从未被组合的预设由其组合文件作答,disabled 门用 Loader 上下文求值,且读取从不挂载预设。`conditional` 表示宿主无法求值的门;无人组合的坏预设保留在列表中,携带原因且没有行。没有 roster 时该字段缺席。
 
 ### 你能用它做什么、不能做什么
 

+ 0 - 4
packages/host/plugin-inventory/src/types.ts

@@ -66,10 +66,6 @@ export interface AgentPresetPluginRow {
   readonly condition?: string
   /** Root-fiber phase when the composition is live; null otherwise. */
   readonly fiberPhase: PluginFiberPhase
-  /** Whether the preset's composition file or its user patch layer supplied the row. */
-  readonly source: 'preset' | 'user'
-  /** Present exactly when `enabled` is false: the composition's own gate, or the user patch layer. */
-  readonly disabledBy?: PluginDisabledBy
 }
 
 /** One agent preset's identity and flattened composition in the inventory. */

+ 2 - 2
packages/host/plugin-manager/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/host/plugin-manager/README.md
-README.md: c991e1675effe9ca0633b3637f839ee8835a5aa7
-README.zh.md: e2be2a6cd613a09ea4260a40d5f0765eb6a8520b
+README.md: 1eea9fe8e3a0d785a52e4109716ea8d4bb698c34
+README.zh.md: afbdc28c8021c4e5260f7d1ab8f510d02c6299d2

+ 3 - 3
packages/host/plugin-manager/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-host-plugin-manager` mounts `pluginManager` as a Typert service and exposes the `plugins` Remote — `list`, `add`, `uninstall`, `enable`, `disable`, `retry`, `addRow`, `removeRow`, `setRowDisabled`, `dependents` — each relaying to the [`dsh-plugin-manager`](../../boot/plugin-manager/README.md) `PluginManager` built over this context, with every `plugins/*` failure crossing the wire as a `RemoteError` of the same code. What the operations do, and the `plugins/changed` and `plugins/install-log` events that follow them, belong to that package; this one reads the profile runtime, the preset roster, and the agent registry off the context and hands them over. Clients consume the Remote through the [`api-remotes`](../../api/remotes/README.md) assembly.
+`dsh-host-plugin-manager` mounts `pluginManager` as a Typert service and exposes the `plugins` Remote — `list`, `add`, `uninstall`, `enable`, `disable`, `retry`, `addRow`, `removeRow`, `setRowDisabled`, `dependents` — each relaying to the [`dsh-plugin-manager`](../../boot/plugin-manager/README.md) `PluginManager` built over this context, with every `plugins/*` failure crossing the wire as a `RemoteError` of the same code. What the operations do, and the `plugins/changed` and `plugins/install-log` events that follow them, belong to that package; this one reads the profile runtime and the agent registry off the context and hands them over. Clients consume the Remote through the [`api-remotes`](../../api/remotes/README.md) assembly.
 
 ## Table of Contents
 
@@ -53,7 +53,7 @@ A manager failure reaches the client as a `RemoteError` with the same `code` and
 
 ### A relay, not a second manager
 
-The adapter supplies per-call profile, preset and agent readers to one `PluginManager`, relays each Remote method, and maps domain errors. It coalesces native entry/status events into one reader, waits for Loader and queued profile recomposition to settle, and publishes `plugins/changed` only when the row diagnostics change. Management operations retain their own change notifications. Disposal cancels pending notifications and removes the listeners. Tests may replace the manager, pnpm spawner or static metadata reader.
+The adapter supplies per-call profile and agent readers to one `PluginManager`, relays each Remote method, and maps domain errors. It coalesces native entry/status events into one reader, waits for Loader and queued profile recomposition to settle, and publishes `plugins/changed` only when the row diagnostics change. Management operations retain their own change notifications. Disposal cancels pending notifications and removes the listeners. Tests may replace the manager, pnpm spawner or static metadata reader.
 
 ### Source map
 
@@ -96,7 +96,7 @@ None; this package neither assembles nor sends a provider request.
 
 These limits define what the Remote will not do for a client. They are current package constraints, not a task backlog.
 
-- **What the manager cannot do, the Remote cannot either** — the [manager's limits](../../boot/plugin-manager/README.md#known-limitations-and-deferred-work) apply unchanged: a loaded package updates at the next restart, dependents stop at injection, a preset row composes for later sessions, `engines.dsh` is reported and not enforced.
+- **What the manager cannot do, the Remote cannot either** — the [manager's limits](../../boot/plugin-manager/README.md#known-limitations-and-deferred-work) apply unchanged: a loaded package updates at the next restart, dependents stop at injection, `engines.dsh` is reported and not enforced.
 
 <a id="dev-note"></a>
 ### Dev Note

+ 3 - 3
packages/host/plugin-manager/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-host-plugin-manager` 把 `pluginManager` 挂载为 Typert 服务并暴露 `plugins` Remote——`list`、`add`、`uninstall`、`enable`、`disable`、`retry`、`addRow`、`removeRow`、`setRowDisabled`、`dependents`——每个方法都转给建立在本上下文之上的 [`dsh-plugin-manager`](../../boot/plugin-manager/README.zh.md) `PluginManager`,每个 `plugins/*` 失败以同码的 `RemoteError` 过线。操作做什么、以及随之而来的 `plugins/changed` 与 `plugins/install-log` 事件属于那个包;本包只是从上下文读取 profile runtime、预设名册与 agent 注册表并交给它。客户端经 [`api-remotes`](../../api/remotes/README.zh.md) 装配消费该 Remote。
+`dsh-host-plugin-manager` 把 `pluginManager` 挂载为 Typert 服务并暴露 `plugins` Remote——`list`、`add`、`uninstall`、`enable`、`disable`、`retry`、`addRow`、`removeRow`、`setRowDisabled`、`dependents`——每个方法都转给建立在本上下文之上的 [`dsh-plugin-manager`](../../boot/plugin-manager/README.zh.md) `PluginManager`,每个 `plugins/*` 失败以同码的 `RemoteError` 过线。操作做什么、以及随之而来的 `plugins/changed` 与 `plugins/install-log` 事件属于那个包;本包只是从上下文读取 profile runtime 与 agent 注册表并交给它。客户端经 [`api-remotes`](../../api/remotes/README.zh.md) 装配消费该 Remote。
 
 ## 目录
 
@@ -53,7 +53,7 @@ kind: "package-reference"
 
 ### 转接,不是第二个管理器
 
-适配器为一个 `PluginManager` 提供逐次调用的 profile、预设与 agent 读取器,转接各 Remote 方法并映射领域错误。它将原生条目与状态事件合并到一次读取中,等待 Loader 和已排队的 profile 重组完成后,仅在行诊断变化时发布 `plugins/changed` 通知。管理操作仍保留各自的变更通知。销毁会取消待发通知并移除监听器。测试可替换管理器、pnpm 启动器或静态元信息读取器。
+适配器为一个 `PluginManager` 提供逐次调用的 profile 与 agent 读取器,转接各 Remote 方法并映射领域错误。它将原生条目与状态事件合并到一次读取中,等待 Loader 和已排队的 profile 重组完成后,仅在行诊断变化时发布 `plugins/changed` 通知。管理操作仍保留各自的变更通知。销毁会取消待发通知并移除监听器。测试可替换管理器、pnpm 启动器或静态元信息读取器。
 
 ### 源码地图
 
@@ -96,7 +96,7 @@ Typert 生成 `./typert` 与 `./remote` 暴露的宿主与客户端 Remote 工
 
 这些限制界定该 Remote 不会为客户端做什么。它们是当前包的约束,不是任务清单。
 
-- **管理器做不到的,Remote 也做不到**——[管理器的限制](../../boot/plugin-manager/README.zh.md#known-limitations-and-deferred-work)原样适用:已加载的包下次重启才更新,依赖检测止于注入,preset 的行供之后的会话组合,`engines.dsh` 只报告不强制。
+- **管理器做不到的,Remote 也做不到**——[管理器的限制](../../boot/plugin-manager/README.zh.md#known-limitations-and-deferred-work)原样适用:已加载的包下次重启才更新,依赖检测止于注入,`engines.dsh` 只报告不强制。
 
 <a id="dev-note"></a>
 ### 开发备注

+ 1 - 7
packages/host/plugin-manager/package.json

@@ -51,24 +51,18 @@
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
-    "@deepseek-ai/dsh-agent-presets": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-plugin-manager": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-util-values": "workspace:^"
   },
-  "peerDependenciesMeta": {
-    "@deepseek-ai/dsh-agent-presets": {
-      "optional": true
-    }
-  },
+  "peerDependenciesMeta": {},
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/cordis-plugin-group": "workspace:^",
     "@deepseek-ai/cordis-plugin-include": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
-    "@deepseek-ai/dsh-agent-presets": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-plugin-manager": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",

+ 8 - 16
packages/host/plugin-manager/src/index.ts

@@ -3,7 +3,7 @@
  * Typert service that relays each call to the shared plugin manager over the
  * booted profile and maps its `plugins/*` failures onto Remote errors of the
  * same code. What plugin management does lives in `dsh-plugin-manager`; this
- * row only reads the profile runtime, the preset roster, and the agent
+ * row only reads the profile runtime and the agent
  * registry off the context and hands them over.
  * @module @deepseek-ai/dsh-host-plugin-manager
  */
@@ -12,7 +12,6 @@ import type { Context } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
 import { inspectEntryIssues, type readPackageMetadata } from '@deepseek-ai/dsh-app-boot'
 import type {} from '@deepseek-ai/dsh-agent'
-import type {} from '@deepseek-ai/dsh-agent-presets'
 import {
   PluginManager,
   pluginOperationFailureOf,
@@ -22,7 +21,6 @@ import {
   type PluginOperationFailure,
   type PluginPackageView,
   type PluginRowAddition,
-  type PluginRowTarget,
   type SpawnLike,
 } from '@deepseek-ai/dsh-plugin-manager'
 import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
@@ -59,8 +57,7 @@ export interface PluginManagerInternals {
 /**
  * The `pluginManager` service and the `plugins` Remote.
  *
- * The row injects only the Loader; the profile runtime, the preset roster,
- * and the agent registry are read off the context per call, so a
+ * The row injects only the Loader; the profile runtime and the agent registry are read off the context per call, so a
  * composition without them (a test, a launcher other than the profile
  * launcher) still mounts this service and every call then reports
  * `plugins/unavailable` rather than the service failing to start.
@@ -81,7 +78,6 @@ export class PluginManagerRemote extends TypertRemoteService {
     this.manager = internals.manager ?? new PluginManager(ctx, {
       config,
       runtime: () => ctx.get('profileRuntime'),
-      presets: () => ctx.get('agentPresets'),
       runningAgents: () => (ctx.get('agents')?.list() ?? []).filter(agent => agent.status === 'running').length,
       ...internals.spawn === undefined ? {} : { spawn: internals.spawn },
       ...internals.metadata === undefined ? {} : { metadata: internals.metadata },
@@ -185,9 +181,8 @@ export class PluginManagerRemote extends TypertRemoteService {
   }
 
   /**
-   * Add a row naming one of the package's modules to a user layer.
+   * Add a row naming one of the package's modules to the profile user layer.
    * @param packageName - the installed package.
-   * @param target - which layer.
    * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
    * `id` overrides the derived row id, `config` overrides the declared default.
    * @returns where the row landed.
@@ -195,31 +190,28 @@ export class PluginManagerRemote extends TypertRemoteService {
   @Remote('addRow')
   async addRow(
     packageName: string,
-    target: PluginRowTarget,
     options?: { module?: string; id?: string; config?: JsonValue },
   ): Promise<PluginRowAddition> {
-    return relay(() => this.manager.addRow(packageName, target, options))
+    return relay(() => this.manager.addRow(packageName, options))
   }
 
   /**
    * Remove a row a user layer inserted.
-   * @param target - which layer.
    * @param rowId - the inserted row's id.
    */
   @Remote('removeRow')
-  async removeRow(target: PluginRowTarget, rowId: string): Promise<void> {
-    return relay(() => this.manager.removeRow(target, rowId))
+  async removeRow(rowId: string): Promise<void> {
+    return relay(() => this.manager.removeRow(rowId))
   }
 
   /**
    * Switch one row off or on in a user layer; deny-only.
-   * @param target - which layer.
    * @param rowId - the row's id as the composition declares it.
    * @param disabled - whether the layer should switch the row off.
    */
   @Remote('setRowDisabled')
-  async setRowDisabled(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void> {
-    return relay(() => this.manager.setRowDisabled(target, rowId, disabled))
+  async setRowDisabled(rowId: string, disabled: boolean): Promise<void> {
+    return relay(() => this.manager.setRowDisabled(rowId, disabled))
   }
 
   /**

+ 11 - 12
packages/host/plugin-manager/tests/plugin-manager.spec.ts

@@ -274,9 +274,9 @@ describe('PluginManagerRemote', () => {
     await remote.enable('pkg')
     await remote.disable('pkg')
     await remote.retry('pkg')
-    await remote.addRow('pkg', { kind: 'global' }, { module: './x.js', id: 'x', config: { a: 1 } })
-    await remote.removeRow({ kind: 'preset', preset: 'standard' }, 'x')
-    await remote.setRowDisabled({ kind: 'global' }, 'x', true)
+    await remote.addRow('pkg', { module: './x.js', id: 'x', config: { a: 1 } })
+    await remote.removeRow('x')
+    await remote.setRowDisabled('x', true)
     await remote.dependents('pkg')
 
     expect(calls).toEqual([
@@ -286,14 +286,14 @@ describe('PluginManagerRemote', () => {
       ['enable', 'pkg'],
       ['disable', 'pkg'],
       ['retry', 'pkg'],
-      ['addRow', 'pkg', { kind: 'global' }, { module: './x.js', id: 'x', config: { a: 1 } }],
-      ['removeRow', { kind: 'preset', preset: 'standard' }, 'x'],
-      ['setRowDisabled', { kind: 'global' }, 'x', true],
+      ['addRow', 'pkg', { module: './x.js', id: 'x', config: { a: 1 } }],
+      ['removeRow', 'x'],
+      ['setRowDisabled', 'x', true],
       ['dependents', 'pkg'],
     ])
   })
 
-  it('hands the manager readers into the context: the runtime, the agent count, the roster, and the seams', async () => {
+  it('hands the manager readers into the context: the runtime, the agent count, and the seams', async () => {
     const profileDir = mkdtempSync(join(tmpdir(), 'dsh-host-plugin-manager-'))
     writeFileSync(join(profileDir, 'package.json'), JSON.stringify({
       name: 'dsh-profile-web', private: true, dependencies: { pkg: '1.0.0' }, dsh: { profile: { bundles: [], patchReload: 'startup' } },
@@ -323,8 +323,7 @@ describe('PluginManagerRemote', () => {
       // The agent-count reader: a running session refuses an install before pnpm runs.
       ctx.provide('agents', { list: () => [{ status: 'running' }, { status: 'idle' }] } as never)
       await expect(remote.add('anything')).rejects.toMatchObject({ code: 'plugins/agents-running', details: { operation: 'add', running: 1 } })
-      // The static reader answers for the package, then the roster reader finds no roster for a preset target.
-      await expect(remote.addRow('pkg', { kind: 'preset', preset: 'standard' })).rejects.toMatchObject({ code: 'plugins/unavailable', details: { reason: 'no roster' } })
+      await expect(remote.addRow('pkg', { module: './unknown' })).rejects.toMatchObject({ code: 'plugins/not-enableable' })
     } finally {
       rmSync(profileDir, { recursive: true, force: true })
     }
@@ -353,7 +352,7 @@ describe('remoteErrorOf', () => {
       new PluginOperationError('plugins/not-enableable', 'm', { packageName: 'p', reason: 'r' }),
       new PluginOperationError('plugins/enable-failed', 'm', { packageName: 'p', reason: 'r' }),
       new PluginOperationError('plugins/install-failed', 'm', { spec: 's', exitCode: 1, log: 'l' }),
-      new PluginOperationError('plugins/row-conflict', 'm', { rowId: 'x', target: { kind: 'global' } }),
+      new PluginOperationError('plugins/row-conflict', 'm', { rowId: 'x' }),
       new PluginOperationError('plugins/busy', 'm', { operation: 'add', subject: 'y', active: { operation: 'add', subject: 'x' } }),
       new PluginOperationError('plugins/agents-running', 'm', { operation: 'add', running: 1 }),
     ]
@@ -369,7 +368,7 @@ describe('remoteErrorOf', () => {
 
 describe('RemoteError codes', () => {
   it('declare their details', () => {
-    const error = new RemoteError('plugins/row-conflict', 'taken', { rowId: 'x', target: { kind: 'global' } })
-    expect(error.details).toEqual({ rowId: 'x', target: { kind: 'global' } })
+    const error = new RemoteError('plugins/row-conflict', 'taken', { rowId: 'x' })
+    expect(error.details).toEqual({ rowId: 'x' })
   })
 })

+ 0 - 3
packages/host/plugin-manager/tsconfig.json

@@ -26,9 +26,6 @@
     {
       "path": "../../boot/plugin-manager"
     },
-    {
-      "path": "../../preset/agent-presets"
-    },
     {
       "path": "../../typert/protocol"
     },

+ 2 - 2
packages/preset/agent-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/preset/agent-presets/README.md
-README.md: f6916023772d22163ce470e30efd255e400e2dc6
-README.zh.md: b4de89926ff3e91beb302c203660d23c34fc5989
+README.md: 07691afa0b0c37327e553df8e55d221bbb9a8bcf
+README.zh.md: 3fa2d633c0ffae84d843795a42ff6d3a5adcc39f

+ 6 - 22
packages/preset/agent-presets/README.md

@@ -77,22 +77,6 @@ Authoring is copy-only: creating a preset copies an existing preset's whole dire
 
 A copy is refused when the id is not `[a-z0-9][a-z0-9-]*` (the id becomes a directory name), when the id is already taken (a copy never overwrites), or when the source is unknown. Deleting removes only locally authored presets; presets that ship with the deployment are not removable. A session already running on a deleted preset keeps running on it.
 
-### Adjusting a preset without editing it
-
-Every preset accepts a user patch layer: a `cordis.patch.yml` in the Loader's patch-list format — the same file a profile's user layer is — that switches rows off, replaces a row's config, or inserts rows, applied over the composition at every mount. For a locally authored preset the file sits beside `agent.cordis.yml`; for a shipped preset it sits alone in `<dshHome>/.agent-presets/<id>/cordis.patch.yml`, so the shipped install stays as it came:
-
-```yaml
-- id: tool-web
-  disabled: true
-- insert:
-    - id: sql
-      name: '@acme/dsh-sql-tool'
-      config:
-        dsn: sqlite://local.db
-```
-
-The layer is judged with the composition: a layer that does not parse, inserts a malformed row, or inserts a row naming a module that cannot resolve makes the preset broken with that reason, and a layer whose id no root supplies is listed as a broken slot. Sessions created after the layer changes compose the new content; running sessions keep theirs. The composition inventory reports each row's `source` (`preset` or `user`) and, for a row that is off, whether the composition or the layer switched it off. A copy carries the layer in beside the new composition, and `removeOverlay` deletes it. The [plugin manager](../../host/plugin-manager/README.md) writes this file through `overlayPathFor`.
-
 ### Switching a session's preset
 
 A session can switch to a different preset only while it has produced nothing — no messages or tool calls. After that, the composition is fixed for the session's life, because swapping tools mid-conversation would leave logged tool calls the new composition cannot make. A committed switch emits `tools/change` because the resolved tool set changed without a registry edit. The switch is also recorded in the session log, so a resumed or forked session rebuilds under the composition it ran.
@@ -114,7 +98,7 @@ This section explains the design behind the roster and the standing mount; obser
 ### Design philosophy
 
 - **One standing composition per preset.** A preset is mounted once per process under a standing scope; agents join by parenting their scope key to the mount, so the mount's registrations and listeners cover every joined agent and no sibling preset's.
-- **Generations keyed on the composition file and the user patch layer.** The mount records the composition file's stamp (mtime and size) and a digest of the layer's text; a session that finds the stamp stale starts the next generation, while sessions already joined keep the generation they run on — a running session outlives its file changing or disappearing. A layer edited back to a content an earlier generation composed returns to that generation instead of composing a third.
+- **Generations keyed on the composition file.** The mount records the composition file's stamp (mtime and size); a session that finds the stamp stale starts the next generation, while sessions already joined keep the generation they run on — a running session outlives its file changing or disappearing.
 - **The preset file is an input, never a persistence target.** The mounted subtree overrides `write()` as a no-op, so a loader-initiated write-back never rewrites a shared preset file.
 - **Discovery owns health.** A directory whose composition is missing or unloadable is a broken roster row with a reason, not a skip — a skipped directory would still occupy its id while no surface shows anything to delete.
 
@@ -123,11 +107,11 @@ This section explains the design behind the roster and the standing mount; obser
 | File | Role |
 |---|---|
 | [`src/index.ts`](src/index.ts) | Service entry: `Config` schema, settings namespace, roster API, standing-mount coordination |
-| [`src/discovery.ts`](src/discovery.ts) | Filesystem discovery: root scanning, user patch layer attachment, health checks, id validation, ordering |
-| [`src/composition-inventory.ts`](src/composition-inventory.ts) | Flattened composition rows for plugin-listing surfaces: file reads with evaluated disabled gates and the user patch layer applied, mount reads with fiber states, per-row `source` and `disabledBy` |
+| [`src/discovery.ts`](src/discovery.ts) | Filesystem discovery: root scanning, health checks, id validation, ordering |
+| [`src/composition-inventory.ts`](src/composition-inventory.ts) | Flattened composition rows for plugin-listing surfaces: file reads with evaluated disabled gates, mount reads with fiber states |
 | [`src/preset.ts`](src/preset.ts) | Vocabulary: preset id rule, `AgentPreset` and `PresetRoot`, error types |
-| [`src/mount.ts`](src/mount.ts) | Subtree mounting with the user patch layer as runtime patches, host base-URL handling, mount audit, `write()` suppression |
-| [`src/authoring.ts`](src/authoring.ts) | Copy/delete/read of locally authored presets, user patch layer copy and removal, permission tightening |
+| [`src/mount.ts`](src/mount.ts) | Subtree mounting, host base-URL handling, mount audit, `write()` suppression |
+| [`src/authoring.ts`](src/authoring.ts) | Copy/delete/read of locally authored presets, permission tightening |
 | [`src/metadata.ts`](src/metadata.ts) | `preset.yml` display metadata |
 | [`src/session.ts`](src/session.ts) | `agent-preset/selected` event and the `agentPreset` Session projection |
 | [`src/types.ts`](src/types.ts) | Client-safe wire payloads and cordis event declaration |
@@ -189,7 +173,7 @@ These limits define when the roster is a poor fit or needs special operational c
 
 - **A preset outside the writable root is discoverable but not deletable** — `remove()` refuses anything that does not live under the first `user` root, so a deployment that configures its own writable root while leaving `includeUserRoot` on lists the harness-home presets, mounts them, and answers "it does not live under the writable preset root" for every delete. A deployment that wants only its own presets sets `includeUserRoot: false`.
 - **A session cannot change preset once it has produced anything** — switching re-links a blank session's parent scope to another standing mount, and only a blank one: swapping tools mid-conversation would strand tools the model has called.
-- **A generation is keyed on the composition file and the user patch layer alone** — the stamp check notices `agent.cordis.yml` or `cordis.patch.yml` changing, not an edit to a skill file or asset beside them; those reach new sessions only once one of the two files itself moves or the process restarts.
+- **A generation is keyed on the composition file alone** — the stamp check notices `agent.cordis.yml` changing, not an edit to a skill file or asset beside it; those reach new sessions only once the composition file itself moves or the process restarts.
 - **A superseded generation is never reclaimed** — sessions already joined keep the generation they run on, and the roster holds no join count that could tell when the last one left, so the whole subtree stays mounted until the process ends. The cost is per generation rather than per session, but it is not free: `dsh-skill-filesystem` watches its roots by default, so each edit-then-create cycle adds a live watcher set.
 - **A copy is never mounted to validate** — it is byte-identical to its source, so a source broken on disk yields a copy exactly as broken as the source; discovery's health check marks both rows on the next roster read rather than deferring the failure to a session start.
 - **Health asks what is installed, not what would import** — discovery proves the composition parses in the loader dialect, holds named rows, and that each row it can prove will start names a package present above the harness base or a file that exists; it never imports one, so a package whose own entry file is missing, a plugin that throws on apply, and one waiting forever for a service all still fail at the first session. `disabled` is the one entry field the Loader interpolates, so a row carrying an expression there is left unchecked rather than judged from the file.

+ 6 - 22
packages/preset/agent-presets/README.zh.md

@@ -77,22 +77,6 @@ agent-presets:
 
 以下情况会拒绝复制:id 不符合 `[a-z0-9][a-z0-9-]*`(id 会成为目录名)、id 已被占用(复制从不覆写)、或来源未知。删除只移除本地创作的 preset;随部署提供的 preset 不可删除。已在被删除 preset 上运行的会话会继续运行。
 
-### 不编辑 preset 也能调整它
-
-每个 preset 都接受一层用户补丁:一个采用 Loader 补丁列表格式的 `cordis.patch.yml`——与 profile 的用户层是同一种文件——用来关掉行、替换某行的 config 或插入行,在每次挂载时施加在组装之上。本地创作的 preset 把它放在 `agent.cordis.yml` 旁边;随附 preset 则单独放在 `<dshHome>/.agent-presets/<id>/cordis.patch.yml`,随附安装保持原样:
-
-```yaml
-- id: tool-web
-  disabled: true
-- insert:
-    - id: sql
-      name: '@acme/dsh-sql-tool'
-      config:
-        dsn: sqlite://local.db
-```
-
-这一层与组装一起判定:无法解析的层、插入了畸形行、或插入了引用无法解析模块的行,都会让 preset 带着该原因变为 broken;没有任何根目录提供其 id 的层会被列为损坏的槽位。在层变化之后创建的会话组合新内容;运行中的会话保持各自的。组合清单报告每一行的 `source`(`preset` 或 `user`),以及被关掉的行是组装还是这一层关掉的。复制会把这一层带到新组装旁边,`removeOverlay` 删除它。[插件管理器](../../host/plugin-manager/README.zh.md) 经 `overlayPathFor` 写入这个文件。
-
 ### 切换会话的 preset
 
 会话只有在尚未产出任何内容——没有消息或工具调用——时才能切换到不同的 preset。此后组装在会话的生命周期内固定,因为在对话中途调换工具会留下新组装无法执行的已记录工具调用。已提交的切换会发出 `tools/change`,因为解析后的工具集在没有注册表编辑的情况下发生了变化。切换也会记入会话日志,因此恢复或 fork 的会话会按它运行的组装重建。
@@ -114,7 +98,7 @@ agent-presets:
 ### 设计理念
 
 - **每个 preset 一份常驻组装。** preset 在进程内只挂载一次,挂到常驻 scope 之下;agent 通过把自己的 scope key 认父到该挂载来加入,因此挂载的注册与监听器覆盖每个已加入的 agent,而不覆盖兄弟 preset 的。
-- **代际以组装文件与用户补丁层为键。** 挂载记录组装文件的 stamp(mtime 与大小)以及补丁层文本的摘要;发现 stamp 过期的会话会开启下一个代际,而已加入的会话保持各自运行的那个代际——运行中的会话在文件被修改或删除后继续存活。补丁层被改回某个早先代际组装过的内容时,回到那一代而不是再组装第三个。
+- **代际以组装文件为键。** 挂载记录组装文件的 stamp(mtime 与大小);发现 stamp 过期的会话会开启下一个代际,而已加入的会话保持各自运行的那个代际——运行中的会话在文件被修改或删除后继续存活。
 - **preset 文件是输入,绝不是持久化目标。** 被挂载的子树把 `write()` 覆写为空操作,因此 loader 发起的写回绝不会重写共享的 preset 文件。
 - **发现过程拥有健康。** 组装缺失或不可加载的目录是携带原因的 broken 名单行,而不是被跳过——被跳过的目录仍占着它的 id,而任何界面都没有可删的东西。
 
@@ -123,11 +107,11 @@ agent-presets:
 | 文件 | 职责 |
 |---|---|
 | [`src/index.ts`](src/index.ts) | 服务入口:`Config` schema、settings 命名空间、名单 API、常驻挂载协调 |
-| [`src/discovery.ts`](src/discovery.ts) | 文件系统发现:根目录扫描、用户补丁层附着、健康检查、id 校验、排序 |
-| [`src/composition-inventory.ts`](src/composition-inventory.ts) | 面向插件清单表面的压平组合行:文件读取(求值 disabled 门并施加用户补丁层)与挂载读取(携带 fiber 状态),每行的 `source` 与 `disabledBy` |
+| [`src/discovery.ts`](src/discovery.ts) | 文件系统发现:根目录扫描、健康检查、id 校验、排序 |
+| [`src/composition-inventory.ts`](src/composition-inventory.ts) | 面向插件清单表面的压平组合行:文件读取(求值 disabled 门)与挂载读取(携带 fiber 状态) |
 | [`src/preset.ts`](src/preset.ts) | 词汇体系:preset id 规则、`AgentPreset` 与 `PresetRoot`、错误类型 |
-| [`src/mount.ts`](src/mount.ts) | 以用户补丁层为运行时 patches 的子树挂载、宿主 base-URL 处理、挂载审计、`write()` 抑制 |
-| [`src/authoring.ts`](src/authoring.ts) | 本地创作 preset 的复制/删除/读取、用户补丁层的复制与移除、权限收紧 |
+| [`src/mount.ts`](src/mount.ts) | 子树挂载、宿主 base-URL 处理、挂载审计、`write()` 抑制 |
+| [`src/authoring.ts`](src/authoring.ts) | 本地创作 preset 的复制/删除/读取、权限收紧 |
 | [`src/metadata.ts`](src/metadata.ts) | `preset.yml` 展示元数据 |
 | [`src/session.ts`](src/session.ts) | `agent-preset/selected` 事件与 `agentPreset` Session 投影 |
 | [`src/types.ts`](src/types.ts) | client-safe 的协议载荷与 cordis 事件声明 |
@@ -189,7 +173,7 @@ agent-presets:
 
 - **位于可写根目录之外的 preset 可被发现却无法删除**——`remove()` 拒绝任何不在第一个 `user` 根目录下的 preset,因此一个既配置了自有可写根、又保留 `includeUserRoot` 的部署,会列出并挂载 harness home 下的 preset,却对每次删除回答「它不在可写 preset 根目录之下」。只想要自有 preset 的部署应设置 `includeUserRoot: false`。
 - **会话一旦产出任何内容便无法更换 preset**——切换会把空白会话的父作用域重链到另一个常驻挂载,且仅限空白会话:在对话中途调换工具会抽走模型已调用的工具。
-- **代际只以组装文件与用户补丁层为键**——stamp 检查只察觉 `agent.cordis.yml` 或 `cordis.patch.yml` 的变化,察觉不到旁边 skill 文件或资产的编辑;那些编辑要等这两个文件之一本身变动或进程重启才达到新会话。
+- **代际只以组装文件为键**——stamp 检查只察觉 `agent.cordis.yml` 的变化,察觉不到旁边 skill 文件或资产的编辑;那些编辑要等组装文件本身变动或进程重启才达到新会话。
 - **被替代的代际永不回收**——已加入的会话保持其运行所在的代际,而名单没有加入计数可以判断最后一个何时离开,因此整棵子树一直挂到进程结束。代价按代际计而非按会话计,但并非为零:`dsh-skill-filesystem` 默认监听自己的根目录,因此每一轮「编辑后建会话」都会新增一套活的 watcher。
 - **副本从不被实际挂载以校验**——它与来源逐字节相同,因此磁盘上已坏的来源会产出与来源同样损坏的副本;发现过程的健康检查会在下一次读取名单时把两行都标出来,而不是把失败推迟到会话启动。
 - **健康问的是「装没装」,不是「能不能 import」**——发现过程证明组装能以加载器方言解析、由具名行组成,且对于每个能证明会启动的行,其引用的包存在于 harness 基址以上,或引用的文件确实存在;它从不 import 任何一个,因此入口文件缺失的包、在 apply 时抛错的插件、以及永远等待某个服务的插件,都仍在第一个会话处失败。`disabled` 是加载器唯一会插值的条目字段,因此该字段带表达式的行不予检查,而不是仅凭文件作出判断。

+ 6 - 8
packages/preset/agent-presets/package.json

@@ -54,21 +54,20 @@
   ],
   "license": "MIT",
   "peerDependencies": {
-    "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/cordis-plugin-include": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
-    "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-atomic-write": "workspace:^",
-    "@deepseek-ai/dsh-home-paths": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/dsh-home-paths": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
-    "@deepseek-ai/dsh-typert-protocol": "workspace:^"
+    "@deepseek-ai/dsh-typert-protocol": "workspace:^",
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "dependencies": {
     "js-yaml": "^4.1.0",
@@ -76,17 +75,15 @@
     "zod": "^4.4.3"
   },
   "devDependencies": {
-    "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/cordis-plugin-group": "workspace:^",
     "@deepseek-ai/cordis-plugin-include": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-agent-loop": "workspace:^",
-    "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-atomic-write": "workspace:^",
-    "@deepseek-ai/dsh-home-paths": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
+    "@deepseek-ai/dsh-home-paths": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-projection": "workspace:^",
@@ -94,6 +91,7 @@
     "@deepseek-ai/dsh-settings-file": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
-    "@deepseek-ai/dsh-typert-protocol": "workspace:^"
+    "@deepseek-ai/dsh-typert-protocol": "workspace:^",
+    "@deepseek-ai/cordis": "workspace:^"
   }
 }

+ 0 - 35
packages/preset/agent-presets/src/authoring.ts

@@ -17,7 +17,6 @@ import { dirname, isAbsolute, join, resolve } from 'node:path'
 import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
 import { expandHomePath } from '@deepseek-ai/dsh-home-paths'
 import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
-import { OVERLAY_FILE } from './discovery.ts'
 import { METADATA_FILE, renderPresetMetadata } from './metadata.ts'
 import { PRESET_ID, type AgentPreset, type PresetRoot } from './preset.ts'
 
@@ -117,11 +116,6 @@ async function tightenModes(dir: string): Promise<void> {
  * sorted into the shipped set's declared order, would make the roster stop
  * distinguishing them. With no name given and no description to keep, the
  * file is removed so the copy publishes nothing rather than a blank.
- *
- * The source's user patch layer travels with the copy: a layer beside the
- * composition is copied with the directory, and a shipped preset's layer —
- * kept in the user root's slot of the same id — is copied in beside the new
- * composition, so the copy composes exactly what its source composed.
  * @param roots - the configured roots; the first `user` one receives the copy.
  * @param source - the resolved preset the copy starts from.
  * @param id - the new preset's id, which becomes its directory name.
@@ -150,9 +144,6 @@ export async function copyComposition(
     await cp(dirname(source.path), dir, {
       recursive: true, dereference: true, force: false, errorOnExist: true,
     })
-    if (source.overlayPath !== undefined && dirname(source.overlayPath) !== dirname(source.path)) {
-      await cp(source.overlayPath, join(dir, OVERLAY_FILE), { dereference: true, force: false, errorOnExist: true })
-    }
     await tightenModes(dir)
     const rendered = renderPresetMetadata({
       ...name === undefined ? {} : { name },
@@ -198,29 +189,3 @@ export async function deleteComposition(
   }
   await rm(dir, { recursive: true, force: true })
 }
-
-/**
- * Delete a preset's user patch layer, restoring the composition as its root
- * supplies it. Only a layer under the writable root is deletable: a layer
- * beside a shipped composition would be part of the deployment.
- * @param roots - the configured roots.
- * @param preset - the resolved preset whose layer to remove.
- * @returns true when a layer was removed; false when the preset had none.
- * @throws when the layer lies outside the writable root.
- */
-export async function deleteOverlay(
-  roots: readonly PresetRoot[],
-  preset: AgentPreset,
-): Promise<boolean> {
-  if (preset.overlayPath === undefined) return false
-  const dir = join(writableRoot(roots, preset.id), preset.id)
-  if (!isAbsolute(preset.overlayPath) || !preset.overlayPath.startsWith(dir)) {
-    throw notWritable(preset.id, 'its user patch layer does not live under the writable preset root')
-  }
-  await rm(preset.overlayPath, { force: true })
-  // A shipped preset's slot exists only to hold the layer; an emptied slot
-  // would otherwise read as a directory missing its composition.
-  const remaining = await readdir(dir)
-  if (remaining.length === 0) await rm(dir, { recursive: true, force: true })
-  return true
-}

+ 11 - 83
packages/preset/agent-presets/src/composition-inventory.ts

@@ -6,18 +6,15 @@
  * boot answers from its composition file, with `!!js` disabled expressions
  * evaluated through the caller-supplied Loader evaluator so the file answer
  * matches the decision a mount on this host would make. A row whose
- * expression the evaluator refuses stays `'conditional'`. Either way the
- * preset's user patch layer is accounted for: a row the layer inserted reads
- * `source: 'user'`, and a row it switched off with a literal `disabled: true`
- * reads `disabledBy: 'user'`.
+ * expression the evaluator refuses stays `'conditional'`.
  * @module @deepseek-ai/dsh-agent-presets/composition-inventory
  */
 
 import { readFile } from 'node:fs/promises'
 import { load } from 'js-yaml'
 import type { FiberState } from '@deepseek-ai/cordis'
-import { isJsExpr, type EntryOptions, type EntryTree } from '@deepseek-ai/cordis-plugin-loader'
-import { applyEntryPatches, entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import { isJsExpr, type EntryTree } from '@deepseek-ai/cordis-plugin-loader'
+import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
 import { entryListProblem } from './discovery.ts'
 import type { PresetTrust } from './preset.ts'
 
@@ -35,12 +32,6 @@ export type CompositionRowEnablement = boolean | 'conditional'
  */
 export type DisabledExpressionEvaluator = (expression: string) => unknown
 
-/** Who put a row into a preset's composition: its file, or the user patch layer over it. */
-export type CompositionRowSource = 'preset' | 'user'
-
-/** Why a row is off: the composition's own gate, or the user patch layer's literal `disabled: true`. */
-export type CompositionRowDisabledBy = 'composition' | 'user'
-
 /** One plugin row a preset composition names. */
 export interface AgentPresetCompositionRow {
   /**
@@ -56,46 +47,8 @@ export interface AgentPresetCompositionRow {
   readonly condition?: string
   /** Root-fiber state, present only when read from a live mount. */
   readonly fiberState?: FiberState
-  /** Whether the composition file or the user patch layer supplied the row. */
-  readonly source: CompositionRowSource
-  /** Present exactly when `enabled` is false. */
-  readonly disabledBy?: CompositionRowDisabledBy
-}
-
-/** What a user patch layer decided about a composition's rows, by row id. */
-export interface OverlayFacts {
-  /** Ids of rows the layer inserted, including rows inside inserted groups. */
-  readonly inserted: ReadonlySet<string>
-  /** Ids of rows the layer switched off with a literal `disabled: true`. */
-  readonly disabled: ReadonlySet<string>
-}
-
-/**
- * Read one user patch layer's decisions by row id. A `!!js` gate in the layer
- * is a condition, not a user decision, and is left to the composition.
- * @param patches - the layer's patch list, parsed; empty when the preset has none.
- * @returns the inserted and user-disabled row ids.
- */
-export function overlayFacts(patches: readonly PatchOptions[]): OverlayFacts {
-  const inserted = new Set<string>()
-  const disabled = new Set<string>()
-  const visit = (row: EntryOptions): void => {
-    if (typeof row.id === 'string') inserted.add(row.id)
-    if (row.group && Array.isArray(row.config)) (row.config as EntryOptions[]).forEach(visit)
-  }
-  for (const patch of patches) {
-    if (patch.insert !== undefined) {
-      patch.insert.forEach(visit)
-      continue
-    }
-    if (typeof patch.id === 'string' && patch.disabled === true) disabled.add(patch.id)
-  }
-  return { inserted, disabled }
 }
 
-/** The empty layer: nothing inserted, nothing switched off. */
-const NO_OVERLAY: OverlayFacts = { inserted: new Set(), disabled: new Set() }
-
 /** One preset's roster identity beside its composition rows. */
 export interface AgentPresetComposition {
   /** Stable preset id. */
@@ -176,56 +129,39 @@ function flattenRows(
   rows: readonly unknown[],
   outerDisabled: boolean | 'conditional',
   evaluateExpression: DisabledExpressionEvaluator,
-  overlay: OverlayFacts,
   found: AgentPresetCompositionRow[],
 ): void {
   for (const value of rows) {
     const row = value as RawRow
     const disabled = combineDisabled(outerDisabled, disabledContribution(row.disabled, evaluateExpression))
     if (row.group === true) {
-      flattenRows(row.config as readonly unknown[], disabled, evaluateExpression, overlay, found)
+      flattenRows(row.config as readonly unknown[], disabled, evaluateExpression, found)
       continue
     }
-    const id = typeof row.id === 'string' && row.id !== '' ? row.id : null
     found.push({
-      entryId: id,
+      entryId: typeof row.id === 'string' && row.id !== '' ? row.id : null,
       moduleName: row.name,
       enabled: disabled === true ? false : disabled === 'conditional' ? 'conditional' : true,
       ...isJsExpr(row.disabled) ? { condition: row.disabled.__jsExpr } : {},
-      ...rowOverlayFields(id, disabled === true, overlay),
     })
   }
 }
 
-/** The `source` and `disabledBy` fields of one row, from the layer's decisions. */
-function rowOverlayFields(
-  id: string | null, disabled: boolean, overlay: OverlayFacts,
-): Pick<AgentPresetCompositionRow, 'source' | 'disabledBy'> {
-  const userRow = id !== null && overlay.inserted.has(id)
-  return {
-    source: userRow ? 'user' : 'preset',
-    ...disabled ? { disabledBy: id !== null && overlay.disabled.has(id) ? 'user' as const : 'composition' as const } : {},
-  }
-}
-
 /**
  * Plugin rows of one composition file, for a preset with no live mount.
  *
  * Parsed with the Loader's own dialect ({@link entryListSchema}), so the rows
- * reported are the rows a mount would start from, with the user patch layer
- * applied through the include's own patch algorithm. A file that stopped
- * reading as a composition — discovery judged the preset healthy moments
- * earlier, so only an edit racing this read gets here — answers as broken
- * with the raced reason rather than dropping the rows silently.
+ * reported are the rows a mount would start from. A file that stopped reading
+ * as a composition — discovery judged the preset healthy moments earlier, so
+ * only an edit racing this read gets here — answers as broken with the raced
+ * reason rather than dropping the rows silently.
  * @param path - absolute path of the composition file.
  * @param evaluateExpression - the Loader-context evaluator for `!!js` nodes.
- * @param overlay - the preset's user patch layer, parsed; empty when it has none.
  * @returns flattened rows in composition order, or why they cannot be read.
  */
 export async function fileComposition(
   path: string,
   evaluateExpression: DisabledExpressionEvaluator,
-  overlay: readonly PatchOptions[] = [],
 ): Promise<{ rows: AgentPresetCompositionRow[] } | { broken: string }> {
   let rows: unknown
   try {
@@ -236,22 +172,17 @@ export async function fileComposition(
   }
   const problem = entryListProblem(rows)
   if (problem !== undefined) return { broken: problem }
-  // Silent: a patch that matches no row is the Loader's warning at mount
-  // time, not an inventory failure.
-  const composed = applyEntryPatches(rows as EntryOptions[], [...overlay], () => {})
   const found: AgentPresetCompositionRow[] = []
-  flattenRows(composed, false, evaluateExpression, overlay.length === 0 ? NO_OVERLAY : overlayFacts(overlay), found)
+  flattenRows(rows as readonly unknown[], false, evaluateExpression, found)
   return { rows: found }
 }
 
 /**
  * Plugin rows of one live standing composition, in Loader-entry order.
  * @param tree - the standing mount's entry tree.
- * @param overlay - the preset's user patch layer, parsed; empty when it has none.
  * @returns rows with the Loader's evaluated enablement and root-fiber states.
  */
-export function mountedCompositionRows(tree: EntryTree, overlay: readonly PatchOptions[] = []): AgentPresetCompositionRow[] {
-  const facts = overlay.length === 0 ? NO_OVERLAY : overlayFacts(overlay)
+export function mountedCompositionRows(tree: EntryTree): AgentPresetCompositionRow[] {
   const found: AgentPresetCompositionRow[] = []
   for (const entry of tree.entries()) {
     if (entry.options.group) continue
@@ -261,9 +192,6 @@ export function mountedCompositionRows(tree: EntryTree, overlay: readonly PatchO
       enabled: !entry.disabled,
       ...isJsExpr(entry.options.disabled) ? { condition: entry.options.disabled.__jsExpr } : {},
       ...entry.fiber === undefined ? {} : { fiberState: entry.fiber.state },
-      // Layer decisions and user patches address rows by the id the composition
-      // declares; the tree-wide `entry.id` carries the include's prefix.
-      ...rowOverlayFields(entry.options.id, entry.disabled, facts),
     })
   }
   return found

+ 13 - 128
packages/preset/agent-presets/src/discovery.ts

@@ -5,12 +5,6 @@
  * re-reads the roots on every call so a preset authored while the process is
  * running is visible without a restart.
  *
- * A preset may carry a user patch layer, {@link OVERLAY_FILE}: beside its
- * composition, or — for a shipped preset — alone in the user root's directory
- * of the same id. Discovery attaches the layer to the preset that wins the
- * id, so the mount applies it and the inventory reports it, and judges its
- * health with the composition's.
- *
  * Discovery also owns preset HEALTH: a directory whose composition is
  * missing or unloadable is reported as a broken roster row rather than
  * skipped. A skipped directory would still occupy its id on disk — the copy
@@ -33,25 +27,15 @@ import { isBuiltin } from 'node:module'
 import { dirname, join, resolve } from 'node:path'
 import { fileURLToPath, pathToFileURL } from 'node:url'
 import { load } from 'js-yaml'
-import { entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
 import { expandHomePath } from '@deepseek-ai/dsh-home-paths'
-import { readPatchListFile } from '@deepseek-ai/dsh-app-boot/patch-file'
 import { readPresetMetadata } from './metadata.ts'
-import { PRESET_ID, type AgentPreset, type PresetRoot, type PresetTrust } from './preset.ts'
+import { PRESET_ID, type AgentPreset, type PresetRoot } from './preset.ts'
 import { classifyRowSpecifier, type RowSpecifier } from './specifier.ts'
 
 /** The composition file that makes a directory a preset. */
 export const COMPOSITION_FILE = 'agent.cordis.yml'
 
-/**
- * The user patch layer over a preset's composition: a Loader patch list in
- * the same format as a profile's `cordis.patch.yml`, applied at every mount.
- */
-export const OVERLAY_FILE = 'cordis.patch.yml'
-
-/** Diagnostic prefix on overlay parse errors. */
-const BIN_NAME = 'agent-presets'
-
 /**
  * Harness-home directory holding locally authored presets.
  *
@@ -272,51 +256,6 @@ async function compositionProblem(path: string, harnessBase: string): Promise<st
     + unresolvable.map(row => `- ${row.label}: ${row.name}`).join('\n')
 }
 
-/**
- * Why the user patch layer at `path` cannot apply, or undefined when it can.
- * Parsed with the shared patch-list parser, so a layer the profile launcher
- * would accept is never called broken here; every inserted row is then
- * shape-checked and resolved the way composition rows are.
- * @param path - absolute path of the overlay file.
- * @param harnessBase - base URL a row's package name resolves against.
- * @returns one human-readable reason, or undefined when the layer is loadable.
- */
-async function overlayProblem(path: string, harnessBase: string): Promise<string | undefined> {
-  let patches: PatchOptions[] | undefined
-  try {
-    patches = await readPatchListFile(BIN_NAME, path, 'user patch layer')
-  } catch (error) {
-    /* v8 ignore next -- the parser throws Errors for every failure; the fallback keeps a hostile value readable */
-    const full = error instanceof Error ? error.message : String(error)
-    return `the user patch layer ${OVERLAY_FILE} cannot be applied: ${full.replace(/\n[\s\S]*$/, '')}`
-  }
-  // The caller statted this file moments ago; gone in between reads as nothing to apply.
-  /* v8 ignore next -- a deletion between the stat and this read cannot be provoked deterministically */
-  if (patches === undefined) return undefined
-  const presetBase = new URL('.', pathToFileURL(path)).href
-  for (const [index, patch] of patches.entries()) {
-    if (patch.insert === undefined) continue
-    const shape = entryListProblem(patch.insert, `user patch layer entry ${String(index + 1)}`)
-    if (shape !== undefined) return shape
-    const unresolvable = await unresolvableRows(patch.insert, presetBase, harnessBase, `user patch layer entry ${String(index + 1)}`)
-    const [first] = unresolvable
-    if (first !== undefined) {
-      return `the user patch layer ${OVERLAY_FILE} inserts ${first.label}, which names a plugin that cannot be resolved: ${first.name}`
-    }
-  }
-  return undefined
-}
-
-/** One user patch layer found alone in a preset slot: a directory with the layer and no composition. */
-interface OverlayRecord {
-  /** The preset id the layer applies to; the directory's name. */
-  readonly id: string
-  /** Absolute path of the layer file. */
-  readonly path: string
-  /** Trust of the root the layer was found under. */
-  readonly trust: PresetTrust
-}
-
 /**
  * Whether `path` names an existing regular file.
  * @param path - absolute path to test.
@@ -351,75 +290,40 @@ async function isFile(path: string): Promise<boolean> {
  * @returns the root's presets ordered by id.
  */
 export async function scanRoot(root: PresetRoot, harnessBase: string): Promise<AgentPreset[]> {
-  return (await scanRootSlots(root, harnessBase)).presets
-}
-
-/** Sort presets by declared order, then id. */
-function sortPresets(found: AgentPreset[]): AgentPreset[] {
-  // Declared order first so the shipped set reads by capability; everything
-  // else falls back to the id, which keeps authored presets stable.
-  return found.sort((left, right) => {
-    const byOrder = (left.order ?? Number.POSITIVE_INFINITY) - (right.order ?? Number.POSITIVE_INFINITY)
-    return byOrder === 0 ? left.id.localeCompare(right.id) : byOrder
-  })
-}
-
-/**
- * Scan one root for every preset slot: the presets it holds, and the user
- * patch layers found alone in a directory that holds no composition.
- * @param root - the directory and the trust its presets inherit.
- * @param harnessBase - base URL a row's package name resolves against.
- * @returns the root's presets ordered by id, and its lone overlays.
- */
-async function scanRootSlots(
-  root: PresetRoot, harnessBase: string,
-): Promise<{ presets: AgentPreset[]; overlays: OverlayRecord[] }> {
   const dir = resolve(expandHomePath(root.path))
   let children
   try {
     children = await readdir(dir, { withFileTypes: true })
   } catch (error) {
-    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { presets: [], overlays: [] }
+    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []
     throw new Error(`agent-presets: cannot read preset root ${dir}: ${String(error)}`, { cause: error })
   }
-  const presets: AgentPreset[] = []
-  const overlays: OverlayRecord[] = []
+  const found: AgentPreset[] = []
   for (const child of children) {
     if (!child.isDirectory() || !PRESET_ID.test(child.name)) continue
     const directory = join(dir, child.name)
     const path = join(directory, COMPOSITION_FILE)
-    const overlayPath = join(directory, OVERLAY_FILE)
-    const hasOverlay = await isFile(overlayPath)
-    if (!await isFile(path)) {
-      // A layer alone in the slot is not a preset: it belongs to the preset
-      // of the same id another root supplies, and only fails when none does.
-      if (hasOverlay) {
-        overlays.push({ id: child.name, path: overlayPath, trust: root.trust })
-        continue
-      }
-    }
     const broken = await isFile(path)
-      ? await compositionProblem(path, harnessBase) ?? (hasOverlay ? await overlayProblem(overlayPath, harnessBase) : undefined)
+      ? await compositionProblem(path, harnessBase)
       : `the composition file ${COMPOSITION_FILE} is missing — the directory still occupies the id; delete it or restore the file`
     // Display text only, and never fatal: a preset with unreadable metadata
     // still mounts, it just shows its id.
     const metadata = await readPresetMetadata(directory)
-    presets.push({
+    found.push({
       id: child.name, trust: root.trust, path, ...metadata,
-      ...hasOverlay ? { overlayPath } : {},
       ...broken === undefined ? {} : { broken },
     })
   }
-  return { presets: sortPresets(presets), overlays }
+  // Declared order first so the shipped set reads by capability; everything
+  // else falls back to the id, which keeps authored presets stable.
+  return found.sort((left, right) => {
+    const byOrder = (left.order ?? Number.POSITIVE_INFINITY) - (right.order ?? Number.POSITIVE_INFINITY)
+    return byOrder === 0 ? left.id.localeCompare(right.id) : byOrder
+  })
 }
 
 /**
  * Scan every root in precedence order.
- *
- * A user patch layer found alone in a later root's slot attaches to the
- * preset that won the id, unless that preset carries a layer of its own; one
- * whose id no root supplies becomes a broken row, because the directory still
- * occupies the id and a roster that hid it would leave nothing to delete.
  * @param roots - roots in precedence order; an earlier root wins a duplicate id.
  * @param harnessBase - base URL a row's package name resolves against.
  * @returns every discovered preset, first-root-wins per id.
@@ -429,30 +333,11 @@ export async function discoverPresets(
   harnessBase: string,
 ): Promise<AgentPreset[]> {
   const byId = new Map<string, AgentPreset>()
-  const loneOverlays: OverlayRecord[] = []
   for (const root of roots) {
-    const { presets, overlays } = await scanRootSlots(root, harnessBase)
-    for (const preset of presets) {
+    for (const preset of await scanRoot(root, harnessBase)) {
       if (byId.has(preset.id)) continue
       byId.set(preset.id, preset)
     }
-    loneOverlays.push(...overlays)
-  }
-  for (const overlay of loneOverlays) {
-    const preset = byId.get(overlay.id)
-    if (preset === undefined) {
-      byId.set(overlay.id, {
-        id: overlay.id,
-        trust: overlay.trust,
-        path: join(dirname(overlay.path), COMPOSITION_FILE),
-        overlayPath: overlay.path,
-        broken: `the directory holds a user patch layer ${OVERLAY_FILE} but no root supplies a preset "${overlay.id}" for it to apply to — delete the directory or restore the preset`,
-      })
-      continue
-    }
-    if (preset.overlayPath !== undefined) continue
-    const problem = preset.broken === undefined ? await overlayProblem(overlay.path, harnessBase) : undefined
-    byId.set(preset.id, { ...preset, overlayPath: overlay.path, ...problem === undefined ? {} : { broken: problem } })
   }
   return [...byId.values()]
 }

+ 32 - 141
packages/preset/agent-presets/src/index.ts

@@ -21,11 +21,8 @@
  * @module @deepseek-ai/dsh-agent-presets
  */
 
-import { createHash } from 'node:crypto'
-import { readFile, stat } from 'node:fs/promises'
-import { join } from 'node:path'
+import { stat } from 'node:fs/promises'
 import { Context } from '@deepseek-ai/cordis'
-import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
 import { evaluate } from '@deepseek-ai/cordis-plugin-loader'
 import z from '@deepseek-ai/schemastery'
 import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
@@ -40,9 +37,8 @@ import type {} from '@deepseek-ai/dsh-tools'
 import type SettingsService from '@deepseek-ai/dsh-settings'
 import type { SettingsScope } from '@deepseek-ai/dsh-settings'
 import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
-import { readPatchListFile } from '@deepseek-ai/dsh-app-boot/patch-file'
-import { discoverPresets, OVERLAY_FILE, SHIPPED_PRESET_ROOT, USER_PRESET_DIR } from './discovery.ts'
-import { copyComposition, deleteComposition, deleteOverlay, presetExists, readComposition, writableRoot } from './authoring.ts'
+import { discoverPresets, SHIPPED_PRESET_ROOT, USER_PRESET_DIR } from './discovery.ts'
+import { copyComposition, deleteComposition, presetExists, readComposition } from './authoring.ts'
 import { livePresetMounts, mountPreset, serviceForAgent, standingMountFor } from './mount.ts'
 import {
   fileComposition, mountedCompositionRows,
@@ -52,10 +48,8 @@ import type { AgentPreset, Config, PresetRoot } from './preset.ts'
 import { agentPresetProjectionDefinition } from './session.ts'
 export type * from './types.ts'
 export type {
-  AgentPresetComposition, AgentPresetCompositionRow, CompositionRowDisabledBy, CompositionRowEnablement,
-  CompositionRowSource, OverlayFacts,
+  AgentPresetComposition, AgentPresetCompositionRow, CompositionRowEnablement,
 } from './composition-inventory.ts'
-export { overlayFacts } from './composition-inventory.ts'
 
 /** Settings namespace carrying the user's preset-picker preference and chosen default. */
 export const SETTINGS_NAMESPACE = 'agent-presets'
@@ -81,7 +75,7 @@ export const AgentPresetSettingsSchema: z<AgentPresetSettings> = z.object({
   modeSelectionEnabled: z.boolean(),
 })
 
-export { COMPOSITION_FILE, discoverPresets, OVERLAY_FILE, scanRoot, SHIPPED_PRESET_ROOT } from './discovery.ts'
+export { COMPOSITION_FILE, discoverPresets, scanRoot, SHIPPED_PRESET_ROOT } from './discovery.ts'
 export {
   METADATA_FILE, readPresetMetadata, renderPresetMetadata, type PresetMetadata,
 } from './metadata.ts'
@@ -89,7 +83,7 @@ export {
   inactiveRows, leakedServices, livePresetMounts, mountPreset, serviceForAgent, standingMountFor,
   type JoinedPresetMount, type PresetMount,
 } from './mount.ts'
-export { copyComposition, deleteComposition, deleteOverlay, readComposition, writableRoot } from './authoring.ts'
+export { copyComposition, deleteComposition, readComposition, writableRoot } from './authoring.ts'
 export { agentPresetProjectionDefinition } from './session.ts'
 export type { AgentPreset, Config, PresetRoot, PresetTrust } from './preset.ts'
 
@@ -343,14 +337,14 @@ export class AgentPresets extends TypertRemoteService {
       // superseded generation's record precedes its replacement's.
       const mount = livePresetMounts(rootFiber).findLast(candidate => candidate.presetId === preset.id)
       if (mount !== undefined) {
-        found.push({ ...identity, rows: mountedCompositionRows(mount.tree, await readOverlay(preset)) })
+        found.push({ ...identity, rows: mountedCompositionRows(mount.tree) })
         continue
       }
       if (preset.broken !== undefined) {
         found.push({ ...identity, broken: preset.broken, rows: [] })
         continue
       }
-      const read = await fileComposition(preset.path, evaluateExpression, await readOverlay(preset))
+      const read = await fileComposition(preset.path, evaluateExpression)
       found.push('broken' in read
         ? { ...identity, broken: read.broken, rows: [] }
         : { ...identity, rows: read.rows })
@@ -409,24 +403,15 @@ export class AgentPresets extends TypertRemoteService {
    * Standing mounts by preset id, single-flight so two agents racing the
    * first use of one preset share one composition. A settled failure is
    * removed so a later session retries a preset whose file has been fixed; a
-   * settled success serves until the composition FILE or the user patch
-   * layer visibly changes — each generation records its stamp, and a stale
-   * stamp starts the next generation for sessions created afterwards.
-   * Sessions already joined keep the generation they run on; a superseded
-   * one is never disposed while the process lives (reclaimed only by
-   * whole-tree teardown), so editing files is bounded by how often
-   * compositions change, not by session count.
+   * settled success serves until the composition FILE visibly changes — each
+   * generation records its file stamp, and a stale stamp starts the next
+   * generation for sessions created afterwards. Sessions already joined keep
+   * the generation they run on; a superseded one is never disposed while the
+   * process lives (reclaimed only by whole-tree teardown), so editing files
+   * is bounded by how often compositions change, not by session count.
    */
   private readonly standing = new Map<string, Promise<StandingMount>>()
 
-  /**
-   * Superseded generations by preset id and stamp key. A layer edited back
-   * to an earlier content — a row switched off and on again — returns to the
-   * generation that content already composed instead of composing a third,
-   * which keeps a toggle from stacking live subtrees.
-   */
-  private readonly retired = new Map<string, Map<string, StandingMount>>()
-
   /**
    * Parent bindings of the agents this roster composed, keyed by the agent's
    * scope key. The binding is dsh-scope's only re-link capability; holding it
@@ -539,33 +524,6 @@ export class AgentPresets extends TypertRemoteService {
     return await readComposition(await this.resolve(id))
   }
 
-  /**
-   * Where one preset's user patch layer is, or would be written: the layer
-   * discovery attached, else the writable root's slot of the same id — beside
-   * the composition for a locally authored preset, alone in the slot for a
-   * shipped one. The file need not exist yet.
-   * @param id - the preset id.
-   * @returns the absolute path of the layer file.
-   * @throws when the preset is unknown, or it has no layer and the
-   * deployment configures no writable root.
-   */
-  async overlayPathFor(id: string): Promise<string> {
-    const preset = await this.resolve(id)
-    return preset.overlayPath ?? join(writableRoot(this.resolvedRoots, id), id, OVERLAY_FILE)
-  }
-
-  /**
-   * Delete one preset's user patch layer, so the next generation composes the
-   * preset exactly as its root supplies it. Sessions already joined keep the
-   * generation they run on.
-   * @param id - the preset id.
-   * @returns true when a layer was removed; false when the preset had none.
-   * @throws when the preset is unknown or its layer lies outside the writable root.
-   */
-  async removeOverlay(id: string): Promise<boolean> {
-    return await deleteOverlay(this.resolvedRoots, await this.resolve(id))
-  }
-
   /**
    * One preset's composition text with the roster row it belongs to.
    * @param agentPreset - the preset id.
@@ -812,13 +770,12 @@ export class AgentPresets extends TypertRemoteService {
     const pending = this.standing.get(preset.id)
     if (pending !== undefined) {
       const mounted = await pending
-      // Files are the only composition editor (authoring is copy/delete, the
-      // user patch layer is a file), so the stamp is what notices an edit: a
-      // changed file starts the next generation here, for this and later
-      // sessions. An unreadable stamp serves the current generation — a
-      // mount must survive its file disappearing, and failing the session
-      // over a stat would not.
-      const current = await compositionStamp(preset)
+      // Files are the only composition editor (authoring is copy/delete), so
+      // the stamp is what notices an edit: a changed file starts the next
+      // generation here, for this and later sessions. An unreadable stamp
+      // serves the current generation — a mount must survive its file
+      // disappearing, and failing the session over a stat would not.
+      const current = await compositionStamp(preset.path)
       if (current === undefined || sameStamp(mounted.stamp, current)) return mounted
       // TODO: reclaim the superseded generation once the last agent joined to
       // it is gone. The subtree is not inert — `dsh-skill-filesystem` watches its
@@ -828,25 +785,17 @@ export class AgentPresets extends TypertRemoteService {
       // decremented when the agent's scope key dies.
       // Guarded delete: a caller that raced this one may have already started
       // the next generation, and dropping THAT pointer would fork a third.
-      if (this.standing.get(preset.id) === pending) {
-        this.standing.delete(preset.id)
-        this.retire(preset.id, mounted)
-      }
+      if (this.standing.get(preset.id) === pending) this.standing.delete(preset.id)
       return this.ensureStanding(preset)
     }
     const created = (async (): Promise<StandingMount> => {
-      // Stamped before the file is read: an edit racing the mount makes the
-      // stamp stale rather than silently current, so the next session
-      // refreshes instead of trusting a composition older than its stamp.
-      const stamp = await compositionStamp(preset)
-      const reused = stamp === undefined ? undefined : this.retired.get(preset.id)?.get(stampKey(stamp))
-      if (reused !== undefined) {
-        this.retired.get(preset.id)?.delete(stampKey(reused.stamp))
-        return reused
-      }
       const key: ScopeKey = { agentPreset: preset.id }
       const scope = createScope(this.selfCtx, key)
       try {
+        // Stamped before the file is read: an edit racing the mount makes the
+        // stamp stale rather than silently current, so the next session
+        // refreshes instead of trusting a composition older than its stamp.
+        const stamp = await compositionStamp(preset.path)
         if (stamp === undefined) {
           const reason = `composition file is unreadable: ${preset.path}`
           throw new RemoteError(
@@ -866,42 +815,21 @@ export class AgentPresets extends TypertRemoteService {
     this.standing.set(preset.id, created)
     return created
   }
-
-  /** File a superseded generation under its stamp, for a later edit that restores its content. */
-  private retire(presetId: string, mount: StandingMount): void {
-    let generations = this.retired.get(presetId)
-    if (generations === undefined) {
-      generations = new Map()
-      this.retired.set(presetId, generations)
-    }
-    generations.set(stampKey(mount.stamp), mount)
-  }
 }
 
-/**
- * The composition identity one standing generation was mounted from: the
- * composition file's stat, and the user patch layer's content — content
- * rather than stat, so a layer edited back to what an earlier generation
- * composed identifies that generation.
- */
+/** The composition file identity one standing generation was mounted from. */
 interface CompositionStamp {
   /** Modification time in milliseconds, as `stat` reports it. */
   readonly mtimeMs: number
   /** File size in bytes, the tiebreak for edits within one mtime tick. */
   readonly size: number
-  /** Digest of the user patch layer's text; empty when the preset has none. */
-  readonly overlay: string
 }
 
-/**
- * Read one preset's stamp, or undefined when its composition cannot be statted.
- * A layer that cannot be read stamps as absent: the mount then composes the
- * bare preset, which is also what an unreadable layer applies.
- */
-async function compositionStamp(preset: AgentPreset): Promise<CompositionStamp | undefined> {
+/** Read one composition file's stamp, or undefined when it cannot be statted. */
+async function compositionStamp(path: string): Promise<CompositionStamp | undefined> {
   try {
-    const { mtimeMs, size } = await stat(preset.path)
-    return { mtimeMs, size, overlay: await overlayDigest(preset.overlayPath) }
+    const { mtimeMs, size } = await stat(path)
+    return { mtimeMs, size }
   } catch {
     // Deleted, replaced by an unreadable entry, or otherwise unstattable all
     // mean the same to the caller: the file offers no identity to compare.
@@ -909,46 +837,9 @@ async function compositionStamp(preset: AgentPreset): Promise<CompositionStamp |
   }
 }
 
-/** Digest of the layer file's text, empty when the preset has none or it cannot be read. */
-async function overlayDigest(overlayPath: string | undefined): Promise<string> {
-  if (overlayPath === undefined) return ''
-  let text: string
-  try {
-    text = await readFile(overlayPath, 'utf8')
-  } catch {
-    // A layer deleted since discovery is a layer that applies nothing.
-    /* v8 ignore next -- a deletion between the layer read and this digest cannot be provoked deterministically */
-    return ''
-  }
-  return createHash('sha1').update(text).digest('hex')
-}
-
-/** Whether two stamps name the same composition state. */
+/** Whether two stamps name the same file state. */
 function sameStamp(a: CompositionStamp, b: CompositionStamp): boolean {
-  return a.mtimeMs === b.mtimeMs && a.size === b.size && a.overlay === b.overlay
-}
-
-/** One stamp as a map key. */
-function stampKey(stamp: CompositionStamp): string {
-  return `${String(stamp.mtimeMs)}:${String(stamp.size)}:${stamp.overlay}`
-}
-
-/**
- * The preset's user patch layer as parsed, or empty when it has none or the
- * layer cannot be read — the inventory then reports the rows the bare
- * composition holds, which is also what such a layer applies.
- */
-async function readOverlay(preset: AgentPreset): Promise<PatchOptions[]> {
-  if (preset.overlayPath === undefined) return []
-  try {
-    // The inventory re-discovers before it reads; a layer deleted between the two is a race no test can provoke.
-    /* v8 ignore next */
-    return await readPatchListFile('agent-presets', preset.overlayPath, 'user patch layer') ?? []
-  } catch {
-    // Discovery already reported an unparsable layer as the preset's health;
-    // the inventory answers with the composition alone.
-    return []
-  }
+  return a.mtimeMs === b.mtimeMs && a.size === b.size
 }
 
 /** One preset's standing composition. */

+ 3 - 17
packages/preset/agent-presets/src/mount.ts

@@ -20,7 +20,6 @@ import { Include } from '@deepseek-ai/cordis-plugin-include'
 import type { EntryTree } from '@deepseek-ai/cordis-plugin-loader'
 import { scopeOf, scopeParentOf, type ScopeKey } from '@deepseek-ai/dsh-scope'
 import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
-import { readPatchListFile } from '@deepseek-ai/dsh-app-boot/patch-file'
 import type { AgentPreset } from './preset.ts'
 import { classifyRowSpecifier } from './specifier.ts'
 
@@ -360,19 +359,12 @@ function mountDetail(error: unknown): string {
 /**
  * Mount `preset` under `agentCtx` and return only once every row is usable.
  *
- * The preset's user patch layer, when it has one, is read here and handed to
- * the include as its runtime patches, so the layer is applied over the
- * composition with the Loader's own patch semantics — a `disabled: true`
- * switches a row off, a `config` replaces the row's, an `insert` adds rows —
- * and re-read at every mount, which is what makes a generation follow the
- * layer file.
- *
  * The subtree is owned by `agentCtx`'s fiber, so it unwinds with the agent and
  * the caller receives no disposer. A rejection leaves nothing mounted.
  * @param agentCtx - the agent's scope context, from the agent factory's `setup`.
  * @param preset - the resolved preset to compose the agent from.
- * @throws when `agentCtx` carries no scope, the user patch layer cannot be
- * read, a row is unusable, or a row published a service into the root realm.
+ * @throws when `agentCtx` carries no scope, a row is unusable, or a row
+ * published a service into the root realm.
  */
 export async function mountPreset(agentCtx: Context, preset: AgentPreset): Promise<void> {
   const scope = scopeOf(agentCtx)
@@ -382,13 +374,7 @@ export async function mountPreset(agentCtx: Context, preset: AgentPreset): Promi
       + 'its registrations would apply to every agent in the process',
     )
   }
-  const patches = preset.overlayPath === undefined
-    ? undefined
-    : await readPatchListFile('agent-presets', preset.overlayPath, 'user patch layer')
-  const config: Include.Config = {
-    path: pathToFileURL(preset.path).href,
-    ...patches === undefined ? {} : { patches },
-  }
+  const config: Include.Config = { path: pathToFileURL(preset.path).href }
   // Captured before the subtree exists: the standing scope context still
   // carries the host composition's base, which is inside the installed
   // harness and is therefore where a row's package name has to resolve from.

+ 0 - 8
packages/preset/agent-presets/src/preset.ts

@@ -25,14 +25,6 @@ export interface AgentPreset {
   readonly trust: PresetTrust
   /** Absolute path of the preset's agent composition file. */
   readonly path: string
-  /**
-   * Absolute path of the preset's user patch layer (`cordis.patch.yml`), when
-   * one exists: beside the composition for a locally authored preset, or in
-   * the user root's directory of the same id for a shipped one. Applied over
-   * the composition at every mount, so a person can disable or add a row
-   * without editing the file the deployment ships.
-   */
-  readonly overlayPath?: string
   /** Display name from the preset's own metadata; absent falls back to {@link id}. */
   readonly name?: string
   /** One sentence on what this preset is for, when it published one. */

+ 19 - 23
packages/preset/agent-presets/tests/composition-inventory.spec.ts

@@ -109,21 +109,20 @@ describe('fileComposition', () => {
 
     expect(await fileComposition(path, refuseExpression)).toEqual({
       rows: [
-        { entryId: 'alpha', moduleName: 'pkg-alpha', enabled: true, source: 'preset' },
-        { entryId: null, moduleName: 'pkg-anonymous', enabled: true, source: 'preset' },
-        { entryId: 'off', moduleName: 'pkg-off', enabled: false, source: 'preset', disabledBy: 'composition' },
+        { entryId: 'alpha', moduleName: 'pkg-alpha', enabled: true },
+        { entryId: null, moduleName: 'pkg-anonymous', enabled: true },
+        { entryId: 'off', moduleName: 'pkg-off', enabled: false },
         {
           entryId: 'cond',
           moduleName: 'pkg-cond',
           enabled: 'conditional',
-          source: 'preset',
           condition: 'process.platform === \'win32\'',
         },
-        { entryId: 'child', moduleName: 'pkg-child', enabled: true, source: 'preset' },
-        { entryId: 'child-off', moduleName: 'pkg-child-off', enabled: false, source: 'preset', disabledBy: 'composition' },
-        { entryId: 'buried', moduleName: 'pkg-buried', enabled: false, source: 'preset', disabledBy: 'composition' },
-        { entryId: 'maybe', moduleName: 'pkg-maybe', enabled: 'conditional', source: 'preset' },
-        { entryId: 'certainly-off', moduleName: 'pkg-certainly-off', enabled: false, source: 'preset', disabledBy: 'composition' },
+        { entryId: 'child', moduleName: 'pkg-child', enabled: true },
+        { entryId: 'child-off', moduleName: 'pkg-child-off', enabled: false },
+        { entryId: 'buried', moduleName: 'pkg-buried', enabled: false },
+        { entryId: 'maybe', moduleName: 'pkg-maybe', enabled: 'conditional' },
+        { entryId: 'certainly-off', moduleName: 'pkg-certainly-off', enabled: false },
       ],
     })
   })
@@ -143,8 +142,8 @@ describe('fileComposition', () => {
 
     expect(await fileComposition(path, evaluateExpression)).toEqual({
       rows: [
-        { entryId: 'off', moduleName: 'pkg-off', enabled: false, condition: '1 === 1', source: 'preset', disabledBy: 'composition' },
-        { entryId: 'on', moduleName: 'pkg-on', enabled: true, condition: '1 === 2', source: 'preset' },
+        { entryId: 'off', moduleName: 'pkg-off', enabled: false, condition: '1 === 1' },
+        { entryId: 'on', moduleName: 'pkg-on', enabled: true, condition: '1 === 2' },
       ],
     })
   })
@@ -193,14 +192,13 @@ describe('mountedCompositionRows', () => {
     const byId = new Map(rows.map(row => [row.entryId, row]))
     expect(rows).toHaveLength(3)
     expect(byId.get(activeId)).toEqual(
-      { entryId: activeId, moduleName: 'cordis:active', enabled: true, fiberState: FiberState.ACTIVE, source: 'preset' })
+      { entryId: activeId, moduleName: 'cordis:active', enabled: true, fiberState: FiberState.ACTIVE })
     expect(byId.get(disabledId)).toEqual(
-      { entryId: disabledId, moduleName: 'cordis:active', enabled: false, source: 'preset', disabledBy: 'composition' })
+      { entryId: disabledId, moduleName: 'cordis:active', enabled: false })
     expect(byId.get(evaluatedId)).toEqual({
       entryId: evaluatedId,
       moduleName: 'cordis:active',
       enabled: true,
-      source: 'preset',
       condition: 'false',
       fiberState: FiberState.ACTIVE,
     })
@@ -234,15 +232,15 @@ describe('AgentPresets.compositionInventory', () => {
         id: 'minimal',
         trust: 'system',
         isDefault: true,
-        rows: [{ entryId: 'beta', moduleName: '../../plugins/contribute.js', enabled: true, source: 'preset' }],
+        rows: [{ entryId: 'beta', moduleName: '../../plugins/contribute.js', enabled: true }],
       },
       {
         id: 'standard',
         trust: 'system',
         isDefault: false,
         rows: [
-          { entryId: 'alpha', moduleName: '../../plugins/contribute.js', enabled: true, source: 'preset' },
-          { entryId: 'alpha-extra', moduleName: '../../plugins/contribute.js', enabled: false, source: 'preset', disabledBy: 'composition' },
+          { entryId: 'alpha', moduleName: '../../plugins/contribute.js', enabled: true },
+          { entryId: 'alpha-extra', moduleName: '../../plugins/contribute.js', enabled: false },
         ],
       },
       {
@@ -251,16 +249,15 @@ describe('AgentPresets.compositionInventory', () => {
         name: '我的模式',
         isDefault: false,
         rows: [
-          { entryId: 'prompt', moduleName: '@deepseek-ai/dsh-system-prompt', enabled: true, source: 'preset' },
+          { entryId: 'prompt', moduleName: '@deepseek-ai/dsh-system-prompt', enabled: true },
           // The platform-gate shape: the service evaluates it with the
           // Loader's own scope, so the file answer matches a mount's.
-          { entryId: 'gated', moduleName: '@deepseek-ai/dsh-system-prompt', enabled: false, condition: '1 === 1', source: 'preset', disabledBy: 'composition' },
+          { entryId: 'gated', moduleName: '@deepseek-ai/dsh-system-prompt', enabled: false, condition: '1 === 1' },
           // An expression the evaluator refuses stays a mount's decision.
           {
             entryId: 'undecidable',
             moduleName: '@deepseek-ai/dsh-system-prompt',
             enabled: 'conditional',
-            source: 'preset',
             condition: 'nothing.here',
           },
         ],
@@ -290,10 +287,9 @@ describe('AgentPresets.compositionInventory', () => {
         entryId: 'alpha',
         moduleName: '../../plugins/contribute.js',
         enabled: true,
-        source: 'preset',
         fiberState: FiberState.ACTIVE,
       },
-      { entryId: 'alpha-extra', moduleName: '../../plugins/contribute.js', enabled: false, source: 'preset', disabledBy: 'composition' },
+      { entryId: 'alpha-extra', moduleName: '../../plugins/contribute.js', enabled: false },
     ])
   })
 
@@ -324,7 +320,7 @@ describe('AgentPresets.compositionInventory', () => {
     expect(volatile).toMatchObject({ id: 'volatile', trust: 'user', isDefault: true })
     expect(volatile?.broken).toBeUndefined()
     expect(volatile?.rows).toEqual([
-      { entryId: 'only', moduleName: plugin, enabled: true, fiberState: FiberState.ACTIVE, source: 'preset' },
+      { entryId: 'only', moduleName: plugin, enabled: true, fiberState: FiberState.ACTIVE },
     ])
   })
 

+ 2 - 2
packages/preset/agent-presets/tests/mount.spec.ts

@@ -752,7 +752,7 @@ describe('editing a composition file', () => {
       standing: Map<string, Promise<{
         key: unknown
         scope: unknown
-        stamp: { mtimeMs: number; size: number; overlay: string }
+        stamp: { mtimeMs: number; size: number }
       }>>
       ensureStanding(current: typeof preset): Promise<unknown>
     }
@@ -760,7 +760,7 @@ describe('editing a composition file', () => {
     const stale = await stalePromise
     await writeFile(path, rowFor('afterwards'))
     const { mtimeMs, size } = await stat(path)
-    const newer = { ...stale, stamp: { mtimeMs, size, overlay: '' } }
+    const newer = { ...stale, stamp: { mtimeMs, size } }
     const newerPromise = Promise.resolve(newer)
 
     // `await pending` yields before the guarded delete, letting the winning

+ 0 - 370
packages/preset/agent-presets/tests/overlay.spec.ts

@@ -1,370 +0,0 @@
-/**
- * The per-preset user patch layer: a `cordis.patch.yml` beside a locally
- * authored composition, or alone in the user root's slot of a shipped
- * preset's id. Discovery attaches it and judges it, the mount applies it,
- * generations follow its content, the inventory reports its decisions, a
- * copy carries it, and authoring can remove it.
- */
-
-import { existsSync } from 'node:fs'
-import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
-import { tmpdir } from 'node:os'
-import { dirname, join } from 'node:path'
-import { fileURLToPath, pathToFileURL } from 'node:url'
-import { Context, FiberState } from '@deepseek-ai/cordis'
-import Loader from '@deepseek-ai/cordis-plugin-loader'
-import Include from '@deepseek-ai/cordis-plugin-include'
-import Group from '@deepseek-ai/cordis-plugin-group'
-import LlmRuntime from '@deepseek-ai/dsh-llm'
-import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
-import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
-import ToolRuntime from '@deepseek-ai/dsh-tools'
-import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
-import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
-import AgentLoop from '@deepseek-ai/dsh-agent-loop'
-import { afterEach, describe, expect, it } from 'vitest'
-import AgentPresets, {
-  COMPOSITION_FILE, discoverPresets, livePresetMounts, overlayFacts, OVERLAY_FILE, scanRoot,
-} from '@deepseek-ai/dsh-agent-presets'
-import { fileComposition } from '../src/composition-inventory.ts'
-import type { Config } from '@deepseek-ai/dsh-agent-presets'
-
-const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
-const HARNESS = new URL('.', import.meta.url).href
-const SYSTEM = { path: join(FIXTURES, 'system'), trust: 'system' as const }
-const CONTRIBUTE = join(FIXTURES, 'plugins', 'contribute.js')
-
-const contexts: Context[] = []
-
-afterEach(async () => {
-  await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
-})
-
-/** A fresh user root, optionally holding one slot with the given files. */
-async function userRoot(slots: Record<string, Record<string, string>> = {}): Promise<string> {
-  const root = await mkdtemp(join(tmpdir(), 'dsh-overlay-root-'))
-  for (const [id, files] of Object.entries(slots)) {
-    await mkdir(join(root, id), { recursive: true })
-    for (const [file, content] of Object.entries(files)) await writeFile(join(root, id, file), content)
-  }
-  return root
-}
-
-/** An overlay that switches `alpha` off and inserts one row registering `tool`. */
-function overlayInserting(tool: string): string {
-  return [
-    '- id: alpha',
-    '  disabled: true',
-    '- insert:',
-    `    - id: user-${tool}`,
-    `      name: ${CONTRIBUTE}`,
-    '      config:',
-    `        tool: ${tool}`,
-    '',
-  ].join('\n')
-}
-
-async function harness(roster: Config): Promise<Context> {
-  const ctx = new Context()
-  contexts.push(ctx)
-  ctx.baseUrl = pathToFileURL(FIXTURES).href + '/'
-  await ctx.plugin(Loader)
-  ctx.loader.builtins.include = Include
-  ctx.loader.builtins.group = Group
-  await ctx.plugin(LlmRuntime)
-  await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt)
-  await ctx.plugin(ToolRuntime)
-  await ctx.plugin(AgentRegistry)
-  await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(AgentLoop, { agents: [] })
-  await ctx.plugin(AgentPresets, roster)
-  return ctx
-}
-
-async function agentOn(ctx: Context, id: string, presetId: string): Promise<Agent> {
-  const handle = await ctx.agents.create({
-    sessionId: SessionId(id),
-    setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, presetId),
-  })
-  return handle.agent
-}
-
-const toolNames = (ctx: Context, agent: Agent): string[] =>
-  ctx.tools.schemas(agent).map(schema => schema.name).sort()
-
-const rosterOver = (root: string, extra: Partial<Config> = {}): Config => ({
-  default: 'standard',
-  roots: [SYSTEM, { path: root, trust: 'user' }],
-  includeShippedRoot: false,
-  includeUserRoot: false,
-  ...extra,
-})
-
-describe('discovery', () => {
-  it('attaches a lone layer in a later root to the preset that won the id', async () => {
-    const root = await userRoot({ standard: { [OVERLAY_FILE]: overlayInserting('gamma') } })
-
-    const presets = await discoverPresets([SYSTEM, { path: root, trust: 'user' }], HARNESS)
-
-    const standard = presets.find(preset => preset.id === 'standard')
-    expect(standard).toMatchObject({ trust: 'system', overlayPath: join(root, 'standard', OVERLAY_FILE) })
-    expect(standard?.broken).toBeUndefined()
-    // The lone layer is not a preset of its own root.
-    expect(await scanRoot({ path: root, trust: 'user' }, HARNESS)).toEqual([])
-  })
-
-  it('reads a layer beside a locally authored composition as that preset\'s own', async () => {
-    const root = await userRoot({
-      mine: {
-        [COMPOSITION_FILE]: `- id: only\n  name: ${CONTRIBUTE}\n  config:\n    tool: only\n`,
-        [OVERLAY_FILE]: '- id: only\n  disabled: true\n',
-      },
-    })
-    const other = await userRoot({ mine: { [OVERLAY_FILE]: '- id: only\n  config: {}\n' } })
-
-    const presets = await discoverPresets([{ path: root, trust: 'user' }, { path: other, trust: 'user' }], HARNESS)
-
-    // The preset's own layer wins; a later root's lone layer for the same id is ignored.
-    expect(presets).toEqual([expect.objectContaining({ id: 'mine', overlayPath: join(root, 'mine', OVERLAY_FILE) })])
-  })
-
-  it('reports a lone layer whose id no root supplies as a broken slot', async () => {
-    const root = await userRoot({ ghost: { [OVERLAY_FILE]: '[]\n' } })
-
-    const presets = await discoverPresets([SYSTEM, { path: root, trust: 'user' }], HARNESS)
-
-    expect(presets.find(preset => preset.id === 'ghost')).toMatchObject({
-      trust: 'user',
-      path: join(root, 'ghost', COMPOSITION_FILE),
-      overlayPath: join(root, 'ghost', OVERLAY_FILE),
-      broken: expect.stringContaining('no root supplies a preset "ghost"') as string,
-    })
-    // A slot with neither file is still the missing-composition case.
-    const empty = await userRoot({ hollow: { 'notes.txt': '' } })
-    const hollow = (await discoverPresets([{ path: empty, trust: 'user' }], HARNESS)).find(preset => preset.id === 'hollow')
-    expect(hollow?.broken).toContain(`${COMPOSITION_FILE} is missing`)
-  })
-
-  it('judges the layer with the composition: unparsable, malformed, or unresolvable inserts break the preset', async () => {
-    const unparsable = await userRoot({ standard: { [OVERLAY_FILE]: 'a: [\n' } })
-    const malformed = await userRoot({ standard: { [OVERLAY_FILE]: '- insert:\n    - config: {}\n' } })
-    const unresolvable = await userRoot({ standard: { [OVERLAY_FILE]: '- insert:\n    - id: x\n      name: ./nowhere.js\n' } })
-    const own = await userRoot({
-      mine: { [COMPOSITION_FILE]: '[]\n', [OVERLAY_FILE]: 'a: 1\n' },
-    })
-
-    const [a, b, c, d] = await Promise.all([unparsable, malformed, unresolvable].map(
-      async root => (await discoverPresets([SYSTEM, { path: root, trust: 'user' }], HARNESS)).find(preset => preset.id === 'standard'),
-    ).concat([
-      (async () => (await discoverPresets([{ path: own, trust: 'user' }], HARNESS)).find(preset => preset.id === 'mine'))(),
-    ]))
-
-    expect(a?.broken).toContain(`the user patch layer ${OVERLAY_FILE} cannot be applied`)
-    expect(b?.broken).toContain('user patch layer entry 1 row 1 names no plugin')
-    expect(c?.broken).toContain('inserts row "x", which names a plugin that cannot be resolved')
-    expect(d?.broken).toContain('must be a top-level YAML array')
-  })
-
-  it('leaves a broken composition\'s verdict in place rather than judging its layer', async () => {
-    const shipped = await userRoot({ bad: { [COMPOSITION_FILE]: '- id: x\n  name: ./nowhere.js\n' } })
-    const root = await userRoot({ bad: { [OVERLAY_FILE]: 'a: [\n' } })
-
-    const presets = await discoverPresets([{ path: shipped, trust: 'system' }, { path: root, trust: 'user' }], HARNESS)
-
-    const broken = presets.find(preset => preset.id === 'bad')
-    expect(broken?.overlayPath).toBe(join(root, 'bad', OVERLAY_FILE))
-    expect(broken?.broken).toContain('cannot be resolved')
-    expect(broken?.broken).not.toContain('user patch layer')
-  })
-})
-
-describe('mounting', () => {
-  it('applies the layer over the composition: a row switched off, a row added', async () => {
-    const root = await userRoot({ standard: { [OVERLAY_FILE]: overlayInserting('gamma') } })
-    const ctx = await harness(rosterOver(root))
-
-    const agent = await agentOn(ctx, 'sess-overlaid', 'standard')
-
-    expect(toolNames(ctx, agent)).toEqual(['gamma'])
-  })
-
-  it('starts a new generation when the layer changes and returns to one whose content comes back', async () => {
-    const root = await userRoot({ standard: { [OVERLAY_FILE]: overlayInserting('gamma') } })
-    const ctx = await harness(rosterOver(root))
-    const first = await agentOn(ctx, 'sess-gen-1', 'standard')
-    const firstMount = livePresetMounts().findLast(mount => mount.presetId === 'standard')
-
-    await writeFile(join(root, 'standard', OVERLAY_FILE), overlayInserting('delta'))
-    const second = await agentOn(ctx, 'sess-gen-2', 'standard')
-    const secondMount = livePresetMounts().findLast(mount => mount.presetId === 'standard')
-
-    expect(toolNames(ctx, first)).toEqual(['gamma'])
-    expect(toolNames(ctx, second)).toEqual(['delta'])
-    expect(secondMount).not.toBe(firstMount)
-    expect(livePresetMounts().filter(mount => mount.presetId === 'standard')).toHaveLength(2)
-
-    // The same content again: the retired generation serves, no third mount.
-    await writeFile(join(root, 'standard', OVERLAY_FILE), overlayInserting('gamma'))
-    const third = await agentOn(ctx, 'sess-gen-3', 'standard')
-    expect(toolNames(ctx, third)).toEqual(['gamma'])
-    expect(livePresetMounts().filter(mount => mount.presetId === 'standard')).toHaveLength(2)
-    expect(await ctx.agentPresets.standingKeyFor('standard')).toBe(firstMount?.key)
-  })
-
-  it('composes the bare preset when the layer disappears between discovery and mount', async () => {
-    const root = await userRoot({ standard: { [OVERLAY_FILE]: overlayInserting('gamma') } })
-    const ctx = await harness(rosterOver(root))
-    const preset = await ctx.agentPresets.resolve('standard')
-    await writeFile(join(root, 'standard', OVERLAY_FILE), '[]\n')
-
-    const racer = ctx.agentPresets as unknown as { ensureStanding(current: typeof preset): Promise<unknown> }
-    await racer.ensureStanding(preset)
-    const agent = await agentOn(ctx, 'sess-bare', 'standard')
-
-    expect(toolNames(ctx, agent)).toEqual(['alpha'])
-  })
-})
-
-describe('inventory', () => {
-  it('reports the layer\'s decisions from the file and from a standing mount', async () => {
-    const root = await userRoot({ standard: { [OVERLAY_FILE]: overlayInserting('gamma') } })
-    const ctx = await harness(rosterOver(root))
-
-    const fromFile = (await ctx.agentPresets.compositionInventory()).find(composition => composition.id === 'standard')
-    expect(fromFile?.rows).toEqual([
-      { entryId: 'alpha', moduleName: '../../plugins/contribute.js', enabled: false, source: 'preset', disabledBy: 'user' },
-      { entryId: 'alpha-extra', moduleName: '../../plugins/contribute.js', enabled: false, source: 'preset', disabledBy: 'composition' },
-      // An absolute path in a user row mounts as a file URL, like a `./` path anchored beside the file.
-      { entryId: 'user-gamma', moduleName: pathToFileURL(CONTRIBUTE).href, enabled: true, source: 'user' },
-    ])
-
-    await agentOn(ctx, 'sess-inventory', 'standard')
-    const mounted = (await ctx.agentPresets.compositionInventory()).find(composition => composition.id === 'standard')
-    expect(mounted?.rows).toEqual([
-      { entryId: 'alpha', moduleName: '../../plugins/contribute.js', enabled: false, source: 'preset', disabledBy: 'user' },
-      { entryId: 'alpha-extra', moduleName: '../../plugins/contribute.js', enabled: false, source: 'preset', disabledBy: 'composition' },
-      { entryId: 'user-gamma', moduleName: pathToFileURL(CONTRIBUTE).href, enabled: true, source: 'user', fiberState: FiberState.ACTIVE },
-    ])
-  })
-
-  it('answers from the composition alone when the layer disappears before the inventory reads it', async () => {
-    const root = await userRoot({ standard: { [OVERLAY_FILE]: overlayInserting('gamma') } })
-    const ctx = await harness(rosterOver(root))
-    await rm(join(root, 'standard', OVERLAY_FILE))
-
-    const fromFile = (await ctx.agentPresets.compositionInventory()).find(composition => composition.id === 'standard')
-
-    expect(fromFile?.rows.map(row => [row.entryId, row.source])).toEqual([
-      ['alpha', 'preset'],
-      ['alpha-extra', 'preset'],
-    ])
-  })
-
-  it('answers from the composition alone when the layer stopped reading', async () => {
-    const root = await userRoot({ standard: { [OVERLAY_FILE]: overlayInserting('gamma') } })
-    const ctx = await harness(rosterOver(root))
-    await agentOn(ctx, 'sess-inventory-broken-layer', 'standard')
-    await writeFile(join(root, 'standard', OVERLAY_FILE), 'a: [\n')
-
-    const mounted = (await ctx.agentPresets.compositionInventory()).find(composition => composition.id === 'standard')
-
-    expect(mounted?.rows.map(row => [row.entryId, row.source, row.disabledBy])).toEqual([
-      ['alpha', 'preset', 'composition'],
-      ['alpha-extra', 'preset', 'composition'],
-      ['user-gamma', 'preset', undefined],
-    ])
-  })
-
-  it('reads a layer\'s inserted and disabled ids, ignoring !!js gates', () => {
-    expect(overlayFacts([
-      { id: 'a', disabled: true },
-      { id: 'b', disabled: { __jsExpr: 'true' } as unknown as boolean },
-      { id: 'c', config: {} },
-      { insert: [{ id: 'd', name: 'x' }, { id: 'g', name: 'cordis:group', group: true, config: [{ id: 'e', name: 'y' }] }] },
-      // A group inserted without members contributes its own id alone; an anonymous row contributes nothing.
-      { insert: [{ id: 'h', name: 'cordis:group', group: true }, { name: 'anonymous' } as never] },
-    ])).toEqual({ inserted: new Set(['d', 'g', 'e', 'h']), disabled: new Set(['a']) })
-  })
-
-  it('reads a composition file under a layer that targets a row it does not have', async () => {
-    // A patch matching no row is the Loader's warning at mount time; the
-    // inventory applies the rest and stays silent about it.
-    const dir = await mkdtemp(join(tmpdir(), 'dsh-preset-inventory-'))
-    const path = join(dir, COMPOSITION_FILE)
-    await writeFile(path, '- id: alpha\n  name: ../../plugins/contribute.js\n')
-    expect(await fileComposition(path, () => false, [{ id: 'missing', disabled: true }])).toEqual({
-      rows: [{ entryId: 'alpha', moduleName: '../../plugins/contribute.js', enabled: true, source: 'preset' }],
-    })
-  })
-})
-
-describe('authoring', () => {
-  it('names the layer path for shipped and locally authored presets', async () => {
-    const root = await userRoot({
-      mine: { [COMPOSITION_FILE]: `- id: only\n  name: ${CONTRIBUTE}\n  config:\n    tool: only\n` },
-    })
-    const ctx = await harness(rosterOver(root))
-
-    expect(await ctx.agentPresets.overlayPathFor('standard')).toBe(join(root, 'standard', OVERLAY_FILE))
-    expect(await ctx.agentPresets.overlayPathFor('mine')).toBe(join(root, 'mine', OVERLAY_FILE))
-    await writeFile(join(root, 'mine', OVERLAY_FILE), '[]\n')
-    expect(await ctx.agentPresets.overlayPathFor('mine')).toBe(join(root, 'mine', OVERLAY_FILE))
-  })
-
-  it('refuses a layer path when the deployment configures no writable root', async () => {
-    const ctx = await harness({ default: 'standard', roots: [SYSTEM], includeShippedRoot: false, includeUserRoot: false })
-
-    await expect(ctx.agentPresets.overlayPathFor('standard')).rejects.toMatchObject({ code: 'agent-preset/read-only' })
-  })
-
-  it('copies a shipped preset\'s layer in beside the new composition', async () => {
-    // A shipped preset whose rows resolve from anywhere, so its copy mounts.
-    const shipped = await userRoot({
-      base: { [COMPOSITION_FILE]: `- id: alpha\n  name: ${CONTRIBUTE}\n  config:\n    tool: alpha\n` },
-    })
-    const root = await userRoot({ base: { [OVERLAY_FILE]: overlayInserting('gamma') } })
-    const ctx = await harness({
-      default: 'base',
-      roots: [{ path: shipped, trust: 'system' }, { path: root, trust: 'user' }],
-      includeShippedRoot: false,
-      includeUserRoot: false,
-    })
-
-    await ctx.agentPresets.copy('base', 'research', 'Research')
-
-    expect(await readFile(join(root, 'research', OVERLAY_FILE), 'utf8')).toBe(overlayInserting('gamma'))
-    const research = await ctx.agentPresets.resolve('research')
-    expect(research.overlayPath).toBe(join(root, 'research', OVERLAY_FILE))
-    const agent = await agentOn(ctx, 'sess-copied', 'research')
-    expect(toolNames(ctx, agent)).toEqual(['gamma'])
-  })
-
-  it('removes a layer and the slot it alone occupied', async () => {
-    const root = await userRoot({
-      standard: { [OVERLAY_FILE]: overlayInserting('gamma') },
-      mine: { [COMPOSITION_FILE]: '[]\n', [OVERLAY_FILE]: '[]\n' },
-    })
-    const ctx = await harness(rosterOver(root))
-
-    expect(await ctx.agentPresets.removeOverlay('standard')).toBe(true)
-    expect(existsSync(join(root, 'standard'))).toBe(false)
-    expect(await ctx.agentPresets.removeOverlay('standard')).toBe(false)
-    expect(await ctx.agentPresets.removeOverlay('mine')).toBe(true)
-    expect(existsSync(join(root, 'mine', COMPOSITION_FILE))).toBe(true)
-    expect((await ctx.agentPresets.resolve('standard')).overlayPath).toBeUndefined()
-  })
-
-  it('refuses to remove a layer outside the writable root', async () => {
-    const shipped = await userRoot({ standard: { [OVERLAY_FILE]: '[]\n' } })
-    const writable = await userRoot()
-    const ctx = await harness({
-      default: 'standard',
-      roots: [SYSTEM, { path: shipped, trust: 'system' }, { path: writable, trust: 'user' }],
-      includeShippedRoot: false,
-      includeUserRoot: false,
-    })
-
-    await expect(ctx.agentPresets.removeOverlay('standard')).rejects.toMatchObject({ code: 'agent-preset/read-only' })
-  })
-})

+ 0 - 3
packages/preset/agent-presets/tsconfig.json

@@ -51,9 +51,6 @@
     {
       "path": "../../util/home-paths"
     },
-    {
-      "path": "../../boot/app-boot"
-    },
     {
       "path": "../../runtime-diagnostics/invariants"
     }

+ 0 - 6
pnpm-lock.yaml

@@ -6962,9 +6962,6 @@ importers:
       '@deepseek-ai/dsh-agent':
         specifier: workspace:^
         version: link:../../core/agent
-      '@deepseek-ai/dsh-agent-presets':
-        specifier: workspace:^
-        version: link:../../preset/agent-presets
       '@deepseek-ai/dsh-app-boot':
         specifier: workspace:^
         version: link:../../boot/app-boot
@@ -7826,9 +7823,6 @@ importers:
       '@deepseek-ai/dsh-agent-loop':
         specifier: workspace:^
         version: link:../../core/agent-loop
-      '@deepseek-ai/dsh-app-boot':
-        specifier: workspace:^
-        version: link:../../boot/app-boot
       '@deepseek-ai/dsh-atomic-write':
         specifier: workspace:^
         version: link:../../util/atomic-write

+ 0 - 1
scripts/gen-cordis-catalog.ts

@@ -721,7 +721,6 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
   PluginPackageView: 'plugin manager package views are owned by packages/host/plugin-manager/README.md',
   PluginInstallResult: 'plugin manager install results are owned by packages/host/plugin-manager/README.md',
   PluginEnableResult: 'plugin manager enable results are owned by packages/host/plugin-manager/README.md',
-  PluginRowTarget: 'plugin manager row targets are owned by packages/host/plugin-manager/README.md',
   PluginRowAddition: 'plugin manager row additions are owned by packages/host/plugin-manager/README.md',
   PluginDependents: 'plugin manager dependents are owned by packages/host/plugin-manager/README.md',
   AgentPresetRoster: 'path-free preset roster is owned by packages/preset/agent-presets/README.md',