Files
jason.woltje bad760ce08 Merge refactor fbf2f6fc8 (rename stage B) into F2
Conflict in packages/webui/src/serve.mjs imports: kept F2's
THREAD_DEFINITION and onboarding imports on stage B's organization path.
The F2 assistant test edit followed the move to packages/organization;
onboarding-browser now imports the organization service directly.
2026-11-09 13:17:33 -06:00
..

Organization

@mosaic/organization: role definitions, the business and project files, the variable registry and the resolver (slice 1 row S1, #1518). Rename design r2, stage B moved it here from packages/business. That path and @mosaic/business stay as the old names: each module there re-exports the module of the same name here (src/business.mjs re-exports src/organization-config.mjs), and its src/cli.mjs runs this package's src/cli.mjs. Brief: docs/plans/2026-10-04_slice-1.md, row S1. Design: agents/darkwing/work/slice1-data-model-2026-10-04.md and its addenda A and B. The package reads files and refuses on any problem. It never writes, and it never opens a token file.

scripts/mosaic org config validate mosaic-stack
scripts/mosaic org config resolve mosaic-stack coder --project stack
node --test packages/organization/tests/

mosaic business validate|resolve is the old spelling, with the same arguments. Both spellings run runConfigCommand in src/config-command.mjs, so status, stdout and stderr are the same. The one exception is a usage error: its line names the spelling you used (mosaic org config: usage: ...). Every other stderr line, refusals and warnings included, starts mosaic business: in both spellings, so the diagnostics stay byte for byte what the old command printed. Importing src/config-command.mjs runs nothing and never exits the process; runConfigCommand(argv, {stdout, stderr, env, spelling}) returns the exit code. src/cli.mjs is the thin executable that the old spelling runs. mosaic org validate FILE is different: it checks a draft file by path and prints valid (packages/cli/README.md).

Files

File Who writes it Holds
roles/<name>.json reviewed commits a role definition: tools, network, authority, credential scopes
roles/<name>.md reviewed commits the duty contract a version 2 role file names
<configDir>/businesses/<id>.json Jason role instances, holders, bots, token references, arbiters, projects, launch limits
<root>/.mosaic/project.json reviewed commits in that project project variables, per-instance project variables

<configDir> is the directory holding the system config: the directory of $MOSAIC_CONFIG, or ~/.config/mosaic-dev. Role files come from roles/ in this repository, or $MOSAIC_ROLES_DIR if set.

Role files, version 2

{
  "roleVersion": 2, "name": "coder", "title": "Coder", "contract": "coder.md",
  "tools": ["read", "write", "edit", "bash", "grep", "find", "ls"],
  "network": "api-only",
  "authority": {
    "withinRole": ["task.update.assigned", "git.push.working", "review.request", "message.send"],
    "crossRole": ["task.scope.change"]
  },
  "credentials": [
    { "service": "gitea", "scopes": ["write:issue", "write:repository", "read:user"] },
    { "service": "vikunja", "scopes": { "tasks": ["read_one", "update"], "tasks_comments": ["create"], "projects": ["views_buckets_tasks"] } }
  ]
}
  • name matches the file name. contract is a Markdown file name in the same directory, not a path. It must exist, be a regular file and not be empty.
  • authority names actions from the closed list in src/vocabulary.mjs. withinRole actions run without asking. crossRole actions need the arbiter. Any action not listed is gated: it goes to Jason, or to the arbiter for its domain when the raise cites the project's current spec commit (PRD 1.5 REQ-DEC-2). The nine hard waits in GATED_ONLY (REQ-WAIT-1: spend, data.delete, network.expose, account.create, credential.mint, message.external, git.push.protected, git.push.remote, deploy) always go to Jason and can't appear in either list (REQ-WAIT-2). git.push.remote joined in row 199 (#1697). GATED_ONLY is every action that ROUTED doesn't name, so an action added to the vocabulary is a hard wait until a PRD says otherwise and the commit adds it to ROUTED (REQ-WAIT-3). git.merge.protected, policy.change, prd.approve and role.revoke left the list in 1.5 (row 139, #1633).
  • Gitea scopes are read:<category> or write:<category>, one level per category. all and admin are refused. Vikunja scopes map a route group to verbs, and only the pairs in VIKUNJA_GRANTABLE pass.
  • Version 1 files (roleVersion, name, tools, network) still load. They have no authority and no credentials, and a business can't use them for a role instance.

scripts/mosaic-task.mjs resolve-role checks both versions through this package and prints MOSAIC_ROLE_CONTRACT for version 2.

Business file

examples/mosaic-stack.example.json is a complete example. As shipped it refuses on purpose: every botId is 0 and every date is YYYY-MM-DD. Copy it to <configDir>/businesses/mosaic-stack.json, put in the bot ids and token dates from docs/guides/slice-1-identities.md, fix the token paths and run validate. The file must belong to you and must not be writable by group or other. The loader checks the owner and mode on the descriptor it reads from, so the file can't be swapped between the check and the read.

The business-level tracker is optional (Q247). Omit it for a business with no tracker sync: validate and bus boot need no sync credential, and the Console shows no sync credential entry. The loader represents omission as tracker: null; writing a literal null in the business file still refuses. A present tracker keeps its existing bot, credential and validation rules. Tracker variables alone do not enable sync when the tracker block is absent.

Each role instance names a definition (a version 2 role file), an optional holder, its Vikunja bot (tracker, required when the definition uses Vikunja), a credential reference for each service the definition lists, and optional agent-layer vars. Several instances may share one definition. Bot names and bot ids are unique across the instances and the sync bot.

A credential reference names a token file by absolute path, or an environment variable by name, plus rotateBy for Gitea or expires for Vikunja. The checks use lstat and realpath only. A token file must be a regular file you own, mode 600 or tighter, not empty, and outside the repository, dataRoot and every project root. A Vikunja token past expires refuses. One within seven days of it, or a Gitea token past rotateBy, prints a warning.

launch lets one instance (by) start the listed instances. models maps selection keys to exact {provider, model} pairs; each role vars.model selects a key. limits accepts optional total, providers: {provider: count} and models: {provider: {model: count}}. Counts are whole numbers of zero or more with no policy ceiling; absent means unlimited and zero stops new launches. The example starts with four Opus and four Sonnet sessions. The old max shape requires print-only mosaic limits migrate before the new copy can load it. by must name an instance whose definition holds role.launch within-role; otherwise the file refuses. Only that instance keeps role.launch. Every other instance loses it, so for them it's gated.

launch.account (optional, row 126) is {name, credential}: the account the business's Claude Code launches spend, an identifier, and the environment variables of one login set joined with + (for example CLAUDE_CODE_OAUTH_TOKEN), never a value. Which sets exist is the launcher's check. launch.limits.tokens.daily maps an account to {limit, since, reset, timeZone}: reset is HH:MM, timeZone an IANA name Intl accepts. A daily quota for any name but launch.account.name refuses the file, since it would count nothing.

launch.limits.tokens.keys holds token limits by key name. A leaf named by a twelve-hex model key id with {limit, since, reset, timeZone} is a daily quota on one agent's model key (row 333); a plain {limit, since} leaf stays a cumulative account limit, even when its name is twelve hex characters. Only the human sets or resets a model-key quota. The settings writer stamps since as for every token limit (a new limit gets now, a changed one keeps its time, a caller's different time refuses, a reset moves it to now), with one rule for a new model-key quota: it takes the earliest since among the old file's model-key quotas whose ids are in the same agent's key lineage, so moving a quota to the agent's new key id keeps its count, and takes now only when none of them holds one. A quota removed in one write and added back in a later one is new and gets now. The bus host passes keyLineage(business, keyId), which answers ids only from the bus's own credential records. A writer built without it, a lookup that throws, or an answer that is neither an empty list nor a list of key ids holding the asked id refuses a write that adds a model-key quota with proxy-key-refused. A write that adds none never calls it.

launch.threadIdleSeconds (optional, row 121) is how long a thread's session runs with nothing pending and no new message or open before the launcher stops it. Absent means 900 (THREAD_IDLE_SECONDS in src/organization-config.mjs). A set value is a positive integer with no upper bound; anything else refuses the file. The key is accepted whether or not the file declares a thread instance. A thread instance is one whose definition is assistant. The launcher starts it once per thread, when the thread has a pending message or was opened, and never on its own.

plugins (optional) switches on plug-in modules, one key per kind from packages/plugins/src/kinds.mjs, each a list of module names matching ^[a-z][a-z0-9-]*$ with no name twice. An unknown kind or a bad name refuses the file. Absent means nothing is switched on. Whether a named module exists and loads is the plug-in loader's check, at load time; see packages/plugins/README.md.

Chief-of-staff and assistant instances

Two roles stand above the PM (PRD 1.2; row 119, #1613).

The assistant role (roles/assistant.json, contract roles/assistant.md, REQ-ASSIST-1) is the user's main agent: one per user per organization. It talks with the user and passes work to the chief of staff. It holds message.send within role and nothing else: no credentials, no crossRole action, read-only tools. It doesn't direct agents, create or assign tasks, or launch sessions. The broker's send limits for it are row 120 (REQ-ASSIST-2).

The chief-of-staff role (roles/chief-of-staff.json, contract roles/chief-of-staff.md, REQ-COS-1) runs the organization: it hires and terminates role instances, owns the structure, takes work from the assistant and never talks to the user. It holds message.send and role.launch within role (decision 139), plus role.end and organization.change (row 123): it ends a run with a reason and no decision (no shipped role is granted role.revoke, so it stays gated), and it changes the organization file through the broker's organization.set, limited to the keys the human's limits.set takes; the harness's end_run and change_organization tools call them (row 132). Under Jason's ruling Q169 (row 155, #1649) the chief of staff is the one agent that ends the assistant, which ends every assistant thread run, and it configures the assistant through assistants (Assistant settings, below) and names instances through set_name. PRD 1.7 REQ-COS-3 (row 194) grants it role.hire and role.fire within role: it hires and fires with a reason and no decision, through the broker's instance.hire and instance.fire, which the harness's hire and fire tools call. It has the same read-only tools. When the business declares a library, its one credential reference is gitea, for the read grant the library mirror fetches with (row 285, amendment 1, mos-1580). With no library it holds no credential references. The broker holds any token, never the agent. It is the only shipped role that holds role.launch, so it is the only role launch.by can name. The PM holds no role.launch (REQ-LAUNCH-1); it asks the chief of staff for a session.

The owner adds both instances under roles in the business file, next to the others, and points launch.by at the chief of staff. Initial role entry and schema migration installation are human steps; later launch settings edits use the shared, broker-backed organization writer. The instance names are the owner's choice; these are the example file's:

"chief": { "definition": "chief-of-staff", "vars": { "model": "opus" } },
"assistant": { "definition": "assistant", "holder": "mos", "vars": { "model": "opus" } }
...
"launch": { "by": "chief", "instances": ["coder", "reviewer"], "models": {
  "opus": { "provider": "anthropic", "model": "claude-opus-5-5" },
  "sonnet": { "provider": "anthropic", "model": "claude-sonnet-5-5" }
}, "limits": { "models": { "anthropic": { "claude-opus-5-5": 4, "claude-sonnet-5-5": 4 } } } }

Neither entry has a tracker or credential references in this example, which has no library. A business with a library must give the chief exactly one gitea reference; a business without a library must give it none (mos-1580). The assistant has none. A { "store": true, "rotateBy": "YYYY-MM-DD" } reference works as well as a file. Neither form creates an account, mints a token or grants anything. A session that launches needs a harness and a model, so give each instance vars (harness, model) as the PM has, unless the system's execution settings cover it. A launch.by that names a PM, an assistant or any other instance whose role lacks role.launch is refused. A file that still names the PM fails to load with the shipped roles, so edit the file before a copy with these roles goes live. packages/organization/tests/chief-of-staff.test.mjs and assistant.test.mjs check each role against its approved list and load it through loadBusiness; role.test.mjs loads every file in roles/ through the validator.

Security-reviewer and merge-gate instances

Two roles finish the coding workflow next to coder and reviewer (REQ-ROLE-5; row 149, #1643).

The security-reviewer role (roles/security-reviewer.json, contract roles/security-reviewer.md) reviews a candidate for security only. It holds review.verdict within role and nothing else: read-only tools (read, grep, find, ls), no shell, no write tools, no credentials. The verdict reaches the role that asked through review.verdict itself, so the role needs no message.send. Without it the runner's automatic reply to a message is refused; the verdict still arrives.

The merge-gate role (roles/merge-gate.json, contract roles/merge-gate.md) lands approved work. Its one grant is git.merge.protected as a cross-role action, the grant row 137's land op checks. It raises the merge decision itself with domain technical, because only the raising instance and thread can use an approval (REQ-DEC-5), and lands once the technical arbiter resolves it. It has the same read-only tools and no credentials.

Both declare messaging lists. The technical arbiter must be able to message merge-gate, or the raise is refused (row 133): the shipped lists include cto, so a business whose technical arbiter is another definition has to change merge-gate's lists by a reviewed commit. Because of those lists, neither role can be named in arbiters; the business file is refused.

No verb adds a role instance. organization.set's instances only sets the model of an instance the business file already declares (launch-settings.mjs), so the human adds both entries by editing the business file, then runs validate. No shipped business declares either role yet; adding them to a live business is a separate step with its own go. The entries are:

"merge-gate": { "definition": "merge-gate" },
"security-reviewer": { "definition": "security-reviewer" }

packages/organization/tests/workflow-roles.test.mjs checks both against their approved lists, the arbiter refusal and the proactive line in every contract; packages/bus/tests/workflow-roles.test.mjs checks the merge route and the verdict path on the broker.

Project file

{ "projectVersion": 1, "id": "stack",
  "vars": { "tracker.project": 3, "git.workingBranch": "refactor", "git.protectedBranches": ["main", "next"] },
  "roles": { "coder": { "vars": { "limits.tools": ["read", "edit", "bash"] } } } }

id must match the business file's name for the project. A missing project file is a warning in validate and an error in resolve --project.

Variables

Every key is declared once in src/vars.mjs with its type, the layers that may set it and its merge rule. An unknown key refuses, and so does a key set at a layer it doesn't belong to.

Layers, least specific first: defaults, system (the system config), business (vars), project (vars, then roles.<instance>.vars), agent (the business file's roles.<instance>.vars). For a plain key the most specific layer wins. limits.tools, limits.network and limits.authority intersect: each layer that sets one narrows it, and the result also narrows the role definition. limits.authority is an allowlist, so a role action it leaves out becomes gated.

bus.tester is business-only and defaults to false. Set to true, the bus binds a tester from tester-cli.mjs for PM tests; it reads the value at bus start, so a change takes a bus restart.

A managed session's route is three agent vars (row 65, REQ-PLUG-2): harness names a harness plug-in module (the loader's name pattern), model is a launch.models key, and thinking is one of off, minimal, low, medium, high, xhigh and max. Whether the module exists, is switched on and applies the level is checked at launch and at a write, never at load. fallback.harness, fallback.model and fallback.thinking state a second route; a layer sets all three or none, and a partial set refuses. No layer falls back to the system's execution.adapter or execution.model, which stay for run-task.sh.

resolve prints provenance beside vars. For a plain key it's the layer that set the value (default, system, business:<id>, project:<id>, project:<id>:roles.<instance>, business:<id>:roles.<instance>). For a limit it's the list of layers that narrowed it.

API

import {
  loadOrganization, loadProject, systemVars, resolveInstance, classify,
  loadRole, validateRoleDocument, checkCredentialRef,
} from "../packages/organization/src/index.mjs";

const business = loadOrganization("mosaic-stack", { rolesDir });   // frozen
const project = loadProject(business.projects.stack.root);     // frozen
const coder = resolveInstance({ system: systemVars(config), business, project, instance: "coder" });
classify(coder, "git.push.working");  // "within" | "cross" | "gated"

resolveInstance returns a frozen record: business, project, instance, definition, holder, contract (absolute path), vars, provenance, limits (tools, network, authority.withinRole, authority.crossRole), credentials (references only), tracker (bot, botId or null), launch (the launch block, or null) and digest, the SHA-256 of the record's canonical JSON. classify refuses an action outside the vocabulary. launch is set exactly when classify(record, "role.launch") is within. If limits.authority leaves out role.launch for the launcher, launch is null. A launcher should still ask classify, not read launch alone.

An assistant also receives coordinator: the validated business's launch.by as instance, and that instance's configured identity.name as name (or null). With no launch block, coordinator is null. This routing context is included in the resolved digest and the generated session prompt; it does not grant authority. The name is descriptive, not a broker display-name alias; the assistant addresses messages to the exact instance key.

Errors are OrganizationConfigError with exitCode 2 (invalid) or 4 (a required file missing). BusinessError is the same constructor under its old name, and an error's name is still BusinessError. OrganizationError (src/organization.mjs, the organization writer's error with a code) is a different class. mosaic org config adds 3 for a system config problem and 4 for usage. The old names loadBusiness, validateBusinessDocument and businessFilePath are the same functions as loadOrganization, validateOrganizationDocument and organizationFilePath.

Limits

  • The action vocabulary lives here, on the host. Neither workers nor container images read it, so it isn't under contracts/.
  • The checks run as you, against files you can change. They catch mistakes; they don't stop someone who already runs as your user.
  • A token file can change between validate and the moment it's opened. The code that opens it at run time repeats these checks (rows S2 and S3).
  • Slice 1 knows two services and one tracker kind. A new one is a reviewed change to src/vocabulary.mjs and src/vars.mjs.

Organization writes

src/organization.mjs is the trusted host write module. The caller supplies a verified actor and the SHA-256 revision read earlier. It validates the whole document, and again as the bytes it will write read back as JSON, so a value JSON can't carry (a Date, a Map) refuses instead of landing as something else; changes.jsonl logs those bytes. It refuses stale revisions and concurrent writers, keeps exact previous bytes under businesses/.history/<id>/<transaction>/previous.json, then publishes a fsynced temporary file by atomic rename. changes.jsonl records prepared and committed transactions with actor, hashes and old/new values per changed field. Write-time recovery compares prepared hashes with current bytes; uncertain or torn evidence refuses organization writes. Bus startup, Settings reads and launches keep using the whole validated file on disk, without consulting receipts. A stale writer lock also refuses: an operator must confirm that its old writer is gone before clearing it; the host does not race another recovering process by deleting locks automatically. Settings reads never repair evidence themselves. The narrow setProjectRemote host method can fill only a missing remote on an existing software project. It checks the actor, revision, normalized HTTPS repository URL and the exact document delta under the same lock. It refuses libraries, credentials in URLs, replacement and clearing. It does not contact the remote or grant publishing authority. The Console setter is currently closed until session liveness can be verified at the commit boundary. launch-settings.mjs restricts human edits to limits, model table, instance model selections, routes and assistant settings, and checks every route before publication. plugin-settings.mjs is the only writer of plugins: the host calls it, and no broker verb, mosaic command or Settings form reaches it. A refused plugins write throws invalid-plugins or invalid-request before the writer runs and logs nothing. Migration uses none of this: it only prints a converted document (decision 145).

Routes (row 65, #1558, mos-0840)

launch-settings.mjs also has the routing table's read and write. The bus reaches them through the human-only verbs routes and routes.set. Neither is in ORGANIZATION_KEYS, so organization.set and the chief of staff's change_organization can't send a routes key. Through limits.set the chief changes an instance's model through instances, and the assistant's whole vars through assistants (Q169). Both writes run the same checkRoutes.

routes(id) returns the revision, the switched-on harnesses (plugins.harness), the launch.models keys, levels (each harness the file names or switches on, with the levels its module applies, or null for a module that didn't load) and one entry per instance: harness, model, thinking (null when unset) and fallback ({harness, model, thinking} or null). setRoutes(id, args, actor) takes:

{ "revision": "<64 hex>", "routes": { "coder": { "harness": "pi", "model": "glm-5.3-flash", "thinking": "high", "fallback": null } } }

Each named instance's route vars are replaced in one write, and a null fallback clears all three fallback vars. It refuses, writing and logging nothing:

Code When
invalid-request a key other than revision and routes, a bad revision, an empty routes, or a route without exactly harness, model, thinking and fallback as strings
BusinessError (the bus answers invalid-request) the next document fails validateBusinessDocument: a level no harness has, a harness name outside the pattern, or a model that isn't a launch.models key
organization-unknown-instance an instance the file lacks
organization-partial-fallback a fallback that isn't null and isn't exactly three strings
organization-no-change the file already says that
unknown-model a fallback.model that isn't a launch.models key
harness-off a changed route's harness isn't in plugins.harness
harness-refused a changed route's harness module didn't load
thinking-unsupported a changed route's module doesn't apply its level
revision-conflict the file changed since the read

checkRoutes is shared: instances.set and limits.set run it too, so no write of the route vars skips it. It checks a route only when the write changes its harness or thinking, so a model change still saves while plugins.harness is unset, and that instance's launch refuses harness-off instead. The model check covers every set model and fallback model. A route var the instance doesn't set is left to the launch, which refuses no-route. The bus host passes levels from harnessLevels (packages/bus); a writer built without it answers null for every harness.

Assistant settings (row 155, #1649)

Jason's ruling Q169 lets the human and the chief of staff configure the assistant. The update key is assistants, next to limits, models and instances:

{ "revision": "<64 hex>", "assistants": { "assistant": { "vars": { "model": "opus" }, "tracker": null } } }
  • The key maps an instance to any of vars, tracker and definition. A given field replaces that field of the entry, tracker: null removes the tracker, and a field left out stays. An empty entry is invalid-request.
  • The instance must exist (invalid-request) and its definition must be THREAD_DEFINITION from vocabulary.mjs, or the write refuses organization-not-assistant.
  • A given definition must be THREAD_DEFINITION too, or organization-definition-refused. Today that makes it a no-op, so a change can't strip the protection END_PROTECTED gives the assistant.
  • holder and credentials aren't accepted; they stay hand edits.
  • After applying every key, set refuses organization-definition-refused if any instance's definition differs from the file's, so no key gives an instance an END_PROTECTED definition after a decision about it was resolved.
  • The whole next document still goes through validateBusinessDocument and the model check, the same as a hand edit.

read returns assistants as well: each assistant instance's definition, vars ({} when absent) and tracker (null when absent). The human sends the patch with mosaic limits set --file or the Console's Assistant section; the chief of staff sends it with change_organization. The chief of staff reads the revision with organization.revision (row 134), which returns the revision alone. changes.jsonl logs the actor: the human's id, or chief-of-staff.<instance>.

For inherited selections, print-only migration reads the existing system config through its normal validator and requires an exact provider/model match. Missing or invalid config refuses; it never changes the system config. Explicit role models retain their external model and use the human-supplied provider table.

Role instances (row 165, #1662)

Q171 and REQ-ROLE-3 let the human add a role instance and change an existing one's definition, vars and launch membership without a hand edit. src/instance-settings.mjs is that writer. The bus reaches it through the human-only verbs instances and instances.set. Neither is in the broker's ORGANIZATION_KEYS, so organization.set and the chief of staff's change_organization can't reach it (sage-0918).

read returns the file's revision, each instance (definition, vars, launched, launcher, protected), whether the file has a launch block, and each definition in the roles directory with the services its credentials name and whether add and set accept it. set takes:

{ "revision": "<64 hex>", "instance": "sec-1", "op": "add", "definition": "security-reviewer", "vars": {}, "launch": true }
  • add needs definition; change needs at least one of definition, vars and launch. vars replaces the instance's vars whole. launch adds the instance to launch.instances or removes it. Any other key is invalid-request, so holder, credentials and tracker stay hand edits. There is no delete.
  • An instance named human or tester refuses with invalid-request. The broker refuses a business whose roles use either name when it loads, so such an add would stop the bus at its next start (row 165 r2).
  • A definition that declares credentials refuses with organization-credentials-out-of-scope (the brief's named refusal, sage-0922). So does a definition change on an instance whose definition declares them, because its credentials would bind to the new definition's authority. The writer never writes an instance without its binding. A vars or launch change on such an instance succeeds and leaves credentials and tracker as they are.
  • An END_PROTECTED definition, given or held, refuses with organization-definition-refused; the assistant changes through assistants above.
  • organization-instance-exists, organization-unknown-instance, organization-launch-not-configured and organization-no-change refuse what their names say.
  • The whole next document goes through validateBusinessDocument, the route checks (checkRoutes, under "Routes" above), and each project file's id and instance resolution (organization-project-mismatch, organization-unresolvable-instance), the checks mosaic org config validate runs.
  • The organization writer then refuses a stale revision (revision-conflict) and logs the change with the actor.

A refusal writes nothing and logs nothing. The answer is the read with the transaction and restart. restart is true after an add or a definition change: the running broker keeps the roles it loaded at start. The writer doesn't restart anything. The CLI (mosaic org instance add|set, or the old spelling mosaic business instance add|set) and the Console Settings page send the patch.

Organizations (development release)

org-runtime.json beside the instance config is private runtime metadata. It reserves each organization ID and its derived <dataRoot>/org-runtime/<id> root, which carries an instance-and-ID marker. It is not a system config. The organization service writes the existing businesses/<id>.json format so loadBusiness can read it. Organization updates require the current revision; a conflicting revision refuses. A document or marker that disagrees with the registry refuses rather than falling back to another organization.

Use mosaic org template render ID --fixture local --output FILE, edit the non-secret JSON if needed, then mosaic org validate FILE and mosaic org create ID --from FILE. Show and list use mosaic org show ID [--json] and mosaic org list [--json]; update with mosaic org update ID --from FILE --if-revision N. The development template identifies the local owner, references reviewed roles, and declares the work project. Its path is replaced with a root derived from the selected instance and organization.

Initialize an organization checkout with mosaic org queue init ID --local-only --accept-local-loss-risk and a project repository with mosaic org project repo init ID PROJECT --local-only --accept-local-loss-risk. The explicit acceptance records that this machine may hold the only copy. Alternatively use --remote https://HOST/OWNER/REPO in place of both local-only flags. Both modes create a local repository and genesis commit; remote mode records the URL without a push. Existing paths are never overwritten. mosaic org queue show ID --json and mosaic org project repo show ID PROJECT --json read the receipts back. Queue review transports and transitions are not provided by this setup.

The local development owner is accepted only through the existing named-instance selection on this release's throwaway development instances. Receipt subjects name that principal, not Jason. The dev-purpose bootstrap fence is a separate follow-up before person-used deployment.