# Add Rule

> 分析规则需求，设计 TTSR 规则的 frontmatter（condition、scope），生成 .omp/rules/ 下的规则文件

- Skill: `ghost-him/add-rule` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ghost-him/add-rule`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ghost-him/add-rule/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ghost-him (https://skillmd.com/u/ghost-him)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ghost-him/add-rule

---


# 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` 数组的每个条目都能编译通过（非空、非纯空白、有效的正则语法）

