Triggers are in a file, content is in the graph
domains.toml holds triggers only: the keywords that match against prompts and the paths that match against files. The rules, decisions and notes attached to a domain live in the graph.
Two commands rather than one, for a reason. Triggers are policy you write and read as text, so they belong in a file you can put under version control and copy to another machine. Content accumulates as you work, gets edges to projects and entities, and needs to be queryable. Keeping them apart means you can retune what fires without touching what is said, and add what is said without editing config.
How a match is decided
Nothing is fuzzy and nothing is semantic. A trigger either matches what you sent or it does not. Since 0.16.0 base also scores each rule against your prompt (BM25: shared words count, rare words count more), but the score only orders what already matched. A block lists its rules best first, so when the budget is tight the weakest are held back first.[match] bm25 = false turns the ranking off.
Keyword triggers match whole words. A domain triggered on invoice does not fire on billing, and since v0.14.1 a domain triggered on base no longer fires on database. Before that release keywords matched as bare substrings, so a short trigger word fired constantly on longer words that merely contained it.
Pick trigger words the way you would pick a grep pattern, then check.
How a path trigger decides
A path trigger matches against the files this session has touched through the tool hooks, plus the session’s current directory, when you send a prompt; and against the file a tool is about to touch, when that tool runs. Nothing else. That is narrower than it used to be, on purpose. v0.14.0 matched against every path the store had ever seen active, plus every registered project, task and handoff, on every prompt. A trigger namedDocuments therefore fired on prompt 3 of every session whatever you were doing.
The comparison is a prefix match on whole path components.
Matching is case-blind whenever either side is a Windows path, meaning one carrying a drive letter. So
Tools and tools are one directory there, and /mnt/c/src and C:/src are read as the same place.
How a trigger gets rooted depends on how you wrote it. A relative path resolves against the tier its file came from: your home for the global domains.toml, the workspace root for the workspace one. A leading ~ resolves against your home. An absolute path is taken as written. A glob cannot be rooted at all and never fires. Since 0.16.0, base domain add-trigger stores the full path, and the 0.16.0 update rewrote relative triggers as the full paths they already meant.
Seeing which file did it. base log matches lists every prompt and file touch with the domains it matched and the keyword or folder that matched each one. With devmode on, the LOADED DOMAINS line names the file as well.
The file you touch decides
Since 0.16.0 you do not need a path trigger for a project’s own rules. When a tool reads, edits or writes a file, or a shell command names one, base finds the deepest registered project folder holding that file and loads that project’s domain: the domain named after the project. The file’s own project wins, so the rules of a sibling project, or of a project above it, stay out. Three more things can come with the file:- A path trigger of another domain still fires on it, unless a registered project folder lies between the trigger and the file. That is how a trigger on
~/workstops reaching into the projects inside~/work, while a trigger on a file, or on a folder inside the project, still fires. - A rule scoped to a path fires only when that file, or something under that folder, is touched:
base rule add --path <file or folder>. - The parent’s rules, when the project sits inside another and is marked
nested(base project update <slug> --parent <slug> --nested true). They come after the project’s own rules, so a tight budget drops the parent’s first.
auto_inject = false keeps a domain out here too.
Broad path triggers
A broad path trigger is one on a folder that holds other registered projects, such as~/work when ~/work/portal and ~/work/billing are projects of their own. It cannot say which project you are in.
From v0.14.1 to 0.15.2 base made such a trigger inert: it fired in neither hook, and the domain that relied on it went quiet. Since 0.16.0 it loads again, for files in that folder that sit outside those projects. It never reaches into a project folder below it, because the file’s own project decides there.
base doctor lists each broad trigger as advice, with the projects it holds and the command to narrow it, and still reports HEALTHY. base domain paths --suggest proposes an exact path for every domain (a project’s domain gets its project’s folder), and base domain paths --apply <file> writes the list once you have checked it. base domain add-trigger refuses to create a broad trigger, names the projects it holds and suggests the project’s own folder. The 0.16.0 update named each broad trigger you had, once.
A trigger that cannot be rooted (a glob, or a relative path in a file with no tier root) still never fires, and that one does count against the verdict. With devmode on, the prompt hook names it on every prompt.
Keeping a domain out of automatic injection
Some domains hold terms that must never surface in an unrelated prompt: a live negotiation, a client’s numbers, anything you would not want arriving because a keyword happened to collide.Exclude vetoes everything, always-on included
Anexclude pattern is checked first, for every domain, and a match vetoes that domain whatever its mode. An always-on domain is vetoed the same as any other.
That changed in v0.14.1. Before it, an always-on domain matched before the exclude check ran, so exclude on an always-on domain was dead configuration that looked live.
An exclude entry matches as a case-insensitive substring of the prompt. That is deliberately looser than a keyword’s whole-word match. A veto that fires slightly too often errs toward silence, which is the safe direction for a veto to err in.
Check before you rely on it
base context runs the same matching that runs during a turn, without sending anything. Paste in the prompt you actually mean to send. Either a domain matched or it did not, and you find out in a second rather than after a session where nothing arrived.
base domain get <name> shows one domain’s full configuration, so you can confirm a trigger landed where you meant it to.
Three sources, merged by name
Domains come from three places, loaded in this order:- The global
~/.base-gbl/domains.toml, which applies everywhere. - The workspace’s own
.base/domains.toml, which overlays the global set by name. - Any installed extensions, which merge in at the lowest priority.
create, add-trigger, remove-trigger, remove) all act on the domains.toml of the workspace you are in, or on the global one with --global. list and get read both tiers merged, so check base domain get <name> after a write to see the domain as base will use it.
Rules carry their reasoning
--rationale is injected as the rule followed by its reason. Write the reason. A bare instruction gets argued with when it collides with something the agent thinks is more sensible; an instruction with a reason attached usually survives the collision.
Next
Tiers and scoping
Global and workspace, how they merge, and what stays out of the other project.
Context reference
Every domain, rule, standard and star-command command, with its flags.