| name | add-rule |
| description | 分析规则需求,设计 TTSR 规则的 frontmatter(condition、scope),生成 .omp/rules/ 下的规则文件 |
| argument-hint | 无直接参数 — 通过自然语言描述规则内容来触发 |
add-rule — 新增 TTSR 规则
分析用户描述的约束或模式,设计合适的 condition 和 scope,生成带 frontmatter 的 .omp/rules/*.md 规则文件。
触发方式
在对话中出现以下表述时触发:
- "添加以下的规则:..."
- "将xxx沉淀成一个规则"
- "根据以上的内容,将xxx写成规则"
- 用户描述了需要固化的编码约束
不适合作规则的场景(应拒绝或建议替代方案)
遇到以下情况,不建议生成规则文件:
- 一次性约束:只在某个 PR 里有效的模式,规则文件会永久存在
- 过于具体的匹配:condition 匹配的是 UUID、特定变量名、临时 hack 等不具备通用性的标识
- 工具链相关:某个 linter/clippy 已经能检查的,规则是重复劳动
- 高度易变的路径:scope 指向的目录结构可能经常变,规则跟不上
- 无法自动检测的模式:需要人工理解业务语义才能判断的(如"这个算法的时间复杂度不能超过 O(n²)")
→ 以上情况建议用户改用 conversation 中的一次性提醒。
执行流程
第一步:理解规则内容(需求分析)
通读用户描述的规则内容,将零散的需求翻译成结构化的分析结果。按以下维度逐一确认:
1. 约束性质 — 这条规则是在约束什么?
- ☐ 代码模式(禁止某种写法、强制某种写法)
- ☐ 命名约定(文件命名、函数命名、类型命名)
- ☐ 架构边界(层间依赖、模块可见性、禁止循环引用)
- ☐ 数据流规范(序列化/反序列化方式、状态管理)
- ☐ 全局常识(项目架构说明、编码风格指南)
2. 触发时机 — 用户希望规则在什么场景下"跳出来"?
- 助手正在写某种代码时(如写
unsafe 块、写测试、写某种 import)
- 助手正在调用某个工具时(如调用
read 读某个配置文件)
- 助手在自然语言中提到某个概念时
- 任何时候都应该存在的知识(全局参考)
→ 这个直接决定用
text / thinking / tool:<name> 哪个 scope token
3. 约束对象的具体特征 — 选一个"最独特"的标识符:
- 函数/方法名(
extract_value、Bun.sleep、setTimeout)
- 类型名(
MyType、Result、Optional)
- 关键字(
unsafe、unwrap、todo!)
- import 路径(
@core/、crate::utils)
4. 文件范围 — 规则约束应用在哪些文件上?
- 全局(所有文件)→ 不做路径限制
- 特定目录 → 用 glob 缩小(
src/commands/**/*.rs)
- 特定文件类型 → 用扩展名 glob(
**/*.test.ts)
- 单个文件 → 精确路径(
src/main.rs)
→ 这个直接决定 tool:<name>(<glob>) 中的 glob
在分析过程中如果用户描述的规则有歧义(例如"测试里不能用 unwrap",但没说是 Rust 还是 JS),先确认再继续。分析完成后,将以上结构化结果带入第二步映射到 frontmatter 字段。
第二步:设计 frontmatter
根据规则内容,设计规则文件中的 frontmatter 字段。TTSR(Time-Traveling Stream Rules)通过这套元数据决定何时匹配(condition)和在哪匹配(scope)。
先理解 TTSR 是怎么工作的 —— 数据流向:
助手在"说话"(生成回复)
│
├─ 写自然语言(text)──→ text 缓冲区
├─ 思考过程(thinking)──→ thinking 缓冲区
└─ 调用工具(tool)──→ tool 缓冲区
│
▼
每个缓冲区的内容不断累积,
每来一段新内容就拿 condition 正则去匹配
│
匹配上了?
├─ 否 → 继续流
└─ 是 → scope 允许在这个场景触发吗?
├─ 否 → 继续流
└─ 是 → 中断助手 → 注入规则内容 → 重试
拆开解释几个术语:
- "流"(stream):助手生成回复不是一次性给的,是一段一段(像水流一样)陆续产生的。这个持续输出的过程就叫"流"。
- 三种流来源(source):
text — 助手写的自然语言正文(就是你看到的对话回复)
thinking — 模型的内心独白/思考过程(如果你开了思维链)
tool — 助手调用工具时传的参数(例如 edit 工具传的补丁内容、write 工具传的文件内容)
- "流缓冲区"(stream buffer):流过来的内容不会丢掉,而是暂存在一个"缓冲区"里。每次新内容到达,系统把缓冲区里的全部内容拿去和 condition 正则做匹配。这样可以匹配到跨段的模式(比如一段话前后各一半)。
- "标准化快照"(matcherDigest):当助手调用
edit/write 这类工具时,参数可能是补丁格式(hashline),不是直接的可读代码。matcherDigest 是工具提供的一个"翻译器",能把补丁格式还原成最终要写入的源码。这样你写 condition 时就可以直接写源码里出现的函数名,而不用关心补丁格式长什么样。
一句话总结:condition 正则匹配的是"助手正在说/写的内容"(不是文件名,不是文件内容快照),scope 控制的是"在哪种流场景下才允许触发"。
condition — 触发正则表达式
condition 是一个正则表达式(或表达式数组),匹配流缓冲区内容(stream buffer):
- 工具参数流(
tool:edit、tool:write、tool:read 等):匹配工具调用的参数原始 JSON,如果工具提供了 matcherDigest(如 edit/write 的源码快照还原器),则匹配还原后的标准化源码快照
- 助手的自然语言输出(
text):匹配助手生成的 prose 文本
- 思考过程(
thinking):匹配模型的 thinking/chain-of-thought 文本
当任意 condition 命中缓冲区时,规则触发中断当前流,注入规则内容后重试。
类型:string | string[](数组 = OR 语义,任一匹配即触发)
设计原则:
- 选择规则约束范围内最独特的标识符(函数名、类型名、宏调用、特定关键字、import 路径、API 调用)
- 优先用单一名词或短模式,避免过长正则
- 如果有多个入口点,用数组传递多个条件,或用
| 合并(注意转义)
- 如果规则需要始终存在(全局架构指南), condition 可以用
".*",匹配所有流内容(注意会增加 token 消耗)
匹配内容随流来源变化,请根据规则的实际触发场景选择:
| 约束场景 | condition 匹配的内容 | 推荐 condition | 说明 |
|---|
| 约束 helper 函数调用 | edit/write 的源码快照 | "extract_value" | 函数名出现在写入的代码中 |
| 约束 import 模式 | edit/write 的源码快照 | "from '@core/parser'" | import 语句出现在写入的源码中 |
| 约束工具行为(如禁用某工具) | 工具参数 JSON | '"tool_name"' | 工具名称出现在参数 JSON 中 |
| 约束框架用法(如 unsafe 代码) | 写作的 prose/代码 | "unsafe" | 助手在生成代码时出现关键字 |
| 约束 serde 反序列化 | edit/write 的源码快照 | "serde_json::from_str|serde_json::from_value" | 反序列化调用写入代码时触发 |
| 约束全局架构 | 所有流 | ".*" | 始终触发(注意会增加 token 消耗) |
注意:
- condition 匹配的不是文件名,而是流内容。文件路径约束由
scope 控制
- 如果 condition 值看起来像文件 glob(如
*.rs、src/**/*.ts),系统会自动将其推导为 tool:edit(<glob>), tool:write(<glob>) scope,并将 condition 设为 ".*"。这是一种简写形式,手动编写时建议明确写 condition + scope
- 支持 PCRE 风格的头部内联 flag:
(?i)(忽略大小写)、(?m)(多行)、(?s)(单行/DOTALL),会自动翻译为原生 JS RegExp flags
scope — 触发的流范围
scope 定义哪些流来源(text / thinking / tool)在哪些路径上会触发规则检查。
类型:string | string[],每个 token 为以下格式之一:
| Token | 含义 | 示例 |
|---|
text | 匹配助手自然语言输出(prose) | text |
thinking | 匹配思考过程文本 | thinking |
tool / toolcall | 匹配所有工具调用 | tool |
tool:<name> | 匹配指定工具的所有调用 | tool:edit、tool:write、tool:read |
tool:<name>(<glob>) | 匹配指定工具中路径匹配 glob 的调用 | tool:edit(src/**/*.rs) |
<bare_name> | 裸工具名(等同于 tool:<bare_name>) | edit、write |
设计原则:
- 精确限定触发场景。大多数规则只需要
tool:edit(<glob>) 和/或 tool:write(<glob>),不要用 tool:edit(**/*.rs) 覆盖整个项目,尽量缩小到规则真正约束的目录
- 如果规则既要限制 prose 中提及某 API,也要限制代码中使用,加
text token
- 如果规则是只读参考(项目总览类),用
text 让写作时触发
- 如果规则约束所有文件类型,省略 glob
默认行为:scope 为空时,系统自动启用 text + tool(所有 prose 和工具调用,排除 thinking)
Scope 示例:
| 适用范围 | 推荐 scope |
|---|
| 单个模块的代码写操作 | "tool:edit(src/path/to/mod.rs), tool:write(src/path/to/mod.rs)" |
| 某个目录下所有代码操作 | "tool:edit(src/commands/**/), tool:write(src/commands/**)" |
| 分散文件 + prose 中提及 | "text, tool:edit(src/a.rs), tool:edit(src/b.rs)" |
| 所有 .rs 文件的编辑 | "tool:edit(**/*.rs)" |
| 全局(所有流) | "text, thinking, tool" |
完整 frontmatter 示例
---
name: my-rule
description: 禁止在 Rust 测试中使用 unwrap()
condition:
- "\.unwrap\(\)"
- "(?i)unwrap"
scope:
- tool:edit(**/*.rs)
- tool:write(**/*.rs)
---
提示:生成规则文件时,直接使用 frontmatter YAML 格式。condition 和 scope 支持 YAML 序列(数组形式)或逗号分隔的字符串。
第三步:生成规则文件
在 .omp/rules/ 下创建 <short-name>.md,格式:
---
name: <规则文件名>
description: <一句话描述规则约束什么>
condition: <触发正则,支持数组或字符串>
scope: <流范围 token,支持数组或逗号分隔>
---
# <规则标题>
<规则正文,包含具体的行为约束、原因、示例>
文件命名规则:
- 使用
kebab-case(短横线命名),例如 no-unwrap-in-tests.md
- 文件名去掉
.md 扩展名后即为规则的 name,会被 sanitizeRuleName() 清洗(只保留字母数字和连字符)
- 同名规则按优先级覆盖:项目规则 > 用户规则 > 内置默认规则(同名时优先级高的胜出)
YAML 中的正则转义注意事项:
规则正文编写规范:
- 有禁有导:"不要做 X,应该做 Y" 是标准格式
- 解释原因:每条约束说明为什么
- 给出示例:正确写法 + 错误写法
- 保持简短:超过 200 行考虑拆分
第四步:自我校验
在生成后确认: