# Agent Note：损坏的 preset 是名单行，不是空缺

Status: implemented

[English](2026-08-09-broken-preset-roster-rows.md) | 中文

## 问题

文件成为唯一的组装编辑器之后，手动编辑造成的损坏有两种形态，且都要拖到最糟的时刻才暴露。`agent.cordis.yml` 解析不了的 preset 在名单上是一张完全正常的行——可选择、可复制、可设为默认——直到下一个会话尝试挂载才失败；一旦被设为默认，所有新会话都无法启动。组装文件被整个删掉的目录则从名单上消失，却仍在磁盘上占着它的 id：`copy` 以「先删除既有 preset」拒绝这个名字，`remove` 却回答「找不到」——两条互相矛盾的错误，除了手动删目录别无出路。

## 决定

发现过程负责健康，受损目录是**携带 `broken` 原因的名单行**，绝不是空缺。`scanRoot` 把名字是可用 preset id 的每个目录都当作一个 preset 槽位：组装缺失 → broken（「仍占着该 id；删除目录或恢复文件」），组装不可读/解析失败/不是具名行列表 → broken 并携带解析器的首行。形状检查用加载器自己的 `entryListSchema`（含 `!!js` 的方言）解析，因此健康检查绝不会把加载器接受的组装叫作损坏；名字不符合 `PRESET_ID` 的目录直接跳过，因为复制永远不可能与之相撞。`broken` 依次落在 `AgentPreset`、`agentPreset.list` 的线上条目和 UI 行上。挂载路径（`mount`/`recompose`/`standingKeyFor`）经 `resolveMountable` 用发现时记下的原因在前置拒绝；`resolve` 照样应答（删除/读取/上报都需要这一行），而 `copy` 的名单检查现在看得见幽灵，让「已存在」的拒绝变得可操作——要删的损坏卡片就在同一页上。

界面按职责分开：管理区把损坏行渲染为标记卡片（红边、「已损坏」徽记、原样展示原因、卡片主体与复制禁用，自定义行保留位置与删除——文件正是修复处，删除正是幽灵的出路；损坏的内置行连查看器也不给），而两个选择器（通用设置行、新会话 chip）经 `presetOptions` 完全不列损坏的 preset——它们选的是下一个会话的组装，端出无法组装的选项只会推迟失败。

## 后果

- 幽灵死路端到端消除：目录以损坏行列出，删除即清掉，释放的 id 立刻可用（单测、组件测试与 e2e 各自覆盖）。
- 事后才损坏的默认值仍会在会话启动处大声失败——选择器隐藏损坏行，但没有任何东西改写已存的默认；`resolveMountable` 的前置拒绝让每种不可加载形态得到同一条消息，而不是依赖加载器内部的报错。
- 健康检查随每次 `list()` 运行：每次读名单对每个 preset 一次读取加解析，接受的理由与不做缓存的发现相同——名单很小，新鲜是契约。
- 复制损坏 preset 只在 UI 层拒绝（按钮禁用并给出原因）；宿主的 `copy` 保持形状无关。损坏来源产出同样损坏、同样可见的副本——没有能力增益，而宿主侧拒绝需要为一条被禁用按钮挡住的路径专门发明错误词汇。

## 关键细节

- **`PRESET_ID` 移到 `types.ts`**，让发现与创作共享同一份包含边界词汇；authoring 原样转发导出。
- **原因只留一行。** js-yaml 会附上多行代码框摘录；名单卡片不是终端，`compositionProblem` 只保留首行。
- **mount.spec 的两个竞态用例特意不动**：`ensureStanding` 仍可能拿到删除前一刻解析出的 preset（私有路径测试），其 stamp/unstampable 语义不变——健康检查发生在此之前的公开路径上。
- **创造模式的引导随同一 PR 落地**：`cordis` preset 的 persona 现在禁止编辑随附安装（损坏 `cordis` 会禁用这一模式本身），并把创作指向 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/`；其技能新教了 `preset.yml` 元信息、先复制再改的流程、一次升级的沙箱现实（preset 根目录在会话工作区之外）与诚实的验证方式（agent 无法自己启动会话；设置页的红色标记是用户的检查项）。已实测：被要求直接改随附 `cordis` 组装时，组装出的 agent 援引两条规则拒绝并给出复制路径；被要求真正创建 preset 时，它落在 `$DSH_HOME` 下、把写入合并为一次升级、用加载器方言自查、并把验证交还用户。

## 曾考虑的替代方案

隐藏损坏 preset 但在复制时用更好的报错拒绝该 id：幽灵仍然无法从任何界面清除。深度校验（读名单时解析每一行的模块）：挂载已经拥有这一失败并带回滚，每次读名单逐行 import 既不便宜也不更可操作。阻止 `settings` 写入指向损坏默认值：settings 领域是通用的，而名单是活目录——此刻缺失或损坏的名字到下一个会话可能已经有效，挂载的响亮失败才是拥有那一刻的强制点。
