Tags & scopes
Scopes partition lore by location (global, repo, branch). Tags are free-form labels that cut across scopes — use them to identify memory type, source agent, workflow, or any dimension you care about.
Scope reference
| Type | Format | When to use |
|---|---|---|
global | global | Universal principles — always apply |
project | project::{name} | Memories shared across a monorepo |
repo | repo::{owner}/{repo} | Memories about this repo's codebase |
branch | branch::{owner}/{repo}::{branch} | Experimental learnings on a feature branch |
:: is the only valid separator. Single : or / returns a 400 error. The MCP tools lowercase every segment on ingest; the REST write path (POST /memories) stores the scope exactly as you send it, which is why a filter matches the stored string rather than a lowercased copy of it.
The same grammar applies when you filter by a scope, not only when you write one. GET /memories, GET /memories/activity, GET /memories/facets, GET /memories/pivot, GET /memories/read-activity, DELETE /memories?scope=…&key=… and POST /memories/restore all return a 400 for an ungrammatical ?scope= — and so do the body-transport forms POST /memories/list, POST /memories/activity, POST /memories/facets and POST /memories/pivot, which read the same scope out of the JSON body. They used to pass the value straight into the query, so a typo matched nothing and came back as an empty page — or, on delete and restore, as "no such memory". A scope filter is the question you are asking, so a malformed one is rejected instead of quietly answering a different question. On those routes a filter with leading or trailing whitespace is a 400 too, since the padded form is compared literally and could never match anything. GET /memories/read-activity is the exception here as well: it normalises before comparing, so it trims the padding and answers 200.
On the five /memories routes the filter is validated but not lowercased, so it matches exactly the scope string that was stored — the write path keeps your capitalisation, so the filter has to as well. GET /memories/read-activity is the exception: it filters recorded read events, whose scope was already lowercased when the event was recorded, so it lowercases your filter to match. Same grammar everywhere; the case rule follows whatever wrote the value.
Choose the right scope
Use the narrowest scope that correctly describes where the memory applies:
// Universal: always apply to every agent everywhere
scope: "global"
// Repo-level: applies to this codebase
scope: "repo::mthines/my-app"
// Branch-level: experimental, won't pollute the repo set
scope: "branch::mthines/my-app::feat/new-auth"
// Project-level: shared across a monorepo
scope: "project::my-monorepo"Add tags when writing
Tags are arbitrary strings. Use a namespace::value convention to keep them readable:
memory.write {
scope: "global",
key: "aw-lessons::worktree-naming",
value: "Always use the branch name as the worktree directory name.",
tags: ["skill::aw", "source::stuck-loop", "loop::aw-lessons"],
source_agent: "aw-executor",
trigger: "stuck-loop"
}Common tag namespaces used in the ecosystem:
skill::aw/skill::fix-bug— which skill owns the memory (see the autonomous-workflow skill in mthines/agent-skills)loop::aw-lessons— part of the AW self-improvement loopsource::stuck-loop/source::pr-webhook— what triggered the write
Filter by tag when listing
Pass tags to memory.list to narrow results:
memory.list {
scope: "global",
tags: ["loop::aw-lessons"],
limit: 50
}Filter the Explorer by more than tags
The Explorer filters on nine dimensions, not just tags. Press the Filter button, pick a dimension, then pick values:
| Dimension | Comes from | Example |
|---|---|---|
| Label | tags | loop::aw-lessons |
| Kind | kind | lesson (also bus, signal) |
| Host | host | reviewer |
| Owner | org_id | Personal (or an org you belong to) |
| Agent | source_agent | aw-executor |
| Trigger | trigger | stuck-loop |
| Repository | origin_repo | mthines/lorekit |
| Branch | origin_branch | feat/new-auth |
| Pull request | origin_pr | 311 |
Owner partitions lore by who it belongs to — your own Personal lore, or one of the organizations you are a member of (shown by its slug). It filters server-side like every other dimension, so the list, the filter-menu counts and the stats header all agree.
Values inside one dimension combine with OR; dimensions combine with AND. So "Agent is either of aw, claude" and "Branch is main" is one filter set, and every filter is in the URL — copy the address to share the exact view.
Two shortcuts worth knowing:
- Type the value, not the dimension. With the menu open, typing
mainsurfacesBranch → maindirectly, so you never have to choose "Branch" first. Spacepicks,Enterfinishes.Spacetoggles a value and keeps the menu open for the next one;Entertoggles and closes.←/Backspacego back a level,Escapeclears the search box before it closes.
The last three dimensions come from provenance — the repo, branch and PR a memory was recorded from — which agents record automatically when they write from a git checkout. They are distinct from scope, which says where a memory applies.
Read the stats header above the list
The Explorer opens with four cards above the memory list, describing the scope, filters, and time range you have selected rather than your whole account:
| Card | Counts |
|---|---|
| Memories written | New memories in the range, under the selected scope and filters |
| Memories read | Memory records read — one list call returning 20 memories counts as 20 |
| Scopes active | Distinct scopes with at least one memory written in the range, under your filters |
| Memories expired | Records deleted because their TTL ran out |
A Stat charts / Heatmap switch in the panel header chooses which of the two you get — one or the other, never both stacked:
- Stat charts shows the four cards and unfolds each one's evidence: a period-over-period trend chip and a bar per hour or day. Every card with bars is additive — summing the bars reproduces the number above them, so the chart and the total can never tell different stories. A very large change is abbreviated to keep the badge on one line (
+8.8K%); hovering it shows the exact percentage. - Heatmap gives the write calendar the panel to itself — a quarter of activity on a phone, a full year on a desktop, sized to the width it has. The cards step aside rather than stacking above it, so the calendar is all you scroll past. It is deliberately account-wide and unfiltered, because you use it to pick a window rather than to read the one you already picked. Hover or tab to a day for its exact count.
The panel opens expanded, and the chevron folds it away to just the four numbers — from either view, so the figures are always one chevron away and folded is a compact summary line rather than a screen of tiles. Your choice is remembered — collapse it once and it stays collapsed on every visit, in every tab, until you open it again; the same goes for which of the two views you were last on. It is a preference for you and this browser, not part of the page's address, so a /lore link you share carries what you were looking at and not how tall you left the panel. When a number changes because you picked a different scope, range, or filter, it counts to its new value rather than swapping, so you can see which figures moved.
The time range picker sits in the same header and offers 24h / 7d / 30d plus All.
Before you touch it, it describes the last 24 hours — recent activity is what an activity panel is for, and an all-time total has no preceding period to compare against. That opening view applies to the panel only: the list below still shows all time, so nothing is hidden by default and every /lore link you have already shared still opens on everything.
Once you do pick a range it drives the whole page — the cards, the heatmap highlight, and the list below — and an empty list caused by the window says so and offers a one-click way back to all time.
Memories written and Scopes active follow your full selection — scope, time range, and every filter in the bar — so they always agree with the list beneath them. The other two cards can only go part-way, each stated in its own tooltip:
- Memories read follows the scope and range, but not the filter bar. A read is a query, not a per-memory event, so a label or repo filter cannot narrow it — the card stays scope-level. It also counts only your own reads, and only those LoreKit could attribute to a scope, so per-scope totals can be smaller than the account total. Reads from this dashboard are excluded: browsing your lore is visualisation, not consumption.
- Memories expired is account-wide even with a scope selected. Expiry is recorded when the nightly purge removes a row, and that purge runs per user across every scope, so there is no scope on the event to filter by.
Search across scopes with wildcards
memory.search supports owner-level wildcards in the scopes parameter:
memory.search {
q: "worktree naming conflict",
scopes: ["repo::mthines/*", "global"],
tags: ["skill::aw"],
limit: 10
}Wildcards only work in memory.search — not in memory.read, memory.list, or memory.delete.
Read narrow-to-broad before a task
Agents should read from specific scopes first, then merge with broader ones. More-specific scopes win when the same key exists at multiple levels:
// Read order for a task on branch feat/x in mthines/gw-tools
memory.list { scope: "branch::mthines/gw-tools::feat/x" }
memory.list { scope: "repo::mthines/gw-tools" }
memory.list { scope: "project::gw-tools" } // monorepo only
memory.list { scope: "global" }If you only have a key and no scope, memory.read { key } does that resolution
for you: it matches across every scope you can see and returns the most specific
hit, naming the winning scope in the response's scope field (and any it
shadowed in other_scopes). Omitting scope means everywhere, not
global.
Inspect with the CLI
The CLI offers several commands that respect the same scope/tag logic. You can also browse memories visually in the Explorer.
# Human-readable view of all applicable memories
npx @lorekit/cli list
# Full-text search
npx @lorekit/cli search "worktree"
# Inspect one memory in full
npx @lorekit/cli show --scope global --key aw-lessons::worktree-naming
# Scope precedence tree (which memory wins per key)
npx @lorekit/cli tree
# Flag low-quality or malformed memories
npx @lorekit/cli lint
# Find near-duplicates (Jaccard similarity)
npx @lorekit/cli dedupeNext: see how tags, scopes, and org sharing come together in real-world patterns. See the Use cases tutorial.