# Agent Note: dsh CLI 与来自 Harness home 的个人配置 overlay

Status: implemented

[English](2026-07-20-dsh-cli-personal-config.md) | 中文

## Problem

开发者自己的偏好——TUI 使用哪个提供方和模型、个人凭证、私有的适配器路由——除了改动已提交的文件之外无处安放。要把 TUI 示例指向个人的 Anthropic 代理 Opus 路由，只能在工作区里改 `examples/tui-agent/cordis.yml` 和 `.env`，既有提交密钥的风险，又要在每个 checkout 里重复一遍。也没有可安装的命令：想在任意项目目录里运行这个 agent，必须回到仓库根目录调用示例脚本。Loader 元数据是静态的，所以「条件组合使用 overlay」（AGENTS.md）——但 overlay 此前只以已提交的同级文件形式存在，没有机器级的层。

## Decision

两个耦合的部分，与 `dsh web` PR（#443）提出的 `apps/` 装配层对齐：

**`dsh` CLI（`apps/cli`，npm 名 `@deepseek-ai/dsh`）。** `apps/*` 是位于 `packages/*` 库之上的产品组装层。一个 bin 负责分发默认交互式 TUI、`-p`/`--prompt` 无头轮次和 `web` 界面。TUI 以调用目录为 workspace，启动 `examples/tui-agent/cordis.yml`（或 `--config` 指定的配置）。已提交的 `bin/dsh` 启动器通过自身真实路径解析 checkout，并使用 tsx 的 ESM hook 运行应用；该契约由[源码启动决策](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)维护。`pnpm run demo:tui` 运行同一入口。

**个人配置（`dsh-app-boot`）。** 个人 overlay 存放在 Harness home——`$DSH_HOME`，否则 `~/.dsh`——由共享的 [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md)（`@deepseek-ai/dsh-paths`）解析，与 skills、AGENTS.md 解析所依据的单一根目录相同。dsh 的 TUI、Web 和无头界面使用其中两个可选文件；各示例 bin 仍然逐字节按已提交的配置树启动：

- `.env`——在调用目录的 `.env` 之后加载；`process.loadEnvFile` 从不覆盖已有值，因此优先级为环境变量 > 项目 `.env` > 个人 `.env`。
- `config.yaml`——顶层 YAML 数组，元素为 `@cordisjs/plugin-include` 的 `PatchOptions`，用 include 自己的 `!!js` 方言解析（`loadPersonalPatches`）并传给 `boot()`，由它作为根 include 的 `patches` 转发。补丁语义与交付的 surface overlay 一致：按 id 定位的补丁替换该配置项的整个 `config`，`insert` 追加配置项，未匹配的 id 静默不执行任何操作。[仓库插件集成](2026-07-30-config-only-repository-plugins.md)通过一个已交付配置项，使精确 GitHub 源列表成为纯配置选择。
- 文件缺失即无 overlay；文件存在但不可读、不可解析或非数组则在启动时抛出（配置错误响亮失败，绝不静默跳过）。

PTY 冒烟测试的启动器把 `$DSH_HOME` 隔离到每个测试自己的目录，与它已有的 `DSH_AGENTS_HOME` 隔离方式完全一致，开发者真实的个人 overlay 不可能泄漏进 fixture；只有 dsh CLI 读取个人配置，因此其他测试启动器无需改动。

TUI 和 Web 启动后通过 Cordis HMR（热模块替换）注册确切的个人配置路径。每次新增、变更或移除都会以事务方式通过启动器自己的组合闭包重新组合完整 patch 列表，因此新的个人 patch 落在启动时相同的层次位置。YAML 无效或 Loader 候选被拒时，最后一个可用树保持活动状态，并广播 `hmr/config-update-failed(filename, Error)`；无头界面只在启动时读取该文件。Include 在已提交配置文件刷新时也会重新应用其 patch（见[配置热重载韧性 Agent Note](../bug-fix/2026-07-20-config-hot-reload-resilience.md)）。

## Alternatives considered

**另设一个 `bin/dsh` 包装脚本并由其占用 `dsh` 名称。** 否决，因为 `apps/cli` 是统一的产品 CLI，负责分发默认 TUI、无头和 Web 界面。两个相互竞争的入口会在 `$PATH` 和产品身份上冲突。

**pi 风格的类型化设置文件（`defaultProvider`/`defaultModel`/`providers`）。** 用户否决，选择补丁语义：个人文件是叠加在随仓库提供的默认配置之上的 cordis overlay，而不是需要另行拥有和翻译的第二套配置词汇。

**个人完整 `cordis.yml` 去 include 请求的配置。** 否决：个人文件将不得不写死叶子配置的路径，而该路径随 checkout 变化；补丁反转了依赖方向，bin 仍然选择配置树，个人层只做修正。

**把个人补丁深合并进配置项配置。** 否决：会使补丁语义与已提交 overlay 和 vendor 的 include 分叉；整个 `config` 替换已是成文契约。

**用环境变量开关代替存在性判断。** 否决：默认关闭的个人配置永远不会被用起来；存在即生效加上每个测试的显式隔离，让实际运行获得 overlay、测试获得封闭性。

## Consequences

- 在任意目录运行 `dsh`（以及 `pnpm run demo:tui`），无需修改 checkout，即可应用个人提供方、模型、仓库插件和其他 Loader 配置项；已针对个人 Anthropic 代理与 Opus 4.8 端到端验证，包括一次 bash 工具往返。
- 由于按 id 定位的补丁替换整个 `config`，个人覆盖必须复述它保留的基础字段，并可能随基础配置项形态变化而漂移；诊断手段是 loader 的「配置项未找到/名称不匹配」警告和 [`dsh --dump-config`](2026-07-30-dsh-dump-config.md)（打印这些补丁合成出的配置树）。
- 个人补丁只在被启动文件自身的树里解析 id，因此嵌套 include 的 overlay（Code Mode）不会被个性化；这些叶子的实际运行等价性暂缓。
- `dsh-app-boot` 依赖 `js-yaml`，并直接导入 include 的 `!!js` YAML 方言（`entryListSchema`）；与 `apps/cli` 一样依赖 `@deepseek-ai/dsh-paths` 以获取 `resolveDshHome`。
- 只有长时间运行的 TUI 和 Web 进程进行实时监视。无头自动化使用确定性的启动配置，退出时不会保留 watcher。

## Testing

`packages/ui/app-boot/tests/personal-config.spec.ts` 固定解析、启动时应用、确切路径的新增／失败／恢复／移除、最后可用状态回滚、失败广播以及应用自有 patch 的保留。`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 启动真实 dsh bin，覆盖无 overlay、个人环境与 UI patch、纯配置的缓存 repository skill，以及无效个人 YAML。测试启动器会隔离 `$DSH_HOME`，因此开发者的真实 overlay 不会泄漏进 fixture。
