Skip to content
dsh.fish
Bundle

dsh-story-mode

DSH 短篇小说模式:一个包内含整套能力——agent preset(模式)、写作流程技能、文风契约技能、五个角色化只读审读员与四个结构化工具。一条命令安装,卸载即拔。

Source
Furry-wucheng
stars
1 stars
License
MIT
Updated
Updated yesterday

Readme

# dsh-story-mode · 短篇小说模式

给 **DeepSeek Harness (DSH)** 的一个写作专用模式。装好之后,新建会话时模式选择器里会多出「短篇小说模式」——进去的不是编码 agent,而是一个接稿的短篇小说作者。

整套能力(模式、技能、工具)都在**一个包**里,一条命令落地。

---

## 它解决什么问题

让可测量的内容由程序负责,让需要上下文的内容由读者判断。

| 程序测量或定位 | 审读判断 |
|---|---|
| 中文字数、分场、相对已确认目标的偏离 | 场景是否有效、节奏是否合适 |
| 引号内文字比例、段落长度 | 对话是否自然、人物口吻是否可区分 |
| 重复双字组合、常见用词位置 | 修辞是否有效、心理与情绪是否冗余 |
| 人物卡字段是否非空、姓名与别名字面提及 | 新人物、专名一致性、动机与跨篇设定 |
| 稿件内容版本、原文行号 | 视角、时态、因果与必要交代 |

字面命中不代表违规,统计正常不代表故事好看。工具不再根据“我/你/他”判定视角滑移,也不依据固定对话比例评价节奏。

完整故事与片段、单场景、开头、对话都要确认方向与字数、准备人物卡和节拍、成稿后独立审读。局部任务缩小规划和审读范围,不取消流程。

---

## 安装

```sh
dsh plugin --profile <你的 profile> add github:Furry-wucheng/dsh-story-mode
```

**就这一条。** 不需要第二步、不需要改任何配置文件。装完新建会话,模式选择器里就有「短篇小说模式」。(CLI 启动的 profile 下当场生效;桌面版若没看到,重启一次 DSH——它的 profile 组装只在启动与插件状态变更时跑,没有监听 bundles 的 watcher。)

### 它是怎么做到的

模式**住在包里**(`presets/short-story/`)。包的 `cordis.patch.yml` 在 profile 合成配置时被合并,其中用 `createRequire(ctx.baseUrl)` 在**运行时**问出本包装在哪,然后接管 `agent-presets` 那一行、把包内 `presets/` 声明为 roster 的一个根。

「包被装到哪」在发布时不可能知道,所以路径必须运行时算——这也是社区插件(如 dsh-TUI)解决同一个问题的做法。

### 卸载

```sh
dsh plugin --profile <你的 profile> remove dsh-story-mode
```

**模式与它自带的两份技能都跟着包一起走**,这两样不需要任何清理——它们从来没被复制到你家里去过。(模式住在包的 `presets/short-story/`,技能是 preset 用 `customSkillDirs` 从包内挂上去的,所以文风契约只在这个模式里可见。)

只有一件事要在卸载前确认:

```sh
dsh plugin --profile <你的 profile> exec dsh-story-mode check
```

如果它提示「默认预设指向本模式」,先清理再卸载:

```sh
dsh plugin --profile <你的 profile> exec dsh-story-mode cleanup
```

为什么要跑:如果你在设置里把「短篇小说模式」设成了**默认模式**,卸载包之后新建会话会直接以 `agent-preset/not-found` 失败——DSH 找不到默认 preset 时不会回退到 `standard`,而真正会顺手清掉这个默认值的那条路径被 `dsh plugin remove` 绕过了。这一步必须在卸载**之前**做,因为包一旦 remove,这个命令也就没了。

(`cleanup` 顺带清掉两处历史残留:v1.0.1 复制到 `<DSH_HOME>/.agent-presets/short-story` 的模式副本,以及 v1.0.1/v1.0.2 曾可选安装的 `<DSH_HOME>/skills/writing-style-contract` 副本——后者只在带 `.dsh-story-mode.json` 归属标记时才删,绝不会误伤你自己手写的同名技能。不是本包放的东西一律不动。)

### 一条命令的边界:它接管了官方那一行

`ctx.agentPresets` 这个服务只允许发布一次,所以本包**不能**另插一行——那样会和官方行冲突,只能有一方生效。所以它覆写官方 `agent-presets` 行的 `config`,保留 `default: standard` 并把本包的 `presets/` 追加为根。

**代价必须知道**:补丁按 id 覆盖 `config`。升级 DSH 之后,如果官方给这一行加了新字段,本包会静默抹掉它。对照检查:

```sh
dsh --profile <你的 profile> --dump-default-config
```

**桌面版对同一行也压了一层,而且在本包之后**:它读回合成后的 config,写成 `roots = [<dsh-agent-presets 包>/presets(system), <DSH_HOME>/.agent-presets(user)]` 加 `includeUserRoot: false`。本包没被它盖掉,靠的是本包 `config` 是**一个** `!!js` 表达式——落地后它是 `{ __jsExpr: … }` 标记,桌面那层展开会把这个标记一起带上,而 Loader 的 `interpolate()` 先看整体:`isJsExpr` 命中就整体求值返回,后加的 `roots` 被丢弃。

**这不是“稳”,是“恰好”**:如果把这一行的 config 改成“普通映射 + 内层 `!!js` 算路径”,桌面那层就会反过来盖掉 `roots`,模式会**静默消失**且没有任何报错。改这一行前请先读 `cordis.patch.yml` 里的对应段落。

`story_doctor` 会提示这项风险,但不执行官方配置比对或实际加载验证。将来若多个插件都需要追加根,这是框架层面的限制(`roots` 不是增量合并的)——届时该由 DSH 提供追加语义,而不是每个插件各自覆写。

### 验证安装

下面的命令只检查卸载残留,不证明模式已经加载成功:

```sh
dsh plugin --profile <你的 profile> exec dsh-story-mode check
```

安装验证请在新会话里让 agent 调用 `story_doctor` —— 它会逐项报告:包是否进了 bundles、`package.json` 是否可解析(带 BOM 会让 DSH 读不出 `dsh.bundle`)、patch 是否接管了那一行、包内模式目录是否满足静态发现条件、文风契约是否已挂进 preset,以及卸载前需要注意的两处残留(默认预设 / 旧安装副本)。

### 其他安装方式

```sh
# 全局安装
pnpm add -g github:Furry-wucheng/dsh-story-mode

# 从源码目录直接开发(不装进 profile)
git clone https://github.com/Furry-wucheng/dsh-story-mode
cd dsh-story-mode && pnpm pack        # 得到一个 tgz
dsh plugin --profile <你的 profile> add ./dsh-story-mode-1.1.2.tgz
```

### 升级

```sh
dsh plugin --profile <你的 profile> add github:Furry-wucheng/dsh-story-mode
```

模式与两份技能都跟着更新——它们都住在包里,没有任何需要手动刷新的副本。

## 用它

主代理先加载写作技能,再完整读取共用流程与审读面板。新写或续写前,必须问清尚未给出且未授权自拟的方向、目标字数、基调、关系口吻、视角与停止位置;一次问完,不重复已明确的要求。“角色自拟”只授权角色,片段没有字数也要问。方向与篇幅确定后给人物与节拍方案,确认后成稿;已有同一方案的批准或作者明确免确认时直接沿用。

- **新写整篇或片段**:确认方向与字数 → 人物卡和节拍 → 方案确认 → 成稿 → 独立审读 → 修订复核。正文只写到约定停止点,片段无需全篇闭环。
- **续写**:读取获准前文与相关人物卡、节拍,确认新增内容和字数后执行同一流程;不擅自补完全文。
- **整体改稿**:核对并补齐受影响的人物卡和节拍,按确认范围修改,独立审读新版。
- **局部润色**:核对涉及的人物卡、当前节拍与接缝,缺失时整理最小记录;修改后也必须独立审读,只缩小读稿范围。
- **只要评价或方案**:独立审读对应材料后交付意见或方案,不自动写正文;评价不擅造作者设定。

完整作品的篇幅档位仅供参考:微型 3k–5k、标准短篇 5k–10k、中短篇 10k–20k、系列每篇 3k–10k。片段没有最低字数,但仍须由作者确认字数或明确授权自定;“写一段”“快一点”不能据此猜一个篇幅直接开写。

### 故事资产

新项目通常用 brief.md 保存方向和字数约束、bible.md 保存本次人物卡、outline.md 保存节拍与篇幅安排、draft.md 保存正文。文件可以合并,已有项目沿用既有记录,但不能因为是片段就缺少人物卡与节拍。只有涉及范围内的人物和内容需要规划,不强加人物生平、完整主线或后日谈。

人物卡包括身份、当下动机或需要、关系、性格和口吻、当前状态与相关事实。节拍安排当前内容的先后、快慢详略、字数与停止点;可以有不推进事件的闲聊和停留。详见共用流程的示例。

时间关系复杂时再用 timeline.md,专名多时再用 glossary.md。搜索、枚举和审读输入都限于授权目录,不能为避免新目录重名先列出其他作品。

## 四个工具

全部只读。正文和设定的修改走宿主的常规文件工具。

| 工具 | 实际能力与边界 |
|---|---|
| story_wordcount | 字数、按分隔符或标题分场、目标偏离、段落长度、引号内比例、引号外重复双字组合;不评价节奏好坏 |
| story_lint | 引号外用词线索,带原文行号和版本;不判视角、时态、情绪冗余或“AI 文风”,也不要求按命中删改。**自查与定位用,不进审读流程** |
| story_bible | 校验人物卡必填字段非空、姓名及别名字面提及、术语清单;不识别全部新人物或判断设定合理性 |
| story_doctor | 静态安装自检与残留提示;不能代替 DSH 实际加载和子代理运行验证 |

### 按大纲目标统计

~~~json
{“path”:“故事/draft.md”,“sceneTargets”:“1000,3000”}
~~~

sceneTargets 是按场景顺序排列的正整数字数。工具计算(实际字数 − 目标字数)/ 目标字数,不按场景均分。
逐场成稿时可传包含后续未写场景的完整列表;已写场景必须都有目标。没有计划目标时省略参数,只看分布。
配置 sceneDriftPercent 表示相对目标的偏离百分比,默认 15。旧 dialogueLowPercent / dialogueHighPercent 已不再使用。

引号内比例仅是对话的近似量,包含引用,不含无引号台词。重复项是未经分词的双字组合,不自动构成用词错误。
标题与开头的完整元数据块不计入正文;报告行号仍对应源文件。

### 用词线索与人物检查

story_lint 的 only 可筛选:template-simile、psychological-summary、emotion-explained、dialogue-tag-overuse、emotion-adverb-tag、ai-rhythm、cheap-adverb。这些旧规则 id 保持兼容,报告现在只表示待阅读的线索。旧 pov 参数和 only: pov-drift 仍接受,但不执行人称检查,会说明判断已移交审读员。

story_bible 的“缺卡候选”仅来自 bible 的“已知事实”清单里,在正文出现至少两次、却未建卡的条目。正文中新出现但未进清单的名字不会自动识别;人物、指代与拼写一致性要由审读员核对。

## 审读流程

新写、续写或大幅重写,包括片段:**B1 冷读与 B2 故事逻辑每个版本都派**;**B3 阅读体验、B4 文风执行、B5 动作与空间连续性**命中各自的可观测条件就派(条件见下表与本包的审读面板)。流程是:按角色派发 → 主代理修改 → 原读者复核 → 全新 B1 最终盲读。各角色独立读稿,不预先看彼此报告。
一句或一段的润色也至少有一位独立读者检查改动与接缝;涉及动作、位置、姿态、物品状态或相关删句时必须派 B5。B5 从正文独立追踪状态,检查手脚占用、物品流转、路径与支撑,区分物理矛盾和合理省略;修订后由原 B5 沿受影响人物或物品复核前后状态,直到重新衔接。B5 不由 B2 兼任,B3 不由 B1 兼任。自查、计数和 lint 不等于独立审读;作者催快时缩短报告,不默认取消审读。必要报告或复核缺失时标待审,不虚报验收。

### 关系轴:五个角色之外的那一次(v1.2.0,起点组 v1.2.1)

五个角色的报告结构都在问**“这里有没有矛盾”**(一致性)。它答不了另一个问题:**“这一跳有没有依据”**(充分性)。

**一份所有角色都报“没有大结构问题”的稿子,可以在主轴上完全空心。** 逐处都写得成立、前后都对得上、字数与版本全闭合,而读者仍然会说“他为什么突然就同意了”“我没看出他动过心”——因为把 A 推到 B 的那个东西**从来没有被写出来**,而“没写出来”不是任何一个角色会报的那类问题:B1 只管“读不懂”,B2 只管自洽,B3 明确“不要求证明剧情作用”,B4 明确“不核对剧情因果”,B5 明确“不评论人物动机”。每个角色的边界都写着“这不是我的职责”。

所以关系轴**单独走一次**,仍由 B2 承担(它已经读人物卡与节拍,缺的是**被要求回答**,不是多一双眼睛):

| 时机 | 读什么 | 必答 |
|---|---|---|
| **方案阶段**(默认,必做,先于任何正文落地) | 节拍表、人物卡,**没有正文** | 主线每一跳的推动者是谁;另一方的动心露出有几条、哪些**不可否认**;被动方有没有主动动作;**双方的注意起点落在哪一拍、为什么只对这个人成立** |
| **首次成稿后** | 正文 + 节拍 | 方案里承诺的推动者、露出与**双方起点**,逐条给出正文短引;引不出来算缺 |

判据是硬的:**露出全部可被解释成“尽职/礼貌/习惯”=零**;**“推动者”写不出一个动作或一句台词=缺一拍**;**“为什么是这个人”只有叙述者说得出、任何一场戏都说不出的理由=起点缺失**。报告不接受“整体尚可”“铺垫基本到位”。关系轴报出的缺拍按**优先修复**处理,不归入“可选调整”。

B2 的必答项一共三组,第三组专治作者会读成“上帝视角”的那种毛病:**每个“看穿/识破”的时刻,把它依据的那一句话抄出来;抄不出来就是缺口。** 角色的判断必须建立在这场对话里已经给出的证据上——他比读者先知道又不给证据时,读者不会觉得他敏锐,只会觉得作者在借他的嘴交代剧情。可照抄的对照:**“你站得太近了”是观察,能给依据;“你心事太重”是判断,读者手里只有一句话,接不上。**

配套的两条改动:**节拍表新增“状态变化 / 谁推动 / 代价”三列**(写不出推动者的拍是重复场景,不是节拍);**“留白”这条护栏写明不适用于主轴**——允许留白“他什么时候开始动心”,不允许留白“他动没动过心”,也不允许留白“为什么是这个人”。

### 台词:另一种同样测不到的毛病

同一次调用还暴露了台词的形态问题:**每一句都在交付信息或下判词**,没有人随口附和、没有人重复、没有人答非所问、没有一件聊完就放下的小事,整篇像双方在念台词。文风契约第四节其实写明了要允许“随口附和、确认听清、自我纠正、绕开问题、聊一件小事后自然结束”——**成稿里一个都没落地,因为规划阶段没有为它留位置。**

所以这一条也落在规划与必答项里,而不是靠 lint:节拍表要为每场标注**对话的功能分布**,且**每场至少要有一处“无功能”交流**;“看穿”的能力要**单向、向下**(用在一个地位更低或更被动的人身上——反过来的“学生一眼看透大人”是读者最容易读成上帝视角的形态),且**结论必须有本场已给出的证据**。

工具测不到这一类:`story_lint` 的七条规则全是字面形状(套话、解释性副词、对话提示语密度),而“所有台词都在交付信息”是功能分布问题,**没有正则能写**。

代价:B2 的人设从 2.8 KB 涨到 8.1 KB——每次派发(含复核)约多 1.3 K token。换掉的是一次成稿后才发现缺一拍的整场重写;也顺便解释了为什么不做成第六个角色。

关系轴报告允许放宽篇幅(常规核对仍以 1200 字为度,关系轴要求逐条引用,写到 3000 字以上也可以)——**不要为了压字数把必答项写成概括**,那等于把这一节取消掉。

### 作者说“我没看懂”是指令

**同一处或同一条线被报两次,或困惑落在关系推进、核心动机、关键转折上时,按结构问题处理**:回节拍表逐拍核对“谁推动”,找到缺哪一拍。不做句子级修补,也不靠加一句解释台词糊过去——把解释补进台词(“他其实早就……”)不能替代补一拍。困惑出现在主线上时,“是不是没写出来”优先于“是不是读者没读到”。


**审读员是可复用的持久子代理。** 每个角色派发一次就拿到它的 childId;改稿后的复核用 `send_message` 交给**同一位**读者——它保留着上一版的阅读和自己的报告,只需要回答“这次改了什么、你上次那几条还成不成立”,不必重读全文,前缀命中缓存。只有最终盲读另起一位新读者:独立性不能复用。派发时不要传 `run_in_background: false`,那会退化成一次性会话,后续消息送不到。

这套行为由 preset 的 `backgroundMode: continuable` 加 `send_message` / `list_agents` 两行工具提供;`story_doctor` 会静态检查它们还在不在。

| 角色 | 工具 | 可以读取的材料 | 什么时候派 |
|---|---|---|---|
| B1 冷读者 | `subagent_review_b1` | 本篇正文;连续阅读可读已发布前文,不读大纲、设定、作者摘要或旧问题 | 每版都派;最终盲读换全新读者 |
| B2 故事逻辑 | `subagent_review_b2` | 正文、作者有效要求、人物卡、节拍、必要前文与设定 | 每版都派。**并承担关系轴审读:方案阶段一次、首次成稿后一次** |
| B3 阅读体验 | `subagent_review_b3` | 正文与获准前文;不读大纲、设定与文风契约 | 本文 ≥5000 字或 ≥3 场;作者提过节奏/篇幅/详略;有增删场次或调序;交付前最后一版 |
| B4 文风执行 | `subagent_review_b4` | 正文、作者要求、文风契约(已装进它的人设) | 作者对口吻/视角/叙述提过要求;改过台词、心理或视角;交付前最后一版;≥3000 字的完整成稿 |
| B5 动作与空间连续性 | `subagent_review_b5` | 正文、必要前文及明确的身体或物理规则;不读大纲、节拍、主代理的场景状态摘要、文风契约或其他读者报告 | 多人同场或涉及持物、走动、身体接触;改过动作/位置/姿态/物品状态或相关删句;交付前最后一版 |

每次派发记录正文路径和工具给出的内容版本。审读期间暂停修改;回收后核对版本。
问题位置采用“场景 + 原文短引 + 行号”,主代理追踪改稿后的对应位置。不同版本的段落号不能直接交叉判断。

**最终盲读使用全新读者,只看最新正文。** 旧问题是否解决由主代理在报告返回后对照。读者的困惑是需要核实的证据,也可能是合理悬念,不自动要求补背景。

**审读员不改文件、不再委派,这是权限层的限制而不是提示词请求。** 每个角色由 preset 里独立的一行 `tool-subagent` 提供,带 `toolFilter`:只允许 `read` / `read_image` / `str_replace_editor` / `glob` / `grep`,`write` / `edit` 与全部委派工具都被 `tools.restrict()` 在这个子代理的 scope 上剥掉。角色的人设(身份、读什么、报告格式)同样固定在那一行里,由 `!!js` 从 `references/reviewers/*.md` 读出,所以派发时只给变量、不必重述边界。独立审读无法运行时如实说明,不虚报验收。

写作入口要求主代理完整读取 references/writing-workflow.md(共用写作流程)和 references/review-panel.md(独立审读);整篇与片段都必须读。两份资料位于 presets/short-story/skills/short-story/ 下,skill 调用不会自动加载它们,必须另用文件工具读取。

---

## 包结构

```
dsh-story-mode/
  cordis.patch.yml                  接管 agent-presets 行,把包内 presets/ 声明为 roster 的根
  presets/short-story/              模式本身:agent.cordis.yml + 元数据 + 写作流程技能
    skills/short-story/             SKILL.md:必读入口、方向与字数确认、范围约束
      references/                  writing-workflow.md:人物卡、节拍、成稿与修订;review-panel.md:派发规则与角色触发条件
        reviewers/                 B1–B5 各自的固定人设(身份、读什么、报告格式),由审读员行读出
  lib/index.js                      主入口:写作工具
  lib/doctor.js                     独立入口 dsh-story-mode/doctor:只注册 story_doctor
  lib/tool-kit.js                   零依赖的工具构造器与参数校验(两个入口共用)
  skills/writing-style-contract/    文风契约(由 preset 挂进模式,只在这个模式里可见)
  bin/cli.mjs                       check / cleanup(不安装任何东西,只做卸载前清理)
  scripts/cleanup.mjs               实际干活的那份
```

两个入口是 `exports` 子路径实现的同包多入口。`./doctor` 可以单独挂到**任何**模式里做诊断(例如创造模式),不必连带加载三个写作工具。

---

## 已知约束与设计取舍

这些都是查过框架源码、并且踩过之后才写下来的:

- **审读员是 `continuable` 子代理,不是一次性调用。** 一次性模式(v1.1.0 及以前)下每轮复核都要再派一位读者,把同一篇正文重新从头读一遍;可复用模式下复核走 `send_message`,子代理带着上一版正文的阅读和自己的报告继续。**坑**:可复用模式里只有后台调用会产生持久 child,前台分支走的是 `subagents.start()`,拿不到 childId——所以技能与审读面板都明令派审读员时不要传 `run_in_background: false`。
- **五个审读角色各自一行,人设与只读限制都在组合里。** 每个角色是一个独立的 `tool-subagent` 行(`subagent_review_b1` … `b5`):`persona` 由 `!!js` 从 `references/reviewers/<角色>.md` 读出并装到那个孩子身上,`toolFilter.allow` 只列读类工具,`deny` 拿掉 `write` / `edit` / `present` / 全部委派工具。于是派发时主代理只给变量(路径、版本、范围、改动清单),不再从面板抄模板——抄一次就可能漏一条信息边界。**两个坑**:① `tools.restrict()` 对未知工具名**抛错**,而它抛在孩子的创建窗口里=那次派发直接失败,所以名单里只能出现本组合真实注册过的工具名;② 审读员人设里**不要**给 `skill` 工具,B4 需要的文风契约在挂载时拼进它自己的 persona。
- **写作模式故意不开 `subagent_fork`。** fork 继承主代理的全部上下文(大纲、写作推理、修改理由);读者看过作者的底牌之后,“我没看懂”就不再是读者证据。审读员也因此不开子级模型选择,一律与主代理同路由。
- **技能根由 preset 自己声明,所以文风契约只在这个模式里生效。** 写作流程技能与文风契约分别是包内的 `presets/short-story/skills/` 与 `skills/`,都以「preset 文件所在目录」为基准解析(`!!js` 里的 `baseUrl`),注册落进本 preset 那一层,所以模式、流程技能、文风契约三者永远同进同出。它**故意不**放进 `<DSH_HOME>/skills`:那是用户根(rank 400),而每个 preset 自己挂的 skill-filesystem 实例都会扫它(`includeDefaultRoots` 默认 true)——放进去等于让它出现在所有模式里,包括编码会话,而它只属于写作模式。
- **写作模式保留了搜索工具(`glob` / `grep`)。** shell、计划模式、后台任务、工作流、多级委派都拆掉了,但“找”不能拆:系列连载里核对名字与细节靠搜,不靠通读。这一行要注意 `sampleOverCapGlobResults` 在 framework 侧是**必填**配置(`z.boolean().required()`),漏了它整个 preset 会挂不上。
- **`ctx.agentPresets` 只允许发布一次**,所以一个插件**不能**只“追加自己的 roster 行”——官方行几乎总是存在(web-app bundle 提供它),两行不能共存。注入根就必须接管那一行,代价见上面「一条命令的边界」。
- **`roots` 是整体替换而非增量合并。** 所以本包接管那一行时会把它自己的根写全;多个插件都要追加根时,这是框架层面的限制。
- **根下的 `<id>` 条目必须是真实目录。** roster 的 `scanRoot` 用 `readdir().isDirectory()` 判定,**不跟随符号链接**。Windows 上 Node 把 junction 报成符号链接,于是链接形式的预设会被**静默跳过**,而 `stat()` 读文件却完全正常——本包早期版本正是栽在这里。(技能侧相反:`dsh-skill-filesystem` 会跟随链接一级。)
- **`!!js` 后面是折叠标量,整段代码会压成一行。** 所以那段 JavaScript 里不能有 `//` 行注释(一个就吃掉后面全部),也不能靠自动分号插入。这两条都是实测踩出来的。
- **`createRequire` 的锚点必须按文件 URL 给。** 给它一个不带尾斜杠的目录路径,Node 会把该目录当成文件解析,直接 MODULE_NOT_FOUND。
- **`package.json` 不能有 BOM。** 带 BOM 会让 `JSON.parse` 失败,DSH 因此读不出 `dsh.bundle` 声明,`dsh plugin add` 不会把包加进 profile 的 bundles,patch 永远不生效——表现为“装好了但模式不出现”。这个坑本包也踩过一次,`story_doctor` 里有常驻检查(`package.json 无 BOM` 那一项)。
- **preset 行不能用 `!!js` 动态算路径。** 发现阶段确实支持 `!!js`,但紧随其后的形状检查要求每行的 `name` 是**字符串**;`!!js` 解析出来是对象,整份组成会被判为 broken。
- **preset 行不能用裸包名**(从 harness 解析,到不了用户目录),所以组成里用相对引用 `../../lib/index.js`——以组合文件所在目录为基准,因此**没有任何机器相关的绝对路径**。
- **升级包之后要确认模式真的换了。** 模式住在包里、由 profile 的 patch 声明成 roster 的根,所以你改的是**哪一份包**决定模式的行为:从 GitHub 装的(`dsh plugin --profile <name> add github:<作者>/dsh-story-mode`)取的是远端默认分支,本地改动没推上去就不会生效。更隐蔽的一种:早期版本或插件市场留下过一份 profile 里的包快照,`dsh plugin remove` 之后**不会**自动删掉它——它可能还留在 `node_modules` 与插件目录里,于是“装了新版但模式没变”。判断办法是看模式里有没有 `subagent_review_b1` 这个工具;清理由市场或手动删除那份快照完成(本包的 `cleanup` 脚本只处理 home 下的技能副本与旧预设副本)。
- **本包零运行时依赖**(连 `schemastery` 都没有)。ESM 的解析基准是加载入口的父路径,所以插件若 `import` 任何 `@deepseek-ai/*` 包就必须自带 `node_modules`;它什么都不 import,于是任何布局下都能加载。**改代码时不要引入裸导入**,否则上面那条相对引用会失效。
- **v1.2.0 之前,这套流程有一个已实测的失效形态:五个角色全绿而主轴空心。** 一次真实调用里,五个审读员对一篇 23,399 字的稿子出了 19 份报告(含两次全新读者盲读),逐条命中字形、雨季倒序、浮毛时令、椅子数量、扣子崩落、字数台账,最终盲读明确写「没有大结构问题」;作者随后在半小时内用七个连续发言拆掉了它——主人公在八场里零主动推动、另一方“想要”的露出全部可被解释成尽职、关键那一跳没有任何不可否认的依据。

  **盲测对照(v1.2.0 补做)**:同一份 23,000 字稿、同一组派发变量、**不带任何对话上下文的新子代理**,唯一差别是 B2 的人设文件(标签随机化、判前不知对应):

  | | 旧版人设 2.8 KB | 新版人设 8.7 KB |
  |---|---|---|
  | 报出关系轴缺口 | **否** | **是** |
  | 关键结论 | 第 14 条把因果链判为**「已建立的理由与因果(通过)……越线不是凭空发生……逐级落到具体动作与位置」** | 「转折点之前没有一条不可否认的露出」「推动者分布失衡,被动方在读者眼里是空的」「承认是告知而不是揭示」 |
  | 逐条判定 | 无(核的是“有没有铺垫”) | **第一组 34 条逐条带行号判定**,定位缺口的场次与必要条件 |

  旧版把“逐级有铺垫”当成了通过项——**它核的是“有没有铺垫”,不是“这些铺垫立不立得住”**。这正是「一致性」与「充分性」的差别,也是这道缺口能在一整套流程里活下来的原因。

  留这一条在这里,是因为缺口的形状(自洽但空心)不会自己暴露,只会以“读者看不懂”的形式回来。

  **另有一条人设本身的修订,也是盲测逼出来的**:最初第一组的判据是“**全部**条目都可否认 → 报缺口”。第三轮盲测报了 34 条、其中 15 条判为不可否认,缺口却仍然成立——因为那 15 条**集中在关系已经变化之后**,转折点之前读者手里还是零。所以判据已补成“**可否认总数不是结论,分布才是**”:落在哪、是不是被逼近后的反应、有没有一件是“他单独为对方做的”。**概括的数量判断会漏掉这一种**。

- **v1.2.0 还漏了同一个方向的第二种形态:露出有据可引,起点没有。** 一次真实调用里,一篇 17,900 字的师生短篇把关系轴跑了三轮(方案阶段一次、成稿后两次复核),B2 逐条列出 22 条露出、逐条判定、逐条给正文短引,推动者分布也数过、结论是“不失衡”;补拍之后缺口关掉,交付时验收状态是干净的。作者读完后仍然说这篇**“没有人味”**:两个人从什么时候开始不一样的、老师是怎么开始注意这个学生的,正文一场戏都没交代。

  **形状**:作者的定调是“学生主动进攻、老师屡次退守”,于是老师的朝前动作只落在转折点那一刻;而在转折点**之前**,正文给老师的两处来路是——去年十一月一节语文课上“忽然听不见学生的声音”,和五月里“他记得那个学生的气味……高二下学期还没有。到了冬天,那个味道变了。他没有理由去分辨它变成什么样,他分辨了。”学生的来路是唯一一句台词:“我画你的手,是去年十月开始的。”**三处都是叙述者的句子,不是场上的事。** 稿子处处自洽、前后对得上,读者却说不出这份想要是从哪儿长出来的——**“他动过心”有据可引,答不了“他为什么会动心”。**

  **为什么会漏**:关系轴的必答项管的是“**要**”(露出有哪些、可不可否认、谁推动),而这一篇的两个人的动心起点**都在开篇之前**——节拍表把“从老师知道学生的心思开始”当成了既定前提,规划阶段就没有为起点留位置,于是审读环节也没有东西可核。更直接的一条在护栏里:判据原文写着“允许留白**他什么时候开始动心**、为什么是这个人”,把“什么时候”和“为什么”并列放过了——但两者的代价完全不同:**时刻可以留白,依据不能。** 读者不是不能接受留白,是不能接受空白;没有依据时他只能自己替作者编一个,或者干脆不接。同一处还有一条可核对的信号:作者问的是**“从什么时候开始有异样”**,而这一篇里三个候选起点(去年十月/去年十一月/“高二下学期还没有,到了冬天变了”)互相之间没有换算关系,读者连“哪一个是起点”都推不出来。

  **v1.2.1 的改法**(都在规划与审读两层,不在工具层——正则写不出这类问题):B2 增加**第零组“想要从哪儿来”**,先于第一、二组做,对**双方**各要一条起点与“为什么只对这个人成立”,判据是硬的(**只有叙述者说得出、任何一场戏都说不出的理由=没有理由**;把主语换成另一个学生或另一位老师仍然成立的由来=没有不可替代性);人物卡为动心的双方各加一条**注意起点**(与“当下动机”分开——动机是现在要做什么,起点是这份要的来路);护栏改成“**什么时候**可以留白,**为什么是这个人**不能”;缺的起点**补在开篇之前,或者把开篇提前**,不靠“其实他早就……”这类补注。

  **盲测对照(v1.2.1 补做,同一份 bible/outline、同一份派发 prompt、无上下文污染)**:两名选手互不相识,唯一变量是 B2 的人设文件版本:

  | 轮次 | 旧人设(v1.2.0) | 新人设(v1.2.1) |
  |---|---|---|
  | 第 1 轮 | 起点整节缺席;只在末段“第零组起点单列一句”里承认“起点整场落在开篇之前”,但**没把它当缺口**,四条补拍方向全落在中段 | **新增第零组整节**;“熊谷全篇零起点”列为**第一大缺口**;引到“我不知道他什么时候开始不一样的”;写出“需在开篇前补一场,或把开场提前” |
  | 第 2 轮(确定性检索) | `需在开篇前补一场` 0 次、`把开场提前` 0 次;且把学生侧的起点判成**“依据齐全,不报缺口”** | `需在开篇前补一场` 1 次、`把开场提前` 1 次,三条判据全中;两位互不知情的评委会独立判 found=true / found=false |
  | 终局(精简后的人设) | 全篇 `起点`/`来路`/`开篇` **0 次**,只把它读成“露出分布不均”(“不是写得不够多,是写得全都在对方看不见的地方”) | 单列第零组:学生侧“起点落场且不可替代”;**熊谷侧“没有起点,只写了由来”报缺口**,并点出“去年十一月失手”这个锚**换成另一个学生在场照样成立** |

  **精简**:第零组加进去之后人设是 5115 字符,压缩重复表述(开头与关系轴各自讲一遍“空心”、留白一处的三重表述、第一二组的冗词)后落到 4617 字符——比加组之前的 4673 更短,而新增的判据一条没丢。

  **一条没达标的观察,留在这里**:终局那次新人设对学生侧判的是“起点落场、不算缺口”。学生侧的依据确实可引(画本+去年十月),但**“为什么开始画”这一场仍然没落场**,正确读法应该是双方都报。所以判据“有一场戏,或者有一个可被引出的锚”在宽度上仍偏松——它放过了“锚引得到、但那次注意本身没有戏”的中间形态。下一轮该把这一档单独写死,或者接受“一侧可引、一侧零”也构成本组缺口。

  留这一条在这里,理由和上一条相同:自洽但空心不会自己暴露,它只会以“作者说没有味道”的形式回来,而那时稿子已经过了三轮审读、每一轮都验收通过。

  **相关的两个次级因素,一并记下来**:① 那次调用里**方案没有被独立审读**——五路审读全部派发于正文落地之后,而方案阶段本来可以一句话拦下它;② 主代理收到的子代理回报里,**读者的思维链比报告正文还多**(实测 91,204 字 vs 47,974 字,1.90 倍),注意力被“过程”占满。第 ① 条已由 §4.0 的方案阶段审读修掉;第 ② 条属于宿主回报格式,尚未解决。

  **一条方法论备注,也是踩过的坑**:盲测要用不带对话上下文的子代理。早期我用 `subagent_fork` 做新旧人设对照,两次都“报出了缺口”——但 fork 会把整段对话(包括我写的判定标准)一起继承给被测者,**等于把答案先交给它**。那组结果分辨力为零,已作废。上面的表是改用无上下文子代理(`ralph` 的 fresh child)重跑的。

### 安装会改动你的 home 目录吗

**不会。** `dsh plugin add` 只写 profile 的 `package.json`、`node_modules` 与 patch 层:模式住在 profile 的 `node_modules` 里(也就是 pnpm 装包的地方),技能由 preset 从包内挂载。`<DSH_HOME>/skills/` 与 `<DSH_HOME>/.agent-presets/` 都不碰,也没有需要用户去批准的构建脚本(pnpm 默认就会拦截依赖的生命周期脚本,本包不依赖它)。

`cleanup` 是唯一会写你 home 的命令,而且只**删**本包自己留下的东西:指向本模式的默认预设、v1.0.1 的模式副本、带 `.dsh-story-mode.json` 归属标记的技能副本。不是本包放的一律不动。

**注意**:v1.0.1 把模式**复制**进 `<DSH_HOME>/.agent-presets/short-story`。那份副本会和 patch 声明的根撞 id,roster 只会认先扫到的那一个——留下过期的副本会让“改了包内文件但模式没变”发生。`check` 会报出来,`cleanup` 会清掉。

---

## 文风契约

skills/writing-style-contract/SKILL.md 提供默认文风:克制、自然、具体。作者风格要求优先,不把所有作品改成同一种声音。

契约内附修改前后与保留示例,并展示同一问题在不同场景需要下的长短两类有效写法:删掉重复解释,也可以继续展开动作、感知和等待。默认约束仍然严格,例外必须有原文依据,不能只说“服务叙事”就放行;示例不作为仿写素材,不以缩短篇幅为统一目标。

修辞、心理、台词、动作和直接交代都按上下文判断;允许增写、删减或保留,不设最低删除百分比。
必要信息要在读者需要时可得;合理推断和有效留白可以保留,不要求所有专名首次出现就讲完背景。

视角检查交给审读员:区分叙述者、人物台词、内心引语与面向读者的称呼,指出具体的认知越界,不从人称字样下结论。

## 开发验证

~~~sh
npm test        # 单元与契约测试(Node 自带 test runner,无需依赖)
npm run verify  # 组合自检:把 agent.cordis.yml 当成 loader 那样读一遍
~~~

回归测试覆盖分场、引号统计、真实配额、用词线索、人物卡和卸载检查,并钉住审读流程的形态(角色各自成行、只读 toolFilter、B3/B4 的可观测触发条件、人设里不再夹带流程规则,以及 **v1.2.0 的关系轴契约**:面板 §4.0 的两个时机、B2 的两组必答项、“全可否认=零”判据、节拍表的推动者三列、“免确认”不能跳过关系轴;**v1.2.1 的起点组**:B2 的第零组、双方各一条注意起点、“只有叙述者说得出的理由=没有理由”、缺的起点补在开篇之前,以及“什么时候可以留白、为什么是这个人不能”)。测试只使用内存稿件和临时 DSH 主目录,不修改真实用户设置。
npm run check 与 CLI check 都是卸载残留检查,不是单元测试或完整安装验证。

`npm run verify` 补上单元测试碰不到的那一半:它用 **loader 自己的解析器**(js-yaml + `entryListSchema`)交叉验证组合,对每个 `!!js` 表达式做编译检查,用插件自己的 `Config` 校验每行 config,并把 `toolFilter` 点名的工具与**插件源码里真正注册过的名字**对账。它需要能读到 harness 已安装的插件(仓库本身零依赖),默认按桌面版路径查找,可用 `DSH_HARNESS` 指定。
两件它**不能**代替的事:真实会话里的工具清单,以及子代理真的能起来。装好后请开一个会话跑一次新写或片段,确认 `subagent_review_b1` … `subagent_review_b5` 五个工具都在、派一次能拿到 `started subagent <childId>`。

### 为什么 `!!js` 里不能写 `'\n'`

`!!js` 的值在组合里是 **YAML 双引号标量**,YAML 会先处理转义:`'\n'` 在 loader 拿到源码之前就变成了真正的换行,于是 JS 里出现跨行的字符串字面量 → `SyntaxError: Invalid or unexpected token`。而 preset 的 `mount` 契约是“setup 里抛错就回滚整次 agent 创建”,所以表现不是报错,而是**点新会话没反应**;roster 里 `broken` 仍是 `null`,因为文件形状完全合法。v1.1.3 正是栽在 B4 那一行的换行拼接上(另外六个 `!!js` 不含转义,所以只有它炸)。

结论:需要换行就写 `String.fromCharCode(10)`;需要字面反斜杠就用单引号或折叠标量(它们不处理转义)。`npm run verify` 现在会把这两类写法都拦下来——先做交叉验证比对两种解析结果,再对双引号标量里的反斜杠做预防性检查。

---

## License

MIT

Install

dsh plugin --profile web add github:Furry-wucheng/dsh-story-mode#d8fa8e3685264cb5f475f01962f53526b5e69b83

Profile: web

Source