oh-my-pi Rulebook 匹配管道:从多格式规则发现、归一化、优先级仲裁到 TTSR 分桶的实现全解析
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
本文以 oh-my-pi 项目中 docs/rulebook-matching-pipeline.md 为骨架,结合packages/coding-agent/src下的真实源码,完整剖析 coding-agent 如何从 OMP、Agent、Cursor、Windsurf、Cline、GitHub Copilot 等八类配置源中发现规则,将其归一化为统一的Rule形状,按优先级去重后拆分为「Rulebook 规则」(通过系统提示与rule://URL 提供给模型)与「TTSR 规则」(Time Traveling Stream Rules,用于流式触发中断)两个出口。读完本文,你将掌握每条规则元数据(globs、alwaysApply、agents、condition、astCondition、scope、interruptMode)在整条管道中的真实作用边界,以及哪些语义当前只解析、不强制执行。
1. 统一规则形状:一切来源最终都是Rule
所有 provider 最终都把各自的源文件归一化为同一个Rule结构。该类型定义于 packages/coding-agent/src/capability/rule.ts:
interface Rule { name: string; path: string; content: string; globs?: string[]; alwaysApply?: boolean; description?: string; condition?: string[]; astCondition?: string[]; scope?: string[]; agents?: string[]; interruptMode?: "never" | "prose-only" | "tool-only" | "always"; _source: SourceMeta; }各字段含义与来源:
| 字段 | 说明 |
|---|---|
name | 规则标识,通常由文件名去掉扩展名得到;Capability 的 key 即rule.name |
path | 规则文件的绝对路径 |
content | 剥离 frontmatter 后的正文 |
globs | 该规则适用的文件路径模式 |
alwaysApply | 是否无条件注入系统提示 |
description | 规则描述,是进入 Rulebook 列表的必要条件 |
condition | 正则形式的 TTSR 触发条件(新键;兼容旧ttsr_trigger/ttsrTrigger) |
astCondition | ast-grep 结构化模式触发条件,仅作用于 edit/write 工具流 |
scope | TTSR 匹配的流表面白名单(text/thinking/tool/tool:xxx) |
agents | 该规则适用的 Agent 名称 glob(小写化) |
interruptMode | 单规则级 TTSR 中断模式覆盖 |
_source | 来源元数据(provider、路径、user/project 级别) |
ruleCapability的定义在 packages/coding-agent/src/capability/rule.ts:key: rule => rule.name。一个直接后果是:优先级仲裁与去重完全基于name——两个不同文件只要name相同,就被视为同一条逻辑规则,先到者胜出,后到者被标记为_shadowed。这一点贯穿整个管道的所有阶段。
2. 八类规则发现源及其归一化
packages/coding-agent/src/discovery/index.ts 通过 import 副作用自动注册全部 provider。对rules能力而言,当前生效的 provider 及其优先级如下(高者先):
| 优先级 | Provider | 源文件 |
|---|---|---|
| 100 | native | packages/coding-agent/src/discovery/builtin.ts |
| 90 | omp-plugins | packages/coding-agent/src/discovery/omp-plugins.ts |
| 70 | agents | packages/coding-agent/src/discovery/agents.ts |
| 50 | cursor | packages/coding-agent/src/discovery/cursor.ts |
| 50 | windsurf | packages/coding-agent/src/discovery/windsurf.ts |
| 40 | cline | packages/coding-agent/src/discovery/cline.ts |
| 30 | github | packages/coding-agent/src/discovery/github.ts |
| 1 | builtin-defaults | packages/coding-agent/src/discovery/builtin-defaults.ts |
同优先级(50)时按注册顺序,即cursor先于windsurf。绝大多数 provider 复用 packages/coding-agent/src/discovery/helpers.ts 中的buildRuleFromMarkdown/discoverRuleFromMarkdown共享归一化路径(文件名派生name、剥离 frontmatter 的content、解析globs/alwaysApply/description/condition/astCondition/scope/agents/interruptMode)。注意discoverRuleFromMarkdown与buildRuleFromMarkdown的唯一区别:前者在 frontmatter 中enabled: false时返回null,即被显式禁用(helpers.ts)。
2.1 native provider(.omp,优先级 100)
builtin.ts是 OMP 的原生配置源(builtin.ts),规则加载顺序如下:
- 项目规则:当 cwd 的
.omp/目录非空时,加载<cwd>/.omp/rules/*.{md,mdc}; - 用户规则:
<active-native-agent-dir>/rules/*.{md,mdc}; - 用户粘性规则:
<active-native-agent-dir>/RULES.md; - 项目粘性规则:从 cwd 向仓库根向上查找最近的、非空的
.omp/目录中的RULES.md;OMP 不会越过该目录继续向上查找。
其中 active native agent 目录默认是~/.omp/agent,跟随命名 profile(getAgentDir()),并可通过PI_CODING_AGENT_DIR环境变量重定向。
粘性RULES.md会被合成为强制alwaysApply: true的规则,即每次会话轮次都重新注入,以在长对话中保持约束效力。这里有一处值得注意的源码与文档的差异:文档写作时称两个粘性文件都用固定名RULES,而当前源码(builtin.ts)为:
const ruleName = level === "project" ? "RULES@project" : "RULES";即用户级粘性规则名为RULES,项目级粘性规则名为RULES@project。在按名去重的模型下,这一命名意味着用户粘性内容与项目粘性内容不会互相遮蔽,而普通的rules/RULES.md仍可覆盖两者。
关键 caveat:frontmatter 中形如文件 glob 的condition值会被转换为tool:edit(...)/tool:write(...)的 scope 简写,并配以兜底条件.*(详见 parseRuleConditionAndScope)。
2.2 omp-plugins provider(优先级 90)
从配置好的扩展包根目录内加载rules/*.{md,mdc},同样走共享的buildRuleFromMarkdown归一化路径,按每个扩展包根目录依次追加结果。
2.3 agents provider(.agent / .agents,优先级 70)
同时支持.agent与.agents两种目录约定:
- 项目级:从 cwd 向仓库根向上遍历,加载每个祖先目录的
<ancestor>/.agent/rules/*.{md,mdc}与<ancestor>/.agents/rules/*.{md,mdc}; - 用户级:
~/.agent/rules/*.{md,mdc}与~/.agents/rules/*.{md,mdc}。
加载顺序为项目遍历结果在前、用户主目录结果在后。归一化走共享 Markdown 路径,字段解析与 native 一致。
2.4 cursor provider(.cursor,优先级 50)
从以下位置加载:
- 用户级:
~/.cursor/rules/*.{mdc,md} - 项目级:
<cwd>/.cursor/rules/*.{mdc,md}
归一化由 cursor.ts 的transformMDCRule完成,关键规则:
description:仅当为字符串时保留;alwaysApply:严格布尔归一化——仅当 frontmatter 明确为alwaysApply: true时才为true,其余一切值(包括字符串"true")都变成false;globs:接受字符串数组(仅保留字符串元素)或单个字符串;condition/ttsr_trigger、astCondition、scope、agents、interruptMode均由共享 rule helpers 解析;name由文件名去扩展名得到。
顺序上 user 结果先、project 结果后。
2.5 windsurf provider(.windsurf,优先级 50)
- 用户级:
~/.codeium/windsurf/memories/global_rules.md,规则名固定为global_rules; - 项目级:
<cwd>/.windsurf/rules/*.md,规则名取自文件名。
用户级文件先加载,项目级后加载。其余字段由共享 helpers 解析(windsurf.ts)。
2.6 cline provider(.clinerules,优先级 40)
只支持项目级配置,从 cwd 向上查找最近的一个.clinerules(cline.ts):
- 若是目录:加载其中全部
*.md,规则名取自文件名; - 若是文件:作为单条规则加载,规则名固定为
clinerules。
2.7 github provider(.github/instructions,优先级 30)
递归加载*.instructions.md:
- 项目级:
<cwd>/.github/instructions/; - 用户级:对
COPILOT_CUSTOM_INSTRUCTIONS_DIRS(逗号分隔的目录列表)中的每个目录,加载<dir>/.github/instructions/。
文件名去掉.instructions.md后缀即规则名。共享 Markdown 解析仍然识别 OMP 规则元数据(含 TTSR 字段)。GitHub 的applyTo另有专门归一化逻辑:
- 逗号分隔字符串(或容错的 YAML 数组)→
globs; *、**或**/*→ 规则变为 always-apply 并清空globs;- 其他 glob → 规则非 always-apply;若缺少
description,则根据 globs 自动生成; - 缺少
applyTo→ 生成一条 rulebook 描述外加一条发现警告。
特别注意:由于 TTSR 分桶发生在 always-apply/rulebook 分桶之前,携带了被接受的condition或astCondition的 GitHub instruction,无论applyTo如何,都只属于 TTSR。
2.8 builtin-defaults provider(优先级 1)
随 agent 内置的默认规则集,优先级最低,任何同名用户/项目/工具规则都会覆盖内置默认。其BUILTIN_DEFAULTS_PROVIDER_ID = "builtin-defaults"(rule.ts),并可通过ttsr.builtinRules配置整体开关。加载顺序为内置规则源本身的嵌入顺序。
3. frontmatter 解析行为与歧义处理
所有 provider 都经由 packages/utils/src/frontmatter.ts 的parseFrontmatter,其语义是:
- 仅在内容以
---开头且有闭合的\n---时才解析 frontmatter;否则整份文件按正文处理(frontmatter.ts)。 - 提取 frontmatter 后,正文会被
trim()。 - 若整篇 YAML 解析失败:
- 记录一条警告;
- 回退到简单的
key: value行解析(正则^([\w-]+):\s*(.*)$); - 每个捕获到的值独立地再按 YAML 重解析一次,只有仍然解析失败的值才保留为原始 trim 字符串(frontmatter.ts)。
回退解析的边界情况:
- 多行数组、嵌套对象等依赖缩进的 YAML 结构无法重建;但合法的单行 flow 值(如
[text, thinking])可以在逐值重解析中存活; - 单个格式错误的值保持原始字符串,需要布尔/列表/对象的 provider 可能会丢弃该元数据;
- 下划线键
ttsr_trigger在回退路径中可用;连字符键(如thinking-level)也能解析并被归一化为 camelCase(thinkingLevel)——键归一化同样作用于 YAML 成功路径(normalizeFrontmatterKeys,见 frontmatter.ts); - 没有合法 frontmatter 的文件仍会以空元数据 + 完整正文的形式作为规则加载;
- scope 解析器还能容忍常见的畸形回退值
scope: "text","thinking",但规范写法仍是scope: "text, thinking"(逗号在字符串内)或scope: [text, thinking](YAML 序列)。
4. Provider 优先级与按名去重
loadCapability("rules")合并各 provider 输出后,按rule.name去重(入口在 packages/coding-agent/src/capability/index.ts)。
4.1 优先级模型
- provider 按 priority 降序排列;
- 同优先级保持注册顺序(
cursor在windsurf之前); - 去重为先到先得(first-wins):先遇到的规则名被保留,后续同名项在
all中标记为_shadowed,并从items中剔除。
因此实际生效的规则 provider 顺序就是第 2 节表格中的顺序。
4.2 provider 内部的顺序 caveat
provider 内部顺序来自loadFilesFromDir的 glob 结果顺序加上显式 push 顺序。这在常规使用下是确定性的,但代码中并未显式排序。各来源的追加顺序差异:
native:项目.omp/rules→ 用户~/.omp/agent/rules→ 用户RULES.md→ 最近项目RULES.md;omp-plugins:按每个配置的扩展包根目录依次追加rules/结果;agents:项目遍历的.agent/.agents规则目录在前,用户主目录在后;cursor:用户结果在前,项目结果在后;windsurf:用户global_rules在前,项目规则在后;cline:只加载最近的.clinerules源;github:cwd 项目 instructions 在前,随后按环境变量列表顺序追加各COPILOT_CUSTOM_INSTRUCTIONS_DIRS条目;builtin-defaults:内置规则源的嵌入顺序。
5. 分桶:Rulebook、Always-Apply 与 TTSR
规则发现完成后,packages/coding-agent/src/capability/rule-buckets.ts 中的bucketRules(...)在会话创建(createAgentSession,见 packages/coding-agent/src/sdk.ts)时执行会话级过滤与分桶,共六步:
- 丢弃
ttsr.disabledRules中列出的规则; - 当
ttsr.builtinRules === false时,丢弃来自builtin-defaultsprovider 的全部规则; - 丢弃
agentsglobs 与当前会话 agent 名不匹配的规则(顶层会话 agent 名为main,子会话为 agent 定义名;无agents字段的规则适用于所有 agent); - 将
condition或astCondition非空的规则注册进TtsrManager;注册成功即该规则仅属于 TTSR; - 其余
alwaysApply === true的规则进入alwaysApplyRules; - 其余带
description的规则进入rulebookRules。
对应实现(rule-buckets.ts):
for (const rule of rules) { if (disabled.has(rule.name)) continue; if (!includeBuiltin && rule._source?.provider === BUILTIN_DEFAULTS_PROVIDER_ID) continue; if (!ruleAppliesToAgent(rule, options.agentName)) continue; const hasTtsrCondition = (rule.condition && rule.condition.length > 0) || (rule.astCondition && rule.astCondition.length > 0); const isTtsrRule = hasTtsrCondition ? ttsrManager.addRule(rule) : false; if (isTtsrRule) continue; if (rule.alwaysApply === true) { alwaysApplyRules.push(rule); continue; } if (rule.description) { rulebookRules.push(rule); } }5.1 各桶的行为要点
- TTSR 桶:任何启用且带非空
condition(正则)或astCondition(ast-grep 模式)且被TtsrManager.addRule(...)接受的规则。优先级最高,先于其他桶判断。 - Always-apply 桶:
alwaysApply === true且非 TTSR。完整内容注入系统提示,同时可通过rule://读取。 - Rulebook 桶:必须有
description、非 TTSR、非 always-apply。系统提示只列出name + description,正文通过rule://按需读取。
边界情形:
- 同时带触发条件与
alwaysApply的规则:只有 TTSR 注册拒绝它时才可能落到 always-apply; - 同时带
alwaysApply与description的规则:只进 always-apply,不进 rulebook。
6. 元数据对运行时各表面的影响
6.1description
- 进入 rulebook 的必要条件;
- 渲染在系统提示的 rulebook 区块(默认模板为
<domain-rules>,自定义提示模板为<rules>); - 缺失 description 的规则不进 rulebook 列表;除非它是 always-apply 或被接受的 TTSR 规则,否则也无法通过
rule://寻址。
6.2globs
- 随
Rule原样携带; - 默认提示的 rulebook 列表中以内联形式渲染:
- <name> (<glob>, ...): <description>;自定义提示模板渲染为<glob>...</glob>条目; - 暴露在规则 UI 状态(extensions 模式列表)中;
- 被 TTSR 用作全局路径门:若 TTSR 规则带 globs,匹配上下文必须包含至少一个匹配的文件路径;
- 不用于为
rule://自动挑选 rulebook 规则——rulebook 的匹配仍是提示层面的建议行为。
6.3alwaysApply
- provider 解析并保留;
- UI 中显示为
"always"触发标签; - 作为排除出
rulebookRules的条件; - 规则全文自动注入系统提示(位于 rulebook 规则区块之前);
- 也可通过
rule://<name>重新读取。
6.4agents:把规则限定到特定 Agent
- 接受 YAML 序列、单个字符串或逗号分隔字符串;模式为小写化 glob,对 agent 定义名(
scout、reviewer、foreman-*)做大小写不敏感匹配。{a, b}花括号 glob 组内逗号两侧的空格会被容忍并归一化(parseRuleAgents复用 scope tokenizer,见 rule.ts)。 - 字面量
main匹配顶层会话;无定义名的子 agent 回退为sub。main与sub均为保留哨兵:parseAgentFields拒绝自定义 agent 使用这两个名字(helpers.ts),因此真实 agent 永远无法遮蔽哨兵。 - 省略(或空列表)表示规则适用于所有 agent——即既有行为。
- 过滤在会话创建时的
bucketRules(...)中、TTSR 注册之前执行一次:不匹配的规则不进任何桶、不会被编译进TtsrManager、在该会话中也无法通过rule://寻址。 - 子 agent 会收到父级未过滤的完整规则列表,并以其自身名字重新评估
agents,因此 scout-only 的规则只在 scout 中加载。
agents: [scout, "foreman-*"]# 仅主 agent;所有子 agent 忽略此规则: agents: main6.5condition、astCondition、scope、interruptMode
condition:正则 TTSR 触发字段;解析时接受旧键ttsr_trigger/ttsrTrigger作为回退输入。开头为(?i)、(?m)或(?s)的内联标志组会被翻译为等价的 JavaScriptRegExp标志(compileRuleCondition,见 rule.ts)——因为 Bun/JS 的RegExp拒绝内联标志前缀,若无此翻译,condition: "(?i)pre.existing"会在编译期抛错并被静默丢弃。astCondition:ast-grep 触发字段,字符串或 YAML 模式序列,原样保留(不做 glob 推断);只在 edit/write 工具流上匹配,语言由文件路径推断。一条规则可以同时设置condition与astCondition。scope:把 TTSR 匹配收窄到流表面白名单;接受逗号分隔的 YAML 字符串或 YAML 序列。省略时监视助手散文(text)与全部工具参数(tool),但不监视 thinking。
# 散文与思考;两种等价写法: scope: "text, thinking"scope: [text, thinking]# 块式 YAML 序列同样合法: scope: - text - thinking# 仅 edit/write 产生的 TypeScript 源码快照: scope: "tool:edit(*.ts), tool:write(*.ts)"合法 token 为text、thinking、tool(或toolcall)与tool:<name>(<path-glob>)。解析器容忍畸形回退拼写scope: "text","thinking",但可移植的规则文件应把逗号放进单个 YAML 字符串,或使用 YAML 序列。
- 条件里的文件 glob 简写:形如文件 glob 的
conditiontoken 会变成tool:edit(<glob>)与tool:write(<glob>)两条 scope 条目,外加兜底条件.*;astConditiontoken 永不触发此简写。启发式判断函数isLikelyFileGlob见 rule.ts:含正则元字符\^$+|()的不算,不含?*[]{}的不算,含/的直接算 glob,否则要求形如^\*\.[^\s/]+$(如*.rs)。 interruptMode:可覆盖全局 TTSR 中断模式,取值never | prose-only | tool-only | always,非法值被丢弃(helpers.ts)。
7. 系统提示注入路径
buildSystemPromptInternal同时接收rules(rulebook)与alwaysApplyRules(实现在 packages/coding-agent/src/system-prompt.ts)。
- always-apply 规则会先与生效的 system/custom/append 提示源及已加载的 context-file 正文做去重:某条规则的归一化内容已出现在上述任一来源中时,跳过自动注入。剩余原始正文渲染在 rulebook 列表之前——默认模板放进
<generic-rules>,打包的自定义提示模板则直接渲染。 - rulebook 规则渲染在
<domain-rules>块,格式为- <name> (<globs>): <description>;提示中的 URL 列表记录rule://<name>,工作流章节要求模型先读取相关规则。自定义提示模板(custom-system-prompt.md)则以<rule name="...">条目 +<glob>子元素渲染,并带显式的 "You MUST readrule://<name>" 指令。
需要明确:这是建议性/上下文性行为——提示文本请求模型读取适用规则,但代码并不强制校验 glob 适用性。
8.rule://内部 URL 行为
packages/coding-agent/src/internal-urls/rule-protocol.ts 的RuleProtocolHandler针对进程级 active-rule 快照解析,该快照在每次顶层会话创建时由 sdk.ts 安装一次:
setActiveRules([ ...rulebookRules, ...alwaysApplyRules, ...ttsrManager.getRules(), ]);由此产生以下行为:
rule://<name>可解析rulebookRules、alwaysApplyRules与已注册的 TTSR 规则三者;- TTSR 规则虽然已从 rulebook/always 中分桶出去,但
ttsrManager.getRules()会把它们重新加回快照,使一条被触发的规则(例如内置规则)仍可被重新读取; - 没有 description、没有
alwaysApply、也没有被接受的 TTSR 条件的规则,无法通过rule://寻址; - 解析为精确名称匹配(
rules.find(r => r.name === ruleName)); - 未知名称返回错误,并在错误信息中列出全部可用规则名(
Unknown rule: ...\nAvailable: ...); - 返回内容是原始
rule.content(frontmatter 已剥离),内容类型为text/markdown。
9. 已知的部分语义 / 未强制执行的语义
文档与源码共同确认以下边界,避免使用者产生不切实际的预期:
- 当前为
rules加载的 provider 是native、omp-plugins、agents、cursor、windsurf、cline、github与内置builtin-defaults;其他工具的 provider 文件可能解析其他配置格式,但没有注册规则加载器。 globs元数据暴露给提示/UI,并作为 TTSR 匹配的全局路径门,但不用于为rule://自动挑选 rulebook 规则。rule://的规则选择包含 rulebook、always-apply 与已注册 TTSR 规则(因此被触发的 TTSR 规则可重读),但不包含既无触发条件、又无description与alwaysApply的规则。- 发现警告(
loadCapability("rules").warnings)会产生,但createAgentSession目前在这条路径上不对外展示或记录它们。
10. 从文档到源码的核对清单
想要亲手验证上述每一环,可直接按图索骥:
- 统一形状与字段解析:packages/coding-agent/src/capability/rule.ts
- 分桶漏斗:packages/coding-agent/src/capability/rule-buckets.ts
- 共享 Markdown 归一化与 glob 扫描:packages/coding-agent/src/discovery/helpers.ts
- 各 provider:builtin.ts、omp-plugins.ts、agents.ts、cursor.ts、windsurf.ts、cline.ts、github.ts、builtin-defaults.ts
- frontmatter 解析与回退:packages/utils/src/frontmatter.ts
rule://协议处理器:packages/coding-agent/src/internal-urls/rule-protocol.ts- 会话创建与快照安装:packages/coding-agent/src/sdk.ts
- 系统提示注入:packages/coding-agent/src/system-prompt.ts
- TTSR CLI 入口:packages/coding-agent/src/cli/ttsr-cli.ts
以 4.1 的「先到先得」与 5.1 的「TTSR 优先」两条规则为心智锚点,再对照第 6 节各元数据的真实作用边界,即可对 oh-my-pi 的规则系统建立完整、可预测的理解:文件名决定身份,优先级决定胜负,条件字段决定归属,其余元数据决定运行时表现——而其中相当一部分(如 rulebook 的 glob 匹配)仍是面向模型的建议语义,而非代码强制行为。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考