- Fix typo "stauts" → "status" in stream.test.ts comment - Update frontmatter.ts link to packages/utils/src/ (moved from coding-agent) - Update notebook.ts link to src/edit/ (moved from src/tools/) - Mark removed bash-normalize.ts in docs and update description - Add CHANGELOG.md for swarm-extension and utils packages Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10 KiB
Rulebook Matching Pipeline
This document describes how coding-agent discovers rules from supported config formats, normalizes them into a single Rule shape, resolves precedence conflicts, and splits the result into:
- Rulebook rules (available to the model via system prompt +
rule://URLs) - TTSR rules (time-travel stream interruption rules)
It reflects the current implementation, including partial semantics and metadata that is parsed but not enforced.
Implementation files
../src/capability/rule.ts../src/capability/index.ts../src/discovery/index.ts../src/discovery/helpers.ts../src/discovery/builtin.ts../src/discovery/cursor.ts../src/discovery/windsurf.ts../src/discovery/cline.ts../src/sdk.ts../src/system-prompt.ts../src/internal-urls/rule-protocol.ts../src/utils/frontmatter.ts
1. Canonical rule shape
All providers normalize source files into Rule:
interface Rule {
name: string;
path: string;
content: string;
globs?: string[];
alwaysApply?: boolean;
description?: string;
condition?: string[];
scope?: string[];
interruptMode?: "never" | "prose-only" | "tool-only" | "always";
_source: SourceMeta;
}
Capability identity is rule.name (ruleCapability.key = rule => rule.name).
Consequence: precedence and deduplication are name-based only. Two different files with the same name are considered the same logical rule.
2. Discovery sources and normalization
src/discovery/index.ts auto-registers providers. For rules, current providers are:
native(priority100)cursor(priority50)windsurf(priority50)cline(priority40)
Native provider (builtin.ts)
Loads .omp rules from:
- project:
<cwd>/.omp/rules/*.{md,mdc} - user:
~/.omp/agent/rules/*.{md,mdc}
Normalization:
name= filename without.md/.mdc- frontmatter parsed via
parseFrontmatter content= body (frontmatter stripped)globs,alwaysApply,description,condition/legacyttsr_trigger,scope, andinterruptModeare parsed bybuildRuleFromMarkdown
Important caveat: condition values that look like file globs are converted into tool:edit(...) / tool:write(...) scope shorthands with catch-all condition .*.
Cursor provider (cursor.ts)
Loads from:
- user:
~/.cursor/rules/*.{mdc,md} - project:
<cwd>/.cursor/rules/*.{mdc,md}
Normalization (transformMDCRule):
description: kept only if stringalwaysApply: onlytrueis preserved (falsebecomesundefined)globs: accepts array (string elements only) or single stringcondition/legacyttsr_trigger,scope, andinterruptModeare parsed by shared rule helpersnamefrom filename without extension
Windsurf provider (windsurf.ts)
Loads from:
- user:
~/.codeium/windsurf/memories/global_rules.md(fixed rule nameglobal_rules) - project:
<cwd>/.windsurf/rules/*.md
Normalization:
globs: array-of-string or single stringalwaysApply,description,condition/legacyttsr_trigger,scope, andinterruptModeparsed by shared rule helpersnameis fixed toglobal_rulesfor the user global file and derived from filename for project rules
Cline provider (cline.ts)
Searches upward from cwd for nearest .clinerules:
- if directory: loads
*.mdinside it - if file: loads single file as rule named
clinerules
Normalization:
globs: array-of-string or single stringalwaysApply,description,condition/legacyttsr_trigger,scope, andinterruptModeparsed by shared rule helpersnameis fixed toclinerulesfor a.clinerulesfile and derived from filename for.clinerules/*.md
3. Frontmatter parsing behavior and ambiguity
All providers use parseFrontmatter (utils/frontmatter.ts) with these semantics:
- Frontmatter is parsed only when content starts with
---and has a closing\n---. - Body is trimmed after frontmatter extraction.
- If YAML parse fails:
- warning is logged,
- parser falls back to simple
key: valueline parsing (^(\w+):\s*(.*)$).
Ambiguity consequences:
- Fallback parser does not support arrays, nested objects, quoting rules, or hyphenated keys.
- Fallback values become strings (for example
alwaysApply: truebecomes string"true"), so providers requiring boolean/string types may drop metadata. ttsr_triggerworks in fallback (underscore key); keys likethinking-levelwould not.- Files without valid frontmatter still load as rules with empty metadata and full content body.
4. Provider precedence and deduplication
loadCapability("rules") (capability/index.ts) merges provider outputs and then deduplicates by rule.name.
Precedence model
- Providers are ordered by priority descending.
- Equal priority keeps registration order (
cursorbeforewindsurffromdiscovery/index.ts). - Dedup is first-wins: first encountered rule name is kept; later same-name items are marked
_shadowedinalland excluded fromitems.
Effective rule provider order is currently:
native(100)cursor(50)windsurf(50)cline(40)
Intra-provider ordering caveat
Within a provider, item order comes from loadFilesFromDir glob result ordering plus explicit push order. This is deterministic enough for normal use but not explicitly sorted in code.
Notable source-order differences:
nativeappends project then user config dirs.cursorappends user then project results.windsurfappends userglobal_rulesfirst, then project rules.clineloads only nearest.clinerulessource.
5. Split into Rulebook, Always-Apply, and TTSR buckets
After rule discovery in createAgentSession (sdk.ts):
- All discovered rules are scanned.
- Rules with
conditionentries are registered intoTtsrManager; legacyttsr_trigger/ttsrTriggerare accepted during rule parsing as condition fallbacks. - A separate
rulebookRuleslist is built with this predicate:
!isTtsrRule && rule.alwaysApply !== true && !!rule.description;
- An
alwaysApplyRuleslist is built:
!isTtsrRule && rule.alwaysApply === true;
Bucket behavior
- TTSR bucket: any rule with a non-empty parsed
conditionthatTtsrManager.addRule(...)accepts. Takes priority over other buckets. - Always-apply bucket:
alwaysApply === true, not TTSR. Full content injected into system prompt. Resolvable viarule://. - Rulebook bucket: must have description, must not be TTSR, must not be
alwaysApply. Listed in system prompt by name+description; content read on demand viarule://. - A rule with both
conditionandalwaysApplygoes to TTSR only (TTSR takes priority). - A rule with both
alwaysApplyanddescriptiongoes to always-apply only (not rulebook).
6. How metadata affects runtime surfaces
description
- Required for inclusion in rulebook.
- Rendered in system prompt
<rules>block. - Missing description means rule is not available via
rule://and not listed in system prompt rules.
globs
- Carried through on
Rule. - Rendered as
<glob>...</glob>entries in the system prompt rules block. - Exposed in rules UI state (
extensionsmode list). - Not enforced for automatic matching in this pipeline. There is no runtime glob matcher selecting rules by current file/tool target.
alwaysApply
- Parsed and preserved by providers.
- Used in UI display (
"always"trigger label in extensions state manager). - Used as an exclusion condition from
rulebookRules. - Full rule content is auto-injected into the system prompt (before the rulebook rules section).
- Rule is also addressable via
rule://<name>for re-reading.
condition, scope, and interruptMode
conditionis the current TTSR trigger field; legacyttsr_trigger/ttsrTriggerare accepted as fallback inputs during parsing.scopenarrows TTSR matching scope. A condition token that looks like a file glob becomestool:edit(<glob>)andtool:write(<glob>)scope entries plus catch-all condition.*.interruptModecan override the global TTSR interrupt mode for the rule.
7. System prompt inclusion path
buildSystemPromptInternal receives both rules (rulebook) and alwaysApplyRules.
Always-apply rules are rendered first, injecting their raw content directly into the prompt.
Rulebook rules are rendered in a # Rules section with:
Read rule://<name> when working in matching domain- Each rule's
name,description, and optional<glob>list
This is advisory/contextual: prompt text asks the model to read applicable rules, but code does not enforce glob applicability.
8. rule:// internal URL behavior
RuleProtocolHandler is registered with:
new RuleProtocolHandler({
getRules: () => [...rulebookRules, ...alwaysApplyRules],
});
Implications:
rule://<name>resolves against both rulebookRules and alwaysApplyRules.- TTSR-only rules and rules with no description and no
alwaysApplyare not addressable viarule://. - Resolution is exact name match.
- Unknown names return error listing available rule names.
- Returned content is raw
rule.content(frontmatter stripped), content typetext/markdown.
9. Known partial / non-enforced semantics
- Provider descriptions mention legacy files (
.cursorrules,.windsurfrules), but current loader code paths do not actually read those files. globsmetadata is surfaced to prompt/UI but not enforced by rule selection logic.- Rule selection for
rule://includes rulebook and always-apply rules, but not TTSR-only rules. - Discovery warnings (
loadCapability("rules").warnings) are produced butcreateAgentSessiondoes not currently surface/log them in this path.