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;
ttsrTrigger?: string;
_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 .pisces rules from:
- project:
<cwd>/.pisces/rules/*.{md,mdc} - user:
~/.pisces/agent/rules/*.{md,mdc}
Normalization:
name= filename without.md/.mdc- frontmatter parsed via
parseFrontmatter content= body (frontmatter stripped)globs,alwaysApply,description,ttsr_triggermapped directly
Important caveat: globs is cast as string[] | undefined with no element filtering in this provider.
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 stringttsr_trigger: string onlynamefrom 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,descriptioncast from frontmatterttsr_trigger: string onlynamefrom 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: only if booleandescription: string onlyttsr_trigger: string only
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
condition(frontmatter key;ttsr_trigger/ttsrTriggeraccepted as fallback) are registered intoTtsrManager. - A separate
rulebookRuleslist is built with this predicate:
!registeredTtsrRuleNames.has(rule.name) && !rule.alwaysApply && !!rule.description- An
alwaysApplyRuleslist is built:
!registeredTtsrRuleNames.has(rule.name) && rule.alwaysApply === trueBucket behavior
- TTSR bucket: any rule with
condition(description not required). 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.
ttsr_trigger
- Mapped to
rule.ttsrTrigger. - If present, rule is routed to TTSR manager, not rulebook.
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.