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

  1. 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.
  2. Short lists are sent whole With 10 or fewer eligible rules, or when no task was 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.
  3. 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 marked always, was named in include, or scores above zero and at least 30% of the best match.
  4. Order Always-rules first, then by relevance, then most specific scope, then oldest. Without a task: always-rules, then most specific scope, then oldest.
  5. No repeats A rule already sent in this session is not sent again; the reply counts them instead. refresh=true sends them again, which the agent uses after its context was reset.
  6. 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_rules again with its id in include, or uses search_rules.
  7. 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_rules the words of the rule's scope are included too, so a rule scoped to db is 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.py and user-repo all yield user and repo.
  • 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 .py still means the language.
  • A few aliases (py → python, db → database, repo → repository, spec → test).
  • A rough stem, so validate, validation and validating meet, and sanitize meets sanitization.
  • 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_rules accepts title, triggers and always with each rule.
  • Afterwards, for existing rules: organize_rules lists approved rules that have none, and tag_rules saves 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.

Why an agent and not a script? A script cannot tell that "indent with four spaces" matters when the task says "add a validation check", or that "column" and "migration" are the same area of work. The agent can read both. Doing that once per rule, instead of once per request, keeps requests fast and free.

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:

CallRules sentTokens
Everything, no task (what a flat instruction file costs on every edit)25367
1. add pagination to the orders endpoint4260 (includes the title list)
2. add a nickname column to the users table4135
3. call the Stripe SDK from the payment service3121
4. fix the failing test for the checkout service5147
5. refund a payment and log it2101
Total, five edits764 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 include it. 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_rules because 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
ConstantValueWhereMeaning
SMALL_SET10core/resolver.pyUp to this many eligible rules are all sent.
RELATIVE_CUTOFF0.3core/resolver.pyA rule must score at least this share of the best match.
MAX_INDEX25mcpserver/tools.pyMost titles listed in one reply.
MAX_INCLUDE30mcpserver/tools.pyMost ids honoured in include.
K1, B1.5, 0.75core/search.pyBM25 parameters.