25 Domains
This chapter is the lookup for everything you can set on a domain and every rule you run into while doing it: reach for it when a prompt to the agent isn’t enough (a script, a container or the question of why a command refused). The prose chapters keep the why (domains in Section 4.1, private domains in Chapter 16, team domains and review mode in Chapter 17, membership in Chapter 19, provisioning in Chapter 21). The MANIFEST’s own keys are in Chapter 24 and every other setting is in Chapter 23.
25.1 Kinds of domain
Every domain is one of three kinds, and the same tools read and write all of them. Whether a domain lives in files or in the database is fixed when you register it, and a file domain turns into a team domain once you connect an origin to it.
- file domain
-
A folder of markdown files with a
MANIFEST.mdat its root, the default kind. The files are the source of truth and the index is derived from them, so a rebuild of the index loses nothing. - virtual domain
-
A domain that lives in the database with no folder on disk, for a container without a writable volume or a Postgres backend shared by several machines. Its engrams live only in the index, so removing it deletes them and
crystalline reindex --wiperefuses while one is registered. - team domain
-
A file domain with an origin: a GitHub repository, an optional subfolder in it and a branch. It needs
github.enabledand a GitHub connection, and it can’t be virtual, because a proposal needs files to carry.
25.2 The registration block
Each registered domain is one entry under domains: in the global config.yaml, keyed by its name, and the commands in this chapter write these entries for you. crystalline config set doesn’t reach them, so a key without a command is a hand edit.
domains:
books:
path: ~/Documents/Crystalline/books
provision: true
club:
path: ~/Documents/Crystalline/club
origin:
repo: dallas/book-club
path: knowledge
branch: main
poll_secs: 600
review: overlay
reading:
kind: virtualkind-
Either
fileorvirtual, and it only appears on a virtual domain, since a file domain leaves the key out. path-
The domain’s root folder, with a leading
~expanded; a file domain registered without one lands at<domains_root>/<name>, and a virtual domain has none. origin.repo-
The GitHub repository of a team domain, as
owner/name; its presence is what makes a file domain a team domain. origin.path- The subfolder of the repository that is the domain root, for a repository that holds more than knowledge (absent means the repository root).
origin.branch-
The branch the domain tracks. Adding a team domain without a branch writes the repository’s default branch here, asked from GitHub; an entry without the key, from before that or written by hand, tracks
main. origin.poll_secs-
How often the daemon checks this origin for merged work, in seconds. Absent defers to the global
github.poll_secs(300 unless set), and any value below 60 is raised to 60. No command sets it, so it’s a hand edit. provision-
This machine’s decision on the artifacts the domain ships:
trueinstalls them into every onboarded harness,falsekeeps them out and removes what was installed. Absent means undecided, and nothing installs. review-
overlayputs the domain in review mode; absent means the domain takes changes directly, which is how every domain starts.
25.3 Names
A domain has a name on this machine and a name it declares. The local name is the key in the config and the name in Fluid’s addresses, and a new domain’s folder under the domains root takes it. The declared name is the MANIFEST’s domain_name (Chapter 24), and it’s the one links, crystalline:// addresses and the agent’s routing carry. Usually the two are the same. Where they differ, both reach the domain and a link written here is stored with the declared name, so it works on every machine. Choose the declared name once: a rename is there to repair a name that turned out wrong (Chapter 18).
- What a name may be
-
Letters and digits in any script, plus
-,_and., up to 64 characters, with no dot at the start or the end. The Windows device names (CON,PRN,AUX,NUL,COM1toCOM9,LPT1toLPT9) are refused on every platform. The command line,add_domain, Fluid and the API all run this check, and so dodomain init --nameand a rename, with the same message everywhere. A name registered before the check existed keeps working. - Names taken already
-
Adding a name that is already registered at the same folder, or as the same virtual domain, changes nothing and indexes it again, so every way of adding a domain is safe to repeat. The same name at another folder or as the other kind of domain is refused, and the message names what holds it. A name that differs from another domain’s here only in letter case (
engandEng) is refused too, whether you add, rename or connect, since a disk that ignores case would give both one state folder; a name taken from a folder or a MANIFEST steps aside instead. Every surface also refuses a name that an environment variable already defines, and the refusal names the variable, because that domain belongs to it. A declared name that another domain here already uses as its local name is taken too: the new domain is registered under it with-2added and reported as shadowed, and links that use the declared name keep reaching the domain that holds it. - Names given for you
-
A domain added without a name takes the
domain_nameits MANIFEST declares, then the folder’s or the repository’s name, with a suffix when that one is taken; a name made this way is always turned into a valid one (a folder calledCONbecomescon-2).add_domain,domain addand Fluid all pick it this way, and when you connect a repository Fluid shows the name it will take in the empty Name field before you create the domain. - Former names
-
A rename keeps every former name working on this machine as an alias: a link or an address that uses it still reaches the domain, and an old
/d/<old>/address in Fluid sends you to the new one. An alias gives way to any domain whose local or declared name it is. - When the declared name changes
-
A machine follows a changed
domain_nameon its next sync when its local name was only ever a default, and keeps the old one as an alias. A local name somebody chose on purpose stays, and the declared name reaches the domain there as well. A shadowed domain takes its declared name on the first sync after the domain that held it gives it up, unless its local name was chosen on purpose.
25.4 Registering a domain
There are five ways in: the agent’s tool, the command line, Fluid, the JSON API behind it and environment variables. Each registers the domain, and all but --no-sync index it at once.
add_domain-
The MCP tool, with
domain,folder,virtual,repo,path,branchandreaders. The arguments pick the kind:repomakes a team domain,virtual: truea virtual one (it needsdomain), anything else a file domain, andrepoandvirtualcan’t be combined. On a server with a GitHub App,reponeedsreadersas well,sharedorprivate. add_domainon a folder-
A folder without a
MANIFEST.mdgets a starter one, and the files already there are kept and adopted in place. Withoutfolderthe domain lands at<domains_root>/<domain>. - Who may call
add_domain - Any local session may. Over HTTP, on an instance where agents sign in, it takes the instance admin role, and a read-only instance refuses it.
crystalline domain add [name] [path]-
Registers a domain and indexes it at once, in
<domains_root>/<name>when no path is given. A lone argument is always the name; to let the folder name the domain, point at it with--pathor as the second argument. A file domain needs aMANIFEST.mdin its folder already and refuses with thedomain initline otherwise. --virtual-
Registers a virtual domain and scaffolds its MANIFEST into the index; it takes no path and can’t be combined with
--origin. --origin owner/repo[/subfolder]-
Connects a team domain and downloads the repository, or the subfolder given after its name, into the domain’s folder; it needs
github.enabledand a GitHub connection, a priorcrystalline connect githubor a GitHub App on the server. A folder that already holds knowledge, or a registered domain with no origin yet, is connected in place: nothing local is overwritten, and files that differ from the repository become local changes to share. --branch-
The branch to track, and it only works with
--origin. Left out, Crystalline asks GitHub for the repository’s default branch and records it; when GitHub can’t answer, the add refuses and asks for--branch. --readers shared|private-
Who may read a team domain on a server with a GitHub App:
sharedis everyone signed in,privateonly the people with access to the repository on GitHub (Chapter 19). On a terminal the command asks when the flag is missing, and where nobody can be asked it refuses, so nothing is chosen for you. A shared connect never opens a domain that is already private. It only works with--origin, and a server without an App ignores it with a note. --no-sync-
Registers a file domain without indexing it, for a script that runs
crystalline synclater; a team domain is always indexed as it connects, so the flag refuses with--origin. --privateand--owner <account>-
Registers the domain private from its first minute, owned by the named account.
--privateneeds--owner, and before anything is registered the account has to exist and be enabled. A domain that is already registered is refused here;crystalline domain visibilitymakes that one private. crystalline domain init <path>-
Creates the folder if needed and writes a starter
MANIFEST.mdwith the Scope, When to Use and Notes for Agents sections, then prints thedomain addline to run, or says a MANIFEST is already there and leaves it alone.--namesets the name in the manifest, the folder’s name otherwise. It never touches the config. crystalline domain list-
Lists the registered domains, with engram counts when the index is there. A domain whose declared name differs from its local name shows both, as
local (declared), with its former names beside it and a mark when another domain holds its declared name.list_domainsand the JSON API’s domain list report the same three things. - Fluid’s New domain dialog
- Only admins get the dialog. Its Kind is Local folder, Virtual or GitHub team, and the fields follow it: a Name for the first two; a Repository, Branch, Folder in the repository and an optional Name for a team domain. A local domain always lands under the server’s domains root, since a browser can’t name a folder on the server, and the Private box makes the domain private with you as its owner. On a server with a GitHub App a team domain also asks “Who may read this domain?”, with no answer chosen in advance.
POST /api/v1/domains-
The request behind the dialog, for a script, with
mode(local,virtualorgithub),name,repo,branch,path,privateandreadersas its fields. A field that doesn’t belong to the mode is refused rather than ignored, and a team domain on an instance with no GitHub connection is refused with where to make one. CRYSTALLINE_DOMAIN_<NAME>=<path>-
Registers a file domain for as long as the variable is set, for a container that shouldn’t carry a config file. The domain’s name is
<NAME>in lower case with_turned into-, and the variable replaces aconfig.yamlentry of that name, with a warning at startup, without ever being written into the file. CRYSTALLINE_DOMAIN_<NAME>_ORIGIN-
Attaches an origin to that domain, as
owner/repo[/subfolder][@branch], so a new node downloads the team domain on its first start; withCRYSTALLINE_GITHUB_ENABLEDoff the domain waits and says so, and without the matchingCRYSTALLINE_DOMAIN_<NAME>the daemon refuses to start. Because the suffix is read first, no variable can define a domain whose name ends in-origin,-reviewor-readers, so register such a domain in the file, and look in Chapter 23 for the GitHub token variable that goes with the origin. CRYSTALLINE_DOMAIN_<NAME>_READERS-
sharedorprivate, who may read that team domain on a server with a GitHub App. Without the variable the domain is private, with one warning at startup. It’s applied on every start and only ever closes:sharednever opens a domain that is already private. It needs the_ORIGINvariable beside it, and a server without an App ignores it. A domain whose readers come from here changes them only here.
25.5 Renaming a domain
A rename changes the domain’s local name and, unless you keep it to this machine, its declared name too: it writes domain_name into the MANIFEST and rewrites every link and crystalline:// address that uses an old name, in every domain you can write. A MANIFEST whose frontmatter is in flow style ({title: ...}) is left as it is, and the rename’s report and, in Fluid, the domain page say to set domain_name by hand. A domain you can only read keeps its links as they are, and the rename lists it. In a team domain the rewrites become local changes to share, and in review mode they land in your draft. No MCP tool renames a domain, so an agent asked for one points you to the entries below.
crystalline domain rename <domain> <new>-
Renames the domain, named by its local name, its declared name or any alias. It works on this machine’s own index and configuration only, and it refuses before anything moves when
--dbor--configpoints elsewhere. --local- Renames only this machine’s records: the index, the state folders, the provisioning receipt and the config entry move, and the MANIFEST and every link stay as they are. It’s the way out when the MANIFEST can’t be written here, which the refusal names.
POST /api/v1/domains/{domain}/rename-
The same for a script, with
nameandlocal_onlyas its fields. The answer says what moved, which engrams were rewritten, which domains were left behind and which aliases the domain answers to now. - Rename domain
-
An item in the More actions menu on the domain’s page in Fluid, drawn for admins and for a private domain’s owner, and the Change name button in Domain policies, in the domain’s settings, opens the same dialog. Its “This machine only” box is
--local. While a rename runs the page says Renaming… and holds back every write, and afterwards it says which name the domain was renamed from, along with thedomain_namenote when the MANIFEST was left as it was. When another domain here holds the declared name, a banner at the top of the page says so, with a Rename button beside it. - Who may rename a domain
- An instance admin, a private domain’s owner or the machine operator from the command line.
- A rename that stops halfway
-
A crash or a killed process leaves a journal in the state folder, and the daemon, or the next command, finishes the rename. When it can’t,
crystalline doctorshows the rename under itsrename:heading with the steps done, the steps left and the command that finishes it.crystalline doctor --discard-renamedrops the journal and leaves the steps already run as they are, for you to finish or undo by hand;doctor --fixnever drops one. - Names in
crystalline doctor -
Under its
domain names:heading doctor lists every collision: a domain whose declared name another domain here holds, two domains that declare the same name (so links that use it reach neither) and two domains whose names differ only in letter case, with the rename that ends it. It names each team domain without adomain_nameand asks you to have its owner add one. It also lists the links in your own domains that use a name only this machine knows, a former name that no longer reaches the domain because another domain answers to it and a domain still waiting to take its declared name, with the reason.doctor --fixrewrites those links the ordinary way, as a change to share in a team domain and a draft in review mode, and clears an index entry an older version left behind when a domain was removed.
25.6 Moving knowledge in and out
These commands copy engrams between a domain and a folder or an archive, and none of them changes the domain’s registration. A single engram moves from one registered domain to another with move_engram and its destination_domain. The links to it are rewritten in the domains you may write, the moved engram’s own links keep finding what they found, and the receipt names every link it couldn’t fix, so somebody who may write there can.
crystalline domain import <path> --domain <name>-
Loads engram files into a registered virtual domain. A file that already has every field a reader needs is stored as it is, and one that lacks some is completed the way
crystalline importcompletes it: a missing status becomesstableand a file without tags gets the tagimported. The pictures and documents the notes link to are stored as attachments underassets/, and the report lists every link it couldn’t take. A file domain is refused: for a folder of notes that still needs converting,crystalline importwrites into the domain’s folder andcrystalline syncindexes it. --overwriteand--dry-runondomain import-
An engram whose path or permalink already exists is skipped unless
--overwritesays otherwise, and--dry-runreports what would land and writes nothing. crystalline domain export <path> --domain <name>-
Writes any domain’s engrams out as an ordinary markdown folder, which is how a virtual domain’s content gets checked with
crystalline verifyor turned back into files.--forcewrites into a folder that isn’t empty, and--dry-runonly reports. - Fluid’s Backup section
- The section sits in the domain’s settings, above the Danger zone, and is drawn for admins; the domain page’s More actions menu has Download archive and Import archive too. Download archive saves a zip of the whole domain, MANIFEST and attachments included, and works on a read-only instance too. An archive from a file domain restores into a virtual one and the other way round.
- Import archive
-
Uploads such a zip into the domain, shows a preview of every entry first and imports only after you confirm. Skip existing, the default, leaves a path that’s already there alone, and Overwrite existing replaces it. A MANIFEST in the archive is never imported, a file whose name Windows can’t hold (like
CON.mdor a name that ends in a dot) is skipped on every system and named in the report, and a broken or oversized archive is refused as a whole.
25.7 Read-only domains
A domain can be frozen with read_only: on in its MANIFEST (Chapter 24), for a domain that is finished and should stay as it is. It’s a switch on one domain, where service.read_only makes a whole instance read-only. It guards what goes through Crystalline against accidents and is no sandbox: an agent with a shell can still edit the MANIFEST on disk.
- What it refuses
-
Every write into the domain through Crystalline, from an agent over MCP, a person in Fluid, the command line or the REST API alike: writing, editing (frontmatter changes and evolve acknowledgements included), moving or splitting an engram into or out of the domain, deleting, attachments, review-mode drafts, co-editing saves, imports, switching review mode, a
configurechange of its rule overrides or of a policy other thanread_onlyand renaming the domain. A rename for this machine only (--local) still works, because it writes nothing into the domain. Sharing changes is refused too, since nothing local can change, and so is withdrawing a proposal. The refusal names the domain and says that its owner switchesread_onlyoff in the domain policies. - What keeps working
- Team pulls keep the local copy current, because the team’s repository stays the authority. Discarding local changes and resolving a conflict with the team’s side work too, since both go the way a pull goes, though an agent may not do either to the MANIFEST while the switch is on; a person does that in Fluid or over the REST API. The index, sync and search work as before. On a team domain a switch turned on here stays on this machine, because sharing is refused, so switch it on in the team’s repository when the whole team should follow; when the team switches it off there, the next pull opens the domain here too.
- How it shows
-
list_domains,browse_domainandread_engramanswerread_only: true, and the domain’s line in the briefing says “(read-only)”, so an agent doesn’t plan a write there. The same mark appears on a domain you may read but not write, as a viewer or with a write list that leaves it out (Chapter 16), and the refusal says which reason applies. A domain from a connected server carries the server’s mark. Fluid shows a lock on a locked domain and on every engram page in it, and wherever you can’t write it offers no edit, new, move or delete. The agent’s tool list doesn’t change; the write tools refuse when they’re called. - The sweep
- Evolve and the write nudges skip a read-only domain, because nobody could act on its findings, and the findings come back once the switch is off.
25.8 Removing a domain
Removing a domain unregisters it and clears its rows from the index. A file or team domain’s files stay on disk and the team’s repository is never touched: the only knowledge a removal can delete is a virtual domain’s, and that takes an explicit confirmation.
crystalline domain remove <name>- Unregisters the domain and frees its name for a rename or a new registration. Registering the same folder again adopts the files as they are, and a team domain is connected again with its repository, which keeps its origin. The visibility and membership records go too, so a later domain under the same name starts out shared.
--purge- Confirms that a virtual domain’s engrams are deleted with it, and nothing brings them back afterwards; a virtual domain that holds any refuses without it, so export first when you want a copy.
--end-drafts <actor>- Names one other person whose private drafts in a review-mode domain end with the removal; repeat it for each. The refusal says who is drafting and how many drafts each holds, and your own drafts need no naming.
remove_domain-
The MCP tool, with
domain,purgeandend_draftsworking as the flags do. On a harness that can ask, the first call removes nothing and puts a confirmation question to you naming the domain, its kind, its engram count and the drafts at stake. The question says in one line what happens, and its box starts checked, so Accept removes the domain and Decline keeps everything. - Who may remove a domain
- The machine operator, an instance admin or, for a private domain, its owner. An agent on an instance that leaves agent authentication off is refused. A domain defined by an environment variable can’t be removed at all: unset the variable instead.
- Unregister domain
- The button in the Danger zone of the domain’s settings, drawn for admins and for a private domain’s owner. It asks you to type the domain’s name and says whether the files stay on disk or, for a virtual domain, are deleted.
25.10 Visibility and membership
Every domain starts shared, visible to every account on the instance. A private domain is visible only to its owner, the instance’s admins and the accounts invited into it, and to everyone else it answers as if it had never been registered (Chapter 16).
crystalline domain visibility <domain> private --owner <account>-
Makes the domain private and hands it to that account, which has to exist and be enabled. Making a domain that is already private private again changes nothing, its owner included. On a domain under a GitHub App
--owneris ignored with a note and no account becomes its owner, and a domain whose readers come fromCRYSTALLINE_DOMAIN_<NAME>_READERSis refused with the variable’s name. crystalline domain visibility <domain> default- Shares the domain with every account again and forgets its list of members, so making it private later starts from the owner alone.
- Make private and Share with everyone
- The same switch in the Danger zone of the domain’s settings in Fluid, confirmed by typing the domain’s name. Making a domain private is for admins, and the admin who does it becomes the owner; sharing it again is for the owner or an admin. On a domain under a GitHub App the switch sits on the Members section instead, for a GitHub admin or maintainer or an instance admin, and no account becomes its owner.
none-
The membership level that sees no engram in the domain. A member at
noneworks on the domain’s Reflection alone and needs a Reflection level, or the membership is refused. viewer- The membership level that reads and searches the domain and nothing more. It’s the level an invitation takes unless you name another.
editor- A viewer who also writes and edits engrams in the domain, as far as the account’s instance role allows.
manager- An editor who also runs the membership: inviting, removing and changing levels up to manager. A manager can’t change visibility, transfer the domain or remove it.
- Reflection level
-
An account carries a Reflection level for the shared domains and a membership carries one for its private domain, beside the level on the engrams, which has
nonefor somebody who sees no engram at all. It’sdefault,readerorcontributor. An admin sets it on the account, and the owner or a manager on the membership. Withdefault,viewergivesreaderandnonegives nothing. A level that is set only ever raises access (Chapter 12). reader- The Reflection level that reads the domain’s reflections and comments on them: opening, answering, resolving and reopening threads.
contributor- A reader who also edits reflections, creates, moves and deletes them and adds files to them.
- curator
-
A curator also writes sections from the engrams, settles what people changed, edits the pact and replaces, moves and removes files. Curating is never set on its own: everyone who may edit the engrams curates (
editor,manager, the owner, an admin and the machine operator), and Fluid shows them as curators. crystalline domain members <domain> list- Prints the owner and every member with their level, though a shared domain has no owner and no members, because membership only decides anything while a domain is private. The Reflection collaborators follow, on a shared domain too.
crystalline domain members <domain> add <user>-
Invites an account, or moves a member to another level, with
--level none,viewer,editorormanagerand--reflectionfor the Reflection level; the owner can’t be added, because the owner already holds every level. On a domain under a GitHub App it refuses, likeremoveandtransfer, with “access to this domain follows GitHub; change it on GitHub”. crystalline domain members <domain> remove <user>- Ends a membership, though the owner can’t be removed this way; hand the domain on first.
crystalline domain members <domain> reflection add <user> --level reader|contributor-
Makes an account a Reflection collaborator of this one domain, shared or private, as long as it’s kept as files. The grant only raises: it never shows an engram the account couldn’t see before, and the write list still caps it. It also works for an account with the instance role
none. The owner, a manager, an instance admin and, under a GitHub App, a GitHub admin or maintainer give it, and it’s the one grant a domain under a GitHub App still takes. crystalline domain members <domain> reflection remove <user>- Takes the grant away again.
- The Members section
- The same list in Fluid, in the domain’s settings: invite by username, level and Reflection level, change a level, remove, and a Leave button for a member’s own membership, which needs nobody’s permission. Reflection collaborators have a list of their own there. On a domain under a GitHub App the section says that access follows GitHub and refuses a change of membership: people with write access see who has access on GitHub, and a GitHub admin or maintainer or an instance admin switches the domain between shared and private.
crystalline domain transfer <domain> <new_owner>- Hands a private domain to another existing, enabled account, which becomes the owner at once. The old owner keeps nothing and is a stranger to the domain unless invited back. It’s for the owner or an admin, and Fluid offers it in Members.
- A deleted owner
- Deleting the owner’s account leaves the domain with no owner and only an admin can administer it after that, so transfer it before the account goes.
- Instance role and membership level
- On a shared domain an account does what its instance role allows: a viewer reads, an editor writes, an admin does everything. On a private domain the membership decides what the account may reach, and a write takes the lower of level and role. So an instance viewer invited as an editor writes nothing, and neither does an owner whose instance role is viewer. An account’s write list (Chapter 16) narrows that further: a domain it leaves out is read-only for that account. All of this holds for domains without a GitHub App behind them; for a domain under a server’s GitHub App the second table below replaces the role and the membership.
| Action on a domain | Who may |
|---|---|
| Read and search | viewer or better |
| Write | editor or better, capped by role and by a write list |
| Invite, re-level, remove | manager, owner, admin |
| Leave | the member |
| Make private | admin |
| Share again, transfer | owner, admin |
| Review mode on or off | owner, admin |
| Change policies and rule overrides | owner, admin |
| Unregister | owner, admin |
| Register, provision | admin |
| Backup and archive import | admin |
On a server with its own GitHub App, a domain connected to a repository follows that repository (Chapter 19). The account’s GitHub link and its role on the repository decide, and members, invitations and the owner set in Crystalline don’t count:
| Who | Engrams, shared domain | Engrams, private domain | Reflection |
|---|---|---|---|
| Machine owner, instance admin | everything | everything | curate |
| GitHub admin or maintain | write, switch shared or private | the same | curate, manage the Reflection collaborators |
| GitHub write | write | write | curate |
| GitHub triage or read | read | read | read and comment |
| Instance viewer or above, without access on GitHub or without a link | read | nothing | shared domain: read and comment; private domain: nothing |
| Instance role none | nothing, whatever GitHub says | nothing, whatever GitHub says | only through a Reflection collaborator grant |
| Anonymous visitor, where the instance lets one in | read, only when the repository is public | nothing | read, never comment, only when the repository is public |
The instance role doesn’t cap a GitHub role, so an instance viewer with write access on GitHub writes, while the write list still applies. On a shared domain an account’s own Reflection level still raises what the row gives, so an instance contributor edits the Reflection there without access on GitHub; on a private domain it doesn’t count. A private domain counts only the repository’s listed collaborators, never the members of an Enterprise who read an internal repository without being listed. A closed domain, whose repository lost the App, was deleted or moved to another owner, opens to the instance admins and the machine owner alone, and no grant opens it. The open tier acts as the account github.agent_identity names, never above editor; without that setting it’s an anonymous visitor.
The command line and a local stdio session are the machine operator and pass every gate in this table. An agent over HTTP on an instance where agents sign in is its user and gets exactly that user’s row, with no tool for membership at all. Where agent authentication is off, an agent is nobody in particular: it reads and writes the shared domains, registers and provisions without a role, changes a domain’s policies other than read_only wherever it may write and is refused when it unregisters one. On a domain under a GitHub App it’s the account github.agent_identity names instead, private domains included, so turn agent authentication on there.
25.11 Review mode
Review mode gives every writer a private overlay of drafts over a team domain’s folder, and the folder changes only with what lands on the team’s branch (Chapter 17). It lives in the registration as review: overlay, never in the MANIFEST, and no MCP tool turns it on.
crystalline domain review <domain> overlay- Turns review mode on. It needs a GitHub origin and a folder, so a virtual domain is refused, and it refuses while the folder holds unshared changes, listing their paths to share or revert first.
crystalline domain review <domain> direct-
Turns review mode off, which ends every private draft in the domain. Run without answers it prints the plan (who holds drafts, how many and where two people drafted the same path), and then it needs one
--foldor--discardper person the plan names. --fold <actor>-
Writes that person’s drafts into the domain’s folder as they are (your own drafts from the command line or a local session belong to the actor
owner). --discard <actor>-
Ends that person’s drafts without touching the folder. Both flags repeat, one per person, and both refuse with
overlay. - Review mode in Fluid
- The same switch in the domain’s settings in Fluid. Turning it off shows the plan with a fold or discard choice per person, set to fold until you change it.
- Who may switch review mode
- The machine operator, an instance admin or a private domain’s owner; a domain an environment variable defines is refused, because its variable holds the setting.
CRYSTALLINE_DOMAIN_<NAME>_REVIEW=overlay-
Puts a domain defined by
CRYSTALLINE_DOMAIN_<NAME>in review mode, and any other value stops the daemon at startup, while an empty one counts as unset. The variable and a hand-editedreview:key are taken as written, without the origin check the command makes, so pair them with an origin. - One proposal at a time
-
A domain in review mode keeps one open proposal, even with
github.stackson. A second share while one is open is refused: somebody else’s proposal is named with the choice to wait for the merge or ask for a withdrawal, and your own is withdrawn and shared again.
25.13 Running the loops
A domain’s reflect loop and evolve loop run without a session through a runner (Chapter 14). <loop> below is reflect or evolve. status and watch take --domain once per domain, run takes one and prompt needs one; without it, status covers every domain the caller may run the loop on.
crystalline <loop> status-
What’s due per domain, as counts, with a run that’s going, the last run and the webhook’s state;
--jsonprints the answer as it came. It exits 0 when something is due, 1 when nothing is and 2 on an error. An item every run so far left for a person counts as “left for a person” instead of due, until it changes. crystalline <loop> run-
One beat: it takes the lease, starts Claude Code headless with the shipped prompt and only the Crystalline MCP server, shares the run’s changes, records the run and releases the lease. Without
--domainit works every due domain in turn. It exits 0 when the run is done or stopped at its cap, 1 when nothing was due or another run holds the lease and 2 when the run failed.claudehas to be on thePATH. crystalline <loop> watch-
Checks the status every
--intervalseconds (60 by default) and runs when something is due, until it’s stopped. It takes the flags ofrun. crystalline <loop> prompt-
Prints the shipped prompt for one domain, for a runner that starts its own agent, n8n for example;
--max-itemssets the cap the prompt names. Paste it again after an upgrade, since the prompt can change. - Flags of
runandwatch -
--max-items(20) is the most items one run works,--max-turns(60) the most agent turns,--modelthe agent’s model and--runnerthe label the lease, the run record and Fluid show (cliby default).--log-dirmoves the run logs fromruns/in the state folder; the newest 50 per loop and domain are kept, readable by you only.--no-sharekeeps the changes local.runalone takes--dry-run, which shows the command it would start and starts nothing, and--force, which runs while the loop backs off after failed runs. - Local or remote
-
Without anything set, the commands talk to the daemon on this machine and a run acts as the machine owner. With
CRYSTALLINE_REMOTE_URLandCRYSTALLINE_REMOTE_TOKEN(orCRYSTALLINE_REMOTE_TOKEN_FILE) they talk to that server as the token’s account, and the agent reaches the server’s MCP endpoint with the same token. - Who may run a loop
- The reflect loop needs the right to curate the domain’s Reflection, the evolve loop the right to write the domain. Give each loop its own account when both work one domain at once, since the server notes an account’s writes in every lease it holds there.
- Where a loop runs
-
The reflect loop runs on the domain’s Reflection home. The evolve loop on a team domain runs on its evolve home, the instance whose
service.public_urlthe MANIFEST’sevolve_homenames (on a shared database, the one of those instances that holds the domain’s host lock). Crystalline fillsevolve_homein: the first scheduled evolve run at an instance with aservice.public_urlclaims it, and its pull request carries the line. Until then every copy may run evolve, andcrystalline doctorwarns. A domain without a GitHub origin runs it on any instance with its own copy, and on a shared database on the instance that holds the host lock. An instance where the loop doesn’t run answersruns on <name>, is never due and refusesrun. No loop runs on a read-only instance or a read-only domain. - What a run shares
-
On a team domain with a GitHub origin the runner shares the run’s changes as a pull request, whatever
sharingsays, titled with the loop, the domain and the date, with the run’s summary as its description; a second run while that pull request is open adds to it. In remote mode only the account’s own writes through the MCP tools during the lease go in. A local run shares the files new or changed since it took the lease. Either way, a file a person had unshared work on when the run started is held back and counted, the changes of a stopped run stay local and Reflection pages are never part of it. A domain without an origin, or an instance with GitHub off, shares nothing. - The lease and the run record
-
One lease per domain and loop, renewed every 60 seconds and taken over by the next run once it hasn’t been renewed for five minutes, which records the old run as
expired. A run record keeps the start and end, the runner, the account, the outcome (done,stopped_at_cap,failed,nothing_dueorexpired), the counts, a short summary and the pull request; the index keeps the last 50 per domain and loop, so a full reindex loses them. After a failed run the loop waits 5 minutes before it’s due again, twice as long after each further failure in a row and at most 6 hours. - The last run in Fluid
- The Reflection home says “Last reflected” and the maintenance page “Last maintained”, each with the time, the runner and the outcome, and nothing when the loop never ran. A reader sees the line; someone who may run the loop also sees the summary, the pull request and the counts. A Reflection contributor without the right to the knowledge sees no “Last reflected” line.
25.14 The provisioning decision
A domain whose MANIFEST declares a Provisioning section ships skills, commands, subagent definitions and MCP server configs, and each machine decides per domain whether to take them (Chapter 21). The decision is the registration’s provision key.
crystalline provision allow <domain>-
Sets
provision: trueand reconciles, installing the domain’s artifacts into every harness this machine onboarded. crystalline provision deny <domain>-
Sets
provision: falseand reconciles, which removes whatever the domain shipped before. crystalline provision status-
Prints each domain’s decision, grouped allowed, undecided and denied and sorted by name, each harness’s installed state and the domains still waiting for one. The domains that declare no provisioning and the virtual ones fold into one line each;
--alllists every one of them. provision-
The MCP tool, with
actionset tostatus,allow,denyorapplyanddomainfor the two decisions. On an instance where agents sign in, everything butstatustakes the admin role.
25.15 Where domains live and which ones an agent hears about
Four more settings touch domains without belonging to one. Two sit in the global config and two are files named .crystalline.yaml, one in a workspace and one at a domain’s root.
domains_root- The folder a new file domain lands in when nobody names one; the setting is in Chapter 23.
prompt.rules- Narrows which domains the session-start briefing names for a workspace, by path pattern. The rule syntax and an example are in Chapter 23.
preferred_domains-
A list in a
.crystalline.yamlat a workspace’s root that puts those domains first in the briefing, in that order. It only reorders and never hides a domain. - Where the two routing hints apply
-
Both need a workspace path, so they shape the briefing the session-start hook prints with
crystalline prompt system, while an agent connecting over MCP gets the server’s briefing, which covers every domain it may see and ignores both. - The domain’s own
.crystalline.yaml -
A file at a file domain’s root whose
verify:block tunes the checker for that domain, with its keys and the codes they touch in Chapter 26; a virtual domain has no root, so it always takes the defaults.
A domain is a folder, a database-only virtual domain or a team domain with a GitHub origin, and each is one entry under domains: in config.yaml with path, origin, provision and review. You register one with add_domain, crystalline domain add, Fluid’s New domain dialog or an environment variable, and removing it leaves files on disk but deletes a virtual domain’s engrams, which is what --purge confirms. A domain declares its name in its MANIFEST and keeps it on every machine; a rename, from the command line, the API or Fluid, is the repair for a name that turned out wrong and leaves the old one working as an alias. Every domain starts shared, and a private one belongs to one owner and admits members as none, viewer, editor or manager; a write there takes the lower of level and instance role, while on a domain under a server’s GitHub App the role on the repository decides. Review mode is switched with crystalline domain review or in the domain’s settings in Fluid and needs a GitHub origin and a clean folder, and turning it off folds or discards each person’s drafts. Your machine can also offer a connected server’s domains beside its own, and read_only in the MANIFEST freezes a finished domain against every write through Crystalline. prompt.rules and preferred_domains steer the hook’s briefing, and a domain’s own .crystalline.yaml tunes the checker of chapter 26.