How it works
How rules are chosen
Sending every rule on every edit wastes tokens and buries the rules that matter. get_rules sends the rules a change needs, tells the agent what it left out, and never repeats itself within a session.
The idea in one picture
The judgement about which kinds of work a rule matters for is made once, by the agent, when it tags the rule. Applying that judgement on each request is cheap and deterministic: no model call, no network.
Step by step
- Eligibility: scope Only active rules whose scope covers at least one of the given files are considered. A rule that covers several of the files takes its most specific level. Rules with hidden characters are never served.
- Short lists are sent whole
With 10 or fewer eligible rules, or when no
taskwas given, everything eligible is sent. Filtering so little would save almost nothing, and without a task Lore cannot tell what is relevant. A reply without a task says so, so the agent can pass one next time. - Longer lists are cut down
The agent's
task(one sentence, up to 500 characters) is scored against each eligible rule. A rule is sent if it is markedalways, was named ininclude, or scores above zero and at least 30% of the best match. - Order Always-rules first, then by relevance, then most specific scope, then oldest. Without a task: always-rules, then most specific scope, then oldest.
- No repeats
A rule already sent in this session is not sent again; the reply counts them instead.
refresh=truesends them again, which the agent uses after its context was reset. - Titles of what was left out
The reply lists the id and title of every other eligible rule (up to 25 lines), once per session. If one matters, the agent calls
get_rulesagain with its id ininclude, or usessearch_rules. - Optional cap
A caller can pass
max_tokens(at least 100). Serving then stops at the first rule that does not fit, so a general rule is never sent in place of a more specific one that was left out. Without it, there is no cap.
What a reply looks like
Rules for project `shop` (follow before writing code; sections: src/api)
- [global] Never commit secrets or API keys
- [global] Use spaces for indentation, four per level
- [global] Wrap third party SDK calls in an adapter
- [global] Use dependency injection for service classes
- [global] Set an explicit timeout on every outbound HTTP call
9 other rule(s) cover these files but were not chosen for your task. If one of these titles matters for your change, call get_rules again with its id in `include`; or use search_rules:
- 22797ab3 DB via repository
- d58f1c8d Request validation
- de8995b1 Reversible migrations
- f6e0cfe0 Request-id logging
…
The label in brackets is the rule's level: file for glob scopes, section, or global. Further notes can follow, each at most once per session: how many rules were not repeated, untagged rules to organise, review feedback to read, or paths that belong to another project. See get_rules.
How the matching works
Ranking is BM25, a standard keyword-relevance formula, computed in plain Python (core/search.py). What it compares:
- Rule side
- The rule's text, its title, and its trigger words (counted twice). For
search_rulesthe words of the rule's scope are included too, so a rule scoped todbis about databases. - Task side
- The task sentence. The file paths are not used for ranking, because scope already decided which files a rule covers.
Both sides go through the same steps:
- Lower-casing, and splitting identifiers:
UserRepository,user_repo.pyanduser-repoall yielduserandrepo. - Dropping filler words (
the,use,add,file, …). - A file extension glued to a name (
scanner.py) is ignored, so that mentioning a Python file does not make every rule about Python look relevant. A bare.pystill means the language. - A few aliases (
py→ python,db→ database,repo→ repository,spec→ test). - A rough stem, so
validate,validationandvalidatingmeet, andsanitizemeetssanitization. - A topic vocabulary: words in the same area also match each other. A task about a "column" finds the rule about "migrations" because both belong to the data topic. There are 15 topics (data, http, test, log, error, ui, types, naming, security, config, dependency, docs, validate, style, git); you can extend them, see Extending Lore.
Tagging
A rule is tagged when its tagged field is set. Tags come from the agent, in two ways:
- At proposal time:
propose_rulesacceptstitle,triggersandalwayswith each rule. - Afterwards, for existing rules:
organize_ruleslists approved rules that have none, andtag_rulessaves the agent's answer. The first reply of a session reminds the agent to do this when untagged rules cover the files.
The agent is told to be strict with always: an always-rule is sent on every request and costs tokens every time. Style rules for a language, safety rules and architecture invariants qualify; most rules do not. After tagging, the agent reports which rules it marked always, and you can unpin any of them:
python3 review.py --name myapp list --details # tags are shown under each rule
python3 review.py --name myapp pin 1a2b3c4d # send it on every request
python3 review.py --name myapp unpin 1a2b3c4d
A rule that was never tagged is matched on its text alone, so it can be held back when it should not be. The title list is the safety net, and review.py list tells you how many rules are untagged.
What it saves, honestly
Token counts here are estimated as characters divided by four. In one test, 25 active rules were tagged the way an agent would, and five different edits were requested in one session:
| Call | Rules sent | Tokens |
|---|---|---|
| Everything, no task (what a flat instruction file costs on every edit) | 25 | 367 |
| 1. add pagination to the orders endpoint | 4 | 260 (includes the title list) |
| 2. add a nickname column to the users table | 4 | 135 |
| 3. call the Stripe SDK from the payment service | 3 | 121 |
| 4. fix the failing test for the checkout service | 5 | 147 |
| 5. refund a payment and log it | 2 | 101 |
| Total, five edits | 764 instead of 1,835 |
The first call is the expensive one, because it also names the rules that were left out. The saving grows with the number of rules and the number of edits in a session; with a handful of rules there is little to save, which is why short lists are sent whole. These figures come from one made-up rule set, not a benchmark. Measure your own with review.py resolve and review.py usage.
Where it can go wrong
- A relevant rule is held back. Keyword matching is not understanding. Its title is still listed, so the agent can
includeit. Pin rules that must always apply. - An unrelated rule is sent. Topic words can pull in a loosely related rule: in the test above, a task about renaming a variable matched "Name test files after the module they cover" through the naming topic. It costs a few tokens, not correctness.
- The agent skips the call. The agent calls
get_rulesbecause its instructions say to; a model can skip it. Look for the tool call in the chat. - Context was reset. The agent must pass
refresh=true, otherwise Lore believes it still has the rules. The instructions tell it to.
Tuning and checking
# exactly what the agent would be sent for these files and this task (never counts as "served")
python3 review.py --name myapp resolve src/api/orders.py --task "add pagination"
# which approved rules the agent actually receives; unused first
python3 review.py --name myapp usage
| Constant | Value | Where | Meaning |
|---|---|---|---|
SMALL_SET | 10 | core/resolver.py | Up to this many eligible rules are all sent. |
RELATIVE_CUTOFF | 0.3 | core/resolver.py | A rule must score at least this share of the best match. |
MAX_INDEX | 25 | mcpserver/tools.py | Most titles listed in one reply. |
MAX_INCLUDE | 30 | mcpserver/tools.py | Most ids honoured in include. |
K1, B | 1.5, 0.75 | core/search.py | BM25 parameters. |