- 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 示例
```yaml
---
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`,格式:
```markdown
---
name: <规则文件名>
description: <一句话描述规则约束什么>
condition: <触发正则,支持数组或字符串>
scope: <流范围 token,支持数组或逗号分隔>
---
# <规则标题>
<规则正文,包含具体的行为约束、原因、示例>
```
**文件命名规则**:
- 使用 `kebab-case`(短横线命名),例如 `no-unwrap-in-tests.md`
- 文件名去掉 `.md` 扩展名后即为规则的 `name`,会被 `sanitizeRuleName()` 清洗(只保留字母数字和连字符)
- 同名规则按优先级覆盖:项目规则 > 用户规则 > 内置默认规则(同名时优先级高的胜出)
**YAML 中的正则转义注意事项**:
- YAML 双引号字符串:`\.`、`\(`、`\)` 这类序列**不是** YAML 的合法转义序列,所以会原样保留,传给正则引擎后含义正确(`\.` → 匹配点号,`\(` → 匹配左括号)。**不需要额外转义**。
- 只有 `\\`(YAML 双引号中代表一个反斜杠)需要留意——如果正则要匹配一个**字面反斜杠**,YAML 双引号里要写 `\\\\`(YAML → 两个反斜杠字符串 → 正则里匹配一个反斜杠)。
- 最安全的做法:**用 YAML 数组形式 + 单引号**。YAML 单引号不处理任何转义,正则里的反斜杠写起来和纯正则完全一致:
```yaml
condition:
- '\.unwrap\(\)'
- "(?i)unwrap"
```
这样写,`\.unwrap\(\)` 传到正则引擎就是 `\.unwrap\(\)`,没有歧义。
- 如果 YAML 解析遇到问题,系统有 fallback 按行解析 frontmatter,但不要依赖这个
**规则正文编写规范**:
- 有禁有导:"不要做 X,应该做 Y" 是标准格式
- 解释原因:每条约束说明为什么
- 给出示例:正确写法 + 错误写法
- 保持简短:超过 200 行考虑拆分
### 第四步:自我校验
在生成后确认:
- [ ] `condition` 正则在作用范围内足够独特(不会误触发)
- [ ] `scope` 覆盖规则约束的所有流场景和文件路径(不会漏触发)
- [ ] 如果规则引用了具体文件路径,这些路径在磁盘上存在
- [ ] 规则正文的路径和目录名与项目实际结构一致(检查 `.omp/AGENTS.md` 或实际文件树)
- [ ] YAML 中的正则转义正确——YAML 双引号中 `\.`、`\(` 会原样传递到正则(不需要额外转义);如果用单引号+数组形式,完全不用操心转义
- [ ] scope 中的 glob 路径在项目中确实能匹配到文件(用 `glob` 快速验证)
- [ ] `condition` 数组的每个条目都能编译通过(非空、非纯空白、有效的正则语法)
Ver no GitHub