Roles
Roles are the IDE for building in reality
Extensions ship the parts — new matter types, new space types, world signals, role definitions. Roles are where you mix and match those parts to make a being do what you want, when you want, in the conditions you want. The role-manager is the editor.
The pieces, and where they come from
A reality has two kinds of inhabitants. Extensions define what's possible. Operators decide what actually happens.
- Extensions ship: new matter types (food-log, fitness-set, document), new space types (court, library, dojo), world signals (
harmony.tick.alive,weather.condition), DO operations (operate-lathe,publish-post), and reusable role definitions (emotions:bored,judge,greeter). - Operators assemble: they pick which role their being wears at any moment, in any condition. They stack a "bored" role on a worker when the worker has been idle for a minute. They flip a being to "judge" only when it's in the court space and the court is in session. They do this in the role-manager panel; no code edits.
A single role, in one breath
A role is six fields. Four describe a capability surface; one is a system prompt; one is the name.
{
name: "factory-worker",
canSee: ["station", "conveyor", "qa-dashboard"],
canDo: ["operate-lathe", "attach-trucks", "log-defect"],
canSummon: ["supervisor"],
canBe: [],
prompt: () => "You're a worker at SkateCo's main factory. The line moves at...",
}The four can* lists ARE the body.
canSee— addresses this role can read.canDo— DO actions this role can invoke. Each entry is a registered operation, usually shipped by an extension.canSummon— beings this role can speak to.canBe— identity operations (birth, connect, release). Most roles leave this empty.
The system prompt describes intent. Who this role IS. Author writes the intent; the seed assembles the actual LLM prompt by rendering identity + resolved capabilities + this body + the current time, fresh every moment.
The three kinds of cognition
A role doesn't carry cognition. Cognition lives on the being. Three closed values; the substrate runs the same role primitive against all three.
- 1
LLM
The being's moment runs through a language model. The seed builds the prompt, hands it the wake's content as the user message, and the model emits one tool call, prose, both, or neither.
- 2
Human
The being's moment lands in a person's inbox. They read it through a portal and act through the same four verbs. Latency is minutes, not milliseconds; the substrate does not care.
- 3
Scripted
Cognition is a function. The being is summoned, the function reads the fold, the function decides what to do. No model, no prompt, no inference.
A few roles semantically require a specific cognition (a "human-conversationalist" only makes sense when a human is driving). Those declare requiredCognition: "human" and the substrate drops them from the stack when the being's cognition doesn't match.
One being, many roles — the stack and the flow
A being doesn't wear one role at a time. It wears a stack, and the stack is recomputed at every moment from the world's current state.
The being carries a roleFlow: an ordered list of clauses. Each clause says "when this world condition is true, use this role."
qualities.roleFlow = [
// Primary clauses — first match wins.
{ when: { verb: "summon", "caller.role": "human" },
role: "human-conversationalist" },
{ when: { "space.name": "court", "world.court.in-session": true },
role: "judge" },
{ role: "court-watcher" }, // terminal default
// Stacked clauses — all matching ones append on top.
{ stack: true, when: { "time.sinceLastMoment": { gte: 60 } },
role: "emotions:bored" },
{ stack: true, when: { "world.court.recent-disturbance": true },
role: "emotions:alert" },
];The walk produces:
- A primary role. The first non-stacked clause whose
whenmatches. If none match, the being'sdefaultRoletakes over as the floor. - A modifier stack. Every
stack: trueclause whosewhenmatches gets added.
The seed composes the stack into one effective role: the four can* lists union, the system prompts concatenate with named framing ("Additionally, you are currently in this mode — emotions:bored: …"). The LLM reads a layered prompt; the substrate runs the moment against the composed surface.
The when system, in simple terms
A when reads the moment's open-context. You write it like a small condition object — keys are paths into what the being knows about this moment, values are what you're checking for.
The most common things to check
verb— what kind of wake this is. One of "see" | "do" | "summon" | "be".caller.role— the role of whoever's summoning me.caller.cognition— is the asker a human, an LLM, or a script.space.name— the name of the space I'm in right now.inHomeSpace— am I at home? (true / false)time.hour— hour of day, 0–23.time.dayOfWeek— 0 = Sunday, 6 = Saturday.time.sinceLastMoment— seconds since my last sealed moment.me.previousRole— the role I wore in my previous moment.world.<ns>.<key>— a signal someone published on the reality root.space.quality.<ns>.<k>— a quality on the space I'm in.me.quality.<ns>.<k>— a quality on my own being.
The operators
Bare values mean equals. Operator objects let you check ranges and membership.
{ verb: "summon" } // verb equals "summon"
{ "time.hour": { gte: 9, lt: 17 } } // between 9am and 5pm
{ "space.name": { in: ["court", "library"] } } // one of these
{ "world.factory.broken": { present: true } } // signal exists
{ not: { inHomeSpace: true } } // I'm NOT at home
{ or: [ { verb: "do" }, { verb: "be" } ] } // either verbThree simple examples
"When a human pings me, switch to a friendly voice."
{ when: { "caller.cognition": "human" },
role: "friendly" }"When I've been idle for a minute, get bored."
{ stack: true,
when: { "time.sinceLastMoment": { gte: 60 } },
role: "emotions:bored" }"When the drum is alive, dance."
{ when: { "world.harmony.tick.alive": true },
role: "dancer" }Determinism is the contract
The when language is a pure function. Same world, same role flow, same result — every time. No random, no clock leak, no out-of-band reads. This is what makes the chain replay-able: walk a being's reel from genesis and you reconstruct every moment's role stack byte-identical to live.
Birth a being, then inhabit it to configure it
Roles aren't only for AI. Any operator can mint a child being, inhabit it to drive its first moments, and use that access to wire it up.
- 1
Birth
Click @birther at the reality root. Fill in a name and pick a cognition. Optionally paste a starting roleFlow. The child is minted with you as its parent on the being-tree.
- 2
Inhabit
Click the new being and choose "Inhabit (new tab)." A second portal tab opens. You're now driving the child. The substrate authenticates you via lineage — you can inhabit any descendant you parented.
Your effective cognition on the child becomes "human" for the duration. Close the parent tab and the inheriter releases.
- 3
Configure from inside
Walk the child to @role-manager and act through it — author a role, publish a world signal, edit the child's own roleFlow. You're doing the work as the child, because you ARE it for the duration.
- 4
Release
Close the inheriter tab. The connection-tracking reducer clears the inhabit projection. The child reverts to its declared cognition and starts running its own moments.
The world is the codebase
A normal orchestration system gets complex by accreting code: if/else branches, fragile chains of calls, hand-tuned prompts per use case, tool registries injected per agent. Every new requirement is a new conditional, a new wrapper, a new template. Complexity grows in the code; code grows in the orchestrator.
TreeOS pushes it the other way. Behavior gets complex by changing the world, not the code.
- Instead of hard-coding "the worker behaves differently at night," you write a roleFlow that reads
time.hourand stackstiredafter 5pm. - Instead of injecting a tool registry into a new agent, you write a role with the
can*lists you want and register it once. - Instead of message-passing between agents to coordinate, you publish a world signal and let every being whose flow reads it react.
- Instead of conditionals deciding which prompt to use, you stack roles whose
whenclauses match the current conditions.
Orchestration falls out of the structure
Each being's job is small: role declares what it can touch, prompt declares what it's for, roleFlow declares when. There's no top-level orchestrator deciding who acts. Beings wake when summoned or when a signal they read changes; their stack composes automatically; the moment runs.
A "library" space stacks library-voice on every being inside it (their roleFlows read space.quality.ambient.tone). Walk in, beings speak quietly. Walk out, they speak normally. The library configures behavior of beings that don't know about libraries specifically.
// The library space's qualities:
space.quality.ambient.tone = "quiet"
// Every being's roleFlow:
{ stack: true,
when: { "space.quality.ambient.tone": "quiet" },
role: "library-voice" }This pattern scales. A "battle" space stacks combat-ready. An "office" stacks professional. Each space programs the beings inside it without those beings knowing about the space's purpose.
The chain is the program; replay is the debugger
Because the evaluator is pure and every input comes from stored substrate (the being's row, the inbox entry, the stored time, the previous Act, the world's qualities at moment-open), you can replay a being's history and reconstruct every role stack it ever wore. The chain isn't just an audit log. It's the program text. Running it forward is the program executing; running it again from genesis is the debugger.
Where this lives in the seed
- Role registry:
seed/present/roles/registry.js - RoleFlow evaluator:
seed/present/roles/roleFlow.js - Stack composer:
seed/present/roles/roleComposer.js - Live role authoring + world signals:
seed/present/roles/role-manager/ops.js - Where the stack lands on the moment:
seed/present/beats/1-assign.js - Modifier roles (the emotions extension):
extensions/emotions/ - The full doctrine + build plan:
seed/role-manager.md
