ソースを参照

Merge branch 'feat/plugin-mgmt-3-settings' into feat/plugin-mgmt-4-web

Yichen Jiang 1 ヶ月 前
親
コミット
0eff068ec4
39 ファイル変更、148 行追加、405 行削除
  1. 2 2
      .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.i18n.yaml
  2. 3 3
      .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.md
  3. 3 3
      .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md
  4. 2 2
      docs/config-catalog.i18n.yaml
  5. 0 1
      docs/config-catalog.md
  6. 0 1
      docs/config-catalog.zh.md
  7. 2 2
      docs/module-graph.i18n.yaml
  8. 3 6
      docs/module-graph.md
  9. 3 6
      docs/module-graph.zh.md
  10. 2 2
      packages/boot/app-boot/README.i18n.yaml
  11. 39 1
      packages/boot/app-boot/README.md
  12. 39 1
      packages/boot/app-boot/README.zh.md
  13. 7 2
      packages/boot/app-boot/package.json
  14. 1 1
      packages/boot/app-boot/src/index.ts
  15. 1 1
      packages/boot/app-boot/src/patch-file.ts
  16. 1 1
      packages/boot/app-boot/tests/patch-file.spec.ts
  17. 0 3
      packages/boot/app-boot/tsconfig.json
  18. 13 0
      packages/boot/app-boot/tsdown.config.ts
  19. 2 2
      packages/boot/plugin-manager/README.i18n.yaml
  20. 2 2
      packages/boot/plugin-manager/README.md
  21. 2 2
      packages/boot/plugin-manager/README.zh.md
  22. 0 2
      packages/boot/plugin-manager/package.json
  23. 1 1
      packages/boot/plugin-manager/src/index.ts
  24. 0 3
      packages/boot/plugin-manager/tsconfig.json
  25. 8 8
      packages/preset/agent-presets/package.json
  26. 1 1
      packages/preset/agent-presets/src/discovery.ts
  27. 1 1
      packages/preset/agent-presets/src/index.ts
  28. 1 1
      packages/preset/agent-presets/src/mount.ts
  29. 1 1
      packages/preset/agent-presets/tsconfig.json
  30. 0 6
      packages/util/patch-file/README.i18n.yaml
  31. 0 115
      packages/util/patch-file/README.md
  32. 0 115
      packages/util/patch-file/README.zh.md
  33. 0 45
      packages/util/patch-file/package.json
  34. 0 24
      packages/util/patch-file/tsconfig.json
  35. 6 34
      pnpm-lock.yaml
  36. 1 1
      scripts/check-workspace-constraints.ts
  37. 0 1
      scripts/verify-package-readme-model-experience.ts
  38. 1 1
      tsconfig.base.json
  39. 0 1
      tsconfig.host.json

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.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-preset-user-patch-layer-and-patch-file-writer.md
-2026-09-04-preset-user-patch-layer-and-patch-file-writer.md: 6f31e45ddf8aae56af56d7b30bccc36a23b42073
-2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md: 38e8ac614743984636fcf869acb33ed3cd0d1549
+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

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

@@ -14,7 +14,7 @@ A shipped agent preset was read-only in every sense: to switch one of its rows o
 
 **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-patch-file` owns the format.** `parsePatchList` moved out of `dsh-app-boot` and 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.
+**`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
 
@@ -22,7 +22,7 @@ A shipped agent preset was read-only in every sense: to switch one of its rows o
 
 **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; the format got its own package instead, which both depend on.
+**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.
 
@@ -32,4 +32,4 @@ A person hides a tool from `standard` with three lines under `.agent-presets/sta
 
 ## 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/util/patch-file/tests/patch-file.spec.ts` pins the parser, the document edits with comments and `!!js` intact, and the locked atomic mutation with readback.
+`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.

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

@@ -14,7 +14,7 @@ Status: implemented
 
 **代际跟随层的内容。** 常驻挂载的印记现在覆盖组合文件的 stat 与层文本的摘要。层变化即为之后创建的会话开启下一代,一如组合被编辑时的既有行为;层被改回某个早先代际组合过的内容时,回到那一代,因此一行关掉再打开不会堆出第三棵在线子树。
 
-**`dsh-patch-file` 拥有这种格式。** `parsePatchList` 从 `dsh-app-boot` 搬出,成为唯一的解析器(js-yaml 配 include 的 `!!js` 方言,相对名字锚定到文件);`PatchDocument` 通过 `yaml` 包保留注释的 document 在键级编辑文件,它把未解析的 `!!js` 标签留在标量上并原样打印;`mutatePatchFile` 拿 `dsh-atomic-write` 的锁,读取、编辑、在 document 变脏时原子替换,并把写出的文本回读解析。被移除行上方的注释块移到邻居或文档尾注释,而不是消失。
+**`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 变脏时原子替换,并把写出的文本回读解析。被移除行上方的注释块移到邻居或文档尾注释,而不是消失。
 
 ## 考虑过的替代方案
 
@@ -22,7 +22,7 @@ Status: implemented
 
 **原地编辑随附组合。** 否决:升级会覆盖它,而正在运行的会话会读到写了一半的文件。
 
-**在 `dsh-agent-presets` 里再写一个解析器以避免依赖 `dsh-app-boot`。** 否决:同一格式的两个解析器会漂移;改为让格式拥有自己的包,两者都依赖它。
+**在 `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 级设置,不是手写的行。
 
@@ -32,4 +32,4 @@ Status: implemented
 
 ## 测试
 
-`packages/preset/agent-presets/tests/overlay.spec.ts` 钉住 discovery(附着、自有层、孤儿槽位、不可解析、畸形、不可解析的插入、组合判定优先)、施加层的挂载、编辑之间的代际及退役代际的复用、清单从文件与从挂载得到的 `source` 与 `disabledBy`、随附与自作 preset 的层路径、复制携带层,以及移除。`packages/util/patch-file/tests/patch-file.spec.ts` 钉住解析器、保留注释与 `!!js` 的文档编辑,以及带回读的加锁原子变更。
+`packages/preset/agent-presets/tests/overlay.spec.ts` 钉住 discovery(附着、自有层、孤儿槽位、不可解析、畸形、不可解析的插入、组合判定优先)、施加层的挂载、编辑之间的代际及退役代际的复用、清单从文件与从挂载得到的 `source` 与 `disabledBy`、随附与自作 preset 的层路径、复制携带层,以及移除。`packages/boot/app-boot/tests/patch-file.spec.ts` 钉住解析器、保留注释与 `!!js` 的文档编辑,以及带回读的加锁原子变更。

+ 2 - 2
docs/config-catalog.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/config-catalog.md
-config-catalog.md: 25b1009ee1dd150ac4876834c30da89200452ac2
-config-catalog.zh.md: 0ec5126fded0f2bd94aba70847bee4f038ffece4
+config-catalog.md: b21909f37652f606031fee75fc24f53f5eb26c7b
+config-catalog.zh.md: f37a2d22d5e475cfa211d6884e9e21ddf1f4dbbc

+ 0 - 1
docs/config-catalog.md

@@ -3501,7 +3501,6 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
 - `@deepseek-ai/dsh-native-command` ([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
 - `@deepseek-ai/dsh-output-retention` ([`packages/util/output-retention/src/index.ts`](../packages/util/output-retention/src/index.ts))
 - `@deepseek-ai/dsh-package-manifest` ([`packages/util/package-manifest/src/index.ts`](../packages/util/package-manifest/src/index.ts))
-- `@deepseek-ai/dsh-patch-file` ([`packages/util/patch-file/src/index.ts`](../packages/util/patch-file/src/index.ts))
 - `@deepseek-ai/dsh-plugin-manager` ([`packages/boot/plugin-manager/src/index.ts`](../packages/boot/plugin-manager/src/index.ts))
 - `@deepseek-ai/dsh-sandbox-windows-acl` ([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
 - `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))

+ 0 - 1
docs/config-catalog.zh.md

@@ -3502,7 +3502,6 @@ export interface Config {
 - `@deepseek-ai/dsh-native-command`([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
 - `@deepseek-ai/dsh-output-retention`([`packages/util/output-retention/src/index.ts`](../packages/util/output-retention/src/index.ts))
 - `@deepseek-ai/dsh-package-manifest` ([`packages/util/package-manifest/src/index.ts`](../packages/util/package-manifest/src/index.ts))
-- `@deepseek-ai/dsh-patch-file`([`packages/util/patch-file/src/index.ts`](../packages/util/patch-file/src/index.ts))
 - `@deepseek-ai/dsh-plugin-manager`([`packages/boot/plugin-manager/src/index.ts`](../packages/boot/plugin-manager/src/index.ts))
 - `@deepseek-ai/dsh-sandbox-windows-acl`([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
 - `@deepseek-ai/dsh-scope`([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))

+ 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: fc4738c4078e8a868ffd65c1d626f851c5b0249e
-module-graph.zh.md: 84fe86779e210c5668575f6af963f0d7587b1982
+module-graph.md: a946b9fcd7d9108012d6bce515cc7e345ec66d65
+module-graph.zh.md: 4a2408da21dbc2c7cbe41cb7be0a4adff2b2c7e1

+ 3 - 6
docs/module-graph.md

@@ -17,7 +17,6 @@ flowchart TD
     pkg_native_command["native-command"]
     pkg_output_retention["output-retention"]
     pkg_package_manifest["package-manifest"]
-    pkg_patch_file["patch-file"]
     pkg_timeout["timeout"]
     pkg_util_crypto["util-crypto"]
     pkg_util_time["util-time"]
@@ -468,7 +467,6 @@ flowchart TD
   pkg_session_log_export --> pkg_session_persistence
   pkg_plugin_manager --> pkg_app_boot
   pkg_plugin_manager --> pkg_package_manifest
-  pkg_plugin_manager --> pkg_patch_file
   pkg_plugin_manager --> pkg_util_values
   pkg_message_feedback --> pkg_brand
   pkg_message_feedback --> pkg_llm
@@ -820,10 +818,10 @@ flowchart TD
   pkg_mcp_client --> pkg_timeout
   pkg_mcp_client --> 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
-  pkg_agent_presets --> pkg_patch_file
   pkg_agent_presets --> pkg_scope
   pkg_agent_presets --> pkg_session
   pkg_agent_presets --> pkg_session_projection
@@ -1183,7 +1181,6 @@ flowchart TD
 | [`native-command`](../packages/util/native-command) | `util` | — |
 | [`output-retention`](../packages/util/output-retention) | `util` | — |
 | [`package-manifest`](../packages/util/package-manifest) | `util` | — |
-| [`patch-file`](../packages/util/patch-file) | `util` | — |
 | [`timeout`](../packages/util/timeout) | `util` | — |
 | [`util-crypto`](../packages/util/crypto) | `util` | — |
 | [`util-time`](../packages/util/time) | `util` | — |
@@ -1308,7 +1305,7 @@ flowchart TD
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
 | [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
-| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`app-boot`](../packages/boot/app-boot), [`package-manifest`](../packages/util/package-manifest), [`patch-file`](../packages/util/patch-file), [`util-values`](../packages/util/values) |
+| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`app-boot`](../packages/boot/app-boot), [`package-manifest`](../packages/util/package-manifest), [`util-values`](../packages/util/values) |
 | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-protocol`](../packages/typert/protocol) |
 | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
 | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
@@ -1382,7 +1379,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-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`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), [`patch-file`](../packages/util/patch-file), [`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), [`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) |
 | [`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-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) |

+ 3 - 6
docs/module-graph.zh.md

@@ -19,7 +19,6 @@ flowchart TD
     pkg_native_command["native-command"]
     pkg_output_retention["output-retention"]
     pkg_package_manifest["package-manifest"]
-    pkg_patch_file["patch-file"]
     pkg_timeout["timeout"]
     pkg_util_crypto["util-crypto"]
     pkg_util_time["util-time"]
@@ -470,7 +469,6 @@ flowchart TD
   pkg_session_log_export --> pkg_session_persistence
   pkg_plugin_manager --> pkg_app_boot
   pkg_plugin_manager --> pkg_package_manifest
-  pkg_plugin_manager --> pkg_patch_file
   pkg_plugin_manager --> pkg_util_values
   pkg_message_feedback --> pkg_brand
   pkg_message_feedback --> pkg_llm
@@ -822,10 +820,10 @@ flowchart TD
   pkg_mcp_client --> pkg_timeout
   pkg_mcp_client --> 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
-  pkg_agent_presets --> pkg_patch_file
   pkg_agent_presets --> pkg_scope
   pkg_agent_presets --> pkg_session
   pkg_agent_presets --> pkg_session_projection
@@ -1185,7 +1183,6 @@ flowchart TD
 | [`native-command`](../packages/util/native-command) | `util` | — |
 | [`output-retention`](../packages/util/output-retention) | `util` | — |
 | [`package-manifest`](../packages/util/package-manifest) | `util` | — |
-| [`patch-file`](../packages/util/patch-file) | `util` | — |
 | [`timeout`](../packages/util/timeout) | `util` | — |
 | [`util-crypto`](../packages/util/crypto) | `util` | — |
 | [`util-time`](../packages/util/time) | `util` | — |
@@ -1310,7 +1307,7 @@ flowchart TD
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
 | [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
-| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`app-boot`](../packages/boot/app-boot), [`package-manifest`](../packages/util/package-manifest), [`patch-file`](../packages/util/patch-file), [`util-values`](../packages/util/values) |
+| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`app-boot`](../packages/boot/app-boot), [`package-manifest`](../packages/util/package-manifest), [`util-values`](../packages/util/values) |
 | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-protocol`](../packages/typert/protocol) |
 | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
 | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
@@ -1384,7 +1381,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-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`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), [`patch-file`](../packages/util/patch-file), [`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), [`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) |
 | [`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-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) |

+ 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: babb6b74dbdfb57480878b8349ffde6e4dafa5c1
-README.zh.md: c992bbb35cc58210195b25197ee2375afaa19de6
+README.md: ab734b967ddc734b9690e5186df5637a06cdfb8d
+README.zh.md: e1214ec99162df0c2bc1daff6d1c182cba8b01f7

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

@@ -62,6 +62,32 @@ A bundle you installed with `dsh plugin` is an **external** bundle: its rows mou
 
 After the tree is up the launcher provides `ctx.profileRuntime`, which holds the composition the tree runs — the profile, the installation anchor, the layer that owns each row, and the rows the composition left out — reads which rows the user patch files disable, and is the one entry point that recomposes the tree: the patch watchers and the [plugin manager](../../host/plugin-manager/README.md), enabling or retrying a bundle, all call it, recompositions run one at a time, and a rejected update leaves its facts describing the tree still running; `recordContainedStates` is what such a caller runs afterwards, because the boot audit does not run again. Startup's fail-loud rejection guard is uninstalled once the tree is up: an unhandled rejection after boot is reported and the process keeps running, with nothing stopped or attributed to a plugin; an uncaught exception is reported and exits.
 
+<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.
+
+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.
+
+Write through `mutatePatchFile`. The callback receives a `PatchDocument` and edits it by row id, the way the Loader addresses rows:
+
+```ts
+import { mutatePatchFile } from '@deepseek-ai/dsh-app-boot/patch-file'
+
+const file = '/home/me/.dsh/profiles/web/cordis.patch.yml'
+await mutatePatchFile(file, (document) => {
+  document.setRowField('tool-web', 'disabled', true)      // the id-targeted patch is created when absent
+  document.deleteRowField('tool-web', 'config')           // a patch reduced to its id is removed whole
+  document.appendInsert({ id: 'tool-foo', name: 'dsh-tool-foo' })          // into the root list
+  document.appendInsert({ id: 'sql', name: 'dsh-sql' }, 'agents')          // into the group with that id
+  document.removeInsert('tool-foo')                       // an emptied insert patch is removed whole
+}, { binName: 'dsh', mode: 0o600, dirMode: 0o700 })
+```
+
+`setRowField` never accepts `id` or `insert`; `rowField` reads one key back, with a `!!js` scalar returned as its source text. `appendInsert` refuses an id the file already inserts, and `insertedRow`/`removeInsert` find rows inside inserted groups too. Values written are plain data; a `!!js` scalar on another key is left untouched, which is what lets a user layer revert a `disabled: true` it wrote without disturbing a bundle's `!!js` gate on a different row.
+
+`mutatePatchFile` takes the `<file>.lock` sibling the way `dsh-atomic-write` does, reads the file (absent reads as empty), applies the edit, replaces the file atomically with the stated permission bits when the text changed, and returns the patch list as re-read from the written text. An edit that changes nothing writes nothing.
+
 ### Previewing the effective configuration
 
 Before you boot, you can print the exact configuration the app will mount: the dump shows the composed entry list with `!!js` expressions verbatim, grouped under comments naming each source file and the patch layers that changed it, as one loadable YAML document. Patches that match no row are reported with their layer label; a missing, unparsable, or invalid config fails the dump.
@@ -102,16 +128,25 @@ This section explains how the outcomes above are realized and points at the code
 
 The exports each own one stage of the boot: config resolution and snapshot replay, layered environment loading, fail-loud reporting, activation auditing, patch parsing, root-include mounting, config dump rendering, live patch watching, profile composition, and the harness-source section. Per-export contracts live in the code, not this README — see [`src/index.ts`](src/index.ts) and [`src/profile.ts`](src/profile.ts).
 
+### Two parsers, one dialect
+
+Reading a patch file uses `js-yaml` with the include's `entryListSchema`, so `!!js` scalars become the expression nodes the Loader interpolates, exactly as the include mounts them. Writing uses the `yaml` package's comment-preserving `Document`: it keeps the unresolved `!!js` tag on the scalar it decorates (reported as a `TAG_RESOLVE_FAILED` warning, not an error) and prints it back verbatim, so an edit to one key never rewrites another key's expression. The written text is parsed back with the reading parser, which is the readback the writer's contract promises.
+
+### Addressing
+
+An id-targeted patch is the top-level item whose `id` matches and that carries no `insert`. An inserted row is searched in every `insert` list, recursing into inserted groups (`group: true` with a `config` list). A top-level item that is not a mapping, or an `insert` whose value is not a list, fails the parse.
+
 ### Source map
 
 | File | Role |
 |---|---|
-| [`src/index.ts`](src/index.ts) | Boot helpers: config resolution, environment loading, fail-loud guard and runtime guards, activation audit and `recordContainedStates`, patch-file loading through `dsh-patch-file`, config dump, harness-source section |
+| [`src/index.ts`](src/index.ts) | Boot helpers: config resolution, environment loading, fail-loud guard and runtime guards, activation audit and `recordContainedStates`, patch-file loading through `./patch-file`, config dump, harness-source section |
 | [`src/profile.ts`](src/profile.ts) | Profile discovery, initialization, bundle resolution with `layerTrust` and stage, module fallback |
 | [`src/external-bundles.ts`](src/external-bundles.ts) | External layer composition (contained group, overrides), `bundleLayerPatches`, and the manifest operations behind install, enable, and disable |
 | [`src/compose-stack.ts`](src/compose-stack.ts) | Row-id ownership across the stack: `claimLayerIds`, `composeProfileStack`, conflict records |
 | [`src/contained-group.ts`](src/contained-group.ts) | The `cordis:contained-group` builtin and the `pluginFailures` registry |
 | [`src/profile-runtime.ts`](src/profile-runtime.ts) | The `profileRuntime` service: the committed composition (profile, row provenance, conflicts), user-disabled rows, recomposition |
+| [`src/patch-file.ts`](src/patch-file.ts) | The `./patch-file` export: `parsePatchList` and `readPatchListFile`, the comment-preserving `PatchDocument`, and `mutatePatchFile` under the writer lock |
 | [`src/probe.ts`](src/probe.ts) | The package probe and its per-profile cache; [`src/probe-child.ts`](src/probe-child.ts) is the child entry it spawns and [`src/probe-report.ts`](src/probe-report.ts) the report it validates |
 | — | No runtime invariant companion is published; this presentation adapter owns no durable package-local event stream; boundary and replay tests cover its protocol mapping. |
 
@@ -157,6 +192,9 @@ These limits describe when this boot library is a poor fit or needs special care
 - **An external bundle's overrides are not isolated** — a patch it applies to a built-in row edits that row in place, so its effect stays when the bundle's own rows fail and it is the one thing a bundle can break outside its group.
 - **A conflict is decided by order, not merit** — among external bundles the earlier layer in `dsh.profile.bundles` keeps a contested id, so uninstalling that bundle lets the later one mount on the next boot; the plugin list shows which bundle lost and to whom.
 - **The nested-fiber audit is advisory** — a failed `ctx.inject()` continuation under a built-in entry is reported, not fatal, until shipped compositions are known clean.
+- **Patch edits merge at the key level, not the row level** — `setRowField('x', 'config', value)` replaces the whole `config` mapping of that patch; a caller that wants one nested field changed reads the current value with `rowField` and writes the merged mapping back.
+- **The patch writer authors no expressions** — it emits plain data only; a `!!js` gate is something an author types into the file, never something an API call produces.
+- **Lock orphans are an operator action** — a lock file left by a crashed patch writer is never removed by a contender, which fails after the wait instead; `dsh-atomic-write` documents the same choice.
 
 <a id="dev-note"></a>
 ### Dev Note

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

@@ -62,6 +62,32 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
 
 树起来之后 launcher 提供 `ctx.profileRuntime`:它持有树正在运行的组合——profile、安装锚点、每一行的归属层、被组合排除的行——读取用户 patch 文件停用了哪些行,并且是重组整棵树的唯一入口:patch 监视器与启用或重试组合包的[插件管理器](../../host/plugin-manager/README.zh.md)都调用它,重组一次只跑一个,被拒的更新留下的事实仍然描述正在运行的树;这样的调用方之后会运行 `recordContainedStates`,因为启动审计不会再跑一次。启动期的 fail-loud rejection 守卫在树起来后卸载:启动后未处理的 rejection 会被报告,进程继续运行,不停止任何任务也不归属到任何插件;未捕获的异常会被报告并退出。
 
+<a id="patch-files"></a>
+### 补丁文件
+
+上面的每一层都是一个 `cordis.patch.yml`:一个顶层 YAML 序列,元素是 include 插件的 `PatchOptions`——按 id 定位的覆盖与 `insert` 列表——采用 Loader 的方言,其中 `!!js` 标记一个由该行 fiber 求值的表达式。`./patch-file` 导出是读写这种文件的唯一地方,因此启动接受的文件就是 agent preset roster 与插件管理器接受的文件。
+
+用 `parsePatchList`(已有文本)或 `readPatchListFile`(文件不存在时读到 `undefined`)读取一层。两者都把 `insert` 行中相对的名字(如 `./plugin.js`)锚定到文件自己的目录,并对任何不是"映射序列"的内容直接报错,因为一个完全无法施加的补丁文件就是配置错误;而目标行不存在的单条补丁仍然只是 Loader 的逐条警告。
+
+通过 `mutatePatchFile` 写入。回调拿到一个 `PatchDocument`,按 Loader 寻址行的方式以行 id 编辑:
+
+```ts
+import { mutatePatchFile } from '@deepseek-ai/dsh-app-boot/patch-file'
+
+const file = '/home/me/.dsh/profiles/web/cordis.patch.yml'
+await mutatePatchFile(file, (document) => {
+  document.setRowField('tool-web', 'disabled', true)      // the id-targeted patch is created when absent
+  document.deleteRowField('tool-web', 'config')           // a patch reduced to its id is removed whole
+  document.appendInsert({ id: 'tool-foo', name: 'dsh-tool-foo' })          // into the root list
+  document.appendInsert({ id: 'sql', name: 'dsh-sql' }, 'agents')          // into the group with that id
+  document.removeInsert('tool-foo')                       // an emptied insert patch is removed whole
+}, { binName: 'dsh', mode: 0o600, dirMode: 0o700 })
+```
+
+`setRowField` 永远不接受 `id` 与 `insert`;`rowField` 读回一个键,`!!js` 标量以其源文本返回。`appendInsert` 拒绝文件已插入的 id;`insertedRow`/`removeInsert` 也能找到插入组内部的行。写入的值都是普通数据;别的键上的 `!!js` 标量原样不动,这正是用户层能撤回自己写的 `disabled: true` 而不惊动组合包在另一行上的 `!!js` 门的原因。
+
+`mutatePatchFile` 像 `dsh-atomic-write` 一样占用 `<file>.lock` 兄弟文件,读取文件(不存在按空处理),施加编辑,在文本有变化时以声明的权限位原子替换文件,然后返回从写入文本重新读出的补丁列表。什么都没改的编辑什么都不写。
+
 ### 预览生效配置
 
 启动前,你可以打印应用将挂载的确切配置:dump 会以 `!!js` 表达式原样展示组合后的条目列表,并按注释分组标明每个源文件及其 patch 层,输出是一份可加载的 YAML 文档。未匹配到任何行的 patch 会连同其层标签一起报告;配置缺失、无法解析或字段无效都会使 dump 失败。
@@ -102,16 +128,25 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
 
 每个导出各负责启动的一个阶段:配置解析与快照回放、分层环境加载、明确报错的保护机制、激活审计、patch 解析、根 include 挂载、配置 dump 渲染、活动 patch 监视、profile 组合,以及 harness 源码段落。各导出的约定在代码中,不在本 README——见 [`src/index.ts`](src/index.ts) 与 [`src/profile.ts`](src/profile.ts)。
 
+### 两个解析器,一种方言
+
+读取补丁文件用 `js-yaml` 配 include 的 `entryListSchema`,于是 `!!js` 标量成为 Loader 插值的表达式节点,与 include 挂载时完全一致。写入用 `yaml` 包保留注释的 `Document`:它在所修饰的标量上保留未解析的 `!!js` 标签(报告为 `TAG_RESOLVE_FAILED` 警告而非错误)并原样打印回去,因此对一个键的编辑绝不会改写另一个键的表达式。写出的文本再用读取解析器解析一遍,这就是写入器契约承诺的回读。
+
+### 寻址
+
+按 id 定位的补丁是 `id` 匹配且不带 `insert` 的顶层项。插入的行在每个 `insert` 列表中查找,并递归进入插入的组(`group: true` 且带 `config` 列表)。顶层项不是映射,或 `insert` 的值不是列表,都会让解析失败。
+
 ### 源码地图
 
 | 文件 | 职责 |
 |---|---|
-| [`src/index.ts`](src/index.ts) | 启动 helper:配置解析、环境加载、fail-loud 守卫与运行时守卫、激活审计与 `recordContainedStates`、经 `dsh-patch-file` 加载 patch 文件、配置 dump、harness 源码段落 |
+| [`src/index.ts`](src/index.ts) | 启动 helper:配置解析、环境加载、fail-loud 守卫与运行时守卫、激活审计与 `recordContainedStates`、经 `./patch-file` 加载 patch 文件、配置 dump、harness 源码段落 |
 | [`src/profile.ts`](src/profile.ts) | profile 发现、初始化、带 `layerTrust` 与 stage 的组合包解析、模块后备机制 |
 | [`src/external-bundles.ts`](src/external-bundles.ts) | 外部层组合(受控组、覆盖报告)、`bundleLayerPatches`,与安装、启用、停用背后的 manifest 操作 |
 | [`src/compose-stack.ts`](src/compose-stack.ts) | 整叠层的行 id 归属:`claimLayerIds`、`composeProfileStack`、冲突记录 |
 | [`src/contained-group.ts`](src/contained-group.ts) | `cordis:contained-group` builtin 与 `pluginFailures` 注册表 |
 | [`src/profile-runtime.ts`](src/profile-runtime.ts) | `profileRuntime` 服务:已提交的组合(profile、行来源、冲突)、用户停用的行、重新组合 |
+| [`src/patch-file.ts`](src/patch-file.ts) | `./patch-file` 导出:`parsePatchList` 与 `readPatchListFile`、保留注释的 `PatchDocument`,以及持写入锁的 `mutatePatchFile` |
 | [`src/probe.ts`](src/probe.ts) | 包探针及其按 profile 的缓存;[`src/probe-child.ts`](src/probe-child.ts) 是它生成的子进程入口,[`src/probe-report.ts`](src/probe-report.ts) 是它校验的报告 |
 | — | 不发布运行时不变式伴生入口;边界与回放测试覆盖其协议映射。 |
 
@@ -157,6 +192,9 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
 - **外部组合包的覆盖不被隔离**——它对内置行施加的 patch 原地修改那一行,所以即使该组合包自己的行失败,效果仍然保留;这是组合包唯一能在自己的组之外造成影响的地方。
 - **冲突按顺序判定,不看是非**——外部组合包之间,`dsh.profile.bundles` 里靠前的那层保住争议 id,卸掉它之后靠后的那层在下次启动时挂上;插件列表显示谁输给了谁。
 - **嵌套 fiber 审计只是提示**——内置条目下失败的 `ctx.inject()` 延续会被报告而非致命,直到确认随附组合都没有这类失败。
+- **补丁编辑按键级而非行级合并**——`setRowField('x', 'config', value)` 替换该补丁的整个 `config` 映射;只想改一个嵌套字段的调用方先用 `rowField` 读出当前值,再把合并后的映射写回。
+- **补丁写入器不生成表达式**——它只输出普通数据;`!!js` 门是作者敲进文件的东西,从不是某个 API 调用的产物。
+- **锁孤儿由操作者处理**——补丁写入器崩溃留下的锁文件不会被竞争者移除,竞争者等待超时后失败;`dsh-atomic-write` 记录了同样的选择。
 
 <a id="dev-note"></a>
 ### 开发备注

+ 7 - 2
packages/boot/app-boot/package.json

@@ -18,21 +18,26 @@
       "types": "./lib/types/index.d.ts",
       "default": "./lib/index.js"
     },
+    "./patch-file": {
+      "types": "./lib/types/patch-file.d.ts",
+      "default": "./lib/patch-file.js"
+    },
     "./src/*": "./src/*",
     "./package.json": "./package.json"
   },
   "files": [
     "lib/index.js",
     "lib/probe-child.js",
+    "lib/patch-file.js",
     "lib/types/**/*.d.ts"
   ],
   "license": "MIT",
   "dependencies": {
     "@deepseek-ai/dsh-atomic-write": "workspace:^",
     "@deepseek-ai/dsh-package-manifest": "workspace:^",
-    "@deepseek-ai/dsh-patch-file": "workspace:^",
     "js-yaml": "^4.2.0",
-    "resolve.exports": "^2.0.3"
+    "resolve.exports": "^2.0.3",
+    "yaml": "^2.9.0"
   },
   "peerDependencies": {
     "@deepseek-ai/cordis-plugin-group": "workspace:^",

+ 1 - 1
packages/boot/app-boot/src/index.ts

@@ -14,7 +14,7 @@ import * as yaml from 'js-yaml'
 import { Context, type FiberState } from '@deepseek-ai/cordis'
 import Loader, { type Entry, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
 import Include, { applyEntryPatches, entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
-import { parsePatchList } from '@deepseek-ai/dsh-patch-file'
+import { parsePatchList } from './patch-file.ts'
 import Group from '@deepseek-ai/cordis-plugin-group'
 import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-home-paths'
 import { createLaunchEnvironmentSnapshot, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'

+ 1 - 1
packages/util/patch-file/src/index.ts → packages/boot/app-boot/src/patch-file.ts

@@ -12,7 +12,7 @@
  * author wrote them, and {@link mutatePatchFile} commits one edit under the
  * cross-process writer lock, through an atomic replace, and reads the result
  * back through the parser before reporting success.
- * @module @deepseek-ai/dsh-patch-file
+ * @module @deepseek-ai/dsh-app-boot/patch-file
  */
 
 import { mkdir, readFile } from 'node:fs/promises'

+ 1 - 1
packages/util/patch-file/tests/patch-file.spec.ts → packages/boot/app-boot/tests/patch-file.spec.ts

@@ -11,7 +11,7 @@ import {
   parsePatchList,
   PatchDocument,
   readPatchListFile,
-} from '../src/index.ts'
+} from '../src/patch-file.ts'
 
 const SAMPLE = `# top comment
 - id: message-feedback # trailing

+ 0 - 3
packages/boot/app-boot/tsconfig.json

@@ -29,9 +29,6 @@
     {
       "path": "../../util/atomic-write"
     },
-    {
-      "path": "../../util/patch-file"
-    },
     {
       "path": "../../util/launch-environment"
     },

+ 13 - 0
packages/boot/app-boot/tsdown.config.ts

@@ -30,4 +30,17 @@ export default defineConfig([
     dts: false,
     clean: false,
   },
+  {
+    // The patch-file parser and writer ship as the `./patch-file` export,
+    // so the agent-preset roster and the plugin manager load them without
+    // the boot entry.
+    entry: { 'patch-file': 'lib/types/patch-file.js' },
+    outDir: 'lib',
+    format: ['esm'],
+    platform: 'node',
+    target: 'es2024',
+    fixedExtension: false,
+    dts: false,
+    clean: false,
+  },
 ])

+ 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: be82a1b988b44f4a5e59edbcc4801eebb0db3fc7
-README.zh.md: 7320c0030db61287bbb24d6a16b4273100075646
+README.md: 5686c8186d5332c92ae6cff200b71bd8db42e807
+README.zh.md: 8069be7873aa13d057586d089c7dfb89823a2006

+ 2 - 2
packages/boot/plugin-manager/README.md

@@ -88,7 +88,7 @@ The Loader's transactional update leaves an unchanged row alone, so a failed iso
 
 ### What the manager reads and what it is handed
 
-The manager reads the Loader tree, the reflect store, and the `pluginFailures` registry through the context it is built over. Everything that belongs to another package — the profile runtime, the preset roster, the agent registry — arrives through `PluginManagerOptions` as a reader called per call, and the roster only as `PresetLayers`: the layer path, the preset list, and the composition rows the row operations need. The package therefore depends on app-boot and the patch-file writer and on nothing that composes presets or agents.
+The manager reads the Loader tree, the reflect store, and the `pluginFailures` registry through the context it is built over. Everything that belongs to another package — the profile runtime, the preset roster, the agent registry — arrives through `PluginManagerOptions` as a reader called per call, and the roster only as `PresetLayers`: the layer path, the preset list, and the composition rows the row operations need. The package therefore depends on app-boot alone, whose `./patch-file` export reads and writes the layers, and on nothing that composes presets or agents.
 
 ### Source map
 
@@ -109,7 +109,7 @@ The manager reads the Loader tree, the reflect store, and the `pluginFailures` r
 Read these when the manager's contract is not enough: the runtime it drives, the files it edits, and the surfaces that call it.
 
 - [App boot](../app-boot/README.md) — the profile runtime, external bundle isolation, and the package probe.
-- [Patch files](../../util/patch-file/README.md) — how user-layer rows are written.
+- [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.

+ 2 - 2
packages/boot/plugin-manager/README.zh.md

@@ -88,7 +88,7 @@ Loader 的事务性更新不会碰未改变的行,因此一条失败的隔离
 
 ### 管理器自己读什么,别人交给它什么
 
-管理器经它所在的上下文读取 Loader 树、reflect store 与 `pluginFailures` 注册表。属于别的包的东西——profile runtime、preset roster、agent 注册表——都经 `PluginManagerOptions` 以按调用读取的读取器交进来,roster 只以 `PresetLayers` 的形态交进来:层文件路径、preset 列表,以及行操作需要的组合行。因此本包只依赖 app-boot 与补丁文件写入器,不依赖任何组合 preset 或 agent 的包。
+管理器经它所在的上下文读取 Loader 树、reflect store 与 `pluginFailures` 注册表。属于别的包的东西——profile runtime、preset roster、agent 注册表——都经 `PluginManagerOptions` 以按调用读取的读取器交进来,roster 只以 `PresetLayers` 的形态交进来:层文件路径、preset 列表,以及行操作需要的组合行。因此本包只依赖 app-boot(其 `./patch-file` 导出负责读写各层),不依赖任何组合 preset 或 agent 的包。
 
 ### 源码地图
 
@@ -109,7 +109,7 @@ Loader 的事务性更新不会碰未改变的行,因此一条失败的隔离
 当管理器的契约还不够时读这些:它驱动的运行时、它编辑的文件,以及调用它的表面。
 
 - [App boot](../app-boot/README.zh.md)——profile runtime、外部组合包隔离与包探针。
-- [补丁文件](../../util/patch-file/README.zh.md)——用户层的行如何写入。
+- [补丁文件](../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` 命令。

+ 0 - 2
packages/boot/plugin-manager/package.json

@@ -36,7 +36,6 @@
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-package-manifest": "workspace:^",
-    "@deepseek-ai/dsh-patch-file": "workspace:^",
     "@deepseek-ai/dsh-util-values": "workspace:^"
   },
   "devDependencies": {
@@ -45,7 +44,6 @@
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-package-manifest": "workspace:^",
-    "@deepseek-ai/dsh-patch-file": "workspace:^",
     "@deepseek-ai/dsh-util-values": "workspace:^"
   }
 }

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

@@ -46,7 +46,7 @@ import {
   type ProfileRuntime,
 } from '@deepseek-ai/dsh-app-boot'
 import type { BundleStage } from '@deepseek-ai/dsh-package-manifest'
-import { mutatePatchFile, readPatchListFile, type PatchRow } from '@deepseek-ai/dsh-patch-file'
+import { mutatePatchFile, readPatchListFile, type PatchRow } from '@deepseek-ai/dsh-app-boot/patch-file'
 import type { JsonValue } from '@deepseek-ai/dsh-util-values'
 import { PluginOperationError } from './errors.ts'
 import type {

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

@@ -20,9 +20,6 @@
     {
       "path": "../app-boot"
     },
-    {
-      "path": "../../util/patch-file"
-    },
     {
       "path": "../../util/values"
     },

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

@@ -54,21 +54,21 @@
   ],
   "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-invariants": "workspace:^",
     "@deepseek-ai/dsh-home-paths": "workspace:^",
-    "@deepseek-ai/dsh-patch-file": "workspace:^",
+    "@deepseek-ai/dsh-invariants": "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/cordis": "workspace:^"
+    "@deepseek-ai/dsh-typert-protocol": "workspace:^"
   },
   "dependencies": {
     "js-yaml": "^4.1.0",
@@ -76,16 +76,17 @@
     "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-patch-file": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-projection": "workspace:^",
@@ -93,7 +94,6 @@
     "@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/cordis": "workspace:^"
+    "@deepseek-ai/dsh-typert-protocol": "workspace:^"
   }
 }

+ 1 - 1
packages/preset/agent-presets/src/discovery.ts

@@ -35,7 +35,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
 import { load } from 'js-yaml'
 import { entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
 import { expandHomePath } from '@deepseek-ai/dsh-home-paths'
-import { readPatchListFile } from '@deepseek-ai/dsh-patch-file'
+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 { classifyRowSpecifier, type RowSpecifier } from './specifier.ts'

+ 1 - 1
packages/preset/agent-presets/src/index.ts

@@ -40,7 +40,7 @@ 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-patch-file'
+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 { livePresetMounts, mountPreset, serviceForAgent, standingMountFor } from './mount.ts'

+ 1 - 1
packages/preset/agent-presets/src/mount.ts

@@ -20,7 +20,7 @@ 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-patch-file'
+import { readPatchListFile } from '@deepseek-ai/dsh-app-boot/patch-file'
 import type { AgentPreset } from './preset.ts'
 import { classifyRowSpecifier } from './specifier.ts'
 

+ 1 - 1
packages/preset/agent-presets/tsconfig.json

@@ -52,7 +52,7 @@
       "path": "../../util/home-paths"
     },
     {
-      "path": "../../util/patch-file"
+      "path": "../../boot/app-boot"
     },
     {
       "path": "../../runtime-diagnostics/invariants"

+ 0 - 6
packages/util/patch-file/README.i18n.yaml

@@ -1,6 +0,0 @@
-# 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 packages/util/patch-file/README.md
-README.md: d8b13e3be77d28693c30b8ef40f6c57efeacb57a
-README.zh.md: a690dad3a7b79d503347cefb271d2f6cfe71c153

+ 0 - 115
packages/util/patch-file/README.md

@@ -1,115 +0,0 @@
----
-description: "Loader patch-list files: the shared parser for cordis.patch.yml layers and a comment-preserving key-level writer with lock, atomic replace, and readback."
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-patch-file
-
-English | [中文](README.zh.md)
-
-## Summary
-
-`dsh-patch-file` owns the file format every composition layer of the harness is written in: a `cordis.patch.yml` is a top-level YAML sequence of Loader patch entries — id-targeted overrides such as `disabled: true` or a `config` replacement, and `insert` lists of new rows — in the Loader's own dialect, where a `!!js` scalar is an expression the row's fiber evaluates. `parsePatchList` is the one parser the profile launcher, bundle layers, `--patch` overlays, and agent-preset user layers read through, so a file one of them accepts is a file all of them accept. `PatchDocument` edits such a file at the key level while keeping its comments, blank lines, and `!!js` scalars as the author wrote them, and `mutatePatchFile` commits one edit under the cross-process writer lock, through an atomic replace, and reads the written text back through the parser before reporting what every reader will now load.
-
-## Table of Contents
-
-- [Use this package](#use-this-package)
-- [Understand the implementation](#understand-the-implementation)
-- [Further Exploration](#further-exploration)
-- [Model Experience](#model-experience)
-- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
-- [Dev Note](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## Use this package
-
-Read a patch layer with `parsePatchList` (text in hand) or `readPatchListFile` (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.
-
-Write through `mutatePatchFile`. The callback receives a `PatchDocument` and edits it by row id, the way the Loader addresses rows:
-
-```ts
-import { mutatePatchFile } from '@deepseek-ai/dsh-patch-file'
-
-const file = '/home/me/.dsh/profiles/web/cordis.patch.yml'
-await mutatePatchFile(file, (document) => {
-  document.setRowField('tool-web', 'disabled', true)      // the id-targeted patch is created when absent
-  document.deleteRowField('tool-web', 'config')           // a patch reduced to its id is removed whole
-  document.appendInsert({ id: 'tool-foo', name: 'dsh-tool-foo' })          // into the root list
-  document.appendInsert({ id: 'sql', name: 'dsh-sql' }, 'agents')          // into the group with that id
-  document.removeInsert('tool-foo')                       // an emptied insert patch is removed whole
-}, { binName: 'dsh', mode: 0o600, dirMode: 0o700 })
-```
-
-`setRowField` never accepts `id` or `insert`; `rowField` reads one key back, with a `!!js` scalar returned as its source text. `appendInsert` refuses an id the file already inserts, and `insertedRow`/`removeInsert` find rows inside inserted groups too. Values written are plain data; a `!!js` scalar on another key is left untouched, which is what lets a user layer revert a `disabled: true` it wrote without disturbing a bundle's `!!js` gate on a different row.
-
-`mutatePatchFile` takes the `<file>.lock` sibling the way `dsh-atomic-write` does, reads the file (absent reads as empty), applies the edit, replaces the file atomically with the stated permission bits when the text changed, and returns the patch list as re-read from the written text. An edit that changes nothing writes nothing.
-
------
-
-<a id="understand-the-implementation"></a>
-## Understand the implementation
-
-<details>
-<summary>Implementation internals — click to expand</summary>
-
-### Two parsers, one dialect
-
-Reading uses `js-yaml` with the include's `entryListSchema`, so `!!js` scalars become the expression nodes the Loader interpolates, exactly as the include mounts them. Writing uses the `yaml` package's comment-preserving `Document`: it keeps the unresolved `!!js` tag on the scalar it decorates (reported as a `TAG_RESOLVE_FAILED` warning, not an error) and prints it back verbatim, so an edit to one key never rewrites another key's expression. The written text is parsed back with the reading parser, which is the readback the writer's contract promises.
-
-### Addressing
-
-An id-targeted patch is the top-level item whose `id` matches and that carries no `insert`. An inserted row is searched in every `insert` list, recursing into inserted groups (`group: true` with a `config` list). A top-level item that is not a mapping, or an `insert` whose value is not a list, fails the parse.
-
-### Source map
-
-| File | Role |
-|---|---|
-| [`src/index.ts`](src/index.ts) | `parsePatchList`, `readPatchListFile`, `anchorInsertedPluginNames`, `PatchDocument`, `mutatePatchFile` |
-| — | No runtime invariant companion is published; the package holds no runtime state, and its file contract is pinned by unit tests. |
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## Further Exploration
-
-Read these when the file format's place in the composition is the question.
-
-- [App boot](../../boot/app-boot/README.md) — the profile launcher that composes bundle layers, user layers, and overlays from these files.
-- [Cordis include plugin](../../../vendor/include/README.md) — `applyEntryPatches`, the patch semantics every layer is applied with.
-- [Agent presets](../../preset/agent-presets/README.md) — the per-preset user layer written in this format.
-
------
-
-<a id="model-experience"></a>
-## Model Experience
-
-None, as the package only reads and writes composition patch files; the rows they name own every model-facing registration.
-
-#### KV Cache effect
-
-None; this package neither assembles nor sends a provider request.
-
-## Known Limitations and Deferred Work
-
-<a id="known-limitations-and-deferred-work"></a>
-
-
-These limits define what the writer will not do to a file. They are current package constraints, not a task backlog.
-
-- **Key-level, not row-level, merging** — `setRowField('x', 'config', value)` replaces the whole `config` mapping of that patch; a caller that wants one nested field changed reads the current value with `rowField` and writes the merged mapping back.
-- **No expression authoring** — the writer emits plain data only; a `!!js` gate is something an author types into the file, never something an API call produces.
-- **Lock orphans are an operator action** — a lock file left by a crashed writer is never removed by a contender, which fails after the wait instead; `dsh-atomic-write` documents the same choice.
-
-<a id="dev-note"></a>
-### Dev Note
-
-<details>
-<summary>Working context for maintainers — click to expand</summary>
-
-None.
-
-</details>

+ 0 - 115
packages/util/patch-file/README.zh.md

@@ -1,115 +0,0 @@
----
-description: "Loader 补丁列表文件:cordis.patch.yml 各层共用的解析器,以及带锁、原子替换与回读的、保留注释的键级写入器。"
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-patch-file
-
-[English](README.md) | 中文
-
-## 概述
-
-`dsh-patch-file` 拥有 harness 每一个组合层所使用的文件格式:一个 `cordis.patch.yml` 是 Loader 补丁条目的顶层 YAML 序列——按 id 定位的覆盖(如 `disabled: true` 或替换 `config`)与插入新行的 `insert` 列表——采用 Loader 自己的方言,其中 `!!js` 标量是由该行 fiber 求值的表达式。`parsePatchList` 是 profile launcher、组合包层、`--patch` 覆盖层与 agent preset 用户层共用的唯一解析器,因此其中一方接受的文件所有人都接受。`PatchDocument` 在键级编辑这样的文件,同时保留作者写下的注释、空行与 `!!js` 标量;`mutatePatchFile` 在跨进程写锁下提交一次编辑,经原子替换落盘,并在报告结果前把写入的文本经解析器回读,告诉调用方每个读者此后会加载到什么。
-
-## 目录
-
-- [使用本包](#use-this-package)
-- [理解实现](#understand-the-implementation)
-- [进一步探索](#further-exploration)
-- [模型体验](#model-experience)
-- [已知限制与延期工作](#known-limitations-and-deferred-work)
-- [开发备注](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## 使用本包
-
-用 `parsePatchList`(已有文本)或 `readPatchListFile`(文件不存在时读到 `undefined`)读取一个补丁层。两者都把 `insert` 行中相对的名字(如 `./plugin.js`)锚定到文件自己的目录,并对任何不是"映射序列"的内容直接报错,因为一个完全无法施加的补丁文件就是配置错误;而目标行不存在的单条补丁仍然只是 Loader 的逐条警告。
-
-通过 `mutatePatchFile` 写入。回调拿到一个 `PatchDocument`,按 Loader 寻址行的方式以行 id 编辑:
-
-```ts
-import { mutatePatchFile } from '@deepseek-ai/dsh-patch-file'
-
-const file = '/home/me/.dsh/profiles/web/cordis.patch.yml'
-await mutatePatchFile(file, (document) => {
-  document.setRowField('tool-web', 'disabled', true)      // the id-targeted patch is created when absent
-  document.deleteRowField('tool-web', 'config')           // a patch reduced to its id is removed whole
-  document.appendInsert({ id: 'tool-foo', name: 'dsh-tool-foo' })          // into the root list
-  document.appendInsert({ id: 'sql', name: 'dsh-sql' }, 'agents')          // into the group with that id
-  document.removeInsert('tool-foo')                       // an emptied insert patch is removed whole
-}, { binName: 'dsh', mode: 0o600, dirMode: 0o700 })
-```
-
-`setRowField` 永远不接受 `id` 与 `insert`;`rowField` 读回一个键,`!!js` 标量以其源文本返回。`appendInsert` 拒绝文件已插入的 id;`insertedRow`/`removeInsert` 也能找到插入组内部的行。写入的值都是普通数据;别的键上的 `!!js` 标量原样不动,这正是用户层能撤回自己写的 `disabled: true` 而不惊动组合包在另一行上的 `!!js` 门的原因。
-
-`mutatePatchFile` 像 `dsh-atomic-write` 一样占用 `<file>.lock` 兄弟文件,读取文件(不存在按空处理),施加编辑,在文本有变化时以声明的权限位原子替换文件,然后返回从写入文本重新读出的补丁列表。什么都没改的编辑什么都不写。
-
------
-
-<a id="understand-the-implementation"></a>
-## 理解实现
-
-<details>
-<summary>实现内幕——点击展开</summary>
-
-### 两个解析器,一种方言
-
-读取用 `js-yaml` 配 include 的 `entryListSchema`,于是 `!!js` 标量成为 Loader 插值的表达式节点,与 include 挂载时完全一致。写入用 `yaml` 包保留注释的 `Document`:它在所修饰的标量上保留未解析的 `!!js` 标签(报告为 `TAG_RESOLVE_FAILED` 警告而非错误)并原样打印回去,因此对一个键的编辑绝不会改写另一个键的表达式。写出的文本再用读取解析器解析一遍,这就是写入器契约承诺的回读。
-
-### 寻址
-
-按 id 定位的补丁是 `id` 匹配且不带 `insert` 的顶层项。插入的行在每个 `insert` 列表中查找,并递归进入插入的组(`group: true` 且带 `config` 列表)。顶层项不是映射,或 `insert` 的值不是列表,都会让解析失败。
-
-### 源码地图
-
-| 文件 | 职责 |
-|---|---|
-| [`src/index.ts`](src/index.ts) | `parsePatchList`、`readPatchListFile`、`anchorInsertedPluginNames`、`PatchDocument`、`mutatePatchFile` |
-| — | 不发布运行时不变量伴随件;本包不持有运行时状态,其文件契约由单元测试钉住。 |
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## 进一步探索
-
-当问题在于这种文件格式在组合中的位置时,读这些。
-
-- [App boot](../../boot/app-boot/README.zh.md)——从这些文件组合组合包层、用户层与覆盖层的 profile launcher。
-- [Cordis include 插件](../../../vendor/include/README.md)——`applyEntryPatches`,每一层施加时所用的补丁语义。
-- [Agent presets](../../preset/agent-presets/README.zh.md)——以此格式书写的每预设用户层。
-
------
-
-<a id="model-experience"></a>
-## 模型体验
-
-无,本包只读写组合补丁文件;这些文件点名的行拥有全部面向模型的注册。
-
-#### KV Cache 影响
-
-无;本包既不组装也不发送 provider 请求。
-
-## 已知限制与延期工作
-
-<a id="known-limitations-and-deferred-work"></a>
-
-
-这些限制界定写入器不会对文件做什么。它们是当前包的约束,不是任务清单。
-
-- **键级而非行级合并**——`setRowField('x', 'config', value)` 替换该补丁的整个 `config` 映射;只想改一个嵌套字段的调用方先用 `rowField` 读出当前值,再把合并后的映射写回。
-- **不生成表达式**——写入器只输出普通数据;`!!js` 门是作者敲进文件的东西,从不是某个 API 调用的产物。
-- **锁孤儿由操作者处理**——写入器崩溃留下的锁文件不会被竞争者移除,竞争者等待超时后失败;`dsh-atomic-write` 记录了同样的选择。
-
-<a id="dev-note"></a>
-### 开发备注
-
-<details>
-<summary>维护者工作上下文——点击展开</summary>
-
-无。
-
-</details>

+ 0 - 45
packages/util/patch-file/package.json

@@ -1,45 +0,0 @@
-{
-  "name": "@deepseek-ai/dsh-patch-file",
-  "description": "Loader patch-list files (cordis.patch.yml): the shared parser with the !!js dialect and relative-name anchoring, and a comment-preserving key-level writer with lock, atomic replace, and readback",
-  "version": "0.1.3-alpha.1",
-  "publishConfig": {
-    "access": "public"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
-    "directory": "packages/util/patch-file"
-  },
-  "type": "module",
-  "main": "lib/index.js",
-  "types": "lib/types/index.d.ts",
-  "exports": {
-    ".": {
-      "types": "./lib/types/index.d.ts",
-      "default": "./lib/index.js"
-    },
-    "./src/*": "./src/*",
-    "./package.json": "./package.json"
-  },
-  "files": [
-    "lib/index.js",
-    "lib/types/**/*.d.ts"
-  ],
-  "license": "MIT",
-  "dependencies": {
-    "@deepseek-ai/dsh-atomic-write": "workspace:^",
-    "js-yaml": "^4.2.0",
-    "yaml": "^2.9.0"
-  },
-  "peerDependencies": {
-    "@deepseek-ai/cordis": "workspace:^",
-    "@deepseek-ai/cordis-plugin-include": "workspace:^",
-    "@deepseek-ai/cordis-plugin-loader": "workspace:^"
-  },
-  "devDependencies": {
-    "@deepseek-ai/cordis": "workspace:^",
-    "@deepseek-ai/cordis-plugin-include": "workspace:^",
-    "@deepseek-ai/cordis-plugin-loader": "workspace:^",
-    "@types/js-yaml": "^4.0.9"
-  }
-}

+ 0 - 24
packages/util/patch-file/tsconfig.json

@@ -1,24 +0,0 @@
-{
-  "extends": "../../../tsconfig.base.json",
-  "compilerOptions": {
-    "rootDir": "src",
-    "outDir": "lib/types"
-  },
-  "include": [
-    "src"
-  ],
-  "references": [
-    {
-      "path": "../../../vendor/cordis"
-    },
-    {
-      "path": "../../../vendor/include"
-    },
-    {
-      "path": "../../../vendor/loader"
-    },
-    {
-      "path": "../atomic-write"
-    }
-  ]
-}

+ 6 - 34
pnpm-lock.yaml

@@ -1051,15 +1051,15 @@ importers:
       '@deepseek-ai/dsh-package-manifest':
         specifier: workspace:^
         version: link:../../util/package-manifest
-      '@deepseek-ai/dsh-patch-file':
-        specifier: workspace:^
-        version: link:../../util/patch-file
       js-yaml:
         specifier: ^4.2.0
         version: 4.2.0
       resolve.exports:
         specifier: ^2.0.3
         version: 2.0.3
+      yaml:
+        specifier: ^2.9.0
+        version: 2.9.0
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -1124,9 +1124,6 @@ importers:
       '@deepseek-ai/dsh-package-manifest':
         specifier: workspace:^
         version: link:../../util/package-manifest
-      '@deepseek-ai/dsh-patch-file':
-        specifier: workspace:^
-        version: link:../../util/patch-file
       '@deepseek-ai/dsh-util-values':
         specifier: workspace:^
         version: link:../../util/values
@@ -6837,6 +6834,9 @@ 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
@@ -6849,9 +6849,6 @@ importers:
       '@deepseek-ai/dsh-llm':
         specifier: workspace:^
         version: link:../../llm/llm
-      '@deepseek-ai/dsh-patch-file':
-        specifier: workspace:^
-        version: link:../../util/patch-file
       '@deepseek-ai/dsh-scope':
         specifier: workspace:^
         version: link:../../core/scope
@@ -9666,31 +9663,6 @@ importers:
         specifier: workspace:^
         version: link:../../../vendor/cordis
 
-  packages/util/patch-file:
-    dependencies:
-      '@deepseek-ai/dsh-atomic-write':
-        specifier: workspace:^
-        version: link:../atomic-write
-      js-yaml:
-        specifier: ^4.2.0
-        version: 4.3.1
-      yaml:
-        specifier: ^2.9.0
-        version: 2.9.0
-    devDependencies:
-      '@deepseek-ai/cordis':
-        specifier: workspace:^
-        version: link:../../../vendor/cordis
-      '@deepseek-ai/cordis-plugin-include':
-        specifier: workspace:^
-        version: link:../../../vendor/include
-      '@deepseek-ai/cordis-plugin-loader':
-        specifier: workspace:^
-        version: link:../../../vendor/loader
-      '@types/js-yaml':
-        specifier: ^4.0.9
-        version: 4.0.9
-
   packages/util/time:
     devDependencies:
       '@deepseek-ai/cordis':

+ 1 - 1
scripts/check-workspace-constraints.ts

@@ -170,7 +170,7 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
   '@deepseek-ai/dsh-experimental-webworker-packer': ['bin.js', 'lib/repository-*.js'],
   '@deepseek-ai/dsh-subprocess-local': ['scripts/ensure-spawn-helper.mjs'],
   // The package probe's child process entry, path-loaded beside the lib.
-  '@deepseek-ai/dsh-app-boot': ['lib/probe-child.js'],
+  '@deepseek-ai/dsh-app-boot': ['lib/probe-child.js', 'lib/patch-file.js'],
 }
 
 function sameStringList(actual: readonly string[] | undefined, expected: readonly string[]): boolean {

+ 0 - 1
scripts/verify-package-readme-model-experience.ts

@@ -57,7 +57,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
   'packages/client/ui-agent-preset': { kind: 'indirect', reason: 'Browser-side settings row; the preset it selects owns every model-facing effect.' },
   'packages/util/crypto': { kind: 'indirect', reason: 'Pure identifier minting; the ids consumers mint with it never enter prompts as semantic content.' },
   'packages/util/deque': { kind: 'none', reason: 'In-process collection primitive; registers nothing model-facing.' },
-  'packages/util/patch-file': { kind: 'none', reason: 'Composition patch-file parser and writer; the rows those files name own every model-facing registration.' },
   'packages/util/package-manifest': { kind: 'none', reason: 'Type declarations only; registers nothing model-facing.' },
   'packages/util/time': { kind: 'indirect', reason: 'Pure zone validation; the consumer that records a canonical zone owns the model-visible line derived from it.' },
   'packages/core/agent-default-model': { kind: 'indirect', reason: 'The service supplies a ModelSelection; request assembly and adapters own the model-visible request.' },

+ 1 - 1
tsconfig.base.json

@@ -150,6 +150,7 @@
       "@deepseek-ai/dsh-host-plugin-inventory": ["./packages/host/plugin-inventory/src"],
       "@deepseek-ai/dsh-host-plugin-inventory/types": ["./packages/host/plugin-inventory/src/types.ts"],
       "@deepseek-ai/dsh-host-plugin-manager": ["./packages/host/plugin-manager/src"],
+      "@deepseek-ai/dsh-app-boot/patch-file": ["./packages/boot/app-boot/src/patch-file.ts"],
       "@deepseek-ai/dsh-host-plugin-manager/types": ["./packages/host/plugin-manager/src/types.ts"],
       "@deepseek-ai/dsh-plugin-manager/types": ["./packages/boot/plugin-manager/src/types.ts"],
       "@deepseek-ai/dsh-client-ui-slots": ["./packages/client/ui-slots/src"],
@@ -313,7 +314,6 @@
       "@deepseek-ai/dsh-native-command": ["./packages/util/native-command/src"],
       "@deepseek-ai/dsh-output-retention": ["./packages/util/output-retention/src"],
       "@deepseek-ai/dsh-package-manifest": ["./packages/util/package-manifest/src"],
-      "@deepseek-ai/dsh-patch-file": ["./packages/util/patch-file/src"],
       "@deepseek-ai/dsh-permission-presets": ["./packages/interaction/permission-presets/src"],
       "@deepseek-ai/dsh-permission-presets/invariant": ["./packages/interaction/permission-presets/src/invariant.ts"],
       "@deepseek-ai/dsh-persona": ["./packages/preset/persona/src"],

+ 0 - 1
tsconfig.host.json

@@ -148,7 +148,6 @@
     { "path": "./packages/util/workspace-path" },
     { "path": "./packages/util/output-retention" },
     { "path": "./packages/util/atomic-write" },
-    { "path": "./packages/util/patch-file" },
     { "path": "./packages/attachment/attachment" },
     { "path": "./packages/attachment/attachment-local" },
     { "path": "./packages/client/file-upload/tsconfig.host.json" },