dsh-story-mode · 短篇小说模式
给 DeepSeek Harness (DSH) 的一个写作专用模式。装好之后,新建会话时模式选择器里会多出「短篇小说模式」——进去的不是编码 agent,而是一个接稿的短篇小说作者。
整套能力(模式、技能、工具)都在一个包里,一条命令落地。
它解决什么问题
让可测量的内容由程序负责,让需要上下文的内容由读者判断。
| 程序测量或定位 | 审读判断 |
|---|
| 中文字数、分场、相对已确认目标的偏离 | 场景是否有效、节奏是否合适 |
| 引号内文字比例、段落长度 | 对话是否自然、人物口吻是否可区分 |
| 重复双字组合、常见用词位置 | 修辞是否有效、心理与情绪是否冗余 |
| 人物卡字段是否非空、姓名与别名字面提及 | 新人物、专名一致性、动机与跨篇设定 |
| 稿件内容版本、原文行号 | 视角、时态、因果与必要交代 |
字面命中不代表违规,统计正常不代表故事好看。工具不再根据“我/你/他”判定视角滑移,也不依据固定对话比例评价节奏。
完整故事与片段、单场景、开头、对话都要确认方向与字数、准备人物卡和节拍、成稿后独立审读。局部任务缩小规划和审读范围,不取消流程。
安装
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)解决同一个问题的做法。
卸载
dsh plugin --profile <你的 profile> remove dsh-story-mode
模式与它自带的两份技能都跟着包一起走,这两样不需要任何清理——它们从来没被复制到你家里去过。(模式住在包的 presets/short-story/,技能是 preset 用 customSkillDirs 从包内挂上去的,所以文风契约只在这个模式里可见。)
只有一件事要在卸载前确认:
dsh plugin --profile <你的 profile> exec dsh-story-mode check
如果它提示「默认预设指向本模式」,先清理再卸载:
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 之后,如果官方给这一行加了新字段,本包会静默抹掉它。对照检查:
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 提供追加语义,而不是每个插件各自覆写。
验证安装
下面的命令只检查卸载残留,不证明模式已经加载成功:
dsh plugin --profile <你的 profile> exec dsh-story-mode check
安装验证请在新会话里让 agent 调用 story_doctor —— 它会逐项报告:包是否进了 bundles、package.json 是否可解析(带 BOM 会让 DSH 读不出 dsh.bundle)、patch 是否接管了那一行、包内模式目录是否满足静态发现条件、文风契约是否已挂进 preset,以及卸载前需要注意的两处残留(默认预设 / 旧安装副本)。
其他安装方式
# 全局安装
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
升级
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 实际加载和子代理运行验证 |
按大纲目标统计
{“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() 读文件却完全正常——本包早期版本正是栽在这里。(技能侧相反: 会跟随链接一级。)
安装会改动你的 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 提供默认文风:克制、自然、具体。作者风格要求优先,不把所有作品改成同一种声音。
契约内附修改前后与保留示例,并展示同一问题在不同场景需要下的长短两类有效写法:删掉重复解释,也可以继续展开动作、感知和等待。默认约束仍然严格,例外必须有原文依据,不能只说“服务叙事”就放行;示例不作为仿写素材,不以缩短篇幅为统一目标。
修辞、心理、台词、动作和直接交代都按上下文判断;允许增写、删减或保留,不设最低删除百分比。
必要信息要在读者需要时可得;合理推断和有效留白可以保留,不要求所有专名首次出现就讲完背景。
视角检查交给审读员:区分叙述者、人物台词、内心引语与面向读者的称呼,指出具体的认知越界,不从人称字样下结论。
开发验证
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