Browse Source

docs(cli): sync the derived preset-root layer into launcher docs

Review follow-ups: enumerate the derived shipped agent-preset root in
apps/cli README/reference dumps and the profile-boot module JSDoc
(bilingual pairs re-recorded), correct the stale AppCLIEntry/distIndex
analogy in the web scaffold and the shipped-root cross-reference in the
web preset e2e, drop the write-only ComposedProfile.rows field, and make
the Agent Note describe the dump path as sharing the derivation rather
than the builder.
Yichen Jiang 1 month ago
parent
commit
25058f2658

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-20-derive-shipped-preset-root-per-composition.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/bug-fix/2026-08-20-derive-shipped-preset-root-per-composition.md
-2026-08-20-derive-shipped-preset-root-per-composition.md: b303f6a5d08ac2c2ca755d5dbf46eb9a74f5c4ee
-2026-08-20-derive-shipped-preset-root-per-composition.zh.md: cc28789898a74df285a96b7e17c35e3b4d12c452
+2026-08-20-derive-shipped-preset-root-per-composition.md: ecf852ade3720cbf5f5f99efa073cc6e3a352fec
+2026-08-20-derive-shipped-preset-root-per-composition.zh.md: 718bddd6e4159b32db63262bb40a1e0ce38227ac

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-20-derive-shipped-preset-root-per-composition.md

@@ -12,7 +12,7 @@ The overlay also sat in `ComposedProfile.overlays`, the fixed top layers a live
 
 ## Decision
 
-The shipped root is a derivation, not an overlay. `resolveShippedPresetPatch(rows)` builds the roster patch from one composed row set: it keeps every configured key and prepends the shipped root (`system` trust) to the composition's `roots`, so the shipped presets always mount and win a duplicate id while configured roots stay live. `composeProfilePatches(layers)` appends that patch to the flattened stack and is the one builder boot, the live user-layer reloads, and the config dump all go through — a reload derives from the current user layers instead of replaying a boot snapshot, and the dump now renders the derived layer (labeled `dsh launcher (shipped agent-preset root)`) so it composes the roster row exactly as it boots. The telemetry switch stays a boot-only overlay: it is an environment fact of the booting process, carries no config snapshot, and outranking user edits is its purpose.
+The shipped root is a derivation, not an overlay. `resolveShippedPresetPatch(rows)` builds the roster patch from one composed row set: it keeps every configured key and prepends the shipped root (`system` trust) to the composition's `roots`, so the shipped presets always mount and win a duplicate id while configured roots stay live. `composeProfilePatches(layers)` appends that patch to the flattened stack and is the builder boot and the live user-layer reloads share — a reload derives from the current user layers instead of replaying a boot snapshot. The config dump shares the derivation rather than the builder: `renderConfigDump` needs one labeled layer per source, so `runDumpConfig` appends `resolveShippedPresetPatch`'s output as its own layer (labeled `dsh launcher (shipped agent-preset root)`) and composes the roster row exactly as it boots. The telemetry switch stays a boot-only overlay: it is an environment fact of the booting process, carries no config snapshot, and outranking user edits is its purpose.
 
 A `roots` value the launcher cannot statically rewrite — a `!!js` expression or any non-array — now fails loud with a `TypeError` naming the constraint, instead of being silently replaced. The plugin's own contract is untouched: `config.roots` scanned in order, the writable home root appended by `dsh-agent-presets` itself.
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-20-derive-shipped-preset-root-per-composition.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 ## 决定
 
-内置根是一个派生,不是一个 overlay。`resolveShippedPresetPatch(rows)` 从一份已组合的行集构建 roster 补丁:保留全部已配置的键,并把内置根(`system` 信任)前置到组合的 `roots` 中,因此内置 preset 始终挂载并在 id 冲突时胜出,而配置的根目录保持生效。`composeProfilePatches(layers)` 把该补丁追加到展平后的补丁栈,是启动、用户层热重载与配置 dump 共同经过的唯一构建器——热重载从当前用户层派生而非重放启动快照,dump 也渲染这个派生层(标注为 `dsh launcher (shipped agent-preset root)`),使 roster 行的组合与实际启动完全一致。遥测开关仍是仅启动时的 overlay:它是启动进程的环境事实,不携带 config 快照,压过用户编辑正是其目的。
+内置根是一个派生,不是一个 overlay。`resolveShippedPresetPatch(rows)` 从一份已组合的行集构建 roster 补丁:保留全部已配置的键,并把内置根(`system` 信任)前置到组合的 `roots` 中,因此内置 preset 始终挂载并在 id 冲突时胜出,而配置的根目录保持生效。`composeProfilePatches(layers)` 把该补丁追加到展平后的补丁栈,是启动与用户层热重载共用的构建器——热重载从当前用户层派生而非重放启动快照。配置 dump 共用的是派生本身而非构建器:`renderConfigDump` 需要逐层标注来源,所以 `runDumpConfig` 把 `resolveShippedPresetPatch` 的输出作为独立一层追加(标注为 `dsh launcher (shipped agent-preset root)`),对 roster 行的组合与实际启动完全一致。遥测开关仍是仅启动时的 overlay:它是启动进程的环境事实,不携带 config 快照,压过用户编辑正是其目的。
 
 启动器无法静态改写的 `roots` 值——`!!js` 表达式或任何非数组——现在以指明约束的 `TypeError` 大声失败,而不是被静默替换。插件自身的契约不变:`config.roots` 按序扫描,可写 home 根由 `dsh-agent-presets` 自己追加。
 

+ 2 - 2
apps/cli/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 apps/cli/README.md
-README.md: 9a8d722b044ed5d8e31e3c27e54f8c9ef0839f82
-README.zh.md: c092414e2d15e90133d8ea27f0af5cadbe527b22
+README.md: ae6f4c38eee402bcc1a86ba34778afd06da749b1
+README.zh.md: d4661476e4310b987b22fec7a7d417b9ea6811ab

+ 1 - 0
apps/cli/README.md

@@ -35,6 +35,7 @@ The tree composes over an empty root:
 - each bundle's patch in `dsh.profile.bundles` order
 - then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`
 - then `--patch` overlays
+- then, when the composition mounts the preset roster, a launcher-derived patch that prepends the shipped agent-preset root to the configured `roots`
 
 Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins.
 

+ 1 - 0
apps/cli/README.zh.md

@@ -37,6 +37,7 @@ profile 目录包含一个 `package.json`,其中记录树外插件依赖,以
 - `dsh.profile.bundles` 中各组合包的 patch
 - profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`
 - `--patch` 指定的覆盖层
+- 组合挂载预设 roster 时,启动器再派生一个补丁,把内置 agent-preset 根目录前置到已配置的 `roots` 之前
 
 `dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。
 

+ 2 - 2
apps/cli/reference/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/cli/reference/README.md
-README.md: dfddd177a78c348793d3e5c2d290fa62c5ac850b
-README.zh.md: 8e7508b4b8fcbd39e15538c6ee88733bfa9905f1
+README.md: 2e8c262f5c1e7045472eaaa53657e9b9a7bab9a4
+README.zh.md: 27a63337b6166d3aa560a2205912575a007b0c72

+ 1 - 1
apps/cli/reference/README.md

@@ -36,7 +36,7 @@ dsh --profile web --dump-default-config
 dsh --profile web --patch ./extra.yml --dump-config
 ```
 
-`--dump-default-config` prints only the bundle layers; `--dump-config` adds the profile's `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and `--patch` overlays. Both print comments naming the file that supplied each row and every overlay that changed it; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr. A dump never runs app command-line providers, so it shows the composed tree before any app argument is resolved and rejects an invocation that carries app arguments.
+`--dump-default-config` prints the bundle layers; `--dump-config` adds the profile's `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and `--patch` overlays. When the composition mounts the preset roster, both also append the launcher-derived `dsh launcher (shipped agent-preset root)` layer, so the dump composes that row exactly as boot does. Both print comments naming the file that supplied each row and every overlay that changed it; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr. A dump never runs app command-line providers, so it shows the composed tree before any app argument is resolved and rejects an invocation that carries app arguments.
 
 ## Plugin management
 

+ 1 - 1
apps/cli/reference/README.zh.md

@@ -36,7 +36,7 @@ dsh --profile web --dump-default-config
 dsh --profile web --patch ./extra.yml --dump-config
 ```
 
-`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml` 和 `--patch` overlay。两者都会打印注释,标明每行由哪个文件提供,以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。dump 操作不会运行应用的命令行参数提供方,因此展示的是解析任何应用参数之前的组合配置树;如果调用中包含应用参数,dump 会拒绝该调用。
+`--dump-default-config` 打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml` 和 `--patch` overlay。当组合挂载预设 roster 时,两者还会追加启动器派生的 `dsh launcher (shipped agent-preset root)` 层,因此 dump 对该行的组合与实际启动完全一致。两者都会打印注释,标明每行由哪个文件提供,以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。dump 操作不会运行应用的命令行参数提供方,因此展示的是解析任何应用参数之前的组合配置树;如果调用中包含应用参数,dump 会拒绝该调用。
 
 ## 插件管理
 

+ 7 - 11
apps/cli/src/profile-boot.ts

@@ -1,9 +1,10 @@
 /**
  * Shared profile boot for every `dsh` surface: resolve the profile, stack its
  * patch layers (bundle layers in `dsh.profile.bundles` order, the profile's
- * own `cordis.patch.yml`, `--patch` overlays, the telemetry switch), mount the
- * tree over the profile's empty root config, keep the profile patch layer
- * live, and wire fail-loud plus bounded shutdown.
+ * own `cordis.patch.yml`, `--patch` overlays, the telemetry switch, and the
+ * per-composition derived shipped agent-preset root), mount the tree over the
+ * profile's empty root config, keep the profile patch layer live, and wire
+ * fail-loud plus bounded shutdown.
  *
  * App flags are not the launcher's business: the invocation's inner arguments
  * are provided to the tree through `ctx.cmdlineArgs`, where any injected app
@@ -102,7 +103,7 @@ export function prepareProfile(name: string, userLayer = true): Profile {
   return profile
 }
 
-/** One profile's patch layers (application order) and the row index of its pre-flag composition. */
+/** One profile's patch layers, in application order. */
 interface ComposedProfile {
   profile: Profile
   /** Bundle layers concatenated — the part below the user layers on a live reload. */
@@ -111,11 +112,6 @@ interface ComposedProfile {
   homePatches: PatchOptions[]
   /** Layers above the user layers on a live reload: `--patch` overlays and the telemetry switch. */
   overlays: PatchOptions[]
-  /**
-   * id → row of the composed tree (bundles + user layers + overlays), for the
-   * launcher's own row checks.
-   */
-  rows: ReadonlyMap<string, EntryOptions>
 }
 
 /** The full patch stack of one composed profile, in application order. */
@@ -199,7 +195,7 @@ export function composeProfilePatches(layers: readonly PatchOptions[][]): PatchO
  * then the telemetry switch.
  * @param name - the profile name.
  * @param patchFiles - `--patch` overlay paths, in argv order.
- * @returns the profile, its patch layers, and the composed row index.
+ * @returns the profile and its patch layers.
  */
 function composeProfile(
   name: string,
@@ -216,7 +212,7 @@ function composeProfile(
   const composedOverlays = [...overlays]
   const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
   if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
-  return { profile, bundlePatches, homePatches, overlays: composedOverlays, rows }
+  return { profile, bundlePatches, homePatches, overlays: composedOverlays }
 }
 
 /** Options for {@link runProfile}. */

+ 2 - 2
apps/cli/tests/web-agent-presets.e2e.ts

@@ -57,8 +57,8 @@ async function bootWeb(
     // The settings row defaults to `$DSH_HOME/settings.yaml`. Left alone it
     // reads the developer's own document — and since the default preset is a
     // setting, a stored `agent-presets.default` would decide this file's
-    // outcome. Point it at a temp file for the same reason the roster below
-    // names only the shipped root.
+    // outcome. Point it at a temp file for the same reason the roster row
+    // below pins `includeUserRoot` off.
     { id: 'settings', config: { path: settingsFile, watch: false } },
     // storage-json's root is anchored to the real $DSH_HOME. Unpinned, this
     // file writes the developer's own `~/.dsh/storages/` — and then reads it

+ 9 - 8
apps/web/tests/scaffold.ts

@@ -401,14 +401,15 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
     ...basePatches,
     ...surfacePatches,
     ...extraOverlayPatches,
-    // The roster's `roots` is an assembly fact AppCLIEntry resolves and patches
-    // in, exactly like `distIndex` on the webserver row — the shipped preset
-    // directory sits beside the composition that names it, and no config author
-    // chooses it. This lane boots the shipped tree WITHOUT AppCLIEntry, so it
-    // has to supply the same fact or the roster resolves nothing and every
-    // session composes an agent with no tools, no persona, and no token meter.
-    // Only the shipped root: a developer's own `~/.dsh/.agent-presets` must not be
-    // able to change a golden.
+    // The shipped preset root is an assembly fact only the launcher can
+    // resolve: it derives a patch per composition that PREPENDS the root to
+    // whatever `roots` the layers configured. This lane boots the shipped tree
+    // WITHOUT the launcher, so it supplies the same fact — literally rather
+    // than through the derivation, because a golden lane pins its roster — or
+    // the roster resolves nothing and every session composes an agent with no
+    // tools, no persona, and no token meter. Only the shipped root: a
+    // developer's own `~/.dsh/.agent-presets` must not be able to change a
+    // golden.
     {
       id: 'agent-presets',
       config: {