@ndsanes/dsh-rules
Discovers rule files from every convention OMP supports and injects them into the system prompt as an always-apply layer plus a rulebook index, then enforces them with time-traveling stream rules that abort violating output mid-turn, deny a matching tool call, fold a reminder into that call's result
Install
dsh plugin --profile web add @ndsanes/dsh-rules
Sandbox test
This plugin has not been tested yet. Submit a test →
README
dsh-rules
[English](README.md)
面向 DeepSeek Harness 的 OMP 规则。
从 OMP 支持的每一种约定中发现规则文件,把 always-apply 与 rulebook 层注入系统提示词,让模型按需加载规则正文,并强制执行时间旅行流式规则(TTSR)——在当前轮次中途打断违规的模型输出。
安装
dsh plugin --profile <name> add @ndsanes/dsh-rules
然后把这一行加入该 profile 的 cordis.patch.yml(如果包自带的 bundle patch 尚未应用的话),并重新加载该 profile。
如果该 profile 已经用另一个包名装着这个插件——一条陈旧的 link: 依赖,或一个 dsh.profile.bundles 条目——那么在重新添加之前,必须把它从 dependencies 和 dsh.profile.bundles 两处都移除。加载器按当前包名去解析 bundle 行,陈旧条目指向的模块在 profile 里已经不存在,启动时只会报一句 import failed。插件这边对此无能为力。
规则文件
规则是一个带 YAML frontmatter 的 Markdown 文件:
--- description: Read before writing a database migration. globs: "Database/**/*.surql" condition: "DEFINE (FIELD|TABLE|INDEX)" scope: "tool:edit(*.surql), tool:write(*.surql)" interruptMode: always --- The migration body.
字段 · 含义
description · rulebook 列表中必须有此项。
alwaysApply · 把整个正文注入系统提示词。
globs · 路径闸门;TTSR 规则至少需要一条匹配的候选路径。
condition · 正则触发条件(也接受旧式 ttsr_trigger)。
astCondition · ast-grep 结构化触发条件,在工具参数上检查。
question · 由裁判模型回答的自然语言问题。
scope · text、thinking、tool、tool:<name>(<glob)。
agents · 代理名 glob;main 与 sub 为保留值。
interruptMode · never \ · prose-only \ · tool-only \ · always。
在规则编辑器里,取值固定在某个词表内的字段(目前只有 interruptMode)是下拉框而不是自由文本框,所以值不可能被拼错。这一点比「少一个下拉菜单」要紧得多:一个无法识别的 interruptMode 不会被拒绝,而是落回 profile 的默认值,而这个默认值是 always——于是一条本想保持安静的规则,反而变成了打断一切的那条。下拉框的第一项表示「沿用 profile 默认值」,选中它会把这一行从文件里删掉,而不是写一个空值进去;写成 interruptMode: '' 同样会被判为无法识别,并落到同一个默认值。
写正则时使用单引号 YAML 标量。双引号标量会把 \s、( 以及 ( 系列的序列当作 YAML 转义处理,导致 js-yaml 拒绝整份文档。此时插件会退回到逐行读取该块并仍然恢复出该值,所以规则依旧能加载、依旧能匹配——但这种恢复无法还原 YAML 已经拒绝掉的转义序列,因此应当采用的写法是单引号形式。
发现
提供方 · 优先级 · 来源,按从具体到宽泛排序
native · 100 · <cwd>/.omp/rules/.md(c)、<userRulesDir>/rules/.md(c)、粘性 RULES.md
omp-plugins · 90 · 每个已配置 pluginRoots 条目下的 rules/
agents · 70 · .agent/rules、.agents/rules,先项目后用户
cursor · 50 · <cwd>/.cursor/rules、~/.cursor/rules
windsurf · 50 · <cwd>/.windsurf/rules、~/.codeium/windsurf/memories/global_rules.md
cline · 40 · 最近的 .clinerules,文件或目录均可
github · 30 · .github/instructions/*.instructions.md,applyTo 会被归一化
builtin-defaults · 1 · OMP 自带的规则,随包发布于 src/builtin-rules/
行内顺序是有实际意义的:规则的标识只取决于规则名,合并时保留第一个认领该名字的规则,因此用户的 ~/.cursor/rules/style.md 会输给工作区中同名的 <cwd>/.cursor/rules/style.md。
自带规则
src/builtin-rules/ 以原样、MIT 许可、同名的方式收录了 OMP 的 27 条默认规则——TypeScript、Go 与 Rust——因此在任一 harness 中的规则文件都能覆盖另一份。它们是事实来源;pnpm run embed:rules 会把它们内联进 src/generated/builtin-rules.ts,因为打包器无法把 Markdown 当作文本导入。其中每一条都是限定到某文件类型的 TTSR 规则,且都不会阻止写入。Go 与 Rust 规则基于重建出的源码做正则触发;那三条依赖 astCondition 的规则则受 [与 OMP 的差异](#differences-from-omp)中说明的文法限制约束。
Plugins 页面
dsh-rules 自带一个浏览器端实现。同样这两个区域会出现两次:在 Plugins 页面上本插件自身的条目下,以及 Settings → Plugins 的一个标签页里——去配置插件时人们去的是设置页。两者渲染的是同一个组件;Settings 标签页不携带插件 subject,因此这一侧不做 subject 检查。
本插件自身的配置项——enabled、userRulesDir、pluginRoots、copilotInstructionDirs 以及 ttsr 块——根本不由这个插件绘制。宿主会从 Config schema 推导出一张表单,按 profile 行 id 渲染它。这也解释了为什么开关在设置页里,哪怕部署方从未打开过上面这个面板。
规则审计——先给数字,再给明细:
统计行 · 发现的规则、生效中的、已关闭的、在此交付的、在别处交付的、从未触发的
占比 · 规则来自何处,按来源约定拆分
占比 · 哪些处于生效状态
占比 · 它们因何触发——condition、ast-grep、question
排名 · 最常交付给模型的规则
前三个是构成图,不是排名图:一条整宽的条形按占比切分,旁边配图例。为每个类别单独画一条条形严格劣于直接读数字——占主导的类别会占满整个轨道,次要类别只剩一个小残段,于是长度传达不出标签之外的任何信息。只有交付规则那张图是排名图,所以只有它用条形。dsh 的 web 客户端不带任何图表库,而 dsh-usage-chart——唯一会画图的社区插件——也记录了同样的结论:一个与平台自身渲染相匹配的自绘 SVG,比引入一个外部库更小也更稳。
交付计数持久化在 $DSH_HOME/dsh-rules/triggers.json,因为只存在于内存里的计数在每个未曾亲自交付过规则的进程中都会读作零——而会话在 web 应用、CLI 与一次性运行中都一样。一条交付指一条规则确实到达了模型:一次中断、一条折进工具结果里的提醒、一次被拒绝的调用,或一条经裁判的警告。
账本是按 profile 组织的,而规则集是按工作区组织的,因此某个计数可能指向这条工作区从未发现过的规则。页面把两者分开——deliveries here 只统计屏幕上的规则,其余一律显示为 delivered elsewhere,不会被折进工作区的数字里。
图表之下是明细列表:
控件 · 作用
按来源筛选 · 每种约定一个条目,附带计数:Bundled with the plugin 27、.omp/rules 2,以及该 profile 贡献的其他提供方
按文本筛选 · 匹配规则名、描述与来源路径
每行 · 名称、是否生效、描述,以及 source · path
未生效的行 · 说明原因——off · listed in ttsr.disabledRules——而不是光秃秃的 off
Host 下发稳定的原因代码(disabled、builtins-off、agent-filter、no-trigger、shadowed),由页面做本地化;页面无法识别的代码会原样显示,而不是被压成"未知"。rule 工具则把同样的代码用英文拼写出来,因为它的读者是模型。
自带规则——已发布的 27 条规则,按族分组(ts 13、go 8、rs 6),带文本筛选器,位于审计下方的同一页面。每行都有自己的开关,这直接生效;旁边的复选框只负责选中,下方的条则一次性作用于整个选区——Select all、Invert selection、Disable N selected、Enable N selected。
插件不去动带键的 plugins.row.config 槽位:那是平台为该条目提供的配置表单,动了它就会让所有非开关式的设置无路可去。读取来自审计;写入走配置表单,它持有 profile patch。ttsr 块是易失的,所以改动在下一步就会生效,而无需等到下次重启;审计发现配置发生变化时会自行重建。
页面从 --dsw-* 令牌取样式,而不是引入 dsh 的客户端包:那些包在客户端线上是分版本的,把其中一个拉进宿主包的依赖树,会把会话投影的第二份副本也一起拖进来。
管理规则
rule 工具就是管理界面。
调用 · 效果
{"name": "x"} · 加载一条规则的正文。
{"action": "list"} · 全部规则、其状态,以及未生效规则为何关闭。
{"action": "disable", "name": "x"} · 关闭一条自带规则。
{"action": "enable", "name": "x"} · 重新开启一条自带规则。
开关通过设置服务写入 profile patch,因此能跨重启持久化;又因为 ttsr 配置块是易失的,它们在下一步就会生效,而不是等到下次重启。当部署没有设置服务时,工具会如实说明,并打印需要添加的确切 YAML,而不是声称做了它并未做的改动。
只有自带规则可以开关。用户规则或项目规则由它们各自的文件管辖,工具会指向那个文件,而不是替你编辑它。
基于文件的等价物是 ttsr.disabledRules(一个名称列表)和 ttsr.builtinRules: false(全部)。
规则的标识只取决于名称,因此被更高优先级提供方认领的名字获胜,落败者被丢弃。粘性 RULES.md 永远共用名字 RULES 且总是生效;因此用户粘性文件遮蔽项目的那份,而 rules/RULES.md 又遮蔽前两者。
三层结构
1. rulebook——<domain-rules> 中的 - <name> (<globs>): <description>。正文不占用常驻上下文;模型按名称加载它。
2. always-apply——<generic-rules> 中的整个正文。
3. TTSR——带有任意触发字段的规则。它会被注册,离开另外两个桶,并在模型仍在书写时强制执行。
引用一条规则
dsh 没有内部 URL 协议注册表,因此 rule://<name> 地址由一个面向模型的工具提供。提示词中会公布 rule://<name>;模型用该名称调用 rule,收到 Markdown 正文。名称未知时,会回答可寻址规则的列表。
快照覆盖全部三个桶,因此被触发的 TTSR 规则在触发之后仍可再次读取。
写入一条规则
这个工具也能写。当用户说出一条下周依然成立的约束时,系统提示会告诉模型把它写下来,而不是只服从这一次:用 action: "create"、一个名字、一段 frontmatter 和一个 body 调用 rule,文件会落到该作用域解析出的目录里(见下)。它会像任何其他项目规则一样进入审计,因此可以在面板里编辑或删除。
有五样东西是被拒绝的,而不是被写出去:
- **名字其实是一条路径。*
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::3,最后更新 2026-10-02