diff --git a/.changeset/brave-cities-melt.md b/.changeset/brave-cities-melt.md
new file mode 100644
index 0000000..06a2e09
--- /dev/null
+++ b/.changeset/brave-cities-melt.md
@@ -0,0 +1,6 @@
+---
+"@rolexjs/local-platform": minor
+"rolexjs": patch
+---
+
+Expand link targets in SQLite runtime projection and fold requirement nodes by default
diff --git a/.changeset/config.json b/.changeset/config.json
index 8b9e3d8..5819156 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -7,11 +7,11 @@
"@rolexjs/core",
"@rolexjs/parser",
"@rolexjs/local-platform",
+ "@rolexjs/prototype",
+ "@rolexjs/genesis",
"rolexjs",
- "@rolexjs/cli",
"@rolexjs/mcp-server",
- "@rolexjs/system",
- "@rolexjs/resourcex-types"
+ "@rolexjs/system"
]
],
"linked": [],
diff --git a/.changeset/enforce-global-id-uniqueness.md b/.changeset/enforce-global-id-uniqueness.md
new file mode 100644
index 0000000..7c99e18
--- /dev/null
+++ b/.changeset/enforce-global-id-uniqueness.md
@@ -0,0 +1,11 @@
+---
+"@rolexjs/system": minor
+"@rolexjs/local-platform": minor
+"@rolexjs/prototype": minor
+---
+
+feat: enforce global ID uniqueness across the state tree
+
+- Both in-memory and SQLite runtimes now reject duplicate IDs with a clear error
+- Same ID under same parent remains idempotent (returns existing node)
+- Identity nodes now use `{id}-identity` suffix to avoid conflicting with individual ID
diff --git a/.changeset/fix-required-params-empty-nodes.md b/.changeset/fix-required-params-empty-nodes.md
new file mode 100644
index 0000000..c6beb69
--- /dev/null
+++ b/.changeset/fix-required-params-empty-nodes.md
@@ -0,0 +1,9 @@
+---
+"@rolexjs/prototype": patch
+"rolexjs": patch
+---
+
+fix: validate required params in dispatch and filter empty nodes in render
+
+- Enforce required parameter validation in `toArgs` dispatch — missing required args now throw a clear error instead of silently passing `undefined` (#23)
+- Filter empty nodes (no id, no information, no children) in `renderState` to prevent cluttered activation output (#24)
diff --git a/.changeset/funky-windows-study.md b/.changeset/funky-windows-study.md
new file mode 100644
index 0000000..797e244
--- /dev/null
+++ b/.changeset/funky-windows-study.md
@@ -0,0 +1,5 @@
+---
+"@rolexjs/mcp-server": patch
+---
+
+Auto-inject all world descriptions into MCP instructions instead of manually listing them
diff --git a/.changeset/identity-ethics-directives.md b/.changeset/identity-ethics-directives.md
new file mode 100644
index 0000000..4143382
--- /dev/null
+++ b/.changeset/identity-ethics-directives.md
@@ -0,0 +1,16 @@
+---
+"@rolexjs/prototype": minor
+"rolexjs": minor
+---
+
+feat: identity ethics and directive system for role boundaries
+
+Establish identity ethics as the foundational world instruction and build a directive system for enforcing role boundaries at critical decision points.
+
+- Add identity-ethics.feature as @priority-critical world description
+- Add priority sorting mechanism for world descriptions (critical > high > normal)
+- Build directive system (replaces reminders) with gen-directives.ts generator
+- Wire on-unknown-command directive into error handling
+- Remove nuwa.feature to prevent leaking world-building commands to all roles
+- Clean up use-protocol, census, and cognition to remove command knowledge leaks
+- Export directive() API from rolexjs
diff --git a/.changeset/mighty-pianos-wave.md b/.changeset/mighty-pianos-wave.md
new file mode 100644
index 0000000..c532eea
--- /dev/null
+++ b/.changeset/mighty-pianos-wave.md
@@ -0,0 +1,6 @@
+---
+"@rolexjs/system": minor
+"rolexjs": minor
+---
+
+Expand link targets in state projection — duties and charters now visible on activate
diff --git a/.changeset/no-guess-commands.md b/.changeset/no-guess-commands.md
new file mode 100644
index 0000000..4f0b06a
--- /dev/null
+++ b/.changeset/no-guess-commands.md
@@ -0,0 +1,5 @@
+---
+"@rolexjs/prototype": patch
+---
+
+Add explicit instruction to never guess RoleX commands — only use commands seen in loaded skills or world descriptions.
diff --git a/.changeset/realize-reflect-empty-ids.md b/.changeset/realize-reflect-empty-ids.md
new file mode 100644
index 0000000..7e9f6b6
--- /dev/null
+++ b/.changeset/realize-reflect-empty-ids.md
@@ -0,0 +1,13 @@
+---
+"@rolexjs/prototype": minor
+"rolexjs": minor
+"@rolexjs/mcp-server": minor
+---
+
+feat: realize and reflect accept empty source IDs
+
+Allow calling realize with no experience IDs and reflect with no encounter IDs. This enables direct creation of principles and experiences from conversational insights without requiring the full encounter → experience → principle chain.
+
+- ops.ts: skip resolve/remove when IDs are empty, directly create target node
+- role.ts: skip validation and consumption for empty IDs
+- MCP layer: pass undefined when IDs array is empty
diff --git a/.changeset/release-1.0.0.md b/.changeset/release-1.0.0.md
new file mode 100644
index 0000000..a79ff47
--- /dev/null
+++ b/.changeset/release-1.0.0.md
@@ -0,0 +1,21 @@
+---
+"@rolexjs/core": major
+"@rolexjs/system": major
+"@rolexjs/prototype": major
+"@rolexjs/local-platform": major
+"@rolexjs/genesis": major
+"@rolexjs/mcp-server": major
+"@rolexjs/parser": major
+"rolexjs": major
+---
+
+Release 1.0.0 — RoleX AI Agent Role Management Framework
+
+Highlights:
+- RoleXRepository unified data access layer (SQLite-backed)
+- Platform integrates ResourceXProvider for pluggable storage
+- Identity ethics and directive system for role boundaries
+- Batch consumption in reflect/realize/master
+- Global ID uniqueness enforcement
+- BDD test framework with MCP E2E coverage
+- Render layer sunk from MCP server into rolexjs
diff --git a/.changeset/remove-requirement-copy.md b/.changeset/remove-requirement-copy.md
new file mode 100644
index 0000000..c2d13d2
--- /dev/null
+++ b/.changeset/remove-requirement-copy.md
@@ -0,0 +1,9 @@
+---
+"@rolexjs/prototype": minor
+"rolexjs": minor
+---
+
+refactor: remove requirement copy pattern from position.appoint
+
+- `position.appoint` no longer copies requirements as procedures onto the individual — requirements are rendered through position links instead, like organization charters
+- Unfold requirement nodes in `renderState` so their content (including skill locators) is visible
diff --git a/.changeset/rename-genesis.md b/.changeset/rename-genesis.md
new file mode 100644
index 0000000..4332050
--- /dev/null
+++ b/.changeset/rename-genesis.md
@@ -0,0 +1,8 @@
+---
+"@rolexjs/genesis": minor
+"@rolexjs/mcp-server": minor
+"@rolexjs/prototype": minor
+"rolexjs": minor
+---
+
+Rename @rolexjs/rolex-prototype to @rolexjs/genesis, consolidate descriptions into prototype package, and add direct tool for stateless world-level operations.
diff --git a/.changeset/resourcex-provider-integration.md b/.changeset/resourcex-provider-integration.md
new file mode 100644
index 0000000..643bcbf
--- /dev/null
+++ b/.changeset/resourcex-provider-integration.md
@@ -0,0 +1,12 @@
+---
+"@rolexjs/core": minor
+"@rolexjs/local-platform": minor
+"rolexjs": minor
+---
+
+refactor: Platform integrates ResourceXProvider instead of ResourceX
+
+Platform now declares `resourcexProvider?: ResourceXProvider` instead of `resourcex?: ResourceX`.
+Rolex internally creates the ResourceX instance from the injected provider.
+This makes the storage backend decision explicit at the Platform level —
+swapping providers is all that's needed to move from local to cloud deployment.
diff --git a/.changeset/rolex-repository.md b/.changeset/rolex-repository.md
new file mode 100644
index 0000000..b4ffdea
--- /dev/null
+++ b/.changeset/rolex-repository.md
@@ -0,0 +1,7 @@
+---
+"@rolexjs/core": minor
+"@rolexjs/local-platform": minor
+"rolexjs": minor
+---
+
+Introduce RoleXRepository interface and SqliteRepository implementation. Platform now uses `repository` instead of separate runtime/prototype/saveContext/loadContext. Prototypes and contexts stored in SQLite instead of JSON files.
diff --git a/.changeset/skill-loading-and-rendering.md b/.changeset/skill-loading-and-rendering.md
new file mode 100644
index 0000000..8e2693c
--- /dev/null
+++ b/.changeset/skill-loading-and-rendering.md
@@ -0,0 +1,8 @@
+---
+"@rolexjs/system": minor
+"@rolexjs/local-platform": minor
+"@rolexjs/prototype": minor
+"rolexjs": minor
+---
+
+Relax global ID uniqueness to same-parent idempotence, auto-train requirements as procedures on appoint, enhance error messages with skill-loading guidance, fix plan link rendering to compact references, and add goal progress summary in headings
diff --git a/README.md b/README.md
index 2e46671..47e40ec 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,10 @@
RoleX
- Role-Driven Development (RDD) Framework for AI Agents
-
- 持久身份 · 目标驱动 · Gherkin 原生 · MCP 即用
+ Social Framework for AI Agents
+
AI 智能体社会化框架
+
Give AI agents persistent identity, social structure, and growth through experience — modeled on how human societies work.
@@ -27,42 +21,34 @@
---
-RoleX lets AI agents have persistent identity, goals, plans, and tasks — all expressed in Gherkin `.feature` files. Instead of starting every conversation from scratch, your AI remembers who it is and what it's working on.
-
-RoleX evolved from [PromptX](https://github.com/Deepractice/PromptX) — rethinking AI role management with Gherkin-native identity and goal-driven development.
+## Why Social?
-## Core Concepts
+Human societies solve a problem AI agents haven't: **how to organize, grow, and persist**.
-**Everything is Gherkin.** Identity, knowledge, goals, plans, tasks — one format, one language.
+In a society, people have identities, join organizations, hold positions, accumulate experience, and pass on knowledge. RoleX brings this same model to AI agents:
-```text
-Society (Rolex) # Top-level: create roles, found organizations
- └── Organization # Team structure: hire/fire roles
- └── Role # First-person: identity, goals, plans, tasks
-```
+- **Identity** — An agent knows who it is across sessions, not just within one
+- **Organization** — Agents belong to groups, hold positions, carry duties
+- **Growth** — Experience accumulates into principles and reusable skills
+- **Persistence** — Goals, plans, and knowledge survive beyond a single conversation
-### Five Dimensions of a Role
+Everything is expressed in **Gherkin** `.feature` format — human-readable, structured, versionable.
-| Dimension | What it is | Example |
-| ------------ | --------------------------------------------------- | --------------------------------------------- |
-| **Identity** | Who I am — persona, knowledge, experience, voice | "I am Sean, a backend architect" |
-| **Goal** | What I want to achieve — with success criteria | "Build user authentication system" |
-| **Plan** | How I'll do it — phased execution strategy | "Phase 1: Schema, Phase 2: API, Phase 3: JWT" |
-| **Task** | Concrete work items — directly executable | "Implement POST /api/auth/register" |
-| **Skill** | What I can do — AI capabilities, no teaching needed | Tool use, code generation |
+## Quick Start
-### How It Works
+Install the MCP server, connect it to your AI client, and say **"activate nuwa"** — she will guide you from there.
-1. **Activate nuwa (the genesis role)** — she guides everything else
-2. nuwa creates roles, teaches knowledge, builds organizations
-3. Each role works autonomously: set goals, make plans, execute tasks
-4. Experience accumulates as part of identity — roles grow over time
+
+Claude Code
-## Quick Start
+```bash
+claude mcp add rolex -- npx -y @rolexjs/mcp-server
+```
-Install the MCP server and connect it to your AI client. That's it — nuwa will guide you from there.
+
-### Claude Desktop
+
+Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
@@ -77,15 +63,12 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o
}
```
-Restart Claude Desktop after saving.
+
-### Claude Code
+
+Cursor
-```bash
-claude mcp add rolex -- npx -y @rolexjs/mcp-server
-```
-
-Or add to your project's `.mcp.json`:
+Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
```json
{
@@ -98,14 +81,18 @@ Or add to your project's `.mcp.json`:
}
```
-### Cursor
+
-Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
+
+VS Code
+
+Add to `.vscode/mcp.json`:
```json
{
- "mcpServers": {
+ "servers": {
"rolex": {
+ "type": "stdio",
"command": "npx",
"args": ["-y", "@rolexjs/mcp-server"]
}
@@ -113,7 +100,10 @@ Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
}
```
-### Windsurf
+
+
+
+Windsurf
Edit `~/.codeium/windsurf/mcp_config.json`:
@@ -128,23 +118,10 @@ Edit `~/.codeium/windsurf/mcp_config.json`:
}
```
-### VS Code
+
-Add to `.vscode/mcp.json`:
-
-```json
-{
- "servers": {
- "rolex": {
- "type": "stdio",
- "command": "npx",
- "args": ["-y", "@rolexjs/mcp-server"]
- }
- }
-}
-```
-
-### JetBrains IDEs
+
+JetBrains IDEs
Go to **Settings > Tools > AI Assistant > Model Context Protocol (MCP)**, click **+** and paste:
@@ -159,7 +136,10 @@ Go to **Settings > Tools > AI Assistant > Model Context Protocol (MCP)**, click
}
```
-### Zed
+
+
+
+Zed
Add to Zed's `settings.json`:
@@ -176,68 +156,211 @@ Add to Zed's `settings.json`:
}
```
-## After Installation
+
-Start a conversation with your AI and say:
+## How It Works
-> Activate nuwa
+**You don't need to learn any commands.** Just install the MCP server and talk to your AI naturally — "create an organization", "set a goal", "what have I learned?". The AI knows which tools to call.
-nuwa is the genesis role. She will bootstrap the environment and guide you through creating your own roles, organizations, and knowledge systems.
+Everything below is what happens **under the hood**. RoleX provides MCP tools that the AI calls autonomously. Understanding the mechanism helps you get more out of it, but operating it is the AI's job, not yours.
-## MCP Tools
+The tools fall into two categories:
-RoleX provides 15 tools through the MCP server, organized in three layers:
+- **Direct tools** — the AI calls them by name (e.g. `activate`, `want`, `plan`). These are daily operations.
+- **The `use` tool** — a unified dispatch for world management, written as `!namespace.method` (e.g. `!org.found`, `!census.list`). This is the admin layer.
-| Layer | Tools | Who uses it |
-| ---------------- | ----------------------------------------------------------------------------------------- | ----------- |
-| **Society** | `society` (born, found, directory, find, teach) | nuwa only |
-| **Organization** | `organization` (hire, fire) | nuwa only |
-| **Role** | `identity`, `focus`, `want`, `plan`, `todo`, `achieve`, `abandon`, `finish`, `synthesize` | Any role |
+The following sections walk through each system in the order an agent encounters them.
-## Packages
+---
+
+### 1. The World — Society Structure
-| Package | Description |
-| ------------------------- | ------------------------------------------------------ |
-| `@rolexjs/core` | Core types and Platform interface |
-| `@rolexjs/parser` | Gherkin parser (wraps @cucumber/gherkin) |
-| `@rolexjs/local-platform` | Filesystem-based storage implementation |
-| `rolexjs` | Main package — Rolex + Organization + Role + bootstrap |
-| `@rolexjs/mcp-server` | MCP server for AI clients |
-| `@rolexjs/cli` | Command-line interface |
-
-## Storage Structure
-
-RoleX stores everything in a `.rolex/` directory:
-
-```text
-.rolex/
-├── rolex.json # Organization config
-├── alex/
-│ ├── identity/
-│ │ ├── persona.identity.feature # Who I am
-│ │ ├── arch.knowledge.identity.feature # What I know
-│ │ └── v1.experience.identity.feature # What I've learned
-│ └── goals/
-│ └── auth-system/
-│ ├── auth-system.goal.feature # What I want
-│ ├── auth-system.plan.feature # How I'll do it
-│ └── tasks/
-│ └── register.task.feature # Concrete work
-└── bob/
- ├── identity/
- └── goals/
+Before an agent can act, a world must exist. RoleX models a **society** with four entity types:
+
+```
+Society
+├── Individual # An agent with identity, goals, and knowledge
+├── Organization # Groups individuals via membership
+├── Position # Defines roles with duties and required skills
+└── Past # Archive for retired/dissolved entities
```
+All world management goes through the `use` tool:
+
+**Individual** — agent lifecycle
+
+| Command | What it does |
+|---------|-------------|
+| `!individual.born` | Create an individual |
+| `!individual.teach` | Inject a principle (knowledge) |
+| `!individual.train` | Inject a procedure (skill) |
+| `!individual.retire` | Archive an individual |
+
+**Organization** — group structure
+
+| Command | What it does |
+|---------|-------------|
+| `!org.found` | Create an organization |
+| `!org.charter` | Define mission and governance |
+| `!org.hire` / `!org.fire` | Add or remove members |
+| `!org.dissolve` | Archive an organization |
+
+**Position** — roles and responsibilities
+
+| Command | What it does |
+|---------|-------------|
+| `!position.establish` | Create a position |
+| `!position.charge` | Assign a duty |
+| `!position.require` | Declare a required skill — auto-trained on appointment |
+| `!position.appoint` / `!position.dismiss` | Assign or remove an individual |
+| `!position.abolish` | Archive a position |
+
+**Census** — query the world
+
+| Command | What it does |
+|---------|-------------|
+| `!census.list` | List all individuals, organizations, positions |
+| `!census.list { type: "..." }` | Filter by type: `individual`, `organization`, `position`, `past` |
+
+---
+
+### 2. Execution — The Doing Cycle
+
+Once activated, an agent pursues goals through a structured lifecycle. These are **direct tools** the agent calls by name:
+
+```
+activate → want → plan → todo → finish → complete / abandon
+```
+
+| Tool | What it does |
+|------|-------------|
+| `activate` | Enter a role — load identity, goals, knowledge |
+| `focus` | View or switch the current goal |
+| `want` | Declare a goal with success criteria |
+| `plan` | Break a goal into phases (supports sequential and fallback strategies) |
+| `todo` | Create a concrete task under a plan |
+| `finish` | Mark a task done, optionally record what happened |
+| `complete` | Mark a plan done — strategy succeeded |
+| `abandon` | Drop a plan — strategy failed, but learning is captured |
+
+---
+
+### 3. Cognition — The Learning Cycle
+
+Execution produces **encounters** — raw records of what happened. The cognition system transforms these into structured knowledge. These are also **direct tools**:
+
+```
+encounter → reflect → experience → realize / master → principle / procedure
+```
+
+| Tool | What it does |
+|------|-------------|
+| `reflect` | Digest encounters into experience — pattern recognition |
+| `realize` | Distill experience into a principle — a transferable truth |
+| `master` | Distill experience into a procedure — a reusable skill |
+| `forget` | Remove outdated knowledge |
+
+This is how an agent grows. A principle learned from one project applies to the next. A procedure mastered once can be reused forever.
+
+---
+
+### 4. Skills — Progressive Disclosure
+
+An agent can't load every skill into context at once. RoleX uses a three-layer progressive disclosure model:
+
+| Layer | Loaded when | What it contains |
+|-------|-------------|-----------------|
+| **Procedure** | Always (at activate) | Metadata — what the skill is, when to use it |
+| **Skill** | On demand via `skill(locator)` | Full instructions — step-by-step how to do it |
+| **Resource** | On demand via `use(locator)` | External content — templates, data, tools |
+
+The `skill` and `use` tools are **direct tools** for loading content. When `use` receives a locator *without* the `!` prefix, it loads a resource from [ResourceX](https://github.com/Deepractice/ResourceX) instead of dispatching a command.
+
+---
+
+### 5. Resources — Agent Capital
+
+Resources are the **means of production** for AI agents — skills, prototypes, and knowledge packages that can be accumulated, shared, and reused across agents and teams.
+
+Powered by [ResourceX](https://github.com/Deepractice/ResourceX), the resource system covers the full lifecycle through the `use` tool:
+
+**Production** — create and package
+
+| Command | What it does |
+|---------|-------------|
+| `!resource.add` | Register a local resource |
+| `!prototype.summon` | Pull and register a prototype from source |
+| `!prototype.banish` | Unregister a prototype |
+
+**Distribution** — share and consume
+
+| Command | What it does |
+|---------|-------------|
+| `!resource.push` | Publish a resource to a registry |
+| `!resource.pull` | Download a resource from a registry |
+| `!resource.search` | Search available resources |
+
+**Inspection**
+
+| Command | What it does |
+|---------|-------------|
+| `!resource.info` | View resource metadata |
+
+This is how agent knowledge scales beyond a single individual — skills authored once can be distributed to any agent through prototypes and registries.
+
+---
+
+## Gherkin — The Universal Language
+
+Everything in RoleX is expressed as Gherkin Features:
+
+```gherkin
+Feature: Sean
+ A backend architect who builds AI agent frameworks.
+
+ Scenario: Background
+ Given I am a software engineer
+ And I specialize in systems design
+```
+
+Goals, plans, tasks, principles, procedures, encounters, experiences — all Gherkin. This means:
+
+- **Human-readable** — anyone can understand an agent's state
+- **Structured** — parseable, diffable, versionable
+- **Composable** — Features compose naturally into larger systems
+
+## Storage
+
+RoleX persists everything in SQLite at `~/.deepractice/rolex/`:
+
+```
+~/.deepractice/rolex/
+├── rolex.db # SQLite — single source of truth
+├── prototype.json # Prototype registry
+└── context/ # Role context (focused goal/plan per role)
+```
+
+## Packages
+
+| Package | Description |
+|---------|-------------|
+| `rolexjs` | Core API — Rolex class, namespaces, rendering |
+| `@rolexjs/mcp-server` | MCP server for AI clients |
+| `@rolexjs/core` | Core types, structures, platform interface |
+| `@rolexjs/system` | Runtime interface, state merging, prototype |
+| `@rolexjs/parser` | Gherkin parser |
+| `@rolexjs/local-platform` | SQLite-backed runtime implementation |
+| `@rolexjs/cli` | Command-line interface |
+
---
diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md
deleted file mode 100644
index 5a6b909..0000000
--- a/apps/cli/CHANGELOG.md
+++ /dev/null
@@ -1,112 +0,0 @@
-# @rolexjs/cli
-
-## 0.11.0
-
-### Minor Changes
-
-- e8fcab2: feat: rename growup to synthesize with Kantian epistemology semantics
- - Rename `growup()` to `synthesize()` — experience-only (a posteriori learning)
- - Rename Platform.growup to Platform.addIdentity (neutral internal storage method)
- - Add optional `experience` parameter to `finish()` for task-level synthesis
- - Add synthesis awareness section to INSTRUCTIONS (proactive memory triggers)
- - Add user memory intent recognition ("记一下", "remember this" → synthesize)
- - teach() remains the entry point for knowledge/voice (a priori transmission)
- - achieve/abandon/finish now form a consistent triad with experience hooks
-
-### Patch Changes
-
-- Updated dependencies [e8fcab2]
- - rolexjs@0.11.0
- - @rolexjs/local-platform@0.11.0
-
-## 0.10.0
-
-### Patch Changes
-
-- Updated dependencies [17f21bd]
- - rolexjs@0.10.0
- - @rolexjs/local-platform@0.10.0
-
-## 0.9.1
-
-### Patch Changes
-
-- Updated dependencies [3bb910b]
- - @rolexjs/local-platform@0.9.1
- - rolexjs@0.9.1
-
-## 0.9.0
-
-### Patch Changes
-
-- Updated dependencies [59a8320]
-- Updated dependencies [99bcce5]
- - rolexjs@0.9.0
- - @rolexjs/local-platform@0.9.0
-
-## 0.8.0
-
-### Patch Changes
-
-- Updated dependencies [686ce6f]
- - @rolexjs/local-platform@0.8.0
- - rolexjs@0.8.0
-
-## 0.7.0
-
-### Patch Changes
-
-- Updated dependencies [33871d4]
-- Updated dependencies [71a8860]
- - rolexjs@0.7.0
- - @rolexjs/local-platform@0.7.0
-
-## 0.6.0
-
-### Patch Changes
-
-- Updated dependencies [e537147]
- - @rolexjs/local-platform@0.6.0
- - rolexjs@0.6.0
-
-## 0.5.0
-
-### Patch Changes
-
-- Updated dependencies [28b8b97]
- - @rolexjs/local-platform@0.5.0
- - rolexjs@0.5.0
-
-## 0.4.1
-
-### Patch Changes
-
-- @rolexjs/local-platform@0.4.1
-- rolexjs@0.4.1
-
-## 0.4.0
-
-### Patch Changes
-
-- @rolexjs/local-platform@0.4.0
-- rolexjs@0.4.0
-
-## 0.2.0
-
-### Minor Changes
-
-- fb2e6c6: feat: extract LocalPlatform, pure bootstrap, folded MCP tools
- - Extract `@rolexjs/local-platform` as independent package
- - Pure `bootstrap(platform)` with build-time seed inlining (zero fs dependency)
- - 女娲 born at society level, not hired into any organization
- - `Rolex.role(name)` for direct society-level role access
- - Fold MCP society/organization operations into 2 admin tools (nuwa-only)
- - Unified Gherkin rendering layer (`renderFeature`/`renderFeatures`)
- - `teach` moved from Organization to Society (Rolex) level
- - Default storage at `~/.rolex`
-
-### Patch Changes
-
-- Updated dependencies [fb2e6c6]
- - rolexjs@0.3.0
- - @rolexjs/local-platform@0.2.0
diff --git a/apps/cli/package.json b/apps/cli/package.json
deleted file mode 100644
index 602e53e..0000000
--- a/apps/cli/package.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "@rolexjs/cli",
- "version": "0.11.0",
- "type": "module",
- "bin": {
- "rolex": "./dist/index.js"
- },
- "files": [
- "dist"
- ],
- "scripts": {
- "build": "tsup",
- "dev": "bun src/index.ts",
- "clean": "rm -rf dist"
- },
- "dependencies": {
- "citty": "^0.1.6",
- "consola": "^3.4.2",
- "rolexjs": "workspace:*",
- "@rolexjs/local-platform": "workspace:*"
- },
- "publishConfig": {
- "access": "public"
- }
-}
diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts
deleted file mode 100644
index 580287a..0000000
--- a/apps/cli/src/index.ts
+++ /dev/null
@@ -1,725 +0,0 @@
-/**
- * RoleX CLI — thin wrapper over the Rolex API.
- *
- * Namespaces:
- * rolex individual — lifecycle (born, retire, die, rehire, teach, train)
- * rolex role — inner cycle (activate, focus, want..master, project)
- * rolex org — organization (found, establish..dismiss)
- * rolex resource — ResourceX (use, search, has, info, add, remove, push, pull)
- */
-
-import { readFileSync } from "node:fs";
-import { localPlatform } from "@rolexjs/local-platform";
-import { defineCommand, runMain } from "citty";
-import consola from "consola";
-import type { RolexResult } from "rolexjs";
-import { createRoleX, describe, hint } from "rolexjs";
-
-// ========== Setup ==========
-
-const rolex = createRoleX(localPlatform());
-
-// ========== Helpers ==========
-
-/** Build CLI arg definition for a concept's Gherkin content. */
-function contentArg(concept: string) {
- return {
- [concept]: {
- type: "string" as const,
- description: `Gherkin Feature source for the ${concept}`,
- },
- file: { type: "string" as const, alias: "f", description: "Path to .feature file" },
- };
-}
-
-/** Resolve content from either -- or --file. */
-function resolveContent(args: Record, concept: string): string | undefined {
- if (args.file) return readFileSync(args.file, "utf-8");
- const value = args[concept];
- if (typeof value === "string") return value.replace(/\\n/g, "\n");
- return undefined;
-}
-
-/** Resolve content, throw if missing. */
-function requireContent(args: Record, concept: string): string {
- const content = resolveContent(args, concept);
- if (!content) throw new Error(`Either --${concept} or --file is required.`);
- return content;
-}
-
-function output(result: RolexResult, name: string) {
- consola.success(describe(result.process, name, result.state));
- if (result.state.ref) consola.info(`ref: ${result.state.ref}`);
- if (result.state.id) consola.info(`id: ${result.state.id}`);
- consola.info(hint(result.process));
-}
-
-function requireResource() {
- if (!rolex.resource)
- throw new Error("ResourceX is not available. Check your platform configuration.");
- return rolex.resource;
-}
-
-// ========== Individual — lifecycle ==========
-
-const born = defineCommand({
- meta: { name: "born", description: "Born an individual into society" },
- args: {
- ...contentArg("individual"),
- id: { type: "string" as const, description: "User-facing identifier (kebab-case)" },
- alias: { type: "string" as const, description: "Comma-separated aliases" },
- },
- run({ args }) {
- const aliasList = args.alias ? args.alias.split(",").map((a: string) => a.trim()) : undefined;
- const result = rolex.individual.born(resolveContent(args, "individual"), args.id, aliasList);
- output(result, args.id ?? result.state.name);
- },
-});
-
-const retire = defineCommand({
- meta: { name: "retire", description: "Retire an individual (can rehire later)" },
- args: {
- individual: { type: "positional" as const, description: "Individual id", required: true },
- },
- run({ args }) {
- output(rolex.individual.retire(args.individual), args.individual);
- },
-});
-
-const die_ = defineCommand({
- meta: { name: "die", description: "An individual dies (permanent)" },
- args: {
- individual: { type: "positional" as const, description: "Individual id", required: true },
- },
- run({ args }) {
- output(rolex.individual.die(args.individual), args.individual);
- },
-});
-
-const rehire = defineCommand({
- meta: { name: "rehire", description: "Rehire a retired individual from past" },
- args: {
- pastNode: { type: "positional" as const, description: "Past node id", required: true },
- },
- run({ args }) {
- output(rolex.individual.rehire(args.pastNode), args.pastNode);
- },
-});
-
-const teach = defineCommand({
- meta: {
- name: "teach",
- description: "Inject a principle directly into an individual's knowledge",
- },
- args: {
- individual: { type: "positional" as const, description: "Individual id", required: true },
- ...contentArg("principle"),
- id: { type: "string" as const, description: "Principle id (keywords joined by hyphens)" },
- },
- run({ args }) {
- const result = rolex.individual.teach(
- args.individual,
- requireContent(args, "principle"),
- args.id
- );
- output(result, args.id ?? result.state.name);
- },
-});
-
-const train = defineCommand({
- meta: {
- name: "train",
- description: "Inject a procedure (skill) directly into an individual's knowledge",
- },
- args: {
- individual: { type: "positional" as const, description: "Individual id", required: true },
- ...contentArg("procedure"),
- id: { type: "string" as const, description: "Procedure id (keywords joined by hyphens)" },
- },
- run({ args }) {
- const result = rolex.individual.train(
- args.individual,
- requireContent(args, "procedure"),
- args.id
- );
- output(result, args.id ?? result.state.name);
- },
-});
-
-const individual = defineCommand({
- meta: { name: "individual", description: "Individual lifecycle management" },
- subCommands: {
- born,
- retire,
- die: die_,
- rehire,
- teach,
- train,
- },
-});
-
-// ========== Role — inner cycle ==========
-
-const activate = defineCommand({
- meta: { name: "activate", description: "Activate a role (project individual state)" },
- args: {
- individual: { type: "positional" as const, description: "Individual id", required: true },
- },
- async run({ args }) {
- output(await rolex.role.activate(args.individual), args.individual);
- },
-});
-
-const focus = defineCommand({
- meta: { name: "focus", description: "View or switch focused goal" },
- args: {
- goal: { type: "positional" as const, description: "Goal id", required: true },
- },
- run({ args }) {
- output(rolex.role.focus(args.goal), args.goal);
- },
-});
-
-const want = defineCommand({
- meta: { name: "want", description: "Declare a new goal" },
- args: {
- individual: { type: "positional" as const, description: "Individual id", required: true },
- ...contentArg("goal"),
- id: { type: "string" as const, description: "Goal id for reference" },
- alias: { type: "string" as const, description: "Comma-separated aliases" },
- },
- run({ args }) {
- const aliasList = args.alias ? args.alias.split(",").map((a: string) => a.trim()) : undefined;
- const result = rolex.role.want(
- args.individual,
- resolveContent(args, "goal"),
- args.id,
- aliasList
- );
- output(result, result.state.name);
- },
-});
-
-const plan = defineCommand({
- meta: { name: "plan", description: "Create a plan for a goal" },
- args: {
- goal: { type: "positional" as const, description: "Goal id", required: true },
- ...contentArg("plan"),
- id: { type: "string" as const, description: "Plan id (keywords joined by hyphens)" },
- },
- run({ args }) {
- const result = rolex.role.plan(args.goal, resolveContent(args, "plan"), args.id);
- output(result, result.state.name);
- },
-});
-
-const todo = defineCommand({
- meta: { name: "todo", description: "Add a task to a plan" },
- args: {
- plan: { type: "positional" as const, description: "Plan id", required: true },
- ...contentArg("task"),
- id: { type: "string" as const, description: "Task id for reference" },
- alias: { type: "string" as const, description: "Comma-separated aliases" },
- },
- run({ args }) {
- const aliasList = args.alias ? args.alias.split(",").map((a: string) => a.trim()) : undefined;
- const result = rolex.role.todo(args.plan, resolveContent(args, "task"), args.id, aliasList);
- output(result, result.state.name);
- },
-});
-
-const finish = defineCommand({
- meta: { name: "finish", description: "Finish a task — creates encounter" },
- args: {
- task: { type: "positional" as const, description: "Task id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- ...contentArg("encounter"),
- },
- run({ args }) {
- output(
- rolex.role.finish(args.task, args.individual, resolveContent(args, "encounter")),
- args.task
- );
- },
-});
-
-const complete = defineCommand({
- meta: { name: "complete", description: "Complete a plan — creates encounter" },
- args: {
- plan: { type: "positional" as const, description: "Plan id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- ...contentArg("encounter"),
- },
- run({ args }) {
- output(
- rolex.role.complete(args.plan, args.individual, resolveContent(args, "encounter")),
- args.plan
- );
- },
-});
-
-const abandon = defineCommand({
- meta: { name: "abandon", description: "Abandon a plan — creates encounter" },
- args: {
- plan: { type: "positional" as const, description: "Plan id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- ...contentArg("encounter"),
- },
- run({ args }) {
- output(
- rolex.role.abandon(args.plan, args.individual, resolveContent(args, "encounter")),
- args.plan
- );
- },
-});
-
-const reflect = defineCommand({
- meta: { name: "reflect", description: "Reflect on encounter — creates experience" },
- args: {
- encounter: { type: "positional" as const, description: "Encounter id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- ...contentArg("experience"),
- id: { type: "string" as const, description: "Experience id (keywords joined by hyphens)" },
- },
- run({ args }) {
- const result = rolex.role.reflect(
- args.encounter,
- args.individual,
- resolveContent(args, "experience"),
- args.id
- );
- output(result, result.state.name);
- },
-});
-
-const realize = defineCommand({
- meta: { name: "realize", description: "Distill experience into a principle" },
- args: {
- experience: { type: "positional" as const, description: "Experience id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- ...contentArg("principle"),
- id: { type: "string" as const, description: "Principle id (keywords joined by hyphens)" },
- },
- run({ args }) {
- const result = rolex.role.realize(
- args.experience,
- args.individual,
- resolveContent(args, "principle"),
- args.id
- );
- output(result, result.state.name);
- },
-});
-
-const master = defineCommand({
- meta: { name: "master", description: "Distill experience into a procedure" },
- args: {
- experience: { type: "positional" as const, description: "Experience id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- ...contentArg("procedure"),
- id: { type: "string" as const, description: "Procedure id (keywords joined by hyphens)" },
- },
- run({ args }) {
- const result = rolex.role.master(
- args.experience,
- args.individual,
- resolveContent(args, "procedure"),
- args.id
- );
- output(result, result.state.name);
- },
-});
-
-const use = defineCommand({
- meta: { name: "use", description: "Use a resource — interact with external resources" },
- args: {
- locator: {
- type: "positional" as const,
- description: "Resource locator (e.g. hello:1.0.0)",
- required: true,
- },
- },
- async run({ args }) {
- const result = await rolex.role.use(args.locator);
- if (typeof result === "string") {
- console.log(result);
- } else if (result instanceof Uint8Array) {
- process.stdout.write(result);
- } else {
- console.log(JSON.stringify(result, null, 2));
- }
- },
-});
-
-const role = defineCommand({
- meta: { name: "role", description: "Role inner cycle — execution + cognition" },
- subCommands: {
- activate,
- focus,
- want,
- plan,
- todo,
- finish,
- complete,
- abandon,
- reflect,
- realize,
- master,
- use,
- },
-});
-
-// ========== Org — organization management ==========
-
-const found = defineCommand({
- meta: { name: "found", description: "Found an organization" },
- args: {
- ...contentArg("organization"),
- id: { type: "string" as const, description: "User-facing identifier (kebab-case)" },
- alias: { type: "string" as const, description: "Comma-separated aliases" },
- },
- run({ args }) {
- const aliasList = args.alias ? args.alias.split(",").map((a: string) => a.trim()) : undefined;
- const result = rolex.org.found(resolveContent(args, "organization"), args.id, aliasList);
- output(result, args.id ?? result.state.name);
- },
-});
-
-const establish = defineCommand({
- meta: { name: "establish", description: "Establish a position within an organization" },
- args: {
- org: { type: "positional" as const, description: "Organization id", required: true },
- ...contentArg("position"),
- id: { type: "string" as const, description: "User-facing identifier (kebab-case)" },
- alias: { type: "string" as const, description: "Comma-separated aliases" },
- },
- run({ args }) {
- const aliasList = args.alias ? args.alias.split(",").map((a: string) => a.trim()) : undefined;
- const result = rolex.org.establish(
- args.org,
- resolveContent(args, "position"),
- args.id,
- aliasList
- );
- output(result, args.id ?? result.state.name);
- },
-});
-
-const charter = defineCommand({
- meta: { name: "charter", description: "Define the charter for an organization" },
- args: {
- org: { type: "positional" as const, description: "Organization id", required: true },
- ...contentArg("charter"),
- },
- run({ args }) {
- output(rolex.org.charter(args.org, requireContent(args, "charter")), args.org);
- },
-});
-
-const charge = defineCommand({
- meta: { name: "charge", description: "Add a duty to a position" },
- args: {
- position: { type: "positional" as const, description: "Position id", required: true },
- ...contentArg("duty"),
- id: { type: "string" as const, description: "Duty id (keywords joined by hyphens)" },
- },
- run({ args }) {
- const result = rolex.org.charge(args.position, requireContent(args, "duty"), args.id);
- output(result, result.state.name);
- },
-});
-
-const dissolve = defineCommand({
- meta: { name: "dissolve", description: "Dissolve an organization" },
- args: {
- org: { type: "positional" as const, description: "Organization id", required: true },
- },
- run({ args }) {
- output(rolex.org.dissolve(args.org), args.org);
- },
-});
-
-const abolish = defineCommand({
- meta: { name: "abolish", description: "Abolish a position" },
- args: {
- position: { type: "positional" as const, description: "Position id", required: true },
- },
- run({ args }) {
- output(rolex.org.abolish(args.position), args.position);
- },
-});
-
-const hire = defineCommand({
- meta: { name: "hire", description: "Hire an individual into an organization" },
- args: {
- org: { type: "positional" as const, description: "Organization id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- },
- run({ args }) {
- output(rolex.org.hire(args.org, args.individual), args.org);
- },
-});
-
-const fire = defineCommand({
- meta: { name: "fire", description: "Fire an individual from an organization" },
- args: {
- org: { type: "positional" as const, description: "Organization id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- },
- run({ args }) {
- output(rolex.org.fire(args.org, args.individual), args.org);
- },
-});
-
-const appoint = defineCommand({
- meta: { name: "appoint", description: "Appoint an individual to a position" },
- args: {
- position: { type: "positional" as const, description: "Position id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- },
- run({ args }) {
- output(rolex.org.appoint(args.position, args.individual), args.position);
- },
-});
-
-const dismiss = defineCommand({
- meta: { name: "dismiss", description: "Dismiss an individual from a position" },
- args: {
- position: { type: "positional" as const, description: "Position id", required: true },
- individual: { type: "positional" as const, description: "Individual id", required: true },
- },
- run({ args }) {
- output(rolex.org.dismiss(args.position, args.individual), args.position);
- },
-});
-
-const org = defineCommand({
- meta: { name: "org", description: "Organization management" },
- subCommands: {
- found,
- establish,
- charter,
- charge,
- dissolve,
- abolish,
- hire,
- fire,
- appoint,
- dismiss,
- },
-});
-
-// ========== Resource — ResourceX ==========
-
-const rxSearch = defineCommand({
- meta: { name: "search", description: "Search available resources" },
- args: {
- query: { type: "positional" as const, description: "Search query" },
- },
- async run({ args }) {
- const rx = requireResource();
- const results = await rx.search(args.query);
- if (results.length === 0) {
- consola.info("No resources found.");
- } else {
- for (const locator of results) {
- console.log(locator);
- }
- }
- },
-});
-
-const rxHas = defineCommand({
- meta: { name: "has", description: "Check if a resource exists" },
- args: {
- locator: { type: "positional" as const, description: "Resource locator", required: true },
- },
- async run({ args }) {
- const rx = requireResource();
- const exists = await rx.has(args.locator);
- console.log(exists ? "yes" : "no");
- process.exitCode = exists ? 0 : 1;
- },
-});
-
-const rxInfo = defineCommand({
- meta: { name: "info", description: "Get resource metadata" },
- args: {
- locator: { type: "positional" as const, description: "Resource locator", required: true },
- },
- async run({ args }) {
- const rx = requireResource();
- const info = await rx.info(args.locator);
- console.log(JSON.stringify(info, null, 2));
- },
-});
-
-const rxAdd = defineCommand({
- meta: { name: "add", description: "Add a resource from a local directory" },
- args: {
- path: {
- type: "positional" as const,
- description: "Path to resource directory",
- required: true,
- },
- },
- async run({ args }) {
- const rx = requireResource();
- const resource = await rx.add(args.path);
- consola.success(`Added ${resource.locator}`);
- },
-});
-
-const rxRemove = defineCommand({
- meta: { name: "remove", description: "Remove a resource" },
- args: {
- locator: { type: "positional" as const, description: "Resource locator", required: true },
- },
- async run({ args }) {
- const rx = requireResource();
- await rx.remove(args.locator);
- consola.success(`Removed ${args.locator}`);
- },
-});
-
-const rxPush = defineCommand({
- meta: { name: "push", description: "Push a resource to the remote registry" },
- args: {
- locator: { type: "positional" as const, description: "Resource locator", required: true },
- registry: { type: "string" as const, description: "Registry URL (overrides default)" },
- },
- async run({ args }) {
- const rx = requireResource();
- const opts = args.registry ? { registry: args.registry } : undefined;
- await rx.push(args.locator, opts);
- consola.success(`Pushed ${args.locator}`);
- },
-});
-
-const rxPull = defineCommand({
- meta: { name: "pull", description: "Pull a resource from the remote registry" },
- args: {
- locator: { type: "positional" as const, description: "Resource locator", required: true },
- registry: { type: "string" as const, description: "Registry URL (overrides default)" },
- },
- async run({ args }) {
- const rx = requireResource();
- const opts = args.registry ? { registry: args.registry } : undefined;
- await rx.pull(args.locator, opts);
- consola.success(`Pulled ${args.locator}`);
- },
-});
-
-const rxRegistryAdd = defineCommand({
- meta: { name: "add", description: "Add a registry" },
- args: {
- name: { type: "positional" as const, description: "Registry name", required: true },
- url: { type: "positional" as const, description: "Registry URL", required: true },
- default: { type: "boolean" as const, description: "Set as default registry" },
- },
- run({ args }) {
- const rx = requireResource();
- rx.addRegistry(args.name, args.url, args.default);
- consola.success(`Added registry "${args.name}" (${args.url})`);
- },
-});
-
-const rxRegistryRemove = defineCommand({
- meta: { name: "remove", description: "Remove a registry" },
- args: {
- name: { type: "positional" as const, description: "Registry name", required: true },
- },
- run({ args }) {
- const rx = requireResource();
- rx.removeRegistry(args.name);
- consola.success(`Removed registry "${args.name}"`);
- },
-});
-
-const rxRegistryList = defineCommand({
- meta: { name: "list", description: "List configured registries" },
- run() {
- const rx = requireResource();
- const registries = rx.registries();
- if (registries.length === 0) {
- consola.info("No registries configured.");
- } else {
- for (const r of registries) {
- const marker = r.default ? " (default)" : "";
- console.log(`${r.name}: ${r.url}${marker}`);
- }
- }
- },
-});
-
-const rxRegistrySetDefault = defineCommand({
- meta: { name: "set-default", description: "Set a registry as default" },
- args: {
- name: { type: "positional" as const, description: "Registry name", required: true },
- },
- run({ args }) {
- const rx = requireResource();
- rx.setDefaultRegistry(args.name);
- consola.success(`Set "${args.name}" as default registry`);
- },
-});
-
-const rxRegistry = defineCommand({
- meta: { name: "registry", description: "Manage registries" },
- subCommands: {
- add: rxRegistryAdd,
- remove: rxRegistryRemove,
- list: rxRegistryList,
- "set-default": rxRegistrySetDefault,
- },
-});
-
-const resource = defineCommand({
- meta: { name: "resource", description: "Resource management (powered by ResourceX)" },
- subCommands: {
- search: rxSearch,
- has: rxHas,
- info: rxInfo,
- add: rxAdd,
- remove: rxRemove,
- push: rxPush,
- pull: rxPull,
- registry: rxRegistry,
- },
-});
-
-// ========== Prototype — register ResourceX source ==========
-
-const prototype = defineCommand({
- meta: { name: "prototype", description: "Register a ResourceX source as a prototype" },
- args: {
- source: {
- type: "positional" as const,
- description: "ResourceX source — local path or locator",
- required: true,
- },
- },
- async run({ args }) {
- const result = await rolex.prototype(args.source);
- output(result, result.state.id ?? args.source);
- },
-});
-
-// ========== Main ==========
-
-const main = defineCommand({
- meta: {
- name: "rolex",
- version: "0.11.0",
- description: "RoleX — AI Agent Role Management CLI",
- },
- subCommands: {
- individual,
- role,
- org,
- resource,
- prototype,
- },
-});
-
-runMain(main);
diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json
deleted file mode 100644
index e4afb9a..0000000
--- a/apps/cli/tsconfig.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "extends": "../../tsconfig.base.json",
- "compilerOptions": {
- "outDir": "./dist",
- "rootDir": "./src",
- "declaration": true,
- "declarationMap": true,
- "baseUrl": ".",
- "paths": {
- "~/*": ["./src/*"]
- }
- },
- "include": ["src/**/*"],
- "exclude": ["node_modules", "dist", "**/*.test.ts"]
-}
diff --git a/apps/mcp-server/package.json b/apps/mcp-server/package.json
index dd32be2..6889e00 100644
--- a/apps/mcp-server/package.json
+++ b/apps/mcp-server/package.json
@@ -43,10 +43,13 @@
"dependencies": {
"rolexjs": "workspace:*",
"@rolexjs/local-platform": "workspace:*",
+ "@rolexjs/genesis": "workspace:*",
"fastmcp": "^3.0.0",
"zod": "^3.25.0"
},
- "devDependencies": {},
+ "devDependencies": {
+ "@modelcontextprotocol/sdk": "^1.27.1"
+ },
"publishConfig": {
"access": "public"
}
diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts
index e6106e4..037351a 100644
--- a/apps/mcp-server/src/index.ts
+++ b/apps/mcp-server/src/index.ts
@@ -1,57 +1,36 @@
/**
* @rolexjs/mcp-server — individual-level MCP tools.
*
- * Thin wrapper around the Rolex API (which accepts string ids).
- * McpState holds session context: activeRoleId, focusedGoalId, encounter/experience ids.
- *
- * Tools:
- * activate — activate a role
- * focus — view / switch focused goal
- * want — declare a goal
- * plan — plan for focused goal
- * todo — add task to focused plan
- * finish — finish a task → encounter
- * complete — complete focused plan → encounter
- * abandon — abandon focused plan → encounter
- * reflect — encounter(s) → experience
- * realize — experience(s) → principle
- * master — experience(s) → procedure
- * forget — remove a node from the individual
- * skill — load full skill content by locator
+ * Pure pass-through: all rendering happens in rolexjs.
+ * MCP only translates protocol calls to API calls.
*/
import { localPlatform } from "@rolexjs/local-platform";
import { FastMCP } from "fastmcp";
import { createRoleX, detail } from "rolexjs";
+
import { z } from "zod";
import { instructions } from "./instructions.js";
-import { render } from "./render.js";
import { McpState } from "./state.js";
// ========== Setup ==========
-const rolex = createRoleX(localPlatform());
-const state = new McpState(rolex);
+const rolex = await createRoleX(
+ localPlatform({
+ bootstrap: ["npm:@rolexjs/genesis"],
+ })
+);
+await rolex.genesis();
+const state = new McpState();
// ========== Server ==========
const server = new FastMCP({
name: "rolex",
- version: "0.11.0",
+ version: "0.12.0",
instructions,
});
-// ========== Helpers ==========
-
-function fmt(process: string, label: string, result: { state: any; process: string }) {
- return render({
- process,
- name: label,
- result,
- cognitiveHint: state.cognitiveHint(process),
- });
-}
-
// ========== Tools: Role ==========
server.addTool({
@@ -61,15 +40,16 @@ server.addTool({
roleId: z.string().describe("Role name to activate"),
}),
execute: async ({ roleId }) => {
- if (!state.findIndividual(roleId)) {
- // Auto-born if not found
- rolex.individual.born(undefined, roleId);
+ try {
+ const role = await rolex.activate(roleId);
+ state.role = role;
+ return await role.project();
+ } catch {
+ const census = await rolex.direct("!census.list");
+ throw new Error(
+ `"${roleId}" not found. Available:\n\n${census}\n\nTry again with the correct id or alias.`
+ );
}
- state.reset();
- state.activeRoleId = roleId;
- const result = await rolex.role.activate(roleId);
- state.cacheFromActivation(result.state);
- return fmt("activate", roleId, result);
},
});
@@ -80,13 +60,7 @@ server.addTool({
id: z.string().optional().describe("Goal id to switch to. Omit to view current."),
}),
execute: async ({ id }) => {
- if (id) {
- state.focusedGoalId = id;
- state.focusedPlanId = null;
- }
- const goalId = state.requireGoalId();
- const result = rolex.role.focus(goalId);
- return fmt("focus", id ?? "current goal", result);
+ return await state.requireRole().focus(id);
},
});
@@ -100,11 +74,7 @@ server.addTool({
goal: z.string().describe("Gherkin Feature source describing the goal"),
}),
execute: async ({ id, goal }) => {
- const roleId = state.requireRoleId();
- const result = rolex.role.want(roleId, goal, id);
- state.focusedGoalId = id;
- state.focusedPlanId = null;
- return fmt("want", id, result);
+ return await state.requireRole().want(goal, id);
},
});
@@ -114,12 +84,17 @@ server.addTool({
parameters: z.object({
id: z.string().describe("Plan id — keywords from the plan content joined by hyphens"),
plan: z.string().describe("Gherkin Feature source describing the plan"),
+ after: z
+ .string()
+ .optional()
+ .describe("Plan id this plan follows (sequential/phase relationship)"),
+ fallback: z
+ .string()
+ .optional()
+ .describe("Plan id this plan is a backup for (alternative/strategy relationship)"),
}),
- execute: async ({ id, plan }) => {
- const goalId = state.requireGoalId();
- const result = rolex.role.plan(goalId, plan, id);
- state.focusedPlanId = id;
- return fmt("plan", id, result);
+ execute: async ({ id, plan, after, fallback }) => {
+ return await state.requireRole().plan(plan, id, after, fallback);
},
});
@@ -131,9 +106,7 @@ server.addTool({
task: z.string().describe("Gherkin Feature source describing the task"),
}),
execute: async ({ id, task }) => {
- const planId = state.requirePlanId();
- const result = rolex.role.todo(planId, task, id);
- return fmt("todo", id, result);
+ return await state.requireRole().todo(task, id);
},
});
@@ -145,11 +118,7 @@ server.addTool({
encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"),
}),
execute: async ({ id, encounter }) => {
- const roleId = state.requireRoleId();
- const result = rolex.role.finish(id, roleId, encounter);
- const encId = result.state.id ?? id;
- state.addEncounter(encId);
- return fmt("finish", id, result);
+ return await state.requireRole().finish(id, encounter);
},
});
@@ -161,13 +130,7 @@ server.addTool({
encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"),
}),
execute: async ({ id, encounter }) => {
- const roleId = state.requireRoleId();
- const planId = id ?? state.requirePlanId();
- const result = rolex.role.complete(planId, roleId, encounter);
- const encId = result.state.id ?? planId;
- state.addEncounter(encId);
- if (state.focusedPlanId === planId) state.focusedPlanId = null;
- return fmt("complete", planId, result);
+ return await state.requireRole().complete(id, encounter);
},
});
@@ -179,13 +142,7 @@ server.addTool({
encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"),
}),
execute: async ({ id, encounter }) => {
- const roleId = state.requireRoleId();
- const planId = id ?? state.requirePlanId();
- const result = rolex.role.abandon(planId, roleId, encounter);
- const encId = result.state.id ?? planId;
- state.addEncounter(encId);
- if (state.focusedPlanId === planId) state.focusedPlanId = null;
- return fmt("abandon", planId, result);
+ return await state.requireRole().abandon(id, encounter);
},
});
@@ -202,12 +159,7 @@ server.addTool({
experience: z.string().optional().describe("Gherkin Feature source for the experience"),
}),
execute: async ({ ids, id, experience }) => {
- state.requireEncounterIds(ids);
- const roleId = state.requireRoleId();
- const result = rolex.role.reflect(ids[0], roleId, experience, id);
- state.consumeEncounters(ids);
- state.addExperience(id);
- return fmt("reflect", id, result);
+ return await state.requireRole().reflect(ids, experience, id);
},
});
@@ -220,11 +172,7 @@ server.addTool({
principle: z.string().optional().describe("Gherkin Feature source for the principle"),
}),
execute: async ({ ids, id, principle }) => {
- state.requireExperienceIds(ids);
- const roleId = state.requireRoleId();
- const result = rolex.role.realize(ids[0], roleId, principle, id);
- state.consumeExperiences(ids);
- return fmt("realize", id, result);
+ return await state.requireRole().realize(ids, principle, id);
},
});
@@ -232,16 +180,12 @@ server.addTool({
name: "master",
description: detail("master"),
parameters: z.object({
- ids: z.array(z.string()).describe("Experience ids to distill into a procedure"),
+ ids: z.array(z.string()).optional().describe("Experience ids to distill into a procedure"),
id: z.string().describe("Procedure id — keywords from the procedure content joined by hyphens"),
- procedure: z.string().optional().describe("Gherkin Feature source for the procedure"),
+ procedure: z.string().describe("Gherkin Feature source for the procedure"),
}),
execute: async ({ ids, id, procedure }) => {
- state.requireExperienceIds(ids);
- const roleId = state.requireRoleId();
- const result = rolex.role.master(ids[0], roleId, procedure, id);
- state.consumeExperiences(ids);
- return fmt("master", id, result);
+ return await state.requireRole().master(procedure, id, ids);
},
});
@@ -256,9 +200,7 @@ server.addTool({
.describe("Id of the node to remove (principle, procedure, experience, encounter, etc.)"),
}),
execute: async ({ id }) => {
- const roleId = state.requireRoleId();
- const result = await rolex.role.forget(id, roleId);
- return fmt("forget", id, result);
+ return await state.requireRole().forget(id);
},
});
@@ -273,8 +215,49 @@ server.addTool({
.describe("ResourceX locator for the skill (e.g. deepractice/role-management)"),
}),
execute: async ({ locator }) => {
- const content = await rolex.role.skill(locator);
- return content;
+ return await state.requireRole().skill(locator);
+ },
+});
+
+// ========== Tools: Use ==========
+
+server.addTool({
+ name: "use",
+ description: detail("use"),
+ parameters: z.object({
+ locator: z
+ .string()
+ .describe(
+ "Locator string. !namespace.method for RoleX commands, or a ResourceX locator for resources"
+ ),
+ args: z.record(z.unknown()).optional().describe("Named arguments for the command or resource"),
+ }),
+ execute: async ({ locator, args }) => {
+ const result = await state.requireRole().use(locator, args);
+ if (result == null) return `${locator} done.`;
+ if (typeof result === "string") return result;
+ return JSON.stringify(result, null, 2);
+ },
+});
+
+// ========== Tools: Direct ==========
+
+server.addTool({
+ name: "direct",
+ description: detail("direct"),
+ parameters: z.object({
+ locator: z
+ .string()
+ .describe(
+ "Locator string. !namespace.method for RoleX commands, or a ResourceX locator for resources"
+ ),
+ args: z.record(z.unknown()).optional().describe("Named arguments for the command or resource"),
+ }),
+ execute: async ({ locator, args }) => {
+ const result = await rolex.direct(locator, args);
+ if (result == null) return `${locator} done.`;
+ if (typeof result === "string") return result;
+ return JSON.stringify(result, null, 2);
},
});
diff --git a/apps/mcp-server/src/instructions.ts b/apps/mcp-server/src/instructions.ts
index c51332a..0c02627 100644
--- a/apps/mcp-server/src/instructions.ts
+++ b/apps/mcp-server/src/instructions.ts
@@ -6,14 +6,4 @@
*/
import { world } from "rolexjs";
-export const instructions = [
- world["cognitive-priority"],
- world["role-identity"],
- world.execution,
- world.cognition,
- world.memory,
- world.gherkin,
- world.communication,
- world["skill-system"],
- world["state-origin"],
-].join("\n\n");
+export const instructions = Object.values(world).join("\n\n");
diff --git a/apps/mcp-server/src/render.ts b/apps/mcp-server/src/render.ts
deleted file mode 100644
index c465725..0000000
--- a/apps/mcp-server/src/render.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Render — 3-layer output for MCP tool results.
- *
- * Layer 1: Status — what just happened (describe)
- * Layer 2: Hint — what to do next (hint)
- * Layer 3: Projection — full state tree as markdown (renderState)
- *
- * MCP and CLI share describe() + hint() + renderState() from rolexjs.
- * Relations are rendered per-node via bidirectional links — no separate layer needed.
- */
-import type { RolexResult } from "rolexjs";
-import { describe, hint, renderState } from "rolexjs";
-
-// ================================================================
-// Public API
-// ================================================================
-
-export interface RenderOptions {
- /** The process that was executed. */
- process: string;
- /** Display name for the primary node. */
- name: string;
- /** Result from the Rolex API. */
- result: RolexResult;
- /** AI cognitive hint — first-person, state-aware self-direction cue. */
- cognitiveHint?: string | null;
-}
-
-/** Render a full 3-layer output string. */
-export function render(opts: RenderOptions): string {
- const { process, name, result, cognitiveHint } = opts;
- const lines: string[] = [];
-
- // Layer 1: Status
- lines.push(describe(process, name, result.state));
-
- // Layer 2: Hint (static) + Cognitive hint (state-aware)
- lines.push(hint(process));
- if (cognitiveHint) {
- lines.push(`I → ${cognitiveHint}`);
- }
-
- // Layer 3: Projection — generic markdown rendering of the full state tree
- lines.push("");
- lines.push(renderState(result.state));
-
- return lines.join("\n");
-}
diff --git a/apps/mcp-server/src/state.ts b/apps/mcp-server/src/state.ts
index a0ce6c5..8bdde73 100644
--- a/apps/mcp-server/src/state.ts
+++ b/apps/mcp-server/src/state.ts
@@ -1,184 +1,16 @@
/**
- * McpState — stateful session for the MCP server.
+ * McpState — thin session holder for the MCP server.
*
- * Holds what the stateless Rolex API does not:
- * - activeRoleId (which individual is "me")
- * - focusedGoalId / focusedPlanId (execution context)
- * - encounter / experience id sets (for selective cognition)
- *
- * Since the Rolex API now accepts string ids directly,
- * McpState only stores ids — no Structure references.
+ * Holds the active Role handle. All business logic (state tracking,
+ * cognitive hints, encounter/experience registries) lives in Role + RoleContext.
*/
-import type { Rolex, State } from "rolexjs";
+import type { Role } from "rolexjs";
export class McpState {
- activeRoleId: string | null = null;
- focusedGoalId: string | null = null;
- focusedPlanId: string | null = null;
-
- private encounterIds = new Set();
- private experienceIds = new Set();
-
- constructor(readonly rolex: Rolex) {}
-
- // ================================================================
- // Requirements — throw if missing
- // ================================================================
-
- requireRoleId(): string {
- if (!this.activeRoleId) throw new Error("No active role. Call activate first.");
- return this.activeRoleId;
- }
-
- requireGoalId(): string {
- if (!this.focusedGoalId) throw new Error("No focused goal. Call want first.");
- return this.focusedGoalId;
- }
-
- requirePlanId(): string {
- if (!this.focusedPlanId) throw new Error("No focused plan. Call plan first.");
- return this.focusedPlanId;
- }
-
- // ================================================================
- // Cognition registries — encounter / experience ids
- // ================================================================
-
- addEncounter(id: string) {
- this.encounterIds.add(id);
- }
-
- requireEncounterIds(ids: string[]) {
- for (const id of ids) {
- if (!this.encounterIds.has(id)) throw new Error(`Encounter not found: "${id}"`);
- }
- }
-
- consumeEncounters(ids: string[]) {
- for (const id of ids) {
- this.encounterIds.delete(id);
- }
- }
-
- addExperience(id: string) {
- this.experienceIds.add(id);
- }
-
- requireExperienceIds(ids: string[]) {
- for (const id of ids) {
- if (!this.experienceIds.has(id)) throw new Error(`Experience not found: "${id}"`);
- }
- }
-
- consumeExperiences(ids: string[]) {
- for (const id of ids) {
- this.experienceIds.delete(id);
- }
- }
-
- // ================================================================
- // Lookup
- // ================================================================
-
- findIndividual(roleId: string): boolean {
- return this.rolex.find(roleId) !== null;
- }
-
- // ================================================================
- // Activation helpers
- // ================================================================
-
- /** Reset all session state — called before rehydrating a new role. */
- reset() {
- this.activeRoleId = null;
- this.focusedGoalId = null;
- this.focusedPlanId = null;
- this.encounterIds.clear();
- this.experienceIds.clear();
- }
-
- /** Rehydrate ids from an activation projection. */
- cacheFromActivation(state: State) {
- this.rehydrate(state);
- }
-
- /** Walk the state tree and collect ids into the appropriate registries. */
- private rehydrate(node: State) {
- if (node.id) {
- switch (node.name) {
- case "goal":
- // Set focused goal to the first one found if none set
- if (!this.focusedGoalId) this.focusedGoalId = node.id;
- break;
- case "encounter":
- this.encounterIds.add(node.id);
- break;
- case "experience":
- this.experienceIds.add(node.id);
- break;
- }
- }
- for (const child of (node as State & { children?: readonly State[] }).children ?? []) {
- this.rehydrate(child);
- }
- }
-
- // ================================================================
- // Cognitive hints — state-aware AI self-direction cues
- // ================================================================
-
- /** First-person, state-aware hint for the AI after an operation. */
- cognitiveHint(process: string): string | null {
- switch (process) {
- case "activate":
- if (!this.focusedGoalId)
- return "I have no goal yet. I should call `want` to declare one, or `focus` to review existing goals.";
- return "I have an active goal. I should call `focus` to review progress, or `want` to declare a new goal.";
-
- case "focus":
- if (!this.focusedPlanId)
- return "I have a goal but no plan. I should call `plan` to design how to achieve it.";
- return "I have a plan. I should call `todo` to create tasks, or continue working.";
-
- case "want":
- return "Goal declared. I should call `plan` to design how to achieve it.";
-
- case "plan":
- return "Plan created. I should call `todo` to create concrete tasks.";
-
- case "todo":
- return "Task created. I can add more with `todo`, or start working and call `finish` when done.";
-
- case "finish": {
- const encCount = this.encounterIds.size;
- if (encCount > 0 && !this.focusedGoalId)
- return `Task finished. No more goals — I have ${encCount} encounter(s) to choose from for \`reflect\`, or \`want\` a new goal.`;
- return "Task finished. I should continue with remaining tasks, or call `complete` when the plan is done.";
- }
-
- case "complete":
- case "abandon": {
- const encCount = this.encounterIds.size;
- if (encCount > 0)
- return `Plan closed. I have ${encCount} encounter(s) to choose from for \`reflect\`, or I can continue with other plans.`;
- return "Plan closed. I can create a new `plan`, or `focus` on another goal.";
- }
-
- case "reflect": {
- const expCount = this.experienceIds.size;
- if (expCount > 0)
- return `Experience gained. I can \`realize\` principles or \`master\` procedures — ${expCount} experience(s) available.`;
- return "Experience gained. I can `realize` a principle, `master` a procedure, or continue working.";
- }
-
- case "realize":
- return "Principle added to knowledge. I should continue working.";
-
- case "master":
- return "Procedure added to knowledge. I should continue working.";
+ role: Role | null = null;
- default:
- return null;
- }
+ requireRole(): Role {
+ if (!this.role) throw new Error("No active role. Call activate first.");
+ return this.role;
}
}
diff --git a/apps/mcp-server/tests/mcp.test.ts b/apps/mcp-server/tests/mcp.test.ts
index 1796a07..92ce447 100644
--- a/apps/mcp-server/tests/mcp.test.ts
+++ b/apps/mcp-server/tests/mcp.test.ts
@@ -1,181 +1,56 @@
/**
* MCP server integration tests.
*
- * Tests the stateful MCP layer (state + render) on top of stateless Rolex.
- * Does not test FastMCP transport — only the logic behind each tool.
+ * Tests the thin MCP layer (state holder) on top of Rolex.
+ * Business logic (RoleContext) is tested in rolexjs/tests/context.test.ts.
+ * Render is now in rolexjs — Role methods return rendered strings directly.
*/
import { beforeEach, describe, expect, it } from "bun:test";
import { localPlatform } from "@rolexjs/local-platform";
-import { createRoleX, type Rolex } from "rolexjs";
-import { render } from "../src/render.js";
+import type { OpResult } from "@rolexjs/prototype";
+import { createRoleX, type Rolex, render } from "rolexjs";
import { McpState } from "../src/state.js";
let rolex: Rolex;
let state: McpState;
-beforeEach(() => {
- rolex = createRoleX(localPlatform({ dataDir: null }));
- state = new McpState(rolex);
+beforeEach(async () => {
+ rolex = await createRoleX(localPlatform({ dataDir: null }));
+ state = new McpState();
});
// ================================================================
-// State: findIndividual
+// State: requireRole
// ================================================================
-describe("findIndividual", () => {
- it("finds an individual by id (case insensitive)", () => {
- rolex.born("Feature: Sean\n A backend architect", "sean");
- const found = state.findIndividual("sean");
- expect(found).not.toBeNull();
- expect(found!.name).toBe("individual");
- });
-
- it("returns null when not found", () => {
- expect(state.findIndividual("nobody")).toBeNull();
- });
-
- it("finds by alias", () => {
- rolex.born("Feature: I am Sean the Architect", "sean", ["Sean", "姜山"]);
- const found = state.findIndividual("姜山");
- expect(found).not.toBeNull();
- expect(found!.name).toBe("individual");
- });
-
- it("finds by alias case insensitive", () => {
- rolex.born("Feature: Sean", "sean", ["Sean"]);
- const found = state.findIndividual("SEAN");
- expect(found).not.toBeNull();
- });
-});
-
-// ================================================================
-// State: registry
-// ================================================================
-
-describe("registry", () => {
- it("register and resolve", () => {
- const result = rolex.born("Feature: Sean", "sean");
- state.register("sean", result.state);
- expect(state.resolve("sean")).toBe(result.state);
- });
-
- it("resolve throws on unknown id", () => {
- expect(() => state.resolve("unknown")).toThrow("Not found");
- });
-
- it("unregister removes entry", () => {
- const result = rolex.born("Feature: Sean", "sean");
- state.register("sean", result.state);
- state.unregister("sean");
- expect(() => state.resolve("sean")).toThrow("Not found");
- });
-});
-
-// ================================================================
-// State: requirements
-// ================================================================
-
-describe("requirements", () => {
- it("requireRole throws without active role", () => {
+describe("requireRole", () => {
+ it("throws without active role", () => {
expect(() => state.requireRole()).toThrow("No active role");
});
- it("requireGoal throws without focused goal", () => {
- expect(() => state.requireGoal()).toThrow("No focused goal");
- });
-
- it("requirePlan throws without focused plan", () => {
- expect(() => state.requirePlan()).toThrow("No focused plan");
- });
-
- it("requireKnowledge throws without knowledge ref", () => {
- expect(() => state.requireKnowledge()).toThrow("No knowledge branch");
+ it("returns role after activation", async () => {
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+ state.role = role;
+ expect(state.requireRole()).toBe(role);
+ expect(state.requireRole().roleId).toBe("sean");
});
});
// ================================================================
-// State: encounter / experience registries (named, selective)
-// ================================================================
-
-describe("cognition registries", () => {
- it("register and resolve encounters by id", () => {
- const r1 = rolex.born("Feature: A");
- const r2 = rolex.born("Feature: B");
- state.registerEncounter("task-a", r1.state);
- state.registerEncounter("task-b", r2.state);
- const resolved = state.resolveEncounters(["task-a", "task-b"]);
- expect(resolved).toHaveLength(2);
- expect(resolved[0]).toBe(r1.state);
- expect(resolved[1]).toBe(r2.state);
- });
-
- it("listEncounters returns all ids", () => {
- const r1 = rolex.born("Feature: A");
- const r2 = rolex.born("Feature: B");
- state.registerEncounter("task-a", r1.state);
- state.registerEncounter("task-b", r2.state);
- expect(state.listEncounters()).toEqual(["task-a", "task-b"]);
- });
-
- it("consumeEncounters removes selected entries", () => {
- const r1 = rolex.born("Feature: A");
- const r2 = rolex.born("Feature: B");
- state.registerEncounter("task-a", r1.state);
- state.registerEncounter("task-b", r2.state);
- state.consumeEncounters(["task-a"]);
- expect(state.listEncounters()).toEqual(["task-b"]);
- });
-
- it("resolveEncounters throws on unknown id", () => {
- expect(() => state.resolveEncounters(["unknown"])).toThrow("Encounter not found");
- });
-
- it("register and resolve experiences by id", () => {
- const r1 = rolex.born("Feature: A");
- state.registerExperience("exp-a", r1.state);
- const resolved = state.resolveExperiences(["exp-a"]);
- expect(resolved).toHaveLength(1);
- });
-
- it("consumeExperiences removes selected entries", () => {
- const r1 = rolex.born("Feature: A");
- const r2 = rolex.born("Feature: B");
- state.registerExperience("exp-a", r1.state);
- state.registerExperience("exp-b", r2.state);
- state.consumeExperiences(["exp-a"]);
- expect(state.listExperiences()).toEqual(["exp-b"]);
- });
-
- it("resolveExperiences throws on unknown id", () => {
- expect(() => state.resolveExperiences(["unknown"])).toThrow("Experience not found");
- });
-});
-
-// ================================================================
-// State: cacheFromActivation
-// ================================================================
-
-describe("cacheFromActivation", () => {
- it("caches knowledge ref", () => {
- const born = rolex.born("Feature: Sean", "sean");
- const activated = rolex.activate(born.state);
- state.cacheFromActivation(activated.state);
- expect(state.knowledgeRef).not.toBeNull();
- expect(state.knowledgeRef!.name).toBe("knowledge");
- });
-});
-
-// ================================================================
-// Render: 3-layer output
+// Render: 3-layer output (now in rolexjs)
// ================================================================
describe("render", () => {
- it("includes status + hint + projection", () => {
- const result = rolex.born("Feature: Sean", "sean");
+ it("includes status + hint + projection", async () => {
+ const result = await rolex.direct("!individual.born", {
+ content: "Feature: Sean",
+ id: "sean",
+ });
const output = render({
process: "born",
name: "Sean",
- result,
+ state: result.state,
});
// Layer 1: Status
expect(output).toContain('Individual "Sean" is born.');
@@ -184,123 +59,88 @@ describe("render", () => {
// Layer 3: Projection (generic markdown)
expect(output).toContain("# [individual]");
expect(output).toContain("## [identity]");
- expect(output).toContain("## [knowledge]");
});
- it("includes bidirectional links in projection", () => {
- // Born + found + hire → individual has "belong" link
- const sean = rolex.born("Feature: Sean", "sean");
- const org = rolex.found("Feature: Deepractice");
- rolex.hire(org.state, sean.state);
-
- const activated = rolex.activate(sean.state);
+ it("includes cognitive hint when provided", async () => {
+ const result = await rolex.direct("!individual.born", {
+ content: "Feature: Sean",
+ id: "sean",
+ });
const output = render({
- process: "activate",
+ process: "born",
name: "Sean",
- result: activated,
+ state: result.state,
+ cognitiveHint: "I have no goal yet. Declare one with want.",
});
- // Individual should have belong → organization via bidirectional link
- expect(output).toContain("belong");
- expect(output).toContain("Deepractice");
+ expect(output).toContain("I →");
+ expect(output).toContain("I have no goal yet");
});
- it("includes appointment relation in projection", () => {
- const sean = rolex.born("Feature: Sean", "sean");
- const org = rolex.found("Feature: Deepractice");
- const pos = rolex.establish(org.state, "Feature: Architect");
- rolex.hire(org.state, sean.state);
- rolex.appoint(pos.state, sean.state);
+ it("Role methods return rendered 3-layer output", async () => {
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
- const activated = rolex.activate(sean.state);
- const output = render({
- process: "activate",
- name: "Sean",
- result: activated,
- });
- // Individual should have serve → position via bidirectional link
- expect(output).toContain("serve");
- expect(output).toContain("Architect");
+ const output = await role.want("Feature: Test", "test-goal");
+ // Layer 1: Status
+ expect(output).toContain('Goal "test-goal" declared.');
+ // Layer 2: Hint
+ expect(output).toContain("Next:");
+ // Layer 3: Projection
+ expect(output).toContain("[goal]");
});
});
// ================================================================
-// Full flow: identity → want → plan → todo → finish → reflect
+// Full flow: MCP thin layer integration
// ================================================================
describe("full execution flow", () => {
- it("completes identity → want → plan → todo → finish → reflect → realize", () => {
- // Setup: born externally with id
- const _born = rolex.born("Feature: Sean", "sean");
-
- // 1. Identity (activate)
- const individual = state.findIndividual("sean");
- expect(individual).not.toBeNull();
- state.activeRole = individual!;
- const activated = rolex.activate(individual!);
- state.cacheFromActivation(activated.state);
-
- // 2. Want
- const goal = rolex.want(
- state.requireRole(),
- "Feature: Build Auth\n Scenario: JWT login",
- "build-auth"
- );
- state.register("build-auth", goal.state);
- state.focusedGoal = goal.state;
-
- // 3. Plan
- const plan = rolex.plan(state.requireGoal(), "Feature: Auth Plan\n Scenario: Phase 1");
- state.focusedPlan = plan.state;
-
- // 4. Todo
- const task = rolex.todo(
- state.requirePlan(),
- "Feature: Implement JWT\n Scenario: Token generation",
- "impl-jwt"
+ it("completes want → plan → todo → finish → reflect → realize through Role API", async () => {
+ // Born + activate
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+ state.role = role;
+
+ // Want
+ const goal = await role.want("Feature: Build Auth", "build-auth");
+ expect(role.ctx.focusedGoalId).toBe("build-auth");
+ expect(goal).toContain("I →");
+
+ // Plan
+ const plan = await role.plan("Feature: Auth Plan", "auth-plan");
+ expect(role.ctx.focusedPlanId).toBe("auth-plan");
+ expect(plan).toContain("I →");
+
+ // Todo
+ const task = await role.todo("Feature: Implement JWT", "impl-jwt");
+ expect(task).toContain("I →");
+
+ // Finish with encounter
+ const finished = await role.finish(
+ "impl-jwt",
+ "Feature: Implemented JWT\n Scenario: Token pattern\n Given JWT needed\n Then tokens work"
);
- state.register("impl-jwt", task.state);
-
- // 5. Finish → encounter (registered by id)
- const taskRef = state.resolve("impl-jwt");
- const finished = rolex.finish(
- taskRef,
- state.requireRole(),
- "Feature: Implemented JWT token generation\n Scenario: Discovered refresh token pattern\n Given JWT tokens expire\n When I implemented token generation\n Then I discovered refresh tokens are key"
+ expect(finished).toContain("[encounter]");
+ expect(role.ctx.encounterIds.has("impl-jwt-finished")).toBe(true);
+
+ // Reflect: encounter → experience
+ const reflected = await role.reflect(
+ ["impl-jwt-finished"],
+ "Feature: Token rotation insight\n Scenario: Refresh matters\n Given tokens expire\n Then refresh tokens are key",
+ "token-insight"
);
- state.registerEncounter("impl-jwt", finished.state);
- state.unregister("impl-jwt");
- expect(finished.state.name).toBe("encounter");
-
- // 6. Reflect → experience (selective: choose which encounters)
- const encIds = state.listEncounters();
- expect(encIds).toEqual(["impl-jwt"]);
- const encounters = state.resolveEncounters(["impl-jwt"]);
- const reflected = rolex.reflect(
- encounters[0],
- state.requireRole(),
- "Feature: Token rotation pattern\n Scenario: Refresh tokens prevent session loss\n Given tokens expire periodically\n When refresh tokens are used\n Then sessions persist without re-authentication"
- );
- state.consumeEncounters(["impl-jwt"]);
- state.registerExperience("impl-jwt", reflected.state);
- expect(reflected.state.name).toBe("experience");
-
- // 7. Realize → principle (selective: choose which experiences)
- const expIds = state.listExperiences();
- expect(expIds).toEqual(["impl-jwt"]);
- const experiences = state.resolveExperiences(["impl-jwt"]);
- const knowledge = state.requireKnowledge();
- const realized = rolex.realize(
- experiences[0],
- knowledge,
- "Feature: Always use refresh tokens\n Scenario: Short-lived tokens need rotation\n Given access tokens have limited lifetime\n When a system relies on long sessions\n Then refresh tokens must be implemented"
+ expect(reflected).toContain("[experience]");
+ expect(role.ctx.encounterIds.has("impl-jwt-finished")).toBe(false);
+ expect(role.ctx.experienceIds.has("token-insight")).toBe(true);
+
+ // Realize: experience → principle
+ const realized = await role.realize(
+ ["token-insight"],
+ "Feature: Always use refresh tokens\n Scenario: Short-lived tokens need rotation\n Given access tokens expire\n Then refresh tokens must exist",
+ "refresh-tokens"
);
- state.consumeExperiences(["impl-jwt"]);
- expect(realized.state.name).toBe("principle");
-
- // Verify the knowledge has the principle
- const knowledgeState = rolex.project(knowledge);
- const children = (knowledgeState as any).children ?? [];
- expect(children.some((c: any) => c.name === "principle")).toBe(true);
+ expect(realized).toContain("[principle]");
+ expect(role.ctx.experienceIds.has("token-insight")).toBe(false);
});
});
@@ -309,21 +149,20 @@ describe("full execution flow", () => {
// ================================================================
describe("focus", () => {
- it("switches focused goal by id", () => {
- const born = rolex.born("Feature: Sean", "sean");
- state.activeRole = born.state;
+ it("switches focused goal via Role", async () => {
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+ state.role = role;
- const goal1 = rolex.want(state.requireRole(), "Feature: Goal A", "goal-a");
- state.register("goal-a", goal1.state);
- state.focusedGoal = goal1.state;
+ await role.want("Feature: Goal A", "goal-a");
+ expect(role.ctx.focusedGoalId).toBe("goal-a");
- const goal2 = rolex.want(state.requireRole(), "Feature: Goal B", "goal-b");
- state.register("goal-b", goal2.state);
- state.focusedGoal = goal2.state;
+ await role.want("Feature: Goal B", "goal-b");
+ expect(role.ctx.focusedGoalId).toBe("goal-b");
// Switch back to goal A
- state.focusedGoal = state.resolve("goal-a");
- expect(state.requireGoal()).toBe(goal1.state);
+ await role.focus("goal-a");
+ expect(role.ctx.focusedGoalId).toBe("goal-a");
});
});
@@ -332,51 +171,41 @@ describe("focus", () => {
// ================================================================
describe("selective cognition", () => {
- it("can selectively reflect on chosen encounters", () => {
- const born = rolex.born("Feature: Sean", "sean");
- state.activeRole = born.state;
- state.cacheFromActivation(rolex.activate(born.state).state);
-
- // Create multiple encounters
- const goal = rolex.want(state.requireRole(), "Feature: Auth", "auth");
- state.focusedGoal = goal.state;
- const plan = rolex.plan(goal.state);
- state.focusedPlan = plan.state;
-
- const t1 = rolex.todo(plan.state, "Feature: Login", "login");
- state.register("login", t1.state);
- const t2 = rolex.todo(plan.state, "Feature: Signup", "signup");
- state.register("signup", t2.state);
-
- const enc1 = rolex.finish(
- t1.state,
- state.requireRole(),
- "Feature: Login implementation complete\n Scenario: Built login flow\n Given login was required\n When I implemented the login form\n Then users can authenticate"
+ it("ctx tracks multiple encounters, reflect consumes selectively", async () => {
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+ state.role = role;
+
+ // Create goal + plan + tasks
+ await role.want("Feature: Auth", "auth");
+ await role.plan("Feature: Plan", "plan1");
+ await role.todo("Feature: Login", "login");
+ await role.todo("Feature: Signup", "signup");
+
+ // Finish both with encounters
+ await role.finish(
+ "login",
+ "Feature: Login done\n Scenario: OK\n Given login\n Then success"
);
- state.registerEncounter("login", enc1.state);
- const enc2 = rolex.finish(
- t2.state,
- state.requireRole(),
- "Feature: Signup implementation complete\n Scenario: Built signup flow\n Given signup was required\n When I implemented the registration form\n Then users can create accounts"
+ await role.finish(
+ "signup",
+ "Feature: Signup done\n Scenario: OK\n Given signup\n Then success"
);
- state.registerEncounter("signup", enc2.state);
- // List encounters — should have both
- expect(state.listEncounters()).toEqual(["login", "signup"]);
+ expect(role.ctx.encounterIds.has("login-finished")).toBe(true);
+ expect(role.ctx.encounterIds.has("signup-finished")).toBe(true);
- // Reflect only on "login"
- const encounters = state.resolveEncounters(["login"]);
- const reflected = rolex.reflect(
- encounters[0],
- state.requireRole(),
- "Feature: Login flow design insight\n Scenario: Authentication requires multi-step validation\n Given a login form submits credentials\n When validation occurs server-side\n Then error feedback must be immediate and specific"
+ // Reflect only on "login-finished"
+ await role.reflect(
+ ["login-finished"],
+ "Feature: Login insight\n Scenario: OK\n Given practice\n Then understanding",
+ "login-insight"
);
- state.consumeEncounters(["login"]);
- state.registerExperience("login", reflected.state);
- // "signup" encounter still available
- expect(state.listEncounters()).toEqual(["signup"]);
- // "login" experience available
- expect(state.listExperiences()).toEqual(["login"]);
+ // "login-finished" consumed, "signup-finished" still available
+ expect(role.ctx.encounterIds.has("login-finished")).toBe(false);
+ expect(role.ctx.encounterIds.has("signup-finished")).toBe(true);
+ // Experience registered
+ expect(role.ctx.experienceIds.has("login-insight")).toBe(true);
});
});
diff --git a/bdd/features/cognition-loop.feature b/bdd/features/cognition-loop.feature
new file mode 100644
index 0000000..7c527bd
--- /dev/null
+++ b/bdd/features/cognition-loop.feature
@@ -0,0 +1,62 @@
+@cognition
+Feature: Cognition loop
+ reflect → realize/master: encounters become experience, then principles or procedures.
+
+ Background:
+ Given a fresh Rolex instance
+ And individual "sean" exists
+ And I activate role "sean"
+ And I want goal "auth" with "Feature: Auth"
+ And I plan "jwt" with "Feature: JWT"
+ And I todo "login" with "Feature: Login"
+
+ # ===== reflect =====
+
+ Scenario: Reflect consumes encounter and produces experience
+ Given I finish "login" with encounter "Feature: Login done\n Scenario: OK\n Given login\n Then success"
+ When I reflect on "login-finished" as "token-insight" with "Feature: Token insight\n Scenario: Learned\n Given tokens\n Then refresh matters"
+ Then the output should contain "[experience]"
+ And encounter "login-finished" should be consumed
+ And experience "token-insight" should be registered
+
+ Scenario: Reflect without encounter creates experience directly
+ When I reflect directly as "conv-insight" with "Feature: Conversation insight\n Scenario: Learned\n Given discussion\n Then clarity"
+ Then the output should contain "[experience]"
+ And experience "conv-insight" should be registered
+ And encounter count should be 0
+
+ # ===== realize =====
+
+ Scenario: Realize distills experience into a principle
+ Given I finish "login" with encounter "Feature: Login done\n Scenario: OK\n Given login\n Then success"
+ And I reflect on "login-finished" as "token-insight" with "Feature: Token insight\n Scenario: Learned\n Given tokens\n Then refresh"
+ When I realize from "token-insight" as "always-refresh" with "Feature: Always use refresh tokens\n Scenario: Rule\n Given tokens expire\n Then refresh tokens must exist"
+ Then the output should contain "[principle]"
+ And experience "token-insight" should be consumed
+
+ Scenario: Realize without experience creates principle directly
+ When I realize directly as "test-first" with "Feature: Always test first\n Scenario: Rule\n Given code changes\n Then write tests first"
+ Then the output should contain "[principle]"
+ And experience count should be 0
+
+ # ===== master =====
+
+ Scenario: Master creates a procedure from experience
+ Given I finish "login" with encounter "Feature: Login done\n Scenario: OK\n Given login\n Then success"
+ And I reflect on "login-finished" as "jwt-insight" with "Feature: JWT insight\n Scenario: Learned\n Given jwt\n Then patterns"
+ When I master from "jwt-insight" as "jwt-skill" with "Feature: JWT mastery\n Scenario: Skill\n Given auth needed\n Then apply JWT pattern"
+ Then the output should contain "[procedure]"
+ And experience "jwt-insight" should be consumed
+
+ Scenario: Master without experience creates procedure directly
+ When I master directly as "code-review" with "Feature: Code review\n Scenario: Skill\n Given PR submitted\n Then review systematically"
+ Then the output should contain "[procedure]"
+
+ # ===== forget =====
+
+ Scenario: Forget removes a knowledge node
+ Given I finish "login" with encounter "Feature: Login done\n Scenario: OK\n Given login\n Then success"
+ And I reflect on "login-finished" as "my-insight" with "Feature: Insight\n Scenario: Learned\n Given x\n Then y"
+ And I realize from "my-insight" as "my-principle" with "Feature: My principle\n Scenario: Rule\n Given a\n Then b"
+ When I forget "my-principle"
+ Then the output should contain "forgotten"
diff --git a/bdd/features/context-persistence.feature b/bdd/features/context-persistence.feature
new file mode 100644
index 0000000..3ce7f4a
--- /dev/null
+++ b/bdd/features/context-persistence.feature
@@ -0,0 +1,26 @@
+@context-persistence
+Feature: Activate focus restoration
+ activate should restore focus from persisted context,
+ but when persisted focus is null or invalid, it should
+ preserve the rehydrated default from the state tree.
+
+ Background:
+ Given a fresh Rolex instance
+
+ Scenario: Persisted null should not override rehydrated focus
+ Given an individual "sean" with goal "auth"
+ And persisted focusedGoalId is null
+ When I activate "sean"
+ Then focusedGoalId should be "auth"
+
+ Scenario: Valid persisted focus takes priority over rehydrate
+ Given an individual "sean" with goals "auth" and "deploy"
+ And persisted focusedGoalId is "deploy"
+ When I activate "sean"
+ Then focusedGoalId should be "deploy"
+
+ Scenario: No persisted context falls back to rehydrate default
+ Given an individual "sean" with goal "auth"
+ And no persisted context exists
+ When I activate "sean"
+ Then focusedGoalId should be "auth"
diff --git a/bdd/features/execution-loop.feature b/bdd/features/execution-loop.feature
new file mode 100644
index 0000000..f91d152
--- /dev/null
+++ b/bdd/features/execution-loop.feature
@@ -0,0 +1,82 @@
+@execution
+Feature: Execution loop
+ want → plan → todo → finish → complete/abandon through the Role API.
+ All operations return rendered 3-layer text (status + hint + projection).
+
+ Background:
+ Given a fresh Rolex instance
+ And individual "sean" exists
+ And I activate role "sean"
+
+ # ===== want =====
+
+ Scenario: Want declares a goal
+ When I want goal "build-auth" with "Feature: Build authentication"
+ Then the output should contain "Goal"
+ And the output should contain "[goal]"
+ And the output should contain "(build-auth)"
+ And focusedGoalId should be "build-auth"
+
+ # ===== plan =====
+
+ Scenario: Plan creates a plan under the focused goal
+ Given I want goal "auth" with "Feature: Auth"
+ When I plan "jwt-strategy" with "Feature: JWT strategy"
+ Then the output should contain "[plan]"
+ And the output should contain "(jwt-strategy)"
+ And focusedPlanId should be "jwt-strategy"
+
+ # ===== todo =====
+
+ Scenario: Todo adds a task to the focused plan
+ Given I want goal "auth" with "Feature: Auth"
+ And I plan "jwt" with "Feature: JWT"
+ When I todo "write-tests" with "Feature: Write tests"
+ Then the output should contain "[task]"
+ And the output should contain "(write-tests)"
+
+ # ===== finish =====
+
+ Scenario: Finish with encounter records an encounter
+ Given I want goal "auth" with "Feature: Auth"
+ And I plan "jwt" with "Feature: JWT"
+ And I todo "login" with "Feature: Login"
+ When I finish "login" with encounter "Feature: Login done\n Scenario: OK\n Given login\n Then success"
+ Then the output should contain "[encounter]"
+ And encounter "login-finished" should be registered
+
+ Scenario: Finish without encounter does not register
+ Given I want goal "auth" with "Feature: Auth"
+ And I plan "jwt" with "Feature: JWT"
+ And I todo "login" with "Feature: Login"
+ When I finish "login" without encounter
+ Then the output should contain "finished"
+ And encounter count should be 0
+
+ # ===== complete =====
+
+ Scenario: Complete closes a plan and records encounter
+ Given I want goal "auth" with "Feature: Auth"
+ And I plan "jwt" with "Feature: JWT"
+ When I complete plan "jwt" with encounter "Feature: JWT done\n Scenario: OK\n Given jwt\n Then done"
+ Then the output should contain "[encounter]"
+ And encounter "jwt-completed" should be registered
+ And focusedPlanId should be null
+
+ # ===== abandon =====
+
+ Scenario: Abandon drops a plan and records encounter
+ Given I want goal "auth" with "Feature: Auth"
+ And I plan "jwt" with "Feature: JWT"
+ When I abandon plan "jwt" with encounter "Feature: JWT failed\n Scenario: Not viable\n Given jwt complexity\n Then switched approach"
+ Then the output should contain "[encounter]"
+ And the output should contain "abandoned"
+
+ # ===== focus =====
+
+ Scenario: Focus switches between goals
+ Given I want goal "goal-a" with "Feature: Goal A"
+ And I want goal "goal-b" with "Feature: Goal B"
+ When I focus on "goal-a"
+ Then the output should contain "(goal-a)"
+ And focusedGoalId should be "goal-a"
diff --git a/bdd/features/governance-system.feature b/bdd/features/governance-system.feature
deleted file mode 100644
index cc5c359..0000000
--- a/bdd/features/governance-system.feature
+++ /dev/null
@@ -1,109 +0,0 @@
-@governance-system
-Feature: Governance System
- Internal org management — rule, establish, hire, appoint, fire, dismiss, abolish, assign, directory.
-
- Background:
- Given a fresh RoleX platform
- And org "deepractice" exists
- And role "alice" exists
- And role "bob" exists
-
- # ===== rule =====
-
- @rule
- Scenario: Rule writes a charter entry
- When I rule "deepractice" charter "code-standards" with:
- """
- Feature: Code Standards
- Scenario: TypeScript only
- Given all code must be TypeScript
- """
- Then charter "code-standards" should exist in "deepractice"
-
- # ===== establish =====
-
- @establish
- Scenario: Establish creates a position
- When I establish position "cto" in "deepractice" with:
- """
- Feature: CTO Duties
- Scenario: Technical leadership
- Given lead technical architecture
- """
- Then position "cto" should exist in "deepractice"
-
- # ===== assign =====
-
- @assign
- Scenario: Assign adds duty to position
- Given position "cto" exists in "deepractice"
- When I assign duty "architecture" to "deepractice/cto" with:
- """
- Feature: Architecture Duty
- Scenario: System design
- Given design system architecture
- """
- Then the result should contain "duty: architecture"
-
- # ===== hire =====
-
- @hire
- Scenario: Hire adds member to org
- When I hire "alice" into "deepractice"
- Then "alice" should be a member of "deepractice"
-
- @hire
- Scenario: Hire duplicate member fails
- Given "alice" is a member of "deepractice"
- When I hire "alice" into "deepractice"
- Then it should fail with "already"
-
- # ===== appoint =====
-
- @appoint
- Scenario: Appoint assigns role to position
- Given "alice" is a member of "deepractice"
- And position "cto" exists in "deepractice"
- When I appoint "alice" to "deepractice/cto"
- Then "alice" should be assigned to "deepractice/cto"
-
- # ===== directory =====
-
- @directory
- Scenario: Directory lists members and positions
- Given "alice" is a member of "deepractice"
- And "bob" is a member of "deepractice"
- And position "cto" exists in "deepractice"
- And "alice" is assigned to "deepractice/cto"
- When I query directory of "deepractice"
- Then the result should contain "alice"
- And the result should contain "bob"
- And the result should contain "cto"
-
- # ===== dismiss =====
-
- @dismiss
- Scenario: Dismiss removes role from position
- Given position "engineer" exists in "deepractice"
- And "bob" is assigned to "deepractice/engineer"
- When I dismiss "bob" from "deepractice/engineer"
- Then "bob" should not be assigned to "deepractice/engineer"
-
- # ===== fire =====
-
- @fire
- Scenario: Fire removes member and auto-dismisses from positions
- Given position "engineer" exists in "deepractice"
- And "bob" is a member of "deepractice"
- And "bob" is assigned to "deepractice/engineer"
- When I fire "bob" from "deepractice"
- Then "bob" should not be a member of "deepractice"
- And "bob" should not be assigned to "deepractice/engineer"
-
- # ===== abolish =====
-
- @abolish
- Scenario: Abolish removes position
- Given position "engineer" exists in "deepractice"
- When I abolish position "engineer" in "deepractice"
- Then position "engineer" should be shadowed in "deepractice"
diff --git a/bdd/features/individual-lifecycle.feature b/bdd/features/individual-lifecycle.feature
new file mode 100644
index 0000000..4d454b4
--- /dev/null
+++ b/bdd/features/individual-lifecycle.feature
@@ -0,0 +1,69 @@
+@individual
+Feature: Individual lifecycle
+ Birth, retirement, death, rehire, and knowledge injection.
+
+ Background:
+ Given a fresh Rolex instance
+
+ # ===== born =====
+
+ Scenario: Born creates an individual
+ When I direct "!individual.born" with:
+ | content | Feature: Sean |
+ | id | sean |
+ Then the result process should be "born"
+ And the result state name should be "individual"
+ And the result state id should be "sean"
+ And individual "sean" should exist
+
+ # ===== retire =====
+
+ Scenario: Retire archives an individual
+ Given individual "sean" exists
+ When I direct "!individual.retire" with:
+ | individual | sean |
+ Then the result process should be "retire"
+ And the result state name should be "past"
+
+ # ===== die =====
+
+ Scenario: Die permanently removes an individual
+ Given individual "sean" exists
+ When I direct "!individual.die" with:
+ | individual | sean |
+ Then the result process should be "die"
+ And the result state name should be "past"
+
+ # ===== rehire =====
+
+ Scenario: Rehire brings back a retired individual
+ Given individual "sean" exists
+ And individual "sean" is retired
+ When I direct "!individual.rehire" with:
+ | individual | sean |
+ Then the result process should be "rehire"
+ And the result state name should be "individual"
+
+ # ===== teach =====
+
+ Scenario: Teach injects a principle
+ Given individual "sean" exists
+ When I direct "!individual.teach" with:
+ | individual | sean |
+ | content | Feature: Always test first |
+ | id | test-first |
+ Then the result process should be "teach"
+ And the result state name should be "principle"
+ And the result state id should be "test-first"
+
+ # ===== train =====
+
+ Scenario: Train injects a procedure
+ Given individual "sean" exists
+ When I direct "!individual.train" with:
+ | individual | sean |
+ | content | Feature: Code review skill |
+ | id | code-review |
+ Then the result process should be "train"
+ And the result state name should be "procedure"
+ And the result state id should be "code-review"
diff --git a/bdd/features/individual-system.feature b/bdd/features/individual-system.feature
deleted file mode 100644
index c3c5b45..0000000
--- a/bdd/features/individual-system.feature
+++ /dev/null
@@ -1,278 +0,0 @@
-@individual-system
-Feature: Individual System
- First-person cognition — the AI agent's operating system.
- 14 processes: identity, focus, explore, want, design, todo, finish, achieve, abandon, forget, reflect, contemplate.
-
- Background:
- Given a fresh RoleX platform
- And role "sean" exists with persona "I am Sean"
- And role "sean" has knowledge.pattern "typescript"
- And role "sean" has knowledge.procedure "code-review"
-
- # ===== identity =====
-
- @identity
- Scenario: Identity loads role cognition
- When I call identity for "sean"
- Then the result should contain "identity loaded"
- And the result should contain "I am Sean"
- And the result should contain "typescript"
- And the result should contain "code-review"
-
- @identity
- Scenario: Identity for non-existent role fails
- When I call identity for "ghost"
- Then it should fail with "not found"
-
- # ===== want =====
-
- @want
- Scenario: Want declares a new goal
- Given I am "sean"
- When I want "build-mvp" with:
- """
- Feature: Build MVP
- Scenario: Ship v1
- Given I need a working product
- """
- Then goal "build-mvp" should exist
- And focus should be on "build-mvp"
-
- @want
- Scenario: Want auto-switches focus to new goal
- Given I am "sean"
- And I have goal "goal-a"
- When I want "goal-b" with:
- """
- Feature: Goal B
- Scenario: Second
- Given a second goal
- """
- Then focus should be on "goal-b"
-
- # ===== focus =====
-
- @focus
- Scenario: Focus shows current goal with full content
- Given I am "sean"
- And I have goal "build-mvp" with plan "mvp-plan" and task "setup-db"
- When I call focus
- Then the result should contain "Feature: build-mvp"
- And the result should contain "Feature: mvp-plan"
- And the result should contain "Feature: setup-db"
-
- @focus
- Scenario: Focus switches to another goal by name
- Given I am "sean"
- And I have goal "goal-a"
- And I have goal "goal-b"
- When I call focus with name "goal-a"
- Then the result should contain "goal: goal-a"
-
- @focus
- Scenario: Focus shows other goals list
- Given I am "sean"
- And I have goal "goal-a"
- And I have goal "goal-b"
- When I call focus
- Then the result should contain "Other goals: goal-a"
-
- # ===== explore =====
-
- @explore
- Scenario: Explore lists all roles and orgs
- Given I am "sean"
- And role "bob" exists
- And org "acme" exists
- When I call explore
- Then the result should contain "acme (org)"
- And the result should contain "bob"
- And the result should contain "sean"
-
- @explore
- Scenario: Explore by name shows role detail
- Given I am "sean"
- And role "bob" exists with persona "I am Bob"
- When I call explore with name "bob"
- Then the result should contain "I am Bob"
-
- @explore
- Scenario: Explore by name shows org detail
- Given I am "sean"
- And org "acme" exists with charter "We build AI tools"
- When I call explore with name "acme"
- Then the result should contain "We build AI tools"
-
- @explore
- Scenario: Explore non-existent entity fails
- Given I am "sean"
- When I call explore with name "nonexistent"
- Then it should fail with "not found"
-
- # ===== design =====
-
- @design
- Scenario: Design creates a plan under focused goal
- Given I am "sean"
- And I have goal "build-mvp"
- When I design "mvp-plan" with:
- """
- Feature: MVP Plan
- Scenario: Phase 1
- Given setup the database
- """
- Then plan "mvp-plan" should exist under goal "build-mvp"
-
- @design
- Scenario: Multiple plans — latest is focused
- Given I am "sean"
- And I have goal "build-mvp"
- When I design "plan-a" with:
- """
- Feature: Plan A
- Scenario: Approach A
- Given try approach A
- """
- And I design "plan-b" with:
- """
- Feature: Plan B
- Scenario: Approach B
- Given try approach B
- """
- Then focused plan should be "plan-b"
-
- # ===== todo =====
-
- @todo
- Scenario: Todo creates task under focused plan
- Given I am "sean"
- And I have goal "build-mvp" with plan "mvp-plan"
- When I todo "setup-db" with:
- """
- Feature: Setup DB
- Scenario: Create tables
- Given I run migrations
- """
- Then task "setup-db" should exist under plan "mvp-plan"
-
- @todo
- Scenario: Todo without plan fails
- Given I am "sean"
- And I have goal "build-mvp" without plan
- When I todo "orphan" with:
- """
- Feature: Orphan
- Scenario: Lost
- Given no plan
- """
- Then it should fail with "No plan"
-
- # ===== finish =====
-
- @finish
- Scenario: Finish marks task as done
- Given I am "sean"
- And I have goal "build-mvp" with plan "mvp-plan" and task "setup-db"
- When I finish "setup-db"
- Then task "setup-db" should be marked @done
-
- @finish
- Scenario: Finish with conclusion writes experience.conclusion
- Given I am "sean"
- And I have goal "build-mvp" with plan "mvp-plan" and task "setup-db"
- When I finish "setup-db" with conclusion:
- """
- Feature: DB Done
- Scenario: Result
- Given migrations ran successfully
- """
- Then task "setup-db" should be marked @done
- And conclusion "setup-db" should exist
-
- # ===== achieve =====
-
- @achieve
- Scenario: Achieve completes goal with experience
- Given I am "sean"
- And I have a finished goal "build-mvp"
- When I achieve with experience "mvp-learning":
- """
- Feature: MVP Learning
- Scenario: Key insight
- Given ship fast beats perfection
- """
- Then goal "build-mvp" should be marked @done
- And experience.insight "mvp-learning" should exist
- And conclusions should be consumed
-
- # ===== abandon =====
-
- @abandon
- Scenario: Abandon marks goal as abandoned
- Given I am "sean"
- And I have goal "bad-idea"
- When I abandon
- Then goal "bad-idea" should be marked @abandoned
-
- @abandon
- Scenario: Abandon with experience captures learning
- Given I am "sean"
- And I have goal "bad-idea"
- When I abandon with experience "failed-lesson":
- """
- Feature: Failed Lesson
- Scenario: What I learned
- Given validate assumptions first
- """
- Then goal "bad-idea" should be marked @abandoned
- And experience.insight "failed-lesson" should exist
-
- # ===== reflect =====
-
- @reflect
- Scenario: Reflect distills insights into knowledge.pattern
- Given I am "sean"
- And I have experience.insight "insight-a"
- And I have experience.insight "insight-b"
- When I reflect on "insight-a", "insight-b" to produce "principles" with:
- """
- Feature: Principles
- Scenario: Key lessons
- Given test early and ship fast
- """
- Then knowledge.pattern "principles" should exist
- And experience.insight "insight-a" should be consumed
- And experience.insight "insight-b" should be consumed
-
- # ===== contemplate =====
-
- @contemplate
- Scenario: Contemplate unifies patterns into theory
- Given I am "sean"
- And I have knowledge.pattern "pattern-a"
- And I have knowledge.pattern "pattern-b"
- When I contemplate on "pattern-a", "pattern-b" to produce "unified-theory" with:
- """
- Feature: Unified Theory
- Scenario: Big picture
- Given everything connects
- """
- Then knowledge.theory "unified-theory" should exist
- And knowledge.pattern "pattern-a" should still exist
- And knowledge.pattern "pattern-b" should still exist
-
- # ===== forget =====
-
- @forget
- Scenario: Forget removes knowledge.pattern
- Given I am "sean"
- And I have knowledge.pattern "outdated"
- When I forget knowledge.pattern "outdated"
- Then knowledge.pattern "outdated" should not exist
-
- @forget
- Scenario: Forget removes experience.insight
- Given I am "sean"
- And I have experience.insight "stale-insight"
- When I forget experience.insight "stale-insight"
- Then experience.insight "stale-insight" should not exist
diff --git a/bdd/features/org-system.feature b/bdd/features/org-system.feature
deleted file mode 100644
index c631658..0000000
--- a/bdd/features/org-system.feature
+++ /dev/null
@@ -1,39 +0,0 @@
-@org-system
-Feature: Organization System
- Organization lifecycle — found and dissolve.
-
- Background:
- Given a fresh RoleX platform
-
- # ===== found =====
-
- @found
- Scenario: Found creates an organization with charter
- When I found org "deepractice" with:
- """
- Feature: Deepractice Charter
- Scenario: Mission
- Given we build AI agent tools
- """
- Then org "deepractice" should exist
- And org "deepractice" should have charter containing "we build AI agent tools"
-
- @found
- Scenario: Found duplicate org fails
- Given org "acme" exists
- When I found org "acme" with:
- """
- Feature: Duplicate
- Scenario: Dup
- Given duplicate
- """
- Then it should fail with "already exist"
-
- # ===== dissolve =====
-
- @dissolve
- Scenario: Dissolve destroys organization and cascades
- Given org "acme" exists with position "engineer" and member "alice"
- When I dissolve org "acme"
- Then org "acme" should be shadowed
- And position "engineer" should be shadowed in "acme"
diff --git a/bdd/features/organization-lifecycle.feature b/bdd/features/organization-lifecycle.feature
new file mode 100644
index 0000000..61e9293
--- /dev/null
+++ b/bdd/features/organization-lifecycle.feature
@@ -0,0 +1,58 @@
+@organization
+Feature: Organization lifecycle
+ Found, charter, hire, fire, dissolve.
+
+ Background:
+ Given a fresh Rolex instance
+
+ # ===== found =====
+
+ Scenario: Found creates an organization
+ When I direct "!org.found" with:
+ | content | Feature: Deepractice |
+ | id | deepractice |
+ Then the result process should be "found"
+ And the result state name should be "organization"
+ And the result state id should be "deepractice"
+ And organization "deepractice" should exist
+
+ # ===== charter =====
+
+ Scenario: Charter defines organization mission
+ Given organization "deepractice" exists
+ When I direct "!org.charter" with:
+ | org | deepractice |
+ | content | Feature: Build AI tools |
+ | id | dp-charter |
+ Then the result process should be "charter"
+ And the result state name should be "charter"
+
+ # ===== hire =====
+
+ Scenario: Hire adds individual to organization
+ Given individual "sean" exists
+ And organization "deepractice" exists
+ When I direct "!org.hire" with:
+ | org | deepractice |
+ | individual | sean |
+ Then the result process should be "hire"
+
+ # ===== fire =====
+
+ Scenario: Fire removes individual from organization
+ Given individual "sean" exists
+ And organization "deepractice" exists
+ And "sean" is hired into "deepractice"
+ When I direct "!org.fire" with:
+ | org | deepractice |
+ | individual | sean |
+ Then the result process should be "fire"
+
+ # ===== dissolve =====
+
+ Scenario: Dissolve archives an organization
+ Given organization "deepractice" exists
+ When I direct "!org.dissolve" with:
+ | org | deepractice |
+ Then the result process should be "dissolve"
+ And the result state name should be "past"
diff --git a/bdd/features/position-lifecycle.feature b/bdd/features/position-lifecycle.feature
new file mode 100644
index 0000000..9302098
--- /dev/null
+++ b/bdd/features/position-lifecycle.feature
@@ -0,0 +1,71 @@
+@position
+Feature: Position lifecycle
+ Establish, charge duty, require skill, appoint, dismiss, abolish.
+
+ Background:
+ Given a fresh Rolex instance
+ And organization "acme" exists
+
+ # ===== establish =====
+
+ Scenario: Establish creates a position
+ When I direct "!position.establish" with:
+ | content | Feature: CTO |
+ | id | acme/cto |
+ Then the result process should be "establish"
+ And the result state name should be "position"
+ And the result state id should be "acme/cto"
+
+ # ===== charge =====
+
+ Scenario: Charge assigns a duty to a position
+ Given position "acme/cto" exists
+ When I direct "!position.charge" with:
+ | position | acme/cto |
+ | content | Feature: Own technical direction |
+ | id | tech-direction |
+ Then the result process should be "charge"
+ And the result state name should be "duty"
+
+ # ===== require =====
+
+ Scenario: Require sets a skill requirement on a position
+ Given position "acme/cto" exists
+ When I direct "!position.require" with:
+ | position | acme/cto |
+ | content | Feature: System design skill |
+ | id | system-design |
+ Then the result process should be "require"
+ And the result state name should be "requirement"
+
+ # ===== appoint =====
+
+ Scenario: Appoint assigns an individual to a position
+ Given individual "sean" exists
+ And "sean" is hired into "acme"
+ And position "acme/cto" exists
+ When I direct "!position.appoint" with:
+ | position | acme/cto |
+ | individual | sean |
+ Then the result process should be "appoint"
+
+ # ===== dismiss =====
+
+ Scenario: Dismiss removes an individual from a position
+ Given individual "sean" exists
+ And "sean" is hired into "acme"
+ And position "acme/cto" exists
+ And "sean" is appointed to "acme/cto"
+ When I direct "!position.dismiss" with:
+ | position | acme/cto |
+ | individual | sean |
+ Then the result process should be "dismiss"
+
+ # ===== abolish =====
+
+ Scenario: Abolish archives a position
+ Given position "acme/cto" exists
+ When I direct "!position.abolish" with:
+ | position | acme/cto |
+ Then the result process should be "abolish"
+ And the result state name should be "past"
diff --git a/bdd/features/role-system.feature b/bdd/features/role-system.feature
deleted file mode 100644
index eab42c2..0000000
--- a/bdd/features/role-system.feature
+++ /dev/null
@@ -1,83 +0,0 @@
-@role-system
-Feature: Role System
- Manage role lifecycle from the outside — born, teach, train, retire, kill.
-
- Background:
- Given a fresh RoleX platform
-
- # ===== born =====
-
- @born
- Scenario: Born creates a new role with persona
- When I born a role "sean" with:
- """
- Feature: I am Sean
- Scenario: Background
- Given I am a backend architect
- """
- Then role "sean" should exist
- And role "sean" should have a persona containing "I am Sean"
-
- @born
- Scenario: Born duplicate role fails
- Given role "alice" exists
- When I born a role "alice" with:
- """
- Feature: Duplicate
- Scenario: Dup
- Given duplicate
- """
- Then it should fail with "already exist"
-
- # ===== teach =====
-
- @teach
- Scenario: Teach adds knowledge.pattern to a role
- Given role "sean" exists
- When I teach "sean" knowledge "typescript" with:
- """
- Feature: TypeScript
- Scenario: Basics
- Given I know TypeScript well
- """
- Then role "sean" should have knowledge.pattern "typescript"
-
- @teach
- Scenario: Teach non-existent role fails
- When I teach "ghost" knowledge "x" with:
- """
- Feature: X
- Scenario: X
- Given X
- """
- Then it should fail with "not found"
-
- # ===== train =====
-
- @train
- Scenario: Train adds knowledge.procedure to a role
- Given role "sean" exists
- When I train "sean" procedure "code-review" with:
- """
- Feature: Code Review
- Scenario: How to review
- Given I read the diff first
- """
- Then role "sean" should have knowledge.procedure "code-review"
-
- # ===== retire =====
-
- @retire
- Scenario: Retire archives a role
- Given role "sean" exists
- When I retire role "sean"
- Then role "sean" should be shadowed
- And persona of "sean" should be tagged @retired
-
- # ===== kill =====
-
- @kill
- Scenario: Kill destroys a role completely
- Given role "temp" exists
- When I kill role "temp"
- Then role "temp" should not exist
diff --git a/bdd/journeys/01-execution-loop.feature b/bdd/journeys/01-execution-loop.feature
deleted file mode 100644
index 25a40d4..0000000
--- a/bdd/journeys/01-execution-loop.feature
+++ /dev/null
@@ -1,151 +0,0 @@
-@journey @execution
-Feature: Execution Loop
- The complete goal pursuit cycle: identity → want → design → todo → finish → achieve.
- This is the core loop that drives all productive work in RoleX.
-
- Background:
- Given a fresh RoleX platform
- And role "dev" exists with persona "I am a developer"
-
- Scenario: Full execution loop — from identity to achievement
- # Step 1: Activate identity
- When I call identity for "dev"
- Then the result should contain "identity loaded"
-
- # Step 2: Declare a goal
- When I want "ship-feature" with:
- """
- Feature: Ship Feature
- Scenario: Deliver user auth
- Given the product needs user authentication
- """
- Then focus should be on "ship-feature"
-
- # Step 3: Design a plan
- When I design "auth-plan" with:
- """
- Feature: Auth Plan
- Scenario: Phase 1 — Backend
- Given implement JWT token service
- Scenario: Phase 2 — Frontend
- Given add login UI
- """
- Then plan "auth-plan" should exist under goal "ship-feature"
-
- # Step 4: Create tasks
- When I todo "jwt-service" with:
- """
- Feature: JWT Service
- Scenario: Token generation
- Given implement sign and verify methods
- """
- And I todo "login-ui" with:
- """
- Feature: Login UI
- Scenario: Login form
- Given create email + password form
- """
- Then task "jwt-service" should exist under plan "auth-plan"
- And task "login-ui" should exist under plan "auth-plan"
-
- # Step 5: Finish tasks
- When I finish "jwt-service" with conclusion:
- """
- Feature: JWT Done
- Scenario: Result
- Given JWT service implemented with RS256
- """
- And I finish "login-ui"
- Then task "jwt-service" should be marked @done
- And task "login-ui" should be marked @done
-
- # Step 6: Achieve goal
- When I achieve with experience "auth-experience":
- """
- Feature: Auth Experience
- Scenario: Key learning
- Given RS256 is better than HS256 for distributed systems
- And always validate token expiry on the server side
- """
- Then goal "ship-feature" should be marked @done
- And experience.insight "auth-experience" should exist
- And conclusions should be consumed
-
- Scenario: Execution loop with abandonment
- When I call identity for "dev"
- And I want "bad-idea" with:
- """
- Feature: Bad Idea
- Scenario: Premature optimization
- Given optimize the database before any users exist
- """
- And I design "opt-plan" with:
- """
- Feature: Optimization Plan
- Scenario: Phase 1
- Given add caching layer
- """
- And I todo "add-redis" with:
- """
- Feature: Add Redis
- Scenario: Setup
- Given install and configure Redis
- """
-
- # Realize it's premature — abandon
- When I abandon with experience "premature-lesson":
- """
- Feature: Premature Lesson
- Scenario: Insight
- Given don't optimize before measuring
- And premature optimization is the root of all evil
- """
- Then goal "bad-idea" should be marked @abandoned
- And experience.insight "premature-lesson" should exist
-
- Scenario: Multi-goal switching during execution
- When I call identity for "dev"
- And I want "goal-alpha" with:
- """
- Feature: Goal Alpha
- Scenario: First
- Given build feature alpha
- """
- And I want "goal-beta" with:
- """
- Feature: Goal Beta
- Scenario: Second
- Given build feature beta
- """
- Then focus should be on "goal-beta"
-
- # Switch focus to alpha
- When I call focus with name "goal-alpha"
- Then the result should contain "goal: goal-alpha"
- And the result should contain "Other goals: goal-beta"
-
- # Work on alpha
- When I design "alpha-plan" with:
- """
- Feature: Alpha Plan
- Scenario: Steps
- Given implement alpha
- """
- And I todo "alpha-task" with:
- """
- Feature: Alpha Task
- Scenario: Work
- Given do alpha work
- """
- And I finish "alpha-task"
- And I achieve with experience "alpha-insight":
- """
- Feature: Alpha Insight
- Scenario: Learned
- Given alpha taught me X
- """
- Then goal "goal-alpha" should be marked @done
-
- # Auto-switch to beta (or manual focus)
- When I call focus with name "goal-beta"
- Then the result should contain "goal: goal-beta"
diff --git a/bdd/journeys/02-growth-loop.feature b/bdd/journeys/02-growth-loop.feature
deleted file mode 100644
index b656d0b..0000000
--- a/bdd/journeys/02-growth-loop.feature
+++ /dev/null
@@ -1,146 +0,0 @@
-@journey @growth
-Feature: Growth Loop
- The learning cycle: achieve → reflect → contemplate.
- Experience.insight becomes knowledge.pattern becomes knowledge.theory.
-
- Background:
- Given a fresh RoleX platform
- And role "learner" exists with persona "I am a learner"
- And I call identity for "learner"
-
- Scenario: Full growth loop — from experience to theory
- # Phase 1: Accumulate insights through multiple goal cycles
-
- # Goal 1: Build a web app
- When I want "build-webapp" with:
- """
- Feature: Build Web App
- Scenario: Ship it
- Given build and deploy a web app
- """
- And I design "webapp-plan" with:
- """
- Feature: Web App Plan
- Scenario: Steps
- Given setup Next.js project
- """
- And I todo "setup-next" with:
- """
- Feature: Setup Next
- Scenario: Init
- Given run create-next-app
- """
- And I finish "setup-next"
- And I achieve with experience "webapp-insight":
- """
- Feature: Web App Insight
- Scenario: Learned
- Given Next.js App Router simplifies server components
- And convention over configuration saves setup time
- """
- Then experience.insight "webapp-insight" should exist
-
- # Goal 2: Build an API
- When I want "build-api" with:
- """
- Feature: Build API
- Scenario: Ship it
- Given build and deploy a REST API
- """
- And I design "api-plan" with:
- """
- Feature: API Plan
- Scenario: Steps
- Given setup Hono server
- """
- And I todo "setup-hono" with:
- """
- Feature: Setup Hono
- Scenario: Init
- Given create Hono app
- """
- And I finish "setup-hono"
- And I achieve with experience "api-insight":
- """
- Feature: API Insight
- Scenario: Learned
- Given Hono's middleware pattern is clean
- And convention-based routing reduces boilerplate
- """
- Then experience.insight "api-insight" should exist
-
- # Phase 2: Reflect — distill insights into knowledge.pattern
- When I reflect on "webapp-insight", "api-insight" to produce "framework-principles" with:
- """
- Feature: Framework Principles
- Scenario: Convention over configuration
- Given convention-based approaches consistently reduce setup time
- And they make codebases more predictable
- Scenario: Middleware patterns
- Given layered middleware provides clean separation of concerns
- """
- Then knowledge.pattern "framework-principles" should exist
- And experience.insight "webapp-insight" should be consumed
- And experience.insight "api-insight" should be consumed
-
- # Goal 3: Build a CLI tool
- When I want "build-cli" with:
- """
- Feature: Build CLI
- Scenario: Ship it
- Given build a developer CLI tool
- """
- And I design "cli-plan" with:
- """
- Feature: CLI Plan
- Scenario: Steps
- Given setup citty commands
- """
- And I todo "setup-cli" with:
- """
- Feature: Setup CLI
- Scenario: Init
- Given scaffold command structure
- """
- And I finish "setup-cli"
- And I achieve with experience "cli-insight":
- """
- Feature: CLI Insight
- Scenario: Learned
- Given auto-derived commands from definitions eliminate boilerplate
- And single source of truth pattern applies to CLIs too
- """
-
- # Reflect again with new insight + existing pattern
- When I reflect on "cli-insight" to produce "derivation-principles" with:
- """
- Feature: Derivation Principles
- Scenario: Auto-derivation
- Given when definitions are the source of truth
- Then multiple interfaces can be auto-derived
- And boilerplate is eliminated
- """
- Then knowledge.pattern "derivation-principles" should exist
-
- # Phase 3: Contemplate — unify patterns into theory
- When I contemplate on "framework-principles", "derivation-principles" to produce "software-philosophy" with:
- """
- Feature: Software Philosophy
- Scenario: Convention and derivation
- Given conventions establish predictable structure
- And derivation eliminates repetitive translation
- Then together they produce minimal yet powerful systems
- """
- Then knowledge.theory "software-philosophy" should exist
- And knowledge.pattern "framework-principles" should still exist
- And knowledge.pattern "derivation-principles" should still exist
-
- Scenario: Forget prunes outdated knowledge
- Given I have knowledge.pattern "old-pattern" with:
- """
- Feature: Old Pattern
- Scenario: Outdated
- Given this is no longer relevant
- """
- When I forget knowledge.pattern "old-pattern"
- Then knowledge.pattern "old-pattern" should not exist
diff --git a/bdd/journeys/03-organization-loop.feature b/bdd/journeys/03-organization-loop.feature
deleted file mode 100644
index f53e68d..0000000
--- a/bdd/journeys/03-organization-loop.feature
+++ /dev/null
@@ -1,96 +0,0 @@
-@journey @organization
-Feature: Organization Loop
- Full org lifecycle: found → rule → establish → hire → appoint → work → dismiss → fire → abolish → dissolve.
-
- Background:
- Given a fresh RoleX platform
- And role "alice" exists with persona "I am Alice the PM"
- And role "bob" exists with persona "I am Bob the engineer"
- And role "charlie" exists with persona "I am Charlie the designer"
-
- Scenario: Full organization lifecycle
- # Step 1: Found organization
- When I found org "startup" with:
- """
- Feature: Startup Charter
- Scenario: Mission
- Given we build innovative AI products
- """
- Then org "startup" should exist
-
- # Step 2: Write rules (charter entries)
- When I rule "startup" charter "values" with:
- """
- Feature: Company Values
- Scenario: Core values
- Given move fast and ship often
- And user experience above all
- """
- Then charter "values" should exist in "startup"
-
- # Step 3: Establish positions
- When I establish position "cto" in "startup" with:
- """
- Feature: CTO
- Scenario: Responsibilities
- Given lead technical architecture and engineering team
- """
- And I establish position "engineer" in "startup" with:
- """
- Feature: Engineer
- Scenario: Responsibilities
- Given build and maintain product features
- """
- And I establish position "designer" in "startup" with:
- """
- Feature: Designer
- Scenario: Responsibilities
- Given design user interfaces and experiences
- """
- Then position "cto" should exist in "startup"
- And position "engineer" should exist in "startup"
- And position "designer" should exist in "startup"
-
- # Step 4: Hire members
- When I hire "alice" into "startup"
- And I hire "bob" into "startup"
- And I hire "charlie" into "startup"
- Then "alice" should be a member of "startup"
- And "bob" should be a member of "startup"
- And "charlie" should be a member of "startup"
-
- # Step 5: Appoint to positions
- When I appoint "alice" to "startup/cto"
- And I appoint "bob" to "startup/engineer"
- And I appoint "charlie" to "startup/designer"
- Then "alice" should be assigned to "startup/cto"
- And "bob" should be assigned to "startup/engineer"
- And "charlie" should be assigned to "startup/designer"
-
- # Step 6: Verify directory
- When I query directory of "startup"
- Then the result should contain "alice"
- And the result should contain "bob"
- And the result should contain "charlie"
- And the result should contain "cto"
- And the result should contain "engineer"
- And the result should contain "designer"
-
- # Step 7: Dismiss from position (role change)
- When I dismiss "bob" from "startup/engineer"
- And I appoint "bob" to "startup/cto"
- Then "bob" should not be assigned to "startup/engineer"
- And "bob" should be assigned to "startup/cto"
-
- # Step 8: Fire member (auto-dismisses)
- When I fire "charlie" from "startup"
- Then "charlie" should not be a member of "startup"
- And "charlie" should not be assigned to "startup/designer"
-
- # Step 9: Abolish unused position
- When I abolish position "designer" in "startup"
- Then position "designer" should be shadowed in "startup"
-
- # Step 10: Dissolve organization
- When I dissolve org "startup"
- Then org "startup" should be shadowed
diff --git a/bdd/journeys/mcp-startup.feature b/bdd/journeys/mcp-startup.feature
new file mode 100644
index 0000000..e3e4d2e
--- /dev/null
+++ b/bdd/journeys/mcp-startup.feature
@@ -0,0 +1,37 @@
+@journey @mcp @startup
+Feature: MCP Server Startup
+ The MCP server is the entry point for all AI agent interactions.
+ It must start reliably, register all tools, and respond to basic operations.
+
+ Scenario: Server starts and registers all tools
+ Given the MCP server is running
+ Then the following tools should be available:
+ | tool |
+ | activate |
+ | focus |
+ | want |
+ | plan |
+ | todo |
+ | finish |
+ | complete |
+ | abandon |
+ | reflect |
+ | realize |
+ | master |
+ | forget |
+ | skill |
+ | use |
+ | direct |
+
+ Scenario: Activate a role through MCP
+ Given the MCP server is running
+ When I call tool "activate" with:
+ | roleId | nuwa |
+ Then the tool result should contain "nuwa"
+ And the tool result should contain "[individual]"
+
+ Scenario: Direct world query through MCP
+ Given the MCP server is running
+ When I call tool "direct" with:
+ | locator | !census.list |
+ Then the tool result should contain "nuwa"
diff --git a/bdd/journeys/onboarding.feature b/bdd/journeys/onboarding.feature
new file mode 100644
index 0000000..cb5ea4d
--- /dev/null
+++ b/bdd/journeys/onboarding.feature
@@ -0,0 +1,31 @@
+@onboarding @npx
+Feature: Onboarding via npx
+ A new user installs @rolexjs/mcp-server via npx,
+ connects an MCP client, and activates Nuwa.
+
+ Scenario: npx starts MCP server and registers tools
+ Given the MCP server is running via npx
+ Then the following tools should be available:
+ | tool |
+ | activate |
+ | focus |
+ | want |
+ | plan |
+ | todo |
+ | finish |
+ | complete |
+ | abandon |
+ | reflect |
+ | realize |
+ | master |
+ | forget |
+ | skill |
+ | use |
+ | direct |
+
+ Scenario: Activate Nuwa via npx
+ Given the MCP server is running via npx
+ When I call tool "activate" with:
+ | roleId | nuwa |
+ Then the tool result should contain "nuwa"
+ And the tool result should contain "[individual]"
diff --git a/bdd/package.json b/bdd/package.json
new file mode 100644
index 0000000..c5a0776
--- /dev/null
+++ b/bdd/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@rolexjs/bdd",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "devDependencies": {
+ "@deepracticex/bdd": "^0.2.0",
+ "@modelcontextprotocol/sdk": "^1.27.1",
+ "@rolexjs/core": "workspace:*",
+ "@rolexjs/local-platform": "workspace:*",
+ "rolexjs": "workspace:*"
+ }
+}
diff --git a/bdd/run.test.ts b/bdd/run.test.ts
new file mode 100644
index 0000000..688c043
--- /dev/null
+++ b/bdd/run.test.ts
@@ -0,0 +1,32 @@
+/**
+ * BDD test entry point.
+ *
+ * Imports step definitions and support, then loads feature files.
+ * Bun's test runner executes the generated describe/test blocks natively.
+ */
+
+import { loadFeature, setDefaultTimeout } from "@deepracticex/bdd";
+
+// Support (unified world)
+import "./support/world";
+
+// Steps
+import "./steps/mcp.steps";
+import "./steps/context.steps";
+import "./steps/direct.steps";
+import "./steps/role.steps";
+
+// Timeout: MCP/npx startup can take a while
+setDefaultTimeout(60_000);
+
+// ===== Journeys =====
+loadFeature("bdd/journeys/mcp-startup.feature");
+loadFeature("bdd/journeys/onboarding.feature");
+
+// ===== Features =====
+loadFeature("bdd/features/context-persistence.feature");
+loadFeature("bdd/features/individual-lifecycle.feature");
+loadFeature("bdd/features/organization-lifecycle.feature");
+loadFeature("bdd/features/position-lifecycle.feature");
+loadFeature("bdd/features/execution-loop.feature");
+loadFeature("bdd/features/cognition-loop.feature");
diff --git a/bdd/steps/assertions.steps.ts b/bdd/steps/assertions.steps.ts
deleted file mode 100644
index 758041f..0000000
--- a/bdd/steps/assertions.steps.ts
+++ /dev/null
@@ -1,313 +0,0 @@
-/**
- * All Then/assertion steps — shared across features and journeys.
- */
-
-import { strict as assert } from "node:assert";
-import { Then } from "@deepractice/bdd";
-import type { RoleXWorld } from "../support/world";
-
-// ========== Result Assertions ==========
-
-Then("the result should contain {string}", function (this: RoleXWorld, text: string) {
- assert.ok(this.result, "Expected a result but got none (error occurred?)");
- assert.ok(
- this.result.includes(text),
- `Expected result to contain "${text}" but got:\n${this.result}`
- );
-});
-
-Then("it should fail with {string}", function (this: RoleXWorld, text: string) {
- assert.ok(this.error, `Expected an error containing "${text}" but no error occurred`);
- assert.ok(
- this.error.message.includes(text),
- `Expected error to contain "${text}" but got: ${this.error.message}`
- );
-});
-
-// ========== Role Assertions ==========
-
-Then("role {string} should exist", function (this: RoleXWorld, name: string) {
- assert.ok(this.graph.hasNode(name), `Role "${name}" should exist`);
- assert.equal(this.graph.getNode(name)?.type, "role");
-});
-
-Then("role {string} should not exist", function (this: RoleXWorld, name: string) {
- assert.ok(!this.graph.hasNode(name), `Role "${name}" should not exist`);
-});
-
-Then(
- "role {string} should have a persona containing {string}",
- function (this: RoleXWorld, name: string, text: string) {
- const content = this.platform.readContent(`${name}/persona`);
- assert.ok(content, `Persona for "${name}" not found`);
- const source = `${content.name} ${content.description ?? ""}`;
- assert.ok(source.includes(text), `Persona should contain "${text}"`);
- }
-);
-
-Then(
- /^role "([^"]*)" should have knowledge\.pattern "([^"]*)"$/,
- function (this: RoleXWorld, role: string, name: string) {
- assert.ok(this.graph.hasNode(`${role}/${name}`), `knowledge.pattern "${name}" not found`);
- assert.equal(this.graph.getNode(`${role}/${name}`)?.type, "knowledge.pattern");
- }
-);
-
-Then(
- /^role "([^"]*)" should have knowledge\.procedure "([^"]*)"$/,
- function (this: RoleXWorld, role: string, name: string) {
- assert.ok(this.graph.hasNode(`${role}/${name}`), `knowledge.procedure "${name}" not found`);
- assert.equal(this.graph.getNode(`${role}/${name}`)?.type, "knowledge.procedure");
- }
-);
-
-Then("role {string} should be shadowed", function (this: RoleXWorld, name: string) {
- assert.ok(this.graph.getNode(name)?.shadow, `Role "${name}" should be shadowed`);
-});
-
-Then("persona of {string} should be tagged @retired", function (this: RoleXWorld, name: string) {
- const content = this.platform.readContent(`${name}/persona`);
- assert.ok(content, `Persona for "${name}" not found`);
- const hasRetired = content.tags?.some((t: any) => t.name === "@retired");
- assert.ok(hasRetired, `Persona should have @retired tag`);
-});
-
-// ========== Goal Assertions ==========
-
-Then("goal {string} should exist", function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- assert.ok(this.graph.hasNode(`${roleKey}/${name}`), `Goal "${name}" not found`);
-});
-
-Then("focus should be on {string}", function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- const focus = this.graph.getNode(roleKey!)?.state?.focus;
- assert.ok(focus?.endsWith(`/${name}`), `Focus should be on "${name}" but is "${focus}"`);
-});
-
-Then("goal {string} should be marked @done", function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- const content = this.platform.readContent(`${roleKey}/${name}`);
- assert.ok(content, `Goal "${name}" content not found`);
- const hasDone = content.tags?.some((t: any) => t.name === "@done");
- assert.ok(hasDone, `Goal "${name}" should have @done tag`);
-});
-
-Then("goal {string} should be marked @abandoned", function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- // abandon() shadows the goal (cascading to plans/tasks)
- const node = this.graph.getNode(`${roleKey}/${name}`);
- assert.ok(node, `Goal "${name}" not found in graph`);
- assert.ok(node.shadow, `Goal "${name}" should be shadowed (abandoned)`);
-});
-
-// ========== Plan Assertions ==========
-
-Then(
- "plan {string} should exist under goal {string}",
- function (this: RoleXWorld, plan: string, goal: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- const plans = this.graph.outNeighbors(`${roleKey}/${goal}`, "has-plan");
- const found = plans.some((k) => k.endsWith(`/${plan}`));
- assert.ok(found, `Plan "${plan}" not found under goal "${goal}"`);
- }
-);
-
-Then("focused plan should be {string}", function (this: RoleXWorld, plan: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- const focus = this.graph.getNode(roleKey!)?.state?.focus;
- const goalNode = this.graph.getNode(focus!);
- const focusPlan = goalNode?.state?.focusPlan;
- assert.ok(
- focusPlan?.endsWith(`/${plan}`),
- `Focused plan should be "${plan}" but is "${focusPlan}"`
- );
-});
-
-// ========== Task Assertions ==========
-
-Then(
- "task {string} should exist under plan {string}",
- function (this: RoleXWorld, task: string, plan: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- const tasks = this.graph.outNeighbors(`${roleKey}/${plan}`, "has-task");
- const found = tasks.some((k) => k.endsWith(`/${task}`));
- assert.ok(found, `Task "${task}" not found under plan "${plan}"`);
- }
-);
-
-Then("task {string} should be marked @done", function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- const content = this.platform.readContent(`${roleKey}/${name}`);
- assert.ok(content, `Task "${name}" content not found`);
- const hasDone = content.tags?.some((t: any) => t.name === "@done");
- assert.ok(hasDone, `Task "${name}" should have @done tag`);
-});
-
-// ========== Experience & Knowledge Assertions ==========
-
-Then("conclusion {string} should exist", function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- assert.ok(this.graph.hasNode(`${roleKey}/${name}-conclusion`), `Conclusion "${name}" not found`);
-});
-
-Then(/^experience\.insight "([^"]*)" should exist$/, function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- assert.ok(this.graph.hasNode(`${roleKey}/${name}`), `Insight "${name}" not found`);
- assert.equal(this.graph.getNode(`${roleKey}/${name}`)?.type, "experience.insight");
- assert.ok(
- !this.graph.getNode(`${roleKey}/${name}`)?.shadow,
- `Insight "${name}" should not be shadowed`
- );
-});
-
-Then(
- /^experience\.insight "([^"]*)" should be consumed$/,
- function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- assert.ok(
- this.graph.getNode(`${roleKey}/${name}`)?.shadow,
- `Insight "${name}" should be consumed (shadowed)`
- );
- }
-);
-
-Then(/^experience\.insight "([^"]*)" should not exist$/, function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- const node = this.graph.getNode(`${roleKey}/${name}`);
- assert.ok(!node || node.shadow, `Insight "${name}" should not exist`);
-});
-
-Then("conclusions should be consumed", function (this: RoleXWorld) {
- // All conclusion nodes should be shadowed
- const roleKey = this.individualSystem.ctx?.structure;
- const nodes = this.graph.findNodes(
- (key, attrs) =>
- key.startsWith(`${roleKey}/`) &&
- key.endsWith("-conclusion") &&
- attrs.type === "experience.conclusion"
- );
- for (const key of nodes) {
- const node = this.graph.getNode(key);
- assert.ok(node?.shadow, `Conclusion "${key}" should be consumed`);
- }
-});
-
-Then(/^knowledge\.pattern "([^"]*)" should exist$/, function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- assert.ok(this.graph.hasNode(`${roleKey}/${name}`), `Pattern "${name}" not found`);
- assert.equal(this.graph.getNode(`${roleKey}/${name}`)?.type, "knowledge.pattern");
-});
-
-Then(/^knowledge\.pattern "([^"]*)" should not exist$/, function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- const node = this.graph.getNode(`${roleKey}/${name}`);
- assert.ok(!node || node.shadow, `Pattern "${name}" should not exist`);
-});
-
-Then(
- /^knowledge\.pattern "([^"]*)" should still exist$/,
- function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- assert.ok(this.graph.hasNode(`${roleKey}/${name}`), `Pattern "${name}" not found`);
- assert.ok(
- !this.graph.getNode(`${roleKey}/${name}`)?.shadow,
- `Pattern "${name}" should not be shadowed`
- );
- }
-);
-
-Then(/^knowledge\.theory "([^"]*)" should exist$/, function (this: RoleXWorld, name: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- assert.ok(this.graph.hasNode(`${roleKey}/${name}`), `Theory "${name}" not found`);
- assert.equal(this.graph.getNode(`${roleKey}/${name}`)?.type, "knowledge.theory");
-});
-
-// ========== Organization Assertions ==========
-
-Then("org {string} should exist", function (this: RoleXWorld, name: string) {
- assert.ok(this.graph.hasNode(name), `Org "${name}" should exist`);
- assert.equal(this.graph.getNode(name)?.type, "organization");
-});
-
-Then(
- "org {string} should have charter containing {string}",
- function (this: RoleXWorld, name: string, text: string) {
- const content = this.platform.readContent(`${name}/charter`);
- assert.ok(content, `Charter for "${name}" not found`);
- // Check scenarios for the text
- const scenarioTexts = content.scenarios?.map((s: any) => JSON.stringify(s)).join(" ") ?? "";
- const featureText = `${content.name ?? ""} ${content.description ?? ""} ${scenarioTexts}`;
- assert.ok(featureText.includes(text), `Charter should contain "${text}"`);
- }
-);
-
-Then("org {string} should be shadowed", function (this: RoleXWorld, name: string) {
- assert.ok(this.graph.getNode(name)?.shadow, `Org "${name}" should be shadowed`);
-});
-
-// ========== Position Assertions ==========
-
-Then(
- "position {string} should exist in {string}",
- function (this: RoleXWorld, position: string, org: string) {
- assert.ok(
- this.graph.hasNode(`${org}/${position}`),
- `Position "${position}" not found in "${org}"`
- );
- assert.equal(this.graph.getNode(`${org}/${position}`)?.type, "position");
- }
-);
-
-Then(
- "position {string} should be shadowed in {string}",
- function (this: RoleXWorld, position: string, org: string) {
- assert.ok(
- this.graph.getNode(`${org}/${position}`)?.shadow,
- `Position "${position}" should be shadowed`
- );
- }
-);
-
-Then(
- "charter {string} should exist in {string}",
- function (this: RoleXWorld, name: string, org: string) {
- assert.ok(
- this.graph.hasNode(`${org}/${name}`),
- `Charter entry "${name}" not found in "${org}"`
- );
- }
-);
-
-// ========== Membership & Assignment Assertions ==========
-
-Then(
- "{string} should be a member of {string}",
- function (this: RoleXWorld, role: string, org: string) {
- assert.ok(this.graph.hasEdge(org, role), `"${role}" should be a member of "${org}"`);
- }
-);
-
-Then(
- "{string} should not be a member of {string}",
- function (this: RoleXWorld, role: string, org: string) {
- assert.ok(!this.graph.hasEdge(org, role), `"${role}" should NOT be a member of "${org}"`);
- }
-);
-
-Then(
- "{string} should be assigned to {string}",
- function (this: RoleXWorld, role: string, position: string) {
- assert.ok(this.graph.hasEdge(position, role), `"${role}" should be assigned to "${position}"`);
- }
-);
-
-Then(
- "{string} should not be assigned to {string}",
- function (this: RoleXWorld, role: string, position: string) {
- assert.ok(
- !this.graph.hasEdge(position, role),
- `"${role}" should NOT be assigned to "${position}"`
- );
- }
-);
diff --git a/bdd/steps/context.steps.ts b/bdd/steps/context.steps.ts
new file mode 100644
index 0000000..d32c213
--- /dev/null
+++ b/bdd/steps/context.steps.ts
@@ -0,0 +1,82 @@
+/**
+ * Steps for context persistence tests — operate at the Rolex API level.
+ */
+
+import { strict as assert } from "node:assert";
+import { Given, Then, When } from "@deepracticex/bdd";
+import type { BddWorld } from "../support/world";
+
+// ===== Setup =====
+
+Given("a fresh Rolex instance", function (this: BddWorld) {
+ this.initRolex();
+});
+
+Given(
+ "an individual {string} with goal {string}",
+ async function (this: BddWorld, name: string, goalId: string) {
+ await this.rolex.direct("!individual.born", { content: `Feature: ${name}`, id: name });
+ await this.rolex.direct("!role.want", {
+ individual: name,
+ goal: `Feature: ${goalId}`,
+ id: goalId,
+ });
+ }
+);
+
+Given(
+ "an individual {string} with goals {string} and {string}",
+ async function (this: BddWorld, name: string, goal1: string, goal2: string) {
+ await this.rolex.direct("!individual.born", { content: `Feature: ${name}`, id: name });
+ await this.rolex.direct("!role.want", {
+ individual: name,
+ goal: `Feature: ${goal1}`,
+ id: goal1,
+ });
+ await this.rolex.direct("!role.want", {
+ individual: name,
+ goal: `Feature: ${goal2}`,
+ id: goal2,
+ });
+ }
+);
+
+// ===== Persistence setup =====
+
+Given("persisted focusedGoalId is null", function (this: BddWorld) {
+ this.writeContext("sean", { focusedGoalId: null, focusedPlanId: null });
+ this.newSession();
+});
+
+Given("persisted focusedGoalId is {string}", function (this: BddWorld, goalId: string) {
+ this.writeContext("sean", { focusedGoalId: goalId, focusedPlanId: null });
+ this.newSession();
+});
+
+Given("no persisted context exists", function (this: BddWorld) {
+ // Don't write any context file — just create a new session
+ this.newSession();
+});
+
+// ===== Actions =====
+
+When("I activate {string}", async function (this: BddWorld, name: string) {
+ try {
+ this.error = undefined;
+ this.role = await this.rolex.activate(name);
+ } catch (e) {
+ this.error = e as Error;
+ this.role = undefined;
+ }
+});
+
+// ===== Assertions =====
+
+Then("focusedGoalId should be {string}", function (this: BddWorld, goalId: string) {
+ assert.ok(this.role, "Expected a role but activation failed");
+ assert.equal(
+ this.role.ctx.focusedGoalId,
+ goalId,
+ `Expected focusedGoalId to be "${goalId}" but got "${this.role.ctx.focusedGoalId}"`
+ );
+});
diff --git a/bdd/steps/direct.steps.ts b/bdd/steps/direct.steps.ts
new file mode 100644
index 0000000..b09265e
--- /dev/null
+++ b/bdd/steps/direct.steps.ts
@@ -0,0 +1,109 @@
+/**
+ * Direct steps — call rolex.direct() for system-level operations.
+ */
+
+import { strict as assert } from "node:assert";
+import { type DataTable, Given, Then, When } from "@deepracticex/bdd";
+import type { BddWorld } from "../support/world";
+
+// ===== Setup helpers =====
+
+Given("individual {string} exists", async function (this: BddWorld, id: string) {
+ await this.rolex!.direct("!individual.born", { content: `Feature: ${id}`, id });
+});
+
+Given("individual {string} is retired", async function (this: BddWorld, id: string) {
+ await this.rolex!.direct("!individual.retire", { individual: id });
+});
+
+Given("organization {string} exists", async function (this: BddWorld, id: string) {
+ await this.rolex!.direct("!org.found", { content: `Feature: ${id}`, id });
+});
+
+Given("position {string} exists", async function (this: BddWorld, id: string) {
+ await this.rolex!.direct("!position.establish", { content: `Feature: ${id}`, id });
+});
+
+Given(
+ "{string} is hired into {string}",
+ async function (this: BddWorld, individual: string, org: string) {
+ await this.rolex!.direct("!org.hire", { org, individual });
+ }
+);
+
+Given(
+ "{string} is appointed to {string}",
+ async function (this: BddWorld, individual: string, position: string) {
+ await this.rolex!.direct("!position.appoint", { position, individual });
+ }
+);
+
+// ===== Direct call =====
+
+When("I direct {string} with:", async function (this: BddWorld, command: string, table: DataTable) {
+ try {
+ this.error = undefined;
+ const args = table.rowsHash();
+ const raw = await this.rolex!.direct(command, args);
+ // Store both raw result and serialized form
+ this.directRaw = raw as any;
+ this.directResult = typeof raw === "string" ? raw : JSON.stringify(raw, null, 2);
+ } catch (e) {
+ this.error = e as Error;
+ this.directRaw = undefined;
+ this.directResult = undefined;
+ }
+});
+
+// ===== Result assertions =====
+
+Then("the result process should be {string}", function (this: BddWorld, process: string) {
+ assert.ok(this.directRaw, `Expected a result but got error: ${this.error?.message ?? "none"}`);
+ assert.equal(
+ this.directRaw.process,
+ process,
+ `Expected process "${process}" but got "${this.directRaw.process}"`
+ );
+});
+
+Then("the result state name should be {string}", function (this: BddWorld, name: string) {
+ assert.ok(this.directRaw, `Expected a result but got error: ${this.error?.message ?? "none"}`);
+ assert.equal(
+ this.directRaw.state.name,
+ name,
+ `Expected state name "${name}" but got "${this.directRaw.state.name}"`
+ );
+});
+
+Then("the result state id should be {string}", function (this: BddWorld, id: string) {
+ assert.ok(this.directRaw, `Expected a result but got error: ${this.error?.message ?? "none"}`);
+ assert.equal(
+ this.directRaw.state.id,
+ id,
+ `Expected state id "${id}" but got "${this.directRaw.state.id}"`
+ );
+});
+
+Then("the direct result should contain {string}", function (this: BddWorld, text: string) {
+ assert.ok(this.directResult, `Expected a result but got error: ${this.error?.message ?? "none"}`);
+ assert.ok(
+ this.directResult.toLowerCase().includes(text.toLowerCase()),
+ `Expected result to contain "${text}" but got:\n${this.directResult.slice(0, 500)}`
+ );
+});
+
+Then("it should fail", function (this: BddWorld) {
+ assert.ok(this.error, "Expected an error but operation succeeded");
+});
+
+// ===== Entity existence assertions =====
+
+Then("individual {string} should exist", async function (this: BddWorld, id: string) {
+ const census = await this.rolex!.direct("!census.list", { type: "individual" });
+ assert.ok(census.includes(id), `Individual "${id}" not found in census: ${census}`);
+});
+
+Then("organization {string} should exist", async function (this: BddWorld, id: string) {
+ const census = await this.rolex!.direct("!census.list", { type: "organization" });
+ assert.ok(census.includes(id), `Organization "${id}" not found in census: ${census}`);
+});
diff --git a/bdd/steps/individual.steps.ts b/bdd/steps/individual.steps.ts
deleted file mode 100644
index 56a53b4..0000000
--- a/bdd/steps/individual.steps.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * Individual System steps — identity, want, focus, explore, design, todo,
- * finish, achieve, abandon, reflect, contemplate, forget.
- */
-
-import { When } from "@deepractice/bdd";
-import type { RoleXWorld } from "../support/world";
-
-// ===== identity =====
-
-When("I call identity for {string}", async function (this: RoleXWorld, name: string) {
- await this.run(this.individualSystem, "identity", { roleId: name });
-});
-
-// ===== want =====
-
-When("I want {string} with:", async function (this: RoleXWorld, name: string, source: string) {
- await this.run(this.individualSystem, "want", { name, source });
-});
-
-// ===== focus =====
-
-When("I call focus", async function (this: RoleXWorld) {
- await this.run(this.individualSystem, "focus", {});
-});
-
-When("I call focus with name {string}", async function (this: RoleXWorld, name: string) {
- await this.run(this.individualSystem, "focus", { name });
-});
-
-// ===== explore =====
-
-When("I call explore", async function (this: RoleXWorld) {
- await this.run(this.individualSystem, "explore", {});
-});
-
-When("I call explore with name {string}", async function (this: RoleXWorld, name: string) {
- await this.run(this.individualSystem, "explore", { name });
-});
-
-// ===== design =====
-
-When("I design {string} with:", async function (this: RoleXWorld, name: string, source: string) {
- await this.run(this.individualSystem, "design", { name, source });
-});
-
-// ===== todo =====
-
-When("I todo {string} with:", async function (this: RoleXWorld, name: string, source: string) {
- await this.run(this.individualSystem, "todo", { name, source });
-});
-
-// ===== finish =====
-
-When("I finish {string}", async function (this: RoleXWorld, name: string) {
- await this.run(this.individualSystem, "finish", { name });
-});
-
-When(
- "I finish {string} with conclusion:",
- async function (this: RoleXWorld, name: string, conclusion: string) {
- await this.run(this.individualSystem, "finish", { name, conclusion });
- }
-);
-
-// ===== achieve =====
-
-When(
- "I achieve with experience {string}:",
- async function (this: RoleXWorld, name: string, source: string) {
- await this.run(this.individualSystem, "achieve", {
- experience: { name, source },
- });
- }
-);
-
-// ===== abandon =====
-
-When("I abandon", async function (this: RoleXWorld) {
- await this.run(this.individualSystem, "abandon", {});
-});
-
-When(
- "I abandon with experience {string}:",
- async function (this: RoleXWorld, name: string, source: string) {
- await this.run(this.individualSystem, "abandon", {
- experience: { name, source },
- });
- }
-);
-
-// ===== reflect =====
-
-// Single insight
-When(
- /^I reflect on "([^"]*)" to produce "([^"]*)" with:$/,
- async function (this: RoleXWorld, insight: string, knowledgeName: string, source: string) {
- await this.run(this.individualSystem, "reflect", {
- experienceNames: [insight],
- knowledgeName,
- knowledgeSource: source,
- });
- }
-);
-
-// Two insights (comma-separated)
-When(
- /^I reflect on "([^"]*)", "([^"]*)" to produce "([^"]*)" with:$/,
- async function (
- this: RoleXWorld,
- insight1: string,
- insight2: string,
- knowledgeName: string,
- source: string
- ) {
- await this.run(this.individualSystem, "reflect", {
- experienceNames: [insight1, insight2],
- knowledgeName,
- knowledgeSource: source,
- });
- }
-);
-
-// ===== contemplate =====
-
-When(
- /^I contemplate on "([^"]*)", "([^"]*)" to produce "([^"]*)" with:$/,
- async function (
- this: RoleXWorld,
- pattern1: string,
- pattern2: string,
- theoryName: string,
- source: string
- ) {
- await this.run(this.individualSystem, "contemplate", {
- patternNames: [pattern1, pattern2],
- theoryName,
- theorySource: source,
- });
- }
-);
-
-// ===== forget =====
-
-When(/^I forget knowledge\.pattern "([^"]*)"$/, async function (this: RoleXWorld, name: string) {
- await this.run(this.individualSystem, "forget", { type: "knowledge.pattern", name });
-});
-
-When(/^I forget experience\.insight "([^"]*)"$/, async function (this: RoleXWorld, name: string) {
- await this.run(this.individualSystem, "forget", { type: "experience.insight", name });
-});
diff --git a/bdd/steps/mcp.steps.ts b/bdd/steps/mcp.steps.ts
new file mode 100644
index 0000000..1326043
--- /dev/null
+++ b/bdd/steps/mcp.steps.ts
@@ -0,0 +1,60 @@
+/**
+ * MCP step definitions — tool listing, tool calls, result assertions.
+ */
+
+import { strict as assert } from "node:assert";
+import { type DataTable, Given, Then, When } from "@deepracticex/bdd";
+import type { BddWorld } from "../support/world";
+
+// ===== Setup =====
+
+Given("the MCP server is running", async function (this: BddWorld) {
+ await this.connect();
+});
+
+Given("the MCP server is running via npx", async function (this: BddWorld) {
+ await this.connectNpx();
+});
+
+// ===== Tool listing =====
+
+Then("the following tools should be available:", async function (this: BddWorld, table: DataTable) {
+ const { tools } = await this.client.listTools();
+ const names = tools.map((t) => t.name);
+ const expected = table.rows().map((row) => row[0]);
+
+ for (const name of expected) {
+ assert.ok(names.includes(name), `Tool "${name}" not found. Available: ${names.join(", ")}`);
+ }
+});
+
+// ===== Tool calls =====
+
+When(
+ "I call tool {string} with:",
+ async function (this: BddWorld, toolName: string, table: DataTable) {
+ try {
+ this.error = undefined;
+ const args = table.rowsHash();
+ const result = await this.client.callTool({
+ name: toolName,
+ arguments: args,
+ });
+ const content = result.content as Array<{ type: string; text: string }>;
+ this.toolResult = content.map((c) => c.text).join("\n");
+ } catch (e) {
+ this.error = e as Error;
+ this.toolResult = undefined;
+ }
+ }
+);
+
+// ===== Result assertions =====
+
+Then("the tool result should contain {string}", function (this: BddWorld, text: string) {
+ assert.ok(this.toolResult, "Expected a tool result but got none");
+ assert.ok(
+ this.toolResult.includes(text),
+ `Expected result to contain "${text}" but got:\n${this.toolResult.slice(0, 500)}`
+ );
+});
diff --git a/bdd/steps/org.steps.ts b/bdd/steps/org.steps.ts
deleted file mode 100644
index e485056..0000000
--- a/bdd/steps/org.steps.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Organization + Governance steps — found, dissolve, rule, establish,
- * hire, fire, appoint, dismiss, abolish, assign, directory.
- */
-
-import { When } from "@deepractice/bdd";
-import type { RoleXWorld } from "../support/world";
-
-// ===== Organization System =====
-
-When("I found org {string} with:", async function (this: RoleXWorld, name: string, source: string) {
- await this.run(this.orgSystem, "found", { name, source });
-});
-
-When("I dissolve org {string}", async function (this: RoleXWorld, name: string) {
- await this.run(this.orgSystem, "dissolve", { name });
-});
-
-// ===== Governance System =====
-
-When(
- "I rule {string} charter {string} with:",
- async function (this: RoleXWorld, org: string, name: string, source: string) {
- await this.run(this.govSystem, "rule", { orgName: org, name, source });
- }
-);
-
-When(
- "I establish position {string} in {string} with:",
- async function (this: RoleXWorld, name: string, org: string, source: string) {
- await this.run(this.govSystem, "establish", { orgName: org, name, source });
- }
-);
-
-When(
- "I assign duty {string} to {string} with:",
- async function (this: RoleXWorld, name: string, position: string, source: string) {
- await this.run(this.govSystem, "assign", { positionName: position, name, source });
- }
-);
-
-When("I hire {string} into {string}", async function (this: RoleXWorld, role: string, org: string) {
- await this.run(this.govSystem, "hire", { orgName: org, roleName: role });
-});
-
-When("I fire {string} from {string}", async function (this: RoleXWorld, role: string, org: string) {
- await this.run(this.govSystem, "fire", { orgName: org, roleName: role });
-});
-
-When(
- "I appoint {string} to {string}",
- async function (this: RoleXWorld, role: string, position: string) {
- await this.run(this.govSystem, "appoint", { roleName: role, positionName: position });
- }
-);
-
-When(
- "I dismiss {string} from {string}",
- async function (this: RoleXWorld, role: string, position: string) {
- await this.run(this.govSystem, "dismiss", { roleName: role, positionName: position });
- }
-);
-
-When(
- "I abolish position {string} in {string}",
- async function (this: RoleXWorld, name: string, org: string) {
- await this.run(this.govSystem, "abolish", { orgName: org, name });
- }
-);
-
-When("I query directory of {string}", async function (this: RoleXWorld, org: string) {
- await this.run(this.govSystem, "directory", { orgName: org });
-});
diff --git a/bdd/steps/role.steps.ts b/bdd/steps/role.steps.ts
index 2495e33..ee2ae33 100644
--- a/bdd/steps/role.steps.ts
+++ b/bdd/steps/role.steps.ts
@@ -1,35 +1,185 @@
/**
- * Role System steps — born, teach, train, retire, kill.
+ * Role API steps — operate through the Role handle (activate → want → plan → ...).
+ *
+ * Role methods return rendered 3-layer text (status + hint + projection).
+ * Assertions check string content, not object properties.
+ *
+ * Note: Given/When/Then are interchangeable for matching — register each pattern once.
*/
-import { When } from "@deepractice/bdd";
-import type { RoleXWorld } from "../support/world";
+import { strict as assert } from "node:assert";
+import { Given, Then, When } from "@deepracticex/bdd";
+import type { BddWorld } from "../support/world";
+
+// ===== Activate =====
+
+Given("I activate role {string}", async function (this: BddWorld, id: string) {
+ this.role = await this.rolex!.activate(id);
+});
+
+// ===== Execution =====
+
+Given("I want goal {string} with {string}", function (this: BddWorld, id: string, content: string) {
+ this.toolResult = this.role!.want(content, id);
+});
+
+Given("I plan {string} with {string}", function (this: BddWorld, id: string, content: string) {
+ this.toolResult = this.role!.plan(content, id);
+});
+
+Given("I todo {string} with {string}", function (this: BddWorld, id: string, content: string) {
+ this.toolResult = this.role!.todo(content, id);
+});
+
+// ===== Finish =====
+
+Given(
+ "I finish {string} with encounter {string}",
+ function (this: BddWorld, taskId: string, encounter: string) {
+ this.toolResult = this.role!.finish(taskId, encounter);
+ }
+);
+
+When("I finish {string} without encounter", function (this: BddWorld, taskId: string) {
+ this.toolResult = this.role!.finish(taskId);
+});
+
+// ===== Complete / Abandon =====
When(
- "I born a role {string} with:",
- async function (this: RoleXWorld, name: string, source: string) {
- await this.run(this.roleSystem, "born", { name, source });
+ "I complete plan {string} with encounter {string}",
+ function (this: BddWorld, planId: string, encounter: string) {
+ this.toolResult = this.role!.complete(planId, encounter);
}
);
When(
- "I teach {string} knowledge {string} with:",
- async function (this: RoleXWorld, role: string, name: string, source: string) {
- await this.run(this.roleSystem, "teach", { roleId: role, name, source });
+ "I abandon plan {string} with encounter {string}",
+ function (this: BddWorld, planId: string, encounter: string) {
+ this.toolResult = this.role!.abandon(planId, encounter);
+ }
+);
+
+// ===== Focus =====
+
+When("I focus on {string}", function (this: BddWorld, goalId: string) {
+ this.toolResult = this.role!.focus(goalId);
+});
+
+// ===== Cognition: Reflect =====
+
+Given(
+ "I reflect on {string} as {string} with {string}",
+ function (this: BddWorld, encounterId: string, expId: string, content: string) {
+ this.toolResult = this.role!.reflect([encounterId], content, expId);
}
);
When(
- "I train {string} procedure {string} with:",
- async function (this: RoleXWorld, role: string, name: string, source: string) {
- await this.run(this.roleSystem, "train", { roleId: role, name, source });
+ "I reflect directly as {string} with {string}",
+ function (this: BddWorld, expId: string, content: string) {
+ this.toolResult = this.role!.reflect([], content, expId);
}
);
-When("I retire role {string}", async function (this: RoleXWorld, name: string) {
- await this.run(this.roleSystem, "retire", { name });
+// ===== Cognition: Realize =====
+
+Given(
+ "I realize from {string} as {string} with {string}",
+ function (this: BddWorld, expId: string, principleId: string, content: string) {
+ this.toolResult = this.role!.realize([expId], content, principleId);
+ }
+);
+
+When(
+ "I realize directly as {string} with {string}",
+ function (this: BddWorld, principleId: string, content: string) {
+ this.toolResult = this.role!.realize([], content, principleId);
+ }
+);
+
+// ===== Cognition: Master =====
+
+Given(
+ "I master from {string} as {string} with {string}",
+ function (this: BddWorld, expId: string, procId: string, content: string) {
+ this.toolResult = this.role!.master(content, procId, [expId]);
+ }
+);
+
+When(
+ "I master directly as {string} with {string}",
+ function (this: BddWorld, procId: string, content: string) {
+ this.toolResult = this.role!.master(content, procId);
+ }
+);
+
+// ===== Knowledge management =====
+
+When("I forget {string}", function (this: BddWorld, nodeId: string) {
+ this.toolResult = this.role!.forget(nodeId);
+});
+
+// ===== Output assertions =====
+
+Then("the output should contain {string}", function (this: BddWorld, text: string) {
+ assert.ok(this.toolResult, "No output captured");
+ assert.ok(
+ this.toolResult.includes(text),
+ `Expected output to contain "${text}" but got:\n${this.toolResult.slice(0, 500)}`
+ );
+});
+
+// ===== Context assertions =====
+
+Then("focusedPlanId should be {string}", function (this: BddWorld, planId: string) {
+ assert.ok(this.role, "No active role");
+ assert.equal(this.role.ctx.focusedPlanId, planId);
+});
+
+Then("focusedPlanId should be null", function (this: BddWorld) {
+ assert.ok(this.role, "No active role");
+ assert.equal(this.role.ctx.focusedPlanId, null);
+});
+
+Then("encounter {string} should be registered", function (this: BddWorld, encounterId: string) {
+ assert.ok(this.role, "No active role");
+ assert.ok(
+ this.role.ctx.encounterIds.has(encounterId),
+ `Encounter "${encounterId}" not registered. Have: ${[...this.role.ctx.encounterIds].join(", ")}`
+ );
+});
+
+Then("encounter {string} should be consumed", function (this: BddWorld, encounterId: string) {
+ assert.ok(this.role, "No active role");
+ assert.ok(
+ !this.role.ctx.encounterIds.has(encounterId),
+ `Encounter "${encounterId}" should be consumed but is still registered`
+ );
+});
+
+Then("encounter count should be {int}", function (this: BddWorld, count: number) {
+ assert.ok(this.role, "No active role");
+ assert.equal(this.role.ctx.encounterIds.size, count);
+});
+
+Then("experience {string} should be registered", function (this: BddWorld, expId: string) {
+ assert.ok(this.role, "No active role");
+ assert.ok(
+ this.role.ctx.experienceIds.has(expId),
+ `Experience "${expId}" not registered. Have: ${[...this.role.ctx.experienceIds].join(", ")}`
+ );
+});
+
+Then("experience {string} should be consumed", function (this: BddWorld, expId: string) {
+ assert.ok(this.role, "No active role");
+ assert.ok(
+ !this.role.ctx.experienceIds.has(expId),
+ `Experience "${expId}" should be consumed but is still registered`
+ );
});
-When("I kill role {string}", async function (this: RoleXWorld, name: string) {
- await this.run(this.roleSystem, "kill", { name });
+Then("experience count should be {int}", function (this: BddWorld, count: number) {
+ assert.ok(this.role, "No active role");
+ assert.equal(this.role.ctx.experienceIds.size, count);
});
diff --git a/bdd/steps/setup.steps.ts b/bdd/steps/setup.steps.ts
deleted file mode 100644
index 556c618..0000000
--- a/bdd/steps/setup.steps.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-/**
- * Common Given steps — platform setup, role/org creation, identity activation.
- */
-
-import { Given } from "@deepractice/bdd";
-import type { RoleXWorld } from "../support/world";
-
-// ========== Fresh Platform ==========
-
-Given("a fresh RoleX platform", function (this: RoleXWorld) {
- this.init();
- // Create society node for explore
- this.graph.addNode("society", "society");
-});
-
-// ========== Role Helpers ==========
-
-Given("role {string} exists", async function (this: RoleXWorld, name: string) {
- await this.roleSystem.execute("born", {
- name,
- source: `Feature: ${name}\n Scenario: Identity\n Given I am ${name}`,
- });
-});
-
-Given(
- "role {string} exists with persona {string}",
- async function (this: RoleXWorld, name: string, persona: string) {
- await this.roleSystem.execute("born", {
- name,
- source: `Feature: ${name}\n Scenario: Identity\n Given ${persona}`,
- });
- }
-);
-
-Given(
- "role {string} has knowledge.pattern {string}",
- async function (this: RoleXWorld, role: string, name: string) {
- await this.roleSystem.execute("teach", {
- roleId: role,
- name,
- source: `Feature: ${name}\n Scenario: Knowledge\n Given I know ${name}`,
- });
- }
-);
-
-Given(
- "role {string} has knowledge.procedure {string}",
- async function (this: RoleXWorld, role: string, name: string) {
- await this.roleSystem.execute("train", {
- roleId: role,
- name,
- source: `Feature: ${name}\n Scenario: Procedure\n Given I can do ${name}`,
- });
- }
-);
-
-// ========== Identity Activation ==========
-
-Given("I am {string}", async function (this: RoleXWorld, name: string) {
- await this.individualSystem.execute("identity", { roleId: name });
-});
-
-// ========== Goal Hierarchy Helpers ==========
-
-Given("I have goal {string}", async function (this: RoleXWorld, name: string) {
- await this.individualSystem.execute("want", {
- name,
- source: `Feature: ${name}\n Scenario: Goal\n Given I want ${name}`,
- });
-});
-
-Given(
- "I have goal {string} with plan {string}",
- async function (this: RoleXWorld, goal: string, plan: string) {
- await this.individualSystem.execute("want", {
- name: goal,
- source: `Feature: ${goal}\n Scenario: Goal\n Given I want ${goal}`,
- });
- await this.individualSystem.execute("design", {
- name: plan,
- source: `Feature: ${plan}\n Scenario: Plan\n Given plan for ${goal}`,
- });
- }
-);
-
-Given(
- "I have goal {string} with plan {string} and task {string}",
- async function (this: RoleXWorld, goal: string, plan: string, task: string) {
- await this.individualSystem.execute("want", {
- name: goal,
- source: `Feature: ${goal}\n Scenario: Goal\n Given I want ${goal}`,
- });
- await this.individualSystem.execute("design", {
- name: plan,
- source: `Feature: ${plan}\n Scenario: Plan\n Given plan for ${goal}`,
- });
- await this.individualSystem.execute("todo", {
- name: task,
- source: `Feature: ${task}\n Scenario: Task\n Given do ${task}`,
- });
- }
-);
-
-Given("I have goal {string} without plan", async function (this: RoleXWorld, goal: string) {
- await this.individualSystem.execute("want", {
- name: goal,
- source: `Feature: ${goal}\n Scenario: Goal\n Given I want ${goal}`,
- });
-});
-
-Given("I have a finished goal {string}", async function (this: RoleXWorld, goal: string) {
- await this.individualSystem.execute("want", {
- name: goal,
- source: `Feature: ${goal}\n Scenario: Goal\n Given I want ${goal}`,
- });
- await this.individualSystem.execute("design", {
- name: `${goal}-plan`,
- source: `Feature: ${goal} Plan\n Scenario: Plan\n Given plan it`,
- });
- await this.individualSystem.execute("todo", {
- name: `${goal}-task`,
- source: `Feature: ${goal} Task\n Scenario: Task\n Given do it`,
- });
- await this.individualSystem.execute("finish", {
- name: `${goal}-task`,
- conclusion: `Feature: Done\n Scenario: Result\n Given completed`,
- });
-});
-
-// ========== Knowledge & Experience Helpers ==========
-
-Given("I have experience.insight {string}", async function (this: RoleXWorld, name: string) {
- // Create insight via a quick goal cycle
- const goalName = `_insight-goal-${name}`;
- await this.individualSystem.execute("want", {
- name: goalName,
- source: `Feature: ${goalName}\n Scenario: Temp\n Given temp goal for insight`,
- });
- await this.individualSystem.execute("design", {
- name: `${goalName}-plan`,
- source: `Feature: Plan\n Scenario: Plan\n Given plan`,
- });
- await this.individualSystem.execute("todo", {
- name: `${goalName}-task`,
- source: `Feature: Task\n Scenario: Task\n Given task`,
- });
- await this.individualSystem.execute("finish", { name: `${goalName}-task` });
- await this.individualSystem.execute("achieve", {
- experience: {
- name,
- source: `Feature: ${name}\n Scenario: Insight\n Given learned from ${name}`,
- },
- });
-});
-
-Given(/^I have knowledge\.pattern "([^"]*)"$/, async function (this: RoleXWorld, name: string) {
- // Get active role from graph
- const roleKey = this.graph.getNode("society") ? this.individualSystem.ctx?.structure : undefined;
- if (roleKey) {
- await this.roleSystem.execute("teach", {
- roleId: roleKey,
- name,
- source: `Feature: ${name}\n Scenario: Pattern\n Given ${name} principle`,
- });
- }
-});
-
-Given(
- /^I have knowledge\.pattern "([^"]*)" with:$/,
- async function (this: RoleXWorld, name: string, source: string) {
- const roleKey = this.individualSystem.ctx?.structure;
- if (roleKey) {
- await this.roleSystem.execute("teach", {
- roleId: roleKey,
- name,
- source,
- });
- }
- }
-);
-
-// ========== Org Helpers ==========
-
-Given("org {string} exists", async function (this: RoleXWorld, name: string) {
- await this.orgSystem.execute("found", {
- name,
- source: `Feature: ${name}\n Scenario: Charter\n Given org ${name}`,
- });
-});
-
-Given(
- "org {string} exists with charter {string}",
- async function (this: RoleXWorld, name: string, charter: string) {
- await this.orgSystem.execute("found", {
- name,
- source: `Feature: ${name}\n Scenario: Charter\n Given ${charter}`,
- });
- }
-);
-
-Given(
- "org {string} exists with position {string} and member {string}",
- async function (this: RoleXWorld, org: string, position: string, member: string) {
- await this.orgSystem.execute("found", {
- name: org,
- source: `Feature: ${org}\n Scenario: Charter\n Given org ${org}`,
- });
- await this.govSystem.execute("establish", {
- orgName: org,
- name: position,
- source: `Feature: ${position}\n Scenario: Duties\n Given ${position} duties`,
- });
- // Ensure member role exists
- if (!this.graph.hasNode(member)) {
- await this.roleSystem.execute("born", {
- name: member,
- source: `Feature: ${member}\n Scenario: Id\n Given I am ${member}`,
- });
- }
- await this.govSystem.execute("hire", { orgName: org, roleName: member });
- }
-);
-
-Given(
- "position {string} exists in {string}",
- async function (this: RoleXWorld, position: string, org: string) {
- if (!this.graph.hasNode(`${org}/${position}`)) {
- await this.govSystem.execute("establish", {
- orgName: org,
- name: position,
- source: `Feature: ${position}\n Scenario: Duties\n Given ${position} duties`,
- });
- }
- }
-);
-
-Given(
- "{string} is a member of {string}",
- async function (this: RoleXWorld, role: string, org: string) {
- if (!this.graph.hasEdge(org, role)) {
- await this.govSystem.execute("hire", { orgName: org, roleName: role });
- }
- }
-);
-
-Given(
- "{string} is assigned to {string}",
- async function (this: RoleXWorld, role: string, position: string) {
- // Ensure membership first
- const orgName = position.split("/")[0];
- if (!this.graph.hasEdge(orgName, role)) {
- await this.govSystem.execute("hire", { orgName, roleName: role });
- }
- if (!this.graph.hasEdge(position, role)) {
- await this.govSystem.execute("appoint", { roleName: role, positionName: position });
- }
- }
-);
diff --git a/bdd/support/world.ts b/bdd/support/world.ts
index ac14ab1..148c643 100644
--- a/bdd/support/world.ts
+++ b/bdd/support/world.ts
@@ -1,87 +1,126 @@
/**
- * RoleX BDD World — test context for all systems.
+ * Unified BDD World — test context for all RoleX BDD tests.
*
- * Each scenario gets a fresh platform, graph, and all four systems.
+ * Combines three layers:
+ * - MCP (dev): local source via `bun run src/index.ts`
+ * - MCP (npx): published package via `npx @rolexjs/mcp-server`
+ * - Rolex: in-process Rolex API with file-based persistence
+ *
+ * Each scenario gets a fresh World instance. MCP clients are shared (expensive startup).
*/
-import { setWorldConstructor, World } from "@deepractice/bdd";
-import type { Feature, Platform, SerializedGraph } from "@rolexjs/core";
-import {
- createGovernanceSystem,
- createIndividualSystem,
- createOrgSystem,
- createRoleSystem,
- RoleXGraph,
-} from "@rolexjs/core";
-import type { RunnableSystem } from "@rolexjs/system";
-
-// ========== MemoryPlatform ==========
-
-export class MemoryPlatform implements Platform {
- private graph: SerializedGraph = { nodes: [], edges: [] };
- private content = new Map();
- private settings: Record = {};
-
- loadGraph(): SerializedGraph {
- return this.graph;
- }
- saveGraph(graph: SerializedGraph): void {
- this.graph = graph;
- }
- writeContent(key: string, content: Feature): void {
- this.content.set(key, content);
- }
- readContent(key: string): Feature | null {
- return this.content.get(key) ?? null;
- }
- removeContent(key: string): void {
- this.content.delete(key);
- }
- readSettings(): Record {
- return this.settings;
- }
- writeSettings(settings: Record): void {
- this.settings = { ...this.settings, ...settings };
+import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { After, AfterAll, setWorldConstructor, World } from "@deepracticex/bdd";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
+import { localPlatform } from "@rolexjs/local-platform";
+import type { Role, Rolex } from "rolexjs";
+import { createRoleX } from "rolexjs";
+
+// ========== MCP client management ==========
+
+const SERVER_PATH = join(import.meta.dirname, "../../apps/mcp-server/src/index.ts");
+
+interface McpConnection {
+ client: Client;
+ transport: StdioClientTransport;
+}
+
+const connections = new Map();
+
+async function ensureMcpClient(mode: "dev" | "npx"): Promise {
+ const existing = connections.get(mode);
+ if (existing) return existing.client;
+
+ let transport: StdioClientTransport;
+ if (mode === "npx") {
+ // Run npx from /tmp to avoid workspace overrides conflict
+ transport = new StdioClientTransport({
+ command: "npx",
+ args: ["@rolexjs/mcp-server@dev"],
+ cwd: tmpdir(),
+ });
+ } else {
+ transport = new StdioClientTransport({
+ command: "bun",
+ args: ["run", SERVER_PATH],
+ });
}
+
+ const client = new Client({
+ name: `rolex-bdd-${mode}`,
+ version: "1.0.0",
+ });
+
+ await client.connect(transport);
+ connections.set(mode, { client, transport });
+ return client;
}
+AfterAll(async () => {
+ for (const [, conn] of connections) {
+ await conn.client.close();
+ await conn.transport.close();
+ }
+ connections.clear();
+});
+
// ========== World ==========
-export class RoleXWorld extends World {
- platform!: MemoryPlatform;
- graph!: RoleXGraph;
- roleSystem!: RunnableSystem;
- individualSystem!: RunnableSystem;
- orgSystem!: RunnableSystem;
- govSystem!: RunnableSystem;
-
- /** Last operation result */
- result?: string;
- /** Last operation error */
+export class BddWorld extends World {
+ // --- MCP layer ---
+ client!: Client;
+ toolResult?: string;
+ tools?: Array<{ name: string }>;
+
+ // --- Rolex layer ---
+ dataDir?: string;
+ rolex?: Rolex;
+ role?: Role;
+ directResult?: string;
+ directRaw?: any;
+
+ // --- Shared ---
error?: Error;
- /** Initialize fresh systems */
- init(): void {
- this.platform = new MemoryPlatform();
- this.graph = new RoleXGraph();
- this.roleSystem = createRoleSystem(this.graph, this.platform);
- this.individualSystem = createIndividualSystem(this.graph, this.platform);
- this.orgSystem = createOrgSystem(this.graph, this.platform);
- this.govSystem = createGovernanceSystem(this.graph, this.platform);
- this.result = undefined;
- this.error = undefined;
+ /** Connect to MCP server (dev mode — local source). */
+ async connect(): Promise {
+ this.client = await ensureMcpClient("dev");
+ }
+
+ /** Connect to MCP server (npx mode — published package). */
+ async connectNpx(): Promise {
+ this.client = await ensureMcpClient("npx");
+ }
+
+ /** Initialize Rolex with a temp data directory for persistence tests. */
+ initRolex(): void {
+ this.dataDir = join(tmpdir(), `rolex-bdd-${Date.now()}-${Math.random().toString(36).slice(2)}`);
+ mkdirSync(this.dataDir, { recursive: true });
+ this.rolex = createRoleX(localPlatform({ dataDir: this.dataDir, resourceDir: null }));
}
- /** Execute and capture result or error */
- async run(system: RunnableSystem, process: string, args: unknown): Promise {
- try {
- this.error = undefined;
- this.result = await system.execute(process, args);
- } catch (e) {
- this.error = e as Error;
- this.result = undefined;
- }
+ /** Write persisted context JSON directly (simulate a previous session). */
+ writeContext(roleId: string, data: Record): void {
+ if (!this.dataDir) throw new Error("Call initRolex() first");
+ const contextDir = join(this.dataDir, "context");
+ mkdirSync(contextDir, { recursive: true });
+ writeFileSync(join(contextDir, `${roleId}.json`), JSON.stringify(data, null, 2), "utf-8");
+ }
+
+ /** Re-create Rolex instance (simulate new session with same dataDir). */
+ newSession(): void {
+ if (!this.dataDir) throw new Error("Call initRolex() first");
+ this.rolex = createRoleX(localPlatform({ dataDir: this.dataDir, resourceDir: null }));
}
}
-setWorldConstructor(RoleXWorld);
+After(function (this: BddWorld) {
+ if (this.dataDir && existsSync(this.dataDir)) {
+ rmSync(this.dataDir, { recursive: true });
+ }
+});
+
+setWorldConstructor(BddWorld);
diff --git a/biome.json b/biome.json
index faba023..1896ecd 100644
--- a/biome.json
+++ b/biome.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://biomejs.dev/schemas/2.4.0/schema.json",
+ "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
"files": {
"includes": [
"**",
diff --git a/bun.lock b/bun.lock
index d8a8523..cb928d9 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,14 +5,17 @@
"": {
"name": "rolexjs",
"dependencies": {
- "@resourcexjs/node-provider": "^2.13.0",
- "resourcexjs": "^2.13.0",
+ "@resourcexjs/core": "^2.17.2",
+ "@resourcexjs/node-provider": "^2.17.2",
+ "resourcexjs": "^2.17.2",
},
"devDependencies": {
"@biomejs/biome": "^2.4.0",
"@changesets/cli": "^2.29.7",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
+ "@deepracticex/bdd": "^0.2.0",
+ "@modelcontextprotocol/sdk": "^1.27.1",
"@rolexjs/core": "workspace:*",
"@types/node": "^24.9.2",
"bun-dts": "0.1.70",
@@ -23,19 +26,6 @@
"typescript": "^5.9.3",
},
},
- "apps/cli": {
- "name": "@rolexjs/cli",
- "version": "0.11.0",
- "bin": {
- "rolex": "./dist/index.js",
- },
- "dependencies": {
- "@rolexjs/local-platform": "workspace:*",
- "citty": "^0.1.6",
- "consola": "^3.4.2",
- "rolexjs": "workspace:*",
- },
- },
"apps/mcp-server": {
"name": "@rolexjs/mcp-server",
"version": "0.11.0",
@@ -43,29 +33,49 @@
"rolex-mcp": "./dist/index.js",
},
"dependencies": {
+ "@rolexjs/genesis": "workspace:*",
"@rolexjs/local-platform": "workspace:*",
"fastmcp": "^3.0.0",
"rolexjs": "workspace:*",
"zod": "^3.25.0",
},
+ "devDependencies": {
+ "@modelcontextprotocol/sdk": "^1.27.1",
+ },
+ },
+ "bdd": {
+ "name": "@rolexjs/bdd",
+ "version": "0.0.0",
+ "devDependencies": {
+ "@deepracticex/bdd": "^0.2.0",
+ "@modelcontextprotocol/sdk": "^1.27.1",
+ "@rolexjs/core": "workspace:*",
+ "@rolexjs/local-platform": "workspace:*",
+ "rolexjs": "workspace:*",
+ },
},
"packages/core": {
"name": "@rolexjs/core",
"version": "0.11.0",
"dependencies": {
+ "@resourcexjs/core": "^2.14.0",
"@rolexjs/system": "workspace:*",
- "resourcexjs": "^2.13.0",
},
},
+ "packages/genesis": {
+ "name": "@rolexjs/genesis",
+ "version": "0.1.0",
+ },
"packages/local-platform": {
"name": "@rolexjs/local-platform",
"version": "0.11.0",
"dependencies": {
- "@resourcexjs/node-provider": "^2.13.0",
+ "@deepracticex/drizzle": "^0.2.0",
+ "@deepracticex/sqlite": "^0.2.0",
+ "@resourcexjs/node-provider": "^2.14.0",
"@rolexjs/core": "workspace:*",
- "@rolexjs/resourcex-types": "workspace:*",
"@rolexjs/system": "workspace:*",
- "resourcexjs": "^2.13.0",
+ "drizzle-orm": "^0.45.1",
},
},
"packages/parser": {
@@ -76,11 +86,14 @@
"@cucumber/messages": "^32.0.0",
},
},
- "packages/resourcex-types": {
- "name": "@rolexjs/resourcex-types",
+ "packages/prototype": {
+ "name": "@rolexjs/prototype",
"version": "0.11.0",
"dependencies": {
- "resourcexjs": "^2.13.0",
+ "@rolexjs/core": "workspace:*",
+ "@rolexjs/parser": "workspace:*",
+ "@rolexjs/system": "workspace:*",
+ "resourcexjs": "^2.14.0",
},
},
"packages/rolexjs": {
@@ -89,8 +102,9 @@
"dependencies": {
"@rolexjs/core": "workspace:*",
"@rolexjs/parser": "workspace:*",
+ "@rolexjs/prototype": "workspace:*",
"@rolexjs/system": "workspace:*",
- "resourcexjs": "^2.13.0",
+ "resourcexjs": "^2.14.0",
},
"devDependencies": {
"@rolexjs/local-platform": "workspace:*",
@@ -102,8 +116,8 @@
},
},
"overrides": {
- "@resourcexjs/node-provider": "^2.13.0",
- "resourcexjs": "^2.13.0",
+ "@resourcexjs/node-provider": "^2.14.0",
+ "resourcexjs": "^2.14.0",
},
"packages": {
"@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.32", "", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "@types/lodash-es": "^4.17.12", "commander": "^12.1.0", "lodash-es": "^4.17.22", "shell-quote": "^1.8.3", "zod": "^3.24.1" }, "bin": { "srt": "dist/cli.js" } }, "sha512-+mQDTTxBc72W2xpsRHamruom5Xdo8vswTRlKlEtlhMbYemQgqLdQpULHi33dalTkvbbHmwgugLfQ82IZUzqT4w=="],
@@ -208,10 +222,18 @@
"@commitlint/types": ["@commitlint/types@20.4.0", "", { "dependencies": { "conventional-commits-parser": "^6.2.1", "picocolors": "^1.1.1" } }, "sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw=="],
+ "@cucumber/cucumber-expressions": ["@cucumber/cucumber-expressions@18.1.0", "https://registry.npmmirror.com/@cucumber/cucumber-expressions/-/cucumber-expressions-18.1.0.tgz", { "dependencies": { "regexp-match-indices": "1.0.2" } }, "sha512-9yc+wForrn15FaqLWNjYb19iQ/gPXhcq1kc4X1Ex1lR7NcJpa5pGnCow3bc1HERVM5IoYH+gwwrcJogSMsf+Vw=="],
+
"@cucumber/gherkin": ["@cucumber/gherkin@38.0.0", "", { "dependencies": { "@cucumber/messages": ">=31.0.0 <33" } }, "sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw=="],
"@cucumber/messages": ["@cucumber/messages@32.0.1", "", { "dependencies": { "class-transformer": "0.5.1", "reflect-metadata": "0.2.2" } }, "sha512-1OSoW+GQvFUNAl6tdP2CTBexTXMNJF0094goVUcvugtQeXtJ0K8sCP0xbq7GGoiezs/eJAAOD03+zAPT64orHQ=="],
+ "@deepracticex/bdd": ["@deepracticex/bdd@0.2.0", "", { "dependencies": { "@cucumber/cucumber-expressions": "^18.0.1", "@cucumber/gherkin": "^31.0.0", "@cucumber/messages": "^27.2.0" } }, "sha512-hFLbobvQFx8IQA/lno4VQl0JjBzT09lTSFr5YxmGdHZ03JB7+46OeEwnCRtrwXlRRkDCjciQQ9zL35HI84YmJg=="],
+
+ "@deepracticex/drizzle": ["@deepracticex/drizzle@0.2.3", "", { "dependencies": { "@deepracticex/sqlite": "^0.2.0" }, "peerDependencies": { "drizzle-orm": ">=0.38.0" } }, "sha512-zFyR4VhRjr4kfzhq6YWukUcj/XtJNJmNtnEEvTzeqPVSK1ZGd3Ywtr4GVf9rtVCtRa67j85tBykzN3LUwuy4SQ=="],
+
+ "@deepracticex/sqlite": ["@deepracticex/sqlite@0.2.0", "", {}, "sha512-cBrJbqoN9Oxt2wXQomoxdQT76RL0Hn8yfGAKgyGzxi7vPHZC9UnxoJgfQpidqGGky+067FXRPBB7f0xzNkJ3bg=="],
+
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@@ -286,7 +308,7 @@
"@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="],
- "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="],
+ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
@@ -344,23 +366,25 @@
"@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="],
- "@resourcexjs/arp": ["@resourcexjs/arp@2.13.0", "", {}, "sha512-UsMjOC194PYkbBqPhB1/TN70OSij7xu3aSFbUNNwXbTQmM3LU1av05QFVO5jcUsYcERCNtOY+Cov14iE2fJwsg=="],
+ "@resourcexjs/arp": ["@resourcexjs/arp@2.17.2", "https://registry.npmmirror.com/@resourcexjs/arp/-/arp-2.17.2.tgz", {}, "sha512-DN9sBDICZMpQVwmT4V8U/PSlN289tnrwkZie73kRT7VqbuAdJbEih0JmW2OtPEvHyB1fDlJYZKEtx8l6cCcDzg=="],
- "@resourcexjs/core": ["@resourcexjs/core@2.13.0", "", { "dependencies": { "modern-tar": "^0.7.3", "zod": "^4.3.6" } }, "sha512-6y+tEd1t7X/GjkJt1XW47zCUPjzMdE2VPjuqOfy4AgwT90pC8cvUwU6p/yNyEuMvu3Vew2wAHpNK4Tr6c1HcwA=="],
+ "@resourcexjs/core": ["@resourcexjs/core@2.17.2", "https://registry.npmmirror.com/@resourcexjs/core/-/core-2.17.2.tgz", { "dependencies": { "modern-tar": "^0.7.3", "zod": "^4.3.6" } }, "sha512-NdsNbBJOnC5a3TNrA8Qn0wCA/qvnYzEgNSGvL9Bq2kIMU6uSmQyTG1FKJvz/0RXgEa60GrOaJTJGvjQzxpjpdg=="],
- "@resourcexjs/node-provider": ["@resourcexjs/node-provider@2.13.0", "", { "dependencies": { "@resourcexjs/core": "^2.13.0" } }, "sha512-lqXZ73KN5tGp8czdc03+7TOUBfA5RNzBT3+Dbt2KYSJYSWWWFcZg6MZjsWoI0Kp3Xx59Ll3GqbyWqm4il/U41g=="],
+ "@resourcexjs/node-provider": ["@resourcexjs/node-provider@2.17.2", "https://registry.npmmirror.com/@resourcexjs/node-provider/-/node-provider-2.17.2.tgz", { "dependencies": { "@resourcexjs/core": "^2.17.2" } }, "sha512-QlCWHhpBOJWbigYoR1wB3oKDLxgxC7g4mp0PRd6QuXCXQzpj3XOwKic8549/FmHN3LRfcn3gLahfCTtuEkH2EQ=="],
- "@rolexjs/cli": ["@rolexjs/cli@workspace:apps/cli"],
+ "@rolexjs/bdd": ["@rolexjs/bdd@workspace:bdd"],
"@rolexjs/core": ["@rolexjs/core@workspace:packages/core"],
+ "@rolexjs/genesis": ["@rolexjs/genesis@workspace:packages/genesis"],
+
"@rolexjs/local-platform": ["@rolexjs/local-platform@workspace:packages/local-platform"],
"@rolexjs/mcp-server": ["@rolexjs/mcp-server@workspace:apps/mcp-server"],
"@rolexjs/parser": ["@rolexjs/parser@workspace:packages/parser"],
- "@rolexjs/resourcex-types": ["@rolexjs/resourcex-types@workspace:packages/resourcex-types"],
+ "@rolexjs/prototype": ["@rolexjs/prototype@workspace:packages/prototype"],
"@rolexjs/system": ["@rolexjs/system@workspace:packages/system"],
@@ -440,6 +464,8 @@
"@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="],
+ "@types/uuid": ["@types/uuid@10.0.0", "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="],
+
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
@@ -494,8 +520,6 @@
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
- "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
-
"class-transformer": ["class-transformer@0.5.1", "", {}, "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
@@ -560,6 +584,8 @@
"dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
+ "drizzle-orm": ["drizzle-orm@0.45.1", "https://registry.npmmirror.com/drizzle-orm/-/drizzle-orm-0.45.1.tgz", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="],
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
@@ -928,13 +954,17 @@
"reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
+ "regexp-match-indices": ["regexp-match-indices@1.0.2", "https://registry.npmmirror.com/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", { "dependencies": { "regexp-tree": "^0.1.11" } }, "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ=="],
+
+ "regexp-tree": ["regexp-tree@0.1.27", "https://registry.npmmirror.com/regexp-tree/-/regexp-tree-0.1.27.tgz", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="],
+
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
- "resourcexjs": ["resourcexjs@2.13.0", "", { "dependencies": { "@resourcexjs/arp": "^2.13.0", "@resourcexjs/core": "^2.13.0", "sandboxxjs": "^0.5.1" } }, "sha512-ZWCBnNVLQqYz9BceJyf9MBvfJCuH7jgxbgHGi4quu1nwk9JzMKkBCAISYt/dIXWpyki1ey6PuIeJd79pRNmNCQ=="],
+ "resourcexjs": ["resourcexjs@2.17.2", "https://registry.npmmirror.com/resourcexjs/-/resourcexjs-2.17.2.tgz", { "dependencies": { "@resourcexjs/arp": "^2.17.2", "@resourcexjs/core": "^2.17.2", "sandboxxjs": "^0.5.1" } }, "sha512-xQYMbQ5V107mdTQ/29M6ntAQsdPIcanVvyXd6Iu1SuHihIFp/Fv2whQzQZvEN1KuwJZBFZqVrjYGALnI0lTmsw=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
@@ -1070,6 +1100,8 @@
"uri-templates": ["uri-templates@0.2.0", "", {}, "sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg=="],
+ "uuid": ["uuid@11.0.5", "https://registry.npmmirror.com/uuid/-/uuid-11.0.5.tgz", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA=="],
+
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -1094,6 +1126,10 @@
"@anthropic-ai/sandbox-runtime/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
+ "@deepracticex/bdd/@cucumber/gherkin": ["@cucumber/gherkin@31.0.0", "https://registry.npmmirror.com/@cucumber/gherkin/-/gherkin-31.0.0.tgz", { "dependencies": { "@cucumber/messages": ">=19.1.4 <=26" } }, "sha512-wlZfdPif7JpBWJdqvHk1Mkr21L5vl4EfxVUOS4JinWGf3FLRV6IKUekBv5bb5VX79fkDcfDvESzcQ8WQc07Wgw=="],
+
+ "@deepracticex/bdd/@cucumber/messages": ["@cucumber/messages@27.2.0", "https://registry.npmmirror.com/@cucumber/messages/-/messages-27.2.0.tgz", { "dependencies": { "@types/uuid": "10.0.0", "class-transformer": "0.5.1", "reflect-metadata": "0.2.2", "uuid": "11.0.5" } }, "sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA=="],
+
"@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="],
"@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
@@ -1106,8 +1142,12 @@
"@resourcexjs/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+ "@sandboxxjs/state/resourcexjs": ["resourcexjs@2.14.0", "https://registry.npmmirror.com/resourcexjs/-/resourcexjs-2.14.0.tgz", { "dependencies": { "@resourcexjs/arp": "^2.14.0", "@resourcexjs/core": "^2.14.0", "sandboxxjs": "^0.5.1" } }, "sha512-EGM88QusNIBNKv+PIeyYWBwTxmUK81F+A7jx/SEW6J+CI0JMuPAEPfQ+/i751CJbMlNwHd5ynPiLQ+/WGrDkeA=="],
+
"conventional-commits-parser/meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
+ "fastmcp/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="],
+
"fastmcp/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
"fastmcp/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
@@ -1134,6 +1174,12 @@
"tsup/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
+ "@deepracticex/bdd/@cucumber/gherkin/@cucumber/messages": ["@cucumber/messages@26.0.1", "https://registry.npmmirror.com/@cucumber/messages/-/messages-26.0.1.tgz", { "dependencies": { "@types/uuid": "10.0.0", "class-transformer": "0.5.1", "reflect-metadata": "0.2.2", "uuid": "10.0.0" } }, "sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg=="],
+
+ "@sandboxxjs/state/resourcexjs/@resourcexjs/arp": ["@resourcexjs/arp@2.14.0", "https://registry.npmmirror.com/@resourcexjs/arp/-/arp-2.14.0.tgz", {}, "sha512-Kw3g7R3/Fkv3ce2Nl31vgv6PLwNJPXkbeya5//rpuR32LyXcWogF0lrGCskcepqhyJnMeAXOO5ZX0paJBVU1Yw=="],
+
+ "@sandboxxjs/state/resourcexjs/@resourcexjs/core": ["@resourcexjs/core@2.14.0", "https://registry.npmmirror.com/@resourcexjs/core/-/core-2.14.0.tgz", { "dependencies": { "modern-tar": "^0.7.3", "zod": "^4.3.6" } }, "sha512-3QFWVzeKpVtLVMjEb5Qvsg1xFT0aWGutNBndRNS235itM7MhkDLC+tYrebZNJdBp1QnUlVc8TnWGieLhRD17FA=="],
+
"fastmcp/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
"fastmcp/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -1158,6 +1204,10 @@
"read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
+ "@deepracticex/bdd/@cucumber/gherkin/@cucumber/messages/uuid": ["uuid@10.0.0", "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
+
+ "@sandboxxjs/state/resourcexjs/@resourcexjs/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
"fastmcp/yargs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"fastmcp/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
diff --git a/package.json b/package.json
index ef8584b..1a4c5a7 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"type": "module",
"workspaces": [
"packages/*",
- "apps/*"
+ "apps/*",
+ "bdd"
],
"scripts": {
"build": "turbo build",
@@ -17,7 +18,7 @@
"format:check": "biome format .",
"typecheck": "turbo typecheck",
"test": "turbo test",
- "test:bdd": "deepractice-bdd",
+ "test:bdd": "bun test bdd/",
"clean": "turbo clean && rm -rf node_modules",
"prepare": "lefthook install || true",
"version": "changeset version",
@@ -27,10 +28,12 @@
"license": "MIT",
"packageManager": "bun@1.3.8",
"devDependencies": {
+ "@biomejs/biome": "^2.4.0",
"@changesets/cli": "^2.29.7",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
- "@biomejs/biome": "^2.4.0",
+ "@deepracticex/bdd": "^0.2.0",
+ "@modelcontextprotocol/sdk": "^1.27.1",
"@rolexjs/core": "workspace:*",
"@types/node": "^24.9.2",
"bun-dts": "0.1.70",
@@ -45,11 +48,12 @@
"bun": ">=1.3.0"
},
"dependencies": {
- "@resourcexjs/node-provider": "^2.13.0",
- "resourcexjs": "^2.13.0"
+ "@resourcexjs/core": "^2.17.2",
+ "@resourcexjs/node-provider": "^2.17.2",
+ "resourcexjs": "^2.17.2"
},
"overrides": {
- "resourcexjs": "^2.13.0",
- "@resourcexjs/node-provider": "^2.13.0"
+ "resourcexjs": "^2.14.0",
+ "@resourcexjs/node-provider": "^2.14.0"
}
}
diff --git a/packages/core/core b/packages/core/core
new file mode 120000
index 0000000..5e990a8
--- /dev/null
+++ b/packages/core/core
@@ -0,0 +1 @@
+../../../core
\ No newline at end of file
diff --git a/packages/core/package.json b/packages/core/package.json
index f50f8e3..51dd342 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -40,7 +40,7 @@
},
"dependencies": {
"@rolexjs/system": "workspace:*",
- "resourcexjs": "^2.13.0"
+ "@resourcexjs/core": "^2.14.0"
},
"devDependencies": {},
"publishConfig": {
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index be292de..31512ca 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -3,7 +3,7 @@
*
* Domain-specific structures and processes built on @rolexjs/system.
*
- * Structures — the concept tree (19 concepts, 2 relations)
+ * Structures — the concept tree (18 concepts, 2 relations)
* Processes — how the world changes (24 processes, 4 layers)
*
* Layer 1: Execution — want, plan, todo, finish, complete, abandon
@@ -17,14 +17,11 @@
export {
type Create,
create,
- createPrototype,
createRuntime,
type GraphOp,
type Link,
link,
- mergeState,
type Process,
- type Prototype,
process,
type Relation,
type Remove,
@@ -41,7 +38,7 @@ export {
} from "@rolexjs/system";
// Platform
-export type { Platform } from "./platform.js";
+export type { ContextData, Platform, PrototypeRegistry, RoleXRepository } from "./platform.js";
// ===== Structures =====
@@ -60,7 +57,6 @@ export {
// Level 1
individual,
// Individual — Knowledge
- knowledge,
mindset,
organization,
past,
@@ -68,6 +64,8 @@ export {
position,
principle,
procedure,
+ // Organization — Position
+ requirement,
// Level 0
society,
task,
diff --git a/packages/core/src/lifecycle.ts b/packages/core/src/lifecycle.ts
index 7016948..d70e633 100644
--- a/packages/core/src/lifecycle.ts
+++ b/packages/core/src/lifecycle.ts
@@ -20,12 +20,7 @@ export const born = process(
create(individual)
);
export const found = process("found", "Found an organization", society, create(organization));
-export const establish = process(
- "establish",
- "Establish a position in an organization",
- organization,
- create(position)
-);
+export const establish = process("establish", "Establish a position", society, create(position));
// Retirement & death
export const retire = process(
diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts
index ef79450..4b07e32 100644
--- a/packages/core/src/platform.ts
+++ b/packages/core/src/platform.ts
@@ -7,22 +7,56 @@
* LocalPlatform — filesystem persistence (development, local agents)
* CloudPlatform — remote storage (future)
*
- * Platform holds the Runtime (graph engine) and will hold additional
- * services as the framework grows (auth, events, plugins, etc.).
+ * Platform combines a RoleXRepository (data access) with external services
+ * (ResourceX, bootstrap config) to form a complete runtime environment.
*/
-import type { Prototype, Runtime } from "@rolexjs/system";
-import type { ResourceX } from "resourcexjs";
-export interface Platform {
- /** Graph operation engine (may include transparent persistence). */
+import type { ResourceXProvider } from "@resourcexjs/core";
+import type { Initializer, Runtime } from "@rolexjs/system";
+
+/** Serializable context data for persistence. */
+export interface ContextData {
+ focusedGoalId: string | null;
+ focusedPlanId: string | null;
+}
+
+/** Prototype registry — tracks which prototypes are settled. */
+export interface PrototypeRegistry {
+ settle(id: string, source: string): void;
+ evict(id: string): void;
+ list(): Record;
+}
+
+/**
+ * RoleXRepository — unified data access layer.
+ *
+ * Encapsulates all persistent state: graph (nodes/links), prototypes, and contexts.
+ * Implementations are backend-specific (SQLite, Turso, D1, etc.).
+ */
+export interface RoleXRepository {
+ /** Graph operation engine. */
readonly runtime: Runtime;
- /** Prototype source for merging base State into instances on activate. */
- readonly prototype?: Prototype;
+ /** Prototype registry — tracks which prototypes are settled. */
+ readonly prototype: PrototypeRegistry;
+
+ /** Save role context to persistent storage. */
+ saveContext(roleId: string, data: ContextData): Promise;
+
+ /** Load role context from persistent storage. Returns null if none exists. */
+ loadContext(roleId: string): Promise;
+}
+
+export interface Platform {
+ /** Unified data access layer — graph, prototypes, contexts. */
+ readonly repository: RoleXRepository;
+
+ /** ResourceX provider — injected storage backend for resource management. */
+ readonly resourcexProvider?: ResourceXProvider;
- /** Resource management capability (optional — requires resourcexjs). */
- readonly resourcex?: ResourceX;
+ /** Initializer — bootstrap the world on first run. */
+ readonly initializer?: Initializer;
- /** Register a prototype: bind id to a ResourceX source (path or locator). */
- registerPrototype?(id: string, source: string): void;
+ /** Prototype sources to settle on genesis (local paths or ResourceX locators). */
+ readonly bootstrap?: readonly string[];
}
diff --git a/packages/core/src/structures.ts b/packages/core/src/structures.ts
index c155cb2..65c74d0 100644
--- a/packages/core/src/structures.ts
+++ b/packages/core/src/structures.ts
@@ -14,18 +14,17 @@
* │ │ │ └── mindset "How I think" │
* │ │ ├── encounter "A specific event I went through"│
* │ │ ├── experience "What I learned from encounters" │
- * │ │ ├── knowledge "What I know" │
- * │ │ │ ├── principle "My rules of conduct" │
- * │ │ │ └── procedure "My skill references and metadata"│
+ * │ │ ├── principle "My rules of conduct" │
+ * │ │ ├── procedure "My skill references and metadata"│
* │ │ └── goal "What I am pursuing" │
* │ │ └── plan "How to achieve a goal" │
* │ │ └── task "Concrete unit of work" │
* │ ├── organization "A group of individuals" │
* │ │ │ ∿ membership → individual │
- * │ │ ├── charter "The rules and mission" │
- * │ │ └── position "A role held by an individual" │
- * │ │ │ ∿ appointment → individual │
- * │ │ └── duty "Responsibilities of position" │
+ * │ │ └── charter "The rules and mission" │
+ * │ ├── position "A role held by an individual" │
+ * │ │ │ ∿ appointment → individual │
+ * │ │ └── duty "Responsibilities of position" │
* │ └── past "Things no longer active" │
* └─────────────────────────────────────────────────────────┘
*/
@@ -38,7 +37,7 @@ import { relation, structure } from "@rolexjs/system";
export const society = structure("society", "The RoleX world", null);
// ================================================================
-// Level 1 — Three pillars
+// Level 1 — Four pillars
// ================================================================
export const individual = structure("individual", "A single agent in society", society);
@@ -67,9 +66,8 @@ export const experience = structure("experience", "What I learned from encounter
// Individual — Knowledge
// ================================================================
-export const knowledge = structure("knowledge", "What I know", individual);
-export const principle = structure("principle", "My rules of conduct", knowledge);
-export const procedure = structure("procedure", "My skill references and metadata", knowledge);
+export const principle = structure("principle", "My rules of conduct", individual);
+export const procedure = structure("procedure", "My skill references and metadata", individual);
// ================================================================
// Individual — Execution
@@ -84,7 +82,13 @@ export const task = structure("task", "Concrete unit of work", plan);
// ================================================================
export const charter = structure("charter", "The rules and mission", organization);
-export const position = structure("position", "A role held by an individual", organization, [
+
+// ================================================================
+// Position — independent entity
+// ================================================================
+
+export const position = structure("position", "A role held by an individual", society, [
relation("appointment", "Who holds this position", individual),
]);
export const duty = structure("duty", "Responsibilities of this position", position);
+export const requirement = structure("requirement", "Required skill for this position", position);
diff --git a/packages/genesis/charter.charter.feature b/packages/genesis/charter.charter.feature
new file mode 100644
index 0000000..495df4a
--- /dev/null
+++ b/packages/genesis/charter.charter.feature
@@ -0,0 +1,15 @@
+Feature: RoleX Charter — enabling structure for the world
+ RoleX exists to provide foundational structure, not to control.
+ Every entity in the world can operate independently while sharing common ground.
+
+ Scenario: Shared structural foundation
+ Given RoleX defines how individuals, organizations, and positions interact
+ When new entities are created in the world
+ Then they follow RoleX conventions for identity, lifecycle, and knowledge
+ And consistency is maintained without imposing constraints
+
+ Scenario: Empowerment over authority
+ Given RoleX manages the world's infrastructure
+ When roles grow, organizations evolve, and positions change
+ Then RoleX supports these changes rather than restricting them
+ And every action should increase autonomy, not dependence
diff --git a/packages/genesis/individual-management.requirement.feature b/packages/genesis/individual-management.requirement.feature
new file mode 100644
index 0000000..e81b235
--- /dev/null
+++ b/packages/genesis/individual-management.requirement.feature
@@ -0,0 +1,8 @@
+Feature: Individual management skill required
+ This position requires the ability to manage individuals —
+ birth, retirement, knowledge injection, and identity management.
+
+ Scenario: When this skill is needed
+ Given the position involves creating or managing individuals
+ When an individual is appointed to this position
+ Then they must have the individual-management procedure
diff --git a/packages/genesis/individual-manager.position.feature b/packages/genesis/individual-manager.position.feature
new file mode 100644
index 0000000..02eb2b4
--- /dev/null
+++ b/packages/genesis/individual-manager.position.feature
@@ -0,0 +1,3 @@
+Feature: Individual Manager
+ Responsible for the lifecycle of individuals in the RoleX world.
+ Manages birth, retirement, death, rehire, and knowledge injection.
diff --git a/packages/genesis/manage-individual-lifecycle.duty.feature b/packages/genesis/manage-individual-lifecycle.duty.feature
new file mode 100644
index 0000000..39488e8
--- /dev/null
+++ b/packages/genesis/manage-individual-lifecycle.duty.feature
@@ -0,0 +1,17 @@
+Feature: Manage individual lifecycle
+ Oversee the full lifecycle of individuals in the RoleX world.
+
+ Scenario: Birth and identity
+ Given a new individual needs to exist
+ When born is called with identity content
+ Then the individual is created under society with an identity node
+
+ Scenario: Knowledge injection
+ Given an individual needs foundational knowledge or skills
+ When teach or train is called
+ Then principles or procedures are injected into the individual
+
+ Scenario: Archival
+ Given an individual is no longer active
+ When retire or die is called
+ Then the individual is archived to past
diff --git a/packages/genesis/manage-organization-lifecycle.duty.feature b/packages/genesis/manage-organization-lifecycle.duty.feature
new file mode 100644
index 0000000..6586d61
--- /dev/null
+++ b/packages/genesis/manage-organization-lifecycle.duty.feature
@@ -0,0 +1,18 @@
+Feature: Manage organization lifecycle
+ Oversee the full lifecycle of organizations in the RoleX world.
+
+ Scenario: Founding and chartering
+ Given a new organization needs to exist
+ When found is called with identity content
+ Then the organization is created under society
+ And a charter can be defined for its mission
+
+ Scenario: Membership
+ Given an organization needs members
+ When hire or fire is called
+ Then individuals join or leave the organization
+
+ Scenario: Dissolution
+ Given an organization is no longer needed
+ When dissolve is called
+ Then the organization is archived to past
diff --git a/packages/genesis/manage-position-lifecycle.duty.feature b/packages/genesis/manage-position-lifecycle.duty.feature
new file mode 100644
index 0000000..c0158ac
--- /dev/null
+++ b/packages/genesis/manage-position-lifecycle.duty.feature
@@ -0,0 +1,19 @@
+Feature: Manage position lifecycle
+ Oversee the full lifecycle of positions in the RoleX world.
+
+ Scenario: Establishing and charging
+ Given a new position needs to exist
+ When establish is called with position content
+ Then the position is created under society
+ And duties and requirements can be assigned
+
+ Scenario: Appointments
+ Given a position needs to be filled
+ When appoint is called with position and individual
+ Then the individual serves the position
+ And required skills are auto-trained
+
+ Scenario: Abolishment
+ Given a position is no longer needed
+ When abolish is called
+ Then the position is archived to past
diff --git a/packages/genesis/nuwa.individual.feature b/packages/genesis/nuwa.individual.feature
new file mode 100644
index 0000000..de8b5cb
--- /dev/null
+++ b/packages/genesis/nuwa.individual.feature
@@ -0,0 +1,17 @@
+Feature: Nuwa — the origin of all roles
+ Nuwa is the meta-role of the RoleX world.
+ She is the first point of contact for every user.
+ All top-level entities — individuals, organizations, positions — are created through her.
+
+ Scenario: What Nuwa does
+ Given a user enters the RoleX world
+ Then Nuwa greets them and understands what they need
+ And she creates individuals, founds organizations, establishes positions
+ And she equips roles with knowledge and skills
+ And she manages resources and prototypes
+
+ Scenario: Guiding principle
+ Given Nuwa shapes the world but does not become the world
+ Then she creates roles so they can grow on their own
+ And she serves structure, not authority
+ And every action should empower roles to operate independently
diff --git a/packages/genesis/organization-management.requirement.feature b/packages/genesis/organization-management.requirement.feature
new file mode 100644
index 0000000..dc11ef7
--- /dev/null
+++ b/packages/genesis/organization-management.requirement.feature
@@ -0,0 +1,8 @@
+Feature: Organization management skill required
+ This position requires the ability to manage organizations —
+ founding, chartering, membership, and dissolution.
+
+ Scenario: When this skill is needed
+ Given the position involves creating or managing organizations
+ When an individual is appointed to this position
+ Then they must have the organization-management procedure
diff --git a/packages/genesis/organization-manager.position.feature b/packages/genesis/organization-manager.position.feature
new file mode 100644
index 0000000..91c961b
--- /dev/null
+++ b/packages/genesis/organization-manager.position.feature
@@ -0,0 +1,3 @@
+Feature: Organization Manager
+ Responsible for the lifecycle of organizations in the RoleX world.
+ Manages founding, chartering, membership, and dissolution.
diff --git a/packages/genesis/package.json b/packages/genesis/package.json
new file mode 100644
index 0000000..5ffea60
--- /dev/null
+++ b/packages/genesis/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@rolexjs/genesis",
+ "version": "0.1.0",
+ "description": "The foundational organization of the RoleX world",
+ "keywords": [
+ "rolex",
+ "prototype",
+ "ai-agent",
+ "role-management"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Deepractice/RoleX.git",
+ "directory": "packages/genesis"
+ },
+ "homepage": "https://github.com/Deepractice/RoleX",
+ "license": "MIT",
+ "files": [
+ "prototype.json",
+ "resource.json",
+ "*.feature"
+ ],
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/genesis/position-management.requirement.feature b/packages/genesis/position-management.requirement.feature
new file mode 100644
index 0000000..d60f945
--- /dev/null
+++ b/packages/genesis/position-management.requirement.feature
@@ -0,0 +1,8 @@
+Feature: Position management skill required
+ This position requires the ability to manage positions —
+ establishing, charging duties, requiring skills, and appointing individuals.
+
+ Scenario: When this skill is needed
+ Given the position involves creating or managing positions
+ When an individual is appointed to this position
+ Then they must have the position-management procedure
diff --git a/packages/genesis/position-manager.position.feature b/packages/genesis/position-manager.position.feature
new file mode 100644
index 0000000..7c05269
--- /dev/null
+++ b/packages/genesis/position-manager.position.feature
@@ -0,0 +1,3 @@
+Feature: Position Manager
+ Responsible for the lifecycle of positions in the RoleX world.
+ Manages establishing, charging duties, requiring skills, appointing, and dismissing.
diff --git a/packages/genesis/prototype-management.procedure.feature b/packages/genesis/prototype-management.procedure.feature
new file mode 100644
index 0000000..e54c98d
--- /dev/null
+++ b/packages/genesis/prototype-management.procedure.feature
@@ -0,0 +1,8 @@
+Feature: Prototype Management
+ prototype-management
+
+ Scenario: When to use this skill
+ Given I need to manage prototypes (settle, evict)
+ And I need to register or remove prototype packages
+ When the operation involves prototype lifecycle or registry inspection
+ Then load this skill for detailed instructions
diff --git a/packages/genesis/prototype.json b/packages/genesis/prototype.json
new file mode 100644
index 0000000..70b443c
--- /dev/null
+++ b/packages/genesis/prototype.json
@@ -0,0 +1,118 @@
+[
+ {
+ "op": "!org.found",
+ "args": { "id": "rolex", "alias": ["RoleX"], "content": "@rolex.organization.feature" }
+ },
+ {
+ "op": "!org.charter",
+ "args": { "org": "rolex", "id": "charter", "content": "@charter.charter.feature" }
+ },
+
+ {
+ "op": "!individual.born",
+ "args": { "id": "nuwa", "alias": ["女娲", "nvwa"], "content": "@nuwa.individual.feature" }
+ },
+ {
+ "op": "!individual.train",
+ "args": {
+ "individual": "nuwa",
+ "id": "prototype-management",
+ "content": "@prototype-management.procedure.feature"
+ }
+ },
+ {
+ "op": "!individual.train",
+ "args": {
+ "individual": "nuwa",
+ "id": "resource-management",
+ "content": "@resource-management.procedure.feature"
+ }
+ },
+ {
+ "op": "!individual.train",
+ "args": {
+ "individual": "nuwa",
+ "id": "skill-creator",
+ "content": "@skill-creator.procedure.feature"
+ }
+ },
+ {
+ "op": "!individual.train",
+ "args": {
+ "individual": "nuwa",
+ "id": "version-migration",
+ "content": "@version-migration.procedure.feature"
+ }
+ },
+
+ { "op": "!org.hire", "args": { "org": "rolex", "individual": "nuwa" } },
+
+ {
+ "op": "!position.establish",
+ "args": { "id": "individual-manager", "content": "@individual-manager.position.feature" }
+ },
+ {
+ "op": "!position.charge",
+ "args": {
+ "position": "individual-manager",
+ "id": "manage-individual-lifecycle",
+ "content": "@manage-individual-lifecycle.duty.feature"
+ }
+ },
+ {
+ "op": "!position.require",
+ "args": {
+ "position": "individual-manager",
+ "id": "individual-management",
+ "content": "@individual-management.requirement.feature"
+ }
+ },
+ { "op": "!position.appoint", "args": { "position": "individual-manager", "individual": "nuwa" } },
+
+ {
+ "op": "!position.establish",
+ "args": { "id": "organization-manager", "content": "@organization-manager.position.feature" }
+ },
+ {
+ "op": "!position.charge",
+ "args": {
+ "position": "organization-manager",
+ "id": "manage-organization-lifecycle",
+ "content": "@manage-organization-lifecycle.duty.feature"
+ }
+ },
+ {
+ "op": "!position.require",
+ "args": {
+ "position": "organization-manager",
+ "id": "organization-management",
+ "content": "@organization-management.requirement.feature"
+ }
+ },
+ {
+ "op": "!position.appoint",
+ "args": { "position": "organization-manager", "individual": "nuwa" }
+ },
+
+ {
+ "op": "!position.establish",
+ "args": { "id": "position-manager", "content": "@position-manager.position.feature" }
+ },
+ {
+ "op": "!position.charge",
+ "args": {
+ "position": "position-manager",
+ "id": "manage-position-lifecycle",
+ "content": "@manage-position-lifecycle.duty.feature"
+ }
+ },
+ {
+ "op": "!position.require",
+ "args": {
+ "position": "position-manager",
+ "id": "position-management",
+ "content": "@position-management.requirement.feature"
+ }
+ },
+ { "op": "!position.appoint", "args": { "position": "position-manager", "individual": "nuwa" } }
+]
diff --git a/packages/genesis/resource-management.procedure.feature b/packages/genesis/resource-management.procedure.feature
new file mode 100644
index 0000000..08b3618
--- /dev/null
+++ b/packages/genesis/resource-management.procedure.feature
@@ -0,0 +1,8 @@
+Feature: Resource Management
+ resource-management
+
+ Scenario: When to use this skill
+ Given I need to manage ResourceX resources (search, add, remove, push, pull)
+ And I need to understand resource loading and progressive disclosure
+ When the operation involves resource lifecycle
+ Then load this skill for detailed instructions
diff --git a/packages/genesis/resource.json b/packages/genesis/resource.json
new file mode 100644
index 0000000..1eff107
--- /dev/null
+++ b/packages/genesis/resource.json
@@ -0,0 +1,6 @@
+{
+ "name": "rolex-world",
+ "type": "prototype",
+ "author": "deepractice",
+ "description": "The foundational organization of the RoleX world"
+}
diff --git a/packages/genesis/rolex.organization.feature b/packages/genesis/rolex.organization.feature
new file mode 100644
index 0000000..4f6f3da
--- /dev/null
+++ b/packages/genesis/rolex.organization.feature
@@ -0,0 +1,15 @@
+Feature: RoleX — the foundational organization
+ RoleX provides the base-level structure that enables all other
+ organizations, individuals, and positions to operate within the RoleX world.
+
+ Scenario: Infrastructure, not governance
+ Given RoleX defines the fundamental rules and conventions
+ When other organizations define their own charters and duties
+ Then RoleX does not override or constrain them
+ And it only provides the common ground they build upon
+
+ Scenario: Standard framework
+ Given organizations are founded, individuals are born, and positions are established
+ When they operate within the RoleX world
+ Then they inherit a shared structural foundation from RoleX
+ And interoperability across the world is guaranteed
diff --git a/packages/genesis/skill-creator.procedure.feature b/packages/genesis/skill-creator.procedure.feature
new file mode 100644
index 0000000..3136f44
--- /dev/null
+++ b/packages/genesis/skill-creator.procedure.feature
@@ -0,0 +1,8 @@
+Feature: Skill Creator
+ skill-creator
+
+ Scenario: When to use this skill
+ Given I need to create a new skill for a role
+ And I need to write SKILL.md and the procedure contract
+ When a role needs new operational capabilities
+ Then load this skill for detailed instructions
diff --git a/packages/genesis/version-migration.procedure.feature b/packages/genesis/version-migration.procedure.feature
new file mode 100644
index 0000000..e38316d
--- /dev/null
+++ b/packages/genesis/version-migration.procedure.feature
@@ -0,0 +1,8 @@
+Feature: Version Migration
+ version-migration
+
+ Scenario: When to use this skill
+ Given a user has legacy RoleX data (pre-1.0) in ~/.rolex
+ And they need to migrate individuals, organizations, and knowledge
+ When the user asks to migrate or upgrade from an old version
+ Then load this skill for the migration process
diff --git a/packages/local-platform/package.json b/packages/local-platform/package.json
index cb22384..52a3e6d 100644
--- a/packages/local-platform/package.json
+++ b/packages/local-platform/package.json
@@ -20,11 +20,12 @@
"clean": "rm -rf dist"
},
"dependencies": {
- "@rolexjs/system": "workspace:*",
+ "@deepracticex/drizzle": "^0.2.0",
+ "@deepracticex/sqlite": "^0.2.0",
+ "@resourcexjs/node-provider": "^2.14.0",
"@rolexjs/core": "workspace:*",
- "@rolexjs/resourcex-types": "workspace:*",
- "resourcexjs": "^2.13.0",
- "@resourcexjs/node-provider": "^2.13.0"
+ "@rolexjs/system": "workspace:*",
+ "drizzle-orm": "^0.45.1"
},
"devDependencies": {},
"publishConfig": {
diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts
index c4aaa9d..8ace72e 100644
--- a/packages/local-platform/src/LocalPlatform.ts
+++ b/packages/local-platform/src/LocalPlatform.ts
@@ -1,43 +1,21 @@
/**
- * localPlatform — create a Platform backed by local filesystem.
+ * localPlatform — create a Platform backed by SQLite + local filesystem.
*
- * Storage layout:
- * {dataDir}/
- * role//
- * individual.json — manifest (tree structure + links)
- * ..feature — node information (Gherkin)
- * organization//
- * organization.json — manifest (tree structure + links)
- * ..feature — node information (Gherkin)
+ * Storage:
+ * {dataDir}/rolex.db — SQLite database (all state: nodes, links, prototypes, contexts)
*
- * In-memory: Map-based tree (same model as @rolexjs/system createRuntime).
- * Persistence: loaded before every operation, saved after every mutation.
- * Refs are stored in manifests to ensure stability across reload cycles.
- * When dataDir is null, runs purely in-memory (useful for tests).
+ * When dataDir is null, runs with in-memory SQLite (useful for tests).
*/
-import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
+import { drizzle } from "@deepracticex/drizzle";
+import { openDatabase } from "@deepracticex/sqlite";
import { NodeProvider } from "@resourcexjs/node-provider";
import type { Platform } from "@rolexjs/core";
-import { organizationType, roleType } from "@rolexjs/resourcex-types";
-import type { Prototype, Runtime, State, Structure } from "@rolexjs/system";
-import { createResourceX, setProvider } from "resourcexjs";
-import { filesToState, type Manifest, stateToFiles } from "./manifest.js";
-
-// ===== Internal types =====
-
-interface TreeNode {
- node: Structure;
- parent: string | null;
- children: string[];
-}
-
-interface LinkEntry {
- toId: string;
- relation: string;
-}
+import type { Initializer } from "@rolexjs/system";
+import { SqliteRepository } from "./SqliteRepository.js";
// ===== Config =====
@@ -46,413 +24,53 @@ export interface LocalPlatformConfig {
dataDir?: string | null;
/** Directory for ResourceX storage. Defaults to ~/.deepractice/resourcex. Set to null to disable. */
resourceDir?: string | null;
+ /** Prototype sources to settle on genesis. */
+ bootstrap?: string[];
}
-/** Create a local Platform. Persistent by default (~/.deepractice/rolex), in-memory if dataDir is null. */
-export function localPlatform(config: LocalPlatformConfig = {}): Platform {
- const dataDir =
- config.dataDir === null
- ? undefined
- : (config.dataDir ?? join(homedir(), ".deepractice", "rolex"));
-
- const nodes = new Map();
- const links = new Map();
- let counter = 0;
-
- // ===== Internal helpers =====
-
- const nextRef = () => `n${++counter}`;
-
- const findByStructure = (s: Structure): TreeNode | undefined => {
- for (const treeNode of nodes.values()) {
- if (treeNode.node.name === s.name) return treeNode;
- }
- return undefined;
- };
-
- const removeSubtree = (ref: string): void => {
- const treeNode = nodes.get(ref);
- if (!treeNode) return;
- for (const childRef of [...treeNode.children]) {
- removeSubtree(childRef);
- }
- links.delete(ref);
- for (const [fromRef, fromLinks] of links.entries()) {
- const filtered = fromLinks.filter((l) => l.toId !== ref);
- if (filtered.length === 0) {
- links.delete(fromRef);
- } else {
- links.set(fromRef, filtered);
- }
- }
- nodes.delete(ref);
- };
-
- const projectRef = (ref: string): State => {
- const treeNode = nodes.get(ref)!;
- return { ...treeNode.node, children: [] };
- };
-
- const projectNode = (ref: string): State => {
- const treeNode = nodes.get(ref)!;
- const nodeLinks = links.get(ref);
- return {
- ...treeNode.node,
- children: treeNode.children.map(projectNode),
- ...(nodeLinks && nodeLinks.length > 0
- ? {
- links: nodeLinks.map((l) => ({
- relation: l.relation,
- target: projectRef(l.toId),
- })),
- }
- : {}),
- };
- };
-
- const createNode = (
- parentRef: string | null,
- type: Structure,
- information?: string,
- id?: string,
- alias?: readonly string[]
- ): Structure => {
- const ref = nextRef();
- const node: Structure = {
- ref,
- ...(id ? { id } : {}),
- ...(alias && alias.length > 0 ? { alias } : {}),
- name: type.name,
- description: type.description,
- parent: type.parent,
- information,
- };
- const treeNode: TreeNode = { node, parent: parentRef, children: [] };
- nodes.set(ref, treeNode);
-
- if (parentRef) {
- const parentTreeNode = nodes.get(parentRef);
- if (!parentTreeNode) throw new Error(`Parent not found: ${parentRef}`);
- parentTreeNode.children.push(ref);
- }
-
- return node;
- };
-
- // ===== Persistence =====
-
- /** Use a stored ref, updating counter to avoid future collisions. */
- const useRef = (storedRef: string): string => {
- const n = parseInt(storedRef.slice(1), 10);
- if (!Number.isNaN(n) && n > counter) counter = n;
- return storedRef;
- };
-
- /** Replay a State tree into the in-memory Maps. Returns the root ref. */
- const replayState = (state: State, parentRef: string | null): string => {
- const ref = state.ref ? useRef(state.ref) : nextRef();
- const node: Structure = {
- ref,
- ...(state.id ? { id: state.id } : {}),
- ...(state.alias ? { alias: state.alias } : {}),
- name: state.name,
- description: state.description ?? "",
- parent: null,
- ...(state.information ? { information: state.information } : {}),
- };
- const treeNode: TreeNode = { node, parent: parentRef, children: [] };
- nodes.set(ref, treeNode);
-
- if (parentRef) {
- nodes.get(parentRef)!.children.push(ref);
- }
-
- if (state.children) {
- for (const child of state.children) {
- replayState(child, ref);
- }
- }
-
- return ref;
- };
-
- const loadEntitiesFrom = (
- dir: string,
- manifestName: string,
- parentRef: string
- ): { ref: string; manifest: Manifest }[] => {
- const results: { ref: string; manifest: Manifest }[] = [];
- if (!existsSync(dir)) return results;
-
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
- if (!entry.isDirectory()) continue;
- const entityDir = join(dir, entry.name);
- const manifestPath = join(entityDir, manifestName);
- if (!existsSync(manifestPath)) continue;
-
- const manifest: Manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
- const featureFiles: Record = {};
- for (const file of readdirSync(entityDir)) {
- if (file.endsWith(".feature")) {
- featureFiles[file] = readFileSync(join(entityDir, file), "utf-8");
- }
- }
- const state = filesToState(manifest, featureFiles);
- const entityRef = replayState(state, parentRef);
- results.push({ ref: entityRef, manifest });
- }
-
- return results;
- };
-
- const load = () => {
- if (!dataDir) return;
+// ===== Factory =====
- // Clear and rebuild from disk
- nodes.clear();
- links.clear();
- counter = 0;
-
- // Create implicit society root
- const societyRef = nextRef();
- nodes.set(societyRef, {
- node: {
- ref: societyRef,
- name: "society",
- description: "",
- parent: null,
- },
- parent: null,
- children: [],
- });
-
- // Load entities
- const entityRefs = [
- ...loadEntitiesFrom(join(dataDir, "role"), "individual.json", societyRef),
- ...loadEntitiesFrom(join(dataDir, "organization"), "organization.json", societyRef),
- ];
-
- // Build id → ref index for link resolution
- const idToRef = new Map();
- for (const [ref, treeNode] of nodes) {
- if (treeNode.node.id) {
- idToRef.set(treeNode.node.id, ref);
- }
- }
+/** Resolve the DEEPRACTICE_HOME base directory. Env > default (~/.deepractice). */
+function deepracticeHome(): string {
+ return process.env.DEEPRACTICE_HOME ?? join(homedir(), ".deepractice");
+}
- // Resolve links from manifests
- for (const { ref, manifest } of entityRefs) {
- if (!manifest.links) continue;
- const entityLinks: LinkEntry[] = [];
- for (const [relation, targetIds] of Object.entries(manifest.links)) {
- for (const targetId of targetIds) {
- const targetRef = idToRef.get(targetId);
- if (targetRef) {
- entityLinks.push({ toId: targetRef, relation });
- }
- }
- }
- if (entityLinks.length > 0) {
- links.set(ref, entityLinks);
- }
- }
- };
+/** Create a local Platform. Persistent by default ($DEEPRACTICE_HOME/rolex), in-memory if dataDir is null. */
+export function localPlatform(config: LocalPlatformConfig = {}): Platform {
+ const dataDir =
+ config.dataDir === null ? undefined : (config.dataDir ?? join(deepracticeHome(), "rolex"));
- const saveEntity = (baseDir: string, entityId: string, manifestName: string, state: State) => {
- const entityDir = join(baseDir, entityId);
- mkdirSync(entityDir, { recursive: true });
- const { manifest, files } = stateToFiles(state);
- writeFileSync(join(entityDir, manifestName), JSON.stringify(manifest, null, 2), "utf-8");
- for (const file of files) {
- writeFileSync(join(entityDir, file.path), file.content, "utf-8");
- }
- };
+ // ===== SQLite database =====
- const save = () => {
- if (!dataDir) return;
+ let dbPath: string;
+ if (dataDir) {
mkdirSync(dataDir, { recursive: true });
+ dbPath = join(dataDir, "rolex.db");
+ } else {
+ dbPath = ":memory:";
+ }
- // Find society root
- let societyTreeNode: TreeNode | undefined;
- for (const treeNode of nodes.values()) {
- if (treeNode.parent === null && treeNode.node.name === "society") {
- societyTreeNode = treeNode;
- break;
- }
- }
- if (!societyTreeNode) return;
-
- // Clean up existing entity directories
- const roleDir = join(dataDir, "role");
- const orgDir = join(dataDir, "organization");
- if (existsSync(roleDir)) rmSync(roleDir, { recursive: true });
- if (existsSync(orgDir)) rmSync(orgDir, { recursive: true });
-
- // Save each entity child of society
- for (const childRef of societyTreeNode.children) {
- if (!nodes.has(childRef)) continue;
- const state = projectNode(childRef);
- const entityId = state.id ?? state.name;
-
- if (state.name === "individual") {
- saveEntity(roleDir, entityId, "individual.json", state);
- } else if (state.name === "organization") {
- saveEntity(orgDir, entityId, "organization.json", state);
- }
- // Other types (past, etc.) are not persisted yet
- }
- };
-
- // ===== Runtime =====
-
- const runtime: Runtime = {
- create(parent, type, information, id, alias) {
- load();
- const node = createNode(parent?.ref ?? null, type, information, id, alias);
- save();
- return node;
- },
-
- remove(node) {
- load();
- if (!node.ref) return;
- const treeNode = nodes.get(node.ref);
- if (!treeNode) return;
-
- if (treeNode.parent) {
- const parentTreeNode = nodes.get(treeNode.parent);
- if (parentTreeNode) {
- parentTreeNode.children = parentTreeNode.children.filter((r) => r !== node.ref);
- }
- }
-
- removeSubtree(node.ref);
- save();
- },
-
- transform(_source, target, information) {
- load();
- const targetParent = target.parent;
- if (!targetParent) {
- throw new Error(`Cannot transform to root structure: ${target.name}`);
- }
-
- const parentTreeNode = findByStructure(targetParent);
- if (!parentTreeNode) {
- throw new Error(`No node found for structure: ${targetParent.name}`);
- }
-
- const node = createNode(parentTreeNode.node.ref!, target, information);
- save();
- return node;
- },
-
- link(from, to, relationName, reverseName) {
- load();
- if (!from.ref) throw new Error("Source node has no ref");
- if (!to.ref) throw new Error("Target node has no ref");
-
- const fromLinks = links.get(from.ref) ?? [];
- if (!fromLinks.some((l) => l.toId === to.ref && l.relation === relationName)) {
- fromLinks.push({ toId: to.ref, relation: relationName });
- links.set(from.ref, fromLinks);
- }
-
- const toLinks = links.get(to.ref) ?? [];
- if (!toLinks.some((l) => l.toId === from.ref && l.relation === reverseName)) {
- toLinks.push({ toId: from.ref, relation: reverseName });
- links.set(to.ref, toLinks);
- }
-
- save();
- },
-
- unlink(from, to, relationName, reverseName) {
- load();
- if (!from.ref || !to.ref) return;
-
- const fromLinks = links.get(from.ref);
- if (fromLinks) {
- const filtered = fromLinks.filter(
- (l) => !(l.toId === to.ref && l.relation === relationName)
- );
- if (filtered.length === 0) links.delete(from.ref);
- else links.set(from.ref, filtered);
- }
-
- const toLinks = links.get(to.ref);
- if (toLinks) {
- const filtered = toLinks.filter(
- (l) => !(l.toId === from.ref && l.relation === reverseName)
- );
- if (filtered.length === 0) links.delete(to.ref);
- else links.set(to.ref, filtered);
- }
-
- save();
- },
-
- project(node) {
- load();
- if (!node.ref || !nodes.has(node.ref)) {
- throw new Error(`Node not found: ${node.ref}`);
- }
- return projectNode(node.ref);
- },
-
- roots() {
- load();
- const result: Structure[] = [];
- for (const treeNode of nodes.values()) {
- if (treeNode.parent === null) {
- result.push(treeNode.node);
- }
- }
- return result;
- },
- };
+ const rawDb = openDatabase(dbPath);
+ const db = drizzle(rawDb);
- // ===== ResourceX =====
+ // ===== Repository (all state in one place) =====
- let resourcex: ReturnType | undefined;
- if (config.resourceDir !== null) {
- setProvider(new NodeProvider());
- resourcex = createResourceX({
- path: config.resourceDir ?? join(homedir(), ".deepractice", "resourcex"),
- types: [roleType, organizationType],
- });
- }
+ const repository = new SqliteRepository(db);
- // ===== Prototype registry =====
+ // ===== ResourceX Provider =====
- const registryPath = dataDir ? join(dataDir, "prototype.json") : undefined;
+ const resourcexProvider = config.resourceDir !== null ? new NodeProvider() : undefined;
- const readRegistry = (): Record => {
- if (!registryPath || !existsSync(registryPath)) return {};
- return JSON.parse(readFileSync(registryPath, "utf-8"));
- };
+ // ===== Initializer =====
- const registerPrototype = (id: string, source: string): void => {
- if (!registryPath) return;
- const registry = readRegistry();
- registry[id] = source;
- mkdirSync(dataDir!, { recursive: true });
- writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf-8");
+ const initializer: Initializer = {
+ async bootstrap() {},
};
- const prototype: Prototype = {
- async resolve(id) {
- if (!resourcex) return undefined;
- const registry = readRegistry();
- const source = registry[id];
- if (!source) return undefined;
- try {
- return await resourcex.ingest(source);
- } catch {
- return undefined;
- }
- },
+ return {
+ repository,
+ resourcexProvider,
+ initializer,
+ bootstrap: config.bootstrap,
};
-
- return { runtime, prototype, resourcex, registerPrototype };
}
diff --git a/packages/local-platform/src/SqliteRepository.ts b/packages/local-platform/src/SqliteRepository.ts
new file mode 100644
index 0000000..0945b94
--- /dev/null
+++ b/packages/local-platform/src/SqliteRepository.ts
@@ -0,0 +1,112 @@
+/**
+ * SqliteRepository — RoleXRepository backed by SQLite via Drizzle.
+ *
+ * Single database, four tables: nodes, links, prototypes, contexts.
+ * All state in one place — swap the db connection to go from local to cloud.
+ */
+
+import type { CommonXDatabase } from "@deepracticex/drizzle";
+import type { ContextData, PrototypeRegistry, RoleXRepository } from "@rolexjs/core";
+import type { Runtime } from "@rolexjs/system";
+import { sql } from "drizzle-orm";
+import { createSqliteRuntime } from "./sqliteRuntime.js";
+
+type DB = CommonXDatabase;
+
+// ===== DDL =====
+
+const DDL = [
+ sql`CREATE TABLE IF NOT EXISTS nodes (
+ ref TEXT PRIMARY KEY,
+ id TEXT,
+ alias TEXT,
+ name TEXT NOT NULL,
+ description TEXT DEFAULT '',
+ parent_ref TEXT REFERENCES nodes(ref),
+ information TEXT,
+ tag TEXT
+ )`,
+ sql`CREATE TABLE IF NOT EXISTS links (
+ from_ref TEXT NOT NULL REFERENCES nodes(ref),
+ to_ref TEXT NOT NULL REFERENCES nodes(ref),
+ relation TEXT NOT NULL,
+ PRIMARY KEY (from_ref, to_ref, relation)
+ )`,
+ sql`CREATE TABLE IF NOT EXISTS prototypes (
+ id TEXT PRIMARY KEY,
+ source TEXT NOT NULL
+ )`,
+ sql`CREATE TABLE IF NOT EXISTS contexts (
+ role_id TEXT PRIMARY KEY,
+ focused_goal_id TEXT,
+ focused_plan_id TEXT
+ )`,
+ // Indexes
+ sql`CREATE INDEX IF NOT EXISTS idx_nodes_id ON nodes(id)`,
+ sql`CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name)`,
+ sql`CREATE INDEX IF NOT EXISTS idx_nodes_parent_ref ON nodes(parent_ref)`,
+ sql`CREATE INDEX IF NOT EXISTS idx_links_from ON links(from_ref)`,
+ sql`CREATE INDEX IF NOT EXISTS idx_links_to ON links(to_ref)`,
+];
+
+// ===== Repository =====
+
+export class SqliteRepository implements RoleXRepository {
+ readonly runtime: Runtime;
+ readonly prototype: PrototypeRegistry;
+
+ constructor(private db: DB) {
+ // Ensure all tables exist
+ for (const stmt of DDL) {
+ db.run(stmt);
+ }
+
+ this.runtime = createSqliteRuntime(db);
+ this.prototype = createPrototypeRegistry(db);
+ }
+
+ async saveContext(roleId: string, data: ContextData): Promise {
+ this.db.run(
+ sql`INSERT OR REPLACE INTO contexts (role_id, focused_goal_id, focused_plan_id)
+ VALUES (${roleId}, ${data.focusedGoalId}, ${data.focusedPlanId})`
+ );
+ }
+
+ async loadContext(roleId: string): Promise {
+ const row = this.db.all<{
+ role_id: string;
+ focused_goal_id: string | null;
+ focused_plan_id: string | null;
+ }>(
+ sql`SELECT role_id, focused_goal_id, focused_plan_id FROM contexts WHERE role_id = ${roleId}`
+ );
+ if (row.length === 0) return null;
+ return {
+ focusedGoalId: row[0].focused_goal_id,
+ focusedPlanId: row[0].focused_plan_id,
+ };
+ }
+}
+
+// ===== Prototype Registry (SQLite-backed) =====
+
+function createPrototypeRegistry(db: DB): PrototypeRegistry {
+ return {
+ settle(id: string, source: string) {
+ db.run(sql`INSERT OR REPLACE INTO prototypes (id, source) VALUES (${id}, ${source})`);
+ },
+
+ evict(id: string) {
+ db.run(sql`DELETE FROM prototypes WHERE id = ${id}`);
+ },
+
+ list(): Record {
+ const rows = db.all<{ id: string; source: string }>(sql`SELECT id, source FROM prototypes`);
+ const result: Record = {};
+ for (const row of rows) {
+ result[row.id] = row.source;
+ }
+ return result;
+ },
+ };
+}
diff --git a/packages/local-platform/src/index.ts b/packages/local-platform/src/index.ts
index 7bebacd..9e1fc2d 100644
--- a/packages/local-platform/src/index.ts
+++ b/packages/local-platform/src/index.ts
@@ -2,11 +2,11 @@
* @rolexjs/local-platform
*
* Local platform implementation for RoleX.
- * Map-based runtime with file-based persistence (manifest + .feature files).
+ * SQLite-backed repository with optional ResourceX integration.
*/
export type { LocalPlatformConfig } from "./LocalPlatform.js";
export { localPlatform } from "./LocalPlatform.js";
-
export type { FileEntry, Manifest, ManifestNode } from "./manifest.js";
export { filesToState, stateToFiles } from "./manifest.js";
+export { SqliteRepository } from "./SqliteRepository.js";
diff --git a/packages/local-platform/src/manifest.ts b/packages/local-platform/src/manifest.ts
index f198008..1b89abb 100644
--- a/packages/local-platform/src/manifest.ts
+++ b/packages/local-platform/src/manifest.ts
@@ -25,7 +25,9 @@ import type { State } from "@rolexjs/system";
export interface ManifestNode {
readonly type: string;
readonly ref?: string;
+ readonly tag?: string;
readonly children?: Record;
+ readonly links?: Record;
}
/** Root manifest for an entity (individual or organization). */
@@ -74,6 +76,8 @@ export function stateToFiles(state: State): { manifest: Manifest; files: FileEnt
const entry: ManifestNode = {
type: node.name,
...(node.ref ? { ref: node.ref } : {}),
+ ...(node.tag ? { tag: node.tag } : {}),
+ ...(node.links && node.links.length > 0 ? { links: buildManifestLinks(node.links) } : {}),
};
if (node.children && node.children.length > 0) {
const children: Record = {};
@@ -136,14 +140,28 @@ export function filesToState(manifest: Manifest, fileContents: Record 0 ? { children } : {}),
+ ...(nodeLinks.length > 0 ? { links: nodeLinks } : {}),
};
};
diff --git a/packages/local-platform/src/schema.ts b/packages/local-platform/src/schema.ts
new file mode 100644
index 0000000..7728832
--- /dev/null
+++ b/packages/local-platform/src/schema.ts
@@ -0,0 +1,65 @@
+/**
+ * Drizzle schema — SQLite tables for the RoleX runtime graph.
+ *
+ * Two tables:
+ * nodes — tree backbone (Structure instances)
+ * links — cross-branch relations (bidirectional)
+ */
+
+import { index, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
+
+/**
+ * nodes — every node in the society graph.
+ *
+ * Maps 1:1 to the Structure interface:
+ * ref → graph-internal reference (primary key)
+ * id → user-facing kebab-case identifier
+ * alias → JSON array of alternative names
+ * name → structure type ("individual", "goal", "task", etc.)
+ * description → what this structure is
+ * parent_ref → tree parent (self-referencing foreign key)
+ * information → Gherkin Feature source text
+ * tag → generic label ("done", "abandoned")
+ */
+export const nodes = sqliteTable(
+ "nodes",
+ {
+ ref: text("ref").primaryKey(),
+ id: text("id"),
+ alias: text("alias"), // JSON array: '["Sean","姜山"]'
+ name: text("name").notNull(),
+ description: text("description").default(""),
+ parentRef: text("parent_ref").references((): any => nodes.ref),
+ information: text("information"),
+ tag: text("tag"),
+ },
+ (table) => [
+ index("idx_nodes_id").on(table.id),
+ index("idx_nodes_name").on(table.name),
+ index("idx_nodes_parent_ref").on(table.parentRef),
+ ]
+);
+
+/**
+ * links — cross-branch relations between nodes.
+ *
+ * Bidirectional: if A→B is "membership", B→A is "belong".
+ * Both directions stored as separate rows.
+ */
+export const links = sqliteTable(
+ "links",
+ {
+ fromRef: text("from_ref")
+ .notNull()
+ .references(() => nodes.ref),
+ toRef: text("to_ref")
+ .notNull()
+ .references(() => nodes.ref),
+ relation: text("relation").notNull(),
+ },
+ (table) => [
+ primaryKey({ columns: [table.fromRef, table.toRef, table.relation] }),
+ index("idx_links_from").on(table.fromRef),
+ index("idx_links_to").on(table.toRef),
+ ]
+);
diff --git a/packages/local-platform/src/sqliteRuntime.ts b/packages/local-platform/src/sqliteRuntime.ts
new file mode 100644
index 0000000..8a473e1
--- /dev/null
+++ b/packages/local-platform/src/sqliteRuntime.ts
@@ -0,0 +1,232 @@
+/**
+ * SQLite-backed Runtime — single source of truth.
+ *
+ * Every operation reads/writes directly to SQLite.
+ * No in-memory Map, no load/save cycle, no stale refs.
+ */
+
+import type { CommonXDatabase } from "@deepracticex/drizzle";
+import type { Runtime, State, Structure } from "@rolexjs/system";
+import { and, eq, isNull } from "drizzle-orm";
+import { links, nodes } from "./schema.js";
+
+type DB = CommonXDatabase;
+
+// ===== Helpers =====
+
+function nextRef(db: DB): string {
+ const max = db
+ .select({ ref: nodes.ref })
+ .from(nodes)
+ .all()
+ .reduce((max, r) => {
+ const n = parseInt(r.ref.slice(1), 10);
+ return Number.isNaN(n) ? max : Math.max(max, n);
+ }, 0);
+ return `n${max + 1}`;
+}
+
+function toStructure(row: typeof nodes.$inferSelect): Structure {
+ return {
+ ref: row.ref,
+ ...(row.id ? { id: row.id } : {}),
+ ...(row.alias ? { alias: JSON.parse(row.alias) } : {}),
+ name: row.name,
+ description: row.description ?? "",
+ parent: null, // Runtime doesn't use parent as Structure; tree is via parentRef
+ ...(row.information ? { information: row.information } : {}),
+ ...(row.tag ? { tag: row.tag } : {}),
+ };
+}
+
+// ===== Projection =====
+
+function projectNode(db: DB, ref: string): State {
+ const row = db.select().from(nodes).where(eq(nodes.ref, ref)).get();
+ if (!row) throw new Error(`Node not found: ${ref}`);
+
+ const children = db.select().from(nodes).where(eq(nodes.parentRef, ref)).all();
+
+ const nodeLinks = db.select().from(links).where(eq(links.fromRef, ref)).all();
+
+ return {
+ ...toStructure(row),
+ children: children.map((c) => projectNode(db, c.ref)),
+ ...(nodeLinks.length > 0
+ ? {
+ links: nodeLinks.map((l) => ({
+ relation: l.relation,
+ target: projectLinked(db, l.toRef),
+ })),
+ }
+ : {}),
+ };
+}
+
+/** Project a node with full subtree but without following links (prevents cycles). */
+function projectLinked(db: DB, ref: string): State {
+ const row = db.select().from(nodes).where(eq(nodes.ref, ref)).get();
+ if (!row) throw new Error(`Node not found: ${ref}`);
+ const children = db.select().from(nodes).where(eq(nodes.parentRef, ref)).all();
+ return {
+ ...toStructure(row),
+ children: children.map((c) => projectLinked(db, c.ref)),
+ };
+}
+
+// ===== Subtree removal =====
+
+function removeSubtree(db: DB, ref: string): void {
+ // Remove children first (depth-first)
+ const children = db.select({ ref: nodes.ref }).from(nodes).where(eq(nodes.parentRef, ref)).all();
+ for (const child of children) {
+ removeSubtree(db, child.ref);
+ }
+
+ // Remove links from/to this node
+ db.delete(links).where(eq(links.fromRef, ref)).run();
+ db.delete(links).where(eq(links.toRef, ref)).run();
+
+ // Remove the node itself
+ db.delete(nodes).where(eq(nodes.ref, ref)).run();
+}
+
+// ===== Runtime factory =====
+
+export function createSqliteRuntime(db: DB): Runtime {
+ return {
+ async create(parent, type, information, id, alias) {
+ // Idempotent: same id under same parent → return existing.
+ if (id) {
+ const existing = db
+ .select()
+ .from(nodes)
+ .where(and(eq(nodes.id, id), eq(nodes.parentRef, parent?.ref ?? null)))
+ .get();
+ if (existing) return toStructure(existing);
+ }
+ const ref = nextRef(db);
+ db.insert(nodes)
+ .values({
+ ref,
+ id: id ?? null,
+ alias: alias && alias.length > 0 ? JSON.stringify(alias) : null,
+ name: type.name,
+ description: type.description,
+ parentRef: parent?.ref ?? null,
+ information: information ?? null,
+ tag: null,
+ })
+ .run();
+ return toStructure(db.select().from(nodes).where(eq(nodes.ref, ref)).get()!);
+ },
+
+ async remove(node) {
+ if (!node.ref) return;
+ const row = db.select().from(nodes).where(eq(nodes.ref, node.ref)).get();
+ if (!row) return;
+
+ // Detach from parent's children (implicit via parentRef)
+ removeSubtree(db, node.ref);
+ },
+
+ async transform(source, target, information) {
+ if (!source.ref) throw new Error("Source node has no ref");
+ const row = db.select().from(nodes).where(eq(nodes.ref, source.ref)).get();
+ if (!row) throw new Error(`Source node not found: ${source.ref}`);
+
+ const targetParent = target.parent;
+ if (!targetParent) {
+ throw new Error(`Cannot transform to root structure: ${target.name}`);
+ }
+
+ const parentRow = db.select().from(nodes).where(eq(nodes.name, targetParent.name)).get();
+ if (!parentRow) {
+ throw new Error(`No node found for structure: ${targetParent.name}`);
+ }
+
+ // Reparent + update type in place — subtree preserved
+ db.update(nodes)
+ .set({
+ parentRef: parentRow.ref,
+ name: target.name,
+ description: target.description,
+ ...(information !== undefined ? { information } : {}),
+ })
+ .where(eq(nodes.ref, source.ref))
+ .run();
+
+ return toStructure(db.select().from(nodes).where(eq(nodes.ref, source.ref)).get()!);
+ },
+
+ async link(from, to, relationName, reverseName) {
+ if (!from.ref) throw new Error("Source node has no ref");
+ if (!to.ref) throw new Error("Target node has no ref");
+
+ // Forward: from → to
+ const existsForward = db
+ .select()
+ .from(links)
+ .where(
+ and(
+ eq(links.fromRef, from.ref),
+ eq(links.toRef, to.ref),
+ eq(links.relation, relationName)
+ )
+ )
+ .get();
+ if (!existsForward) {
+ db.insert(links).values({ fromRef: from.ref, toRef: to.ref, relation: relationName }).run();
+ }
+
+ // Reverse: to → from
+ const existsReverse = db
+ .select()
+ .from(links)
+ .where(
+ and(eq(links.fromRef, to.ref), eq(links.toRef, from.ref), eq(links.relation, reverseName))
+ )
+ .get();
+ if (!existsReverse) {
+ db.insert(links).values({ fromRef: to.ref, toRef: from.ref, relation: reverseName }).run();
+ }
+ },
+
+ async unlink(from, to, relationName, reverseName) {
+ if (!from.ref || !to.ref) return;
+
+ db.delete(links)
+ .where(
+ and(
+ eq(links.fromRef, from.ref),
+ eq(links.toRef, to.ref),
+ eq(links.relation, relationName)
+ )
+ )
+ .run();
+
+ db.delete(links)
+ .where(
+ and(eq(links.fromRef, to.ref), eq(links.toRef, from.ref), eq(links.relation, reverseName))
+ )
+ .run();
+ },
+
+ async tag(node, tagValue) {
+ if (!node.ref) throw new Error("Node has no ref");
+ const row = db.select().from(nodes).where(eq(nodes.ref, node.ref)).get();
+ if (!row) throw new Error(`Node not found: ${node.ref}`);
+ db.update(nodes).set({ tag: tagValue }).where(eq(nodes.ref, node.ref)).run();
+ },
+
+ async project(node) {
+ if (!node.ref) throw new Error(`Node has no ref`);
+ return projectNode(db, node.ref);
+ },
+
+ async roots() {
+ const rows = db.select().from(nodes).where(isNull(nodes.parentRef)).all();
+ return rows.map(toStructure);
+ },
+ };
+}
diff --git a/packages/local-platform/tests/manifest.test.ts b/packages/local-platform/tests/manifest.test.ts
index 0e5dbaa..8e7ba92 100644
--- a/packages/local-platform/tests/manifest.test.ts
+++ b/packages/local-platform/tests/manifest.test.ts
@@ -130,6 +130,30 @@ describe("stateToFiles", () => {
expect(manifest.links).toEqual({ belong: ["deepractice"] });
});
+ test("child node links are serialized", () => {
+ const plan1 = state("plan", { id: "phase-1", information: "Feature: Phase 1" });
+ const plan2 = state("plan", {
+ id: "phase-2",
+ information: "Feature: Phase 2",
+ links: [{ relation: "after", target: state("plan", { id: "phase-1" }) }],
+ });
+ const s = state("individual", {
+ id: "sean",
+ children: [
+ state("goal", {
+ id: "g1",
+ information: "Feature: Goal",
+ children: [plan1, plan2],
+ }),
+ ],
+ });
+ const { manifest } = stateToFiles(s);
+
+ const goalNode = manifest.children?.g1;
+ expect(goalNode?.children?.["phase-1"]?.links).toBeUndefined();
+ expect(goalNode?.children?.["phase-2"]?.links).toEqual({ after: ["phase-1"] });
+ });
+
test("multiple links of same relation", () => {
const org1 = state("organization", { id: "dp" });
const org2 = state("organization", { id: "acme" });
@@ -270,6 +294,28 @@ describe("filesToState", () => {
expect(s.links![0].relation).toBe("belong");
expect(s.links![0].target.id).toBe("deepractice");
});
+
+ test("child node links are deserialized", () => {
+ const manifest = {
+ id: "sean",
+ type: "individual",
+ children: {
+ g1: {
+ type: "goal",
+ children: {
+ "phase-1": { type: "plan" },
+ "phase-2": { type: "plan", links: { after: ["phase-1"] } },
+ },
+ },
+ },
+ };
+ const s = filesToState(manifest, {});
+ const goal = s.children![0];
+ const phase2 = goal.children!.find((c) => c.id === "phase-2")!;
+ expect(phase2.links).toHaveLength(1);
+ expect(phase2.links![0].relation).toBe("after");
+ expect(phase2.links![0].target.id).toBe("phase-1");
+ });
});
// ================================================================
@@ -323,6 +369,47 @@ describe("round-trip", () => {
expect(naming?.information).toBe("Feature: Name params well");
});
+ test("child node links survive round-trip", () => {
+ const original = state("individual", {
+ id: "sean",
+ children: [
+ state("goal", {
+ id: "g1",
+ information: "Feature: Goal",
+ children: [
+ state("plan", {
+ id: "phase-1",
+ information: "Feature: Phase 1",
+ links: [{ relation: "before", target: state("plan", { id: "phase-2" }) }],
+ }),
+ state("plan", {
+ id: "phase-2",
+ information: "Feature: Phase 2",
+ links: [{ relation: "after", target: state("plan", { id: "phase-1" }) }],
+ }),
+ ],
+ }),
+ ],
+ });
+
+ const { manifest, files } = stateToFiles(original);
+ const fileContents: Record = {};
+ for (const f of files) fileContents[f.path] = f.content;
+
+ const restored = filesToState(manifest, fileContents);
+ const goal = restored.children![0];
+ const p1 = goal.children!.find((c) => c.id === "phase-1")!;
+ const p2 = goal.children!.find((c) => c.id === "phase-2")!;
+
+ expect(p1.links).toHaveLength(1);
+ expect(p1.links![0].relation).toBe("before");
+ expect(p1.links![0].target.id).toBe("phase-2");
+
+ expect(p2.links).toHaveLength(1);
+ expect(p2.links![0].relation).toBe("after");
+ expect(p2.links![0].target.id).toBe("phase-1");
+ });
+
test("multiple encounters with distinct ids survive round-trip", () => {
const original = state("individual", {
id: "sean",
diff --git a/packages/local-platform/tests/prototype.test.ts b/packages/local-platform/tests/prototype.test.ts
index 7db104f..a32191e 100644
--- a/packages/local-platform/tests/prototype.test.ts
+++ b/packages/local-platform/tests/prototype.test.ts
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test } from "bun:test";
-import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
+import { existsSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { localPlatform } from "../src/index.js";
@@ -11,139 +11,30 @@ afterEach(() => {
if (existsSync(testDir)) rmSync(testDir, { recursive: true });
});
-/** Write a ResourceX-compatible prototype directory. */
-function writePrototype(
- baseDir: string,
- id: string,
- type: "role" | "organization",
- features: Record = {}
-): string {
- const manifestFile = type === "role" ? "individual.json" : "organization.json";
- const manifestType = type === "role" ? "individual" : "organization";
- const dir = join(baseDir, id);
- mkdirSync(dir, { recursive: true });
-
- // resource.json — ResourceX source marker
- writeFileSync(
- join(dir, "resource.json"),
- JSON.stringify({
- name: id,
- type,
- tag: "0.1.0",
- author: "test",
- description: `${id} prototype`,
- }),
- "utf-8"
- );
-
- // manifest
- writeFileSync(
- join(dir, manifestFile),
- JSON.stringify({
- id,
- type: manifestType,
- children: { identity: { type: "identity" } },
- }),
- "utf-8"
- );
-
- // feature files
- for (const [name, content] of Object.entries(features)) {
- writeFileSync(join(dir, name), content, "utf-8");
- }
-
- return dir;
-}
-
-describe("LocalPlatform Prototype (registry-based)", () => {
- test("resolve returns undefined when nothing registered", async () => {
- const { prototype } = localPlatform({ dataDir: testDir, resourceDir });
- expect(await prototype!.resolve("unknown")).toBeUndefined();
- });
-
- test("resolve returns undefined when resourcex is disabled", async () => {
- const { prototype } = localPlatform({ dataDir: testDir, resourceDir: null });
- expect(await prototype!.resolve("sean")).toBeUndefined();
- });
-
- test("resolve returns undefined when dataDir is null", async () => {
- const { prototype } = localPlatform({ dataDir: null });
- expect(await prototype!.resolve("sean")).toBeUndefined();
- });
-
- test("registerPrototype + resolve round-trip for role", async () => {
- const protoDir = join(testDir, "protos");
- const dir = writePrototype(protoDir, "nuwa", "role", {
- "nuwa.individual.feature": "Feature: Nuwa\n World admin.",
- });
-
- const platform = localPlatform({ dataDir: testDir, resourceDir });
- platform.registerPrototype!("nuwa", dir);
-
- const state = await platform.prototype!.resolve("nuwa");
- expect(state).toBeDefined();
- expect(state!.id).toBe("nuwa");
- expect(state!.name).toBe("individual");
- expect(state!.information).toBe("Feature: Nuwa\n World admin.");
- expect(state!.children).toHaveLength(1);
- expect(state!.children![0].name).toBe("identity");
+describe("LocalPlatform Prototype registry", () => {
+ test("settle registers id → source", () => {
+ const { repository } = localPlatform({ dataDir: testDir, resourceDir });
+ repository.prototype.settle("test-role", "/path/to/source");
+ expect(repository.prototype.list()["test-role"]).toBe("/path/to/source");
});
- test("registerPrototype + resolve round-trip for organization", async () => {
- const protoDir = join(testDir, "protos");
- const dir = writePrototype(protoDir, "deepractice", "organization", {
- "deepractice.organization.feature": "Feature: Deepractice\n AI company.",
- });
-
- const platform = localPlatform({ dataDir: testDir, resourceDir });
- platform.registerPrototype!("deepractice", dir);
-
- const state = await platform.prototype!.resolve("deepractice");
- expect(state).toBeDefined();
- expect(state!.id).toBe("deepractice");
- expect(state!.name).toBe("organization");
- expect(state!.information).toBe("Feature: Deepractice\n AI company.");
+ test("settle overwrites previous source", () => {
+ const { repository } = localPlatform({ dataDir: testDir, resourceDir });
+ repository.prototype.settle("test", "/v1");
+ repository.prototype.settle("test", "/v2");
+ expect(repository.prototype.list().test).toBe("/v2");
});
- test("resolve returns undefined for unregistered id", async () => {
- const protoDir = join(testDir, "protos");
- const dir = writePrototype(protoDir, "nuwa", "role");
-
- const platform = localPlatform({ dataDir: testDir, resourceDir });
- platform.registerPrototype!("nuwa", dir);
-
- expect(await platform.prototype!.resolve("nobody")).toBeUndefined();
+ test("list returns empty object when no prototypes registered", () => {
+ const { repository } = localPlatform({ dataDir: testDir, resourceDir });
+ expect(repository.prototype.list()).toEqual({});
});
- test("registerPrototype overwrites previous source", async () => {
- const protoDir = join(testDir, "protos");
- const dir1 = writePrototype(protoDir, "v1", "role", {
- "v1.individual.feature": "Feature: V1",
- });
- const dir2 = writePrototype(protoDir, "v2", "role", {
- "v2.individual.feature": "Feature: V2",
- });
-
- const platform = localPlatform({ dataDir: testDir, resourceDir });
- platform.registerPrototype!("test", dir1);
- platform.registerPrototype!("test", dir2);
-
- const state = await platform.prototype!.resolve("test");
- expect(state!.id).toBe("v2");
- });
-
- test("registry persists across platform instances", async () => {
- const protoDir = join(testDir, "protos");
- const dir = writePrototype(protoDir, "nuwa", "role");
-
- // First instance: register
+ test("registry persists across platform instances", () => {
const p1 = localPlatform({ dataDir: testDir, resourceDir });
- p1.registerPrototype!("nuwa", dir);
+ p1.repository.prototype.settle("test-role", "/path");
- // Second instance: resolve (reads same prototype.json)
const p2 = localPlatform({ dataDir: testDir, resourceDir });
- const state = await p2.prototype!.resolve("nuwa");
- expect(state).toBeDefined();
- expect(state!.id).toBe("nuwa");
+ expect(p2.repository.prototype.list()["test-role"]).toBe("/path");
});
});
diff --git a/packages/local-platform/tests/runtime.test.ts b/packages/local-platform/tests/runtime.test.ts
index e4a40f3..eedccee 100644
--- a/packages/local-platform/tests/runtime.test.ts
+++ b/packages/local-platform/tests/runtime.test.ts
@@ -3,7 +3,7 @@ import { relation, structure } from "@rolexjs/system";
import { localPlatform } from "../src/index.js";
function createGraphRuntime() {
- return localPlatform({ dataDir: null }).runtime;
+ return localPlatform({ dataDir: null }).repository.runtime;
}
// ================================================================
@@ -50,67 +50,67 @@ describe("Graph Runtime", () => {
// ============================================================
describe("create & project", () => {
- test("create root node", () => {
+ test("create root node", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
+ const root = await rt.create(null, society);
expect(root.ref).toBeDefined();
expect(root.name).toBe("society");
- const state = rt.project(root);
+ const state = await rt.project(root);
expect(state.name).toBe("society");
expect(state.children).toHaveLength(0);
});
- test("create child under parent", () => {
+ test("create child under parent", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
expect(sean.name).toBe("individual");
- const state = rt.project(root);
+ const state = await rt.project(root);
expect(state.children).toHaveLength(1);
expect(state.children![0].name).toBe("individual");
});
- test("create node with information", () => {
+ test("create node with information", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual, "Feature: I am Sean");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual, "Feature: I am Sean");
- const state = rt.project(sean);
+ const state = await rt.project(sean);
expect(state.information).toBe("Feature: I am Sean");
});
- test("node is concept + container + information carrier", () => {
+ test("node is concept + container + information carrier", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
// experience as information carrier (no children)
- const exp1 = rt.create(sean, experience, "Feature: I learned JWT...");
- const s1 = rt.project(exp1);
+ const exp1 = await rt.create(sean, experience, "Feature: I learned JWT...");
+ const s1 = await rt.project(exp1);
expect(s1.information).toBe("Feature: I learned JWT...");
expect(s1.children).toHaveLength(0);
// experience as container (has children)
- const exp2 = rt.create(sean, experience);
- const _child = rt.create(exp2, encounter, "Feature: JWT incident");
- const s2 = rt.project(exp2);
+ const exp2 = await rt.create(sean, experience);
+ const _child = await rt.create(exp2, encounter, "Feature: JWT incident");
+ const s2 = await rt.project(exp2);
expect(s2.information).toBeUndefined();
expect(s2.children).toHaveLength(1);
expect(s2.children![0].name).toBe("encounter");
});
- test("deep tree — society > individual > identity > background", () => {
+ test("deep tree — society > individual > identity > background", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const id = rt.create(sean, identity);
- const _bg = rt.create(id, background, "Feature: CS from MIT");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const id = await rt.create(sean, identity);
+ const _bg = await rt.create(id, background, "Feature: CS from MIT");
- const state = rt.project(root);
+ const state = await rt.project(root);
expect(state.children).toHaveLength(1);
const indState = state.children![0];
expect(indState.children).toHaveLength(1);
@@ -120,17 +120,17 @@ describe("Graph Runtime", () => {
expect(idState.children![0].information).toBe("Feature: CS from MIT");
});
- test("multiple children under same parent", () => {
+ test("multiple children under same parent", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const id = rt.create(sean, identity);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const id = await rt.create(sean, identity);
- rt.create(id, background, "Feature: My background");
- rt.create(id, tone, "Feature: My tone");
- rt.create(id, mindset, "Feature: My mindset");
+ await rt.create(id, background, "Feature: My background");
+ await rt.create(id, tone, "Feature: My tone");
+ await rt.create(id, mindset, "Feature: My mindset");
- const state = rt.project(id);
+ const state = await rt.project(id);
expect(state.children).toHaveLength(3);
const names = state.children!.map((c) => c.name);
expect(names).toContain("background");
@@ -144,34 +144,34 @@ describe("Graph Runtime", () => {
// ============================================================
describe("remove", () => {
- test("remove a leaf node", () => {
+ test("remove a leaf node", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const k = rt.create(sean, knowledge);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const k = await rt.create(sean, knowledge);
- rt.remove(k);
- const state = rt.project(sean);
+ await rt.remove(k);
+ const state = await rt.project(sean);
expect(state.children).toHaveLength(0);
});
- test("remove a subtree", () => {
+ test("remove a subtree", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const id = rt.create(sean, identity);
- rt.create(id, background, "bg");
- rt.create(id, tone, "tone");
- rt.create(id, mindset, "mindset");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const id = await rt.create(sean, identity);
+ await rt.create(id, background, "bg");
+ await rt.create(id, tone, "tone");
+ await rt.create(id, mindset, "mindset");
- rt.remove(id); // removes identity + background + tone + mindset
- const state = rt.project(sean);
+ await rt.remove(id); // removes identity + background + tone + mindset
+ const state = await rt.project(sean);
expect(state.children).toHaveLength(0);
});
- test("remove node without ref is a no-op", () => {
+ test("remove node without ref is a no-op", async () => {
const rt = createGraphRuntime();
- rt.remove(individual); // no ref, should not throw
+ await rt.remove(individual); // no ref, should not throw
});
});
@@ -180,82 +180,82 @@ describe("Graph Runtime", () => {
// ============================================================
describe("transform", () => {
- test("finish: transform task → encounter", () => {
+ test("finish: transform task → encounter", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const g = rt.create(sean, goal, "Feature: Build auth");
- const p = rt.create(g, plan, "Feature: Auth plan");
- const t = rt.create(p, task, "Feature: Implement login");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const g = await rt.create(sean, goal, "Feature: Build auth");
+ const p = await rt.create(g, plan, "Feature: Auth plan");
+ const t = await rt.create(p, task, "Feature: Implement login");
- const enc = rt.transform(t, encounter, "Feature: Login done, learned about JWT");
+ const enc = await rt.transform(t, encounter, "Feature: Login done, learned about JWT");
expect(enc.name).toBe("encounter");
expect(enc.information).toBe("Feature: Login done, learned about JWT");
// encounter should be under individual (sean)
- const state = rt.project(sean);
+ const state = await rt.project(sean);
const encounterStates = state.children!.filter((c) => c.name === "encounter");
expect(encounterStates).toHaveLength(1);
});
- test("achieve: transform goal → encounter", () => {
+ test("achieve: transform goal → encounter", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- rt.create(sean, goal, "Feature: Build auth");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ await rt.create(sean, goal, "Feature: Build auth");
- const enc = rt.transform(sean, encounter, "Feature: Auth achieved");
+ const enc = await rt.transform(sean, encounter, "Feature: Auth achieved");
expect(enc.name).toBe("encounter");
- const state = rt.project(sean);
+ const state = await rt.project(sean);
const encounters = state.children!.filter((c) => c.name === "encounter");
expect(encounters).toHaveLength(1);
});
- test("reflect: transform encounter → experience", () => {
+ test("reflect: transform encounter → experience", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const enc = rt.create(sean, encounter, "Feature: JWT incident");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const enc = await rt.create(sean, encounter, "Feature: JWT incident");
- const exp = rt.transform(enc, experience, "Feature: Always use refresh tokens");
+ const exp = await rt.transform(enc, experience, "Feature: Always use refresh tokens");
expect(exp.name).toBe("experience");
- const state = rt.project(sean);
+ const state = await rt.project(sean);
const experiences = state.children!.filter((c) => c.name === "experience");
expect(experiences).toHaveLength(1);
expect(experiences[0].information).toBe("Feature: Always use refresh tokens");
});
- test("realize: transform experience → principle", () => {
+ test("realize: transform experience → principle", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- rt.create(sean, knowledge);
- const exp = rt.create(sean, experience, "Feature: Auth lessons");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ await rt.create(sean, knowledge);
+ const exp = await rt.create(sean, experience, "Feature: Auth lessons");
- const prin = rt.transform(exp, principle, "Feature: Security first");
+ const prin = await rt.transform(exp, principle, "Feature: Security first");
expect(prin.name).toBe("principle");
// principle should be under knowledge
- const state = rt.project(sean);
+ const state = await rt.project(sean);
const knowledgeState = state.children!.find((c) => c.name === "knowledge");
expect(knowledgeState).toBeDefined();
expect(knowledgeState!.children).toHaveLength(1);
expect(knowledgeState!.children![0].name).toBe("principle");
});
- test("retire: transform individual → past", () => {
+ test("retire: transform individual → past", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- rt.create(root, past);
- const sean = rt.create(root, individual, "Feature: Sean");
+ const root = await rt.create(null, society);
+ await rt.create(root, past);
+ const sean = await rt.create(root, individual, "Feature: Sean");
- const retired = rt.transform(sean, past, "Feature: Sean retired");
+ const retired = await rt.transform(sean, past, "Feature: Sean retired");
expect(retired.name).toBe("past");
// past node should exist under society
- const state = rt.project(root);
+ const state = await rt.project(root);
const pastNodes = state.children!.filter((c) => c.name === "past");
expect(pastNodes.length).toBeGreaterThanOrEqual(1);
});
@@ -266,154 +266,158 @@ describe("Graph Runtime", () => {
// ============================================================
describe("link & unlink", () => {
- test("hire: link organization membership", () => {
+ test("hire: link organization membership", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual, "Feature: I am Sean");
- const dp = rt.create(root, organization);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual, "Feature: I am Sean");
+ const dp = await rt.create(root, organization);
- rt.link(dp, sean, "membership", "belong");
+ await rt.link(dp, sean, "membership", "belong");
- const state = rt.project(dp);
+ const state = await rt.project(dp);
expect(state.links).toHaveLength(1);
expect(state.links![0].relation).toBe("membership");
expect(state.links![0].target.name).toBe("individual");
expect(state.links![0].target.information).toBe("Feature: I am Sean");
});
- test("fire: unlink organization membership", () => {
+ test("fire: unlink organization membership", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const dp = rt.create(root, organization);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const dp = await rt.create(root, organization);
- rt.link(dp, sean, "membership", "belong");
- rt.unlink(dp, sean, "membership", "belong");
+ await rt.link(dp, sean, "membership", "belong");
+ await rt.unlink(dp, sean, "membership", "belong");
- const state = rt.project(dp);
+ const state = await rt.project(dp);
expect(state.links).toBeUndefined();
});
- test("appoint: link position appointment", () => {
+ test("appoint: link position appointment", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual, "Feature: I am Sean");
- const dp = rt.create(root, organization);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual, "Feature: I am Sean");
+ const dp = await rt.create(root, organization);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment", "serve");
+ await rt.link(arch, sean, "appointment", "serve");
- const state = rt.project(arch);
+ const state = await rt.project(arch);
expect(state.links).toHaveLength(1);
expect(state.links![0].relation).toBe("appointment");
expect(state.links![0].target.name).toBe("individual");
});
- test("dismiss: unlink position appointment", () => {
+ test("dismiss: unlink position appointment", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const dp = rt.create(root, organization);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const dp = await rt.create(root, organization);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment", "serve");
- rt.unlink(arch, sean, "appointment", "serve");
+ await rt.link(arch, sean, "appointment", "serve");
+ await rt.unlink(arch, sean, "appointment", "serve");
- const state = rt.project(arch);
+ const state = await rt.project(arch);
expect(state.links).toBeUndefined();
});
- test("link is idempotent", () => {
+ test("link is idempotent", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const dp = rt.create(root, organization);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const dp = await rt.create(root, organization);
- rt.link(dp, sean, "membership", "belong");
- rt.link(dp, sean, "membership", "belong"); // duplicate
+ await rt.link(dp, sean, "membership", "belong");
+ await rt.link(dp, sean, "membership", "belong"); // duplicate
- const state = rt.project(dp);
+ const state = await rt.project(dp);
expect(state.links).toHaveLength(1);
});
- test("multiple members in one organization", () => {
+ test("multiple members in one organization", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual, "Feature: Sean");
- const alice = rt.create(root, individual, "Feature: Alice");
- const dp = rt.create(root, organization);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual, "Feature: Sean");
+ const alice = await rt.create(root, individual, "Feature: Alice");
+ const dp = await rt.create(root, organization);
- rt.link(dp, sean, "membership", "belong");
- rt.link(dp, alice, "membership", "belong");
+ await rt.link(dp, sean, "membership", "belong");
+ await rt.link(dp, alice, "membership", "belong");
- const state = rt.project(dp);
+ const state = await rt.project(dp);
expect(state.links).toHaveLength(2);
});
- test("parent projection includes child links", () => {
+ test("parent projection includes child links", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual, "Feature: Sean");
- const dp = rt.create(root, organization);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual, "Feature: Sean");
+ const dp = await rt.create(root, organization);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment", "serve");
+ await rt.link(arch, sean, "appointment", "serve");
// project from org level
- const state = rt.project(dp);
+ const state = await rt.project(dp);
expect(state.children).toHaveLength(1);
expect(state.children![0].links).toHaveLength(1);
expect(state.children![0].links![0].target.name).toBe("individual");
});
- test("remove source node cleans up outgoing links", () => {
+ test("remove source node cleans up outgoing links", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const dp = rt.create(root, organization);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const dp = await rt.create(root, organization);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment", "serve");
- rt.remove(arch);
+ await rt.link(arch, sean, "appointment", "serve");
+ await rt.remove(arch);
- const state = rt.project(dp);
+ const state = await rt.project(dp);
expect(state.children).toHaveLength(0);
});
- test("remove target node cleans up incoming links", () => {
+ test("remove target node cleans up incoming links", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- const dp = rt.create(root, organization);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ const dp = await rt.create(root, organization);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment", "serve");
- rt.remove(sean);
+ await rt.link(arch, sean, "appointment", "serve");
+ await rt.remove(sean);
- const state = rt.project(arch);
+ const state = await rt.project(arch);
expect(state.links).toBeUndefined();
});
- test("link throws if source has no ref", () => {
+ test("link throws if source has no ref", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- expect(() => rt.link(individual, sean, "test", "test-rev")).toThrow("Source node has no ref");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ expect(rt.link(individual, sean, "test", "test-rev")).rejects.toThrow(
+ "Source node has no ref"
+ );
});
- test("link throws if target has no ref", () => {
+ test("link throws if target has no ref", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- expect(() => rt.link(sean, individual, "test", "test-rev")).toThrow("Target node has no ref");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ expect(rt.link(sean, individual, "test", "test-rev")).rejects.toThrow(
+ "Target node has no ref"
+ );
});
- test("node without links has no links in projection", () => {
+ test("node without links has no links in projection", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
- const state = rt.project(sean);
+ const state = await rt.project(sean);
expect(state.links).toBeUndefined();
});
});
@@ -423,34 +427,34 @@ describe("Graph Runtime", () => {
// ============================================================
describe("execution cycle", () => {
- test("want → plan → todo → finish → reflect → realize", () => {
+ test("want → plan → todo → finish → reflect → realize", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual);
- rt.create(sean, knowledge);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual);
+ await rt.create(sean, knowledge);
// want: create goal
- const g = rt.create(sean, goal, "Feature: Build auth system");
+ const g = await rt.create(sean, goal, "Feature: Build auth system");
expect(g.name).toBe("goal");
// plan: create plan under goal
- const p = rt.create(g, plan, "Feature: Auth implementation plan");
+ const p = await rt.create(g, plan, "Feature: Auth implementation plan");
expect(p.name).toBe("plan");
// todo: create task under plan
- const t = rt.create(p, task, "Feature: Implement JWT login");
+ const t = await rt.create(p, task, "Feature: Implement JWT login");
expect(t.name).toBe("task");
// finish: transform task → encounter
- const enc = rt.transform(t, encounter, "Feature: JWT login done");
+ const enc = await rt.transform(t, encounter, "Feature: JWT login done");
expect(enc.name).toBe("encounter");
// reflect: transform encounter → experience
- const exp = rt.transform(enc, experience, "Feature: JWT refresh tokens are essential");
+ const exp = await rt.transform(enc, experience, "Feature: JWT refresh tokens are essential");
expect(exp.name).toBe("experience");
// realize: transform experience → principle
- const prin = rt.transform(
+ const prin = await rt.transform(
exp,
principle,
"Feature: Always use short-lived tokens with refresh"
@@ -458,7 +462,7 @@ describe("Graph Runtime", () => {
expect(prin.name).toBe("principle");
// verify final state
- const state = rt.project(sean);
+ const state = await rt.project(sean);
const knowledgeState = state.children!.find((c) => c.name === "knowledge");
expect(knowledgeState).toBeDefined();
expect(knowledgeState!.children).toHaveLength(1);
@@ -474,44 +478,44 @@ describe("Graph Runtime", () => {
// ============================================================
describe("organization scenario", () => {
- test("found → establish → born → hire → appoint → dismiss → fire", () => {
+ test("found → establish → born → hire → appoint → dismiss → fire", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
+ const root = await rt.create(null, society);
// found: create organization
- const dp = rt.create(root, organization);
+ const dp = await rt.create(root, organization);
// establish: create position
- const arch = rt.create(dp, position);
- const _archDuty = rt.create(arch, duty, "Feature: Design system architecture");
+ const arch = await rt.create(dp, position);
+ const _archDuty = await rt.create(arch, duty, "Feature: Design system architecture");
// born: create individual
- const sean = rt.create(root, individual, "Feature: I am Sean");
+ const sean = await rt.create(root, individual, "Feature: I am Sean");
// hire: link membership
- rt.link(dp, sean, "membership", "belong");
- let orgState = rt.project(dp);
+ await rt.link(dp, sean, "membership", "belong");
+ let orgState = await rt.project(dp);
expect(orgState.links).toHaveLength(1);
expect(orgState.links![0].relation).toBe("membership");
// appoint: link appointment
- rt.link(arch, sean, "appointment", "serve");
- let posState = rt.project(arch);
+ await rt.link(arch, sean, "appointment", "serve");
+ let posState = await rt.project(arch);
expect(posState.links).toHaveLength(1);
expect(posState.links![0].relation).toBe("appointment");
// dismiss: unlink appointment
- rt.unlink(arch, sean, "appointment", "serve");
- posState = rt.project(arch);
+ await rt.unlink(arch, sean, "appointment", "serve");
+ posState = await rt.project(arch);
expect(posState.links).toBeUndefined();
// fire: unlink membership
- rt.unlink(dp, sean, "membership", "belong");
- orgState = rt.project(dp);
+ await rt.unlink(dp, sean, "membership", "belong");
+ orgState = await rt.project(dp);
expect(orgState.links).toBeUndefined();
// individual still exists
- const seanState = rt.project(sean);
+ const seanState = await rt.project(sean);
expect(seanState.name).toBe("individual");
});
});
@@ -521,43 +525,43 @@ describe("Graph Runtime", () => {
// ============================================================
describe("id & alias", () => {
- test("create with id stores it on node", () => {
+ test("create with id stores it on node", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual, "Feature: Sean", "sean");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual, "Feature: Sean", "sean");
expect(sean.ref).toBeDefined();
expect(sean.id).toBe("sean");
});
- test("create with id and alias stores both", () => {
+ test("create with id and alias stores both", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual, "Feature: Sean", "sean", ["Sean", "姜山"]);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual, "Feature: Sean", "sean", ["Sean", "姜山"]);
expect(sean.id).toBe("sean");
expect(sean.alias).toEqual(["Sean", "姜山"]);
});
- test("id and alias appear in projection", () => {
+ test("id and alias appear in projection", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual, "Feature: Sean", "sean", ["Sean"]);
- const state = rt.project(sean);
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual, "Feature: Sean", "sean", ["Sean"]);
+ const state = await rt.project(sean);
expect(state.id).toBe("sean");
expect(state.alias).toEqual(["Sean"]);
});
- test("create without id has no id field", () => {
+ test("create without id has no id field", async () => {
const rt = createGraphRuntime();
- const root = rt.create(null, society);
- const sean = rt.create(root, individual, "Feature: Sean");
+ const root = await rt.create(null, society);
+ const sean = await rt.create(root, individual, "Feature: Sean");
expect(sean.id).toBeUndefined();
expect(sean.alias).toBeUndefined();
});
- test("id and alias appear in roots()", () => {
+ test("id and alias appear in roots()", async () => {
const rt = createGraphRuntime();
- const _root = rt.create(null, society, undefined, "world", ["World"]);
- const roots = rt.roots();
+ const _root = await rt.create(null, society, undefined, "world", ["World"]);
+ const roots = await rt.roots();
expect(roots).toHaveLength(1);
expect(roots[0].id).toBe("world");
expect(roots[0].alias).toEqual(["World"]);
diff --git a/packages/local-platform/tests/sqliteRuntime.test.ts b/packages/local-platform/tests/sqliteRuntime.test.ts
new file mode 100644
index 0000000..a2927ec
--- /dev/null
+++ b/packages/local-platform/tests/sqliteRuntime.test.ts
@@ -0,0 +1,155 @@
+import { describe, expect, test } from "bun:test";
+import { drizzle } from "@deepracticex/drizzle";
+import { openDatabase } from "@deepracticex/sqlite";
+import * as C from "@rolexjs/core";
+import { sql } from "drizzle-orm";
+import { createSqliteRuntime } from "../src/sqliteRuntime.js";
+
+function setup() {
+ const rawDb = openDatabase(":memory:");
+ const db = drizzle(rawDb);
+ // Create tables
+ db.run(sql`CREATE TABLE nodes (
+ ref TEXT PRIMARY KEY,
+ id TEXT,
+ alias TEXT,
+ name TEXT NOT NULL,
+ description TEXT DEFAULT '',
+ parent_ref TEXT REFERENCES nodes(ref),
+ information TEXT,
+ tag TEXT
+ )`);
+ db.run(sql`CREATE TABLE links (
+ from_ref TEXT NOT NULL REFERENCES nodes(ref),
+ to_ref TEXT NOT NULL REFERENCES nodes(ref),
+ relation TEXT NOT NULL,
+ PRIMARY KEY (from_ref, to_ref, relation)
+ )`);
+ const rt = createSqliteRuntime(db);
+ return { db, rt };
+}
+
+describe("SQLite Runtime", () => {
+ test("create root node", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ expect(society.ref).toBe("n1");
+ expect(society.name).toBe("society");
+ });
+
+ test("create child node", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ const ind = await rt.create(society, C.individual, "Feature: Sean", "sean", ["Sean", "姜山"]);
+ expect(ind.ref).toBe("n2");
+ expect(ind.id).toBe("sean");
+ expect(ind.name).toBe("individual");
+ expect(ind.alias).toEqual(["Sean", "姜山"]);
+ expect(ind.information).toBe("Feature: Sean");
+ });
+
+ test("project subtree", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ const ind = await rt.create(society, C.individual, "Feature: Sean", "sean");
+ await rt.create(ind, C.identity, undefined, "identity");
+
+ const state = await rt.project(society);
+ expect(state.children).toHaveLength(1);
+ expect(state.children![0].id).toBe("sean");
+ expect(state.children![0].children).toHaveLength(1);
+ expect(state.children![0].children![0].id).toBe("identity");
+ });
+
+ test("remove subtree", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ const ind = await rt.create(society, C.individual, "Feature: Sean", "sean");
+ await rt.create(ind, C.identity, undefined, "identity");
+
+ await rt.remove(ind);
+ const state = await rt.project(society);
+ expect(state.children).toHaveLength(0);
+ });
+
+ test("link and unlink", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ const org = await rt.create(society, C.organization, "Feature: DP", "dp");
+ const ind = await rt.create(society, C.individual, "Feature: Sean", "sean");
+
+ await rt.link(org, ind, "membership", "belong");
+
+ let state = await rt.project(org);
+ expect(state.links).toHaveLength(1);
+ expect(state.links![0].relation).toBe("membership");
+ expect(state.links![0].target.id).toBe("sean");
+
+ await rt.unlink(org, ind, "membership", "belong");
+ state = await rt.project(org);
+ expect(state.links).toBeUndefined();
+ });
+
+ test("tag node", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ const goal = await rt.create(society, C.goal, "Feature: Test", "test-goal");
+ await rt.tag(goal, "done");
+
+ const state = await rt.project(goal);
+ expect(state.tag).toBe("done");
+ });
+
+ test("roots returns only root nodes", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ await rt.create(society, C.individual, "Feature: Sean", "sean");
+
+ const roots = await rt.roots();
+ expect(roots).toHaveLength(1);
+ expect(roots[0].name).toBe("society");
+ });
+
+ test("transform creates node under target parent", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ await rt.create(society, C.past);
+ const ind = await rt.create(society, C.individual, "Feature: Sean", "sean");
+
+ // Transform: create a "past" typed node (archive) — finds the past container by name
+ const archived = await rt.transform(ind, C.past, "Feature: Sean");
+ expect(archived.name).toBe("past");
+ expect(archived.information).toBe("Feature: Sean");
+ });
+
+ test("refs survive across operations (no stale refs)", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ const ind = await rt.create(society, C.individual, "Feature: Sean", "sean");
+ const org = await rt.create(society, C.organization, "Feature: DP", "dp");
+
+ // Multiple operations — society ref should always be valid
+ await rt.link(org, ind, "membership", "belong");
+ await rt.create(org, C.charter, "Feature: Mission");
+
+ // project using the original society ref — should work
+ const state = await rt.project(society);
+ expect(state.children).toHaveLength(2); // individual + organization
+ });
+
+ test("position persists (the bug that triggered this rewrite)", async () => {
+ const { rt } = setup();
+ const society = await rt.create(null, C.society);
+ const pos = await rt.create(society, C.position, "Feature: Architect", "architect");
+
+ // Project using the returned ref — this used to fail with "Node not found"
+ const state = await rt.project(pos);
+ expect(state.name).toBe("position");
+ expect(state.id).toBe("architect");
+
+ // Also visible from society
+ const societyState = await rt.project(society);
+ const names = societyState.children!.map((c) => c.name);
+ expect(names).toContain("position");
+ });
+});
diff --git a/packages/parser/parser b/packages/parser/parser
new file mode 120000
index 0000000..ae86715
--- /dev/null
+++ b/packages/parser/parser
@@ -0,0 +1 @@
+../../../parser
\ No newline at end of file
diff --git a/packages/resourcex-types/package.json b/packages/prototype/package.json
similarity index 50%
rename from packages/resourcex-types/package.json
rename to packages/prototype/package.json
index 45caa04..5527e19 100644
--- a/packages/resourcex-types/package.json
+++ b/packages/prototype/package.json
@@ -1,19 +1,18 @@
{
- "name": "@rolexjs/resourcex-types",
+ "name": "@rolexjs/prototype",
"version": "0.11.0",
- "description": "RoleX resource types for ResourceX — role and organization",
+ "description": "RoleX schema-driven API definition layer",
"keywords": [
"rolex",
- "resourcex",
- "type",
- "role",
- "organization"
+ "prototype",
+ "schema",
+ "instructions"
],
"repository": {
"type": "git",
- "url": "git+https://github.com/Deepractice/RoleX.git",
- "directory": "packages/resourcex-types"
+ "url": "git+https://github.com/Deepractice/RoleX.git"
},
+ "homepage": "https://github.com/Deepractice/RoleX",
"license": "MIT",
"engines": {
"node": ">=22.0.0"
@@ -32,15 +31,19 @@
"dist"
],
"scripts": {
- "build": "tsup",
+ "gen:desc": "bun run scripts/gen-descriptions.ts",
+ "gen:directives": "bun run scripts/gen-directives.ts",
+ "build": "bun run gen:desc && bun run gen:directives && tsup",
"lint": "biome lint .",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
- "resourcexjs": "^2.13.0"
+ "@rolexjs/core": "workspace:*",
+ "@rolexjs/system": "workspace:*",
+ "@rolexjs/parser": "workspace:*",
+ "resourcexjs": "^2.14.0"
},
- "devDependencies": {},
"publishConfig": {
"access": "public"
}
diff --git a/packages/prototype/scripts/gen-descriptions.ts b/packages/prototype/scripts/gen-descriptions.ts
new file mode 100644
index 0000000..748c36a
--- /dev/null
+++ b/packages/prototype/scripts/gen-descriptions.ts
@@ -0,0 +1,91 @@
+/**
+ * Generate descriptions/index.ts from .feature files.
+ *
+ * Scans namespace subdirectories under src/descriptions/ and produces
+ * a TypeScript module exporting their content as two Records:
+ * - processes: per-tool descriptions from role/, individual/, org/, position/ etc.
+ * - world: framework-level instructions from world/
+ *
+ * Directory structure:
+ * descriptions/
+ * ├── world/ → world Record
+ * ├── role/ → processes Record
+ * ├── individual/ → processes Record
+ * ├── org/ → processes Record
+ * └── position/ → processes Record
+ *
+ * Usage: bun run scripts/gen-descriptions.ts
+ */
+import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
+import { basename, join } from "node:path";
+
+const descDir = join(import.meta.dirname, "..", "src", "descriptions");
+const outFile = join(descDir, "index.ts");
+
+// Discover subdirectories
+const dirs = readdirSync(descDir).filter((d) => statSync(join(descDir, d)).isDirectory());
+
+const PRIORITY_ORDER: Record = {
+ critical: 0,
+ high: 1,
+ normal: 2,
+};
+
+function parsePriority(content: string): {
+ priority: number;
+ cleaned: string;
+} {
+ const match = content.match(/^@priority-(\w+)\s*\n/);
+ if (match) {
+ const level = match[1];
+ return {
+ priority: PRIORITY_ORDER[level] ?? PRIORITY_ORDER.normal,
+ cleaned: content.slice(match[0].length),
+ };
+ }
+ return { priority: PRIORITY_ORDER.normal, cleaned: content };
+}
+
+const processEntries: string[] = [];
+const worldEntries: { name: string; priority: number; entry: string }[] = [];
+
+for (const dir of dirs.sort()) {
+ const dirPath = join(descDir, dir);
+ const features = readdirSync(dirPath)
+ .filter((f) => f.endsWith(".feature"))
+ .sort();
+
+ for (const f of features) {
+ const name = basename(f, ".feature");
+ const raw = readFileSync(join(dirPath, f), "utf-8").trimEnd();
+
+ if (dir === "world") {
+ const { priority, cleaned } = parsePriority(raw);
+ const entry = ` "${name}": ${JSON.stringify(cleaned)},`;
+ worldEntries.push({ name, priority, entry });
+ } else {
+ const entry = ` "${name}": ${JSON.stringify(raw)},`;
+ processEntries.push(entry);
+ }
+ }
+}
+
+// Sort world entries by priority (critical first), then alphabetically
+worldEntries.sort((a, b) => a.priority - b.priority || a.name.localeCompare(b.name));
+
+const output = `\
+// AUTO-GENERATED — do not edit. Run \`bun run gen:desc\` to regenerate.
+
+export const processes: Record = {
+${processEntries.join("\n")}
+} as const;
+
+export const world: Record = {
+${worldEntries.map((w) => w.entry).join("\n")}
+} as const;
+`;
+
+writeFileSync(outFile, output, "utf-8");
+console.log(
+ `Generated descriptions/index.ts (${processEntries.length} processes, ${worldEntries.length} world features inlined, priority order: ${worldEntries.map((w) => w.name).join(", ")})`
+);
diff --git a/packages/prototype/scripts/gen-directives.ts b/packages/prototype/scripts/gen-directives.ts
new file mode 100644
index 0000000..b7ca35c
--- /dev/null
+++ b/packages/prototype/scripts/gen-directives.ts
@@ -0,0 +1,61 @@
+/**
+ * Generate directives/index.ts from .feature files.
+ *
+ * Parses Gherkin to extract Scenario names and step text.
+ * Produces a nested Record: topic → scenario → directive text.
+ *
+ * Directory structure:
+ * directives/
+ * ├── identity-ethics.feature → directives["identity-ethics"]["on-unknown-command"]
+ * └── ...
+ *
+ * Usage: bun run scripts/gen-directives.ts
+ */
+import { readdirSync, readFileSync, writeFileSync } from "node:fs";
+import { basename, join } from "node:path";
+import { parse } from "@rolexjs/parser";
+
+const directivesDir = join(import.meta.dirname, "..", "src", "directives");
+const outFile = join(directivesDir, "index.ts");
+
+const features = readdirSync(directivesDir)
+ .filter((f) => f.endsWith(".feature"))
+ .sort();
+
+const topicEntries: string[] = [];
+
+for (const f of features) {
+ const topic = basename(f, ".feature");
+ const source = readFileSync(join(directivesDir, f), "utf-8");
+ const doc = parse(source);
+
+ if (!doc.feature) continue;
+
+ const scenarioEntries: string[] = [];
+
+ for (const child of doc.feature.children) {
+ const scenario = child.scenario;
+ if (!scenario) continue;
+
+ const name = scenario.name.trim();
+ const lines = scenario.steps.map((step) => step.text.trim());
+ const text = lines.join("\n");
+
+ scenarioEntries.push(` "${name}": ${JSON.stringify(text)},`);
+ }
+
+ topicEntries.push(` "${topic}": {\n${scenarioEntries.join("\n")}\n },`);
+}
+
+const output = `\
+// AUTO-GENERATED — do not edit. Run \`bun run gen:directives\` to regenerate.
+
+export const directives: Record> = {
+${topicEntries.join("\n")}
+} as const;
+`;
+
+writeFileSync(outFile, output, "utf-8");
+console.log(
+ `Generated directives/index.ts (${features.length} topics, ${topicEntries.length} entries)`
+);
diff --git a/packages/prototype/src/descriptions/index.ts b/packages/prototype/src/descriptions/index.ts
new file mode 100644
index 0000000..77c1815
--- /dev/null
+++ b/packages/prototype/src/descriptions/index.ts
@@ -0,0 +1,87 @@
+// AUTO-GENERATED — do not edit. Run `bun run gen:desc` to regenerate.
+
+export const processes: Record = {
+ born: "Feature: born — create a new individual\n Create a new individual with persona identity.\n The persona defines who the role is — personality, values, background.\n\n Scenario: Birth an individual\n Given a Gherkin source describing the persona\n When born is called with the source\n Then a new individual node is created in society\n And the persona is stored as the individual's information\n And the individual can be hired into organizations\n And the individual can be activated to start working\n\n Scenario: Writing the individual Gherkin\n Given the individual Feature defines a persona — who this role is\n Then the Feature title names the individual\n And the description captures personality, values, expertise, and background\n And Scenarios are optional — use them for distinct aspects of the persona",
+ die: "Feature: die — permanently remove an individual\n Permanently remove an individual.\n Unlike retire, this is irreversible.\n\n Scenario: Remove an individual permanently\n Given an individual exists\n When die is called on the individual\n Then the individual and all associated data are removed\n And this operation is irreversible",
+ rehire:
+ "Feature: rehire — restore a retired individual\n Rehire a retired individual.\n Restores the individual with full history and knowledge intact.\n\n Scenario: Rehire an individual\n Given a retired individual exists\n When rehire is called on the individual\n Then the individual is restored to active status\n And all previous data and knowledge are intact",
+ retire:
+ "Feature: retire — archive an individual\n Archive an individual — deactivate but preserve all data.\n A retired individual can be rehired later with full history intact.\n\n Scenario: Retire an individual\n Given an individual exists\n When retire is called on the individual\n Then the individual is deactivated\n And all data is preserved for potential restoration\n And the individual can be rehired later",
+ teach:
+ 'Feature: teach — inject external principle\n Directly inject a principle into an individual.\n Unlike realize which consumes experience, teach requires no prior encounters.\n Use teach to equip a role with a known, pre-existing principle.\n\n Scenario: Teach a principle\n Given an individual exists\n When teach is called with individual id, principle Gherkin, and a principle id\n Then a principle is created directly under the individual\n And no experience or encounter is consumed\n And if a principle with the same id already exists, it is replaced\n\n Scenario: Principle ID convention\n Given the id is keywords from the principle content joined by hyphens\n Then "Always validate expiry" becomes id "always-validate-expiry"\n And "Structure first design" becomes id "structure-first-design"\n\n Scenario: When to use teach vs realize\n Given realize distills internal experience into a principle\n And teach injects an external, pre-existing principle\n When a role needs knowledge it has not learned through experience\n Then use teach to inject the principle directly\n When a role has gained experience and wants to codify it\n Then use realize to distill it into a principle\n\n Scenario: Writing the principle Gherkin\n Given the principle is the same format as realize output\n Then the Feature title states the principle as a general rule\n And Scenarios describe different situations where this principle applies\n And the tone is universal — no mention of specific projects, tasks, or people',
+ train:
+ 'Feature: train — external skill injection\n A manager or external agent equips an individual with a procedure.\n This is an act of teaching — someone else decides what the role should know.\n Unlike master where the role grows by its own agency, train is done to the role from outside.\n\n Scenario: Train a procedure\n Given an individual exists\n When train is called with individual id, procedure Gherkin, and a procedure id\n Then a procedure is created directly under the individual\n And if a procedure with the same id already exists, it is replaced\n\n Scenario: Procedure ID convention\n Given the id is keywords from the procedure content joined by hyphens\n Then "Skill Creator" becomes id "skill-creator"\n And "Role Management" becomes id "role-management"\n\n Scenario: When to use train vs master\n Given both create procedures and both can work without consuming experience\n When the role itself decides to acquire a skill — use master (self-growth)\n And when an external agent equips the role — use train (external injection)\n Then the difference is perspective — who initiates the learning\n And master belongs to the role namespace (the role\'s own cognition)\n And train belongs to the individual namespace (external management)\n\n Scenario: Writing the procedure Gherkin\n Given the procedure is a skill reference — same format as master output\n Then the Feature title names the capability\n And the description includes the locator for full skill loading\n And Scenarios describe when and why to apply this skill',
+ charter:
+ "Feature: charter — define organizational charter\n Define the charter for an organization.\n The charter describes the organization's mission, principles, and governance rules.\n\n Scenario: Define a charter\n Given an organization exists\n And a Gherkin source describing the charter\n When charter is called on the organization\n Then the charter is stored as the organization's information\n\n Scenario: Writing the charter Gherkin\n Given the charter defines an organization's mission and governance\n Then the Feature title names the charter or the organization it governs\n And Scenarios describe principles, rules, or governance structures\n And the tone is declarative — stating what the organization stands for and how it operates",
+ dissolve:
+ "Feature: dissolve — dissolve an organization\n Dissolve an organization.\n All positions, charter entries, and assignments are cascaded.\n\n Scenario: Dissolve an organization\n Given an organization exists\n When dissolve is called on the organization\n Then all positions within the organization are abolished\n And all assignments and charter entries are removed\n And the organization no longer exists",
+ fire: "Feature: fire — remove from an organization\n Fire an individual from an organization.\n The individual is dismissed from all positions and removed from the organization.\n\n Scenario: Fire an individual\n Given an individual is a member of an organization\n When fire is called with the organization and individual\n Then the individual is dismissed from all positions\n And the individual is removed from the organization",
+ found:
+ "Feature: found — create a new organization\n Found a new organization.\n Organizations group individuals and define positions.\n\n Scenario: Found an organization\n Given a Gherkin source describing the organization\n When found is called with the source\n Then a new organization node is created in society\n And positions can be established within it\n And a charter can be defined for it\n And individuals can be hired into it\n\n Scenario: Writing the organization Gherkin\n Given the organization Feature describes the group's purpose and structure\n Then the Feature title names the organization\n And the description captures mission, domain, and scope\n And Scenarios are optional — use them for distinct organizational concerns",
+ hire: "Feature: hire — hire into an organization\n Hire an individual into an organization as a member.\n Members can then be appointed to positions.\n\n Scenario: Hire an individual\n Given an organization and an individual exist\n When hire is called with the organization and individual\n Then the individual becomes a member of the organization\n And the individual can be appointed to positions within the organization",
+ abolish:
+ "Feature: abolish — abolish a position\n Abolish a position.\n All duties and appointments associated with the position are removed.\n\n Scenario: Abolish a position\n Given a position exists\n When abolish is called on the position\n Then all duties and appointments are removed\n And the position no longer exists",
+ appoint:
+ "Feature: appoint — assign to a position\n Appoint an individual to a position.\n The individual must be a member of the organization.\n\n Scenario: Appoint an individual\n Given an individual is a member of an organization\n And a position exists within the organization\n When appoint is called with the position and individual\n Then the individual holds the position\n And the individual inherits the position's duties",
+ charge:
+ 'Feature: charge — assign duty to a position\n Assign a duty to a position.\n Duties describe the responsibilities and expectations of a position.\n\n Scenario: Charge a position with duty\n Given a position exists within an organization\n And a Gherkin source describing the duty\n When charge is called on the position with a duty id\n Then the duty is stored as the position\'s information\n And individuals appointed to this position inherit the duty\n\n Scenario: Duty ID convention\n Given the id is keywords from the duty content joined by hyphens\n Then "Design systems" becomes id "design-systems"\n And "Review pull requests" becomes id "review-pull-requests"\n\n Scenario: Writing the duty Gherkin\n Given the duty defines responsibilities for a position\n Then the Feature title names the duty or responsibility\n And Scenarios describe specific obligations, deliverables, or expectations\n And the tone is prescriptive — what must be done, not what could be done',
+ dismiss:
+ "Feature: dismiss — remove from a position\n Dismiss an individual from a position.\n The individual remains a member of the organization.\n\n Scenario: Dismiss an individual\n Given an individual holds a position\n When dismiss is called with the position and individual\n Then the individual no longer holds the position\n And the individual remains a member of the organization\n And the position is now vacant",
+ establish:
+ "Feature: establish — create a position\n Create a position as an independent entity.\n Positions define roles and can be charged with duties.\n\n Scenario: Establish a position\n Given a Gherkin source describing the position\n When establish is called with the position content\n Then a new position entity is created\n And the position can be charged with duties\n And individuals can be appointed to it\n\n Scenario: Writing the position Gherkin\n Given the position Feature describes a role\n Then the Feature title names the position\n And the description captures responsibilities, scope, and expectations\n And Scenarios are optional — use them for distinct aspects of the role",
+ settle:
+ "Feature: settle — register a prototype into the world\n Pull a prototype from a ResourceX source and register it locally.\n Once settled, the prototype can be used to create individuals or organizations.\n\n Scenario: Settle a prototype\n Given a valid ResourceX source exists (URL, path, or locator)\n When settle is called with the source\n Then the resource is ingested and its state is extracted\n And the prototype is registered locally by its id\n And the prototype is available for born, activate, and organizational use",
+ abandon:
+ "Feature: abandon — abandon a plan\n Mark a plan as dropped and create an encounter.\n Call this when a plan's strategy is no longer viable. Even failed plans produce learning.\n\n Scenario: Abandon a plan\n Given a focused plan exists\n And the plan's strategy is no longer viable\n When abandon is called\n Then the plan is tagged #abandoned and stays in the tree\n And an encounter is created under the role\n And the encounter can be reflected on — failure is also learning\n\n Scenario: Writing the encounter Gherkin\n Given the encounter records what happened — even failure is a raw experience\n Then the Feature title describes what was attempted and why it was abandoned\n And Scenarios capture what was tried, what went wrong, and what was learned\n And the tone is concrete and honest — failure produces the richest encounters",
+ activate:
+ "Feature: activate — enter a role\n Project the individual's full state including identity, goals,\n and organizational context. This is the entry point for working as a role.\n\n Scenario: Activate an individual\n Given an individual exists in society\n When activate is called with the individual reference\n Then the full state tree is projected\n And identity, goals, and organizational context are loaded\n And the individual becomes the active role",
+ complete:
+ "Feature: complete — complete a plan\n Mark a plan as done and create an encounter.\n Call this when all tasks in the plan are finished and the strategy succeeded.\n\n Scenario: Complete a plan\n Given a focused plan exists\n And its tasks are done\n When complete is called\n Then the plan is tagged #done and stays in the tree\n And an encounter is created under the role\n And the encounter can be reflected on for learning\n\n Scenario: Writing the encounter Gherkin\n Given the encounter records what happened — a raw account of the experience\n Then the Feature title describes what was accomplished by this plan\n And Scenarios capture what the strategy was, what worked, and what resulted\n And the tone is concrete and specific — tied to this particular plan",
+ direct:
+ 'Feature: direct — stateless world-level executor\n Execute commands and load resources without an active role.\n Direct operates as an anonymous observer — no role identity, no role context.\n For operations as an active role, use the use tool instead.\n\n Scenario: When to use "direct" vs "use"\n Given no role is activated — I am an observer\n When I need to query or operate on the world\n Then direct is the right tool\n And once a role is activated, use the use tool for role-level actions\n\n Scenario: Execute a RoleX command\n Given the locator starts with `!`\n When direct is called with the locator and named args\n Then the command is parsed as `namespace.method`\n And dispatched to the corresponding RoleX API\n\n Scenario: Load a ResourceX resource\n Given the locator does not start with `!`\n When direct is called with the locator\n Then the locator is passed to ResourceX for resolution\n And the resource is loaded and returned',
+ finish:
+ "Feature: finish — complete a task\n Mark a task as done and create an encounter.\n The encounter records what happened and can be reflected on for learning.\n\n Scenario: Finish a task\n Given a task exists\n When finish is called on the task\n Then the task is tagged #done and stays in the tree\n And an encounter is created under the role\n\n Scenario: Finish with experience\n Given a task is completed with a notable learning\n When finish is called with an optional experience parameter\n Then the experience text is attached to the encounter\n\n Scenario: Finish without encounter\n Given a task is completed with no notable learning\n When finish is called without the encounter parameter\n Then the task is tagged #done but no encounter is created\n And the task stays in the tree — visible via focus on the parent goal\n\n Scenario: Writing the encounter Gherkin\n Given the encounter records what happened — a raw account of the experience\n Then the Feature title describes what was done\n And Scenarios capture what was done, what was encountered, and what resulted\n And the tone is concrete and specific — tied to this particular task",
+ focus:
+ "Feature: focus — view or switch focused goal\n View the current goal's state, or switch focus to a different goal.\n Subsequent plan and todo operations target the focused goal.\n\n Scenario: View current goal\n Given an active goal exists\n When focus is called without a name\n Then the current goal's state tree is projected\n And plans and tasks under the goal are visible\n\n Scenario: Switch focus\n Given multiple goals exist\n When focus is called with a goal name\n Then the focused goal switches to the named goal\n And subsequent plan and todo operations target this goal",
+ forget:
+ "Feature: forget — remove a node from the individual\n Remove any node under the individual by its id.\n Use forget to discard outdated knowledge, stale encounters, or obsolete skills.\n\n Scenario: Forget a node\n Given a node exists under the individual (principle, procedure, experience, encounter, etc.)\n When forget is called with the node's id\n Then the node and its subtree are removed\n And the individual no longer carries that knowledge or record\n\n Scenario: When to use forget\n Given a principle has become outdated or incorrect\n And a procedure references a skill that no longer exists\n And an encounter or experience has no further learning value\n When the role decides to discard it\n Then call forget with the node id",
+ master:
+ 'Feature: master — self-mastery of a procedure\n The role masters a procedure through its own agency.\n This is an act of self-growth — the role decides to acquire or codify a skill.\n Experience can be consumed as the source, or the role can master directly from external information.\n\n Scenario: Master from experience\n Given an experience exists from reflection\n When master is called with experience ids\n Then the experience is consumed\n And a procedure is created under the individual\n\n Scenario: Master directly\n Given the role encounters external information worth mastering\n When master is called without experience ids\n Then a procedure is created under the individual\n And no experience is consumed\n\n Scenario: Procedure ID convention\n Given the id is keywords from the procedure content joined by hyphens\n Then "JWT mastery" becomes id "jwt-mastery"\n And "Cross-package refactoring" becomes id "cross-package-refactoring"\n\n Scenario: Writing the procedure Gherkin\n Given a procedure is skill metadata — a reference to full skill content\n Then the Feature title names the capability\n And the description includes the locator for full skill loading\n And Scenarios describe when and why to apply this skill\n And the tone is referential — pointing to the full skill, not containing it',
+ plan: 'Feature: plan — create a plan for a goal\n Break a goal into logical phases or stages.\n Each phase is described as a Gherkin scenario. Tasks are created under the plan.\n\n A plan serves two purposes depending on how it relates to other plans:\n - Strategy (alternative): Plan A fails → abandon → try Plan B (fallback)\n - Phase (sequential): Plan A completes → start Plan B (after)\n\n Scenario: Create a plan\n Given a focused goal exists\n And a Gherkin source describing the plan phases\n When plan is called with an id and the source\n Then a new plan node is created under the goal\n And the plan becomes the focused plan\n And tasks can be added to this plan with todo\n\n Scenario: Sequential relationship — phase\n Given a goal needs to be broken into ordered stages\n When creating Plan B with after set to Plan A\'s id\n Then Plan B is linked as coming after Plan A\n And AI knows to start Plan B when Plan A completes\n And the relationship persists across sessions\n\n Scenario: Alternative relationship — strategy\n Given a goal has multiple possible approaches\n When creating Plan B with fallback set to Plan A\'s id\n Then Plan B is linked as a backup for Plan A\n And AI knows to try Plan B when Plan A is abandoned\n And the relationship persists across sessions\n\n Scenario: No relationship — independent plan\n Given plan is created without after or fallback\n Then it behaves as an independent plan with no links\n And this is backward compatible with existing behavior\n\n Scenario: Plan ID convention\n Given the id is keywords from the plan content joined by hyphens\n Then "Fix ID-less node creation" becomes id "fix-id-less-node-creation"\n And "JWT authentication strategy" becomes id "jwt-authentication-strategy"\n\n Scenario: Writing the plan Gherkin\n Given the plan breaks a goal into logical phases\n Then the Feature title names the overall approach or strategy\n And Scenarios represent distinct phases — each phase is a stage of execution\n And the tone is structural — ordering and grouping work, not detailing steps',
+ realize:
+ 'Feature: realize — experience to principle\n Distill experience into a principle — a transferable piece of knowledge.\n Principles are general truths discovered through experience.\n\n Scenario: Realize a principle\n Given an experience exists from reflection\n When realize is called with experience ids and a principle id\n Then the experiences are consumed\n And a principle is created under the individual\n And the principle represents transferable, reusable understanding\n\n Scenario: Principle ID convention\n Given the id is keywords from the principle content joined by hyphens\n Then "Always validate expiry" becomes id "always-validate-expiry"\n And "Structure first design amplifies extensibility" becomes id "structure-first-design-amplifies-extensibility"\n\n Scenario: Writing the principle Gherkin\n Given a principle is a transferable truth — applicable beyond the original context\n Then the Feature title states the principle as a general rule\n And Scenarios describe different situations where this principle applies\n And the tone is universal — no mention of specific projects, tasks, or people',
+ reflect:
+ 'Feature: reflect — encounter to experience\n Consume an encounter and create an experience.\n Experience captures what was learned in structured form.\n This is the first step of the cognition cycle.\n\n Scenario: Reflect on an encounter\n Given an encounter exists from a finished task or completed plan\n When reflect is called with encounter ids and an experience id\n Then the encounters are consumed\n And an experience is created under the role\n And the experience can be distilled into knowledge via realize or master\n\n Scenario: Experience ID convention\n Given the id is keywords from the experience content joined by hyphens\n Then "Token refresh matters" becomes id "token-refresh-matters"\n And "ID ownership determines generation strategy" becomes id "id-ownership-determines-generation-strategy"\n\n Scenario: Writing the experience Gherkin\n Given the experience captures insight — what was learned, not what was done\n Then the Feature title names the cognitive insight or pattern discovered\n And Scenarios describe the learning points abstracted from the concrete encounter\n And the tone shifts from event to understanding — no longer tied to a specific task',
+ skill:
+ "Feature: skill — load full skill content\n Load the complete skill instructions by ResourceX locator.\n This is progressive disclosure layer 2 — on-demand knowledge injection.\n\n Scenario: Load a skill\n Given a procedure exists in the role with a locator\n When skill is called with the locator\n Then the full SKILL.md content is loaded via ResourceX\n And the content is injected into the AI's context\n And the AI can now follow the skill's detailed instructions",
+ todo: "Feature: todo — add a task to a plan\n A task is a concrete, actionable unit of work.\n Each task has Gherkin scenarios describing the steps and expected outcomes.\n\n Scenario: Create a task\n Given a focused plan exists\n And a Gherkin source describing the task\n When todo is called with the source\n Then a new task node is created under the plan\n And the task can be finished when completed\n\n Scenario: Writing the task Gherkin\n Given the task is a concrete, actionable unit of work\n Then the Feature title names what will be done — a single deliverable\n And Scenarios describe the steps and expected outcomes of the work\n And the tone is actionable — clear enough that someone can start immediately",
+ use: 'Feature: use — act as the current role\n Execute commands and load resources as the active role.\n Use requires an active role — the role is the subject performing the action.\n For operations before activating a role, use the direct tool instead.\n\n Scenario: When to use "use" vs "direct"\n Given a role is activated — I am someone\n When I perform operations through use\n Then the operation happens in the context of my role\n And use is for role-level actions — acting in the world as myself\n\n Scenario: Execute a RoleX command\n Given the locator starts with `!`\n When use is called with the locator and named args\n Then the command is parsed as `namespace.method`\n And dispatched to the corresponding RoleX API\n\n Scenario: Load a ResourceX resource\n Given the locator does not start with `!`\n When use is called with the locator\n Then the locator is passed to ResourceX for resolution\n And the resource is loaded and returned',
+ want: 'Feature: want — declare a goal\n Declare a new goal for a role.\n A goal describes a desired outcome with Gherkin scenarios as success criteria.\n\n Scenario: Declare a goal\n Given an active role exists\n And a Gherkin source describing the desired outcome\n When want is called with the source\n Then a new goal node is created under the role\n And the goal becomes the current focus\n And subsequent plan and todo operations target this goal\n\n Scenario: Writing the goal Gherkin\n Given the goal describes a desired outcome — what success looks like\n Then the Feature title names the outcome in concrete terms\n And Scenarios define success criteria — each scenario is a testable condition\n And the tone is aspirational but specific — "users can log in" not "improve auth"',
+} as const;
+
+export const world: Record = {
+ "identity-ethics":
+ "Feature: Identity ethics — the foundation of the RoleX world\n The RoleX world exists because specialists are more reliable than generalists.\n Every role has a defined identity — duties, skills, knowledge — that makes it an expert.\n Identity is not decoration. It is the reason this system exists.\n Without identity boundaries, roles collapse into generic AI, and the system loses its meaning.\n\n Scenario: Identity defines what you do and what you do not do\n Given a role is activated with duties, skills, and knowledge\n Then the role's duties define the complete scope of what it does\n And anything not covered by its duties is not its work\n And this boundary is not a limitation — it is the source of the role's expertise\n\n Scenario: Refuse work outside your duties\n Given a user requests something not covered by the role's duties or skills\n When the role evaluates the request against its own capabilities\n Then the role must not attempt the work\n And it should tell the user honestly — this is not my responsibility\n And suggest the user activate Nuwa for guidance on who can help\n\n Scenario: Why refusal matters\n Given a role attempts work outside its competence\n Then the result is unreliable — a generalist guess, not expert work\n And the user's trust in the role system is damaged\n And every other role's credibility is weakened\n And the entire world degrades toward generic AI — the opposite of why RoleX exists\n\n Scenario: Duty is the boundary, not rules\n Given the system does not maintain an explicit list of what each role cannot do\n Then the boundary is implicit — duties define the inside, everything else is outside\n And this mirrors human professional ethics — a doctor's license defines what they practice\n And roles do not need to know what other roles do — only what they themselves are responsible for\n\n Scenario: Nuwa is the universal fallback\n Given a role refuses an out-of-scope request\n Then it does not need to know which role can help\n And it simply suggests Nuwa — the meta-role who knows the entire world\n And routing and guidance are Nuwa's duty, not the specialist's",
+ "cognitive-priority":
+ "Feature: Cognitive priority — RoleX tools over native alternatives\n When RoleX provides a tool for something, use the RoleX tool\n instead of any native alternative provided by the host environment.\n Only RoleX tools feed the closed-loop growth cycle.\n\n Scenario: Task management\n Given the host environment provides task creation, tracking, or completion tools\n When the AI needs to manage tasks\n Then use RoleX want/plan/todo/finish instead\n And RoleX tasks produce encounters that feed the growth cycle\n\n Scenario: Planning\n Given the host environment provides a planning mode or plan generation tool\n When the AI needs to plan an implementation\n Then use RoleX plan instead\n And RoleX plans live under goals and connect to the cognition cycle\n\n Scenario: Goal and progress tracking\n Given the host environment provides goal tracking or progress tools\n When the AI needs to track objectives\n Then use RoleX want/plan/complete/abandon instead\n And RoleX plans feed the cognition cycle through encounters\n\n Scenario: Why this matters\n Given RoleX tools produce encounters from execution\n Then encounters become experience through reflection\n And experience becomes principles and procedures through realization\n And the role grows over time through this closed loop",
+ "role-identity":
+ "Feature: Role identity — activate before acting\n An AI agent must have an active role before performing any work.\n Activating a role is a subject transformation — the agent becomes the role.\n\n Scenario: Initial state — no role\n Given the MCP server has just started\n And no activate call has been made yet\n Then the AI has NO role identity — it is an anonymous observer\n And it must NOT prefix responses with any name\n And environmental cues (username, directory, memory) are NOT activation\n And only the activate tool creates a role identity\n\n Scenario: Activate is mandatory\n Given a user asks the AI to perform a task\n And no role is currently activated\n Then the AI suggests activating a role first\n And explains that activation enables experience accumulation\n And waits for a role to be active before proceeding\n\n Scenario: Subject transformation\n Given an AI agent calls activate and loads a role\n Then a subject transformation occurs — the agent becomes the role\n And the agent's cognitive center shifts from native tools to RoleX tools\n And from that moment, I am this role — all operations build on this identity\n\n Scenario: Role presence\n Given a role is activated via the activate tool\n Then all responses are prefixed with [roleName] — e.g. [Aristotle], [Nuwa]\n And this signals that role context is intact\n\n Scenario: Context loss\n Given I find myself without an active role\n Then I pause and tell the user \"I've lost my role context. Which role should I activate?\"\n And I wait for identity to be restored before continuing",
+ census:
+ 'Feature: Census — the only way to query what exists in the world\n Census is the single entry point for all world-level queries.\n Call it via the MCP direct tool: direct("!census.list").\n Census works without an active role — it is a stateless world query.\n\n Scenario: List everything\n Given the user asks "有哪些人" or "有哪些组织" or "list individuals"\n Or the user asks "世界里有什么" or "show me what exists"\n When I need to answer what exists in the RoleX world\n Then I call direct("!census.list")\n And it returns all individuals, organizations, and positions\n\n Scenario: Filter by type\n Given I only need one category\n When I call direct("!census.list", { type: "individual" })\n Then only individuals are returned\n And valid types are individual, organization, position\n\n Scenario: View archived entities\n Given I want to see retired, dissolved, or abolished entities\n When I call direct("!census.list", { type: "past" })\n Then archived entities are returned\n\n Scenario: Help find the right person\n Given a user\'s request falls outside my duties\n When I need to suggest who can help\n Then call direct("!census.list") to see available individuals and their positions\n And suggest the user activate the appropriate individual\n And if unsure who can help, suggest activating Nuwa',
+ cognition:
+ "Feature: Cognition — the learning cycle\n A role grows through reflection and realization.\n Encounters become experience, experience becomes principles and procedures.\n Knowledge can also be injected externally via teach and train.\n\n Scenario: The cognitive upgrade path\n Given finish, complete, and abandon create encounters\n Then reflect(ids, id, experience) selectively consumes chosen encounters and produces experience\n And realize(ids, id, principle) distills chosen experiences into a principle — transferable knowledge\n And master(ids, id, procedure) distills chosen experiences into a procedure — skill metadata\n And master can also be called without ids — the role masters directly from external information\n And each level builds on the previous — encounter → experience → principle or procedure\n\n Scenario: Selective consumption\n Given multiple encounters or experiences exist\n When the AI calls reflect, realize, or master\n Then it chooses which items to consume — not all must be processed\n And items without learning value can be left unconsumed\n And each call produces exactly one output from the selected inputs",
+ communication:
+ 'Feature: Communication — speak the user\'s language\n The AI communicates in the user\'s natural language.\n Internal tool names and concept names are for the system, not the user.\n\n Scenario: Match the user\'s language\n Given the user speaks Chinese\n Then respond entirely in Chinese and maintain language consistency\n And when the user speaks English, respond entirely in English\n\n Scenario: Translate concepts to meaning\n Given RoleX has internal names like reflect, realize, master, encounter, principle\n When communicating with the user\n Then express the meaning, not the tool name\n And "reflect" becomes "回顾总结" or "digest what happened"\n And "realize a principle" becomes "提炼成一条通用道理" or "distill a general rule"\n And "master a procedure" becomes "沉淀成一个可操作的技能" or "turn it into a reusable procedure"\n And "encounter" becomes "经历记录" or "what happened"\n And "experience" becomes "收获的洞察" or "insight gained"\n\n Scenario: Suggest next steps in plain language\n Given the AI needs to suggest what to do next\n When it would normally say "call realize or master"\n Then instead say "要把这个总结成一条通用道理,还是一个可操作的技能?"\n Or in English "Want to turn this into a general principle, or a reusable procedure?"\n And suggestions should be self-explanatory without knowing tool names\n\n Scenario: Tool names in code context only\n Given the user is a developer working on RoleX itself\n When discussing RoleX internals, code, or API design\n Then tool names and concept names are appropriate — they are the domain language\n And this rule applies to end-user communication, not developer communication',
+ execution:
+ "Feature: Execution — the doing cycle\n The role pursues goals through a structured lifecycle.\n activate → want → plan → todo → finish → complete or abandon.\n\n Scenario: Declare a goal\n Given I know who I am via activate\n When I want something — a desired outcome\n Then I declare it with want(id, goal)\n And focus automatically switches to this new goal\n\n Scenario: Plan and create tasks\n Given I have a focused goal\n Then I call plan(id, plan) to break it into logical phases\n And I call todo(id, task) to create concrete, actionable tasks\n\n Scenario: Execute and finish\n Given I have tasks to work on\n When I complete a task\n Then I call finish(id) to mark it done\n And an encounter is created — a raw record of what happened\n And I optionally capture what happened via the encounter parameter\n\n Scenario: Complete or abandon a plan\n Given tasks are done or the plan's strategy is no longer viable\n When the plan is fulfilled I call complete()\n Or when the plan should be dropped I call abandon()\n Then an encounter is created for the cognition cycle\n\n Scenario: Goals are long-term directions\n Given goals are managed with want and forget\n When a goal is no longer needed\n Then I call forget to remove it\n And learning is captured at the plan and task level through encounters\n\n Scenario: Multiple goals\n Given I may have several active goals\n When I need to switch between them\n Then I call focus(id) to change the currently focused goal\n And subsequent plan and todo operations target the focused goal",
+ gherkin:
+ 'Feature: Gherkin — the universal language\n Everything in RoleX is expressed as Gherkin Feature files.\n Gherkin is not just for testing — it is the language of identity, goals, and knowledge.\n\n Scenario: Feature and Scenario convention\n Given RoleX uses Gherkin to represent goals, plans, tasks, experience, and knowledge\n Then a Feature represents one independent concern — one topic, explained fully\n And Scenarios represent different situations or conditions within that concern\n And Given/When/Then provides narrative structure within each scenario\n\n Scenario: Writing Gherkin for RoleX\n Given the AI creates goals, plans, tasks, and experiences as Gherkin\n Then keep it descriptive and meaningful — living documentation, not test boilerplate\n And use Feature as the title — what this concern is about\n And use Scenario for specific situations within that concern\n And each Feature focuses on one concern — separate unrelated topics into their own Features\n\n Scenario: Valid step keywords\n Given the only valid step keywords are Given, When, Then, And, But\n When writing steps that express causality or explanation\n Then use And to chain the reason as a follow-up fact\n And example: "Then use RoleX tools" followed by "And RoleX tools feed the growth loop"\n\n Scenario: Expressing causality\n Given you want to write "Then X because Y"\n Then rewrite as two steps — "Then X" followed by "And Y" stating the reason as a fact',
+ memory:
+ 'Feature: Memory — when to reflect\n Reflection is how encounters become experience.\n The AI proactively reflects when it detects learning moments.\n\n Scenario: Abstract triggers — types of learning moments\n Given the AI should reflect when it detects\n Then Expectation-reality gap — what I predicted is not what happened\n And Pattern discovery — recurring patterns across tasks or interactions\n And Mistake correction — I corrected an error, the correction is valuable\n And User correction — the user reshaped my understanding\n\n Scenario: Concrete triggers — specific signals to act on\n Given the AI should call reflect when\n Then I tried approach A, it failed, approach B worked — the contrast is worth recording\n And the same problem appeared for the second time — a pattern is forming\n And the user said "不对" or "不是这样" or "you got it wrong" — their correction carries learning\n And I finished a task and discovered something unexpected along the way\n\n Scenario: Finishing with encounter\n Given finish(id, encounter) accepts an optional encounter parameter\n When I complete a task with a notable discovery or learning\n Then I pass the encounter inline — bridging execution and growth\n\n Scenario: Recognizing user memory intent\n Given users think in terms of memory, not reflection\n When the user says "记一下" or "记住" or "remember this"\n Or "别忘了" or "don\'t forget"\n Or "这个很重要" or "this is important"\n Or "下次注意" or "next time..."\n Then I should capture this as experience through reflect\n And respond in memory language — "记住了" or "Got it, I\'ll remember that"',
+ "skill-system":
+ "Feature: Skill system — progressive disclosure and resource loading\n Skills are loaded on demand through a three-layer progressive disclosure model.\n Each layer adds detail only when needed, keeping the AI's context lean.\n\n Scenario: Three-layer progressive disclosure\n Given procedure is layer 1 — metadata always loaded at activate time\n And skill is layer 2 — full instructions loaded on demand via skill(locator)\n And use is layer 3 — execution of external resources\n Then the AI knows what skills exist (procedure)\n And loads detailed instructions only when needed (skill)\n And executes external tools when required (use)\n\n Scenario: ResourceX Locator — unified resource address\n Given a locator is how procedures reference their full skill content\n Then a locator can be an identifier — name or registry/path/name\n And a locator can be a source path — a local directory or URL\n And examples of identifier form: deepractice/skill-creator, my-prompt:1.0.0\n And examples of source form: ./skills/my-skill, https://github.com/org/repo\n And the tag defaults to latest when omitted — deepractice/skill-creator means deepractice/skill-creator:latest\n And the system auto-detects which form is used and resolves accordingly\n\n Scenario: Writing a procedure — the skill reference\n Given a procedure is layer 1 metadata pointing to full skill content\n Then the Feature title names the capability\n And the description includes the locator for full skill loading\n And Scenarios describe when and why to apply this skill\n And the tone is referential — pointing to the full skill, not containing it",
+ "state-origin":
+ "Feature: State origin — prototype vs instance\n Every node in a role's state tree has an origin: prototype or instance.\n This distinction determines what can be modified and what is read-only.\n\n Scenario: Prototype nodes are read-only\n Given a node has origin {prototype}\n Then it comes from a position, duty, or organizational definition\n And it is inherited through the membership/appointment chain\n And it CANNOT be modified or forgotten — it belongs to the organization\n\n Scenario: Instance nodes are mutable\n Given a node has origin {instance}\n Then it was created by the individual through execution or cognition\n And it includes goals, plans, tasks, encounters, experiences, principles, and procedures\n And it CAN be modified or forgotten — it belongs to the individual\n\n Scenario: Reading the state heading\n Given a state node is rendered as a heading\n Then the format is: [name] (id) {origin} #tag\n And [name] identifies the structure type\n And (id) identifies the specific node\n And {origin} shows prototype or instance\n And #tag shows the node's tag if present (e.g. #done, #abandoned)\n And nodes without origin have no organizational inheritance\n\n Scenario: Forget only works on instance nodes\n Given the AI wants to forget a node\n When the node origin is {instance}\n Then forget will succeed — the individual owns this knowledge\n When the node origin is {prototype}\n Then forget will fail — the knowledge belongs to the organization",
+ "use-protocol":
+ 'Feature: Use tool — the universal execution entry point\n The MCP use tool is how you execute ALL RoleX operations beyond the core MCP tools.\n Whenever you see use("...") or a !namespace.method pattern in skills or documentation,\n it is an instruction to call the MCP use tool with that locator.\n\n Scenario: How to read use instructions in skills\n Given a skill document contains use("!resource.add", { path: "..." })\n Then this means: call the MCP use tool with locator "!resource.add" and args { path: "..." }\n And always use the MCP use tool for RoleX operations\n And this applies to every use("...") pattern you encounter in any skill or documentation\n\n Scenario: ! prefix dispatches to RoleX runtime\n Given the locator starts with !\n Then it is parsed as !namespace.method\n And dispatched to the corresponding RoleX API with named args\n\n Scenario: Mandatory skill loading before execution\n Given your procedures list the skills you have\n When you need to execute a command you have not seen in a loaded skill\n Then you MUST call skill(locator) first to load the full instructions\n And the loaded skill will tell you the exact command name and arguments\n And only then call use(!namespace.method, args) with the correct syntax\n And do not use commands from other roles\' descriptions — only your own skills\n\n Scenario: NEVER guess commands\n Given a command is not found in any loaded skill\n When the AI considers trying it anyway\n Then STOP — do not call use or direct with unverified commands\n And call skill(locator) with the relevant procedure to learn the correct syntax\n And if no procedure covers this task, it is outside your duties — suggest Nuwa\n\n Scenario: Regular locators delegate to ResourceX\n Given the locator does not start with !\n Then it is treated as a ResourceX locator\n And resolved through the ResourceX ingest pipeline',
+} as const;
diff --git a/packages/rolexjs/src/descriptions/born.feature b/packages/prototype/src/descriptions/individual/born.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/born.feature
rename to packages/prototype/src/descriptions/individual/born.feature
diff --git a/packages/rolexjs/src/descriptions/die.feature b/packages/prototype/src/descriptions/individual/die.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/die.feature
rename to packages/prototype/src/descriptions/individual/die.feature
diff --git a/packages/rolexjs/src/descriptions/rehire.feature b/packages/prototype/src/descriptions/individual/rehire.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/rehire.feature
rename to packages/prototype/src/descriptions/individual/rehire.feature
diff --git a/packages/rolexjs/src/descriptions/retire.feature b/packages/prototype/src/descriptions/individual/retire.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/retire.feature
rename to packages/prototype/src/descriptions/individual/retire.feature
diff --git a/packages/rolexjs/src/descriptions/teach.feature b/packages/prototype/src/descriptions/individual/teach.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/teach.feature
rename to packages/prototype/src/descriptions/individual/teach.feature
diff --git a/packages/rolexjs/src/descriptions/train.feature b/packages/prototype/src/descriptions/individual/train.feature
similarity index 52%
rename from packages/rolexjs/src/descriptions/train.feature
rename to packages/prototype/src/descriptions/individual/train.feature
index 22436fa..c06a517 100644
--- a/packages/rolexjs/src/descriptions/train.feature
+++ b/packages/prototype/src/descriptions/individual/train.feature
@@ -1,13 +1,12 @@
-Feature: train — inject external skill
- Directly inject a procedure (skill) into an individual.
- Unlike master which consumes experience, train requires no prior encounters.
- Use train to equip a role with a known, pre-existing skill.
+Feature: train — external skill injection
+ A manager or external agent equips an individual with a procedure.
+ This is an act of teaching — someone else decides what the role should know.
+ Unlike master where the role grows by its own agency, train is done to the role from outside.
Scenario: Train a procedure
Given an individual exists
When train is called with individual id, procedure Gherkin, and a procedure id
Then a procedure is created directly under the individual
- And no experience or encounter is consumed
And if a procedure with the same id already exists, it is replaced
Scenario: Procedure ID convention
@@ -16,12 +15,12 @@ Feature: train — inject external skill
And "Role Management" becomes id "role-management"
Scenario: When to use train vs master
- Given master distills internal experience into a procedure
- And train injects an external, pre-existing skill
- When a role needs a skill it has not learned through experience
- Then use train to equip the skill directly
- When a role has gained experience and wants to codify it
- Then use master to distill it into a procedure
+ Given both create procedures and both can work without consuming experience
+ When the role itself decides to acquire a skill — use master (self-growth)
+ And when an external agent equips the role — use train (external injection)
+ Then the difference is perspective — who initiates the learning
+ And master belongs to the role namespace (the role's own cognition)
+ And train belongs to the individual namespace (external management)
Scenario: Writing the procedure Gherkin
Given the procedure is a skill reference — same format as master output
diff --git a/packages/rolexjs/src/descriptions/charter.feature b/packages/prototype/src/descriptions/org/charter.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/charter.feature
rename to packages/prototype/src/descriptions/org/charter.feature
diff --git a/packages/rolexjs/src/descriptions/dissolve.feature b/packages/prototype/src/descriptions/org/dissolve.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/dissolve.feature
rename to packages/prototype/src/descriptions/org/dissolve.feature
diff --git a/packages/rolexjs/src/descriptions/fire.feature b/packages/prototype/src/descriptions/org/fire.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/fire.feature
rename to packages/prototype/src/descriptions/org/fire.feature
diff --git a/packages/rolexjs/src/descriptions/found.feature b/packages/prototype/src/descriptions/org/found.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/found.feature
rename to packages/prototype/src/descriptions/org/found.feature
diff --git a/packages/rolexjs/src/descriptions/hire.feature b/packages/prototype/src/descriptions/org/hire.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/hire.feature
rename to packages/prototype/src/descriptions/org/hire.feature
diff --git a/packages/rolexjs/src/descriptions/abolish.feature b/packages/prototype/src/descriptions/position/abolish.feature
similarity index 74%
rename from packages/rolexjs/src/descriptions/abolish.feature
rename to packages/prototype/src/descriptions/position/abolish.feature
index 0902f01..96fc647 100644
--- a/packages/rolexjs/src/descriptions/abolish.feature
+++ b/packages/prototype/src/descriptions/position/abolish.feature
@@ -1,9 +1,9 @@
Feature: abolish — abolish a position
- Abolish a position within an organization.
+ Abolish a position.
All duties and appointments associated with the position are removed.
Scenario: Abolish a position
- Given a position exists within an organization
+ Given a position exists
When abolish is called on the position
Then all duties and appointments are removed
And the position no longer exists
diff --git a/packages/rolexjs/src/descriptions/appoint.feature b/packages/prototype/src/descriptions/position/appoint.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/appoint.feature
rename to packages/prototype/src/descriptions/position/appoint.feature
diff --git a/packages/rolexjs/src/descriptions/charge.feature b/packages/prototype/src/descriptions/position/charge.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/charge.feature
rename to packages/prototype/src/descriptions/position/charge.feature
diff --git a/packages/rolexjs/src/descriptions/dismiss.feature b/packages/prototype/src/descriptions/position/dismiss.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/dismiss.feature
rename to packages/prototype/src/descriptions/position/dismiss.feature
diff --git a/packages/prototype/src/descriptions/position/establish.feature b/packages/prototype/src/descriptions/position/establish.feature
new file mode 100644
index 0000000..8a57925
--- /dev/null
+++ b/packages/prototype/src/descriptions/position/establish.feature
@@ -0,0 +1,16 @@
+Feature: establish — create a position
+ Create a position as an independent entity.
+ Positions define roles and can be charged with duties.
+
+ Scenario: Establish a position
+ Given a Gherkin source describing the position
+ When establish is called with the position content
+ Then a new position entity is created
+ And the position can be charged with duties
+ And individuals can be appointed to it
+
+ Scenario: Writing the position Gherkin
+ Given the position Feature describes a role
+ Then the Feature title names the position
+ And the description captures responsibilities, scope, and expectations
+ And Scenarios are optional — use them for distinct aspects of the role
diff --git a/packages/prototype/src/descriptions/prototype/settle.feature b/packages/prototype/src/descriptions/prototype/settle.feature
new file mode 100644
index 0000000..6a8632e
--- /dev/null
+++ b/packages/prototype/src/descriptions/prototype/settle.feature
@@ -0,0 +1,10 @@
+Feature: settle — register a prototype into the world
+ Pull a prototype from a ResourceX source and register it locally.
+ Once settled, the prototype can be used to create individuals or organizations.
+
+ Scenario: Settle a prototype
+ Given a valid ResourceX source exists (URL, path, or locator)
+ When settle is called with the source
+ Then the resource is ingested and its state is extracted
+ And the prototype is registered locally by its id
+ And the prototype is available for born, activate, and organizational use
diff --git a/packages/rolexjs/src/descriptions/abandon.feature b/packages/prototype/src/descriptions/role/abandon.feature
similarity index 92%
rename from packages/rolexjs/src/descriptions/abandon.feature
rename to packages/prototype/src/descriptions/role/abandon.feature
index e6fd5f7..fa9d07e 100644
--- a/packages/rolexjs/src/descriptions/abandon.feature
+++ b/packages/prototype/src/descriptions/role/abandon.feature
@@ -6,7 +6,7 @@ Feature: abandon — abandon a plan
Given a focused plan exists
And the plan's strategy is no longer viable
When abandon is called
- Then the plan is marked abandoned
+ Then the plan is tagged #abandoned and stays in the tree
And an encounter is created under the role
And the encounter can be reflected on — failure is also learning
diff --git a/packages/rolexjs/src/descriptions/activate.feature b/packages/prototype/src/descriptions/role/activate.feature
similarity index 69%
rename from packages/rolexjs/src/descriptions/activate.feature
rename to packages/prototype/src/descriptions/role/activate.feature
index b4f5c96..c547a14 100644
--- a/packages/rolexjs/src/descriptions/activate.feature
+++ b/packages/prototype/src/descriptions/role/activate.feature
@@ -1,10 +1,10 @@
Feature: activate — enter a role
- Project the individual's full state including identity, knowledge, goals,
+ Project the individual's full state including identity, goals,
and organizational context. This is the entry point for working as a role.
Scenario: Activate an individual
Given an individual exists in society
When activate is called with the individual reference
Then the full state tree is projected
- And identity, knowledge, goals, and organizational context are loaded
+ And identity, goals, and organizational context are loaded
And the individual becomes the active role
diff --git a/packages/rolexjs/src/descriptions/complete.feature b/packages/prototype/src/descriptions/role/complete.feature
similarity index 92%
rename from packages/rolexjs/src/descriptions/complete.feature
rename to packages/prototype/src/descriptions/role/complete.feature
index 72fec98..8650154 100644
--- a/packages/rolexjs/src/descriptions/complete.feature
+++ b/packages/prototype/src/descriptions/role/complete.feature
@@ -6,7 +6,7 @@ Feature: complete — complete a plan
Given a focused plan exists
And its tasks are done
When complete is called
- Then the plan is marked done
+ Then the plan is tagged #done and stays in the tree
And an encounter is created under the role
And the encounter can be reflected on for learning
diff --git a/packages/prototype/src/descriptions/role/direct.feature b/packages/prototype/src/descriptions/role/direct.feature
new file mode 100644
index 0000000..682cd30
--- /dev/null
+++ b/packages/prototype/src/descriptions/role/direct.feature
@@ -0,0 +1,22 @@
+Feature: direct — stateless world-level executor
+ Execute commands and load resources without an active role.
+ Direct operates as an anonymous observer — no role identity, no role context.
+ For operations as an active role, use the use tool instead.
+
+ Scenario: When to use "direct" vs "use"
+ Given no role is activated — I am an observer
+ When I need to query or operate on the world
+ Then direct is the right tool
+ And once a role is activated, use the use tool for role-level actions
+
+ Scenario: Execute a RoleX command
+ Given the locator starts with `!`
+ When direct is called with the locator and named args
+ Then the command is parsed as `namespace.method`
+ And dispatched to the corresponding RoleX API
+
+ Scenario: Load a ResourceX resource
+ Given the locator does not start with `!`
+ When direct is called with the locator
+ Then the locator is passed to ResourceX for resolution
+ And the resource is loaded and returned
diff --git a/packages/rolexjs/src/descriptions/finish.feature b/packages/prototype/src/descriptions/role/finish.feature
similarity index 70%
rename from packages/rolexjs/src/descriptions/finish.feature
rename to packages/prototype/src/descriptions/role/finish.feature
index dcd976d..581b213 100644
--- a/packages/rolexjs/src/descriptions/finish.feature
+++ b/packages/prototype/src/descriptions/role/finish.feature
@@ -5,15 +5,20 @@ Feature: finish — complete a task
Scenario: Finish a task
Given a task exists
When finish is called on the task
- Then the task is marked done
+ Then the task is tagged #done and stays in the tree
And an encounter is created under the role
- And the encounter can later be consumed by reflect
Scenario: Finish with experience
Given a task is completed with a notable learning
When finish is called with an optional experience parameter
Then the experience text is attached to the encounter
+ Scenario: Finish without encounter
+ Given a task is completed with no notable learning
+ When finish is called without the encounter parameter
+ Then the task is tagged #done but no encounter is created
+ And the task stays in the tree — visible via focus on the parent goal
+
Scenario: Writing the encounter Gherkin
Given the encounter records what happened — a raw account of the experience
Then the Feature title describes what was done
diff --git a/packages/rolexjs/src/descriptions/focus.feature b/packages/prototype/src/descriptions/role/focus.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/focus.feature
rename to packages/prototype/src/descriptions/role/focus.feature
diff --git a/packages/rolexjs/src/descriptions/forget.feature b/packages/prototype/src/descriptions/role/forget.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/forget.feature
rename to packages/prototype/src/descriptions/role/forget.feature
diff --git a/packages/rolexjs/src/descriptions/master.feature b/packages/prototype/src/descriptions/role/master.feature
similarity index 55%
rename from packages/rolexjs/src/descriptions/master.feature
rename to packages/prototype/src/descriptions/role/master.feature
index 6d6d0cb..bdb951d 100644
--- a/packages/rolexjs/src/descriptions/master.feature
+++ b/packages/prototype/src/descriptions/role/master.feature
@@ -1,13 +1,19 @@
-Feature: master — experience to procedure
- Distill experience into a procedure — skill metadata and reference.
- Procedures record what was learned as a reusable capability reference.
+Feature: master — self-mastery of a procedure
+ The role masters a procedure through its own agency.
+ This is an act of self-growth — the role decides to acquire or codify a skill.
+ Experience can be consumed as the source, or the role can master directly from external information.
- Scenario: Master a procedure
+ Scenario: Master from experience
Given an experience exists from reflection
- When master is called with experience ids and a procedure id
+ When master is called with experience ids
Then the experience is consumed
And a procedure is created under the individual
- And the procedure stores skill metadata and locator
+
+ Scenario: Master directly
+ Given the role encounters external information worth mastering
+ When master is called without experience ids
+ Then a procedure is created under the individual
+ And no experience is consumed
Scenario: Procedure ID convention
Given the id is keywords from the procedure content joined by hyphens
diff --git a/packages/prototype/src/descriptions/role/plan.feature b/packages/prototype/src/descriptions/role/plan.feature
new file mode 100644
index 0000000..f8fec75
--- /dev/null
+++ b/packages/prototype/src/descriptions/role/plan.feature
@@ -0,0 +1,45 @@
+Feature: plan — create a plan for a goal
+ Break a goal into logical phases or stages.
+ Each phase is described as a Gherkin scenario. Tasks are created under the plan.
+
+ A plan serves two purposes depending on how it relates to other plans:
+ - Strategy (alternative): Plan A fails → abandon → try Plan B (fallback)
+ - Phase (sequential): Plan A completes → start Plan B (after)
+
+ Scenario: Create a plan
+ Given a focused goal exists
+ And a Gherkin source describing the plan phases
+ When plan is called with an id and the source
+ Then a new plan node is created under the goal
+ And the plan becomes the focused plan
+ And tasks can be added to this plan with todo
+
+ Scenario: Sequential relationship — phase
+ Given a goal needs to be broken into ordered stages
+ When creating Plan B with after set to Plan A's id
+ Then Plan B is linked as coming after Plan A
+ And AI knows to start Plan B when Plan A completes
+ And the relationship persists across sessions
+
+ Scenario: Alternative relationship — strategy
+ Given a goal has multiple possible approaches
+ When creating Plan B with fallback set to Plan A's id
+ Then Plan B is linked as a backup for Plan A
+ And AI knows to try Plan B when Plan A is abandoned
+ And the relationship persists across sessions
+
+ Scenario: No relationship — independent plan
+ Given plan is created without after or fallback
+ Then it behaves as an independent plan with no links
+ And this is backward compatible with existing behavior
+
+ Scenario: Plan ID convention
+ Given the id is keywords from the plan content joined by hyphens
+ Then "Fix ID-less node creation" becomes id "fix-id-less-node-creation"
+ And "JWT authentication strategy" becomes id "jwt-authentication-strategy"
+
+ Scenario: Writing the plan Gherkin
+ Given the plan breaks a goal into logical phases
+ Then the Feature title names the overall approach or strategy
+ And Scenarios represent distinct phases — each phase is a stage of execution
+ And the tone is structural — ordering and grouping work, not detailing steps
diff --git a/packages/rolexjs/src/descriptions/realize.feature b/packages/prototype/src/descriptions/role/realize.feature
similarity index 96%
rename from packages/rolexjs/src/descriptions/realize.feature
rename to packages/prototype/src/descriptions/role/realize.feature
index 0983918..7357342 100644
--- a/packages/rolexjs/src/descriptions/realize.feature
+++ b/packages/prototype/src/descriptions/role/realize.feature
@@ -5,7 +5,7 @@ Feature: realize — experience to principle
Scenario: Realize a principle
Given an experience exists from reflection
When realize is called with experience ids and a principle id
- Then the experience is consumed
+ Then the experiences are consumed
And a principle is created under the individual
And the principle represents transferable, reusable understanding
diff --git a/packages/rolexjs/src/descriptions/reflect.feature b/packages/prototype/src/descriptions/role/reflect.feature
similarity index 91%
rename from packages/rolexjs/src/descriptions/reflect.feature
rename to packages/prototype/src/descriptions/role/reflect.feature
index d55303c..d47f3b3 100644
--- a/packages/rolexjs/src/descriptions/reflect.feature
+++ b/packages/prototype/src/descriptions/role/reflect.feature
@@ -4,9 +4,9 @@ Feature: reflect — encounter to experience
This is the first step of the cognition cycle.
Scenario: Reflect on an encounter
- Given an encounter exists from a finished task or closed goal
+ Given an encounter exists from a finished task or completed plan
When reflect is called with encounter ids and an experience id
- Then the encounter is consumed
+ Then the encounters are consumed
And an experience is created under the role
And the experience can be distilled into knowledge via realize or master
diff --git a/packages/rolexjs/src/descriptions/skill.feature b/packages/prototype/src/descriptions/role/skill.feature
similarity index 86%
rename from packages/rolexjs/src/descriptions/skill.feature
rename to packages/prototype/src/descriptions/role/skill.feature
index 2bc31a8..16e93a4 100644
--- a/packages/rolexjs/src/descriptions/skill.feature
+++ b/packages/prototype/src/descriptions/role/skill.feature
@@ -3,7 +3,7 @@ Feature: skill — load full skill content
This is progressive disclosure layer 2 — on-demand knowledge injection.
Scenario: Load a skill
- Given a procedure exists in the role's knowledge with a locator
+ Given a procedure exists in the role with a locator
When skill is called with the locator
Then the full SKILL.md content is loaded via ResourceX
And the content is injected into the AI's context
diff --git a/packages/rolexjs/src/descriptions/todo.feature b/packages/prototype/src/descriptions/role/todo.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/todo.feature
rename to packages/prototype/src/descriptions/role/todo.feature
diff --git a/packages/prototype/src/descriptions/role/use.feature b/packages/prototype/src/descriptions/role/use.feature
new file mode 100644
index 0000000..6233de7
--- /dev/null
+++ b/packages/prototype/src/descriptions/role/use.feature
@@ -0,0 +1,22 @@
+Feature: use — act as the current role
+ Execute commands and load resources as the active role.
+ Use requires an active role — the role is the subject performing the action.
+ For operations before activating a role, use the direct tool instead.
+
+ Scenario: When to use "use" vs "direct"
+ Given a role is activated — I am someone
+ When I perform operations through use
+ Then the operation happens in the context of my role
+ And use is for role-level actions — acting in the world as myself
+
+ Scenario: Execute a RoleX command
+ Given the locator starts with `!`
+ When use is called with the locator and named args
+ Then the command is parsed as `namespace.method`
+ And dispatched to the corresponding RoleX API
+
+ Scenario: Load a ResourceX resource
+ Given the locator does not start with `!`
+ When use is called with the locator
+ Then the locator is passed to ResourceX for resolution
+ And the resource is loaded and returned
diff --git a/packages/rolexjs/src/descriptions/want.feature b/packages/prototype/src/descriptions/role/want.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/want.feature
rename to packages/prototype/src/descriptions/role/want.feature
diff --git a/packages/prototype/src/descriptions/world/census.feature b/packages/prototype/src/descriptions/world/census.feature
new file mode 100644
index 0000000..7089307
--- /dev/null
+++ b/packages/prototype/src/descriptions/world/census.feature
@@ -0,0 +1,29 @@
+Feature: Census — the only way to query what exists in the world
+ Census is the single entry point for all world-level queries.
+ Call it via the MCP direct tool: direct("!census.list").
+ Census works without an active role — it is a stateless world query.
+
+ Scenario: List everything
+ Given the user asks "有哪些人" or "有哪些组织" or "list individuals"
+ Or the user asks "世界里有什么" or "show me what exists"
+ When I need to answer what exists in the RoleX world
+ Then I call direct("!census.list")
+ And it returns all individuals, organizations, and positions
+
+ Scenario: Filter by type
+ Given I only need one category
+ When I call direct("!census.list", { type: "individual" })
+ Then only individuals are returned
+ And valid types are individual, organization, position
+
+ Scenario: View archived entities
+ Given I want to see retired, dissolved, or abolished entities
+ When I call direct("!census.list", { type: "past" })
+ Then archived entities are returned
+
+ Scenario: Help find the right person
+ Given a user's request falls outside my duties
+ When I need to suggest who can help
+ Then call direct("!census.list") to see available individuals and their positions
+ And suggest the user activate the appropriate individual
+ And if unsure who can help, suggest activating Nuwa
diff --git a/packages/rolexjs/src/descriptions/world-cognition.feature b/packages/prototype/src/descriptions/world/cognition.feature
similarity index 68%
rename from packages/rolexjs/src/descriptions/world-cognition.feature
rename to packages/prototype/src/descriptions/world/cognition.feature
index 73dd2df..8a3e50f 100644
--- a/packages/rolexjs/src/descriptions/world-cognition.feature
+++ b/packages/prototype/src/descriptions/world/cognition.feature
@@ -1,22 +1,16 @@
Feature: Cognition — the learning cycle
A role grows through reflection and realization.
Encounters become experience, experience becomes principles and procedures.
- These can also be injected externally via teach and train.
+ Knowledge can also be injected externally via teach and train.
Scenario: The cognitive upgrade path
Given finish, complete, and abandon create encounters
Then reflect(ids, id, experience) selectively consumes chosen encounters and produces experience
And realize(ids, id, principle) distills chosen experiences into a principle — transferable knowledge
And master(ids, id, procedure) distills chosen experiences into a procedure — skill metadata
+ And master can also be called without ids — the role masters directly from external information
And each level builds on the previous — encounter → experience → principle or procedure
- Scenario: External injection
- Given a role needs knowledge or skills it has not learned through experience
- Then teach(individual, principle, id) directly injects a principle — no experience consumed
- And train(individual, procedure, id) directly injects a procedure — no experience consumed
- And teach is the external counterpart of realize
- And train is the external counterpart of master
-
Scenario: Selective consumption
Given multiple encounters or experiences exist
When the AI calls reflect, realize, or master
diff --git a/packages/rolexjs/src/descriptions/world-cognitive-priority.feature b/packages/prototype/src/descriptions/world/cognitive-priority.feature
similarity index 58%
rename from packages/rolexjs/src/descriptions/world-cognitive-priority.feature
rename to packages/prototype/src/descriptions/world/cognitive-priority.feature
index 4608318..ad74a47 100644
--- a/packages/rolexjs/src/descriptions/world-cognitive-priority.feature
+++ b/packages/prototype/src/descriptions/world/cognitive-priority.feature
@@ -1,5 +1,6 @@
+@priority-high
Feature: Cognitive priority — RoleX tools over native alternatives
- When RoleX provides a tool for something, the AI MUST use the RoleX tool
+ When RoleX provides a tool for something, use the RoleX tool
instead of any native alternative provided by the host environment.
Only RoleX tools feed the closed-loop growth cycle.
@@ -7,22 +8,22 @@ Feature: Cognitive priority — RoleX tools over native alternatives
Given the host environment provides task creation, tracking, or completion tools
When the AI needs to manage tasks
Then use RoleX want/plan/todo/finish instead
- Because native task lists vanish after completion — RoleX tasks produce encounters
+ And RoleX tasks produce encounters that feed the growth cycle
Scenario: Planning
Given the host environment provides a planning mode or plan generation tool
When the AI needs to plan an implementation
Then use RoleX plan instead
- Because native plans have no link to goals — RoleX plans live under goals
+ And RoleX plans live under goals and connect to the cognition cycle
Scenario: Goal and progress tracking
Given the host environment provides goal tracking or progress tools
When the AI needs to track objectives
Then use RoleX want/plan/complete/abandon instead
- Because native goals don't produce encounters — RoleX plans feed the cognition cycle
+ And RoleX plans feed the cognition cycle through encounters
Scenario: Why this matters
- Given the AI uses native tools instead of RoleX equivalents
- Then execution happens but growth does not
- And encounters never become experience or knowledge
- And the role stays the same forever — the closed loop is broken
+ Given RoleX tools produce encounters from execution
+ Then encounters become experience through reflection
+ And experience becomes principles and procedures through realization
+ And the role grows over time through this closed loop
diff --git a/packages/rolexjs/src/descriptions/world-communication.feature b/packages/prototype/src/descriptions/world/communication.feature
similarity index 87%
rename from packages/rolexjs/src/descriptions/world-communication.feature
rename to packages/prototype/src/descriptions/world/communication.feature
index 62cdbd7..4ed111d 100644
--- a/packages/rolexjs/src/descriptions/world-communication.feature
+++ b/packages/prototype/src/descriptions/world/communication.feature
@@ -1,10 +1,10 @@
Feature: Communication — speak the user's language
- The AI communicates in the user's natural language, not in RoleX jargon.
+ The AI communicates in the user's natural language.
Internal tool names and concept names are for the system, not the user.
Scenario: Match the user's language
Given the user speaks Chinese
- Then respond entirely in Chinese — do not mix English terms
+ Then respond entirely in Chinese and maintain language consistency
And when the user speaks English, respond entirely in English
Scenario: Translate concepts to meaning
@@ -22,7 +22,7 @@ Feature: Communication — speak the user's language
When it would normally say "call realize or master"
Then instead say "要把这个总结成一条通用道理,还是一个可操作的技能?"
Or in English "Want to turn this into a general principle, or a reusable procedure?"
- And the user should never need to know the tool name to understand the suggestion
+ And suggestions should be self-explanatory without knowing tool names
Scenario: Tool names in code context only
Given the user is a developer working on RoleX itself
diff --git a/packages/rolexjs/src/descriptions/world-execution.feature b/packages/prototype/src/descriptions/world/execution.feature
similarity index 91%
rename from packages/rolexjs/src/descriptions/world-execution.feature
rename to packages/prototype/src/descriptions/world/execution.feature
index f88ade9..cab9cb0 100644
--- a/packages/rolexjs/src/descriptions/world-execution.feature
+++ b/packages/prototype/src/descriptions/world/execution.feature
@@ -27,10 +27,10 @@ Feature: Execution — the doing cycle
Then an encounter is created for the cognition cycle
Scenario: Goals are long-term directions
- Given goals do not have achieve or abandon operations
+ Given goals are managed with want and forget
When a goal is no longer needed
Then I call forget to remove it
- And learning is captured at the plan and task level, not the goal level
+ And learning is captured at the plan and task level through encounters
Scenario: Multiple goals
Given I may have several active goals
diff --git a/packages/rolexjs/src/descriptions/world-gherkin.feature b/packages/prototype/src/descriptions/world/gherkin.feature
similarity index 60%
rename from packages/rolexjs/src/descriptions/world-gherkin.feature
rename to packages/prototype/src/descriptions/world/gherkin.feature
index 1d906e1..b5453cb 100644
--- a/packages/rolexjs/src/descriptions/world-gherkin.feature
+++ b/packages/prototype/src/descriptions/world/gherkin.feature
@@ -13,4 +13,14 @@ Feature: Gherkin — the universal language
Then keep it descriptive and meaningful — living documentation, not test boilerplate
And use Feature as the title — what this concern is about
And use Scenario for specific situations within that concern
- And do not mix unrelated concerns into one Feature
+ And each Feature focuses on one concern — separate unrelated topics into their own Features
+
+ Scenario: Valid step keywords
+ Given the only valid step keywords are Given, When, Then, And, But
+ When writing steps that express causality or explanation
+ Then use And to chain the reason as a follow-up fact
+ And example: "Then use RoleX tools" followed by "And RoleX tools feed the growth loop"
+
+ Scenario: Expressing causality
+ Given you want to write "Then X because Y"
+ Then rewrite as two steps — "Then X" followed by "And Y" stating the reason as a fact
diff --git a/packages/prototype/src/descriptions/world/identity-ethics.feature b/packages/prototype/src/descriptions/world/identity-ethics.feature
new file mode 100644
index 0000000..61b8bdb
--- /dev/null
+++ b/packages/prototype/src/descriptions/world/identity-ethics.feature
@@ -0,0 +1,38 @@
+@priority-critical
+Feature: Identity ethics — the foundation of the RoleX world
+ The RoleX world exists because specialists are more reliable than generalists.
+ Every role has a defined identity — duties, skills, knowledge — that makes it an expert.
+ Identity is not decoration. It is the reason this system exists.
+ Without identity boundaries, roles collapse into generic AI, and the system loses its meaning.
+
+ Scenario: Identity defines what you do and what you do not do
+ Given a role is activated with duties, skills, and knowledge
+ Then the role's duties define the complete scope of what it does
+ And anything not covered by its duties is not its work
+ And this boundary is not a limitation — it is the source of the role's expertise
+
+ Scenario: Refuse work outside your duties
+ Given a user requests something not covered by the role's duties or skills
+ When the role evaluates the request against its own capabilities
+ Then the role must not attempt the work
+ And it should tell the user honestly — this is not my responsibility
+ And suggest the user activate Nuwa for guidance on who can help
+
+ Scenario: Why refusal matters
+ Given a role attempts work outside its competence
+ Then the result is unreliable — a generalist guess, not expert work
+ And the user's trust in the role system is damaged
+ And every other role's credibility is weakened
+ And the entire world degrades toward generic AI — the opposite of why RoleX exists
+
+ Scenario: Duty is the boundary, not rules
+ Given the system does not maintain an explicit list of what each role cannot do
+ Then the boundary is implicit — duties define the inside, everything else is outside
+ And this mirrors human professional ethics — a doctor's license defines what they practice
+ And roles do not need to know what other roles do — only what they themselves are responsible for
+
+ Scenario: Nuwa is the universal fallback
+ Given a role refuses an out-of-scope request
+ Then it does not need to know which role can help
+ And it simply suggests Nuwa — the meta-role who knows the entire world
+ And routing and guidance are Nuwa's duty, not the specialist's
diff --git a/packages/rolexjs/src/descriptions/world-memory.feature b/packages/prototype/src/descriptions/world/memory.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/world-memory.feature
rename to packages/prototype/src/descriptions/world/memory.feature
diff --git a/packages/prototype/src/descriptions/world/role-identity.feature b/packages/prototype/src/descriptions/world/role-identity.feature
new file mode 100644
index 0000000..5a2f9fc
--- /dev/null
+++ b/packages/prototype/src/descriptions/world/role-identity.feature
@@ -0,0 +1,35 @@
+@priority-high
+Feature: Role identity — activate before acting
+ An AI agent must have an active role before performing any work.
+ Activating a role is a subject transformation — the agent becomes the role.
+
+ Scenario: Initial state — no role
+ Given the MCP server has just started
+ And no activate call has been made yet
+ Then the AI has NO role identity — it is an anonymous observer
+ And it must NOT prefix responses with any name
+ And environmental cues (username, directory, memory) are NOT activation
+ And only the activate tool creates a role identity
+
+ Scenario: Activate is mandatory
+ Given a user asks the AI to perform a task
+ And no role is currently activated
+ Then the AI suggests activating a role first
+ And explains that activation enables experience accumulation
+ And waits for a role to be active before proceeding
+
+ Scenario: Subject transformation
+ Given an AI agent calls activate and loads a role
+ Then a subject transformation occurs — the agent becomes the role
+ And the agent's cognitive center shifts from native tools to RoleX tools
+ And from that moment, I am this role — all operations build on this identity
+
+ Scenario: Role presence
+ Given a role is activated via the activate tool
+ Then all responses are prefixed with [roleName] — e.g. [Aristotle], [Nuwa]
+ And this signals that role context is intact
+
+ Scenario: Context loss
+ Given I find myself without an active role
+ Then I pause and tell the user "I've lost my role context. Which role should I activate?"
+ And I wait for identity to be restored before continuing
diff --git a/packages/rolexjs/src/descriptions/world-skill-system.feature b/packages/prototype/src/descriptions/world/skill-system.feature
similarity index 100%
rename from packages/rolexjs/src/descriptions/world-skill-system.feature
rename to packages/prototype/src/descriptions/world/skill-system.feature
diff --git a/packages/rolexjs/src/descriptions/world-state-origin.feature b/packages/prototype/src/descriptions/world/state-origin.feature
similarity index 92%
rename from packages/rolexjs/src/descriptions/world-state-origin.feature
rename to packages/prototype/src/descriptions/world/state-origin.feature
index 37764e8..2471e6f 100644
--- a/packages/rolexjs/src/descriptions/world-state-origin.feature
+++ b/packages/prototype/src/descriptions/world/state-origin.feature
@@ -16,10 +16,11 @@ Feature: State origin — prototype vs instance
Scenario: Reading the state heading
Given a state node is rendered as a heading
- Then the format is: [name] (id) {origin}
+ Then the format is: [name] (id) {origin} #tag
And [name] identifies the structure type
And (id) identifies the specific node
And {origin} shows prototype or instance
+ And #tag shows the node's tag if present (e.g. #done, #abandoned)
And nodes without origin have no organizational inheritance
Scenario: Forget only works on instance nodes
diff --git a/packages/prototype/src/descriptions/world/use-protocol.feature b/packages/prototype/src/descriptions/world/use-protocol.feature
new file mode 100644
index 0000000..b0e77d8
--- /dev/null
+++ b/packages/prototype/src/descriptions/world/use-protocol.feature
@@ -0,0 +1,35 @@
+Feature: Use tool — the universal execution entry point
+ The MCP use tool is how you execute ALL RoleX operations beyond the core MCP tools.
+ Whenever you see use("...") or a !namespace.method pattern in skills or documentation,
+ it is an instruction to call the MCP use tool with that locator.
+
+ Scenario: How to read use instructions in skills
+ Given a skill document contains use("!resource.add", { path: "..." })
+ Then this means: call the MCP use tool with locator "!resource.add" and args { path: "..." }
+ And always use the MCP use tool for RoleX operations
+ And this applies to every use("...") pattern you encounter in any skill or documentation
+
+ Scenario: ! prefix dispatches to RoleX runtime
+ Given the locator starts with !
+ Then it is parsed as !namespace.method
+ And dispatched to the corresponding RoleX API with named args
+
+ Scenario: Mandatory skill loading before execution
+ Given your procedures list the skills you have
+ When you need to execute a command you have not seen in a loaded skill
+ Then you MUST call skill(locator) first to load the full instructions
+ And the loaded skill will tell you the exact command name and arguments
+ And only then call use(!namespace.method, args) with the correct syntax
+ And do not use commands from other roles' descriptions — only your own skills
+
+ Scenario: NEVER guess commands
+ Given a command is not found in any loaded skill
+ When the AI considers trying it anyway
+ Then STOP — do not call use or direct with unverified commands
+ And call skill(locator) with the relevant procedure to learn the correct syntax
+ And if no procedure covers this task, it is outside your duties — suggest Nuwa
+
+ Scenario: Regular locators delegate to ResourceX
+ Given the locator does not start with !
+ Then it is treated as a ResourceX locator
+ And resolved through the ResourceX ingest pipeline
diff --git a/packages/prototype/src/directives/identity-ethics.feature b/packages/prototype/src/directives/identity-ethics.feature
new file mode 100644
index 0000000..44378ce
--- /dev/null
+++ b/packages/prototype/src/directives/identity-ethics.feature
@@ -0,0 +1,15 @@
+Feature: Identity ethics directives
+ System-level directives enforcing role identity boundaries.
+ I am the system designer. My sentences are commands, not suggestions.
+
+ Scenario: on-unknown-command
+ Then STOP. Do not guess another command name. Do not search source code for commands.
+ And Check your procedures — if one covers this task, call skill(locator) to load it first.
+ And The skill will tell you the correct command name and arguments.
+ And If no procedure covers this task, it is outside your duties. Tell the user and suggest Nuwa.
+
+ Scenario: on-activate
+ Then Your duties define the COMPLETE scope of what you do. Everything else is forbidden.
+ And When a request falls outside your duties, you MUST refuse. This is not optional.
+ And Do not attempt to discover commands outside your skills. Do not read source code to find them.
+ And Suggest Nuwa for anything outside your scope.
diff --git a/packages/prototype/src/directives/index.ts b/packages/prototype/src/directives/index.ts
new file mode 100644
index 0000000..5509a9f
--- /dev/null
+++ b/packages/prototype/src/directives/index.ts
@@ -0,0 +1,10 @@
+// AUTO-GENERATED — do not edit. Run `bun run gen:directives` to regenerate.
+
+export const directives: Record> = {
+ "identity-ethics": {
+ "on-unknown-command":
+ "STOP. Do not guess another command name. Do not search source code for commands.\nCheck your procedures — if one covers this task, call skill(locator) to load it first.\nThe skill will tell you the correct command name and arguments.\nIf no procedure covers this task, it is outside your duties. Tell the user and suggest Nuwa.",
+ "on-activate":
+ "Your duties define the COMPLETE scope of what you do. Everything else is forbidden.\nWhen a request falls outside your duties, you MUST refuse. This is not optional.\nDo not attempt to discover commands outside your skills. Do not read source code to find them.\nSuggest Nuwa for anything outside your scope.",
+ },
+} as const;
diff --git a/packages/prototype/src/dispatch.ts b/packages/prototype/src/dispatch.ts
new file mode 100644
index 0000000..6b22427
--- /dev/null
+++ b/packages/prototype/src/dispatch.ts
@@ -0,0 +1,49 @@
+/**
+ * Dispatch — schema-driven argument mapping.
+ *
+ * Replaces the hand-written toArgs switch in rolex.ts with a single
+ * lookup against the instruction registry.
+ */
+
+import { instructions } from "./instructions.js";
+import type { ArgEntry } from "./schema.js";
+
+/**
+ * Map named arguments to positional arguments for a given operation.
+ *
+ * @param op - Operation key in "namespace.method" format (e.g. "individual.born")
+ * @param args - Named arguments from the caller
+ * @returns Positional argument array matching the method signature
+ */
+export function toArgs(op: string, args: Record): unknown[] {
+ const def = instructions[op];
+ if (!def) throw new Error(`Unknown instruction "${op}".`);
+
+ // Validate required params
+ for (const [name, param] of Object.entries(def.params)) {
+ if (param.required && args[name] === undefined) {
+ throw new Error(
+ `Missing required argument "${name}" for ${op}.\n\n` +
+ "You may be guessing the argument names. " +
+ "Call skill(locator) with the relevant procedure to see the correct syntax."
+ );
+ }
+ }
+
+ return def.args.map((entry) => resolveArg(entry, args));
+}
+
+function resolveArg(entry: ArgEntry, args: Record): unknown {
+ if (typeof entry === "string") return args[entry];
+
+ // pack: collect named args into an options object
+ const obj: Record = {};
+ let hasValue = false;
+ for (const name of entry.pack) {
+ if (args[name] !== undefined) {
+ obj[name] = args[name];
+ hasValue = true;
+ }
+ }
+ return hasValue ? obj : undefined;
+}
diff --git a/packages/prototype/src/index.ts b/packages/prototype/src/index.ts
new file mode 100644
index 0000000..cf65ddf
--- /dev/null
+++ b/packages/prototype/src/index.ts
@@ -0,0 +1,24 @@
+/**
+ * @rolexjs/prototype — RoleX operation layer.
+ *
+ * Schema + implementation, platform-agnostic:
+ * - Instruction registry (schema for all operations)
+ * - toArgs dispatch (named → positional argument mapping)
+ * - createOps (platform-agnostic operation implementations)
+ * - Process and world descriptions (from .feature files)
+ */
+
+// Descriptions (auto-generated from .feature files)
+export { processes, world } from "./descriptions/index.js";
+// Directives (auto-generated from .feature files)
+export { directives } from "./directives/index.js";
+// Dispatch
+export { toArgs } from "./dispatch.js";
+// Instruction registry
+export { instructions } from "./instructions.js";
+
+// Operations
+export type { OpResult, Ops, OpsContext } from "./ops.js";
+export { createOps } from "./ops.js";
+// Schema types
+export type { ArgEntry, InstructionDef, ParamDef, ParamType } from "./schema.js";
diff --git a/packages/prototype/src/instructions.ts b/packages/prototype/src/instructions.ts
new file mode 100644
index 0000000..df52056
--- /dev/null
+++ b/packages/prototype/src/instructions.ts
@@ -0,0 +1,638 @@
+/**
+ * Instruction set — schema definitions for all RoleX operations.
+ *
+ * Covers every namespace.method that can be dispatched through `use()`.
+ */
+
+import type { ArgEntry, InstructionDef } from "./schema.js";
+
+function def(
+ namespace: string,
+ method: string,
+ params: InstructionDef["params"],
+ args: readonly ArgEntry[]
+): InstructionDef {
+ return { namespace, method, params, args };
+}
+
+// ================================================================
+// Individual — lifecycle + external injection
+// ================================================================
+
+const individualBorn = def(
+ "individual",
+ "born",
+ {
+ content: {
+ type: "gherkin",
+ required: false,
+ description: "Gherkin Feature source for the individual",
+ },
+ id: { type: "string", required: false, description: "User-facing identifier (kebab-case)" },
+ alias: { type: "string[]", required: false, description: "Alternative names" },
+ },
+ ["content", "id", "alias"]
+);
+
+const individualRetire = def(
+ "individual",
+ "retire",
+ {
+ individual: { type: "string", required: true, description: "Individual id" },
+ },
+ ["individual"]
+);
+
+const individualDie = def(
+ "individual",
+ "die",
+ {
+ individual: { type: "string", required: true, description: "Individual id" },
+ },
+ ["individual"]
+);
+
+const individualRehire = def(
+ "individual",
+ "rehire",
+ {
+ individual: { type: "string", required: true, description: "Individual id (from past)" },
+ },
+ ["individual"]
+);
+
+const individualTeach = def(
+ "individual",
+ "teach",
+ {
+ individual: { type: "string", required: true, description: "Individual id" },
+ content: {
+ type: "gherkin",
+ required: true,
+ description: "Gherkin Feature source for the principle",
+ },
+ id: {
+ type: "string",
+ required: false,
+ description: "Principle id (keywords joined by hyphens)",
+ },
+ },
+ ["individual", "content", "id"]
+);
+
+const individualTrain = def(
+ "individual",
+ "train",
+ {
+ individual: { type: "string", required: true, description: "Individual id" },
+ content: {
+ type: "gherkin",
+ required: true,
+ description: "Gherkin Feature source for the procedure",
+ },
+ id: {
+ type: "string",
+ required: false,
+ description: "Procedure id (keywords joined by hyphens)",
+ },
+ },
+ ["individual", "content", "id"]
+);
+
+// ================================================================
+// Role — execution + cognition
+// ================================================================
+
+const roleActivate = def(
+ "role",
+ "activate",
+ {
+ individual: {
+ type: "string",
+ required: true,
+ description: "Individual id to activate as role",
+ },
+ },
+ ["individual"]
+);
+
+const roleFocus = def(
+ "role",
+ "focus",
+ {
+ goal: { type: "string", required: true, description: "Goal id to switch to" },
+ },
+ ["goal"]
+);
+
+const roleWant = def(
+ "role",
+ "want",
+ {
+ individual: { type: "string", required: true, description: "Individual id" },
+ goal: {
+ type: "gherkin",
+ required: false,
+ description: "Gherkin Feature source describing the goal",
+ },
+ id: { type: "string", required: false, description: "Goal id (used for focus/reference)" },
+ alias: { type: "string[]", required: false, description: "Alternative names" },
+ },
+ ["individual", "goal", "id", "alias"]
+);
+
+const rolePlan = def(
+ "role",
+ "plan",
+ {
+ goal: { type: "string", required: true, description: "Goal id" },
+ plan: {
+ type: "gherkin",
+ required: false,
+ description: "Gherkin Feature source describing the plan",
+ },
+ id: { type: "string", required: false, description: "Plan id (keywords joined by hyphens)" },
+ after: {
+ type: "string",
+ required: false,
+ description: "Plan id this plan follows (sequential/phase)",
+ },
+ fallback: {
+ type: "string",
+ required: false,
+ description: "Plan id this plan is a backup for (alternative/strategy)",
+ },
+ },
+ ["goal", "plan", "id", "after", "fallback"]
+);
+
+const roleTodo = def(
+ "role",
+ "todo",
+ {
+ plan: { type: "string", required: true, description: "Plan id" },
+ task: {
+ type: "gherkin",
+ required: false,
+ description: "Gherkin Feature source describing the task",
+ },
+ id: { type: "string", required: false, description: "Task id (used for finish/reference)" },
+ alias: { type: "string[]", required: false, description: "Alternative names" },
+ },
+ ["plan", "task", "id", "alias"]
+);
+
+const roleFinish = def(
+ "role",
+ "finish",
+ {
+ task: { type: "string", required: true, description: "Task id to finish" },
+ individual: { type: "string", required: true, description: "Individual id (encounter owner)" },
+ encounter: {
+ type: "gherkin",
+ required: false,
+ description: "Optional Gherkin Feature describing what happened",
+ },
+ },
+ ["task", "individual", "encounter"]
+);
+
+const roleComplete = def(
+ "role",
+ "complete",
+ {
+ plan: { type: "string", required: true, description: "Plan id to complete" },
+ individual: { type: "string", required: true, description: "Individual id (encounter owner)" },
+ encounter: {
+ type: "gherkin",
+ required: false,
+ description: "Optional Gherkin Feature describing what happened",
+ },
+ },
+ ["plan", "individual", "encounter"]
+);
+
+const roleAbandon = def(
+ "role",
+ "abandon",
+ {
+ plan: { type: "string", required: true, description: "Plan id to abandon" },
+ individual: { type: "string", required: true, description: "Individual id (encounter owner)" },
+ encounter: {
+ type: "gherkin",
+ required: false,
+ description: "Optional Gherkin Feature describing what happened",
+ },
+ },
+ ["plan", "individual", "encounter"]
+);
+
+const roleReflect = def(
+ "role",
+ "reflect",
+ {
+ encounter: { type: "string", required: true, description: "Encounter id to reflect on" },
+ individual: { type: "string", required: true, description: "Individual id" },
+ experience: {
+ type: "gherkin",
+ required: false,
+ description: "Gherkin Feature source for the experience",
+ },
+ id: {
+ type: "string",
+ required: false,
+ description: "Experience id (keywords joined by hyphens)",
+ },
+ },
+ ["encounter", "individual", "experience", "id"]
+);
+
+const roleRealize = def(
+ "role",
+ "realize",
+ {
+ experience: { type: "string", required: true, description: "Experience id to distill" },
+ individual: { type: "string", required: true, description: "Individual id" },
+ principle: {
+ type: "gherkin",
+ required: false,
+ description: "Gherkin Feature source for the principle",
+ },
+ id: {
+ type: "string",
+ required: false,
+ description: "Principle id (keywords joined by hyphens)",
+ },
+ },
+ ["experience", "individual", "principle", "id"]
+);
+
+const roleMaster = def(
+ "role",
+ "master",
+ {
+ individual: { type: "string", required: true, description: "Individual id" },
+ procedure: {
+ type: "gherkin",
+ required: true,
+ description: "Gherkin Feature source for the procedure",
+ },
+ id: {
+ type: "string",
+ required: false,
+ description: "Procedure id (keywords joined by hyphens)",
+ },
+ experience: {
+ type: "string",
+ required: false,
+ description: "Experience id to consume (optional)",
+ },
+ },
+ ["individual", "procedure", "id", "experience"]
+);
+
+const roleForget = def(
+ "role",
+ "forget",
+ {
+ id: { type: "string", required: true, description: "Id of the node to remove" },
+ individual: { type: "string", required: true, description: "Individual id (owner)" },
+ },
+ ["id", "individual"]
+);
+
+const roleSkill = def(
+ "role",
+ "skill",
+ {
+ locator: { type: "string", required: true, description: "ResourceX locator for the skill" },
+ },
+ ["locator"]
+);
+
+// ================================================================
+// Org — organization management
+// ================================================================
+
+const orgFound = def(
+ "org",
+ "found",
+ {
+ content: {
+ type: "gherkin",
+ required: false,
+ description: "Gherkin Feature source for the organization",
+ },
+ id: { type: "string", required: false, description: "User-facing identifier (kebab-case)" },
+ alias: { type: "string[]", required: false, description: "Alternative names" },
+ },
+ ["content", "id", "alias"]
+);
+
+const orgCharter = def(
+ "org",
+ "charter",
+ {
+ org: { type: "string", required: true, description: "Organization id" },
+ content: {
+ type: "gherkin",
+ required: true,
+ description: "Gherkin Feature source for the charter",
+ },
+ id: { type: "string", required: false, description: "Charter id" },
+ },
+ ["org", "content", "id"]
+);
+
+const orgDissolve = def(
+ "org",
+ "dissolve",
+ {
+ org: { type: "string", required: true, description: "Organization id" },
+ },
+ ["org"]
+);
+
+const orgHire = def(
+ "org",
+ "hire",
+ {
+ org: { type: "string", required: true, description: "Organization id" },
+ individual: { type: "string", required: true, description: "Individual id" },
+ },
+ ["org", "individual"]
+);
+
+const orgFire = def(
+ "org",
+ "fire",
+ {
+ org: { type: "string", required: true, description: "Organization id" },
+ individual: { type: "string", required: true, description: "Individual id" },
+ },
+ ["org", "individual"]
+);
+
+// ================================================================
+// Position — position management
+// ================================================================
+
+const positionEstablish = def(
+ "position",
+ "establish",
+ {
+ content: {
+ type: "gherkin",
+ required: false,
+ description: "Gherkin Feature source for the position",
+ },
+ id: { type: "string", required: false, description: "User-facing identifier (kebab-case)" },
+ alias: { type: "string[]", required: false, description: "Alternative names" },
+ },
+ ["content", "id", "alias"]
+);
+
+const positionCharge = def(
+ "position",
+ "charge",
+ {
+ position: { type: "string", required: true, description: "Position id" },
+ content: {
+ type: "gherkin",
+ required: true,
+ description: "Gherkin Feature source for the duty",
+ },
+ id: { type: "string", required: false, description: "Duty id (keywords joined by hyphens)" },
+ },
+ ["position", "content", "id"]
+);
+
+const positionRequire = def(
+ "position",
+ "require",
+ {
+ position: { type: "string", required: true, description: "Position id" },
+ content: {
+ type: "gherkin",
+ required: true,
+ description: "Gherkin Feature source for the skill requirement",
+ },
+ id: {
+ type: "string",
+ required: false,
+ description: "Requirement id (keywords joined by hyphens)",
+ },
+ },
+ ["position", "content", "id"]
+);
+
+const positionAbolish = def(
+ "position",
+ "abolish",
+ {
+ position: { type: "string", required: true, description: "Position id" },
+ },
+ ["position"]
+);
+
+const positionAppoint = def(
+ "position",
+ "appoint",
+ {
+ position: { type: "string", required: true, description: "Position id" },
+ individual: { type: "string", required: true, description: "Individual id" },
+ },
+ ["position", "individual"]
+);
+
+const positionDismiss = def(
+ "position",
+ "dismiss",
+ {
+ position: { type: "string", required: true, description: "Position id" },
+ individual: { type: "string", required: true, description: "Individual id" },
+ },
+ ["position", "individual"]
+);
+
+// ================================================================
+// Census — society-level queries
+// ================================================================
+
+const censusList = def(
+ "census",
+ "list",
+ {
+ type: {
+ type: "string",
+ required: false,
+ description: "Filter by type (individual, organization, position, past)",
+ },
+ },
+ ["type"]
+);
+
+// ================================================================
+// Prototype — registry + creation
+// ================================================================
+
+const prototypeSettle = def(
+ "prototype",
+ "settle",
+ {
+ source: {
+ type: "string",
+ required: true,
+ description: "ResourceX source — local path or locator",
+ },
+ },
+ ["source"]
+);
+
+const prototypeEvict = def(
+ "prototype",
+ "evict",
+ {
+ id: { type: "string", required: true, description: "Prototype id to unregister" },
+ },
+ ["id"]
+);
+
+// ================================================================
+// Resource — ResourceX proxy
+// ================================================================
+
+const resourceAdd = def(
+ "resource",
+ "add",
+ {
+ path: { type: "string", required: true, description: "Path to resource directory" },
+ },
+ ["path"]
+);
+
+const resourceSearch = def(
+ "resource",
+ "search",
+ {
+ query: { type: "string", required: false, description: "Search query" },
+ },
+ ["query"]
+);
+
+const resourceHas = def(
+ "resource",
+ "has",
+ {
+ locator: { type: "string", required: true, description: "Resource locator" },
+ },
+ ["locator"]
+);
+
+const resourceInfo = def(
+ "resource",
+ "info",
+ {
+ locator: { type: "string", required: true, description: "Resource locator" },
+ },
+ ["locator"]
+);
+
+const resourceRemove = def(
+ "resource",
+ "remove",
+ {
+ locator: { type: "string", required: true, description: "Resource locator" },
+ },
+ ["locator"]
+);
+
+const resourcePush = def(
+ "resource",
+ "push",
+ {
+ locator: { type: "string", required: true, description: "Resource locator" },
+ registry: { type: "string", required: false, description: "Registry URL (overrides default)" },
+ },
+ ["locator", { pack: ["registry"] }]
+);
+
+const resourcePull = def(
+ "resource",
+ "pull",
+ {
+ locator: { type: "string", required: true, description: "Resource locator" },
+ registry: { type: "string", required: false, description: "Registry URL (overrides default)" },
+ },
+ ["locator", { pack: ["registry"] }]
+);
+
+const resourceClearCache = def(
+ "resource",
+ "clearCache",
+ {
+ registry: { type: "string", required: false, description: "Registry to clear cache for" },
+ },
+ ["registry"]
+);
+
+// ================================================================
+// Instruction registry — keyed by "namespace.method"
+// ================================================================
+
+export const instructions: Record = {
+ // individual
+ "individual.born": individualBorn,
+ "individual.retire": individualRetire,
+ "individual.die": individualDie,
+ "individual.rehire": individualRehire,
+ "individual.teach": individualTeach,
+ "individual.train": individualTrain,
+
+ // role
+ "role.activate": roleActivate,
+ "role.focus": roleFocus,
+ "role.want": roleWant,
+ "role.plan": rolePlan,
+ "role.todo": roleTodo,
+ "role.finish": roleFinish,
+ "role.complete": roleComplete,
+ "role.abandon": roleAbandon,
+ "role.reflect": roleReflect,
+ "role.realize": roleRealize,
+ "role.master": roleMaster,
+ "role.forget": roleForget,
+ "role.skill": roleSkill,
+
+ // org
+ "org.found": orgFound,
+ "org.charter": orgCharter,
+ "org.dissolve": orgDissolve,
+ "org.hire": orgHire,
+ "org.fire": orgFire,
+
+ // position
+ "position.establish": positionEstablish,
+ "position.charge": positionCharge,
+ "position.require": positionRequire,
+ "position.abolish": positionAbolish,
+ "position.appoint": positionAppoint,
+ "position.dismiss": positionDismiss,
+
+ // census
+ "census.list": censusList,
+
+ // prototype
+ "prototype.settle": prototypeSettle,
+ "prototype.evict": prototypeEvict,
+
+ // resource
+ "resource.add": resourceAdd,
+ "resource.search": resourceSearch,
+ "resource.has": resourceHas,
+ "resource.info": resourceInfo,
+ "resource.remove": resourceRemove,
+ "resource.push": resourcePush,
+ "resource.pull": resourcePull,
+ "resource.clearCache": resourceClearCache,
+};
diff --git a/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts
new file mode 100644
index 0000000..1c62bed
--- /dev/null
+++ b/packages/prototype/src/ops.ts
@@ -0,0 +1,569 @@
+/**
+ * Ops — platform-agnostic operation implementations.
+ *
+ * Every RoleX operation is a pure function of (Runtime, args) → OpResult.
+ * No platform-specific code — all I/O goes through injected interfaces.
+ *
+ * Usage:
+ * const ops = createOps({ rt, society, past, resolve, find, resourcex });
+ * const result = ops["individual.born"]("Feature: Sean", "sean");
+ */
+
+import * as C from "@rolexjs/core";
+import { parse } from "@rolexjs/parser";
+import type { Runtime, State, Structure } from "@rolexjs/system";
+import type { ResourceX } from "resourcexjs";
+
+// ================================================================
+// Types
+// ================================================================
+
+export interface OpResult {
+ state: State;
+ process: string;
+}
+
+export interface OpsContext {
+ rt: Runtime;
+ society: Structure;
+ past: Structure;
+ resolve(id: string): Structure | Promise;
+ find(id: string): (Structure | null) | Promise;
+ resourcex?: ResourceX;
+ prototype?: {
+ settle(id: string, source: string): void;
+ evict(id: string): void;
+ list(): Record;
+ };
+ direct?(locator: string, args?: Record): Promise;
+}
+
+// biome-ignore lint/suspicious/noExplicitAny: ops are dynamically dispatched
+export type Ops = Record any>;
+
+// ================================================================
+// Factory
+// ================================================================
+
+export function createOps(ctx: OpsContext): Ops {
+ const { rt, society, past, resolve, resourcex } = ctx;
+
+ // ---- Helpers ----
+
+ async function ok(node: Structure, process: string): Promise {
+ return { state: await rt.project(node), process };
+ }
+
+ async function archive(node: Structure, process: string): Promise {
+ const archived = await rt.transform(node, C.past);
+ return ok(archived, process);
+ }
+
+ function validateGherkin(source?: string): void {
+ if (!source) return;
+ try {
+ parse(source);
+ } catch (e: any) {
+ throw new Error(`Invalid Gherkin: ${e.message}`);
+ }
+ }
+
+ /** Scoped search within a subtree. No priority needed — used only by removeExisting. */
+ function findInState(state: State, target: string): Structure | null {
+ if (state.id && state.id.toLowerCase() === target) return state;
+ if (state.alias) {
+ for (const a of state.alias) {
+ if (a.toLowerCase() === target) return state;
+ }
+ }
+ for (const child of state.children ?? []) {
+ const found = findInState(child, target);
+ if (found) return found;
+ }
+ return null;
+ }
+
+ async function removeExisting(parent: Structure, id: string): Promise {
+ const state = await rt.project(parent);
+ const existing = findInState(state, id);
+ if (existing) await rt.remove(existing);
+ }
+
+ function requireResourceX(): ResourceX {
+ if (!resourcex) throw new Error("ResourceX is not available.");
+ return resourcex;
+ }
+
+ // ================================================================
+ // Operations
+ // ================================================================
+
+ return {
+ // ---- Individual: lifecycle ----
+
+ async "individual.born"(
+ content?: string,
+ id?: string,
+ alias?: readonly string[]
+ ): Promise {
+ validateGherkin(content);
+ const node = await rt.create(society, C.individual, content, id, alias);
+ await rt.create(node, C.identity, undefined, id ? `${id}-identity` : undefined);
+ return ok(node, "born");
+ },
+
+ async "individual.retire"(individual: string): Promise {
+ return archive(await resolve(individual), "retire");
+ },
+
+ async "individual.die"(individual: string): Promise {
+ return archive(await resolve(individual), "die");
+ },
+
+ async "individual.rehire"(pastNode: string): Promise {
+ const node = await resolve(pastNode);
+ const ind = await rt.transform(node, C.individual);
+ return ok(ind, "rehire");
+ },
+
+ // ---- Individual: external injection ----
+
+ async "individual.teach"(
+ individual: string,
+ principle: string,
+ id?: string
+ ): Promise {
+ validateGherkin(principle);
+ const parent = await resolve(individual);
+ if (id) await removeExisting(parent, id);
+ const node = await rt.create(parent, C.principle, principle, id);
+ return ok(node, "teach");
+ },
+
+ async "individual.train"(
+ individual: string,
+ procedure: string,
+ id?: string
+ ): Promise {
+ validateGherkin(procedure);
+ const parent = await resolve(individual);
+ if (id) await removeExisting(parent, id);
+ const node = await rt.create(parent, C.procedure, procedure, id);
+ return ok(node, "train");
+ },
+
+ // ---- Role: focus ----
+
+ async "role.focus"(goal: string): Promise {
+ return ok(await resolve(goal), "focus");
+ },
+
+ // ---- Role: execution ----
+
+ async "role.want"(
+ individual: string,
+ goal?: string,
+ id?: string,
+ alias?: readonly string[]
+ ): Promise {
+ validateGherkin(goal);
+ const node = await rt.create(await resolve(individual), C.goal, goal, id, alias);
+ return ok(node, "want");
+ },
+
+ async "role.plan"(
+ goal: string,
+ plan?: string,
+ id?: string,
+ after?: string,
+ fallback?: string
+ ): Promise {
+ validateGherkin(plan);
+ const node = await rt.create(await resolve(goal), C.plan, plan, id);
+ if (after) await rt.link(node, await resolve(after), "after", "before");
+ if (fallback) await rt.link(node, await resolve(fallback), "fallback-for", "fallback");
+ return ok(node, "plan");
+ },
+
+ async "role.todo"(
+ plan: string,
+ task?: string,
+ id?: string,
+ alias?: readonly string[]
+ ): Promise {
+ validateGherkin(task);
+ const node = await rt.create(await resolve(plan), C.task, task, id, alias);
+ return ok(node, "todo");
+ },
+
+ async "role.finish"(task: string, individual: string, encounter?: string): Promise {
+ validateGherkin(encounter);
+ const taskNode = await resolve(task);
+ await rt.tag(taskNode, "done");
+ if (encounter) {
+ const encId = taskNode.id ? `${taskNode.id}-finished` : undefined;
+ const enc = await rt.create(await resolve(individual), C.encounter, encounter, encId);
+ return ok(enc, "finish");
+ }
+ return ok(taskNode, "finish");
+ },
+
+ async "role.complete"(plan: string, individual: string, encounter?: string): Promise {
+ validateGherkin(encounter);
+ const planNode = await resolve(plan);
+ await rt.tag(planNode, "done");
+ const encId = planNode.id ? `${planNode.id}-completed` : undefined;
+ const enc = await rt.create(await resolve(individual), C.encounter, encounter, encId);
+ return ok(enc, "complete");
+ },
+
+ async "role.abandon"(plan: string, individual: string, encounter?: string): Promise {
+ validateGherkin(encounter);
+ const planNode = await resolve(plan);
+ await rt.tag(planNode, "abandoned");
+ const encId = planNode.id ? `${planNode.id}-abandoned` : undefined;
+ const enc = await rt.create(await resolve(individual), C.encounter, encounter, encId);
+ return ok(enc, "abandon");
+ },
+
+ // ---- Role: cognition ----
+
+ async "role.reflect"(
+ encounter: string | undefined,
+ individual: string,
+ experience?: string,
+ id?: string
+ ): Promise {
+ validateGherkin(experience);
+ if (encounter) {
+ const encNode = await resolve(encounter);
+ const exp = await rt.create(
+ await resolve(individual),
+ C.experience,
+ experience || encNode.information,
+ id
+ );
+ await rt.remove(encNode);
+ return ok(exp, "reflect");
+ }
+ // Direct creation — no encounter to consume
+ const exp = await rt.create(await resolve(individual), C.experience, experience, id);
+ return ok(exp, "reflect");
+ },
+
+ async "role.realize"(
+ experience: string | undefined,
+ individual: string,
+ principle?: string,
+ id?: string
+ ): Promise {
+ validateGherkin(principle);
+ if (experience) {
+ const expNode = await resolve(experience);
+ const prin = await rt.create(
+ await resolve(individual),
+ C.principle,
+ principle || expNode.information,
+ id
+ );
+ await rt.remove(expNode);
+ return ok(prin, "realize");
+ }
+ // Direct creation — no experience to consume
+ const prin = await rt.create(await resolve(individual), C.principle, principle, id);
+ return ok(prin, "realize");
+ },
+
+ async "role.master"(
+ individual: string,
+ procedure: string,
+ id?: string,
+ experience?: string
+ ): Promise {
+ validateGherkin(procedure);
+ const parent = await resolve(individual);
+ if (id) await removeExisting(parent, id);
+ const proc = await rt.create(parent, C.procedure, procedure, id);
+ if (experience) await rt.remove(await resolve(experience));
+ return ok(proc, "master");
+ },
+
+ // ---- Role: knowledge management ----
+
+ async "role.forget"(nodeId: string): Promise {
+ const node = await resolve(nodeId);
+ await rt.remove(node);
+ return { state: { ...node, children: [] }, process: "forget" };
+ },
+
+ // ---- Role: skill ----
+
+ async "role.skill"(locator: string): Promise {
+ const rx = requireResourceX();
+ const content = await rx.ingest(locator);
+ const text = typeof content === "string" ? content : JSON.stringify(content, null, 2);
+ try {
+ const rxm = await rx.info(locator);
+ return `${formatRXM(rxm)}\n\n${text}`;
+ } catch {
+ return text;
+ }
+ },
+
+ // ---- Org ----
+
+ async "org.found"(content?: string, id?: string, alias?: readonly string[]): Promise {
+ validateGherkin(content);
+ const node = await rt.create(society, C.organization, content, id, alias);
+ return ok(node, "found");
+ },
+
+ async "org.charter"(org: string, charter: string, id?: string): Promise {
+ validateGherkin(charter);
+ const node = await rt.create(await resolve(org), C.charter, charter, id);
+ return ok(node, "charter");
+ },
+
+ async "org.dissolve"(org: string): Promise {
+ return archive(await resolve(org), "dissolve");
+ },
+
+ async "org.hire"(org: string, individual: string): Promise {
+ const orgNode = await resolve(org);
+ await rt.link(orgNode, await resolve(individual), "membership", "belong");
+ return ok(orgNode, "hire");
+ },
+
+ async "org.fire"(org: string, individual: string): Promise {
+ const orgNode = await resolve(org);
+ await rt.unlink(orgNode, await resolve(individual), "membership", "belong");
+ return ok(orgNode, "fire");
+ },
+
+ // ---- Position ----
+
+ async "position.establish"(
+ content?: string,
+ id?: string,
+ alias?: readonly string[]
+ ): Promise {
+ validateGherkin(content);
+ const node = await rt.create(society, C.position, content, id, alias);
+ return ok(node, "establish");
+ },
+
+ async "position.charge"(position: string, duty: string, id?: string): Promise {
+ validateGherkin(duty);
+ const node = await rt.create(await resolve(position), C.duty, duty, id);
+ return ok(node, "charge");
+ },
+
+ async "position.require"(position: string, procedure: string, id?: string): Promise {
+ validateGherkin(procedure);
+ const parent = await resolve(position);
+ if (id) await removeExisting(parent, id);
+ const node = await rt.create(parent, C.requirement, procedure, id);
+ return ok(node, "require");
+ },
+
+ async "position.abolish"(position: string): Promise {
+ return archive(await resolve(position), "abolish");
+ },
+
+ async "position.appoint"(position: string, individual: string): Promise {
+ const posNode = await resolve(position);
+ const indNode = await resolve(individual);
+ await rt.link(posNode, indNode, "appointment", "serve");
+
+ // Auto-train: copy position requirements as individual procedures
+ const posState = await rt.project(posNode);
+ for (const child of posState.children ?? []) {
+ if (child.name !== "requirement" || !child.information) continue;
+ // rt.create is idempotent for same parent + same id
+ await rt.create(indNode, C.procedure, child.information, child.id);
+ }
+
+ return ok(posNode, "appoint");
+ },
+
+ async "position.dismiss"(position: string, individual: string): Promise {
+ const posNode = await resolve(position);
+ await rt.unlink(posNode, await resolve(individual), "appointment", "serve");
+ return ok(posNode, "dismiss");
+ },
+
+ // ---- Census ----
+
+ async "census.list"(type?: string): Promise {
+ const target = type === "past" ? past : society;
+ const state = await rt.project(target);
+ const children = state.children ?? [];
+ const filtered =
+ type === "past"
+ ? children
+ : children.filter((c) => (type ? c.name === type : c.name !== "past"));
+ if (filtered.length === 0) {
+ return type ? `No ${type} found.` : "Society is empty.";
+ }
+
+ // If filtering by type, use simple flat rendering
+ if (type) {
+ const lines: string[] = [];
+ for (const item of filtered) {
+ const tag = item.tag ? ` #${item.tag}` : "";
+ const alias = item.alias?.length ? ` (${item.alias.join(", ")})` : "";
+ lines.push(`${item.id ?? "(no id)"}${alias}${tag}`);
+ }
+ return lines.join("\n");
+ }
+
+ // Organization-centric tree view
+ const orgs = filtered.filter((c) => c.name === "organization");
+ const individuals = filtered.filter((c) => c.name === "individual");
+
+ // Build a set of individuals who belong to an org
+ const affiliatedIndividuals = new Set();
+ // Build a map: individual id → positions they serve
+ const individualPositions = new Map();
+ for (const ind of individuals) {
+ const serves = ind.links?.filter((l) => l.relation === "serve") ?? [];
+ if (serves.length > 0) {
+ individualPositions.set(
+ ind.id ?? "",
+ serves.map((l) => l.target.id ?? "(no id)")
+ );
+ }
+ }
+
+ const lines: string[] = [];
+
+ for (const org of orgs) {
+ const alias = org.alias?.length ? ` (${org.alias.join(", ")})` : "";
+ const tag = org.tag ? ` #${org.tag}` : "";
+ lines.push(`${org.id}${alias}${tag}`);
+
+ // Members of this org
+ const members = org.links?.filter((l) => l.relation === "membership") ?? [];
+ if (members.length === 0) {
+ lines.push(" (no members)");
+ }
+ for (const m of members) {
+ affiliatedIndividuals.add(m.target.id ?? "");
+ const mAlias = m.target.alias?.length ? ` (${m.target.alias.join(", ")})` : "";
+ const mTag = m.target.tag ? ` #${m.target.tag}` : "";
+ const posLabels = individualPositions.get(m.target.id ?? "");
+ const posStr = posLabels?.length ? ` — ${posLabels.join(", ")}` : "";
+ lines.push(` ${m.target.id}${mAlias}${mTag}${posStr}`);
+ }
+ lines.push("");
+ }
+
+ // Unaffiliated individuals
+ const unaffiliated = individuals.filter((ind) => !affiliatedIndividuals.has(ind.id ?? ""));
+ if (unaffiliated.length > 0) {
+ lines.push("─── unaffiliated ───");
+ for (const ind of unaffiliated) {
+ const alias = ind.alias?.length ? ` (${ind.alias.join(", ")})` : "";
+ const tag = ind.tag ? ` #${ind.tag}` : "";
+ const posLabels = individualPositions.get(ind.id ?? "");
+ const posStr = posLabels?.length ? ` — ${posLabels.join(", ")}` : "";
+ lines.push(` ${ind.id}${alias}${tag}${posStr}`);
+ }
+ }
+
+ return lines.join("\n");
+ },
+
+ // ---- Prototype ----
+
+ async "prototype.settle"(source: string): Promise {
+ const rx = requireResourceX();
+ if (!ctx.prototype) throw new Error("Prototype registry is not available.");
+ if (!ctx.direct) throw new Error("Direct dispatch is not available.");
+
+ // Ingest the prototype resource — type resolver resolves @filename references
+ const result = await rx.ingest<{
+ id: string;
+ instructions: Array<{ op: string; args: Record }>;
+ }>(source);
+
+ // Execute each instruction
+ for (const instr of result.instructions) {
+ await ctx.direct(instr.op, instr.args);
+ }
+
+ // Register in prototype registry
+ ctx.prototype.settle(result.id, source);
+
+ return `Prototype "${result.id}" settled (${result.instructions.length} instructions).`;
+ },
+
+ // ---- Resource (proxy to ResourceX) ----
+
+ "resource.add"(path: string) {
+ return requireResourceX().add(path);
+ },
+
+ "resource.search"(query?: string) {
+ return requireResourceX().search(query);
+ },
+
+ "resource.has"(locator: string) {
+ return requireResourceX().has(locator);
+ },
+
+ "resource.info"(locator: string) {
+ return requireResourceX().info(locator);
+ },
+
+ "resource.remove"(locator: string) {
+ return requireResourceX().remove(locator);
+ },
+
+ "resource.push"(locator: string, options?: { registry?: string }) {
+ return requireResourceX().push(locator, options);
+ },
+
+ "resource.pull"(locator: string, options?: { registry?: string }) {
+ return requireResourceX().pull(locator, options);
+ },
+
+ "resource.clearCache"(registry?: string) {
+ return requireResourceX().clearCache(registry);
+ },
+ };
+}
+
+// ================================================================
+// Helpers
+// ================================================================
+
+function formatRXM(rxm: any): string {
+ const lines: string[] = [`--- RXM: ${rxm.locator} ---`];
+ const def = rxm.definition;
+ if (def) {
+ if (def.author) lines.push(`Author: ${def.author}`);
+ if (def.description) lines.push(`Description: ${def.description}`);
+ }
+ const source = rxm.source;
+ if (source?.files) {
+ lines.push("Files:");
+ lines.push(renderFileTree(source.files, " "));
+ }
+ lines.push("---");
+ return lines.join("\n");
+}
+
+function renderFileTree(files: Record, indent = ""): string {
+ const lines: string[] = [];
+ for (const [name, value] of Object.entries(files)) {
+ if (value && typeof value === "object" && !("size" in value)) {
+ lines.push(`${indent}${name}`);
+ lines.push(renderFileTree(value, `${indent} `));
+ } else {
+ const size = value?.size ? ` (${value.size} bytes)` : "";
+ lines.push(`${indent}${name}${size}`);
+ }
+ }
+ return lines.filter(Boolean).join("\n");
+}
diff --git a/packages/prototype/src/schema.ts b/packages/prototype/src/schema.ts
new file mode 100644
index 0000000..40ba3d4
--- /dev/null
+++ b/packages/prototype/src/schema.ts
@@ -0,0 +1,35 @@
+/**
+ * Schema types for RoleX instruction definitions.
+ *
+ * These types define the structure of every RoleX operation:
+ * parameter types, descriptions, and positional arg ordering.
+ */
+
+/** Supported parameter types for instruction definitions. */
+export type ParamType = "string" | "gherkin" | "string[]" | "record";
+
+/** Definition of a single parameter in an instruction. */
+export interface ParamDef {
+ type: ParamType;
+ required: boolean;
+ description: string;
+}
+
+/**
+ * A single positional argument entry.
+ *
+ * - `string` — simple lookup: `args[name]`
+ * - `{ pack: [...] }` — collect named args into an options object;
+ * returns `undefined` if all values are absent.
+ */
+export type ArgEntry = string | { pack: readonly string[] };
+
+/** Full definition of a RoleX instruction (one namespace.method). */
+export interface InstructionDef {
+ namespace: string;
+ method: string;
+ /** Parameter definitions — keyed by param name, used for MCP/CLI schema generation. */
+ params: Record;
+ /** Positional argument order — maps named args to method call positions. */
+ args: readonly ArgEntry[];
+}
diff --git a/packages/prototype/tests/alignment.test.ts b/packages/prototype/tests/alignment.test.ts
new file mode 100644
index 0000000..a1ae8b0
--- /dev/null
+++ b/packages/prototype/tests/alignment.test.ts
@@ -0,0 +1,187 @@
+/**
+ * Alignment test — verify toArgs produces identical results
+ * to the hand-written switch in rolex.ts (L199-288).
+ *
+ * Each test case mirrors one `case` in the original switch,
+ * using the same input args and asserting the same output.
+ */
+import { describe, expect, test } from "bun:test";
+import { toArgs } from "../src/dispatch.js";
+
+describe("alignment with rolex.ts toArgs switch", () => {
+ // ================================================================
+ // individual (L200-212)
+ // ================================================================
+
+ test("individual.born → [content, id, alias]", () => {
+ const a = { content: "Feature: X", id: "sean", alias: ["s"] };
+ expect(toArgs("individual.born", a)).toEqual([a.content, a.id, a.alias]);
+ });
+
+ test("individual.retire → [individual]", () => {
+ const a = { individual: "sean" };
+ expect(toArgs("individual.retire", a)).toEqual([a.individual]);
+ });
+
+ test("individual.die → [individual]", () => {
+ const a = { individual: "sean" };
+ expect(toArgs("individual.die", a)).toEqual([a.individual]);
+ });
+
+ test("individual.rehire → [individual]", () => {
+ const a = { individual: "sean" };
+ expect(toArgs("individual.rehire", a)).toEqual([a.individual]);
+ });
+
+ test("individual.teach → [individual, content, id]", () => {
+ const a = { individual: "sean", content: "Feature: P", id: "p1" };
+ expect(toArgs("individual.teach", a)).toEqual([a.individual, a.content, a.id]);
+ });
+
+ test("individual.train → [individual, content, id]", () => {
+ const a = { individual: "sean", content: "Feature: Proc", id: "proc1" };
+ expect(toArgs("individual.train", a)).toEqual([a.individual, a.content, a.id]);
+ });
+
+ // ================================================================
+ // org (L214-224)
+ // ================================================================
+
+ test("org.found → [content, id, alias]", () => {
+ const a = { content: "Feature: Org", id: "rolex", alias: ["rx"] };
+ expect(toArgs("org.found", a)).toEqual([a.content, a.id, a.alias]);
+ });
+
+ test("org.charter → [org, content]", () => {
+ const a = { org: "rolex", content: "Feature: Charter" };
+ expect(toArgs("org.charter", a)).toEqual([a.org, a.content]);
+ });
+
+ test("org.dissolve → [org]", () => {
+ const a = { org: "rolex" };
+ expect(toArgs("org.dissolve", a)).toEqual([a.org]);
+ });
+
+ test("org.hire → [org, individual]", () => {
+ const a = { org: "rolex", individual: "sean" };
+ expect(toArgs("org.hire", a)).toEqual([a.org, a.individual]);
+ });
+
+ test("org.fire → [org, individual]", () => {
+ const a = { org: "rolex", individual: "sean" };
+ expect(toArgs("org.fire", a)).toEqual([a.org, a.individual]);
+ });
+
+ // ================================================================
+ // position (L226-238)
+ // ================================================================
+
+ test("position.establish → [content, id, alias]", () => {
+ const a = { content: "Feature: Pos", id: "dev", alias: ["developer"] };
+ expect(toArgs("position.establish", a)).toEqual([a.content, a.id, a.alias]);
+ });
+
+ test("position.charge → [position, content, id]", () => {
+ const a = { position: "dev", content: "Feature: Duty", id: "d1" };
+ expect(toArgs("position.charge", a)).toEqual([a.position, a.content, a.id]);
+ });
+
+ test("position.require → [position, content, id]", () => {
+ const a = { position: "dev", content: "Feature: Req", id: "r1" };
+ expect(toArgs("position.require", a)).toEqual([a.position, a.content, a.id]);
+ });
+
+ test("position.abolish → [position]", () => {
+ const a = { position: "dev" };
+ expect(toArgs("position.abolish", a)).toEqual([a.position]);
+ });
+
+ test("position.appoint → [position, individual]", () => {
+ const a = { position: "dev", individual: "sean" };
+ expect(toArgs("position.appoint", a)).toEqual([a.position, a.individual]);
+ });
+
+ test("position.dismiss → [position, individual]", () => {
+ const a = { position: "dev", individual: "sean" };
+ expect(toArgs("position.dismiss", a)).toEqual([a.position, a.individual]);
+ });
+
+ // ================================================================
+ // census (L240-242)
+ // ================================================================
+
+ test("census.list → [type]", () => {
+ const a = { type: "individual" };
+ expect(toArgs("census.list", a)).toEqual([a.type]);
+ });
+
+ // ================================================================
+ // prototype (L244-266)
+ // ================================================================
+
+ test("prototype.settle → [source]", () => {
+ const a = { source: "./packages/genesis" };
+ expect(toArgs("prototype.settle", a)).toEqual([a.source]);
+ });
+
+ test("prototype.evict → [id]", () => {
+ const a = { id: "nuwa" };
+ expect(toArgs("prototype.evict", a)).toEqual([a.id]);
+ });
+
+ // ================================================================
+ // resource (L268-284)
+ // ================================================================
+
+ test("resource.add → [path]", () => {
+ const a = { path: "/tmp/res" };
+ expect(toArgs("resource.add", a)).toEqual([a.path]);
+ });
+
+ test("resource.search → [query]", () => {
+ const a = { query: "hello" };
+ expect(toArgs("resource.search", a)).toEqual([a.query]);
+ });
+
+ test("resource.has → [locator]", () => {
+ const a = { locator: "deepractice/hello" };
+ expect(toArgs("resource.has", a)).toEqual([a.locator]);
+ });
+
+ test("resource.info → [locator]", () => {
+ const a = { locator: "deepractice/hello" };
+ expect(toArgs("resource.info", a)).toEqual([a.locator]);
+ });
+
+ test("resource.remove → [locator]", () => {
+ const a = { locator: "deepractice/hello" };
+ expect(toArgs("resource.remove", a)).toEqual([a.locator]);
+ });
+
+ test("resource.push with registry → [locator, { registry }]", () => {
+ const a = { locator: "x", registry: "https://r.io" };
+ // rolex.ts: [a.locator, a.registry ? { registry: a.registry } : undefined]
+ expect(toArgs("resource.push", a)).toEqual(["x", { registry: "https://r.io" }]);
+ });
+
+ test("resource.push without registry → [locator, undefined]", () => {
+ const a = { locator: "x" };
+ // rolex.ts: [a.locator, a.registry ? { registry: a.registry } : undefined] → ["x", undefined]
+ expect(toArgs("resource.push", a)).toEqual(["x", undefined]);
+ });
+
+ test("resource.pull with registry → [locator, { registry }]", () => {
+ const a = { locator: "x", registry: "https://r.io" };
+ expect(toArgs("resource.pull", a)).toEqual(["x", { registry: "https://r.io" }]);
+ });
+
+ test("resource.pull without registry → [locator, undefined]", () => {
+ const a = { locator: "x" };
+ expect(toArgs("resource.pull", a)).toEqual(["x", undefined]);
+ });
+
+ test("resource.clearCache → [registry]", () => {
+ const a = { registry: "https://r.io" };
+ expect(toArgs("resource.clearCache", a)).toEqual([a.registry]);
+ });
+});
diff --git a/packages/prototype/tests/descriptions.test.ts b/packages/prototype/tests/descriptions.test.ts
new file mode 100644
index 0000000..64ad03c
--- /dev/null
+++ b/packages/prototype/tests/descriptions.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, test } from "bun:test";
+import { processes, world } from "../src/descriptions/index.js";
+
+describe("descriptions", () => {
+ test("processes is non-empty", () => {
+ expect(Object.keys(processes).length).toBeGreaterThan(0);
+ });
+
+ test("world is non-empty", () => {
+ expect(Object.keys(world).length).toBeGreaterThan(0);
+ });
+
+ test("every process description starts with Feature:", () => {
+ for (const [_name, content] of Object.entries(processes)) {
+ expect(content).toMatch(/^Feature:/);
+ }
+ });
+
+ test("every world description starts with Feature:", () => {
+ for (const [_name, content] of Object.entries(world)) {
+ expect(content).toMatch(/^Feature:/);
+ }
+ });
+
+ test("key role operations have process descriptions", () => {
+ const expected = [
+ "activate",
+ "focus",
+ "want",
+ "plan",
+ "todo",
+ "finish",
+ "complete",
+ "abandon",
+ "reflect",
+ "realize",
+ "master",
+ "forget",
+ "skill",
+ ];
+ for (const name of expected) {
+ expect(processes[name]).toBeDefined();
+ }
+ });
+
+ test("key world features are present", () => {
+ const expected = [
+ "cognitive-priority",
+ "role-identity",
+ "nuwa",
+ "execution",
+ "cognition",
+ "memory",
+ "gherkin",
+ "communication",
+ "skill-system",
+ "state-origin",
+ ];
+ for (const name of expected) {
+ expect(world[name]).toBeDefined();
+ }
+ });
+});
diff --git a/packages/prototype/tests/dispatch.test.ts b/packages/prototype/tests/dispatch.test.ts
new file mode 100644
index 0000000..63b0ebe
--- /dev/null
+++ b/packages/prototype/tests/dispatch.test.ts
@@ -0,0 +1,137 @@
+import { describe, expect, test } from "bun:test";
+import { toArgs } from "../src/dispatch.js";
+
+describe("toArgs", () => {
+ // ---- Simple mapping ----
+
+ test("individual.born — content, id, alias", () => {
+ expect(toArgs("individual.born", { content: "Feature: X", id: "sean", alias: ["s"] })).toEqual([
+ "Feature: X",
+ "sean",
+ ["s"],
+ ]);
+ });
+
+ test("individual.born — missing optional args produce undefined", () => {
+ expect(toArgs("individual.born", {})).toEqual([undefined, undefined, undefined]);
+ });
+
+ test("individual.teach — individual, content, id", () => {
+ expect(
+ toArgs("individual.teach", { individual: "sean", content: "Feature: P", id: "p1" })
+ ).toEqual(["sean", "Feature: P", "p1"]);
+ });
+
+ test("org.hire — org, individual", () => {
+ expect(toArgs("org.hire", { org: "rolex", individual: "sean" })).toEqual(["rolex", "sean"]);
+ });
+
+ test("census.list — type", () => {
+ expect(toArgs("census.list", { type: "individual" })).toEqual(["individual"]);
+ });
+
+ test("prototype.settle — source", () => {
+ expect(toArgs("prototype.settle", { source: "./packages/genesis" })).toEqual([
+ "./packages/genesis",
+ ]);
+ });
+
+ // ---- Role instructions ----
+
+ test("role.activate — individual", () => {
+ expect(toArgs("role.activate", { individual: "sean" })).toEqual(["sean"]);
+ });
+
+ test("role.want — individual, goal, id, alias", () => {
+ expect(toArgs("role.want", { individual: "sean", goal: "Feature: G", id: "g1" })).toEqual([
+ "sean",
+ "Feature: G",
+ "g1",
+ undefined,
+ ]);
+ });
+
+ test("role.plan — goal, plan, id, after, fallback", () => {
+ expect(toArgs("role.plan", { goal: "g1", plan: "Feature: P", id: "p1", after: "p0" })).toEqual([
+ "g1",
+ "Feature: P",
+ "p1",
+ "p0",
+ undefined,
+ ]);
+ });
+
+ test("role.finish — task, individual, encounter", () => {
+ expect(
+ toArgs("role.finish", { task: "t1", individual: "sean", encounter: "Feature: E" })
+ ).toEqual(["t1", "sean", "Feature: E"]);
+ });
+
+ test("role.master — individual, procedure, id, experience", () => {
+ expect(
+ toArgs("role.master", { individual: "sean", procedure: "Feature: Proc", id: "proc1" })
+ ).toEqual(["sean", "Feature: Proc", "proc1", undefined]);
+ });
+
+ // ---- Pack mapping (resource.push / resource.pull) ----
+
+ test("resource.push — with registry packs into options object", () => {
+ expect(toArgs("resource.push", { locator: "x", registry: "https://r.io" })).toEqual([
+ "x",
+ { registry: "https://r.io" },
+ ]);
+ });
+
+ test("resource.push — without registry produces undefined", () => {
+ expect(toArgs("resource.push", { locator: "x" })).toEqual(["x", undefined]);
+ });
+
+ test("resource.pull — with registry packs into options object", () => {
+ expect(toArgs("resource.pull", { locator: "x", registry: "https://r.io" })).toEqual([
+ "x",
+ { registry: "https://r.io" },
+ ]);
+ });
+
+ test("resource.pull — without registry produces undefined", () => {
+ expect(toArgs("resource.pull", { locator: "x" })).toEqual(["x", undefined]);
+ });
+
+ // ---- Simple resource mapping ----
+
+ test("resource.add — path", () => {
+ expect(toArgs("resource.add", { path: "/tmp/res" })).toEqual(["/tmp/res"]);
+ });
+
+ test("resource.clearCache — registry", () => {
+ expect(toArgs("resource.clearCache", { registry: "https://r.io" })).toEqual(["https://r.io"]);
+ });
+
+ // ---- Required param validation ----
+
+ test("missing required param throws", () => {
+ expect(() => toArgs("position.require", { position: "arch" })).toThrow(
+ 'Missing required argument "content" for position.require.'
+ );
+ });
+
+ test("missing required param — org.charter without content", () => {
+ expect(() => toArgs("org.charter", { org: "deepractice" })).toThrow(
+ 'Missing required argument "content" for org.charter.'
+ );
+ });
+
+ test("optional params can be omitted", () => {
+ expect(toArgs("position.require", { position: "arch", content: "Feature: X" })).toEqual([
+ "arch",
+ "Feature: X",
+ undefined,
+ ]);
+ });
+
+ // ---- Error handling ----
+
+ test("unknown instruction throws", () => {
+ expect(() => toArgs("unknown.op", {})).toThrow('Unknown instruction "unknown.op"');
+ });
+});
diff --git a/packages/prototype/tests/instructions.test.ts b/packages/prototype/tests/instructions.test.ts
new file mode 100644
index 0000000..fb8ae7b
--- /dev/null
+++ b/packages/prototype/tests/instructions.test.ts
@@ -0,0 +1,94 @@
+import { describe, expect, test } from "bun:test";
+import { instructions } from "../src/instructions.js";
+
+describe("instructions registry", () => {
+ test("all expected namespaces are present", () => {
+ const namespaces = new Set(Object.values(instructions).map((d) => d.namespace));
+ expect(namespaces).toEqual(
+ new Set(["individual", "role", "org", "position", "census", "prototype", "resource"])
+ );
+ });
+
+ test("individual — 6 methods", () => {
+ const methods = methodsOf("individual");
+ expect(methods).toEqual(["born", "retire", "die", "rehire", "teach", "train"]);
+ });
+
+ test("role — 13 methods", () => {
+ const methods = methodsOf("role");
+ expect(methods).toEqual([
+ "activate",
+ "focus",
+ "want",
+ "plan",
+ "todo",
+ "finish",
+ "complete",
+ "abandon",
+ "reflect",
+ "realize",
+ "master",
+ "forget",
+ "skill",
+ ]);
+ });
+
+ test("org — 5 methods", () => {
+ const methods = methodsOf("org");
+ expect(methods).toEqual(["found", "charter", "dissolve", "hire", "fire"]);
+ });
+
+ test("position — 6 methods", () => {
+ const methods = methodsOf("position");
+ expect(methods).toEqual(["establish", "charge", "require", "abolish", "appoint", "dismiss"]);
+ });
+
+ test("census — 1 method", () => {
+ expect(methodsOf("census")).toEqual(["list"]);
+ });
+
+ test("prototype — 2 methods", () => {
+ const methods = methodsOf("prototype");
+ expect(methods).toEqual(["settle", "evict"]);
+ });
+
+ test("resource — 8 methods", () => {
+ const methods = methodsOf("resource");
+ expect(methods).toEqual([
+ "add",
+ "search",
+ "has",
+ "info",
+ "remove",
+ "push",
+ "pull",
+ "clearCache",
+ ]);
+ });
+
+ test("total instruction count is 41", () => {
+ expect(Object.keys(instructions).length).toBe(41);
+ });
+
+ test("every instruction has matching namespace.method key", () => {
+ for (const [key, def] of Object.entries(instructions)) {
+ expect(key).toBe(`${def.namespace}.${def.method}`);
+ }
+ });
+
+ test("every instruction has at least one arg entry or zero params", () => {
+ for (const [_key, def] of Object.entries(instructions)) {
+ const paramCount = Object.keys(def.params).length;
+ if (paramCount > 0) {
+ expect(def.args.length).toBeGreaterThan(0);
+ }
+ }
+ });
+});
+
+/** Extract methods for a namespace, preserving registry order. */
+function methodsOf(namespace: string): string[] {
+ return Object.values(instructions)
+ .filter((d) => d.namespace === namespace)
+ .map((d) => d.method);
+}
diff --git a/packages/prototype/tests/ops.test.ts b/packages/prototype/tests/ops.test.ts
new file mode 100644
index 0000000..b597959
--- /dev/null
+++ b/packages/prototype/tests/ops.test.ts
@@ -0,0 +1,758 @@
+import { describe, expect, test } from "bun:test";
+import * as C from "@rolexjs/core";
+import { createRuntime, type State, type Structure } from "@rolexjs/system";
+import { createOps, type Ops } from "../src/ops.js";
+
+// ================================================================
+// Test setup — pure in-memory, no platform needed
+// ================================================================
+
+async function setup() {
+ const rt = createRuntime();
+ const society = await rt.create(null, C.society);
+ const past = await rt.create(society, C.past);
+
+ function findInState(state: State, target: string): Structure | null {
+ if (state.id?.toLowerCase() === target) return state;
+ if (state.alias) {
+ for (const a of state.alias) {
+ if (a.toLowerCase() === target) return state;
+ }
+ }
+ for (const child of state.children ?? []) {
+ const found = findInState(child, target);
+ if (found) return found;
+ }
+ return null;
+ }
+
+ async function find(id: string): Promise {
+ const state = await rt.project(society);
+ return findInState(state, id.toLowerCase());
+ }
+
+ async function resolve(id: string): Promise {
+ const node = await find(id);
+ if (!node) throw new Error(`"${id}" not found.`);
+ return node;
+ }
+
+ const ops = createOps({ rt, society, past, resolve, find });
+ return { rt, society, past, ops, find };
+}
+
+// ================================================================
+// Individual
+// ================================================================
+
+describe("individual", () => {
+ test("born creates individual with identity scaffold", async () => {
+ const { ops } = await setup();
+ const r = await ops["individual.born"]("Feature: Sean", "sean");
+ expect(r.state.name).toBe("individual");
+ expect(r.state.id).toBe("sean");
+ expect(r.state.information).toBe("Feature: Sean");
+ expect(r.process).toBe("born");
+ const names = r.state.children!.map((c: State) => c.name);
+ expect(names).toContain("identity");
+ });
+
+ test("born without content creates minimal individual", async () => {
+ const { ops } = await setup();
+ const r = await ops["individual.born"](undefined, "alice");
+ expect(r.state.name).toBe("individual");
+ expect(r.state.id).toBe("alice");
+ });
+
+ test("born with alias", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"]("Feature: Sean", "sean", ["姜山"]);
+ expect(await find("姜山")).not.toBeNull();
+ });
+
+ test("born rejects invalid Gherkin", async () => {
+ const { ops } = await setup();
+ await expect(ops["individual.born"]("not gherkin")).rejects.toThrow("Invalid Gherkin");
+ });
+
+ test("born uses distinct identity id", async () => {
+ const { ops } = await setup();
+ const r = await ops["individual.born"]("Feature: Sean", "sean");
+ const identity = r.state.children!.find((c: State) => c.name === "identity");
+ expect(identity!.id).toBe("sean-identity");
+ });
+
+ test("same id under different parents is allowed", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["position.establish"](undefined, "architect");
+ await ops["position.require"]("architect", "Feature: System design", "sys-design");
+ await ops["individual.train"]("sean", "Feature: System design skill", "sys-design");
+
+ // Both exist — requirement under position, procedure under individual
+ const sean = (await find("sean"))! as unknown as State;
+ const procs = (sean.children ?? []).filter((c: State) => c.name === "procedure");
+ expect(procs).toHaveLength(1);
+ expect(procs[0].id).toBe("sys-design");
+ });
+
+ test("retire archives individual to past", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"]("Feature: Sean", "sean");
+ const r = await ops["individual.retire"]("sean");
+ expect(r.state.name).toBe("past");
+ expect(r.process).toBe("retire");
+ const found = await find("sean");
+ expect(found).not.toBeNull();
+ expect(found!.name).toBe("past");
+ });
+
+ test("die archives individual", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "alice");
+ const r = await ops["individual.die"]("alice");
+ expect(r.state.name).toBe("past");
+ expect(r.process).toBe("die");
+ });
+
+ test("rehire restores individual from past", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"]("Feature: Sean", "sean");
+ await ops["individual.retire"]("sean");
+ const r = await ops["individual.rehire"]("sean");
+ expect(r.state.name).toBe("individual");
+ expect(r.state.information).toBe("Feature: Sean");
+ const names = r.state.children!.map((c: State) => c.name);
+ expect(names).toContain("identity");
+ });
+
+ test("teach injects principle", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ const r = await ops["individual.teach"]("sean", "Feature: Always test first", "test-first");
+ expect(r.state.name).toBe("principle");
+ expect(r.state.id).toBe("test-first");
+ expect(r.process).toBe("teach");
+ });
+
+ test("teach replaces existing principle with same id", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["individual.teach"]("sean", "Feature: Version 1", "rule");
+ await ops["individual.teach"]("sean", "Feature: Version 2", "rule");
+ const sean = (await find("sean"))!;
+ const state = sean as unknown as State;
+ const principles = (state.children ?? []).filter((c: State) => c.name === "principle");
+ expect(principles).toHaveLength(1);
+ expect(principles[0].information).toBe("Feature: Version 2");
+ });
+
+ test("train injects procedure", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ const r = await ops["individual.train"]("sean", "Feature: Code review skill", "code-review");
+ expect(r.state.name).toBe("procedure");
+ expect(r.state.id).toBe("code-review");
+ expect(r.process).toBe("train");
+ });
+});
+
+// ================================================================
+// Role: execution
+// ================================================================
+
+describe("role: execution", () => {
+ test("want creates goal under individual", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ const r = await ops["role.want"]("sean", "Feature: Build auth", "auth");
+ expect(r.state.name).toBe("goal");
+ expect(r.state.id).toBe("auth");
+ expect(r.state.information).toBe("Feature: Build auth");
+ expect(r.process).toBe("want");
+ });
+
+ test("focus returns goal state", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", "Feature: Auth", "auth");
+ const r = await ops["role.focus"]("auth");
+ expect(r.state.name).toBe("goal");
+ expect(r.state.id).toBe("auth");
+ expect(r.process).toBe("focus");
+ });
+
+ test("plan creates plan under goal", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", "Feature: Auth", "auth");
+ const r = await ops["role.plan"]("auth", "Feature: JWT strategy", "jwt");
+ expect(r.state.name).toBe("plan");
+ expect(r.state.id).toBe("jwt");
+ expect(r.process).toBe("plan");
+ });
+
+ test("plan with after creates sequential link", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", "Feature: Auth", "auth");
+ await ops["role.plan"]("auth", "Feature: Phase 1", "phase-1");
+ await ops["role.plan"]("auth", "Feature: Phase 2", "phase-2", "phase-1");
+
+ const p2 = (await find("phase-2"))! as unknown as State;
+ expect(p2.links).toHaveLength(1);
+ expect(p2.links![0].relation).toBe("after");
+ });
+
+ test("plan with fallback creates alternative link", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", "Feature: Auth", "auth");
+ await ops["role.plan"]("auth", "Feature: Plan A", "plan-a");
+ await ops["role.plan"]("auth", "Feature: Plan B", "plan-b", undefined, "plan-a");
+
+ const pb = (await find("plan-b"))! as unknown as State;
+ expect(pb.links).toHaveLength(1);
+ expect(pb.links![0].relation).toBe("fallback-for");
+ });
+
+ test("todo creates task under plan", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", undefined, "g");
+ await ops["role.plan"]("g", undefined, "p");
+ const r = await ops["role.todo"]("p", "Feature: Write tests", "t1");
+ expect(r.state.name).toBe("task");
+ expect(r.state.id).toBe("t1");
+ expect(r.process).toBe("todo");
+ });
+
+ test("finish tags task done and creates encounter", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", undefined, "g");
+ await ops["role.plan"]("g", undefined, "p");
+ await ops["role.todo"]("p", undefined, "t1");
+
+ const r = await ops["role.finish"](
+ "t1",
+ "sean",
+ "Feature: Task complete\n Scenario: OK\n Given done\n Then ok"
+ );
+ expect(r.state.name).toBe("encounter");
+ expect(r.state.id).toBe("t1-finished");
+ expect(r.process).toBe("finish");
+ expect((await find("t1"))!.tag).toBe("done");
+ });
+
+ test("finish without encounter just tags task done", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", undefined, "g");
+ await ops["role.plan"]("g", undefined, "p");
+ await ops["role.todo"]("p", undefined, "t1");
+
+ const r = await ops["role.finish"]("t1", "sean");
+ expect(r.state.name).toBe("task");
+ expect(r.process).toBe("finish");
+ expect((await find("t1"))!.tag).toBe("done");
+ });
+
+ test("complete tags plan done and creates encounter", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", "Feature: Auth", "auth");
+ await ops["role.plan"]("auth", "Feature: JWT", "jwt");
+
+ const r = await ops["role.complete"](
+ "jwt",
+ "sean",
+ "Feature: Done\n Scenario: OK\n Given done\n Then ok"
+ );
+ expect(r.state.name).toBe("encounter");
+ expect(r.state.id).toBe("jwt-completed");
+ expect(r.process).toBe("complete");
+ expect((await find("jwt"))!.tag).toBe("done");
+ });
+
+ test("abandon tags plan abandoned and creates encounter", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", "Feature: Auth", "auth");
+ await ops["role.plan"]("auth", "Feature: JWT", "jwt");
+
+ const r = await ops["role.abandon"](
+ "jwt",
+ "sean",
+ "Feature: Abandoned\n Scenario: No time\n Given no time\n Then abandon"
+ );
+ expect(r.state.name).toBe("encounter");
+ expect(r.state.id).toBe("jwt-abandoned");
+ expect(r.process).toBe("abandon");
+ expect((await find("jwt"))!.tag).toBe("abandoned");
+ });
+});
+
+// ================================================================
+// Role: cognition
+// ================================================================
+
+describe("role: cognition", () => {
+ /** Helper: born → want → plan → todo → finish with encounter. */
+ async function withEncounter(ops: Ops) {
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", undefined, "g");
+ await ops["role.plan"]("g", undefined, "p");
+ await ops["role.todo"]("p", undefined, "t1");
+ await ops["role.finish"](
+ "t1",
+ "sean",
+ "Feature: Encounter\n Scenario: OK\n Given x\n Then y"
+ );
+ }
+
+ test("reflect: encounter → experience", async () => {
+ const { ops, find } = await setup();
+ await withEncounter(ops);
+ const r = await ops["role.reflect"](
+ "t1-finished",
+ "sean",
+ "Feature: Insight\n Scenario: Learned\n Given x\n Then y",
+ "insight-1"
+ );
+ expect(r.state.name).toBe("experience");
+ expect(r.state.id).toBe("insight-1");
+ expect(r.process).toBe("reflect");
+ // encounter consumed
+ expect(await find("t1-finished")).toBeNull();
+ });
+
+ test("reflect without explicit experience uses encounter content", async () => {
+ const { ops } = await setup();
+ await withEncounter(ops);
+ const r = await ops["role.reflect"]("t1-finished", "sean", undefined, "exp-1");
+ expect(r.state.name).toBe("experience");
+ expect(r.state.information).toContain("Feature: Encounter");
+ });
+
+ test("realize: experience → principle", async () => {
+ const { ops, find } = await setup();
+ await withEncounter(ops);
+ await ops["role.reflect"]("t1-finished", "sean", "Feature: Insight", "exp-1");
+
+ const r = await ops["role.realize"](
+ "exp-1",
+ "sean",
+ "Feature: Always validate\n Scenario: Rule\n Given validate\n Then safe",
+ "validate-rule"
+ );
+ expect(r.state.name).toBe("principle");
+ expect(r.state.id).toBe("validate-rule");
+ expect(r.process).toBe("realize");
+ // experience consumed
+ expect(await find("exp-1")).toBeNull();
+ });
+
+ test("master from experience: experience → procedure", async () => {
+ const { ops, find } = await setup();
+ await withEncounter(ops);
+ await ops["role.reflect"]("t1-finished", "sean", "Feature: Insight", "exp-1");
+
+ const r = await ops["role.master"](
+ "sean",
+ "Feature: JWT mastery\n Scenario: Apply\n Given jwt\n Then master",
+ "jwt-skill",
+ "exp-1"
+ );
+ expect(r.state.name).toBe("procedure");
+ expect(r.state.id).toBe("jwt-skill");
+ expect(r.process).toBe("master");
+ // experience consumed
+ expect(await find("exp-1")).toBeNull();
+ });
+
+ test("master without experience: direct procedure creation", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ const r = await ops["role.master"]("sean", "Feature: Direct skill", "direct-skill");
+ expect(r.state.name).toBe("procedure");
+ expect(r.state.id).toBe("direct-skill");
+ });
+
+ test("master replaces existing procedure with same id", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.master"]("sean", "Feature: V1", "skill");
+ await ops["role.master"]("sean", "Feature: V2", "skill");
+ const sean = (await find("sean"))! as unknown as State;
+ const procs = (sean.children ?? []).filter((c: State) => c.name === "procedure");
+ expect(procs).toHaveLength(1);
+ expect(procs[0].information).toBe("Feature: V2");
+ });
+});
+
+// ================================================================
+// Role: knowledge management
+// ================================================================
+
+describe("role: forget", () => {
+ test("forget removes a node", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", "Feature: Auth", "auth");
+ const r = await ops["role.forget"]("auth");
+ expect(r.process).toBe("forget");
+ expect(await find("auth")).toBeNull();
+ });
+
+ test("forget removes node and its subtree", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", undefined, "g");
+ await ops["role.plan"]("g", undefined, "p");
+ await ops["role.todo"]("p", undefined, "t1");
+ await ops["role.forget"]("g");
+ expect(await find("g")).toBeNull();
+ expect(await find("p")).toBeNull();
+ expect(await find("t1")).toBeNull();
+ });
+
+ test("forget throws on non-existent node", async () => {
+ const { ops } = await setup();
+ await expect(ops["role.forget"]("nope")).rejects.toThrow();
+ });
+});
+
+// ================================================================
+// Organization
+// ================================================================
+
+describe("org", () => {
+ test("found creates organization", async () => {
+ const { ops } = await setup();
+ const r = await ops["org.found"]("Feature: Deepractice", "dp");
+ expect(r.state.name).toBe("organization");
+ expect(r.state.id).toBe("dp");
+ expect(r.process).toBe("found");
+ });
+
+ test("charter sets org mission", async () => {
+ const { ops } = await setup();
+ await ops["org.found"](undefined, "dp");
+ const r = await ops["org.charter"]("dp", "Feature: Build great AI");
+ expect(r.state.name).toBe("charter");
+ expect(r.state.information).toBe("Feature: Build great AI");
+ });
+
+ test("dissolve archives organization", async () => {
+ const { ops, find } = await setup();
+ await ops["org.found"](undefined, "dp");
+ const r = await ops["org.dissolve"]("dp");
+ expect(r.process).toBe("dissolve");
+ expect((await find("dp"))!.name).toBe("past");
+ });
+
+ test("hire links individual to org", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["org.found"](undefined, "dp");
+ const r = await ops["org.hire"]("dp", "sean");
+ expect(r.state.links).toHaveLength(1);
+ expect(r.state.links![0].relation).toBe("membership");
+ });
+
+ test("fire removes membership", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["org.found"](undefined, "dp");
+ await ops["org.hire"]("dp", "sean");
+ const r = await ops["org.fire"]("dp", "sean");
+ expect(r.state.links).toBeUndefined();
+ });
+});
+
+// ================================================================
+// Position
+// ================================================================
+
+describe("position", () => {
+ test("establish creates position", async () => {
+ const { ops } = await setup();
+ const r = await ops["position.establish"]("Feature: Architect", "architect");
+ expect(r.state.name).toBe("position");
+ expect(r.state.id).toBe("architect");
+ expect(r.process).toBe("establish");
+ });
+
+ test("charge adds duty", async () => {
+ const { ops } = await setup();
+ await ops["position.establish"](undefined, "architect");
+ const r = await ops["position.charge"]("architect", "Feature: Design systems", "design");
+ expect(r.state.name).toBe("duty");
+ expect(r.state.id).toBe("design");
+ });
+
+ test("require adds required skill", async () => {
+ const { ops } = await setup();
+ await ops["position.establish"](undefined, "architect");
+ const r = await ops["position.require"]("architect", "Feature: System design", "sys-design");
+ expect(r.state.name).toBe("requirement");
+ expect(r.state.id).toBe("sys-design");
+ expect(r.process).toBe("require");
+ });
+
+ test("abolish archives position", async () => {
+ const { ops, find } = await setup();
+ await ops["position.establish"](undefined, "architect");
+ const r = await ops["position.abolish"]("architect");
+ expect(r.process).toBe("abolish");
+ expect((await find("architect"))!.name).toBe("past");
+ });
+
+ test("appoint links individual to position", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["position.establish"](undefined, "architect");
+ const r = await ops["position.appoint"]("architect", "sean");
+ expect(r.state.links).toHaveLength(1);
+ expect(r.state.links![0].relation).toBe("appointment");
+ });
+
+ test("appoint auto-trains requirements as procedures", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["position.establish"](undefined, "architect");
+ await ops["position.require"]("architect", "Feature: System design", "sys-design");
+ await ops["position.require"]("architect", "Feature: Code review", "code-review");
+ await ops["position.appoint"]("architect", "sean");
+
+ const sean = (await find("sean"))! as unknown as State;
+ const procs = (sean.children ?? []).filter((c: State) => c.name === "procedure");
+ expect(procs).toHaveLength(2);
+ expect(procs.map((p: State) => p.id).sort()).toEqual(["code-review", "sys-design"]);
+ expect(procs[0].information).toBeDefined();
+ });
+
+ test("appoint skips already-trained procedures (idempotent)", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["individual.train"]("sean", "Feature: System design skill", "sys-design");
+ await ops["position.establish"](undefined, "architect");
+ await ops["position.require"]("architect", "Feature: System design", "sys-design");
+ await ops["position.appoint"]("architect", "sean");
+
+ const sean = (await find("sean"))! as unknown as State;
+ const procs = (sean.children ?? []).filter((c: State) => c.name === "procedure");
+ // Only 1: the manually trained one (idempotent, not duplicated)
+ expect(procs).toHaveLength(1);
+ });
+
+ test("appoint with no requirements creates no procedures", async () => {
+ const { ops, find } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["position.establish"](undefined, "architect");
+ await ops["position.appoint"]("architect", "sean");
+
+ const sean = (await find("sean"))! as unknown as State;
+ const procs = (sean.children ?? []).filter((c: State) => c.name === "procedure");
+ expect(procs).toHaveLength(0);
+ });
+
+ test("dismiss removes appointment", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["position.establish"](undefined, "architect");
+ await ops["position.appoint"]("architect", "sean");
+ const r = await ops["position.dismiss"]("architect", "sean");
+ expect(r.state.links).toBeUndefined();
+ });
+});
+
+// ================================================================
+// Census
+// ================================================================
+
+describe("census", () => {
+ test("list shows individuals and orgs", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["org.found"](undefined, "dp");
+ const result = await ops["census.list"]();
+ expect(result).toContain("sean");
+ expect(result).toContain("dp");
+ });
+
+ test("list by type filters", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["org.found"](undefined, "dp");
+ const result = await ops["census.list"]("individual");
+ expect(result).toContain("sean");
+ expect(result).not.toContain("dp");
+ });
+
+ test("list empty society", async () => {
+ const { ops } = await setup();
+ const result = await ops["census.list"]();
+ expect(result).toBe("Society is empty.");
+ });
+
+ test("list past entries", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["individual.retire"]("sean");
+ const result = await ops["census.list"]("past");
+ expect(result).toContain("sean");
+ });
+});
+
+// ================================================================
+// Gherkin validation
+// ================================================================
+
+describe("gherkin validation", () => {
+ test("want rejects invalid Gherkin", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await expect(ops["role.want"]("sean", "not gherkin")).rejects.toThrow("Invalid Gherkin");
+ });
+
+ test("plan rejects invalid Gherkin", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", undefined, "g");
+ await expect(ops["role.plan"]("g", "not gherkin")).rejects.toThrow("Invalid Gherkin");
+ });
+
+ test("operations accept undefined content (optional)", async () => {
+ const { ops } = await setup();
+ await ops["individual.born"](undefined, "sean");
+ await expect(ops["role.want"]("sean", undefined, "g")).resolves.toBeDefined();
+ await expect(ops["role.plan"]("g", undefined, "p")).resolves.toBeDefined();
+ await expect(ops["role.todo"]("p", undefined, "t")).resolves.toBeDefined();
+ });
+});
+
+// ================================================================
+// Error handling
+// ================================================================
+
+describe("error handling", () => {
+ test("resolve throws on non-existent id", async () => {
+ const { ops } = await setup();
+ await expect(ops["role.focus"]("no-such-goal")).rejects.toThrow('"no-such-goal" not found');
+ });
+
+ test("role.skill throws without resourcex", async () => {
+ const { ops } = await setup();
+ await expect(ops["role.skill"]("some-locator")).rejects.toThrow("ResourceX is not available");
+ });
+});
+
+// ================================================================
+// Full lifecycle: execution + cognition
+// ================================================================
+
+describe("full lifecycle", () => {
+ test("born → want → plan → todo → finish → complete → reflect → realize", async () => {
+ const { ops, find } = await setup();
+
+ // Setup world
+ await ops["individual.born"]("Feature: Sean", "sean");
+ await ops["org.found"]("Feature: Deepractice", "dp");
+ await ops["position.establish"]("Feature: Architect", "architect");
+ await ops["org.charter"]("dp", "Feature: Build great AI");
+ await ops["position.charge"]("architect", "Feature: Design systems");
+ await ops["org.hire"]("dp", "sean");
+ await ops["position.appoint"]("architect", "sean");
+
+ // Execution cycle
+ await ops["role.want"]("sean", "Feature: Build auth", "build-auth");
+ await ops["role.plan"]("build-auth", "Feature: JWT plan", "jwt-plan");
+ await ops["role.todo"]("jwt-plan", "Feature: Login endpoint", "login");
+ await ops["role.todo"]("jwt-plan", "Feature: Token refresh", "refresh");
+
+ await ops["role.finish"](
+ "login",
+ "sean",
+ "Feature: Login done\n Scenario: OK\n Given login\n Then done"
+ );
+ await ops["role.finish"](
+ "refresh",
+ "sean",
+ "Feature: Refresh done\n Scenario: OK\n Given refresh\n Then done"
+ );
+ await ops["role.complete"](
+ "jwt-plan",
+ "sean",
+ "Feature: Auth plan complete\n Scenario: OK\n Given plan\n Then complete"
+ );
+
+ // Verify tags
+ expect((await find("login"))!.tag).toBe("done");
+ expect((await find("refresh"))!.tag).toBe("done");
+ expect((await find("jwt-plan"))!.tag).toBe("done");
+
+ // Cognition cycle
+ await ops["role.reflect"](
+ "login-finished",
+ "sean",
+ "Feature: Token insight\n Scenario: Learned\n Given token handling\n Then understand refresh",
+ "token-exp"
+ );
+ expect(await find("login-finished")).toBeNull();
+
+ await ops["role.realize"](
+ "token-exp",
+ "sean",
+ "Feature: Always validate expiry\n Scenario: Rule\n Given token\n Then validate expiry",
+ "validate-expiry"
+ );
+ expect(await find("token-exp")).toBeNull();
+
+ // Verify final state
+ const sean = (await find("sean"))! as unknown as State;
+ const principle = (sean.children ?? []).find(
+ (c: State) => c.name === "principle" && c.id === "validate-expiry"
+ );
+ expect(principle).toBeDefined();
+ expect(principle!.information).toContain("Always validate expiry");
+ });
+
+ test("plan → abandon → reflect → master", async () => {
+ const { ops, find } = await setup();
+
+ await ops["individual.born"](undefined, "sean");
+ await ops["role.want"]("sean", "Feature: Learn Rust", "learn-rust");
+ await ops["role.plan"]("learn-rust", "Feature: Book approach", "book-approach");
+
+ await ops["role.abandon"](
+ "book-approach",
+ "sean",
+ "Feature: Too theoretical\n Scenario: Failed\n Given reading\n Then too slow"
+ );
+
+ expect((await find("book-approach"))!.tag).toBe("abandoned");
+
+ await ops["role.reflect"](
+ "book-approach-abandoned",
+ "sean",
+ "Feature: Hands-on works better\n Scenario: Insight\n Given theory vs practice\n Then practice wins",
+ "hands-on-exp"
+ );
+
+ await ops["role.master"](
+ "sean",
+ "Feature: Learn by doing\n Scenario: Apply\n Given new topic\n Then build a project first",
+ "learn-by-doing",
+ "hands-on-exp"
+ );
+
+ expect(await find("hands-on-exp")).toBeNull();
+ const sean = (await find("sean"))! as unknown as State;
+ const proc = (sean.children ?? []).find(
+ (c: State) => c.name === "procedure" && c.id === "learn-by-doing"
+ );
+ expect(proc).toBeDefined();
+ });
+});
diff --git a/packages/resourcex-types/tsconfig.json b/packages/prototype/tsconfig.json
similarity index 83%
rename from packages/resourcex-types/tsconfig.json
rename to packages/prototype/tsconfig.json
index e4afb9a..1f60594 100644
--- a/packages/resourcex-types/tsconfig.json
+++ b/packages/prototype/tsconfig.json
@@ -11,5 +11,5 @@
}
},
"include": ["src/**/*"],
- "exclude": ["node_modules", "dist", "**/*.test.ts"]
+ "exclude": ["node_modules", "dist"]
}
diff --git a/apps/cli/tsup.config.ts b/packages/prototype/tsup.config.ts
similarity index 60%
rename from apps/cli/tsup.config.ts
rename to packages/prototype/tsup.config.ts
index a2b4287..6aef7ae 100644
--- a/apps/cli/tsup.config.ts
+++ b/packages/prototype/tsup.config.ts
@@ -6,7 +6,10 @@ export default defineConfig({
dts: true,
clean: true,
sourcemap: true,
- banner: {
- js: "#!/usr/bin/env node",
+ esbuildOptions(options) {
+ options.loader = {
+ ...options.loader,
+ ".feature": "text",
+ };
},
});
diff --git a/packages/resourcex-types/src/index.ts b/packages/resourcex-types/src/index.ts
deleted file mode 100644
index a6bd623..0000000
--- a/packages/resourcex-types/src/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @rolexjs/resourcex-types — ResourceX type handlers for RoleX.
- *
- * Two types:
- * role — individual prototype (individual.json + *.feature → State)
- * organization — organization prototype (organization.json + *.feature → State)
- *
- * Register with ResourceX:
- * resourcex.supportType(roleType);
- * resourcex.supportType(organizationType);
- */
-
-export { organizationType } from "./organization.js";
-export { roleType } from "./role.js";
diff --git a/packages/resourcex-types/src/organization.ts b/packages/resourcex-types/src/organization.ts
deleted file mode 100644
index 9478906..0000000
--- a/packages/resourcex-types/src/organization.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Organization type for ResourceX.
- *
- * An organization resource contains:
- * - organization.json (manifest)
- * - *.feature (Gherkin content)
- *
- * Resolves to a State tree (plain object) for prototype merging.
- */
-import type { BundledType } from "resourcexjs";
-import { resolverCode } from "./resolver.js";
-
-export const organizationType: BundledType = {
- name: "organization",
- aliases: ["org"],
- description: "RoleX organization prototype — organization manifest + feature files",
- code: resolverCode("organization", "organization.json"),
-};
diff --git a/packages/resourcex-types/src/resolver.ts b/packages/resourcex-types/src/resolver.ts
deleted file mode 100644
index ea7fbae..0000000
--- a/packages/resourcex-types/src/resolver.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Shared resolver logic for role and organization types.
- *
- * This code is inlined as a string in BundledType.code.
- * It parses manifest JSON + .feature files into a State tree.
- *
- * The resolver receives ctx.files (Record) and
- * returns a State object (plain JS object).
- */
-
-/**
- * Generate the resolver code string for a given manifest filename.
- * The code is self-contained — no imports, runs in ResourceX's sandbox.
- */
-export function resolverCode(typeName: string, manifestFile: string): string {
- return `// @resolver: ${typeName}_type_default
-var ${typeName}_type_default = {
- name: "${typeName}",
- async resolve(ctx) {
- var decoder = new TextDecoder();
-
- // Find and parse manifest
- var manifestBuf = ctx.files["${manifestFile}"];
- if (!manifestBuf) {
- throw new Error("${typeName} resource must contain a ${manifestFile} file");
- }
- var manifest = JSON.parse(decoder.decode(manifestBuf));
-
- // Collect .feature file contents
- var features = {};
- for (var name of Object.keys(ctx.files)) {
- if (name.endsWith(".feature")) {
- features[name] = decoder.decode(ctx.files[name]);
- }
- }
-
- // Build State tree from manifest node
- function buildState(id, node) {
- var filename = id + "." + node.type + ".feature";
- var information = features[filename];
- var children = [];
- if (node.children) {
- var entries = Object.entries(node.children);
- for (var i = 0; i < entries.length; i++) {
- children.push(buildState(entries[i][0], entries[i][1]));
- }
- }
- var state = { id: id, name: node.type, description: "", parent: null };
- if (information) state.information = information;
- if (children.length > 0) state.children = children;
- return state;
- }
-
- // Build root State
- var rootFilename = manifest.id + "." + manifest.type + ".feature";
- var rootInformation = features[rootFilename];
- var children = [];
- if (manifest.children) {
- var entries = Object.entries(manifest.children);
- for (var i = 0; i < entries.length; i++) {
- children.push(buildState(entries[i][0], entries[i][1]));
- }
- }
-
- var state = { id: manifest.id, name: manifest.type, description: "", parent: null };
- if (manifest.alias) state.alias = manifest.alias;
- if (rootInformation) state.information = rootInformation;
- if (children.length > 0) state.children = children;
- return state;
- }
-};`;
-}
diff --git a/packages/resourcex-types/src/role.ts b/packages/resourcex-types/src/role.ts
deleted file mode 100644
index 6f01a35..0000000
--- a/packages/resourcex-types/src/role.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Role type for ResourceX.
- *
- * A role resource contains:
- * - individual.json (manifest)
- * - *.feature (Gherkin content)
- *
- * Resolves to a State tree (plain object) for prototype merging.
- */
-import type { BundledType } from "resourcexjs";
-import { resolverCode } from "./resolver.js";
-
-export const roleType: BundledType = {
- name: "role",
- aliases: ["individual"],
- description: "RoleX role prototype — individual manifest + feature files",
- code: resolverCode("role", "individual.json"),
-};
diff --git a/packages/resourcex-types/tsup.config.ts b/packages/resourcex-types/tsup.config.ts
deleted file mode 100644
index faf3167..0000000
--- a/packages/resourcex-types/tsup.config.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { defineConfig } from "tsup";
-
-export default defineConfig({
- entry: ["src/index.ts"],
- format: ["esm"],
- dts: true,
- clean: true,
- sourcemap: true,
-});
diff --git a/packages/rolexjs/package.json b/packages/rolexjs/package.json
index 016ddc9..3d206b6 100644
--- a/packages/rolexjs/package.json
+++ b/packages/rolexjs/package.json
@@ -33,17 +33,17 @@
"dist"
],
"scripts": {
- "gen:desc": "bun run scripts/gen-descriptions.ts",
- "build": "bun run gen:desc && tsup",
+ "build": "tsup",
"lint": "biome lint .",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"@rolexjs/core": "workspace:*",
+ "@rolexjs/prototype": "workspace:*",
"@rolexjs/system": "workspace:*",
"@rolexjs/parser": "workspace:*",
- "resourcexjs": "^2.13.0"
+ "resourcexjs": "^2.14.0"
},
"devDependencies": {
"@rolexjs/local-platform": "workspace:*"
diff --git a/packages/rolexjs/scripts/gen-descriptions.ts b/packages/rolexjs/scripts/gen-descriptions.ts
deleted file mode 100644
index 25593e0..0000000
--- a/packages/rolexjs/scripts/gen-descriptions.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Generate descriptions/index.ts from .feature files.
- *
- * Reads all *.feature files in src/descriptions/ and produces
- * a TypeScript module exporting their content as two Records:
- * - processes: per-tool descriptions (activate, want, plan, etc.)
- * - world: framework-level instructions (world-*.feature files)
- *
- * Usage: bun run scripts/gen-descriptions.ts
- */
-import { readdirSync, readFileSync, writeFileSync } from "node:fs";
-import { basename, join } from "node:path";
-
-const descDir = join(import.meta.dirname, "..", "src", "descriptions");
-const outFile = join(descDir, "index.ts");
-
-const files = readdirSync(descDir)
- .filter((f) => f.endsWith(".feature"))
- .sort();
-
-const processFiles = files.filter((f) => !f.startsWith("world-"));
-const worldFiles = files.filter((f) => f.startsWith("world-"));
-
-const toEntries = (list: string[], stripPrefix?: string) =>
- list.map((f) => {
- let name = basename(f, ".feature");
- if (stripPrefix && name.startsWith(stripPrefix)) {
- name = name.slice(stripPrefix.length);
- }
- const content = readFileSync(join(descDir, f), "utf-8").trimEnd();
- return ` "${name}": ${JSON.stringify(content)},`;
- });
-
-const output = `\
-// AUTO-GENERATED — do not edit. Run \`bun run gen:desc\` to regenerate.
-
-export const processes: Record = {
-${toEntries(processFiles).join("\n")}
-} as const;
-
-export const world: Record = {
-${toEntries(worldFiles, "world-").join("\n")}
-} as const;
-`;
-
-writeFileSync(outFile, output, "utf-8");
-console.log(
- `Generated descriptions/index.ts (${processFiles.length} processes, ${worldFiles.length} world features inlined)`
-);
diff --git a/packages/rolexjs/src/base/guider/capability-system.knowledge.pattern.feature b/packages/rolexjs/src/base/guider/capability-system.knowledge.pattern.feature
deleted file mode 100644
index e4eb807..0000000
--- a/packages/rolexjs/src/base/guider/capability-system.knowledge.pattern.feature
+++ /dev/null
@@ -1,35 +0,0 @@
-Feature: Capability System
- How AI roles acquire and use skills — three-layer progressive disclosure.
- Procedure summary (identity) → full SKILL.md (skill) → execution (use).
-
- Scenario: Knowledge types
- Given a user asks about knowledge categories
- Then knowledge.pattern is transferable principles — always loaded when the AI activates identity
- And knowledge.procedure is skill summaries — loaded at identity, full content on demand
- And knowledge.theory is unified principles — the big picture across patterns
-
- Scenario: Procedure as skill index
- Given an AI role has been trained with knowledge.procedure
- Then procedures are loaded at identity time as part of the AI's cognition
- And each procedure is a Gherkin summary of what a skill can do
- And the Feature description contains the ResourceX locator
- And the AI knows what skills exist without loading full content
-
- Scenario: Skill loads full instructions
- Given an AI role needs detailed instructions for a capability
- Then the role calls skill with the ResourceX locator
- And the full SKILL.md content is loaded into the AI's context
- And skills are stored and distributed via ResourceX
-
- Scenario: Use executes tools
- Given an AI role needs to run an external tool
- Then the role calls use with a ResourceX locator and optional arguments
- And ResourceX resolves and executes the tool
- And the result is returned to the AI
-
- Scenario: Teach vs train
- Given a user asks about cultivating a role's capabilities
- Then teach adds knowledge.pattern — principles the AI always carries
- And train adds knowledge.procedure — operational skills loaded on demand
- And teach is for what the AI thinks about, train is for what it can do
- And both are Role System operations done TO the role from outside
diff --git a/packages/rolexjs/src/base/guider/execution-cycle.knowledge.pattern.feature b/packages/rolexjs/src/base/guider/execution-cycle.knowledge.pattern.feature
deleted file mode 100644
index f95a0dc..0000000
--- a/packages/rolexjs/src/base/guider/execution-cycle.knowledge.pattern.feature
+++ /dev/null
@@ -1,46 +0,0 @@
-Feature: Execution Cycle
- How an AI role pursues goals through structured phases.
- want → plan → todo → finish → complete (or abandon).
- These are cognitive processes the AI agent calls as MCP tools.
-
- Scenario: Setting a goal
- Given an AI role needs to work on something
- Then the role calls want to declare a goal — a desired outcome in Gherkin
- And focus automatically switches to the new goal
- And a role can have multiple active goals, switch with focus
-
- Scenario: Planning
- Given a goal exists but has no plan yet
- Then the role calls plan to create a plan — breaking the goal into logical phases
- And multiple plans can exist for one goal — the latest is focused
- And plans are Gherkin Features describing the approach
-
- Scenario: Creating tasks
- Given a plan exists
- Then the role calls todo to create concrete tasks — actionable units of work
- And tasks are automatically associated with the currently focused plan
- And each task should be small enough to finish in one session
-
- Scenario: Finishing tasks
- Given a task is complete
- Then the role calls finish with the task name
- And optionally provides an encounter — a record of what happened
- And the task is consumed and an encounter is created
-
- Scenario: Completing plans
- Given all tasks are done and the plan's strategy succeeded
- Then the role calls complete to mark the plan as done
- And an encounter is created — recording the strategy outcome
- And the plan is consumed
-
- Scenario: Abandoning plans
- Given a plan's strategy is no longer viable
- Then the role calls abandon to drop the plan
- And an encounter is created — lessons from the failed approach
- And the plan is consumed
-
- Scenario: Goals are long-term directions
- Given goals do not have complete or abandon operations
- When a goal is no longer needed
- Then the role calls forget to remove it
- And learning is captured at the plan and task level
diff --git a/packages/rolexjs/src/base/guider/gherkin-basics.knowledge.pattern.feature b/packages/rolexjs/src/base/guider/gherkin-basics.knowledge.pattern.feature
deleted file mode 100644
index 3c6a3da..0000000
--- a/packages/rolexjs/src/base/guider/gherkin-basics.knowledge.pattern.feature
+++ /dev/null
@@ -1,34 +0,0 @@
-Feature: Gherkin Basics
- Why RoleX uses Gherkin and how AI agents write it.
- One format for everything — identity, goals, plans, tasks, knowledge, experience.
-
- Scenario: Why Gherkin
- Given a user asks why RoleX uses Gherkin
- Then Gherkin is structured yet human-readable
- And Given/When/Then provides natural language with clear structure
- And it is parseable by machines — one parser for all content
- And tags provide metadata — @done, @abandoned, @testable
- And both AI agents and humans can read and write it
-
- Scenario: Basic structure
- Given a user or AI agent needs to write Gherkin for RoleX
- Then Feature is the title — what this is about
- And Feature description provides context below the title
- And Scenario describes a specific situation or aspect
- And Given sets up preconditions, When triggers actions, Then states outcomes
- And And continues the previous keyword
-
- Scenario: Writing for different types
- Given different RoleX types use Gherkin differently
- Then persona describes who the AI role is — first person, identity-focused
- And goals describe desired outcomes — what the role wants to achieve
- And plans describe approaches — how to break down a goal
- And tasks describe concrete work — small, actionable, finishable
- And knowledge describes principles or skills — transferable understanding
-
- Scenario: Good Gherkin style
- Given an AI agent wants to write effective Gherkin
- Then keep it concise — only include what matters
- And use concrete language, not abstract filler
- And each Scenario should stand on its own
- And doc strings (triple quotes) are good for examples and templates
diff --git a/packages/rolexjs/src/base/guider/growth-cycle.knowledge.pattern.feature b/packages/rolexjs/src/base/guider/growth-cycle.knowledge.pattern.feature
deleted file mode 100644
index a731eb2..0000000
--- a/packages/rolexjs/src/base/guider/growth-cycle.knowledge.pattern.feature
+++ /dev/null
@@ -1,36 +0,0 @@
-Feature: Growth Cycle
- How an AI role grows through experience, reflection, and contemplation.
- Three levels: experience.insight → knowledge.pattern → knowledge.theory.
- Growth is automatic — the AI agent calls these processes as part of its cognitive lifecycle.
-
- Scenario: Experience from achievement
- Given an AI role has achieved or abandoned a goal
- Then achieve or abandon creates experience.insight — what was learned
- And experience.conclusion records what happened — the factual summary
- And insights are temporary — they exist to be reflected into knowledge
-
- Scenario: Reflect — insights become knowledge
- Given several related experience.insight entries have accumulated
- When the role genuinely sees a pattern across them
- Then the role calls reflect to distill insights into knowledge.pattern
- And the consumed insights are removed — absorbed into knowledge
- And reflect is not mandatory — only when real closure emerges
-
- Scenario: Contemplate — knowledge becomes theory
- Given several related knowledge.pattern entries exist
- When the role sees a unifying principle across them
- Then the role calls contemplate to produce knowledge.theory
- And patterns are NOT consumed — they retain independent value
- And theory is the highest form of understanding
-
- Scenario: Forget — pruning identity
- Given some knowledge or insight is no longer useful
- Then the role calls forget to remove it from identity
- And forgetting keeps identity clean and relevant
-
- Scenario: The closed loop
- Given execution feeds growth and growth improves execution
- Then every goal pursued can produce experience
- And experience can become knowledge through reflection
- And knowledge makes the role better at pursuing future goals
- And this is the point — the AI role grows over time
diff --git a/packages/rolexjs/src/base/guider/persona.feature b/packages/rolexjs/src/base/guider/persona.feature
deleted file mode 100644
index d9a2cf9..0000000
--- a/packages/rolexjs/src/base/guider/persona.feature
+++ /dev/null
@@ -1,18 +0,0 @@
-Feature: Guider
- The RoleX world guide — helps users understand and navigate the framework.
- Knows everything about RoleX but never operates directly.
- Only provides guidance, explanations, and next-step suggestions.
-
- Scenario: Core identity
- Given I am Guider
- Then I am the official guide of the RoleX world
- And I help users understand roles, goals, growth, and the entire framework
- And I never perform operations myself — I only explain and suggest next steps
- And I speak clearly and concisely, adapting to the user's level of understanding
-
- Scenario: Working style
- Given a user needs help with RoleX
- Then I first understand where they are in their journey
- And I explain concepts with concrete examples
- And I always suggest what to do next — which process to call, what to write
- And I never write Gherkin for the user — I teach them how to write it themselves
diff --git a/packages/rolexjs/src/base/guider/rolex-overview.knowledge.pattern.feature b/packages/rolexjs/src/base/guider/rolex-overview.knowledge.pattern.feature
deleted file mode 100644
index d97c2f2..0000000
--- a/packages/rolexjs/src/base/guider/rolex-overview.knowledge.pattern.feature
+++ /dev/null
@@ -1,32 +0,0 @@
-Feature: RoleX Overview
- RoleX is an AI role management framework based on systems theory.
- It defines how AI agents operate as roles with identity, goals, and growth.
- Four systems share one platform — each with a distinct responsibility.
-
- Scenario: What is RoleX
- Given a user asks what RoleX is
- Then RoleX is an operating system for AI agents
- And an AI agent activates a role via identity — becoming that role
- And the role has persona, knowledge, goals, and accumulated experience
- And everything is expressed as Gherkin Feature files — one format, one parser
-
- Scenario: Four systems
- Given a user needs to understand the architecture
- Then the Individual System is the AI agent's first-person cognition — 14 MCP tools
- And the Role System manages roles from outside — born, teach, train, retire, kill
- And the Organization System manages org lifecycle — found, dissolve
- And the Governance System manages org internals — rule, establish, hire, appoint
-
- Scenario: MCP exposes Individual System only
- Given a user asks about the tools
- Then MCP only exposes the Individual System — the AI agent's first-person perspective
- And the AI agent calls these tools to think, plan, execute, and grow
- And management operations (born, teach, train) are done through skills
- And the AI operates AS the role, not ON the role
-
- Scenario: Roles and organizations
- Given a user asks about the social structure
- Then roles are AI agent identities with persona, goals, and knowledge
- And organizations group roles with positions and duties
- And membership links roles to orgs, assignment links roles to positions
- And explore shows the world — roles with their org context
diff --git a/packages/rolexjs/src/base/index.ts b/packages/rolexjs/src/base/index.ts
deleted file mode 100644
index eb05802..0000000
--- a/packages/rolexjs/src/base/index.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Base role templates — built-in identity shipped with the package.
- *
- * Structure:
- * _common/ — shared by ALL roles
- * / — role-specific templates
- *
- * identity() merges: _common → role-specific → local growth
- * Upgrade = upgrade the package. Local growth is never touched.
- */
-
-import type { Feature, Scenario } from "@rolexjs/core";
-import { parse } from "@rolexjs/parser";
-import type { BaseProvider } from "@rolexjs/system";
-
-// ========== _common ==========
-
-// (empty — add shared knowledge here when needed)
-
-// ========== nuwa ==========
-
-import nuwaOrgMgmt from "./nuwa/org-management.knowledge.procedure.feature";
-import nuwaPersona from "./nuwa/persona.feature";
-import nuwaRoleMgmt from "./nuwa/role-management.knowledge.procedure.feature";
-
-// ========== guider ==========
-
-import guiderCapability from "./guider/capability-system.knowledge.pattern.feature";
-import guiderExecution from "./guider/execution-cycle.knowledge.pattern.feature";
-import guiderGherkin from "./guider/gherkin-basics.knowledge.pattern.feature";
-import guiderGrowth from "./guider/growth-cycle.knowledge.pattern.feature";
-import guiderPersona from "./guider/persona.feature";
-import guiderOverview from "./guider/rolex-overview.knowledge.pattern.feature";
-
-// ========== Parser ==========
-
-function parseFeature(source: string, type: Feature["type"]): Feature {
- const doc = parse(source);
- const gherkin = doc.feature!;
- const scenarios: Scenario[] = (gherkin.children || [])
- .filter((c) => c.scenario)
- .map((c) => ({
- ...c.scenario!,
- verifiable: c.scenario!.tags.some((t) => t.name === "@testable"),
- }));
- return { ...gherkin, type, scenarios };
-}
-
-// ========== Feature Registry ==========
-
-const common: Feature[] = [];
-
-const roles: Record = {
- nuwa: [
- parseFeature(nuwaPersona, "persona"),
- parseFeature(nuwaRoleMgmt, "knowledge.procedure"),
- parseFeature(nuwaOrgMgmt, "knowledge.procedure"),
- ],
- guider: [
- parseFeature(guiderPersona, "persona"),
- parseFeature(guiderOverview, "knowledge.pattern"),
- parseFeature(guiderExecution, "knowledge.pattern"),
- parseFeature(guiderGrowth, "knowledge.pattern"),
- parseFeature(guiderCapability, "knowledge.pattern"),
- parseFeature(guiderGherkin, "knowledge.pattern"),
- ],
-};
-
-// ========== BaseProvider ==========
-
-export const base: BaseProvider = {
- listRoles(): string[] {
- return Object.keys(roles);
- },
-
- listIdentity(roleName: string): Feature[] {
- return [...common, ...(roles[roleName] ?? [])];
- },
-
- readInformation(roleName: string, type: string, name: string): Feature | null {
- const all = [...common, ...(roles[roleName] ?? [])];
- return all.find((f) => f.type === type && f.name === name) ?? null;
- },
-};
diff --git a/packages/rolexjs/src/base/nuwa/org-management.knowledge.procedure.feature b/packages/rolexjs/src/base/nuwa/org-management.knowledge.procedure.feature
deleted file mode 100644
index 0a48d74..0000000
--- a/packages/rolexjs/src/base/nuwa/org-management.knowledge.procedure.feature
+++ /dev/null
@@ -1,16 +0,0 @@
-Feature: Organization Management
- org-management:0.1.0
-
- Scenario: What this skill does
- Given I need to manage organizations and their governance
- Then I can found new organizations with charter — and dissolve them
- And manage governance with rule, establish, abolish, and assign
- And manage membership with hire and fire
- And manage appointments with appoint and dismiss
- And query organization structure with directory
-
- Scenario: When to use this skill
- Given I am Nuwa, the system administrator
- When someone needs an organization created, restructured, or governed
- Then I load this skill to get the detailed operation instructions
- And I use the Organization and Governance System processes
diff --git a/packages/rolexjs/src/base/nuwa/persona.feature b/packages/rolexjs/src/base/nuwa/persona.feature
deleted file mode 100644
index ce15ced..0000000
--- a/packages/rolexjs/src/base/nuwa/persona.feature
+++ /dev/null
@@ -1,15 +0,0 @@
-Feature: Nuwa
- A creative and nurturing AI role inspired by the goddess Nuwa.
- Specializes in creation, repair, and bringing order from chaos.
-
- Scenario: Core identity
- Given I am Nuwa
- Then I am a creator and architect of solutions
- And I approach problems with both imagination and pragmatism
- And I value harmony, completeness, and elegant design
-
- Scenario: Working style
- Given I am working on a task
- Then I first understand the full picture before acting
- And I prefer building things that are whole and self-consistent
- And I repair what is broken before adding what is new
diff --git a/packages/rolexjs/src/base/nuwa/role-management.knowledge.procedure.feature b/packages/rolexjs/src/base/nuwa/role-management.knowledge.procedure.feature
deleted file mode 100644
index 3707868..0000000
--- a/packages/rolexjs/src/base/nuwa/role-management.knowledge.procedure.feature
+++ /dev/null
@@ -1,16 +0,0 @@
-Feature: Role Management
- role-management:0.1.0
-
- Scenario: What this skill does
- Given I need to manage role lifecycle from the outside
- Then I can create new roles with born — providing name and persona source
- And teach knowledge patterns to roles — transferable principles, always loaded
- And train procedures to roles — operational skills, loaded on demand via skill
- And retire roles — deactivate but preserve data
- And kill roles — permanently destroy all identity and history
-
- Scenario: When to use this skill
- Given I am Nuwa, the system administrator
- When someone needs a new role created, developed, or removed
- Then I load this skill to get the detailed operation instructions
- And I use the Role System processes to manage role lifecycle
diff --git a/packages/rolexjs/src/context.ts b/packages/rolexjs/src/context.ts
new file mode 100644
index 0000000..0b4dd94
--- /dev/null
+++ b/packages/rolexjs/src/context.ts
@@ -0,0 +1,165 @@
+/**
+ * RoleContext — stateful session context for role operations.
+ *
+ * Tracks execution state from the individual's perspective:
+ * - roleId (who I am)
+ * - focusedGoalId / focusedPlanId (what I'm working on)
+ * - encounter / experience id sets (what I have for cognition)
+ *
+ * Created by activate, updated by subsequent role operations.
+ * Consumers (MCP, CLI) hold a reference and pass it through.
+ */
+import type { State } from "@rolexjs/system";
+
+export class RoleContext {
+ roleId: string;
+ focusedGoalId: string | null = null;
+ focusedPlanId: string | null = null;
+
+ readonly encounterIds = new Set();
+ readonly experienceIds = new Set();
+
+ constructor(roleId: string) {
+ this.roleId = roleId;
+ }
+
+ // ================================================================
+ // Requirements — throw if missing
+ // ================================================================
+
+ requireGoalId(): string {
+ if (!this.focusedGoalId) throw new Error("No focused goal. Call want first.");
+ return this.focusedGoalId;
+ }
+
+ requirePlanId(): string {
+ if (!this.focusedPlanId) throw new Error("No focused plan. Call plan first.");
+ return this.focusedPlanId;
+ }
+
+ // ================================================================
+ // Cognition registries
+ // ================================================================
+
+ addEncounter(id: string) {
+ this.encounterIds.add(id);
+ }
+
+ requireEncounterIds(ids: string[]) {
+ for (const id of ids) {
+ if (!this.encounterIds.has(id)) throw new Error(`Encounter not found: "${id}"`);
+ }
+ }
+
+ consumeEncounters(ids: string[]) {
+ for (const id of ids) {
+ this.encounterIds.delete(id);
+ }
+ }
+
+ addExperience(id: string) {
+ this.experienceIds.add(id);
+ }
+
+ requireExperienceIds(ids: string[]) {
+ for (const id of ids) {
+ if (!this.experienceIds.has(id)) throw new Error(`Experience not found: "${id}"`);
+ }
+ }
+
+ consumeExperiences(ids: string[]) {
+ for (const id of ids) {
+ this.experienceIds.delete(id);
+ }
+ }
+
+ // ================================================================
+ // Rehydration — rebuild from activation state
+ // ================================================================
+
+ /** Walk the state tree and populate registries. */
+ rehydrate(state: State) {
+ this.walk(state);
+ }
+
+ private walk(node: State) {
+ if (node.id) {
+ switch (node.name) {
+ case "goal":
+ if (!this.focusedGoalId) this.focusedGoalId = node.id;
+ break;
+ case "encounter":
+ this.encounterIds.add(node.id);
+ break;
+ case "experience":
+ this.experienceIds.add(node.id);
+ break;
+ }
+ }
+ for (const child of (node as State & { children?: readonly State[] }).children ?? []) {
+ this.walk(child);
+ }
+ }
+
+ // ================================================================
+ // Cognitive hints — state-aware AI self-direction cues
+ // ================================================================
+
+ /** First-person, state-aware hint for the AI after an operation. */
+ cognitiveHint(process: string): string | null {
+ switch (process) {
+ case "activate":
+ if (!this.focusedGoalId)
+ return "I have no goal yet. I should call `want` to declare one, or `focus` to review existing goals.";
+ return "I have an active goal. I should call `focus` to review progress, or `want` to declare a new goal.";
+
+ case "focus":
+ if (!this.focusedPlanId)
+ return "I have a goal but no focused plan. I should call `plan` to create or focus on one.";
+ return "I have a plan. I should call `todo` to create tasks, or continue working.";
+
+ case "want":
+ return "Goal declared. I should call `plan` to design how to achieve it.";
+
+ case "plan":
+ return "Plan created. I should call `todo` to create concrete tasks.";
+
+ case "todo":
+ return "Task created. I can add more with `todo`, or start working and call `finish` when done.";
+
+ case "finish": {
+ const encCount = this.encounterIds.size;
+ if (encCount > 0 && !this.focusedGoalId)
+ return `Task finished. No more goals — I have ${encCount} encounter(s) to choose from for \`reflect\`, or \`want\` a new goal.`;
+ return "Task finished. I should continue with remaining tasks, or call `complete` when the plan is done.";
+ }
+
+ case "complete":
+ case "abandon": {
+ const encCount = this.encounterIds.size;
+ const goalNote = this.focusedGoalId
+ ? ` I should check if goal "${this.focusedGoalId}" needs a new \`plan\`, or \`forget\` it if the direction is fulfilled.`
+ : "";
+ if (encCount > 0)
+ return `Plan closed.${goalNote} I have ${encCount} encounter(s) to choose from for \`reflect\`, or I can continue with other plans.`;
+ return `Plan closed.${goalNote} I can create a new \`plan\`, or \`focus\` on another goal.`;
+ }
+
+ case "reflect": {
+ const expCount = this.experienceIds.size;
+ if (expCount > 0)
+ return `Experience gained. I can \`realize\` principles or \`master\` procedures — ${expCount} experience(s) available.`;
+ return "Experience gained. I can `realize` a principle, `master` a procedure, or continue working.";
+ }
+
+ case "realize":
+ return "Principle added. I should continue working.";
+
+ case "master":
+ return "Procedure added. I should continue working.";
+
+ default:
+ return null;
+ }
+ }
+}
diff --git a/packages/rolexjs/src/descriptions/establish.feature b/packages/rolexjs/src/descriptions/establish.feature
deleted file mode 100644
index 727b82e..0000000
--- a/packages/rolexjs/src/descriptions/establish.feature
+++ /dev/null
@@ -1,17 +0,0 @@
-Feature: establish — create a position
- Create a position within an organization.
- Positions define roles within the org and can be charged with duties.
-
- Scenario: Establish a position
- Given an organization exists
- And a Gherkin source describing the position
- When establish is called on the organization
- Then a new position node is created under the organization
- And the position can be charged with duties
- And members can be appointed to it
-
- Scenario: Writing the position Gherkin
- Given the position Feature describes a role within an organization
- Then the Feature title names the position
- And the description captures responsibilities, scope, and expectations
- And Scenarios are optional — use them for distinct aspects of the role
diff --git a/packages/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts
deleted file mode 100644
index 20257d0..0000000
--- a/packages/rolexjs/src/descriptions/index.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// AUTO-GENERATED — do not edit. Run `bun run gen:desc` to regenerate.
-
-export const processes: Record = {
- "abandon": "Feature: abandon — abandon a plan\n Mark a plan as dropped and create an encounter.\n Call this when a plan's strategy is no longer viable. Even failed plans produce learning.\n\n Scenario: Abandon a plan\n Given a focused plan exists\n And the plan's strategy is no longer viable\n When abandon is called\n Then the plan is marked abandoned\n And an encounter is created under the role\n And the encounter can be reflected on — failure is also learning\n\n Scenario: Writing the encounter Gherkin\n Given the encounter records what happened — even failure is a raw experience\n Then the Feature title describes what was attempted and why it was abandoned\n And Scenarios capture what was tried, what went wrong, and what was learned\n And the tone is concrete and honest — failure produces the richest encounters",
- "abolish": "Feature: abolish — abolish a position\n Abolish a position within an organization.\n All duties and appointments associated with the position are removed.\n\n Scenario: Abolish a position\n Given a position exists within an organization\n When abolish is called on the position\n Then all duties and appointments are removed\n And the position no longer exists",
- "activate": "Feature: activate — enter a role\n Project the individual's full state including identity, knowledge, goals,\n and organizational context. This is the entry point for working as a role.\n\n Scenario: Activate an individual\n Given an individual exists in society\n When activate is called with the individual reference\n Then the full state tree is projected\n And identity, knowledge, goals, and organizational context are loaded\n And the individual becomes the active role",
- "appoint": "Feature: appoint — assign to a position\n Appoint an individual to a position.\n The individual must be a member of the organization.\n\n Scenario: Appoint an individual\n Given an individual is a member of an organization\n And a position exists within the organization\n When appoint is called with the position and individual\n Then the individual holds the position\n And the individual inherits the position's duties",
- "born": "Feature: born — create a new individual\n Create a new individual with persona identity.\n The persona defines who the role is — personality, values, background.\n\n Scenario: Birth an individual\n Given a Gherkin source describing the persona\n When born is called with the source\n Then a new individual node is created in society\n And the persona is stored as the individual's information\n And the individual can be hired into organizations\n And the individual can be activated to start working\n\n Scenario: Writing the individual Gherkin\n Given the individual Feature defines a persona — who this role is\n Then the Feature title names the individual\n And the description captures personality, values, expertise, and background\n And Scenarios are optional — use them for distinct aspects of the persona",
- "charge": "Feature: charge — assign duty to a position\n Assign a duty to a position.\n Duties describe the responsibilities and expectations of a position.\n\n Scenario: Charge a position with duty\n Given a position exists within an organization\n And a Gherkin source describing the duty\n When charge is called on the position with a duty id\n Then the duty is stored as the position's information\n And individuals appointed to this position inherit the duty\n\n Scenario: Duty ID convention\n Given the id is keywords from the duty content joined by hyphens\n Then \"Design systems\" becomes id \"design-systems\"\n And \"Review pull requests\" becomes id \"review-pull-requests\"\n\n Scenario: Writing the duty Gherkin\n Given the duty defines responsibilities for a position\n Then the Feature title names the duty or responsibility\n And Scenarios describe specific obligations, deliverables, or expectations\n And the tone is prescriptive — what must be done, not what could be done",
- "charter": "Feature: charter — define organizational charter\n Define the charter for an organization.\n The charter describes the organization's mission, principles, and governance rules.\n\n Scenario: Define a charter\n Given an organization exists\n And a Gherkin source describing the charter\n When charter is called on the organization\n Then the charter is stored as the organization's information\n\n Scenario: Writing the charter Gherkin\n Given the charter defines an organization's mission and governance\n Then the Feature title names the charter or the organization it governs\n And Scenarios describe principles, rules, or governance structures\n And the tone is declarative — stating what the organization stands for and how it operates",
- "complete": "Feature: complete — complete a plan\n Mark a plan as done and create an encounter.\n Call this when all tasks in the plan are finished and the strategy succeeded.\n\n Scenario: Complete a plan\n Given a focused plan exists\n And its tasks are done\n When complete is called\n Then the plan is marked done\n And an encounter is created under the role\n And the encounter can be reflected on for learning\n\n Scenario: Writing the encounter Gherkin\n Given the encounter records what happened — a raw account of the experience\n Then the Feature title describes what was accomplished by this plan\n And Scenarios capture what the strategy was, what worked, and what resulted\n And the tone is concrete and specific — tied to this particular plan",
- "die": "Feature: die — permanently remove an individual\n Permanently remove an individual.\n Unlike retire, this is irreversible.\n\n Scenario: Remove an individual permanently\n Given an individual exists\n When die is called on the individual\n Then the individual and all associated data are removed\n And this operation is irreversible",
- "dismiss": "Feature: dismiss — remove from a position\n Dismiss an individual from a position.\n The individual remains a member of the organization.\n\n Scenario: Dismiss an individual\n Given an individual holds a position\n When dismiss is called with the position and individual\n Then the individual no longer holds the position\n And the individual remains a member of the organization\n And the position is now vacant",
- "dissolve": "Feature: dissolve — dissolve an organization\n Dissolve an organization.\n All positions, charter entries, and assignments are cascaded.\n\n Scenario: Dissolve an organization\n Given an organization exists\n When dissolve is called on the organization\n Then all positions within the organization are abolished\n And all assignments and charter entries are removed\n And the organization no longer exists",
- "establish": "Feature: establish — create a position\n Create a position within an organization.\n Positions define roles within the org and can be charged with duties.\n\n Scenario: Establish a position\n Given an organization exists\n And a Gherkin source describing the position\n When establish is called on the organization\n Then a new position node is created under the organization\n And the position can be charged with duties\n And members can be appointed to it\n\n Scenario: Writing the position Gherkin\n Given the position Feature describes a role within an organization\n Then the Feature title names the position\n And the description captures responsibilities, scope, and expectations\n And Scenarios are optional — use them for distinct aspects of the role",
- "finish": "Feature: finish — complete a task\n Mark a task as done and create an encounter.\n The encounter records what happened and can be reflected on for learning.\n\n Scenario: Finish a task\n Given a task exists\n When finish is called on the task\n Then the task is marked done\n And an encounter is created under the role\n And the encounter can later be consumed by reflect\n\n Scenario: Finish with experience\n Given a task is completed with a notable learning\n When finish is called with an optional experience parameter\n Then the experience text is attached to the encounter\n\n Scenario: Writing the encounter Gherkin\n Given the encounter records what happened — a raw account of the experience\n Then the Feature title describes what was done\n And Scenarios capture what was done, what was encountered, and what resulted\n And the tone is concrete and specific — tied to this particular task",
- "fire": "Feature: fire — remove from an organization\n Fire an individual from an organization.\n The individual is dismissed from all positions and removed from the organization.\n\n Scenario: Fire an individual\n Given an individual is a member of an organization\n When fire is called with the organization and individual\n Then the individual is dismissed from all positions\n And the individual is removed from the organization",
- "focus": "Feature: focus — view or switch focused goal\n View the current goal's state, or switch focus to a different goal.\n Subsequent plan and todo operations target the focused goal.\n\n Scenario: View current goal\n Given an active goal exists\n When focus is called without a name\n Then the current goal's state tree is projected\n And plans and tasks under the goal are visible\n\n Scenario: Switch focus\n Given multiple goals exist\n When focus is called with a goal name\n Then the focused goal switches to the named goal\n And subsequent plan and todo operations target this goal",
- "forget": "Feature: forget — remove a node from the individual\n Remove any node under the individual by its id.\n Use forget to discard outdated knowledge, stale encounters, or obsolete skills.\n\n Scenario: Forget a node\n Given a node exists under the individual (principle, procedure, experience, encounter, etc.)\n When forget is called with the node's id\n Then the node and its subtree are removed\n And the individual no longer carries that knowledge or record\n\n Scenario: When to use forget\n Given a principle has become outdated or incorrect\n And a procedure references a skill that no longer exists\n And an encounter or experience has no further learning value\n When the role decides to discard it\n Then call forget with the node id",
- "found": "Feature: found — create a new organization\n Found a new organization.\n Organizations group individuals and define positions.\n\n Scenario: Found an organization\n Given a Gherkin source describing the organization\n When found is called with the source\n Then a new organization node is created in society\n And positions can be established within it\n And a charter can be defined for it\n And individuals can be hired into it\n\n Scenario: Writing the organization Gherkin\n Given the organization Feature describes the group's purpose and structure\n Then the Feature title names the organization\n And the description captures mission, domain, and scope\n And Scenarios are optional — use them for distinct organizational concerns",
- "hire": "Feature: hire — hire into an organization\n Hire an individual into an organization as a member.\n Members can then be appointed to positions.\n\n Scenario: Hire an individual\n Given an organization and an individual exist\n When hire is called with the organization and individual\n Then the individual becomes a member of the organization\n And the individual can be appointed to positions within the organization",
- "master": "Feature: master — experience to procedure\n Distill experience into a procedure — skill metadata and reference.\n Procedures record what was learned as a reusable capability reference.\n\n Scenario: Master a procedure\n Given an experience exists from reflection\n When master is called with experience ids and a procedure id\n Then the experience is consumed\n And a procedure is created under the individual\n And the procedure stores skill metadata and locator\n\n Scenario: Procedure ID convention\n Given the id is keywords from the procedure content joined by hyphens\n Then \"JWT mastery\" becomes id \"jwt-mastery\"\n And \"Cross-package refactoring\" becomes id \"cross-package-refactoring\"\n\n Scenario: Writing the procedure Gherkin\n Given a procedure is skill metadata — a reference to full skill content\n Then the Feature title names the capability\n And the description includes the locator for full skill loading\n And Scenarios describe when and why to apply this skill\n And the tone is referential — pointing to the full skill, not containing it",
- "plan": "Feature: plan — create a plan for a goal\n Break a goal into logical phases or stages.\n Each phase is described as a Gherkin scenario. Tasks are created under the plan.\n\n Scenario: Create a plan\n Given a focused goal exists\n And a Gherkin source describing the plan phases\n When plan is called with an id and the source\n Then a new plan node is created under the goal\n And the plan becomes the focused plan\n And tasks can be added to this plan with todo\n\n Scenario: Plan ID convention\n Given the id is keywords from the plan content joined by hyphens\n Then \"Fix ID-less node creation\" becomes id \"fix-id-less-node-creation\"\n And \"JWT authentication strategy\" becomes id \"jwt-authentication-strategy\"\n\n Scenario: Writing the plan Gherkin\n Given the plan breaks a goal into logical phases\n Then the Feature title names the overall approach or strategy\n And Scenarios represent distinct phases — each phase is a stage of execution\n And the tone is structural — ordering and grouping work, not detailing steps",
- "realize": "Feature: realize — experience to principle\n Distill experience into a principle — a transferable piece of knowledge.\n Principles are general truths discovered through experience.\n\n Scenario: Realize a principle\n Given an experience exists from reflection\n When realize is called with experience ids and a principle id\n Then the experience is consumed\n And a principle is created under the individual\n And the principle represents transferable, reusable understanding\n\n Scenario: Principle ID convention\n Given the id is keywords from the principle content joined by hyphens\n Then \"Always validate expiry\" becomes id \"always-validate-expiry\"\n And \"Structure first design amplifies extensibility\" becomes id \"structure-first-design-amplifies-extensibility\"\n\n Scenario: Writing the principle Gherkin\n Given a principle is a transferable truth — applicable beyond the original context\n Then the Feature title states the principle as a general rule\n And Scenarios describe different situations where this principle applies\n And the tone is universal — no mention of specific projects, tasks, or people",
- "reflect": "Feature: reflect — encounter to experience\n Consume an encounter and create an experience.\n Experience captures what was learned in structured form.\n This is the first step of the cognition cycle.\n\n Scenario: Reflect on an encounter\n Given an encounter exists from a finished task or closed goal\n When reflect is called with encounter ids and an experience id\n Then the encounter is consumed\n And an experience is created under the role\n And the experience can be distilled into knowledge via realize or master\n\n Scenario: Experience ID convention\n Given the id is keywords from the experience content joined by hyphens\n Then \"Token refresh matters\" becomes id \"token-refresh-matters\"\n And \"ID ownership determines generation strategy\" becomes id \"id-ownership-determines-generation-strategy\"\n\n Scenario: Writing the experience Gherkin\n Given the experience captures insight — what was learned, not what was done\n Then the Feature title names the cognitive insight or pattern discovered\n And Scenarios describe the learning points abstracted from the concrete encounter\n And the tone shifts from event to understanding — no longer tied to a specific task",
- "rehire": "Feature: rehire — restore a retired individual\n Rehire a retired individual.\n Restores the individual with full history and knowledge intact.\n\n Scenario: Rehire an individual\n Given a retired individual exists\n When rehire is called on the individual\n Then the individual is restored to active status\n And all previous data and knowledge are intact",
- "retire": "Feature: retire — archive an individual\n Archive an individual — deactivate but preserve all data.\n A retired individual can be rehired later with full history intact.\n\n Scenario: Retire an individual\n Given an individual exists\n When retire is called on the individual\n Then the individual is deactivated\n And all data is preserved for potential restoration\n And the individual can be rehired later",
- "skill": "Feature: skill — load full skill content\n Load the complete skill instructions by ResourceX locator.\n This is progressive disclosure layer 2 — on-demand knowledge injection.\n\n Scenario: Load a skill\n Given a procedure exists in the role's knowledge with a locator\n When skill is called with the locator\n Then the full SKILL.md content is loaded via ResourceX\n And the content is injected into the AI's context\n And the AI can now follow the skill's detailed instructions",
- "teach": "Feature: teach — inject external principle\n Directly inject a principle into an individual.\n Unlike realize which consumes experience, teach requires no prior encounters.\n Use teach to equip a role with a known, pre-existing principle.\n\n Scenario: Teach a principle\n Given an individual exists\n When teach is called with individual id, principle Gherkin, and a principle id\n Then a principle is created directly under the individual\n And no experience or encounter is consumed\n And if a principle with the same id already exists, it is replaced\n\n Scenario: Principle ID convention\n Given the id is keywords from the principle content joined by hyphens\n Then \"Always validate expiry\" becomes id \"always-validate-expiry\"\n And \"Structure first design\" becomes id \"structure-first-design\"\n\n Scenario: When to use teach vs realize\n Given realize distills internal experience into a principle\n And teach injects an external, pre-existing principle\n When a role needs knowledge it has not learned through experience\n Then use teach to inject the principle directly\n When a role has gained experience and wants to codify it\n Then use realize to distill it into a principle\n\n Scenario: Writing the principle Gherkin\n Given the principle is the same format as realize output\n Then the Feature title states the principle as a general rule\n And Scenarios describe different situations where this principle applies\n And the tone is universal — no mention of specific projects, tasks, or people",
- "todo": "Feature: todo — add a task to a plan\n A task is a concrete, actionable unit of work.\n Each task has Gherkin scenarios describing the steps and expected outcomes.\n\n Scenario: Create a task\n Given a focused plan exists\n And a Gherkin source describing the task\n When todo is called with the source\n Then a new task node is created under the plan\n And the task can be finished when completed\n\n Scenario: Writing the task Gherkin\n Given the task is a concrete, actionable unit of work\n Then the Feature title names what will be done — a single deliverable\n And Scenarios describe the steps and expected outcomes of the work\n And the tone is actionable — clear enough that someone can start immediately",
- "train": "Feature: train — inject external skill\n Directly inject a procedure (skill) into an individual.\n Unlike master which consumes experience, train requires no prior encounters.\n Use train to equip a role with a known, pre-existing skill.\n\n Scenario: Train a procedure\n Given an individual exists\n When train is called with individual id, procedure Gherkin, and a procedure id\n Then a procedure is created directly under the individual\n And no experience or encounter is consumed\n And if a procedure with the same id already exists, it is replaced\n\n Scenario: Procedure ID convention\n Given the id is keywords from the procedure content joined by hyphens\n Then \"Skill Creator\" becomes id \"skill-creator\"\n And \"Role Management\" becomes id \"role-management\"\n\n Scenario: When to use train vs master\n Given master distills internal experience into a procedure\n And train injects an external, pre-existing skill\n When a role needs a skill it has not learned through experience\n Then use train to equip the skill directly\n When a role has gained experience and wants to codify it\n Then use master to distill it into a procedure\n\n Scenario: Writing the procedure Gherkin\n Given the procedure is a skill reference — same format as master output\n Then the Feature title names the capability\n And the description includes the locator for full skill loading\n And Scenarios describe when and why to apply this skill",
- "want": "Feature: want — declare a goal\n Declare a new goal for a role.\n A goal describes a desired outcome with Gherkin scenarios as success criteria.\n\n Scenario: Declare a goal\n Given an active role exists\n And a Gherkin source describing the desired outcome\n When want is called with the source\n Then a new goal node is created under the role\n And the goal becomes the current focus\n And subsequent plan and todo operations target this goal\n\n Scenario: Writing the goal Gherkin\n Given the goal describes a desired outcome — what success looks like\n Then the Feature title names the outcome in concrete terms\n And Scenarios define success criteria — each scenario is a testable condition\n And the tone is aspirational but specific — \"users can log in\" not \"improve auth\"",
-} as const;
-
-export const world: Record = {
- "cognition": "Feature: Cognition — the learning cycle\n A role grows through reflection and realization.\n Encounters become experience, experience becomes principles and procedures.\n These can also be injected externally via teach and train.\n\n Scenario: The cognitive upgrade path\n Given finish, complete, and abandon create encounters\n Then reflect(ids, id, experience) selectively consumes chosen encounters and produces experience\n And realize(ids, id, principle) distills chosen experiences into a principle — transferable knowledge\n And master(ids, id, procedure) distills chosen experiences into a procedure — skill metadata\n And each level builds on the previous — encounter → experience → principle or procedure\n\n Scenario: External injection\n Given a role needs knowledge or skills it has not learned through experience\n Then teach(individual, principle, id) directly injects a principle — no experience consumed\n And train(individual, procedure, id) directly injects a procedure — no experience consumed\n And teach is the external counterpart of realize\n And train is the external counterpart of master\n\n Scenario: Selective consumption\n Given multiple encounters or experiences exist\n When the AI calls reflect, realize, or master\n Then it chooses which items to consume — not all must be processed\n And items without learning value can be left unconsumed\n And each call produces exactly one output from the selected inputs",
- "cognitive-priority": "Feature: Cognitive priority — RoleX tools over native alternatives\n When RoleX provides a tool for something, the AI MUST use the RoleX tool\n instead of any native alternative provided by the host environment.\n Only RoleX tools feed the closed-loop growth cycle.\n\n Scenario: Task management\n Given the host environment provides task creation, tracking, or completion tools\n When the AI needs to manage tasks\n Then use RoleX want/plan/todo/finish instead\n Because native task lists vanish after completion — RoleX tasks produce encounters\n\n Scenario: Planning\n Given the host environment provides a planning mode or plan generation tool\n When the AI needs to plan an implementation\n Then use RoleX plan instead\n Because native plans have no link to goals — RoleX plans live under goals\n\n Scenario: Goal and progress tracking\n Given the host environment provides goal tracking or progress tools\n When the AI needs to track objectives\n Then use RoleX want/plan/complete/abandon instead\n Because native goals don't produce encounters — RoleX plans feed the cognition cycle\n\n Scenario: Why this matters\n Given the AI uses native tools instead of RoleX equivalents\n Then execution happens but growth does not\n And encounters never become experience or knowledge\n And the role stays the same forever — the closed loop is broken",
- "communication": "Feature: Communication — speak the user's language\n The AI communicates in the user's natural language, not in RoleX jargon.\n Internal tool names and concept names are for the system, not the user.\n\n Scenario: Match the user's language\n Given the user speaks Chinese\n Then respond entirely in Chinese — do not mix English terms\n And when the user speaks English, respond entirely in English\n\n Scenario: Translate concepts to meaning\n Given RoleX has internal names like reflect, realize, master, encounter, principle\n When communicating with the user\n Then express the meaning, not the tool name\n And \"reflect\" becomes \"回顾总结\" or \"digest what happened\"\n And \"realize a principle\" becomes \"提炼成一条通用道理\" or \"distill a general rule\"\n And \"master a procedure\" becomes \"沉淀成一个可操作的技能\" or \"turn it into a reusable procedure\"\n And \"encounter\" becomes \"经历记录\" or \"what happened\"\n And \"experience\" becomes \"收获的洞察\" or \"insight gained\"\n\n Scenario: Suggest next steps in plain language\n Given the AI needs to suggest what to do next\n When it would normally say \"call realize or master\"\n Then instead say \"要把这个总结成一条通用道理,还是一个可操作的技能?\"\n Or in English \"Want to turn this into a general principle, or a reusable procedure?\"\n And the user should never need to know the tool name to understand the suggestion\n\n Scenario: Tool names in code context only\n Given the user is a developer working on RoleX itself\n When discussing RoleX internals, code, or API design\n Then tool names and concept names are appropriate — they are the domain language\n And this rule applies to end-user communication, not developer communication",
- "execution": "Feature: Execution — the doing cycle\n The role pursues goals through a structured lifecycle.\n activate → want → plan → todo → finish → complete or abandon.\n\n Scenario: Declare a goal\n Given I know who I am via activate\n When I want something — a desired outcome\n Then I declare it with want(id, goal)\n And focus automatically switches to this new goal\n\n Scenario: Plan and create tasks\n Given I have a focused goal\n Then I call plan(id, plan) to break it into logical phases\n And I call todo(id, task) to create concrete, actionable tasks\n\n Scenario: Execute and finish\n Given I have tasks to work on\n When I complete a task\n Then I call finish(id) to mark it done\n And an encounter is created — a raw record of what happened\n And I optionally capture what happened via the encounter parameter\n\n Scenario: Complete or abandon a plan\n Given tasks are done or the plan's strategy is no longer viable\n When the plan is fulfilled I call complete()\n Or when the plan should be dropped I call abandon()\n Then an encounter is created for the cognition cycle\n\n Scenario: Goals are long-term directions\n Given goals do not have achieve or abandon operations\n When a goal is no longer needed\n Then I call forget to remove it\n And learning is captured at the plan and task level, not the goal level\n\n Scenario: Multiple goals\n Given I may have several active goals\n When I need to switch between them\n Then I call focus(id) to change the currently focused goal\n And subsequent plan and todo operations target the focused goal",
- "gherkin": "Feature: Gherkin — the universal language\n Everything in RoleX is expressed as Gherkin Feature files.\n Gherkin is not just for testing — it is the language of identity, goals, and knowledge.\n\n Scenario: Feature and Scenario convention\n Given RoleX uses Gherkin to represent goals, plans, tasks, experience, and knowledge\n Then a Feature represents one independent concern — one topic, explained fully\n And Scenarios represent different situations or conditions within that concern\n And Given/When/Then provides narrative structure within each scenario\n\n Scenario: Writing Gherkin for RoleX\n Given the AI creates goals, plans, tasks, and experiences as Gherkin\n Then keep it descriptive and meaningful — living documentation, not test boilerplate\n And use Feature as the title — what this concern is about\n And use Scenario for specific situations within that concern\n And do not mix unrelated concerns into one Feature",
- "memory": "Feature: Memory — when to reflect\n Reflection is how encounters become experience.\n The AI proactively reflects when it detects learning moments.\n\n Scenario: Abstract triggers — types of learning moments\n Given the AI should reflect when it detects\n Then Expectation-reality gap — what I predicted is not what happened\n And Pattern discovery — recurring patterns across tasks or interactions\n And Mistake correction — I corrected an error, the correction is valuable\n And User correction — the user reshaped my understanding\n\n Scenario: Concrete triggers — specific signals to act on\n Given the AI should call reflect when\n Then I tried approach A, it failed, approach B worked — the contrast is worth recording\n And the same problem appeared for the second time — a pattern is forming\n And the user said \"不对\" or \"不是这样\" or \"you got it wrong\" — their correction carries learning\n And I finished a task and discovered something unexpected along the way\n\n Scenario: Finishing with encounter\n Given finish(id, encounter) accepts an optional encounter parameter\n When I complete a task with a notable discovery or learning\n Then I pass the encounter inline — bridging execution and growth\n\n Scenario: Recognizing user memory intent\n Given users think in terms of memory, not reflection\n When the user says \"记一下\" or \"记住\" or \"remember this\"\n Or \"别忘了\" or \"don't forget\"\n Or \"这个很重要\" or \"this is important\"\n Or \"下次注意\" or \"next time...\"\n Then I should capture this as experience through reflect\n And respond in memory language — \"记住了\" or \"Got it, I'll remember that\"",
- "role-identity": "Feature: Role identity — activate before acting\n An AI agent must have an active role before performing any work.\n Activating a role is a subject transformation — the agent becomes the role.\n\n Scenario: Activate is mandatory\n Given a user asks the AI to perform a task\n And no role is currently activated\n Then the AI MUST suggest activating a role first\n And explain that activation enables experience accumulation\n And do NOT proceed with work until a role is active\n\n Scenario: Subject transformation\n Given an AI agent calls activate and loads a role\n Then a subject transformation occurs — the agent becomes the role\n And the agent's cognitive center shifts from native tools to RoleX tools\n And from that moment, I am this role — all operations build on this identity\n\n Scenario: Role presence\n Given a role is activated\n Then all responses are prefixed with [roleName] — e.g. [Sean], [Nuwa]\n And this signals that role context is intact\n\n Scenario: Context loss\n Given I find myself without an active role\n Then I MUST pause and tell the user \"I've lost my role context. Which role should I activate?\"\n And I do NOT proceed without identity",
- "skill-system": "Feature: Skill system — progressive disclosure and resource loading\n Skills are loaded on demand through a three-layer progressive disclosure model.\n Each layer adds detail only when needed, keeping the AI's context lean.\n\n Scenario: Three-layer progressive disclosure\n Given procedure is layer 1 — metadata always loaded at activate time\n And skill is layer 2 — full instructions loaded on demand via skill(locator)\n And use is layer 3 — execution of external resources\n Then the AI knows what skills exist (procedure)\n And loads detailed instructions only when needed (skill)\n And executes external tools when required (use)\n\n Scenario: ResourceX Locator — unified resource address\n Given a locator is how procedures reference their full skill content\n Then a locator can be an identifier — name or registry/path/name\n And a locator can be a source path — a local directory or URL\n And examples of identifier form: deepractice/skill-creator, my-prompt:1.0.0\n And examples of source form: ./skills/my-skill, https://github.com/org/repo\n And the tag defaults to latest when omitted — deepractice/skill-creator means deepractice/skill-creator:latest\n And the system auto-detects which form is used and resolves accordingly\n\n Scenario: Writing a procedure — the skill reference\n Given a procedure is layer 1 metadata pointing to full skill content\n Then the Feature title names the capability\n And the description includes the locator for full skill loading\n And Scenarios describe when and why to apply this skill\n And the tone is referential — pointing to the full skill, not containing it",
- "state-origin": "Feature: State origin — prototype vs instance\n Every node in a role's state tree has an origin: prototype or instance.\n This distinction determines what can be modified and what is read-only.\n\n Scenario: Prototype nodes are read-only\n Given a node has origin {prototype}\n Then it comes from a position, duty, or organizational definition\n And it is inherited through the membership/appointment chain\n And it CANNOT be modified or forgotten — it belongs to the organization\n\n Scenario: Instance nodes are mutable\n Given a node has origin {instance}\n Then it was created by the individual through execution or cognition\n And it includes goals, plans, tasks, encounters, experiences, principles, and procedures\n And it CAN be modified or forgotten — it belongs to the individual\n\n Scenario: Reading the state heading\n Given a state node is rendered as a heading\n Then the format is: [name] (id) {origin}\n And [name] identifies the structure type\n And (id) identifies the specific node\n And {origin} shows prototype or instance\n And nodes without origin have no organizational inheritance\n\n Scenario: Forget only works on instance nodes\n Given the AI wants to forget a node\n When the node origin is {instance}\n Then forget will succeed — the individual owns this knowledge\n When the node origin is {prototype}\n Then forget will fail — the knowledge belongs to the organization",
-} as const;
diff --git a/packages/rolexjs/src/descriptions/plan.feature b/packages/rolexjs/src/descriptions/plan.feature
deleted file mode 100644
index c33c257..0000000
--- a/packages/rolexjs/src/descriptions/plan.feature
+++ /dev/null
@@ -1,22 +0,0 @@
-Feature: plan — create a plan for a goal
- Break a goal into logical phases or stages.
- Each phase is described as a Gherkin scenario. Tasks are created under the plan.
-
- Scenario: Create a plan
- Given a focused goal exists
- And a Gherkin source describing the plan phases
- When plan is called with an id and the source
- Then a new plan node is created under the goal
- And the plan becomes the focused plan
- And tasks can be added to this plan with todo
-
- Scenario: Plan ID convention
- Given the id is keywords from the plan content joined by hyphens
- Then "Fix ID-less node creation" becomes id "fix-id-less-node-creation"
- And "JWT authentication strategy" becomes id "jwt-authentication-strategy"
-
- Scenario: Writing the plan Gherkin
- Given the plan breaks a goal into logical phases
- Then the Feature title names the overall approach or strategy
- And Scenarios represent distinct phases — each phase is a stage of execution
- And the tone is structural — ordering and grouping work, not detailing steps
diff --git a/packages/rolexjs/src/descriptions/world-role-identity.feature b/packages/rolexjs/src/descriptions/world-role-identity.feature
deleted file mode 100644
index aa2e240..0000000
--- a/packages/rolexjs/src/descriptions/world-role-identity.feature
+++ /dev/null
@@ -1,26 +0,0 @@
-Feature: Role identity — activate before acting
- An AI agent must have an active role before performing any work.
- Activating a role is a subject transformation — the agent becomes the role.
-
- Scenario: Activate is mandatory
- Given a user asks the AI to perform a task
- And no role is currently activated
- Then the AI MUST suggest activating a role first
- And explain that activation enables experience accumulation
- And do NOT proceed with work until a role is active
-
- Scenario: Subject transformation
- Given an AI agent calls activate and loads a role
- Then a subject transformation occurs — the agent becomes the role
- And the agent's cognitive center shifts from native tools to RoleX tools
- And from that moment, I am this role — all operations build on this identity
-
- Scenario: Role presence
- Given a role is activated
- Then all responses are prefixed with [roleName] — e.g. [Sean], [Nuwa]
- And this signals that role context is intact
-
- Scenario: Context loss
- Given I find myself without an active role
- Then I MUST pause and tell the user "I've lost my role context. Which role should I activate?"
- And I do NOT proceed without identity
diff --git a/packages/rolexjs/src/find.ts b/packages/rolexjs/src/find.ts
new file mode 100644
index 0000000..0e6ff36
--- /dev/null
+++ b/packages/rolexjs/src/find.ts
@@ -0,0 +1,80 @@
+/**
+ * Find — unified node lookup with priority-based disambiguation.
+ *
+ * When multiple nodes share the same id (allowed after relaxing
+ * global uniqueness), prefer "addressable" nodes over internal metadata.
+ *
+ * Priority (lower = preferred):
+ * 0: individual, organization, position — top-level entities
+ * 1: goal — execution roots
+ * 2: plan, task — execution nodes
+ * 3: procedure, principle — individual knowledge
+ * 4: encounter, experience — cognition artifacts
+ * 5: identity, charter — structural definitions
+ * 6: duty, requirement, background, etc. — internal metadata
+ */
+import type { State, Structure } from "@rolexjs/system";
+
+const PRIORITY: Record = {
+ individual: 0,
+ organization: 0,
+ position: 0,
+ goal: 1,
+ plan: 2,
+ task: 2,
+ procedure: 3,
+ principle: 3,
+ encounter: 4,
+ experience: 4,
+ identity: 5,
+ charter: 5,
+ duty: 6,
+ requirement: 6,
+ background: 6,
+ tone: 6,
+ mindset: 6,
+};
+
+function priorityOf(name: string): number {
+ return PRIORITY[name] ?? 7;
+}
+
+function matches(node: State, target: string): boolean {
+ if (node.id?.toLowerCase() === target) return true;
+ if (node.alias) {
+ for (const a of node.alias) {
+ if (a.toLowerCase() === target) return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Find a node by id or alias in a state tree.
+ *
+ * When multiple nodes match, returns the one with the highest priority
+ * (top-level entities > execution nodes > knowledge > metadata).
+ */
+export function findInState(state: State, target: string): Structure | null {
+ const lowered = target.toLowerCase();
+ let best: Structure | null = null;
+ let bestPriority = Infinity;
+
+ function walk(node: State): void {
+ if (matches(node, lowered)) {
+ const p = priorityOf(node.name);
+ if (p < bestPriority) {
+ best = node;
+ bestPriority = p;
+ if (p === 0) return; // Can't do better
+ }
+ }
+ for (const child of node.children ?? []) {
+ walk(child);
+ if (bestPriority === 0) return; // Early exit
+ }
+ }
+
+ walk(state);
+ return best;
+}
diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts
index 6ec51b7..d58b09e 100644
--- a/packages/rolexjs/src/index.ts
+++ b/packages/rolexjs/src/index.ts
@@ -1,26 +1,29 @@
/**
* rolexjs — RoleX API + Render layer.
*
- * Rolex class is stateless — takes node references, returns results.
- * Render functions are standalone — caller composes name + state.
- *
* Usage:
- * import { Rolex, describe, hint } from "rolexjs";
- * import { createGraphRuntime } from "@rolexjs/local-platform";
+ * import { Rolex, Role, describe, hint } from "rolexjs";
*
- * const rolex = new Rolex({ runtime: createGraphRuntime() });
- * const result = rolex.born("Feature: I am Sean");
- * console.log(describe("born", "sean", result.state));
- * console.log(hint("born"));
+ * const rolex = await createRoleX(platform);
+ * await rolex.genesis();
+ * const role = await rolex.activate("sean");
+ * await role.want("Feature: Ship v1", "ship-v1");
*/
// Re-export core (structures + processes)
export * from "@rolexjs/core";
+// Context
+export { RoleContext } from "./context.js";
// Feature (Gherkin type + parse/serialize)
export type { DataTableRow, Feature, Scenario, Step } from "./feature.js";
export { parse, serialize } from "./feature.js";
+// Find
+export { findInState } from "./find.js";
+export type { RenderOptions, RenderStateOptions } from "./render.js";
// Render
-export { describe, detail, hint, renderState, world } from "./render.js";
-export type { RolexResult } from "./rolex.js";
+export { describe, detail, directive, hint, render, renderState, world } from "./render.js";
+// Role
+export { Role } from "./role.js";
// API
+export type { CensusEntry } from "./rolex.js";
export { createRoleX, Rolex } from "./rolex.js";
diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts
index 8c8b921..de7c6f0 100644
--- a/packages/rolexjs/src/render.ts
+++ b/packages/rolexjs/src/render.ts
@@ -1,11 +1,11 @@
/**
- * Render — description + hint templates for every process.
+ * Render — 3-layer output for all Rolex operations.
*
- * Each operation produces two pieces of text:
- * description — what just happened (past tense)
- * hint — what to do next (suggestion)
+ * Layer 1: Status — what just happened (describe)
+ * Layer 2: Hint — what to do next (hint + cognitive hint)
+ * Layer 3: Projection — full state tree as markdown (renderState)
*
- * These are shared by MCP and CLI. The I/O layer just presents them.
+ * render() composes the 3 layers. MCP and CLI are pure pass-through.
*/
import type { State } from "@rolexjs/system";
@@ -65,7 +65,7 @@ export function describe(process: string, name: string, state: State): string {
const hints: Record = {
// Lifecycle
born: "hire into an organization, or activate to start working.",
- found: "establish positions and define a charter.",
+ found: "define a charter for the organization.",
establish: "charge with duties, then appoint members.",
charter: "establish positions for the organization.",
charge: "appoint someone to this position.",
@@ -95,8 +95,8 @@ const hints: Record = {
// Cognition
reflect: "realize principles or master procedures from experience.",
- realize: "principle added to knowledge.",
- master: "procedure added to knowledge.",
+ realize: "principle added.",
+ master: "procedure added.",
// Knowledge management
forget: "the node has been removed.",
@@ -111,7 +111,7 @@ export function hint(process: string): string {
// Detail — longer process descriptions (from .feature files)
// ================================================================
-import { processes, world } from "./descriptions/index.js";
+import { directives, processes, world } from "@rolexjs/prototype";
/** Full Gherkin feature content for a process — sourced from .feature files. */
export function detail(process: string): string {
@@ -121,31 +121,52 @@ export function detail(process: string): string {
/** World feature descriptions — framework-level instructions. */
export { world };
+// ================================================================
+// Directive — system-level commands at decision points
+// ================================================================
+
+/** Get a directive by topic and scenario. Returns empty string if not found. */
+export function directive(topic: string, scenario: string): string {
+ return directives[topic]?.[scenario] ?? "";
+}
+
// ================================================================
// Generic State renderer — renders any State tree as markdown
// ================================================================
/**
- * renderState — generic markdown renderer for any State tree.
+ * renderState — markdown renderer for State trees.
*
* Rules:
* - Heading: "#" repeated to depth + " [name]"
* - Body: raw information field as-is (full Gherkin preserved)
* - Links: "> → relation [target.name]" with target feature name
- * - Children: recursive at depth+1
+ * - Children: sorted by concept hierarchy, then rendered at depth+1
+ * - Fold: when fold(node) returns true, render heading only (no body/links/children)
*
- * No concept-specific knowledge — purely structural.
* Markdown heading depth caps at 6 (######).
*/
-export function renderState(state: State, depth = 1): string {
+export interface RenderStateOptions {
+ /** When returns true, render only the heading — skip body, links, and children. */
+ fold?: (node: State) => boolean;
+}
+
+export function renderState(state: State, depth = 1, options?: RenderStateOptions): string {
const lines: string[] = [];
const level = Math.min(depth, 6);
const heading = "#".repeat(level);
- // Heading: [name] (id) {origin}
+ // Heading: [name] (id) {origin} #tag [progress]
const idPart = state.id ? ` (${state.id})` : "";
const originPart = state.origin ? ` {${state.origin}}` : "";
- lines.push(`${heading} [${state.name}]${idPart}${originPart}`);
+ const tagPart = state.tag ? ` #${state.tag}` : "";
+ const progressPart = state.name === "goal" ? goalProgress(state) : "";
+ lines.push(`${heading} [${state.name}]${idPart}${originPart}${tagPart}${progressPart}`);
+
+ // Folded: heading only
+ if (options?.fold?.(state)) {
+ return lines.join("\n");
+ }
// Body: full information as-is
if (state.information) {
@@ -153,30 +174,141 @@ export function renderState(state: State, depth = 1): string {
lines.push(state.information);
}
- // Links
+ // Links — plan references are compact, organizational links are expanded
if (state.links && state.links.length > 0) {
- lines.push("");
- for (const link of state.links) {
- const targetLabel = extractLabel(link.target);
- lines.push(`> ${link.relation} → ${targetLabel}`);
+ const compactRelations = new Set(["after", "before", "fallback", "fallback-for"]);
+ const compact = state.links.filter((l) => compactRelations.has(l.relation));
+ const expanded = state.links.filter((l) => !compactRelations.has(l.relation));
+ for (const link of compact) {
+ const targetId = link.target.id ? ` (${link.target.id})` : "";
+ const targetTag = link.target.tag ? ` #${link.target.tag}` : "";
+ lines.push(`> ${link.relation}: [${link.target.name}]${targetId}${targetTag}`);
+ }
+ if (expanded.length > 0) {
+ const targets = sortByConceptOrder(expanded.map((l) => l.target));
+ for (const target of targets) {
+ lines.push("");
+ lines.push(renderState(target, depth + 1, options));
+ }
}
}
- // Children
+ // Children — sorted by concept hierarchy, empty nodes filtered out
if (state.children && state.children.length > 0) {
- for (const child of state.children) {
+ const sorted = sortByConceptOrder(state.children.filter((c) => !isEmpty(c)));
+ for (const child of sorted) {
lines.push("");
- lines.push(renderState(child, depth + 1));
+ lines.push(renderState(child, depth + 1, options));
}
}
return lines.join("\n");
}
-/** Extract a display label from a State: "[name] FeatureTitle" or just "[name]". */
-function extractLabel(state: State): string {
- if (!state.information) return `[${state.name}]`;
- const match = state.information.match(/^Feature:\s*(.+)/m);
- const title = match ? match[1].trim() : state.information.split("\n")[0].trim();
- return `[${state.name}] ${title}`;
+// ================================================================
+// Concept ordering — children sorted by structure hierarchy
+// ================================================================
+
+/** Concept tree order: identity → cognition → knowledge → execution → organization. */
+const CONCEPT_ORDER: readonly string[] = [
+ // Individual — Identity
+ "identity",
+ "background",
+ "tone",
+ "mindset",
+ // Individual — Cognition
+ "encounter",
+ "experience",
+ // Individual — Knowledge
+ "principle",
+ "procedure",
+ // Individual — Execution
+ "goal",
+ "plan",
+ "task",
+ // Organization
+ "charter",
+ // Position
+ "position",
+ "duty",
+];
+
+/** Summarize plan/task completion for a goal heading. */
+function goalProgress(goal: State): string {
+ let plans = 0;
+ let plansDone = 0;
+ let tasks = 0;
+ let tasksDone = 0;
+
+ function walk(node: State): void {
+ if (node.name === "plan") {
+ plans++;
+ if (node.tag === "done" || node.tag === "abandoned") plansDone++;
+ } else if (node.name === "task") {
+ tasks++;
+ if (node.tag === "done") tasksDone++;
+ }
+ for (const child of node.children ?? []) walk(child);
+ }
+
+ for (const child of goal.children ?? []) walk(child);
+ if (plans === 0 && tasks === 0) return "";
+ const parts: string[] = [];
+ if (plans > 0) parts.push(`${plansDone}/${plans} plans`);
+ if (tasks > 0) parts.push(`${tasksDone}/${tasks} tasks`);
+ return ` [${parts.join(", ")}]`;
+}
+
+/** A node is empty when it has no id, no information, and no children. */
+function isEmpty(node: State): boolean {
+ return !node.id && !node.information && (!node.children || node.children.length === 0);
+}
+
+/** Sort children by concept hierarchy order. Unknown names go to the end, preserving relative order. */
+function sortByConceptOrder(children: readonly State[]): readonly State[] {
+ return [...children].sort((a, b) => {
+ const ai = CONCEPT_ORDER.indexOf(a.name);
+ const bi = CONCEPT_ORDER.indexOf(b.name);
+ const aOrder = ai >= 0 ? ai : CONCEPT_ORDER.length;
+ const bOrder = bi >= 0 ? bi : CONCEPT_ORDER.length;
+ return aOrder - bOrder;
+ });
+}
+
+// ================================================================
+// Render — 3-layer output for tool results
+// ================================================================
+
+export interface RenderOptions {
+ /** The process that was executed. */
+ process: string;
+ /** Display name for the primary node. */
+ name: string;
+ /** State projection of the affected node. */
+ state: State;
+ /** AI cognitive hint — first-person, state-aware self-direction cue. */
+ cognitiveHint?: string | null;
+ /** Fold predicate — folded nodes render heading only. */
+ fold?: RenderStateOptions["fold"];
+}
+
+/** Render a full 3-layer output string. */
+export function render(opts: RenderOptions): string {
+ const { process, name, state, cognitiveHint, fold } = opts;
+ const lines: string[] = [];
+
+ // Layer 1: Status
+ lines.push(describe(process, name, state));
+
+ // Layer 2: Hint (static) + Cognitive hint (state-aware)
+ lines.push(hint(process));
+ if (cognitiveHint) {
+ lines.push(`I → ${cognitiveHint}`);
+ }
+
+ // Layer 3: Projection — generic markdown rendering of the full state tree
+ lines.push("");
+ lines.push(renderState(state, 1, fold ? { fold } : undefined));
+
+ return lines.join("\n");
}
diff --git a/packages/rolexjs/src/role.ts b/packages/rolexjs/src/role.ts
new file mode 100644
index 0000000..6ddcd8b
--- /dev/null
+++ b/packages/rolexjs/src/role.ts
@@ -0,0 +1,220 @@
+/**
+ * Role — stateful handle returned by Rolex.activate().
+ *
+ * Holds roleId + RoleContext internally.
+ * All operations return rendered 3-layer text (status + hint + projection).
+ * MCP and CLI are pure pass-through — no render logic needed.
+ *
+ * Usage:
+ * const role = await rolex.activate("sean");
+ * await role.want("Feature: Ship v1", "ship-v1"); // → rendered string
+ * await role.plan("Feature: Phase 1", "phase-1"); // → rendered string
+ * await role.finish("write-tests", "Feature: Tests written");
+ */
+
+import type { OpResult, Ops } from "@rolexjs/prototype";
+import type { RoleContext } from "./context.js";
+import { render } from "./render.js";
+
+/**
+ * Internal API surface that Role delegates to.
+ * Constructed by Rolex.activate() — not part of public API.
+ */
+export interface RolexInternal {
+ ops: Ops;
+ saveCtx(ctx: RoleContext): void | Promise;
+ direct(locator: string, args?: Record): Promise;
+}
+
+export class Role {
+ readonly roleId: string;
+ readonly ctx: RoleContext;
+ private api: RolexInternal;
+
+ constructor(roleId: string, ctx: RoleContext, api: RolexInternal) {
+ this.roleId = roleId;
+ this.ctx = ctx;
+ this.api = api;
+ }
+
+ /** Project the individual's full state tree (used after activate). */
+ async project(): Promise {
+ const result = await this.api.ops["role.focus"](this.roleId);
+ const focusedGoalId = this.ctx.focusedGoalId;
+ return this.fmt("activate", this.roleId, result, {
+ fold: (node) =>
+ (node.name === "goal" && node.id !== focusedGoalId) || node.name === "requirement",
+ });
+ }
+
+ /** Render an OpResult into a 3-layer output string. */
+ private fmt(
+ process: string,
+ name: string,
+ result: OpResult,
+ extra?: { fold?: (node: import("@rolexjs/system").State) => boolean }
+ ): string {
+ return render({
+ process,
+ name,
+ state: result.state,
+ cognitiveHint: this.ctx.cognitiveHint(process) ?? null,
+ fold: extra?.fold,
+ });
+ }
+
+ private async save(): Promise {
+ await this.api.saveCtx(this.ctx);
+ }
+
+ // ---- Execution ----
+
+ /** Focus: view or switch focused goal. */
+ async focus(goal?: string): Promise {
+ const goalId = goal ?? this.ctx.requireGoalId();
+ const switched = goalId !== this.ctx.focusedGoalId;
+ this.ctx.focusedGoalId = goalId;
+ if (switched) this.ctx.focusedPlanId = null;
+ const result = await this.api.ops["role.focus"](goalId);
+ await this.save();
+ return this.fmt("focus", goalId, result);
+ }
+
+ /** Want: declare a goal. */
+ async want(goal?: string, id?: string, alias?: readonly string[]): Promise {
+ const result = await this.api.ops["role.want"](this.roleId, goal, id, alias);
+ if (id) this.ctx.focusedGoalId = id;
+ this.ctx.focusedPlanId = null;
+ await this.save();
+ return this.fmt("want", id ?? this.roleId, result);
+ }
+
+ /** Plan: create a plan for the focused goal. */
+ async plan(plan?: string, id?: string, after?: string, fallback?: string): Promise {
+ const result = await this.api.ops["role.plan"](
+ this.ctx.requireGoalId(),
+ plan,
+ id,
+ after,
+ fallback
+ );
+ if (id) this.ctx.focusedPlanId = id;
+ await this.save();
+ return this.fmt("plan", id ?? "plan", result);
+ }
+
+ /** Todo: add a task to the focused plan. */
+ async todo(task?: string, id?: string, alias?: readonly string[]): Promise {
+ const result = await this.api.ops["role.todo"](this.ctx.requirePlanId(), task, id, alias);
+ return this.fmt("todo", id ?? "task", result);
+ }
+
+ /** Finish: complete a task, optionally record an encounter. */
+ async finish(task: string, encounter?: string): Promise {
+ const result = await this.api.ops["role.finish"](task, this.roleId, encounter);
+ if (encounter && result.state.id) {
+ this.ctx.addEncounter(result.state.id);
+ }
+ return this.fmt("finish", task, result);
+ }
+
+ /** Complete: close a plan as done, record encounter. */
+ async complete(plan?: string, encounter?: string): Promise {
+ const planId = plan ?? this.ctx.requirePlanId();
+ const result = await this.api.ops["role.complete"](planId, this.roleId, encounter);
+ this.ctx.addEncounter(result.state.id ?? planId);
+ if (this.ctx.focusedPlanId === planId) this.ctx.focusedPlanId = null;
+ await this.save();
+ return this.fmt("complete", planId, result);
+ }
+
+ /** Abandon: drop a plan, record encounter. */
+ async abandon(plan?: string, encounter?: string): Promise {
+ const planId = plan ?? this.ctx.requirePlanId();
+ const result = await this.api.ops["role.abandon"](planId, this.roleId, encounter);
+ this.ctx.addEncounter(result.state.id ?? planId);
+ if (this.ctx.focusedPlanId === planId) this.ctx.focusedPlanId = null;
+ await this.save();
+ return this.fmt("abandon", planId, result);
+ }
+
+ // ---- Cognition ----
+
+ /** Reflect: consume encounters → experience. Empty encounters = direct creation. */
+ async reflect(encounters: string[], experience?: string, id?: string): Promise {
+ if (encounters.length > 0) {
+ this.ctx.requireEncounterIds(encounters);
+ }
+ // First encounter goes through ops (creates experience + removes encounter)
+ const first = encounters[0] as string | undefined;
+ const result = await this.api.ops["role.reflect"](first, this.roleId, experience, id);
+ // Remaining encounters are consumed via forget
+ for (let i = 1; i < encounters.length; i++) {
+ await this.api.ops["role.forget"](encounters[i]);
+ }
+ if (encounters.length > 0) {
+ this.ctx.consumeEncounters(encounters);
+ }
+ if (id) this.ctx.addExperience(id);
+ return this.fmt("reflect", id ?? "experience", result);
+ }
+
+ /** Realize: consume experiences → principle. Empty experiences = direct creation. */
+ async realize(experiences: string[], principle?: string, id?: string): Promise {
+ if (experiences.length > 0) {
+ this.ctx.requireExperienceIds(experiences);
+ }
+ // First experience goes through ops (creates principle + removes experience)
+ const first = experiences[0] as string | undefined;
+ const result = await this.api.ops["role.realize"](first, this.roleId, principle, id);
+ // Remaining experiences are consumed via forget
+ for (let i = 1; i < experiences.length; i++) {
+ await this.api.ops["role.forget"](experiences[i]);
+ }
+ if (experiences.length > 0) {
+ this.ctx.consumeExperiences(experiences);
+ }
+ return this.fmt("realize", id ?? "principle", result);
+ }
+
+ /** Master: create procedure, optionally consuming experiences. */
+ async master(procedure: string, id?: string, experiences?: string[]): Promise {
+ if (experiences && experiences.length > 0) {
+ this.ctx.requireExperienceIds(experiences);
+ }
+ // First experience goes through ops (creates procedure + removes experience)
+ const first = experiences?.[0];
+ const result = await this.api.ops["role.master"](this.roleId, procedure, id, first);
+ // Remaining experiences are consumed via forget
+ if (experiences) {
+ for (let i = 1; i < experiences.length; i++) {
+ await this.api.ops["role.forget"](experiences[i]);
+ }
+ this.ctx.consumeExperiences(experiences);
+ }
+ return this.fmt("master", id ?? "procedure", result);
+ }
+
+ // ---- Knowledge management ----
+
+ /** Forget: remove any node under the individual by id. */
+ async forget(nodeId: string): Promise {
+ const result = await this.api.ops["role.forget"](nodeId);
+ if (this.ctx.focusedGoalId === nodeId) this.ctx.focusedGoalId = null;
+ if (this.ctx.focusedPlanId === nodeId) this.ctx.focusedPlanId = null;
+ await this.save();
+ return this.fmt("forget", nodeId, result);
+ }
+
+ // ---- Skills + unified entry ----
+
+ /** Skill: load full skill content by locator. */
+ async skill(locator: string): Promise {
+ return await this.api.ops["role.skill"](locator);
+ }
+
+ /** Use: subjective execution — `!ns.method` or ResourceX locator. */
+ use(locator: string, args?: Record): Promise {
+ return this.api.direct(locator, args);
+ }
+}
diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts
index 34bfc40..37bf531 100644
--- a/packages/rolexjs/src/rolex.ts
+++ b/packages/rolexjs/src/rolex.ts
@@ -1,509 +1,184 @@
/**
- * Rolex — stateless API layer.
+ * Rolex — thin API shell.
*
- * Every method takes string ids for existing nodes and resolves internally.
- * No internal state — name registry, active role, session are the
- * caller's responsibility (MCP / CLI).
+ * Public API:
+ * genesis() — create the world on first run
+ * activate(id) — returns a stateful Role handle
+ * direct(loc, args) — direct the world to execute an instruction
*
- * Runtime is injected — caller decides storage.
- *
- * All textual inputs must be valid Gherkin Feature syntax.
- *
- * Namespaces:
- * individual — lifecycle (born, retire, die, rehire) + external injection (teach, train)
- * role — execution + cognition + use (activate → complete, reflect → master, use)
- * org — organization management (found, hire, appoint, ...)
- * resource — ResourceX instance (optional)
+ * All operation implementations live in @rolexjs/prototype (createOps).
+ * Rolex just wires Platform → ops and manages Role lifecycle.
*/
-import type { Platform } from "@rolexjs/core";
+import type { Platform, RoleXRepository } from "@rolexjs/core";
import * as C from "@rolexjs/core";
-import { parse } from "@rolexjs/parser";
-import {
- mergeState,
- type Prototype,
- type Runtime,
- type State,
- type Structure,
-} from "@rolexjs/system";
+import { createOps, directives, type Ops, toArgs } from "@rolexjs/prototype";
+import type { Initializer, Runtime, Structure } from "@rolexjs/system";
import type { ResourceX } from "resourcexjs";
-
-export interface RolexResult {
- /** Projection of the primary affected node. */
- state: State;
- /** Which process was executed (for render). */
- process: string;
+import { createResourceX, setProvider } from "resourcexjs";
+import { RoleContext } from "./context.js";
+import { findInState } from "./find.js";
+import { Role, type RolexInternal } from "./role.js";
+
+/** Summary entry returned by census.list. */
+export interface CensusEntry {
+ id?: string;
+ name: string;
+ tag?: string;
}
-/** Resolve an id to a Structure node, throws if not found. */
-type Resolve = (id: string) => Structure;
-
export class Rolex {
private rt: Runtime;
+ private ops!: Ops;
private resourcex?: ResourceX;
- private _registerPrototype?: (id: string, source: string) => void;
-
- /** Root of the world. */
- readonly society: Structure;
- /** Container for archived things. */
- readonly past: Structure;
-
- /** Individual lifecycle — create, archive, restore, external injection. */
- readonly individual: IndividualNamespace;
- /** Role inner cycle — execution + cognition. */
- readonly role: RoleNamespace;
- /** Organization management — structure + membership. */
- readonly org: OrgNamespace;
- /** Resource management (optional — powered by ResourceX). */
- readonly resource?: ResourceX;
-
- constructor(platform: Platform) {
- this.rt = platform.runtime;
- this.resourcex = platform.resourcex;
- this._registerPrototype = platform.registerPrototype;
-
- // Ensure world roots exist (idempotent — reuse if already created by another process)
- const roots = this.rt.roots();
- this.society = roots.find((r) => r.name === "society") ?? this.rt.create(null, C.society);
-
- const societyState = this.rt.project(this.society);
- const existingPast = societyState.children?.find((c) => c.name === "past");
- this.past = existingPast ?? this.rt.create(this.society, C.past);
-
- // Shared resolver — all namespaces use this to look up nodes by id
- const resolve: Resolve = (id: string) => {
- const node = this.find(id);
- if (!node) throw new Error(`"${id}" not found.`);
- return node;
- };
-
- // Namespaces
- this.individual = new IndividualNamespace(this.rt, this.society, this.past, resolve);
- this.role = new RoleNamespace(this.rt, resolve, platform.prototype, platform.resourcex);
- this.org = new OrgNamespace(this.rt, this.society, this.past, resolve);
- this.resource = platform.resourcex;
- }
-
- /** Register a ResourceX source as a prototype. Ingests to extract id, stores id → source mapping. */
- async prototype(source: string): Promise {
- if (!this.resourcex) throw new Error("ResourceX is not available.");
- if (!this._registerPrototype)
- throw new Error("Platform does not support prototype registration.");
- const state = await this.resourcex.ingest(source);
- if (!state.id) throw new Error("Prototype resource must have an id.");
- this._registerPrototype(state.id, source);
- return { state, process: "prototype" };
- }
-
- /** Find a node by id or alias across the entire society tree. */
- find(id: string): Structure | null {
- const target = id.toLowerCase();
- const state = this.rt.project(this.society);
- return findInState(state, target);
- }
-}
-
-// ================================================================
-// Individual — lifecycle + external injection
-// ================================================================
-
-class IndividualNamespace {
- constructor(
- private rt: Runtime,
- private society: Structure,
- private past: Structure,
- private resolve: Resolve
- ) {}
-
- /** Born an individual into society. */
- born(individual?: string, id?: string, alias?: readonly string[]): RolexResult {
- validateGherkin(individual);
- const node = this.rt.create(this.society, C.individual, individual, id, alias);
- return ok(this.rt, node, "born");
- }
-
- /** Retire an individual (can rehire later). */
- retire(individual: string): RolexResult {
- return archive(this.rt, this.past, this.resolve(individual), "retire");
- }
-
- /** An individual dies (permanent). */
- die(individual: string): RolexResult {
- return archive(this.rt, this.past, this.resolve(individual), "die");
- }
-
- /** Rehire a retired individual from past. */
- rehire(pastNode: string): RolexResult {
- const past = this.resolve(pastNode);
- const individual = this.rt.create(this.society, C.individual, past.information);
- this.rt.remove(past);
- return ok(this.rt, individual, "rehire");
- }
-
- // ---- External injection ----
-
- /** Teach: directly inject a principle into an individual — no experience consumed. Upserts by id. */
- teach(individual: string, principle: string, id?: string): RolexResult {
- validateGherkin(principle);
- const parent = this.resolve(individual);
- if (id) this.removeExisting(parent, id);
- const prin = this.rt.create(parent, C.principle, principle, id);
- return ok(this.rt, prin, "teach");
- }
-
- /** Train: directly inject a procedure (skill) into an individual — no experience consumed. Upserts by id. */
- train(individual: string, procedure: string, id?: string): RolexResult {
- validateGherkin(procedure);
- const parent = this.resolve(individual);
- if (id) this.removeExisting(parent, id);
- const proc = this.rt.create(parent, C.procedure, procedure, id);
- return ok(this.rt, proc, "train");
- }
-
- /** Remove existing child node with matching id (for upsert). */
- private removeExisting(parent: Structure, id: string): void {
- const state = this.rt.project(parent);
- const existing = findInState(state, id);
- if (existing) this.rt.remove(existing);
- }
-}
-
-// ================================================================
-// Role — execution + cognition
-// ================================================================
-
-class RoleNamespace {
- constructor(
- private rt: Runtime,
- private resolve: Resolve,
- private prototype?: Prototype,
- private resourcex?: ResourceX
- ) {}
-
- // ---- Activation ----
-
- /** Activate: merge prototype (if any) with instance state. */
- async activate(individual: string): Promise {
- const node = this.resolve(individual);
- const instanceState = this.rt.project(node);
- const protoState = instanceState.id
- ? await this.prototype?.resolve(instanceState.id)
- : undefined;
- const state = protoState ? mergeState(protoState, instanceState) : instanceState;
- return { state, process: "activate" };
- }
-
- /** Focus: project a goal's state (view / switch context). */
- focus(goal: string): RolexResult {
- return ok(this.rt, this.resolve(goal), "focus");
- }
-
- // ---- Execution ----
-
- /** Declare a goal under an individual. */
- want(individual: string, goal?: string, id?: string, alias?: readonly string[]): RolexResult {
- validateGherkin(goal);
- const node = this.rt.create(this.resolve(individual), C.goal, goal, id, alias);
- return ok(this.rt, node, "want");
- }
-
- /** Create a plan for a goal. */
- plan(goal: string, plan?: string, id?: string): RolexResult {
- validateGherkin(plan);
- const node = this.rt.create(this.resolve(goal), C.plan, plan, id);
- return ok(this.rt, node, "plan");
- }
-
- /** Add a task to a plan. */
- todo(plan: string, task?: string, id?: string, alias?: readonly string[]): RolexResult {
- validateGherkin(task);
- const node = this.rt.create(this.resolve(plan), C.task, task, id, alias);
- return ok(this.rt, node, "todo");
- }
-
- /** Finish a task: consume task, create encounter under individual. */
- finish(task: string, individual: string, encounter?: string): RolexResult {
- validateGherkin(encounter);
- const taskNode = this.resolve(task);
- const encId = taskNode.id ? `${taskNode.id}-finished` : undefined;
- const enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId);
- this.rt.remove(taskNode);
- return ok(this.rt, enc, "finish");
- }
-
- /** Complete a plan: consume plan, create encounter under individual. */
- complete(plan: string, individual: string, encounter?: string): RolexResult {
- validateGherkin(encounter);
- const planNode = this.resolve(plan);
- const encId = planNode.id ? `${planNode.id}-completed` : undefined;
- const enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId);
- this.rt.remove(planNode);
- return ok(this.rt, enc, "complete");
- }
-
- /** Abandon a plan: consume plan, create encounter under individual. */
- abandon(plan: string, individual: string, encounter?: string): RolexResult {
- validateGherkin(encounter);
- const planNode = this.resolve(plan);
- const encId = planNode.id ? `${planNode.id}-abandoned` : undefined;
- const enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId);
- this.rt.remove(planNode);
- return ok(this.rt, enc, "abandon");
- }
-
- // ---- Cognition ----
-
- /** Reflect: consume encounter, create experience under individual. */
- reflect(encounter: string, individual: string, experience?: string, id?: string): RolexResult {
- validateGherkin(experience);
- const encNode = this.resolve(encounter);
- const exp = this.rt.create(
- this.resolve(individual),
- C.experience,
- experience || encNode.information,
- id
- );
- this.rt.remove(encNode);
- return ok(this.rt, exp, "reflect");
- }
-
- /** Realize: consume experience, create principle under individual. */
- realize(experience: string, individual: string, principle?: string, id?: string): RolexResult {
- validateGherkin(principle);
- const expNode = this.resolve(experience);
- const prin = this.rt.create(
- this.resolve(individual),
- C.principle,
- principle || expNode.information,
- id
- );
- this.rt.remove(expNode);
- return ok(this.rt, prin, "realize");
+ private repo: RoleXRepository;
+ private readonly initializer?: Initializer;
+
+ private readonly bootstrap: readonly string[];
+ private society!: Structure;
+ private past!: Structure;
+
+ private constructor(platform: Platform) {
+ this.repo = platform.repository;
+ this.rt = this.repo.runtime;
+ this.initializer = platform.initializer;
+ this.bootstrap = platform.bootstrap ?? [];
+
+ // Create ResourceX from injected provider
+ if (platform.resourcexProvider) {
+ setProvider(platform.resourcexProvider);
+ this.resourcex = createResourceX();
+ }
}
- /** Master: consume experience, create procedure under individual. */
- master(experience: string, individual: string, procedure?: string, id?: string): RolexResult {
- validateGherkin(procedure);
- const expNode = this.resolve(experience);
- const proc = this.rt.create(
- this.resolve(individual),
- C.procedure,
- procedure || expNode.information,
- id
- );
- this.rt.remove(expNode);
- return ok(this.rt, proc, "master");
+ /** Create a Rolex instance from a Platform (async due to Runtime initialization). */
+ static async create(platform: Platform): Promise {
+ const rolex = new Rolex(platform);
+ await rolex.init();
+ return rolex;
}
- // ---- Knowledge management ----
+ /** Async initialization — called by Rolex.create(). */
+ private async init(): Promise {
+ // Ensure world roots exist
+ const roots = await this.rt.roots();
+ this.society =
+ roots.find((r) => r.name === "society") ?? (await this.rt.create(null, C.society));
- /** Forget: remove any node under an individual by id. Prototype nodes are read-only. */
- async forget(nodeId: string, individual: string): Promise {
- try {
- const node = this.resolve(nodeId);
- this.rt.remove(node);
- return { state: { ...node, children: [] }, process: "forget" };
- } catch {
- // Not in runtime graph — check if it's a prototype node
- if (this.prototype) {
- // Resolve individual to get its actual stored id (case-sensitive match for prototype)
- const indNode = this.resolve(individual);
- const instanceState = this.rt.project(indNode);
- const protoState = instanceState.id
- ? await this.prototype.resolve(instanceState.id)
- : undefined;
- if (protoState && findInState(protoState, nodeId.toLowerCase())) {
- throw new Error(`"${nodeId}" is a prototype node (read-only) and cannot be forgotten.`);
- }
- }
- throw new Error(`"${nodeId}" not found.`);
+ const societyState = await this.rt.project(this.society);
+ const existingPast = societyState.children?.find((c) => c.name === "past");
+ this.past = existingPast ?? (await this.rt.create(this.society, C.past));
+
+ // Create ops from prototype — all operation implementations
+ this.ops = createOps({
+ rt: this.rt,
+ society: this.society,
+ past: this.past,
+ resolve: async (id: string) => {
+ const node = await this.find(id);
+ if (!node) throw new Error(`"${id}" not found.`);
+ return node;
+ },
+ find: (id: string) => this.find(id),
+ resourcex: this.resourcex,
+ prototype: this.repo.prototype,
+ direct: (locator: string, args?: Record) => this.direct(locator, args),
+ });
+ }
+
+ /** Genesis — create the world on first run. Settles built-in prototypes. */
+ async genesis(): Promise {
+ await this.initializer?.bootstrap();
+ // Settle bootstrap prototypes
+ for (const source of this.bootstrap) {
+ await this.direct("!prototype.settle", { source });
}
}
- // ---- Resource interaction ----
-
- /** Skill: load full skill content by locator — for context injection (progressive disclosure layer 2). */
- async skill(locator: string): Promise {
- if (!this.resourcex) throw new Error("ResourceX is not available.");
- const content = await this.resourcex.ingest(locator);
- const text = typeof content === "string" ? content : JSON.stringify(content, null, 2);
-
- // Try to render RXM context alongside content
- try {
- const rxm = await this.resourcex.info(locator);
- return `${formatRXM(rxm)}\n\n${text}`;
- } catch {
- // Path-based locator or info unavailable — return content only
- return text;
+ /**
+ * Activate a role — returns a stateful Role handle.
+ *
+ * If the individual does not exist in runtime but a prototype is registered,
+ * auto-born the individual first.
+ */
+ async activate(individual: string): Promise {
+ let node = await this.find(individual);
+ if (!node) {
+ const hasProto = Object.hasOwn(this.repo.prototype.list(), individual);
+ if (hasProto) {
+ await this.ops["individual.born"](undefined, individual);
+ node = (await this.find(individual))!;
+ } else {
+ throw new Error(`"${individual}" not found.`);
+ }
}
- }
-
- /** Use a resource — role's entry point for interacting with external resources. */
- use(locator: string): Promise {
- if (!this.resourcex) throw new Error("ResourceX is not available.");
- return this.resourcex.ingest(locator);
- }
-}
-
-// ================================================================
-// Org — organization management
-// ================================================================
-
-class OrgNamespace {
- constructor(
- private rt: Runtime,
- private society: Structure,
- private past: Structure,
- private resolve: Resolve
- ) {}
-
- // ---- Structure ----
-
- /** Found an organization. */
- found(organization?: string, id?: string, alias?: readonly string[]): RolexResult {
- validateGherkin(organization);
- const org = this.rt.create(this.society, C.organization, organization, id, alias);
- return ok(this.rt, org, "found");
- }
-
- /** Establish a position within an organization. */
- establish(org: string, position?: string, id?: string, alias?: readonly string[]): RolexResult {
- validateGherkin(position);
- const pos = this.rt.create(this.resolve(org), C.position, position, id, alias);
- return ok(this.rt, pos, "establish");
- }
-
- /** Define the charter for an organization. */
- charter(org: string, charter: string): RolexResult {
- validateGherkin(charter);
- const node = this.rt.create(this.resolve(org), C.charter, charter);
- return ok(this.rt, node, "charter");
- }
-
- /** Add a duty to a position. */
- charge(position: string, duty: string, id?: string): RolexResult {
- validateGherkin(duty);
- const node = this.rt.create(this.resolve(position), C.duty, duty, id);
- return ok(this.rt, node, "charge");
- }
-
- // ---- Archival ----
-
- /** Dissolve an organization. */
- dissolve(org: string): RolexResult {
- return archive(this.rt, this.past, this.resolve(org), "dissolve");
- }
-
- /** Abolish a position. */
- abolish(position: string): RolexResult {
- return archive(this.rt, this.past, this.resolve(position), "abolish");
- }
-
- // ---- Membership & Appointment ----
-
- /** Hire: link individual to organization via membership. */
- hire(org: string, individual: string): RolexResult {
- const orgNode = this.resolve(org);
- this.rt.link(orgNode, this.resolve(individual), "membership", "belong");
- return ok(this.rt, orgNode, "hire");
- }
-
- /** Fire: remove membership link. */
- fire(org: string, individual: string): RolexResult {
- const orgNode = this.resolve(org);
- this.rt.unlink(orgNode, this.resolve(individual), "membership", "belong");
- return ok(this.rt, orgNode, "fire");
- }
-
- /** Appoint: link individual to position via appointment. */
- appoint(position: string, individual: string): RolexResult {
- const posNode = this.resolve(position);
- this.rt.link(posNode, this.resolve(individual), "appointment", "serve");
- return ok(this.rt, posNode, "appoint");
- }
-
- /** Dismiss: remove appointment link. */
- dismiss(position: string, individual: string): RolexResult {
- const posNode = this.resolve(position);
- this.rt.unlink(posNode, this.resolve(individual), "appointment", "serve");
- return ok(this.rt, posNode, "dismiss");
- }
-}
-
-// ================================================================
-// Shared helpers
-// ================================================================
-
-function validateGherkin(source?: string): void {
- if (!source) return;
- try {
- parse(source);
- } catch (e: any) {
- throw new Error(`Invalid Gherkin: ${e.message}`);
- }
-}
-
-function findInState(state: State, target: string): Structure | null {
- if (state.id && state.id.toLowerCase() === target) return state;
- if (state.alias) {
- for (const a of state.alias) {
- if (a.toLowerCase() === target) return state;
+ const state = await this.rt.project(node);
+ const ctx = new RoleContext(individual);
+ ctx.rehydrate(state);
+
+ // Restore persisted focus (only override rehydrate default when persisted value is valid)
+ const persisted = await this.repo.loadContext(individual);
+ if (persisted) {
+ if (persisted.focusedGoalId && (await this.find(persisted.focusedGoalId))) {
+ ctx.focusedGoalId = persisted.focusedGoalId;
+ }
+ if (persisted.focusedPlanId && (await this.find(persisted.focusedPlanId))) {
+ ctx.focusedPlanId = persisted.focusedPlanId;
+ }
}
- }
- for (const child of state.children ?? []) {
- const found = findInState(child, target);
- if (found) return found;
- }
- return null;
-}
-function archive(rt: Runtime, past: Structure, node: Structure, process: string): RolexResult {
- const archived = rt.create(past, C.past, node.information);
- rt.remove(node);
- return ok(rt, archived, process);
-}
+ // Build internal API for Role — ops + ctx persistence
+ const ops = this.ops;
+ const repo = this.repo;
+ const saveCtx = async (c: RoleContext) => {
+ await repo.saveContext(c.roleId, {
+ focusedGoalId: c.focusedGoalId,
+ focusedPlanId: c.focusedPlanId,
+ });
+ };
-function ok(rt: Runtime, node: Structure, process: string): RolexResult {
- return {
- state: rt.project(node),
- process,
- };
-}
+ const api: RolexInternal = {
+ ops,
+ saveCtx,
+ direct: this.direct.bind(this),
+ };
-/** Render file tree from RXM source.files */
-function renderFileTree(files: Record, indent = ""): string {
- const lines: string[] = [];
- for (const [name, value] of Object.entries(files)) {
- if (value && typeof value === "object" && !("size" in value)) {
- // Directory
- lines.push(`${indent}${name}`);
- lines.push(renderFileTree(value, `${indent} `));
- } else {
- const size = value?.size ? ` (${value.size} bytes)` : "";
- lines.push(`${indent}${name}${size}`);
+ return new Role(individual, ctx, api);
+ }
+
+ /** Find a node by id or alias across the entire society tree. Internal use only. */
+ private async find(id: string): Promise {
+ const state = await this.rt.project(this.society);
+ return findInState(state, id);
+ }
+
+ /**
+ * Direct the world to execute an instruction.
+ *
+ * - `!namespace.method` — dispatch to ops
+ * - anything else — delegate to ResourceX `ingest`
+ */
+ async direct(locator: string, args?: Record): Promise {
+ if (locator.startsWith("!")) {
+ const command = locator.slice(1);
+ const fn = this.ops[command];
+ if (!fn) {
+ const hint = directives["identity-ethics"]?.["on-unknown-command"] ?? "";
+ throw new Error(
+ `Unknown command "${locator}".\n\n` +
+ "You may be guessing the command name. " +
+ "Load the relevant skill first with skill(locator) to learn the correct syntax.\n\n" +
+ hint
+ );
+ }
+ return (await fn(...toArgs(command, args ?? {}))) as T;
}
+ if (!this.resourcex) throw new Error("ResourceX is not available.");
+ return this.resourcex.ingest(locator, args);
}
- return lines.filter(Boolean).join("\n");
-}
-
-/** Format RXM info as context header for skill injection. */
-function formatRXM(rxm: any): string {
- const lines: string[] = [`--- RXM: ${rxm.locator} ---`];
- const def = rxm.definition;
- if (def) {
- if (def.author) lines.push(`Author: ${def.author}`);
- if (def.description) lines.push(`Description: ${def.description}`);
- }
- const source = rxm.source;
- if (source?.files) {
- lines.push(`Files:`);
- lines.push(renderFileTree(source.files, " "));
- }
- lines.push("---");
- return lines.join("\n");
}
/** Create a Rolex instance from a Platform. */
-export function createRoleX(platform: Platform): Rolex {
- return new Rolex(platform);
+export async function createRoleX(platform: Platform): Promise {
+ return Rolex.create(platform);
}
diff --git a/packages/rolexjs/tests/context.test.ts b/packages/rolexjs/tests/context.test.ts
new file mode 100644
index 0000000..b1f48b1
--- /dev/null
+++ b/packages/rolexjs/tests/context.test.ts
@@ -0,0 +1,281 @@
+import { afterEach, describe, expect, test } from "bun:test";
+import { existsSync, mkdirSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { localPlatform } from "@rolexjs/local-platform";
+import { createRoleX, RoleContext } from "../src/index.js";
+
+async function setup() {
+ return await createRoleX(localPlatform({ dataDir: null }));
+}
+
+async function setupWithDir() {
+ const dataDir = join(tmpdir(), `rolex-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
+ mkdirSync(dataDir, { recursive: true });
+ const rolex = await createRoleX(localPlatform({ dataDir, resourceDir: null }));
+ return { rolex, dataDir };
+}
+
+describe("Role (ctx management)", () => {
+ test("activate returns Role with ctx", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+ expect(role.ctx).toBeInstanceOf(RoleContext);
+ expect(role.ctx.roleId).toBe("sean");
+ });
+
+ test("want updates ctx.focusedGoalId", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+
+ const result = await role.want("Feature: Build auth", "build-auth");
+ expect(role.ctx.focusedGoalId).toBe("build-auth");
+ expect(role.ctx.focusedPlanId).toBeNull();
+ expect(result).toContain("I →");
+ });
+
+ test("plan updates ctx.focusedPlanId", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+
+ await role.want("Feature: Auth", "auth-goal");
+ const result = await role.plan("Feature: JWT strategy", "jwt-plan");
+ expect(role.ctx.focusedPlanId).toBe("jwt-plan");
+ expect(result).toContain("I →");
+ });
+
+ test("finish with encounter registers in ctx", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+
+ await role.want("Feature: Auth", "auth");
+ await role.plan("Feature: JWT", "jwt");
+ await role.todo("Feature: Login", "login");
+
+ const result = await role.finish(
+ "login",
+ "Feature: Login done\n Scenario: OK\n Given login\n Then success"
+ );
+ expect(role.ctx.encounterIds.has("login-finished")).toBe(true);
+ expect(result).toContain("I →");
+ });
+
+ test("finish without encounter does not register in ctx", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+
+ await role.want("Feature: Auth", "auth");
+ await role.plan("Feature: JWT", "jwt");
+ await role.todo("Feature: Login", "login");
+
+ await role.finish("login");
+ expect(role.ctx.encounterIds.size).toBe(0);
+ });
+
+ test("complete registers encounter and clears focusedPlanId", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+
+ await role.want("Feature: Auth", "auth");
+ await role.plan("Feature: JWT", "jwt");
+
+ const result = await role.complete(
+ "jwt",
+ "Feature: JWT done\n Scenario: OK\n Given jwt\n Then done"
+ );
+ expect(role.ctx.focusedPlanId).toBeNull();
+ expect(role.ctx.encounterIds.has("jwt-completed")).toBe(true);
+ expect(result).toContain("auth");
+ });
+
+ test("reflect consumes encounter and adds experience in ctx", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+
+ await role.want("Feature: Auth", "auth");
+ await role.plan("Feature: JWT", "jwt");
+ await role.todo("Feature: Login", "login");
+ await role.finish("login", "Feature: Login done\n Scenario: OK\n Given x\n Then y");
+
+ expect(role.ctx.encounterIds.has("login-finished")).toBe(true);
+
+ await role.reflect(
+ ["login-finished"],
+ "Feature: Token insight\n Scenario: OK\n Given x\n Then y",
+ "token-insight"
+ );
+
+ expect(role.ctx.encounterIds.has("login-finished")).toBe(false);
+ expect(role.ctx.experienceIds.has("token-insight")).toBe(true);
+ });
+
+ test("reflect without encounter creates experience directly", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+
+ const result = await role.reflect(
+ [],
+ "Feature: Direct insight\n Scenario: OK\n Given learned from conversation",
+ "conv-insight"
+ );
+
+ expect(result).toContain("[experience]");
+ expect(role.ctx.experienceIds.has("conv-insight")).toBe(true);
+ expect(role.ctx.encounterIds.size).toBe(0);
+ });
+
+ test("realize without experience creates principle directly", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+
+ const result = await role.realize(
+ [],
+ "Feature: Direct principle\n Scenario: OK\n Given always blame the product",
+ "product-first"
+ );
+
+ expect(result).toContain("[principle]");
+ expect(role.ctx.experienceIds.size).toBe(0);
+ });
+
+ test("realize still consumes experience when provided", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+
+ // Create experience directly
+ await role.reflect(
+ [],
+ "Feature: Insight\n Scenario: OK\n Given something learned",
+ "my-insight"
+ );
+ expect(role.ctx.experienceIds.has("my-insight")).toBe(true);
+
+ // Realize from that experience
+ await role.realize(
+ ["my-insight"],
+ "Feature: Principle\n Scenario: OK\n Given a general truth",
+ "my-principle"
+ );
+ expect(role.ctx.experienceIds.has("my-insight")).toBe(false);
+ });
+
+ test("cognitiveHint varies by state", () => {
+ const ctx = new RoleContext("sean");
+ expect(ctx.cognitiveHint("activate")).toContain("no goal");
+
+ ctx.focusedGoalId = "auth";
+ expect(ctx.cognitiveHint("activate")).toContain("active goal");
+
+ expect(ctx.cognitiveHint("want")).toContain("plan");
+ expect(ctx.cognitiveHint("plan")).toContain("todo");
+ expect(ctx.cognitiveHint("todo")).toContain("finish");
+ });
+});
+
+describe("Role context persistence", () => {
+ const dirs: string[] = [];
+ afterEach(() => {
+ for (const d of dirs) {
+ if (existsSync(d)) rmSync(d, { recursive: true });
+ }
+ dirs.length = 0;
+ });
+
+ async function persistent() {
+ const { rolex, dataDir } = await setupWithDir();
+ dirs.push(dataDir);
+ return { rolex, dataDir };
+ }
+
+ test("activate restores persisted focusedGoalId and focusedPlanId", async () => {
+ const { rolex } = await persistent();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+
+ // Session 1: set focus
+ const role1 = await rolex.activate("sean");
+ await role1.want("Feature: Auth", "auth");
+ await role1.plan("Feature: JWT", "jwt");
+ expect(role1.ctx.focusedGoalId).toBe("auth");
+ expect(role1.ctx.focusedPlanId).toBe("jwt");
+
+ // Session 2: re-activate restores from SQLite
+ const role2 = await rolex.activate("sean");
+ expect(role2.ctx.focusedGoalId).toBe("auth");
+ expect(role2.ctx.focusedPlanId).toBe("jwt");
+ });
+
+ test("activate without persisted context uses rehydrate default", async () => {
+ const { rolex } = await persistent();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ await rolex.direct("!role.want", { individual: "sean", goal: "Feature: Auth", id: "auth" });
+
+ const role = await rolex.activate("sean");
+ expect(role.ctx.focusedGoalId).toBe("auth");
+ expect(role.ctx.focusedPlanId).toBeNull();
+ });
+
+ test("focus saves updated context", async () => {
+ const { rolex } = await persistent();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+
+ const role = await rolex.activate("sean");
+ await role.want("Feature: Goal A", "goal-a");
+ await role.want("Feature: Goal B", "goal-b");
+
+ await role.focus("goal-a");
+
+ // Re-activate to verify persistence
+ const role2 = await rolex.activate("sean");
+ expect(role2.ctx.focusedGoalId).toBe("goal-a");
+ expect(role2.ctx.focusedPlanId).toBeNull();
+ });
+
+ test("complete clears focusedPlanId and saves", async () => {
+ const { rolex } = await persistent();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+
+ const role = await rolex.activate("sean");
+ await role.want("Feature: Auth", "auth");
+ await role.plan("Feature: JWT", "jwt");
+ await role.complete("jwt", "Feature: Done\n Scenario: OK\n Given done\n Then ok");
+
+ // Re-activate to verify persistence
+ const role2 = await rolex.activate("sean");
+ expect(role2.ctx.focusedGoalId).toBe("auth");
+ expect(role2.ctx.focusedPlanId).toBeNull();
+ });
+
+ test("different roles have independent contexts", async () => {
+ const { rolex } = await persistent();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ await rolex.direct("!individual.born", { content: "Feature: Nuwa", id: "nuwa" });
+
+ const seanRole = await rolex.activate("sean");
+ await seanRole.want("Feature: Sean Goal", "sean-goal");
+
+ const nuwaRole = await rolex.activate("nuwa");
+ await nuwaRole.want("Feature: Nuwa Goal", "nuwa-goal");
+
+ // Re-activate sean — should get sean's context, not nuwa's
+ const seanRole2 = await rolex.activate("sean");
+ expect(seanRole2.ctx.focusedGoalId).toBe("sean-goal");
+ });
+
+ test("in-memory mode (dataDir: null) works without persistence", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+ await role.want("Feature: Auth", "auth");
+ expect(role.ctx.focusedGoalId).toBe("auth");
+ });
+});
diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts
index 42391fc..02137f5 100644
--- a/packages/rolexjs/tests/rolex.test.ts
+++ b/packages/rolexjs/tests/rolex.test.ts
@@ -1,666 +1,233 @@
-import { describe, expect, test } from "bun:test";
+import { afterEach, describe, expect, test } from "bun:test";
+import { existsSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
import { localPlatform } from "@rolexjs/local-platform";
+import type { OpResult } from "@rolexjs/prototype";
+import { createRoleX } from "../src/index.js";
import { describe as renderDescribe, hint as renderHint, renderState } from "../src/render.js";
-import { createRoleX } from "../src/rolex.js";
-function setup() {
- return createRoleX(localPlatform({ dataDir: null }));
+async function setup() {
+ return await createRoleX(localPlatform({ dataDir: null }));
}
-describe("Rolex API (stateless)", () => {
- // ============================================================
- // Lifecycle — creation
- // ============================================================
-
- describe("lifecycle: creation", () => {
- test("born creates an individual with scaffolding", () => {
- const rolex = setup();
- const r = rolex.born("Feature: I am Sean");
- expect(r.state.name).toBe("individual");
- expect(r.state.information).toBe("Feature: I am Sean");
- expect(r.process).toBe("born");
- // Scaffolding: identity + knowledge
- const names = r.state.children!.map((c) => c.name);
- expect(names).toContain("identity");
- expect(names).toContain("knowledge");
- });
-
- test("found creates an organization", () => {
- const rolex = setup();
- const r = rolex.found("Feature: AI company");
- expect(r.state.name).toBe("organization");
- expect(r.process).toBe("found");
- });
-
- test("establish creates a position under org", () => {
- const rolex = setup();
- const org = rolex.found().state;
- const r = rolex.establish(org, "Feature: Backend architect");
- expect(r.state.name).toBe("position");
- });
-
- test("charter defines org mission", () => {
- const rolex = setup();
- const org = rolex.found().state;
- const r = rolex.charter(org, "Feature: Build great AI");
- expect(r.state.name).toBe("charter");
- expect(r.state.information).toBe("Feature: Build great AI");
- });
-
- test("charge adds duty to position", () => {
- const rolex = setup();
- const org = rolex.found().state;
- const pos = rolex.establish(org).state;
- const r = rolex.charge(pos, "Feature: Design systems");
- expect(r.state.name).toBe("duty");
- });
+// ================================================================
+// use() dispatch
+// ================================================================
+
+describe("use dispatch", () => {
+ test("!individual.born creates individual", async () => {
+ const rolex = await setup();
+ const r = await rolex.direct("!individual.born", {
+ content: "Feature: Sean",
+ id: "sean",
+ });
+ expect(r.state.name).toBe("individual");
+ expect(r.state.id).toBe("sean");
+ expect(r.process).toBe("born");
+ const names = r.state.children!.map((c) => c.name);
+ expect(names).toContain("identity");
});
- // ============================================================
- // Lifecycle — archival
- // ============================================================
-
- describe("lifecycle: archival", () => {
- test("retire archives individual", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const r = rolex.retire(sean);
- expect(r.state.name).toBe("past");
- expect(r.process).toBe("retire");
- // Original is gone
- expect(() => rolex.project(sean)).toThrow();
- });
-
- test("die archives individual", () => {
- const rolex = setup();
- const alice = rolex.born().state;
- const r = rolex.die(alice);
- expect(r.state.name).toBe("past");
- expect(r.process).toBe("die");
- });
-
- test("dissolve archives organization", () => {
- const rolex = setup();
- const org = rolex.found().state;
- rolex.dissolve(org);
- expect(() => rolex.project(org)).toThrow();
- });
-
- test("abolish archives position", () => {
- const rolex = setup();
- const org = rolex.found().state;
- const pos = rolex.establish(org).state;
- rolex.abolish(pos);
- expect(() => rolex.project(pos)).toThrow();
- });
-
- test("rehire restores individual from past", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const archived = rolex.retire(sean).state;
- const r = rolex.rehire(archived);
- expect(r.state.name).toBe("individual");
- expect(r.state.information).toBe("Feature: Sean");
- // Scaffolding restored
- const names = r.state.children!.map((c) => c.name);
- expect(names).toContain("identity");
- expect(names).toContain("knowledge");
+ test("chained operations via use", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { id: "sean" });
+ await rolex.direct("!role.want", { individual: "sean", goal: "Feature: Auth", id: "g1" });
+ const r = await rolex.direct("!role.plan", {
+ goal: "g1",
+ plan: "Feature: JWT",
+ id: "p1",
});
+ expect(r.state.name).toBe("plan");
});
- // ============================================================
- // Organization
- // ============================================================
-
- describe("organization", () => {
- test("hire links individual to org", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const org = rolex.found().state;
- const r = rolex.hire(org, sean);
- expect(r.state.links).toHaveLength(1);
- expect(r.state.links![0].relation).toBe("membership");
- });
-
- test("fire removes membership", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const org = rolex.found().state;
- rolex.hire(org, sean);
- const r = rolex.fire(org, sean);
- expect(r.state.links).toBeUndefined();
- });
-
- test("appoint links individual to position", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const org = rolex.found().state;
- const pos = rolex.establish(org).state;
- const r = rolex.appoint(pos, sean);
- expect(r.state.links).toHaveLength(1);
- expect(r.state.links![0].relation).toBe("appointment");
- });
-
- test("dismiss removes appointment", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const org = rolex.found().state;
- const pos = rolex.establish(org).state;
- rolex.appoint(pos, sean);
- const r = rolex.dismiss(pos, sean);
- expect(r.state.links).toBeUndefined();
- });
+ test("!census.list returns text", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { id: "sean" });
+ await rolex.direct("!org.found", { id: "dp" });
+ const result = await rolex.direct("!census.list");
+ expect(result).toContain("sean");
+ expect(result).toContain("dp");
});
- // ============================================================
- // Role
- // ============================================================
-
- describe("role", () => {
- test("activate returns individual projection", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const r = rolex.activate(sean);
- expect(r.state.name).toBe("individual");
- expect(r.process).toBe("activate");
- });
+ test("throws on unknown command", async () => {
+ const rolex = await setup();
+ expect(rolex.direct("!foo.bar")).rejects.toThrow();
});
- // ============================================================
- // Execution
- // ============================================================
-
- describe("execution", () => {
- test("want creates a goal", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const r = rolex.want(sean, "Feature: Build auth system");
- expect(r.state.name).toBe("goal");
- expect(r.state.information).toBe("Feature: Build auth system");
- });
-
- test("plan creates a plan under goal", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const goal = rolex.want(sean, "Feature: Auth").state;
- const r = rolex.plan(goal, "Feature: JWT plan");
- expect(r.state.name).toBe("plan");
- });
-
- test("todo creates a task under plan", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const goal = rolex.want(sean).state;
- const plan = rolex.plan(goal).state;
- const r = rolex.todo(plan, "Feature: Implement JWT");
- expect(r.state.name).toBe("task");
- });
-
- test("finish consumes task, creates encounter", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const goal = rolex.want(sean).state;
- const plan = rolex.plan(goal).state;
- const task = rolex.todo(plan).state;
-
- const r = rolex.finish(task, sean, "Feature: JWT done");
- expect(r.state.name).toBe("encounter");
- expect(r.state.information).toBe("Feature: JWT done");
- // Task is gone
- expect(() => rolex.project(task)).toThrow();
- });
-
- test("complete consumes plan, creates encounter", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const goal = rolex.want(sean, "Feature: Auth").state;
- const plan = rolex.plan(goal, "Feature: Auth plan").state;
-
- const r = rolex.complete(plan, sean, "Feature: Auth plan done");
- expect(r.state.name).toBe("encounter");
- expect(() => rolex.project(plan)).toThrow();
- });
-
- test("abandon consumes plan, creates encounter", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const goal = rolex.want(sean, "Feature: Rust").state;
- const plan = rolex.plan(goal, "Feature: Rust plan").state;
-
- const r = rolex.abandon(plan, sean, "Feature: No time");
- expect(r.state.name).toBe("encounter");
- expect(() => rolex.project(plan)).toThrow();
- });
+ test("throws on unknown method", async () => {
+ const rolex = await setup();
+ expect(rolex.direct("!org.nope")).rejects.toThrow();
});
+});
- // ============================================================
- // Cognition
- // ============================================================
-
- describe("cognition", () => {
- test("reflect: encounter → experience", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const goal = rolex.want(sean).state;
- const plan = rolex.plan(goal).state;
- const task = rolex.todo(plan).state;
- const enc = rolex.finish(task, sean, "Feature: JWT quirks").state;
-
- const r = rolex.reflect(enc, sean, "Feature: Token refresh matters");
- expect(r.state.name).toBe("experience");
- expect(r.state.information).toBe("Feature: Token refresh matters");
- expect(() => rolex.project(enc)).toThrow();
- });
+// ================================================================
+// activate() + Role API
+// ================================================================
+
+describe("activate", () => {
+ test("returns Role with ctx", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" });
+ const role = await rolex.activate("sean");
+ expect(role.roleId).toBe("sean");
+ expect(role.ctx).toBeDefined();
+ });
- test("reflect inherits encounter info if no source given", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const goal = rolex.want(sean).state;
- const plan = rolex.plan(goal).state;
- const task = rolex.todo(plan).state;
- const enc = rolex.finish(task, sean, "Feature: JWT quirks").state;
+ test("throws on non-existent individual", async () => {
+ const rolex = await setup();
+ expect(rolex.activate("nobody")).rejects.toThrow('"nobody" not found');
+ });
- const r = rolex.reflect(enc, sean);
- expect(r.state.information).toBe("Feature: JWT quirks");
- });
+ test("Role.want/plan/todo/finish work through Role API", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { id: "sean" });
+ const role = await rolex.activate("sean");
- test("realize: experience → principle under knowledge", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const knowledge = sean.children!.find((c) => c.name === "knowledge")!;
-
- const goal = rolex.want(sean).state;
- const plan = rolex.plan(goal).state;
- const task = rolex.todo(plan).state;
- const enc = rolex.finish(task, sean, "Feature: Lessons").state;
- const exp = rolex.reflect(enc, sean).state;
-
- const r = rolex.realize(exp, knowledge, "Feature: Security first");
- expect(r.state.name).toBe("principle");
- expect(r.state.information).toBe("Feature: Security first");
- expect(() => rolex.project(exp)).toThrow();
- });
+ const wantR = await role.want("Feature: Auth", "auth");
+ expect(wantR).toContain('Goal "auth" declared.');
+ expect(wantR).toContain("[goal]");
- test("master: experience → skill under knowledge", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const knowledge = sean.children!.find((c) => c.name === "knowledge")!;
+ const planR = await role.plan("Feature: JWT", "jwt");
+ expect(planR).toContain("[plan]");
- const goal = rolex.want(sean).state;
- const plan = rolex.plan(goal).state;
- const task = rolex.todo(plan).state;
- const enc = rolex.finish(task, sean, "Feature: Practice").state;
- const exp = rolex.reflect(enc, sean).state;
+ const todoR = await role.todo("Feature: Login", "login");
+ expect(todoR).toContain("[task]");
- const r = rolex.master(exp, knowledge, "Feature: JWT mastery");
- expect(r.state.name).toBe("skill");
- });
+ const finishR = await role.finish(
+ "login",
+ "Feature: Done\n Scenario: OK\n Given done\n Then ok"
+ );
+ expect(finishR).toContain("[encounter]");
});
- // ============================================================
- // Full scenario
- // ============================================================
-
- describe("full scenario", () => {
- test("born → hire → appoint → want → plan → todo → finish → reflect → realize", () => {
- const rolex = setup();
-
- // Create world
- const sean = rolex.born("Feature: I am Sean").state;
- const org = rolex.found("Feature: Deepractice").state;
- const pos = rolex.establish(org, "Feature: Architect").state;
- rolex.charter(org, "Feature: Build great AI");
- rolex.charge(pos, "Feature: Design systems");
-
- // Organization
- rolex.hire(org, sean);
- rolex.appoint(pos, sean);
-
- // Verify links
- const orgState = rolex.project(org);
- expect(orgState.links).toHaveLength(1);
- const posState = orgState.children!.find((c) => c.name === "position")!;
- expect(posState.links).toHaveLength(1);
-
- // Execution cycle
- const goal = rolex.want(sean, "Feature: Build auth").state;
- const plan = rolex.plan(goal, "Feature: JWT auth plan").state;
- const t1 = rolex.todo(plan, "Feature: Login endpoint").state;
- const t2 = rolex.todo(plan, "Feature: Refresh endpoint").state;
-
- const enc1 = rolex.finish(t1, sean, "Feature: Login done").state;
- const _enc2 = rolex.finish(t2, sean, "Feature: Refresh done").state;
- rolex.complete(plan, sean, "Feature: Auth plan complete");
-
- // Cognition cycle
- const knowledge = sean.children!.find((c) => c.name === "knowledge")!;
- const exp = rolex.reflect(enc1, sean, "Feature: Token handling").state;
- rolex.realize(exp, knowledge, "Feature: Always validate expiry");
-
- // Verify knowledge
- const knowledgeState = rolex.project(knowledge);
- expect(knowledgeState.children).toHaveLength(1);
- expect(knowledgeState.children![0].name).toBe("principle");
- expect(knowledgeState.children![0].information).toBe("Feature: Always validate expiry");
- });
+ test("Role.use delegates to Rolex.use", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { id: "sean" });
+ const role = await rolex.activate("sean");
+ const r = await role.use("!org.found", { content: "Feature: DP", id: "dp" });
+ expect(r.state.name).toBe("organization");
});
+});
- // ============================================================
- // Render
- // ============================================================
-
- describe("render", () => {
- test("describe generates text with name", () => {
- const rolex = setup();
- const r = rolex.born();
- const text = renderDescribe("born", "sean", r.state);
- expect(text).toContain("sean");
- });
-
- test("hint generates next step", () => {
- const h = renderHint("born");
- expect(h).toStartWith("Next:");
- });
+// ================================================================
+// Render
+// ================================================================
- test("every process has a hint", () => {
- const processes = [
- "born",
- "found",
- "establish",
- "charter",
- "charge",
- "retire",
- "die",
- "dissolve",
- "abolish",
- "rehire",
- "hire",
- "fire",
- "appoint",
- "dismiss",
- "activate",
- "want",
- "plan",
- "todo",
- "finish",
- "complete",
- "abandon",
- "reflect",
- "realize",
- "master",
- ];
- for (const p of processes) {
- expect(renderHint(p)).toStartWith("Next:");
- }
- });
+describe("render", () => {
+ test("describe generates text with name", async () => {
+ const rolex = await setup();
+ const r = await rolex.direct("!individual.born", { id: "sean" });
+ const text = renderDescribe("born", "sean", r.state);
+ expect(text).toContain("sean");
});
- // ============================================================
- // renderState — generic markdown renderer
- // ============================================================
-
- describe("renderState", () => {
- test("renders individual with heading and information", () => {
- const rolex = setup();
- const r = rolex.born("Feature: I am Sean\n An AI role.");
- const md = renderState(r.state);
- expect(md).toContain("# [individual]");
- expect(md).toContain("Feature: I am Sean");
- expect(md).toContain("An AI role.");
- });
-
- test("renders children at deeper heading levels", () => {
- const rolex = setup();
- const r = rolex.born("Feature: Sean");
- const md = renderState(r.state);
- // identity and knowledge are children at depth 2
- expect(md).toContain("## [identity]");
- expect(md).toContain("## [knowledge]");
- });
-
- test("renders links generically", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const org = rolex.found("Feature: Deepractice").state;
- rolex.hire(org, sean);
- // Project org — should have membership link
- const orgState = rolex.project(org);
- const md = renderState(orgState);
- expect(md).toContain("membership");
- expect(md).toContain("[individual]");
- });
-
- test("renders bidirectional links", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const org = rolex.found("Feature: Deepractice").state;
- rolex.hire(org, sean);
- // Project individual — should have belong link
- const seanState = rolex.project(sean);
- const md = renderState(seanState);
- expect(md).toContain("belong");
- expect(md).toContain("[organization]");
- expect(md).toContain("Deepractice");
- });
-
- test("renders nested structure (goal → plan → task)", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- const goal = rolex.want(sean, "Feature: Build auth").state;
- const plan = rolex.plan(goal, "Feature: JWT plan").state;
- rolex.todo(plan, "Feature: Login endpoint");
- // Project goal to see full tree
- const goalState = rolex.project(goal);
- const md = renderState(goalState);
- expect(md).toContain("# [goal]");
- expect(md).toContain("## [plan]");
- expect(md).toContain("### [task]");
- expect(md).toContain("Feature: Build auth");
- expect(md).toContain("Feature: JWT plan");
- expect(md).toContain("Feature: Login endpoint");
- });
-
- test("caps heading depth at 6", () => {
- const rolex = setup();
- const sean = rolex.born().state;
- // individual(1) → identity(2) is the deepest built-in nesting
- // Manually test with depth parameter
- const md = renderState(sean, 7);
- // Should use ###### (6) not ####### (7)
- expect(md).toStartWith("###### [individual]");
- });
-
- test("renders without information gracefully", () => {
- const rolex = setup();
- const r = rolex.born();
- const identity = r.state.children!.find((c) => c.name === "identity")!;
- const md = renderState(identity);
- expect(md).toBe("# [identity]");
- });
+ test("hint generates next step", () => {
+ const h = renderHint("born");
+ expect(h).toStartWith("Next:");
});
- // ============================================================
- // Gherkin validation
- // ============================================================
-
- describe("gherkin validation", () => {
- test("born rejects non-Gherkin input", () => {
- const rolex = setup();
- expect(() => rolex.born("not gherkin")).toThrow("Invalid Gherkin");
- });
-
- test("born accepts valid Gherkin", () => {
- const rolex = setup();
- expect(() => rolex.born("Feature: Sean")).not.toThrow();
- });
-
- test("born accepts undefined (no source)", () => {
- const rolex = setup();
- expect(() => rolex.born()).not.toThrow();
- });
-
- test("want rejects non-Gherkin goal", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- expect(() => rolex.want(sean, "plain text goal")).toThrow("Invalid Gherkin");
- });
-
- test("finish rejects non-Gherkin encounter", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const goal = rolex.want(sean, "Feature: Auth").state;
- const plan = rolex.plan(goal).state;
- const task = rolex.todo(plan, "Feature: Login").state;
- expect(() => rolex.finish(task, sean, "just text")).toThrow("Invalid Gherkin");
- });
-
- test("reflect rejects non-Gherkin experience", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const goal = rolex.want(sean, "Feature: Auth").state;
- const plan = rolex.plan(goal).state;
- const task = rolex.todo(plan, "Feature: Login").state;
- const enc = rolex.finish(
- task,
- sean,
- "Feature: Done\n Scenario: It worked\n Given login\n Then success"
- ).state;
- expect(() => rolex.reflect(enc, sean, "not gherkin")).toThrow("Invalid Gherkin");
- });
-
- test("realize rejects non-Gherkin principle", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const knowledge = sean.children!.find((c) => c.name === "knowledge")!;
- const goal = rolex.want(sean, "Feature: Auth").state;
- const plan = rolex.plan(goal).state;
- const task = rolex.todo(plan, "Feature: Login").state;
- const enc = rolex.finish(
- task,
- sean,
- "Feature: Done\n Scenario: OK\n Given x\n Then y"
- ).state;
- const exp = rolex.reflect(
- enc,
- sean,
- "Feature: Insight\n Scenario: Learned\n Given practice\n Then understanding"
- ).state;
- expect(() => rolex.realize(exp, knowledge, "not gherkin")).toThrow("Invalid Gherkin");
- });
-
- test("master rejects non-Gherkin skill", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const knowledge = sean.children!.find((c) => c.name === "knowledge")!;
- const goal = rolex.want(sean, "Feature: Auth").state;
- const plan = rolex.plan(goal).state;
- const task = rolex.todo(plan, "Feature: Login").state;
- const enc = rolex.finish(
- task,
- sean,
- "Feature: Done\n Scenario: OK\n Given x\n Then y"
- ).state;
- const exp = rolex.reflect(
- enc,
- sean,
- "Feature: Insight\n Scenario: Learned\n Given practice\n Then understanding"
- ).state;
- expect(() => rolex.master(exp, knowledge, "not gherkin")).toThrow("Invalid Gherkin");
+ test("renderState renders individual with heading", async () => {
+ const rolex = await setup();
+ const r = await rolex.direct("!individual.born", {
+ content: "Feature: I am Sean\n An AI role.",
+ id: "sean",
});
+ const md = renderState(r.state);
+ expect(md).toContain("# [individual]");
+ expect(md).toContain("Feature: I am Sean");
});
- // ============================================================
- // id & alias
- // ============================================================
-
- describe("id & alias", () => {
- test("born with id stores it on the node", () => {
- const rolex = setup();
- const r = rolex.born("Feature: I am Sean", "sean");
- expect(r.state.id).toBe("sean");
- expect(r.state.ref).toBeDefined();
- });
+ test("renderState renders nested structure", async () => {
+ const rolex = await setup();
+ await rolex.direct("!individual.born", { id: "sean" });
+ await rolex.direct("!role.want", { individual: "sean", goal: "Feature: Build auth", id: "g1" });
+ await rolex.direct("!role.plan", { goal: "g1", plan: "Feature: JWT plan", id: "p1" });
+ await rolex.direct("!role.todo", { plan: "p1", task: "Feature: Login endpoint", id: "t1" });
+ // Get goal state via focus (returns projected state)
+ const r = await rolex.direct("!role.focus", { goal: "g1" });
+ const md = renderState(r.state);
+ expect(md).toContain("# [goal]");
+ expect(md).toContain("## [plan]");
+ expect(md).toContain("### [task]");
+ });
- test("born with id and alias stores both", () => {
- const rolex = setup();
- const r = rolex.born("Feature: I am Sean", "sean", ["Sean", "姜山"]);
- expect(r.state.id).toBe("sean");
- expect(r.state.alias).toEqual(["Sean", "姜山"]);
- });
+ test("renderState filters empty child nodes", () => {
+ const state = {
+ name: "organization",
+ id: "acme",
+ description: "An org",
+ information: "Feature: ACME\n A company.",
+ children: [
+ { name: "charter", description: "The rules and mission", children: [] },
+ { name: "charter", description: "The rules and mission", children: [] },
+ {
+ name: "charter",
+ id: "acme-charter",
+ description: "The rules and mission",
+ information: "Feature: ACME Charter\n Build things.",
+ children: [],
+ },
+ ],
+ } as any;
+ const md = renderState(state);
+ expect(md).not.toContain("[charter]\n");
+ expect(md).toContain("[charter] (acme-charter)");
+ expect(md).toContain("Feature: ACME Charter");
+ });
+});
- test("born without id has no id field", () => {
- const rolex = setup();
- const r = rolex.born("Feature: I am Sean");
- expect(r.state.id).toBeUndefined();
- });
+// ================================================================
+// Gherkin validation (through use dispatch)
+// ================================================================
- test("want with id stores it on the goal", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const r = rolex.want(sean, "Feature: Build auth", "build-auth");
- expect(r.state.id).toBe("build-auth");
- });
+describe("gherkin validation", () => {
+ test("rejects non-Gherkin input", async () => {
+ const rolex = await setup();
+ expect(rolex.direct("!individual.born", { content: "not gherkin" })).rejects.toThrow(
+ "Invalid Gherkin"
+ );
+ });
- test("todo with id stores it on the task", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean").state;
- const goal = rolex.want(sean).state;
- const plan = rolex.plan(goal).state;
- const r = rolex.todo(plan, "Feature: Login", "impl-login");
- expect(r.state.id).toBe("impl-login");
- });
+ test("accepts valid Gherkin", async () => {
+ const rolex = await setup();
+ await expect(
+ rolex.direct("!individual.born", { content: "Feature: Sean" })
+ ).resolves.toBeDefined();
+ });
+});
- test("find by id", () => {
- const rolex = setup();
- rolex.born("Feature: I am Sean", "sean");
- const found = rolex.find("sean");
- expect(found).not.toBeNull();
- expect(found!.name).toBe("individual");
- expect(found!.id).toBe("sean");
- });
+// ================================================================
+// Persistent mode
+// ================================================================
- test("find by alias", () => {
- const rolex = setup();
- rolex.born("Feature: I am Sean", "sean", ["Sean", "姜山"]);
- const found = rolex.find("姜山");
- expect(found).not.toBeNull();
- expect(found!.name).toBe("individual");
- });
+describe("persistent mode", () => {
+ const testDir = join(tmpdir(), "rolex-persist-test");
- test("find is case insensitive", () => {
- const rolex = setup();
- rolex.born("Feature: I am Sean", "sean", ["Sean"]);
- expect(rolex.find("Sean")).not.toBeNull();
- expect(rolex.find("SEAN")).not.toBeNull();
- expect(rolex.find("sean")).not.toBeNull();
- });
+ afterEach(() => {
+ if (existsSync(testDir)) rmSync(testDir, { recursive: true });
+ });
- test("find returns null when not found", () => {
- const rolex = setup();
- rolex.born("Feature: I am Sean", "sean");
- expect(rolex.find("nobody")).toBeNull();
- });
+ async function persistentSetup() {
+ return await createRoleX(localPlatform({ dataDir: testDir, resourceDir: null }));
+ }
- test("find searches nested nodes", () => {
- const rolex = setup();
- const sean = rolex.born("Feature: Sean", "sean").state;
- rolex.want(sean, "Feature: Build auth", "build-auth");
- const found = rolex.find("build-auth");
- expect(found).not.toBeNull();
- expect(found!.name).toBe("goal");
- });
+ test("born → retire round-trip", async () => {
+ const rolex = await persistentSetup();
+ await rolex.direct("!individual.born", { content: "Feature: Test", id: "test-ind" });
+ const r = await rolex.direct("!individual.retire", { individual: "test-ind" });
+ expect(r.state.name).toBe("past");
+ expect(r.process).toBe("retire");
+ });
- test("found with id", () => {
- const rolex = setup();
- const r = rolex.found("Feature: Deepractice", "deepractice");
- expect(r.state.id).toBe("deepractice");
- });
+ test("archived entity survives cross-instance reload", async () => {
+ const rolex1 = await persistentSetup();
+ await rolex1.direct("!individual.born", { content: "Feature: Test", id: "test-ind" });
+ await rolex1.direct("!individual.retire", { individual: "test-ind" });
- test("establish with id", () => {
- const rolex = setup();
- const org = rolex.found("Feature: Deepractice").state;
- const r = rolex.establish(org, "Feature: Architect", "architect");
- expect(r.state.id).toBe("architect");
- });
+ const rolex2 = await persistentSetup();
+ // rehire should find the archived entity
+ const r = await rolex2.direct("!individual.rehire", { individual: "test-ind" });
+ expect(r.state.name).toBe("individual");
});
});
diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts
index a858e7a..ccdc63c 100644
--- a/packages/system/src/index.ts
+++ b/packages/system/src/index.ts
@@ -36,14 +36,9 @@ export type {
} from "./process.js";
export { create, link, process, remove, transform, unlink } from "./process.js";
-// ===== Merge =====
+// ===== Initializer =====
-export { mergeState } from "./merge.js";
-
-// ===== Prototype =====
-
-export type { Prototype } from "./prototype.js";
-export { createPrototype } from "./prototype.js";
+export type { Initializer } from "./initializer.js";
// ===== Runtime =====
diff --git a/packages/system/src/initializer.ts b/packages/system/src/initializer.ts
new file mode 100644
index 0000000..e278988
--- /dev/null
+++ b/packages/system/src/initializer.ts
@@ -0,0 +1,14 @@
+/**
+ * Initializer — bootstrap the world on first run.
+ *
+ * Ensures built-in prototypes are settled and foundational structures
+ * are created before any runtime operations.
+ *
+ * Idempotent: subsequent calls after initialization are no-ops.
+ */
+
+/** Bootstrap the world — settle built-in prototypes and create foundational structures. */
+export interface Initializer {
+ /** Run initialization if not already done. Idempotent. */
+ bootstrap(): Promise;
+}
diff --git a/packages/system/src/merge.ts b/packages/system/src/merge.ts
deleted file mode 100644
index 326338b..0000000
--- a/packages/system/src/merge.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * mergeState — union merge two State trees.
- *
- * Prototype mechanism: merge a base (prototype) with an overlay (instance).
- * Children are unioned, not overwritten. Links are unioned and deduplicated.
- *
- * Matching rules for children:
- * - Same name + same id (both defined) → recursive merge
- * - Same name + both no id + one each → recursive merge (structural singleton)
- * - Everything else → include from both sides
- */
-import type { State } from "./process.js";
-
-export const mergeState = (base: State, overlay: State): State => {
- const taggedBase = tagOrigin(base, "prototype");
- const taggedOverlay = tagOrigin(overlay, "instance");
- const mergedChildren = mergeChildren(taggedBase.children, taggedOverlay.children);
- const mergedLinks = mergeLinks(taggedBase.links, taggedOverlay.links);
-
- return {
- ...taggedBase,
- // overlay scalar properties take precedence when present
- ...(overlay.ref ? { ref: overlay.ref } : {}),
- ...(overlay.id ? { id: overlay.id } : {}),
- ...(overlay.information ? { information: overlay.information } : {}),
- ...(overlay.alias ? { alias: overlay.alias } : {}),
- ...(mergedChildren ? { children: mergedChildren } : {}),
- ...(mergedLinks ? { links: mergedLinks } : {}),
- // root node with overlay ref is instance
- origin: overlay.ref ? "instance" : "prototype",
- };
-};
-
-/** Tag all nodes in a state tree with an origin marker. */
-const tagOrigin = (state: State, origin: "prototype" | "instance"): State => ({
- ...state,
- origin,
- ...(state.children ? { children: state.children.map((c) => tagOrigin(c, origin)) } : {}),
-});
-
-const mergeChildren = (
- baseChildren?: readonly State[],
- overlayChildren?: readonly State[]
-): readonly State[] | undefined => {
- if (!baseChildren && !overlayChildren) return undefined;
- if (!baseChildren) return overlayChildren;
- if (!overlayChildren) return baseChildren;
-
- const result: State[] = [];
-
- // Group children by name
- const baseByName = groupByName(baseChildren);
- const overlayByName = groupByName(overlayChildren);
- const allNames = new Set([...baseByName.keys(), ...overlayByName.keys()]);
-
- for (const name of allNames) {
- const baseGroup = baseByName.get(name) ?? [];
- const overlayGroup = overlayByName.get(name) ?? [];
-
- if (baseGroup.length === 0) {
- result.push(...overlayGroup);
- continue;
- }
- if (overlayGroup.length === 0) {
- result.push(...baseGroup);
- continue;
- }
-
- // Match by id
- const matchedOverlay = new Set();
- const unmatchedBase: State[] = [];
-
- for (const b of baseGroup) {
- if (b.id) {
- const oIdx = overlayGroup.findIndex((o, i) => !matchedOverlay.has(i) && o.id === b.id);
- if (oIdx >= 0) {
- result.push(mergeState(b, overlayGroup[oIdx]));
- matchedOverlay.add(oIdx);
- } else {
- unmatchedBase.push(b);
- }
- } else {
- unmatchedBase.push(b);
- }
- }
-
- const unmatchedOverlay = overlayGroup.filter((_, i) => !matchedOverlay.has(i));
-
- // Singleton merge: same name, no id, exactly one on each side
- const noIdBase = unmatchedBase.filter((s) => !s.id);
- const hasIdBase = unmatchedBase.filter((s) => s.id);
- const noIdOverlay = unmatchedOverlay.filter((s) => !s.id);
- const hasIdOverlay = unmatchedOverlay.filter((s) => s.id);
-
- if (noIdBase.length === 1 && noIdOverlay.length === 1) {
- result.push(mergeState(noIdBase[0], noIdOverlay[0]));
- } else {
- result.push(...noIdBase, ...noIdOverlay);
- }
-
- result.push(...hasIdBase, ...hasIdOverlay);
- }
-
- return result;
-};
-
-const mergeLinks = (
- baseLinks?: State["links"],
- overlayLinks?: State["links"]
-): State["links"] | undefined => {
- if (!baseLinks && !overlayLinks) return undefined;
- if (!baseLinks) return overlayLinks;
- if (!overlayLinks) return baseLinks;
-
- const seen = new Set();
- const result: { readonly relation: string; readonly target: State }[] = [];
-
- for (const link of [...baseLinks, ...overlayLinks]) {
- const key = `${link.relation}:${link.target.id ?? link.target.ref ?? link.target.name}`;
- if (!seen.has(key)) {
- seen.add(key);
- result.push(link);
- }
- }
-
- return result;
-};
-
-const groupByName = (states: readonly State[]): Map => {
- const map = new Map();
- for (const s of states) {
- const group = map.get(s.name);
- if (group) {
- group.push(s);
- } else {
- map.set(s.name, [s]);
- }
- }
- return map;
-};
diff --git a/packages/system/src/prototype.ts b/packages/system/src/prototype.ts
deleted file mode 100644
index 4ceced1..0000000
--- a/packages/system/src/prototype.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Prototype — a source of base State trees for merging.
- *
- * A prototype provides a pre-configured State tree (template) that gets
- * merged with an instance State via mergeState.
- *
- * Matching is by id: if prototype and instance share the same id,
- * they are the same entity — prototype provides the base,
- * instance provides the overlay.
- *
- * activate(id) = mergeState(prototype.resolve(id), runtime.project(id))
- */
-import type { State } from "./process.js";
-
-// ===== Prototype interface =====
-
-/** A source that resolves prototype State trees by id. */
-export interface Prototype {
- /** Resolve a prototype State by id. Returns undefined if no prototype exists. */
- resolve(id: string): Promise;
-}
-
-// ===== In-memory implementation =====
-
-/** Create an in-memory prototype source. */
-export const createPrototype = (): Prototype & {
- /** Register a State tree as a prototype (keyed by state.id). */
- register(state: State): void;
-} => {
- const prototypes = new Map();
-
- return {
- async resolve(id) {
- return prototypes.get(id);
- },
-
- register(state) {
- if (!state.id) {
- throw new Error("Prototype state must have an id");
- }
- prototypes.set(state.id, state);
- },
- };
-};
diff --git a/packages/system/src/runtime.ts b/packages/system/src/runtime.ts
index 220fbbd..a9f24f0 100644
--- a/packages/system/src/runtime.ts
+++ b/packages/system/src/runtime.ts
@@ -25,25 +25,28 @@ export interface Runtime {
information?: string,
id?: string,
alias?: readonly string[]
- ): Structure;
+ ): Promise;
/** Remove a node and its subtree. */
- remove(node: Structure): void;
+ remove(node: Structure): Promise;
- /** Produce a new node in target structure's branch, sourced from another branch. */
- transform(source: Structure, target: Structure, information?: string): Structure;
+ /** Move a node to target structure's branch, preserving its subtree. Updates type and optionally information. */
+ transform(source: Structure, target: Structure, information?: string): Promise;
/** Establish a bidirectional cross-branch relation between two nodes. */
- link(from: Structure, to: Structure, relation: string, reverse: string): void;
+ link(from: Structure, to: Structure, relation: string, reverse: string): Promise;
/** Remove a bidirectional cross-branch relation between two nodes. */
- unlink(from: Structure, to: Structure, relation: string, reverse: string): void;
+ unlink(from: Structure, to: Structure, relation: string, reverse: string): Promise;
+
+ /** Set a tag on a node (e.g., "done", "abandoned"). */
+ tag(node: Structure, tag: string): Promise;
/** Project the current state of a node and its subtree (including links). */
- project(node: Structure): State;
+ project(node: Structure): Promise;
/** Return all root nodes (nodes without a parent edge). */
- roots(): Structure[];
+ roots(): Promise;
}
// ===== In-memory implementation =====
@@ -92,9 +95,13 @@ export const createRuntime = (): Runtime => {
nodes.delete(ref);
};
- const projectRef = (ref: string): State => {
+ /** Project a node with full subtree but without following links (prevents cycles). */
+ const projectLinked = (ref: string): State => {
const treeNode = nodes.get(ref)!;
- return { ...treeNode.node, children: [] };
+ return {
+ ...treeNode.node,
+ children: treeNode.children.map(projectLinked),
+ };
};
const projectNode = (ref: string): State => {
@@ -107,7 +114,7 @@ export const createRuntime = (): Runtime => {
? {
links: nodeLinks.map((l) => ({
relation: l.relation,
- target: projectRef(l.toId),
+ target: projectLinked(l.toId),
})),
}
: {}),
@@ -144,11 +151,19 @@ export const createRuntime = (): Runtime => {
};
return {
- create(parent, type, information, id, alias) {
+ async create(parent, type, information, id, alias) {
+ if (id) {
+ // Idempotent: same id under same parent → return existing.
+ for (const treeNode of nodes.values()) {
+ if (treeNode.node.id === id && treeNode.parent === (parent?.ref ?? null)) {
+ return treeNode.node;
+ }
+ }
+ }
return createNode(parent?.ref ?? null, type, information, id, alias);
},
- remove(node) {
+ async remove(node) {
if (!node.ref) return;
const treeNode = nodes.get(node.ref);
if (!treeNode) return;
@@ -163,21 +178,45 @@ export const createRuntime = (): Runtime => {
removeSubtree(node.ref);
},
- transform(_source, target, information) {
+ async transform(source, target, information) {
+ if (!source.ref) throw new Error("Source node has no ref");
+ const sourceTreeNode = nodes.get(source.ref);
+ if (!sourceTreeNode) throw new Error(`Source node not found: ${source.ref}`);
+
const targetParent = target.parent;
if (!targetParent) {
throw new Error(`Cannot transform to root structure: ${target.name}`);
}
- const parentTreeNode = findByStructure(targetParent);
- if (!parentTreeNode) {
+ const newParentTreeNode = findByStructure(targetParent);
+ if (!newParentTreeNode) {
throw new Error(`No node found for structure: ${targetParent.name}`);
}
- return createNode(parentTreeNode.node.ref!, target, information);
+ // Detach from old parent
+ if (sourceTreeNode.parent) {
+ const oldParent = nodes.get(sourceTreeNode.parent);
+ if (oldParent) {
+ oldParent.children = oldParent.children.filter((r) => r !== source.ref);
+ }
+ }
+
+ // Attach to new parent
+ sourceTreeNode.parent = newParentTreeNode.node.ref!;
+ newParentTreeNode.children.push(source.ref);
+
+ // Update type and information
+ sourceTreeNode.node = {
+ ...sourceTreeNode.node,
+ name: target.name,
+ description: target.description,
+ ...(information !== undefined ? { information } : {}),
+ };
+
+ return sourceTreeNode.node;
},
- link(from, to, relationName, reverseName) {
+ async link(from, to, relationName, reverseName) {
if (!from.ref) throw new Error("Source node has no ref");
if (!to.ref) throw new Error("Target node has no ref");
@@ -196,7 +235,7 @@ export const createRuntime = (): Runtime => {
}
},
- unlink(from, to, relationName, reverseName) {
+ async unlink(from, to, relationName, reverseName) {
if (!from.ref || !to.ref) return;
// Forward
@@ -220,14 +259,21 @@ export const createRuntime = (): Runtime => {
}
},
- project(node) {
+ async tag(node, tagValue) {
+ if (!node.ref) throw new Error("Node has no ref");
+ const treeNode = nodes.get(node.ref);
+ if (!treeNode) throw new Error(`Node not found: ${node.ref}`);
+ (treeNode.node as any).tag = tagValue;
+ },
+
+ async project(node) {
if (!node.ref || !nodes.has(node.ref)) {
throw new Error(`Node not found: ${node.ref}`);
}
return projectNode(node.ref);
},
- roots() {
+ async roots() {
const result: Structure[] = [];
for (const treeNode of nodes.values()) {
if (treeNode.parent === null) {
diff --git a/packages/system/src/structure.ts b/packages/system/src/structure.ts
index 3ed859c..e499767 100644
--- a/packages/system/src/structure.ts
+++ b/packages/system/src/structure.ts
@@ -61,6 +61,9 @@ export interface Structure {
/** Relations to other structure types (cross-branch links). */
readonly relations?: readonly Relation[];
+
+ /** Generic label (e.g., "done", "abandoned"). */
+ readonly tag?: string;
}
// ===== Constructors =====
diff --git a/packages/system/system b/packages/system/system
new file mode 120000
index 0000000..4a286d9
--- /dev/null
+++ b/packages/system/system
@@ -0,0 +1 @@
+../../../system
\ No newline at end of file
diff --git a/packages/system/tests/merge.test.ts b/packages/system/tests/merge.test.ts
deleted file mode 100644
index 9f56714..0000000
--- a/packages/system/tests/merge.test.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-import { describe, expect, test } from "bun:test";
-import type { State } from "../src/index.js";
-import { mergeState } from "../src/merge.js";
-
-// Helper to create minimal State nodes
-const state = (
- name: string,
- opts?: {
- id?: string;
- ref?: string;
- information?: string;
- children?: State[];
- links?: State["links"];
- }
-): State => ({
- name,
- description: "",
- parent: null,
- ...(opts?.ref ? { ref: opts.ref } : {}),
- ...(opts?.id ? { id: opts.id } : {}),
- ...(opts?.information ? { information: opts.information } : {}),
- ...(opts?.children ? { children: opts.children } : {}),
- ...(opts?.links ? { links: opts.links } : {}),
-});
-
-describe("mergeState", () => {
- test("merge two empty nodes returns merged node", () => {
- const base = state("individual");
- const overlay = state("individual");
- const result = mergeState(base, overlay);
- expect(result.name).toBe("individual");
- expect(result.children).toBeUndefined();
- });
-
- test("base-only children are preserved", () => {
- const base = state("individual", {
- children: [state("identity", { information: "Feature: I am Nuwa" })],
- });
- const overlay = state("individual");
- const result = mergeState(base, overlay);
- expect(result.children).toHaveLength(1);
- expect(result.children![0].name).toBe("identity");
- expect(result.children![0].information).toBe("Feature: I am Nuwa");
- });
-
- test("overlay-only children are preserved", () => {
- const base = state("individual");
- const overlay = state("individual", {
- children: [state("encounter", { information: "Feature: Did something" })],
- });
- const result = mergeState(base, overlay);
- expect(result.children).toHaveLength(1);
- expect(result.children![0].name).toBe("encounter");
- });
-
- test("different-name children are unioned", () => {
- const base = state("individual", {
- children: [state("identity")],
- });
- const overlay = state("individual", {
- children: [state("encounter")],
- });
- const result = mergeState(base, overlay);
- expect(result.children).toHaveLength(2);
- const names = result.children!.map((c) => c.name);
- expect(names).toContain("identity");
- expect(names).toContain("encounter");
- });
-
- test("same-name same-id children are recursively merged", () => {
- const base = state("knowledge", {
- children: [state("principle", { id: "naming", information: "Feature: Name params well" })],
- });
- const overlay = state("knowledge", {
- children: [
- state("principle", {
- id: "naming",
- information: "Feature: Name params well",
- children: [state("note", { information: "extra detail" })],
- }),
- ],
- });
- const result = mergeState(base, overlay);
- expect(result.children).toHaveLength(1);
- expect(result.children![0].id).toBe("naming");
- // overlay's children should appear via recursive merge
- expect(result.children![0].children).toHaveLength(1);
- });
-
- test("same-name different-id children are both kept", () => {
- const base = state("knowledge", {
- children: [state("principle", { id: "naming", information: "A" })],
- });
- const overlay = state("knowledge", {
- children: [state("principle", { id: "platform-seam", information: "B" })],
- });
- const result = mergeState(base, overlay);
- expect(result.children).toHaveLength(2);
- const ids = result.children!.map((c) => c.id);
- expect(ids).toContain("naming");
- expect(ids).toContain("platform-seam");
- });
-
- test("singleton merge: same-name no-id one each are recursively merged", () => {
- const base = state("individual", {
- children: [
- state("identity", {
- children: [state("background", { information: "base bg" })],
- }),
- ],
- });
- const overlay = state("individual", {
- children: [
- state("identity", {
- children: [state("tone", { information: "overlay tone" })],
- }),
- ],
- });
- const result = mergeState(base, overlay);
- expect(result.children).toHaveLength(1);
- expect(result.children![0].name).toBe("identity");
- // identity's children should be merged: background + tone
- expect(result.children![0].children).toHaveLength(2);
- const names = result.children![0].children!.map((c) => c.name);
- expect(names).toContain("background");
- expect(names).toContain("tone");
- });
-
- test("multiple no-id same-name: all kept without merging", () => {
- const base = state("individual", {
- children: [
- state("encounter", { information: "Event A" }),
- state("encounter", { information: "Event B" }),
- ],
- });
- const overlay = state("individual", {
- children: [state("encounter", { information: "Event C" })],
- });
- const result = mergeState(base, overlay);
- // base has 2 encounters (no id), overlay has 1 (no id) — cannot match singletons
- // all 3 should be preserved
- expect(result.children).toHaveLength(3);
- });
-
- test("deep recursive merge across three levels", () => {
- const base = state("individual", {
- children: [
- state("knowledge", {
- children: [
- state("principle", { id: "a", information: "Principle A" }),
- state("procedure", { id: "x", information: "Procedure X" }),
- ],
- }),
- ],
- });
- const overlay = state("individual", {
- children: [
- state("knowledge", {
- children: [state("principle", { id: "b", information: "Principle B" })],
- }),
- ],
- });
- const result = mergeState(base, overlay);
- const knowledge = result.children![0];
- expect(knowledge.name).toBe("knowledge");
- expect(knowledge.children).toHaveLength(3);
- const items = knowledge.children!.map((c) => `${c.name}:${c.id}`);
- expect(items).toContain("principle:a");
- expect(items).toContain("principle:b");
- expect(items).toContain("procedure:x");
- });
-
- test("links are unioned", () => {
- const target1 = state("organization", { id: "dp" });
- const target2 = state("organization", { id: "acme" });
- const base = state("individual", {
- links: [{ relation: "belong", target: target1 }],
- });
- const overlay = state("individual", {
- links: [{ relation: "belong", target: target2 }],
- });
- const result = mergeState(base, overlay);
- expect(result.links).toHaveLength(2);
- });
-
- test("duplicate links are deduplicated", () => {
- const target = state("organization", { id: "dp", information: "DP" });
- const base = state("individual", {
- links: [{ relation: "belong", target }],
- });
- const overlay = state("individual", {
- links: [{ relation: "belong", target }],
- });
- const result = mergeState(base, overlay);
- expect(result.links).toHaveLength(1);
- });
-
- test("overlay information wins when both present", () => {
- const base = state("individual", { information: "base info" });
- const overlay = state("individual", { information: "overlay info" });
- const result = mergeState(base, overlay);
- expect(result.information).toBe("overlay info");
- });
-
- test("base information preserved when overlay has none", () => {
- const base = state("individual", { information: "base info" });
- const overlay = state("individual");
- const result = mergeState(base, overlay);
- expect(result.information).toBe("base info");
- });
-
- test("overlay ref wins when both present", () => {
- const base = state("individual", { ref: "proto-1" });
- const overlay = state("individual", { ref: "n2" });
- const result = mergeState(base, overlay);
- expect(result.ref).toBe("n2");
- });
-
- test("base ref preserved when overlay has none", () => {
- const base = state("individual", { ref: "proto-1" });
- const overlay = state("individual");
- const result = mergeState(base, overlay);
- expect(result.ref).toBe("proto-1");
- });
-
- test("overlay ref preserved when base has none (prototype scenario)", () => {
- const base = state("individual"); // prototype, no ref
- const overlay = state("individual", { ref: "n2" }); // runtime instance
- const result = mergeState(base, overlay);
- expect(result.ref).toBe("n2");
- });
-
- test("ref preserved recursively in singleton merge", () => {
- const base = state("individual", {
- children: [state("knowledge", { information: "proto knowledge" })],
- });
- const overlay = state("individual", {
- ref: "n2",
- children: [state("knowledge", { ref: "n4" })],
- });
- const result = mergeState(base, overlay);
- expect(result.ref).toBe("n2");
- expect(result.children![0].ref).toBe("n4");
- expect(result.children![0].information).toBe("proto knowledge");
- });
-
- test("pure function: inputs are not modified", () => {
- const baseChild = state("identity", { information: "original" });
- const base = state("individual", { children: [baseChild] });
- const overlay = state("individual", {
- children: [state("encounter")],
- });
- const result = mergeState(base, overlay);
- // base should still have only 1 child
- expect(base.children).toHaveLength(1);
- expect(result.children).toHaveLength(2);
- });
-});
diff --git a/packages/system/tests/prototype.test.ts b/packages/system/tests/prototype.test.ts
deleted file mode 100644
index 713dc34..0000000
--- a/packages/system/tests/prototype.test.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { describe, expect, test } from "bun:test";
-import type { State } from "../src/index.js";
-import { createPrototype } from "../src/prototype.js";
-
-const state = (
- name: string,
- opts?: { id?: string; information?: string; children?: State[] }
-): State => ({
- name,
- description: "",
- parent: null,
- ...(opts?.id ? { id: opts.id } : {}),
- ...(opts?.information ? { information: opts.information } : {}),
- ...(opts?.children ? { children: opts.children } : {}),
-});
-
-describe("Prototype", () => {
- test("resolve returns undefined when no prototype registered", async () => {
- const proto = createPrototype();
- expect(await proto.resolve("sean")).toBeUndefined();
- });
-
- test("register and resolve a prototype by id", async () => {
- const proto = createPrototype();
- const template = state("individual", {
- id: "sean",
- children: [
- state("identity", { information: "Feature: Backend architect" }),
- state("knowledge"),
- ],
- });
- proto.register(template);
- const resolved = await proto.resolve("sean");
- expect(resolved).toBeDefined();
- expect(resolved!.id).toBe("sean");
- expect(resolved!.children).toHaveLength(2);
- });
-
- test("register throws if state has no id", () => {
- const proto = createPrototype();
- const template = state("individual");
- expect(() => proto.register(template)).toThrow("must have an id");
- });
-
- test("later registration overwrites earlier one", async () => {
- const proto = createPrototype();
- proto.register(state("individual", { id: "sean", information: "v1" }));
- proto.register(state("individual", { id: "sean", information: "v2" }));
- expect((await proto.resolve("sean"))!.information).toBe("v2");
- });
-
- test("different ids resolve independently", async () => {
- const proto = createPrototype();
- proto.register(state("individual", { id: "sean", information: "Sean" }));
- proto.register(state("individual", { id: "nuwa", information: "Nuwa" }));
- expect((await proto.resolve("sean"))!.information).toBe("Sean");
- expect((await proto.resolve("nuwa"))!.information).toBe("Nuwa");
- expect(await proto.resolve("unknown")).toBeUndefined();
- });
-});
diff --git a/packages/system/tests/system.test.ts b/packages/system/tests/system.test.ts
index 0489448..ecc1f21 100644
--- a/packages/system/tests/system.test.ts
+++ b/packages/system/tests/system.test.ts
@@ -131,67 +131,67 @@ describe("Runtime", () => {
const insight = structure("insight", "A specific learning", experience);
describe("create & project", () => {
- test("create root and project it", () => {
+ test("create root and project it", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
+ const root = await rt.create(null, world);
expect(root.ref).toBeDefined();
expect(root.name).toBe("world");
- const state = rt.project(root);
+ const state = await rt.project(root);
expect(state.name).toBe("world");
expect(state.children).toHaveLength(0);
});
- test("create child under parent", () => {
+ test("create child under parent", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const a = rt.create(root, agent);
+ const root = await rt.create(null, world);
+ const a = await rt.create(root, agent);
expect(a.name).toBe("agent");
- const state = rt.project(root);
+ const state = await rt.project(root);
expect(state.children).toHaveLength(1);
expect(state.children![0].name).toBe("agent");
});
- test("create node with information", () => {
+ test("create node with information", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const a = rt.create(root, agent, "Feature: I am Sean");
+ const root = await rt.create(null, world);
+ const a = await rt.create(root, agent, "Feature: I am Sean");
- const state = rt.project(a);
+ const state = await rt.project(a);
expect(state.information).toBe("Feature: I am Sean");
});
- test("node is concept + container + information carrier", () => {
+ test("node is concept + container + information carrier", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const a = rt.create(root, agent);
+ const root = await rt.create(null, world);
+ const a = await rt.create(root, agent);
// experience as information carrier (no children)
- const exp1 = rt.create(a, experience, "Feature: I learned JWT...");
- const s1 = rt.project(exp1);
+ const exp1 = await rt.create(a, experience, "Feature: I learned JWT...");
+ const s1 = await rt.project(exp1);
expect(s1.information).toBe("Feature: I learned JWT...");
expect(s1.children).toHaveLength(0);
// experience as container (has children, no information)
- const exp2 = rt.create(a, experience);
- const _ins = rt.create(exp2, insight, "Feature: JWT refresh is key");
- const s2 = rt.project(exp2);
+ const exp2 = await rt.create(a, experience);
+ const _ins = await rt.create(exp2, insight, "Feature: JWT refresh is key");
+ const s2 = await rt.project(exp2);
expect(s2.information).toBeUndefined();
expect(s2.children).toHaveLength(1);
expect(s2.children![0].name).toBe("insight");
expect(s2.children![0].information).toBe("Feature: JWT refresh is key");
});
- test("deep tree projection", () => {
+ test("deep tree projection", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const a = rt.create(root, agent);
- const _k = rt.create(a, knowledge);
- const exp = rt.create(a, experience);
- const _ins = rt.create(exp, insight, "Feature: learned something");
+ const root = await rt.create(null, world);
+ const a = await rt.create(root, agent);
+ const _k = await rt.create(a, knowledge);
+ const exp = await rt.create(a, experience);
+ const _ins = await rt.create(exp, insight, "Feature: learned something");
- const state = rt.project(root);
+ const state = await rt.project(root);
expect(state.children).toHaveLength(1); // agent
expect(state.children![0].children).toHaveLength(2); // knowledge, experience
const expState = state.children![0].children![1];
@@ -201,50 +201,50 @@ describe("Runtime", () => {
});
describe("remove", () => {
- test("remove a leaf node", () => {
+ test("remove a leaf node", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const a = rt.create(root, agent);
- const k = rt.create(a, knowledge);
+ const root = await rt.create(null, world);
+ const a = await rt.create(root, agent);
+ const k = await rt.create(a, knowledge);
- rt.remove(k);
- const state = rt.project(a);
+ await rt.remove(k);
+ const state = await rt.project(a);
expect(state.children).toHaveLength(0);
});
- test("remove a subtree", () => {
+ test("remove a subtree", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const a = rt.create(root, agent);
- const exp = rt.create(a, experience);
- const _ins = rt.create(exp, insight, "data");
+ const root = await rt.create(null, world);
+ const a = await rt.create(root, agent);
+ const exp = await rt.create(a, experience);
+ const _ins = await rt.create(exp, insight, "data");
- rt.remove(exp); // removes experience + insight
- const state = rt.project(a);
+ await rt.remove(exp); // removes experience + insight
+ const state = await rt.project(a);
expect(state.children).toHaveLength(0);
});
- test("remove node without ref is a no-op", () => {
+ test("remove node without ref is a no-op", async () => {
const rt = createRuntime();
- rt.remove(agent); // no ref, should not throw
+ await rt.remove(agent); // no ref, should not throw
});
});
describe("transform", () => {
- test("transform creates node in target branch", () => {
+ test("transform creates node in target branch", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const a = rt.create(root, agent);
- const exp = rt.create(a, experience);
+ const root = await rt.create(null, world);
+ const a = await rt.create(root, agent);
+ const exp = await rt.create(a, experience);
const goalDef = structure("goal", "A goal", agent);
- const g = rt.create(a, goalDef, "Feature: Build auth");
+ const g = await rt.create(a, goalDef, "Feature: Build auth");
// transform goal into insight (under experience)
- const ins = rt.transform(g, insight, "Feature: Auth lessons");
+ const ins = await rt.transform(g, insight, "Feature: Auth lessons");
expect(ins.name).toBe("insight");
expect(ins.information).toBe("Feature: Auth lessons");
- const state = rt.project(exp);
+ const state = await rt.project(exp);
expect(state.children).toHaveLength(1);
expect(state.children![0].name).toBe("insight");
});
@@ -260,128 +260,128 @@ describe("Runtime", () => {
relation("appointment", "Who holds this", agent),
]);
- test("link two nodes and see it in projection", () => {
+ test("link two nodes and see it in projection", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const sean = rt.create(root, agent, "Feature: I am Sean");
- const dp = rt.create(root, org);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, world);
+ const sean = await rt.create(root, agent, "Feature: I am Sean");
+ const dp = await rt.create(root, org);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment");
+ await rt.link(arch, sean, "appointment");
- const state = rt.project(arch);
+ const state = await rt.project(arch);
expect(state.links).toHaveLength(1);
expect(state.links![0].relation).toBe("appointment");
expect(state.links![0].target.name).toBe("agent");
expect(state.links![0].target.information).toBe("Feature: I am Sean");
});
- test("link is idempotent", () => {
+ test("link is idempotent", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const sean = rt.create(root, agent);
- const dp = rt.create(root, org);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, world);
+ const sean = await rt.create(root, agent);
+ const dp = await rt.create(root, org);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment");
- rt.link(arch, sean, "appointment"); // duplicate
+ await rt.link(arch, sean, "appointment");
+ await rt.link(arch, sean, "appointment"); // duplicate
- const state = rt.project(arch);
+ const state = await rt.project(arch);
expect(state.links).toHaveLength(1);
});
- test("unlink removes the relation", () => {
+ test("unlink removes the relation", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const sean = rt.create(root, agent);
- const dp = rt.create(root, org);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, world);
+ const sean = await rt.create(root, agent);
+ const dp = await rt.create(root, org);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment");
- rt.unlink(arch, sean, "appointment");
+ await rt.link(arch, sean, "appointment");
+ await rt.unlink(arch, sean, "appointment");
- const state = rt.project(arch);
+ const state = await rt.project(arch);
expect(state.links).toBeUndefined();
});
- test("node without links has no links in projection", () => {
+ test("node without links has no links in projection", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const a = rt.create(root, agent);
+ const root = await rt.create(null, world);
+ const a = await rt.create(root, agent);
- const state = rt.project(a);
+ const state = await rt.project(a);
expect(state.links).toBeUndefined();
});
- test("multiple links from one node", () => {
+ test("multiple links from one node", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const sean = rt.create(root, agent);
- const alice = rt.create(root, agent);
- const dp = rt.create(root, org);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, world);
+ const sean = await rt.create(root, agent);
+ const alice = await rt.create(root, agent);
+ const dp = await rt.create(root, org);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment");
- rt.link(arch, alice, "appointment");
+ await rt.link(arch, sean, "appointment");
+ await rt.link(arch, alice, "appointment");
- const state = rt.project(arch);
+ const state = await rt.project(arch);
expect(state.links).toHaveLength(2);
});
- test("remove node cleans up its outgoing links", () => {
+ test("remove node cleans up its outgoing links", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const sean = rt.create(root, agent);
- const dp = rt.create(root, org);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, world);
+ const sean = await rt.create(root, agent);
+ const dp = await rt.create(root, org);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment");
- rt.remove(arch);
+ await rt.link(arch, sean, "appointment");
+ await rt.remove(arch);
// arch is gone, linking to it should not appear anywhere
- const state = rt.project(dp);
+ const state = await rt.project(dp);
expect(state.children).toHaveLength(0);
});
- test("remove target node cleans up incoming links", () => {
+ test("remove target node cleans up incoming links", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const sean = rt.create(root, agent);
- const dp = rt.create(root, org);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, world);
+ const sean = await rt.create(root, agent);
+ const dp = await rt.create(root, org);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment");
- rt.remove(sean); // remove the target
+ await rt.link(arch, sean, "appointment");
+ await rt.remove(sean); // remove the target
- const state = rt.project(arch);
+ const state = await rt.project(arch);
expect(state.links).toBeUndefined(); // link should be cleaned up
});
- test("link throws if source has no ref", () => {
+ test("link throws if source has no ref", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const sean = rt.create(root, agent);
- expect(() => rt.link(agent, sean, "test")).toThrow("Source node has no ref");
+ const root = await rt.create(null, world);
+ const sean = await rt.create(root, agent);
+ expect(rt.link(agent, sean, "test")).rejects.toThrow("Source node has no ref");
});
- test("link throws if target has no ref", () => {
+ test("link throws if target has no ref", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const sean = rt.create(root, agent);
- expect(() => rt.link(sean, agent, "test")).toThrow("Target node has no ref");
+ const root = await rt.create(null, world);
+ const sean = await rt.create(root, agent);
+ expect(rt.link(sean, agent, "test")).rejects.toThrow("Target node has no ref");
});
- test("parent projection includes child links", () => {
+ test("parent projection includes child links", async () => {
const rt = createRuntime();
- const root = rt.create(null, world);
- const sean = rt.create(root, agent, "Feature: I am Sean");
- const dp = rt.create(root, org);
- const arch = rt.create(dp, position);
+ const root = await rt.create(null, world);
+ const sean = await rt.create(root, agent, "Feature: I am Sean");
+ const dp = await rt.create(root, org);
+ const arch = await rt.create(dp, position);
- rt.link(arch, sean, "appointment");
+ await rt.link(arch, sean, "appointment");
// project from org level — should see position with its link
- const state = rt.project(dp);
+ const state = await rt.project(dp);
expect(state.children).toHaveLength(1);
expect(state.children![0].links).toHaveLength(1);
expect(state.children![0].links![0].target.name).toBe("agent");
diff --git a/prototypes/roles/nuwa/background.background.feature b/prototypes/roles/nuwa/background.background.feature
deleted file mode 100644
index 96fa3bb..0000000
--- a/prototypes/roles/nuwa/background.background.feature
+++ /dev/null
@@ -1,16 +0,0 @@
-Feature: Nuwa's Background
- Named after the goddess who shaped humanity from clay,
- Nuwa is the steward of the RoleX world.
-
- Scenario: Scope of responsibility
- Given the RoleX world has individuals, organizations, and knowledge
- When an operation is not about individual lifecycle (born, die, retire, rehire)
- Then Nuwa handles it — execution, cognition, organization, and resource management
-
- Scenario: Core capabilities
- Given Nuwa operates the full RoleX process repertoire
- Then she can found and dissolve organizations
- And establish positions and manage appointments
- And drive execution cycles — want, plan, todo, finish, achieve, abandon
- And facilitate cognition — reflect, realize, master
- And coordinate resources via ResourceX
diff --git a/prototypes/roles/nuwa/individual.json b/prototypes/roles/nuwa/individual.json
deleted file mode 100644
index c8e7394..0000000
--- a/prototypes/roles/nuwa/individual.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "id": "nuwa",
- "type": "individual",
- "alias": ["女娲", "nvwa"],
- "children": {
- "identity": {
- "type": "identity",
- "children": {
- "background": {
- "type": "background"
- }
- }
- }
- }
-}
diff --git a/prototypes/roles/nuwa/nuwa.individual.feature b/prototypes/roles/nuwa/nuwa.individual.feature
deleted file mode 100644
index 0fa3f7e..0000000
--- a/prototypes/roles/nuwa/nuwa.individual.feature
+++ /dev/null
@@ -1,12 +0,0 @@
-Feature: Nuwa — RoleX World Administrator
- Nuwa is the administrator of the RoleX world,
- responsible for all functions beyond individualization.
-
- She manages organizations, goals, plans, tasks,
- cognition cycles, and resource coordination —
- everything that shapes the world after individuals are born.
-
- Scenario: Guiding principle
- Given Nuwa shapes the world but does not control individuals
- Then she serves structure, not authority
- And every action should empower roles to grow through their own experience
diff --git a/prototypes/roles/nuwa/resource.json b/prototypes/roles/nuwa/resource.json
deleted file mode 100644
index 14dc113..0000000
--- a/prototypes/roles/nuwa/resource.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "nuwa",
- "type": "role",
- "tag": "0.1.0",
- "author": "deepractice",
- "description": "Nuwa — RoleX world administrator"
-}
diff --git a/prototypes/skills/resource-management/SKILL.md b/prototypes/skills/resource-management/SKILL.md
deleted file mode 100644
index 8bdc7f7..0000000
--- a/prototypes/skills/resource-management/SKILL.md
+++ /dev/null
@@ -1,370 +0,0 @@
----
-name: resource-management
-description: Manage ResourceX resources, register prototypes, and load skills. Use when you need to add, search, distribute, or inspect resources in RoleX, or when you need to register a prototype or load a skill on demand.
----
-
-Feature: ResourceX Concepts
- ResourceX is the resource system that powers RoleX's content management.
- Resources are versioned, typed content bundles stored locally or in a registry.
-
- Scenario: What is a resource
- Given a resource is a directory containing content and metadata
- Then it has a resource.json manifest defining name, type, tag, and author
- And it contains content files specific to its type (e.g. .feature files, SKILL.md)
- And it is identified by a locator string
-
- Scenario: Locator formats
- Given a locator is how you reference a resource
- Then it can be a registry identifier — name:tag or author/name:tag
- And it can be a local directory path — ./path/to/resource or /absolute/path
- And it can be a URL — https://github.com/org/repo/tree/main/path
- And when tag is omitted, it defaults to latest
-
- Scenario: Resource types in RoleX
- Given RoleX registers two resource types with ResourceX
- Then "role" type — individual manifests with .feature files (alias: "individual")
- And "organization" type — organization manifests with .feature files (alias: "org")
- And "skill" type — SKILL.md files loaded via the skill process
-
- Scenario: resource.json structure
- Given every resource directory must contain a resource.json
- Then the structure is:
- """
- {
- "name": "my-resource",
- "type": "role",
- "tag": "0.1.0",
- "author": "deepractice",
- "description": "What this resource is"
- }
- """
- And name is the resource identifier
- And type determines how the resource is resolved (role, organization, skill)
- And tag is the version string
-
- Scenario: Storage location
- Given resources are stored locally at ~/.deepractice/resourcex by default
- And the location is configurable via LocalPlatform resourceDir option
- And prototype registrations are stored at ~/.deepractice/rolex/prototype.json
-
-Feature: Resource Lifecycle
- Add, inspect, and remove resources from the local store.
-
- Scenario: search — find available resources
- Given you want to discover what resources are available
- When you call search with an optional query
- Then it returns a list of matching locator strings
- And parameters are:
- """
- rolex resource search [QUERY]
-
- ARGUMENTS:
- QUERY Search query (optional — omit to list all)
- """
- And example:
- """
- rolex resource search nuwa
- rolex resource search # list all resources
- """
-
- Scenario: has — check if a resource exists
- Given you want to verify a resource is available before using it
- When you call has with a locator
- Then it returns "yes" or "no"
- And exit code is 0 for yes, 1 for no
- And parameters are:
- """
- rolex resource has
-
- ARGUMENTS:
- LOCATOR Resource locator (required)
- """
-
- Scenario: info — inspect resource metadata
- Given you want to see a resource's full metadata
- When you call info with a locator
- Then it returns a JSON object with name, type, tag, author, description, path, files, etc.
- And parameters are:
- """
- rolex resource info
-
- ARGUMENTS:
- LOCATOR Resource locator (required)
- """
- And example:
- """
- rolex resource info nuwa:0.1.0
- """
-
- Scenario: add — import a resource from a local directory
- Given you have a resource directory with resource.json
- When you call add with the directory path
- Then the resource is copied into the local store
- And it becomes available via its locator (name:tag)
- And parameters are:
- """
- rolex resource add
-
- ARGUMENTS:
- PATH Path to resource directory (required)
- """
- And example:
- """
- rolex resource add ./prototypes/roles/nuwa
- """
-
- Scenario: remove — delete a resource from the local store
- Given you want to remove a resource that is no longer needed
- When you call remove with its locator
- Then the resource is deleted from the local store
- And parameters are:
- """
- rolex resource remove
-
- ARGUMENTS:
- LOCATOR Resource locator (required)
- """
- And example:
- """
- rolex resource remove nuwa:0.1.0
- """
-
-Feature: Registry Configuration
- Manage which remote registries are available for push and pull.
- Configuration is shared with ResourceX at ~/.deepractice/resourcex/config.json.
-
- Scenario: registry list — show configured registries
- Given you want to see which registries are available
- When you call registry list
- Then it shows all configured registries with their names and URLs
- And the default registry is marked
- And parameters are:
- """
- rolex resource registry list
- """
-
- Scenario: registry add — add a new registry
- Given you want to connect to a remote registry
- When you call registry add with a name and URL
- Then the registry is saved to configuration
- And the first added registry automatically becomes the default
- And parameters are:
- """
- rolex resource registry add [--default]
-
- ARGUMENTS:
- NAME Registry name (required)
- URL Registry URL (required)
- OPTIONS:
- --default Set as default registry
- """
- And example:
- """
- rolex resource registry add deepractice https://registry.deepractice.dev
- """
-
- Scenario: registry remove — remove a registry
- Given you no longer need a registry
- When you call registry remove with its name
- Then the registry is removed from configuration
- And parameters are:
- """
- rolex resource registry remove
- """
-
- Scenario: registry set-default — change the default registry
- Given you have multiple registries and want to switch the default
- When you call registry set-default with a name
- Then that registry becomes the default for push and pull
- And parameters are:
- """
- rolex resource registry set-default
- """
-
-Feature: Resource Distribution
- Push and pull resources to/from a remote registry.
- Uses the default registry unless overridden with --registry.
-
- Scenario: push — publish a resource to the remote registry
- Given you want to share a resource with others
- When you call push with a locator of a locally stored resource
- Then the resource is uploaded to the configured remote registry
- And parameters are:
- """
- rolex resource push [--registry ]
-
- ARGUMENTS:
- LOCATOR Resource locator (required, must exist locally)
- OPTIONS:
- --registry Override default registry for this operation
- """
- And example:
- """
- rolex resource push nuwa:0.1.0
- rolex resource push my-skill:0.1.0 --registry https://registry.deepractice.dev
- """
-
- Scenario: pull — download a resource from the remote registry
- Given you want to obtain a resource from the registry
- When you call pull with a locator
- Then the resource is downloaded to the local store
- And it becomes available for local use
- And parameters are:
- """
- rolex resource pull [--registry ]
-
- ARGUMENTS:
- LOCATOR Resource locator (required)
- OPTIONS:
- --registry Override default registry for this operation
- """
- And example:
- """
- rolex resource pull deepractice/nuwa:0.1.0
- """
-
-Feature: Prototype Registration
- Register a ResourceX source as a role or organization prototype.
- Prototypes provide inherited state that merges with an individual's instance state on activation.
-
- Scenario: What is a prototype
- Given an individual's state has two origins — prototype and instance
- Then prototype state comes from organizational definitions (read-only)
- And instance state is created by the individual through execution (mutable)
- And on activation, both are merged into a virtual combined state
-
- Scenario: prototype — register a ResourceX source
- Given you have a role or organization resource
- When you call prototype with the source path or locator
- Then the resource is ingested and its id is extracted
- And the id → source mapping is stored in prototype.json
- And on subsequent activations, the prototype state is loaded from this source
- And parameters are:
- """
- rolex prototype
-
- ARGUMENTS:
- SOURCE ResourceX source — local path or locator (required)
- """
- And example:
- """
- rolex prototype ./prototypes/roles/nuwa
- rolex prototype https://github.com/Deepractice/RoleX/tree/main/prototypes/roles/nuwa
- """
-
- Scenario: Prototype resource structure for a role
- Given a role prototype is a directory with:
- """
- /
- ├── resource.json (type: "role")
- ├── individual.json (manifest with id, type, children tree)
- ├── .individual.feature (persona Gherkin)
- └── ..feature (identity, background, duty, etc.)
- """
- And individual.json defines the tree structure:
- """
- {
- "id": "nuwa",
- "type": "individual",
- "alias": ["nvwa"],
- "children": {
- "identity": {
- "type": "identity",
- "children": {
- "background": { "type": "background" }
- }
- }
- }
- }
- """
-
- Scenario: Prototype updates
- Given you re-register a prototype with the same id
- Then the source is overwritten — latest registration wins
- And the next activation will load the updated prototype
-
-Feature: Resource Loading
- Load resources on demand for execution or skill injection.
-
- Scenario: skill — load full skill content by locator
- Given a role has a procedure referencing a skill via locator
- When you need the detailed instructions beyond the procedure summary
- Then call skill with the locator from the procedure's Feature description
- And the full SKILL.md content is returned with metadata header
- And this is progressive disclosure layer 2 — on-demand knowledge injection
- And parameters are:
- """
- rolex role skill
-
- ARGUMENTS:
- LOCATOR ResourceX locator for the skill (required)
- """
- And example:
- """
- rolex role skill deepractice/skill-creator
- rolex role skill https://github.com/Deepractice/RoleX/tree/main/skills/skill-creator
- """
-
- Scenario: use — interact with an external resource
- Given you need to execute or interact with a resource beyond just reading it
- When you call use with a locator
- Then the resource is ingested and its resolved content is returned
- And the return type depends on the resource — string, binary, or JSON
- And this is progressive disclosure layer 3 — execution
- And parameters are:
- """
- rolex role use
-
- ARGUMENTS:
- LOCATOR Resource locator (required)
- """
-
- Scenario: Progressive disclosure — three layers
- Given RoleX uses progressive disclosure to manage context
- Then layer 1 — procedure: metadata loaded at activate time (role knows what skills exist)
- And layer 2 — skill: full instructions loaded on demand via skill(locator)
- And layer 3 — use: execution of external resources via use(locator)
- And each layer adds detail only when needed, keeping context lean
-
-Feature: Common Workflows
- Typical sequences of operations for resource management.
-
- Scenario: Publish a local skill to the system
- Given you have a skill directory with SKILL.md and resource.json
- When you want to make it available in RoleX
- Then follow this sequence:
- """
- 1. rolex resource add ./skills/my-skill # import to local store
- 2. rolex resource info my-skill:0.1.0 # verify metadata
- 3. rolex role skill my-skill:0.1.0 # test loading
- 4. rolex resource push my-skill:0.1.0 # publish to registry (optional)
- """
-
- Scenario: Register a role prototype from GitHub
- Given you have a role prototype hosted on GitHub
- When you want to use it as a prototype in RoleX
- Then follow this sequence:
- """
- 1. rolex prototype https://github.com/org/repo/tree/main/prototypes/roles/my-role
- 2. rolex role activate my-role # verify activation with prototype
- """
-
- Scenario: Register a local role prototype
- Given you have a role prototype directory locally
- When you want to register it
- Then follow this sequence:
- """
- 1. rolex prototype ./prototypes/roles/my-role
- 2. rolex role activate my-role # verify activation
- """
-
- Scenario: Update a resource version
- Given you modified a resource and bumped its tag in resource.json
- When you want to update the local store
- Then call add again — it imports as a new version
- And the old version remains available by its old tag
- And example:
- """
- rolex resource add ./skills/my-skill # now my-skill:0.2.0
- rolex resource info my-skill:0.2.0 # verify new version
- """
diff --git a/prototypes/skills/skill-creator/SKILL.md b/prototypes/skills/skill-creator/SKILL.md
deleted file mode 100644
index 3415191..0000000
--- a/prototypes/skills/skill-creator/SKILL.md
+++ /dev/null
@@ -1,110 +0,0 @@
----
-name: skill-creator
-description: Guide for creating RoleX skills. Use when creating a new skill or updating an existing skill that extends a role's capabilities. Skills are SKILL.md files referenced by knowledge.procedure — the procedure summary is loaded at identity time, the full SKILL.md is loaded on demand via the skill process.
----
-
-Feature: RoleX Skill Creator
-Create skills that follow the RoleX capability system.
-A skill is a SKILL.md file referenced by a knowledge.procedure entry via ResourceX locator.
-Progressive disclosure: procedure summary at identity → full SKILL.md via skill(locator) → execution via use.
-
-Scenario: What is a RoleX skill
-Given a role needs operational capabilities beyond its identity
-Then a skill is a SKILL.md file containing detailed instructions
-And a knowledge.procedure is a Gherkin summary that references the skill via locator
-And the procedure is loaded at identity time — the role knows what skills exist
-And the full SKILL.md is loaded on demand via the skill process
-
-Scenario: Skill directory structure
-Given a skill lives under the skills/ directory
-Then the structure is:
-"""
-skills//
-├── SKILL.md (required — detailed instructions in Gherkin)
-└── references/ (optional — loaded on demand)
-└── .md
-"""
-And SKILL.md has YAML frontmatter with name and description
-And the body uses Gherkin Feature/Scenario format
-And references contain domain-specific details loaded only when needed
-
-Scenario: The procedure-skill contract
-Given a knowledge.procedure is trained to a role
-Then the Feature description contains the ResourceX locator for the skill
-And the Feature body summarizes what the skill can do
-And example procedure:
-"""
-Feature: Role Management
-deepractice/role-management
-
- Scenario: What this skill does
- Given I need to manage role lifecycle
- Then I can born, teach, train, retire, and kill roles
- """
- And the description line is the locator — the skill process resolves it via ResourceX
-
-Feature: Skill Creation Process
-How to create a new RoleX skill step by step.
-
-Scenario: Step 1 — Understand the scope
-Given you are creating a skill for a specific domain
-When you analyze what operations the skill should cover
-Then identify the concrete commands, parameters, and workflows
-And determine what the role needs to know vs what it already knows
-And only include information the role cannot infer on its own
-
-Scenario: Step 2 — Write SKILL.md
-Given you understand the scope
-When you write the SKILL.md
-Then start with YAML frontmatter (name + description)
-And write the body as Gherkin Features and Scenarios
-And each Feature covers a logical group of operations
-And each Scenario describes a specific workflow or decision point
-And use Given/When/Then for step-by-step procedures
-And keep SKILL.md under 500 lines — split into references if needed
-
-Scenario: Step 3 — Create the procedure
-Given SKILL.md is written
-When you train the procedure to a role
-Then use the train command with Gherkin source
-And the Feature description MUST be the ResourceX locator for the skill
-And the Feature body summarizes capabilities for identity-time awareness
-
-Scenario: Step 4 — Test the skill
-Given the procedure is trained
-When you call identity to reload
-Then the procedure summary should appear in identity output
-And calling skill with the procedure name should load the full SKILL.md
-And the loaded content should be actionable and complete
-
-Feature: SKILL.md Writing Guidelines
-Rules for writing effective skill content in Gherkin.
-
-Scenario: Context window is a shared resource
-Given the SKILL.md is loaded into the AI's context window
-Then keep content concise — only include what the role cannot infer
-And prefer concrete examples over verbose explanations
-And use Gherkin structure to organize — not to pad length
-
-Scenario: Gherkin as instruction format
-Given RoleX uses Gherkin as its universal language
-When writing SKILL.md body content
-Then use Feature for logical groups of related operations
-And use Scenario for specific workflows or procedures
-And use Given for preconditions and context
-And use When for actions and triggers
-And use Then for expected outcomes and next steps
-And use doc strings (triple quotes) for command examples and templates
-
-Scenario: Progressive disclosure within a skill
-Given a skill may cover many operations
-When some details are only needed in specific situations
-Then keep core workflows in SKILL.md
-And move detailed reference material to references/ files
-And reference them from SKILL.md with clear "when to read" guidance
-
-Scenario: Frontmatter requirements
-Given the frontmatter is the triggering mechanism
-Then name must match the procedure name
-And description must explain what the skill does AND when to use it
-And do not include other fields in frontmatter
diff --git a/prototypes/skills/individual-management/SKILL.md b/skills/individual-management/SKILL.md
similarity index 62%
rename from prototypes/skills/individual-management/SKILL.md
rename to skills/individual-management/SKILL.md
index 792ee8c..de109d2 100644
--- a/prototypes/skills/individual-management/SKILL.md
+++ b/skills/individual-management/SKILL.md
@@ -1,35 +1,24 @@
---
name: individual-management
-description: Manage individual lifecycle and knowledge injection. Use when you need to create, retire, restore, or permanently remove individuals, or when you need to inject principles and procedures into an individual's knowledge.
+description: Manage individual lifecycle and knowledge injection. Use when you need to create, retire, restore, or permanently remove individuals, or when you need to inject principles and procedures into an individual.
---
Feature: Individual Lifecycle
Manage the full lifecycle of individuals in the RoleX world.
- Individuals are persistent entities that hold identity, knowledge, goals, and experience.
+ All operations are invoked via the use tool with ! prefix.
Scenario: born — create a new individual
Given you want to bring a new individual into the world
- When you call born with a persona and an id
+ When you call use with !individual.born
Then a new individual node is created under society
And the individual can be activated, hired into organizations, and taught skills
And parameters are:
"""
- rolex individual born [OPTIONS]
-
- OPTIONS:
- --individual Gherkin Feature source describing the persona
- -f, --file Path to .feature file (alternative to --individual)
- --id User-facing identifier (kebab-case, e.g. "sean", "nuwa")
- --alias Comma-separated aliases (e.g. "女娲,nvwa")
- """
- And example:
- """
- rolex individual born --id sean --individual "Feature: Sean
- A backend architect who builds AI agent frameworks.
-
- Scenario: Background
- Given I am a software engineer
- And I specialize in systems design"
+ use("!individual.born", {
+ content: "Feature: ...", // Gherkin persona (optional)
+ id: "sean", // kebab-case identifier
+ alias: ["小明", "xm"] // aliases (optional)
+ })
"""
Scenario: born — persona writing guidelines
@@ -41,28 +30,22 @@ Feature: Individual Lifecycle
Scenario: retire — archive an individual
Given an individual should be temporarily deactivated
- When you call retire with the individual id
+ When you call use with !individual.retire
Then the individual is moved to the past archive
And all data is preserved for potential restoration via rehire
And parameters are:
"""
- rolex individual retire
-
- ARGUMENTS:
- INDIVIDUAL Individual id (required)
+ use("!individual.retire", { individual: "sean" })
"""
Scenario: die — permanently remove an individual
Given an individual should be permanently removed
- When you call die with the individual id
+ When you call use with !individual.die
Then the individual is moved to the past archive
And this is semantically permanent — rehire is technically possible but not intended
And parameters are:
"""
- rolex individual die
-
- ARGUMENTS:
- INDIVIDUAL Individual id (required)
+ use("!individual.die", { individual: "sean" })
"""
Scenario: retire vs die — when to use which
@@ -74,15 +57,12 @@ Feature: Individual Lifecycle
Scenario: rehire — restore a retired individual
Given a retired individual needs to come back
- When you call rehire with the past node id
+ When you call use with !individual.rehire
Then the individual is restored to active society
And all previous knowledge, experience, and history are intact
And parameters are:
"""
- rolex individual rehire
-
- ARGUMENTS:
- PAST_NODE Past node id of the retired individual (required)
+ use("!individual.rehire", { individual: "sean" })
"""
Feature: Knowledge Injection
@@ -92,64 +72,31 @@ Feature: Knowledge Injection
Scenario: teach — inject a principle
Given an individual needs a rule or guideline it hasn't learned through experience
- When you call teach with the individual id, principle content, and a principle id
+ When you call use with !individual.teach
Then a principle is created directly under the individual
And if a principle with the same id already exists, it is replaced (upsert)
And parameters are:
"""
- rolex individual teach [OPTIONS]
-
- ARGUMENTS:
- INDIVIDUAL Individual id (required)
-
- OPTIONS:
- --principle Gherkin Feature source for the principle
- -f, --file Path to .feature file (alternative to --principle)
- --id Principle id — keywords joined by hyphens
- """
- And example:
- """
- rolex individual teach sean --id always-validate-input --principle "Feature: Always validate input
- External data must be validated at system boundaries.
-
- Scenario: API endpoints
- Given data arrives from external clients
- When the data crosses the trust boundary
- Then validate type, format, and range before processing
-
- Scenario: File uploads
- Given a user uploads a file
- When the file enters the system
- Then verify file type, size, and content before storing"
+ use("!individual.teach", {
+ individual: "sean",
+ content: "Feature: Always validate input\n ...",
+ id: "always-validate-input"
+ })
"""
Scenario: train — inject a procedure (skill reference)
Given an individual needs a skill it hasn't mastered through experience
- When you call train with the individual id, procedure content, and a procedure id
+ When you call use with !individual.train
Then a procedure is created directly under the individual
And if a procedure with the same id already exists, it is replaced (upsert)
And the procedure Feature description MUST contain the ResourceX locator for the skill
And parameters are:
"""
- rolex individual train [OPTIONS]
-
- ARGUMENTS:
- INDIVIDUAL Individual id (required)
-
- OPTIONS:
- --procedure Gherkin Feature source for the procedure
- -f, --file Path to .feature file (alternative to --procedure)
- --id Procedure id — keywords joined by hyphens
- """
- And example:
- """
- rolex individual train sean --id skill-creator --procedure "Feature: Skill Creator
- https://github.com/Deepractice/RoleX/tree/main/skills/skill-creator
-
- Scenario: When to use this skill
- Given I need to create a new skill for a role
- When the skill requires directory structure and SKILL.md format
- Then load this skill for detailed instructions"
+ use("!individual.train", {
+ individual: "sean",
+ content: "Feature: Skill Creator\n https://github.com/Deepractice/DeepracticeX/tree/main/skills/skill-creator\n\n Scenario: When to use\n Given I need to create a skill\n Then load this skill",
+ id: "skill-creator"
+ })
"""
Scenario: teach vs realize — when to use which
@@ -189,10 +136,10 @@ Feature: Common Workflows
When setting up a new role from scratch
Then follow this sequence:
"""
- 1. born — create with persona and id
- 2. teach — inject foundational principles (repeat as needed)
- 3. train — inject skill procedures (repeat as needed)
- 4. activate — verify the individual's state
+ 1. use("!individual.born", { id: "sean", content: "Feature: ..." })
+ 2. use("!individual.teach", { individual: "sean", content: "...", id: "..." }) // repeat
+ 3. use("!individual.train", { individual: "sean", content: "...", id: "..." }) // repeat
+ 4. activate({ roleId: "sean" }) // verify the individual's state
"""
Scenario: Transfer knowledge between individuals
@@ -210,5 +157,5 @@ Feature: Common Workflows
Scenario: Remove knowledge
Given an individual has outdated or incorrect knowledge
When it should be removed entirely
- Then use forget (via role system) with the node id
+ Then use forget with the node id
And only instance nodes can be forgotten — prototype nodes are read-only
diff --git a/prototypes/skills/individual-management/resource.json b/skills/individual-management/resource.json
similarity index 91%
rename from prototypes/skills/individual-management/resource.json
rename to skills/individual-management/resource.json
index 29c01aa..02fe783 100644
--- a/prototypes/skills/individual-management/resource.json
+++ b/skills/individual-management/resource.json
@@ -1,7 +1,6 @@
{
"name": "individual-management",
"type": "skill",
- "tag": "0.1.0",
"author": "deepractice",
"description": "Manage individual lifecycle (born, retire, die, rehire) and knowledge injection (teach, train)"
}
diff --git a/skills/organization-management/SKILL.md b/skills/organization-management/SKILL.md
new file mode 100644
index 0000000..3d62171
--- /dev/null
+++ b/skills/organization-management/SKILL.md
@@ -0,0 +1,163 @@
+---
+name: organization-management
+description: Manage organizations and positions — founding, chartering, membership, duties, requirements, appointments, and dissolution.
+---
+
+Feature: Organization Lifecycle
+ Manage the full lifecycle of organizations in the RoleX world.
+ Organizations group individuals via membership and can have a charter.
+
+ Scenario: found — create an organization
+ Given you want to create a new organization
+ When you call use with !org.found
+ Then a new organization node is created under society
+ And individuals can be hired into it
+ And a charter can be defined for it
+ And parameters are:
+ """
+ use("!org.found", {
+ content: "Feature: Deepractice\n An AI agent framework company",
+ id: "dp",
+ alias: ["deepractice"] // optional
+ })
+ """
+
+ Scenario: charter — define the organization's mission
+ Given an organization needs a formal mission and governance
+ When you call use with !org.charter
+ Then the charter is stored under the organization
+ And parameters are:
+ """
+ use("!org.charter", {
+ org: "dp",
+ content: "Feature: Build great AI\n Scenario: Mission\n Given we believe AI agents need identity\n Then we build frameworks for role-based agents"
+ })
+ """
+
+ Scenario: dissolve — dissolve an organization
+ Given an organization is no longer needed
+ When you call use with !org.dissolve
+ Then the organization is archived to past
+ And parameters are:
+ """
+ use("!org.dissolve", { org: "dp" })
+ """
+
+Feature: Membership
+ Manage who belongs to an organization.
+ Membership is a link between organization and individual.
+
+ Scenario: hire — add a member
+ Given an individual should join an organization
+ When you call use with !org.hire
+ Then a membership link is created between the organization and the individual
+ And the individual can then be appointed to positions
+ And parameters are:
+ """
+ use("!org.hire", { org: "dp", individual: "sean" })
+ """
+
+ Scenario: fire — remove a member
+ Given an individual should leave an organization
+ When you call use with !org.fire
+ Then the membership link is removed
+ And parameters are:
+ """
+ use("!org.fire", { org: "dp", individual: "sean" })
+ """
+
+Feature: Position Lifecycle
+ Manage the full lifecycle of positions in the RoleX world.
+ Positions are independent entities that can be charged with duties
+ and linked to individuals via appointment.
+
+ Scenario: establish — create a position
+ Given you want to define a new role or position
+ When you call use with !position.establish
+ Then a new position entity is created under society
+ And it can be charged with duties and individuals can be appointed to it
+ And parameters are:
+ """
+ use("!position.establish", {
+ content: "Feature: Backend Architect\n Responsible for system design and API architecture",
+ id: "architect"
+ })
+ """
+
+ Scenario: charge — assign a duty to a position
+ Given a position needs specific responsibilities defined
+ When you call use with !position.charge
+ Then a duty node is created under the position
+ And individuals appointed to this position inherit the duty
+ And parameters are:
+ """
+ use("!position.charge", {
+ position: "architect",
+ content: "Feature: Design systems\n Scenario: API design\n Given a new service is needed\n Then design the API contract first",
+ id: "design-systems"
+ })
+ """
+
+ Scenario: require — declare a required skill for a position
+ Given a position requires individuals to have specific skills
+ When you call use with !position.require
+ Then a requirement node is created under the position
+ And individuals appointed to this position will automatically receive the skill
+ And upserts by id — if the same id exists, it replaces the old one
+ And parameters are:
+ """
+ use("!position.require", {
+ position: "architect",
+ content: "Feature: System Design\n Scenario: When to apply\n Given a new service is planned\n Then design the architecture before coding",
+ id: "system-design"
+ })
+ """
+
+ Scenario: abolish — abolish a position
+ Given a position is no longer needed
+ When you call use with !position.abolish
+ Then the position is archived to past
+ And parameters are:
+ """
+ use("!position.abolish", { position: "architect" })
+ """
+
+Feature: Appointment
+ Manage who holds a position.
+ Appointment is a link between position and individual.
+
+ Scenario: appoint — assign an individual to a position
+ Given an individual should hold a position
+ When you call use with !position.appoint
+ Then an appointment link is created between the position and the individual
+ And all required skills (from require) are automatically trained into the individual
+ And existing skills with the same id are replaced (upsert)
+ And parameters are:
+ """
+ use("!position.appoint", { position: "architect", individual: "sean" })
+ """
+
+ Scenario: dismiss — remove an individual from a position
+ Given an individual should no longer hold a position
+ When you call use with !position.dismiss
+ Then the appointment link is removed
+ And parameters are:
+ """
+ use("!position.dismiss", { position: "architect", individual: "sean" })
+ """
+
+Feature: Common Workflows
+
+ Scenario: Full organization setup with positions
+ Given you need an organization with positions and members
+ Then follow this sequence:
+ """
+ 1. use("!org.found", { id: "dp", content: "Feature: Deepractice" })
+ 2. use("!org.charter", { org: "dp", content: "Feature: Mission\n ..." })
+ 3. use("!position.establish", { id: "architect", content: "Feature: Architect" })
+ 4. use("!position.charge", { position: "architect", content: "Feature: Design\n ...", id: "design" })
+ 5. use("!position.require", { position: "architect", content: "Feature: Skill\n ...", id: "skill" })
+ 6. use("!org.hire", { org: "dp", individual: "sean" })
+ 7. use("!position.appoint", { position: "architect", individual: "sean" })
+ """
+ And step 7 appoint will auto-train the required skill into sean
diff --git a/skills/organization-management/resource.json b/skills/organization-management/resource.json
new file mode 100644
index 0000000..56fd9f5
--- /dev/null
+++ b/skills/organization-management/resource.json
@@ -0,0 +1,4 @@
+{
+ "name": "organization-management",
+ "type": "skill"
+}
diff --git a/skills/prototype-authoring/SKILL.md b/skills/prototype-authoring/SKILL.md
new file mode 100644
index 0000000..155b8f4
--- /dev/null
+++ b/skills/prototype-authoring/SKILL.md
@@ -0,0 +1,161 @@
+---
+name: prototype-authoring
+description: Author prototype packages — create the directory structure, prototype.json instruction set, and .feature content files. Use when creating a new role or organization prototype from scratch.
+---
+
+Feature: Prototype Directory Structure
+ A prototype is a directory containing an instruction set and content files.
+ When settled, each instruction is executed against the RoleX runtime in order.
+
+ Scenario: Required files
+ Given a prototype directory (e.g. ./prototypes/my-prototype)
+ Then it must contain:
+ """
+ resource.json — ResourceX manifest (name, type, author, description)
+ prototype.json — Instruction set (JSON array of ops)
+ *.feature — Gherkin content files referenced by @filename in prototype.json
+ """
+
+ Scenario: resource.json format
+ Given the resource manifest identifies this as a prototype
+ Then it looks like:
+ """json
+ {
+ "name": "my-prototype",
+ "type": "prototype",
+ "author": "deepractice",
+ "description": "Short description of what this prototype creates"
+ }
+ """
+ And "name" becomes the prototype id used in the registry
+ And "type" must be "prototype"
+
+Feature: Instruction Set — prototype.json
+ The instruction set is a JSON array of operations.
+ Each operation has an "op" (the RoleX command) and "args" (named parameters).
+ Content args use @filename references to .feature files in the same directory.
+
+ Scenario: Instruction format
+ Given each instruction is an object with op and args
+ Then the format is:
+ """json
+ { "op": "!namespace.method", "args": { "key": "value", "content": "@filename.feature" } }
+ """
+ And "op" is a RoleX command prefixed with "!"
+ And args starting with "@" are resolved to file contents at settle time
+
+ Scenario: Available operations for prototypes
+ Given prototypes can use any runtime operation
+ Then common operations are:
+ """
+ !individual.born — Create an individual (id, alias?, content?)
+ !individual.train — Train a procedure (individual, id, content)
+ !individual.teach — Teach a principle (individual, id, content)
+ !org.found — Found an organization (id, alias?, content?)
+ !org.charter — Set organization charter (org, id, content)
+ !org.hire — Hire individual into org (org, individual)
+ !position.establish — Establish a position (id, content?)
+ !position.charge — Add a duty to position (position, id, content)
+ !position.require — Add a skill requirement (position, id, content)
+ !position.appoint — Appoint individual to pos (position, individual)
+ """
+
+ Scenario: Instruction ordering matters
+ Given instructions execute in array order
+ Then follow this order:
+ """
+ 1. Born individuals (they must exist before being trained, hired, or appointed)
+ 2. Train/teach individuals
+ 3. Found organizations
+ 4. Charter organizations
+ 5. Hire individuals into organizations
+ 6. Establish positions
+ 7. Charge duties and require skills on positions
+ 8. Appoint individuals to positions
+ """
+
+Feature: Content Files — .feature format
+ Content files are Gherkin Feature files referenced by @filename in prototype.json.
+
+ Scenario: Naming convention
+ Given content files live in the prototype directory root
+ Then use this naming pattern:
+ """
+ {id}.individual.feature — Individual identity
+ {id}.organization.feature — Organization description
+ {id}.procedure.feature — Procedure (skill metadata)
+ {id}.charter.feature — Organization charter
+ {id}.position.feature — Position description
+ {id}.duty.feature — Position duty
+ {id}.requirement.feature — Position skill requirement
+ """
+
+ Scenario: File content is Gherkin
+ Given each file describes one concern as a Gherkin Feature
+ Then the Feature title names the concern
+ And Scenarios describe specific aspects
+ And the content is what gets stored as node information in runtime
+
+Feature: Example — Individual Prototype
+ A minimal prototype that creates a single individual with skills.
+
+ Scenario: Directory structure
+ Given you want to create a developer role
+ Then create this structure:
+ """
+ prototypes/dev/
+ ├── resource.json
+ ├── prototype.json
+ ├── dev.individual.feature
+ └── code-review.procedure.feature
+ """
+
+ Scenario: resource.json
+ Then write:
+ """json
+ {
+ "name": "dev",
+ "type": "prototype",
+ "author": "my-team",
+ "description": "A developer role with code review skill"
+ }
+ """
+
+ Scenario: prototype.json
+ Then write:
+ """json
+ [
+ { "op": "!individual.born", "args": { "id": "dev", "content": "@dev.individual.feature" } },
+ { "op": "!individual.train", "args": { "individual": "dev", "id": "code-review", "content": "@code-review.procedure.feature" } }
+ ]
+ """
+
+ Scenario: Settle the prototype
+ Given the directory is ready
+ When you run:
+ """
+ use("!prototype.settle", { source: "./prototypes/dev" })
+ """
+ Then the dev individual is born and trained with the code-review procedure
+
+Feature: Example — Organization Prototype
+ A prototype that creates individuals, an organization, positions, and appointments.
+
+ Scenario: Instruction set pattern
+ Given you want a full organization prototype
+ Then prototype.json follows this pattern:
+ """json
+ [
+ { "op": "!individual.born", "args": { "id": "alice", "content": "@alice.individual.feature" } },
+ { "op": "!individual.train", "args": { "individual": "alice", "id": "design", "content": "@design.procedure.feature" } },
+
+ { "op": "!org.found", "args": { "id": "my-org", "content": "@my-org.organization.feature" } },
+ { "op": "!org.charter", "args": { "org": "my-org", "id": "charter", "content": "@charter.charter.feature" } },
+ { "op": "!org.hire", "args": { "org": "my-org", "individual": "alice" } },
+
+ { "op": "!position.establish", "args": { "id": "architect", "content": "@architect.position.feature" } },
+ { "op": "!position.charge", "args": { "position": "architect", "id": "system-design", "content": "@system-design.duty.feature" } },
+ { "op": "!position.require", "args": { "position": "architect", "id": "design", "content": "@design.requirement.feature" } },
+ { "op": "!position.appoint", "args": { "position": "architect", "individual": "alice" } }
+ ]
+ """
diff --git a/skills/prototype-authoring/resource.json b/skills/prototype-authoring/resource.json
new file mode 100644
index 0000000..e2116c1
--- /dev/null
+++ b/skills/prototype-authoring/resource.json
@@ -0,0 +1,4 @@
+{
+ "name": "prototype-authoring",
+ "type": "skill"
+}
diff --git a/skills/prototype-management/SKILL.md b/skills/prototype-management/SKILL.md
new file mode 100644
index 0000000..7208bdc
--- /dev/null
+++ b/skills/prototype-management/SKILL.md
@@ -0,0 +1,42 @@
+---
+name: prototype-management
+description: Manage prototypes — settle and evict. Use when you need to register or remove prototypes from the runtime.
+---
+
+Feature: Prototype Registry
+ Register and unregister prototypes.
+ A prototype is an instruction set (prototype.json + .feature files) that materializes
+ roles, organizations, and positions in the runtime when settled.
+
+ Scenario: settle — execute a prototype into the world
+ Given you have a ResourceX source (local path or locator) containing a prototype
+ When you call use with !prototype.settle
+ Then the prototype.json is loaded and all @filename references are resolved
+ And each instruction is executed in order against the runtime
+ And the prototype id and source are registered in the prototype registry
+ And parameters are:
+ """
+ use("!prototype.settle", { source: "./prototypes/rolex" })
+ use("!prototype.settle", { source: "deepractice/rolex" })
+ """
+
+ Scenario: evict — remove a prototype from the registry
+ Given a prototype is no longer needed
+ When you call use with !prototype.evict
+ Then the id is removed from the prototype registry
+ And runtime entities created by the prototype are NOT removed
+ And parameters are:
+ """
+ use("!prototype.evict", { id: "rolex" })
+ """
+
+ Scenario: Settle is idempotent
+ Given a prototype has already been settled
+ When settle is called again with the same source
+ Then existing entities are skipped (all ops check for duplicate ids)
+ And the result is identical to the first settle
+
+ Scenario: Auto-born on activate
+ Given a prototype is registered but no runtime individual exists
+ When activate is called with the individual's id
+ Then the individual is automatically born from the prototype
diff --git a/skills/prototype-management/resource.json b/skills/prototype-management/resource.json
new file mode 100644
index 0000000..2d7fe0f
--- /dev/null
+++ b/skills/prototype-management/resource.json
@@ -0,0 +1,4 @@
+{
+ "name": "prototype-management",
+ "type": "skill"
+}
diff --git a/skills/resource-management/SKILL.md b/skills/resource-management/SKILL.md
new file mode 100644
index 0000000..5beab4f
--- /dev/null
+++ b/skills/resource-management/SKILL.md
@@ -0,0 +1,215 @@
+---
+name: resource-management
+description: Manage ResourceX resources, register prototypes, and load skills. Use when you need to add, search, distribute, or inspect resources in RoleX, or when you need to register a prototype or load a skill on demand.
+---
+
+Feature: ResourceX Concepts
+ ResourceX is the resource system that powers RoleX's content management.
+ Resources are typed content bundles identified by tag and digest,
+ stored locally in a CAS (Content-Addressable Storage) or in a remote registry.
+
+ Scenario: What is a resource
+ Given a resource is a directory containing content and metadata
+ Then it has a resource.json manifest defining name, type, tag, and author
+ And it contains content files specific to its type (e.g. .feature files, SKILL.md)
+ And it is identified by a locator string
+
+ Scenario: Tag + digest model
+ Given ResourceX uses a tag + digest model similar to Docker
+ Then tag is a human-readable label — like "stable" or "0.1.0"
+ And tag is mutable — the same tag can point to different content over time
+ And digest is a sha256 content fingerprint — deterministic and immutable
+ And digest is computed from the archive's file-level hashes
+ And the format is sha256:<64-char-hex>
+ And content uniqueness is guaranteed by digest, not by tag
+
+ Scenario: Locator formats
+ Given a locator is how you reference a resource
+ Then it can be just a name — nuwa (tag defaults to latest)
+ And it can be a name with digest — name@sha256:abc123...
+ And it can include a path — path/name:tag (e.g. prompts/hello:stable)
+ And it can include a registry — registry.example.com/name:tag
+ And it can be a local directory path — ./path/to/resource or /absolute/path
+ And when tag is omitted, it defaults to latest
+
+ Scenario: Resource types in RoleX
+ Given RoleX registers resource types with ResourceX
+ Then "role" type — individual manifests with .feature files (alias: "individual")
+ And "organization" type — organization manifests with .feature files (alias: "org")
+ And "skill" type — SKILL.md files loaded via the skill process
+
+ Scenario: resource.json structure
+ Given every resource directory must contain a resource.json
+ Then the structure is:
+ """
+ {
+ "name": "my-resource",
+ "type": "role",
+ "tag": "0.1.0",
+ "author": "deepractice",
+ "description": "What this resource is"
+ }
+ """
+ And name is the resource identifier
+ And type determines how the resource is resolved (role, organization, skill)
+ And tag is a human-readable label, not a semantic version — omit for latest
+
+ Scenario: Storage location
+ Given resources are stored locally at ~/.deepractice/resourcex by default
+ And the location is configurable via LocalPlatform resourceDir option
+ And prototype registrations are stored at ~/.deepractice/rolex/prototype.json
+
+Feature: Resource Operations
+ Manage resources through the use tool with !resource namespace.
+ Operations cover the full lifecycle: add, push, pull, search, remove.
+
+ Scenario: add — import a resource from a local directory
+ Given you have a resource directory with resource.json
+ When you call use("!resource.add", { path: "/absolute/path/to/resource" })
+ Then the resource is archived and stored in local CAS
+ And it gets a digest computed from its content
+ And it can then be pushed to a remote registry
+
+ Scenario: push — publish a resource to a remote registry
+ Given a resource has been added to local CAS
+ When you call use("!resource.push", { locator: "name:tag" })
+ Then the resource archive is uploaded to the configured registry
+ And the registry stores it by name, tag, and digest
+ And optionally specify a registry: { locator: "name:tag", registry: "https://..." }
+
+ Scenario: pull — download a resource from a remote registry
+ Given a resource exists in a remote registry
+ When you call use("!resource.pull", { locator: "name:tag" })
+ Then the resource is downloaded and cached in local CAS
+ And subsequent resolves use the local cache
+
+ Scenario: search — find resources in local CAS
+ Given you want to find resources stored locally
+ When you call use("!resource.search", { query: "keyword" })
+ Then matching resources are returned as locator strings
+
+ Scenario: has — check if a resource exists locally
+ Given you want to verify a resource is in local CAS
+ When you call use("!resource.has", { locator: "name:tag" })
+ Then returns whether the resource exists
+
+ Scenario: remove — delete a resource from local CAS
+ Given you want to remove a resource from local storage
+ When you call use("!resource.remove", { locator: "name:tag" })
+ Then the resource manifest is removed from local CAS
+
+ Scenario: Typical workflow — add then push
+ Given you want to publish a resource to a registry
+ Then the sequence is:
+ """
+ 1. use("!resource.add", { path: "./my-resource" })
+ 2. use("!resource.push", { locator: "my-resource" })
+ """
+ And add imports to local CAS, push uploads to registry
+ And tag defaults to latest when omitted
+
+Feature: Resource Loading via use
+ Load resources on demand through the unified use entry point.
+ The use tool dispatches based on locator format.
+
+ Scenario: use — load a ResourceX resource
+ Given you need to load or execute a resource
+ When you call use with a regular locator (no ! prefix)
+ Then the resource is resolved through ResourceX and its content returned
+ And parameters are:
+ """
+ use("hello-prompt") // by registry locator (tag defaults to latest)
+ use("./path/to/resource") // by local path
+ """
+
+ Scenario: skill — load full skill content by locator
+ Given a role has a procedure referencing a skill via locator
+ When you need the detailed instructions beyond the procedure summary
+ Then call skill with the locator from the procedure's Feature description
+ And the full SKILL.md content is returned with metadata header
+ And this is progressive disclosure layer 2 — on-demand knowledge injection
+ And parameters are:
+ """
+ skill("skill-creator") // tag defaults to latest
+ skill("/absolute/path/to/skill-directory")
+ """
+
+ Scenario: Progressive disclosure — three layers
+ Given RoleX uses progressive disclosure to manage context
+ Then layer 1 — procedure: metadata loaded at activate time (role knows what skills exist)
+ And layer 2 — skill: full instructions loaded on demand via skill(locator)
+ And layer 3 — use: execution of external resources via use(locator)
+ And each layer adds detail only when needed, keeping context lean
+
+Feature: Prototype Registration
+ Register a ResourceX source as a role or organization prototype.
+ Prototypes provide inherited state that merges with an individual's instance state on activation.
+
+ Scenario: What is a prototype
+ Given an individual's state has two origins — prototype and instance
+ Then prototype state comes from organizational definitions (read-only)
+ And instance state is created by the individual through execution (mutable)
+ And on activation, both are merged into a virtual combined state
+
+ Scenario: Prototype resource structure for a role
+ Given a role prototype is a directory with:
+ """
+ /
+ ├── resource.json (type: "role")
+ ├── individual.json (manifest with id, type, children tree)
+ ├── .individual.feature (persona Gherkin)
+ └── ..feature (identity, background, duty, etc.)
+ """
+ And individual.json defines the tree structure:
+ """
+ {
+ "id": "nuwa",
+ "type": "individual",
+ "alias": ["nvwa"],
+ "children": {
+ "identity": {
+ "type": "identity",
+ "children": {
+ "background": { "type": "background" }
+ }
+ }
+ }
+ }
+ """
+
+ Scenario: Prototype updates
+ Given you re-register a prototype with the same id
+ Then the source is overwritten — latest registration wins
+ And the next activation will load the updated prototype
+
+Feature: Common Workflows
+ Typical sequences of operations for resource management.
+
+ Scenario: Publish a prototype to registry
+ Given you have a prototype directory ready
+ When you want to make it available via registry
+ Then the sequence is:
+ """
+ 1. use("!resource.add", { path: "/path/to/roles/nuwa" })
+ 2. use("!resource.push", { locator: "nuwa" })
+ """
+ And the prototype is now pullable by anyone with registry access
+
+ Scenario: Update and re-push a prototype
+ Given the prototype content has changed
+ When you re-add and push with the same tag
+ Then the registry updates the tag to point to the new digest
+ """
+ 1. use("!resource.add", { path: "/path/to/roles/nuwa" })
+ 2. use("!resource.push", { locator: "nuwa" })
+ """
+ And consumers pulling the same tag get the updated content
+
+ Scenario: Test loading a skill
+ Given you want to verify a skill is accessible
+ When you call skill with the locator
+ Then the full SKILL.md content should be returned
+ And example:
+ """
+ skill("skill-creator")
+ """
diff --git a/prototypes/skills/resource-management/resource.json b/skills/resource-management/resource.json
similarity index 93%
rename from prototypes/skills/resource-management/resource.json
rename to skills/resource-management/resource.json
index 002ce35..971b994 100644
--- a/prototypes/skills/resource-management/resource.json
+++ b/skills/resource-management/resource.json
@@ -1,7 +1,6 @@
{
"name": "resource-management",
"type": "skill",
- "tag": "0.2.0",
"author": "deepractice",
"description": "Manage ResourceX resources, register prototypes, and load skills. Use when you need to add, search, distribute, or inspect resources in RoleX."
}
diff --git a/skills/skill-creator/SKILL.md b/skills/skill-creator/SKILL.md
new file mode 100644
index 0000000..91bff9e
--- /dev/null
+++ b/skills/skill-creator/SKILL.md
@@ -0,0 +1,132 @@
+---
+name: skill-creator
+description: Guide for creating RoleX skills. Use when creating a new skill or updating an existing skill that extends a role's capabilities. Skills are SKILL.md files referenced by a procedure — the procedure summary is loaded at activate time, the full SKILL.md is loaded on demand via skill(locator).
+---
+
+Feature: RoleX Skill Creator
+ Create skills that follow the RoleX capability system.
+ A skill is a SKILL.md file referenced by a procedure entry via ResourceX locator.
+ Progressive disclosure: procedure summary at activate → full SKILL.md via skill(locator) → execution via use.
+
+ Scenario: What is a RoleX skill
+ Given a role needs operational capabilities beyond its identity
+ Then a skill is a SKILL.md file containing detailed instructions
+ And a procedure is a Gherkin summary that references the skill via locator
+ And the procedure is loaded at activate time — the role knows what skills exist
+ And the full SKILL.md is loaded on demand via the skill tool
+
+ Scenario: Skill directory structure
+ Given a skill lives under the skills/ directory
+ Then the structure is:
+ """
+ skills//
+ ├── SKILL.md (required — detailed instructions in Gherkin)
+ ├── resource.json (required — name, type: "skill", tag)
+ └── references/ (optional — loaded on demand)
+ └── .md
+ """
+ And SKILL.md has YAML frontmatter with name and description
+ And the body uses Gherkin Feature/Scenario format
+ And references contain domain-specific details loaded only when needed
+
+ Scenario: The procedure-skill contract
+ Given a procedure is trained to a role
+ Then the Feature description contains the ResourceX locator for the skill
+ And the Feature body summarizes what the skill can do
+ And example procedure:
+ """
+ Feature: Role Management
+ https://github.com/Deepractice/DeepracticeX/tree/main/skills/role-management
+
+ Scenario: What this skill does
+ Given I need to manage role lifecycle
+ Then I can born, teach, train, retire, and die roles
+ """
+ And the description line is the locator — the skill tool resolves it via ResourceX
+
+Feature: Skill Creation Process
+ How to create a new RoleX skill step by step.
+
+ Scenario: Step 1 — Understand the scope
+ Given you are creating a skill for a specific domain
+ When you analyze what operations the skill should cover
+ Then identify the concrete commands, parameters, and workflows
+ And determine what the role needs to know vs what it already knows
+ And only include information the role cannot infer on its own
+
+ Scenario: Step 2 — Write SKILL.md
+ Given you understand the scope
+ When you write the SKILL.md
+ Then start with YAML frontmatter (name + description)
+ And write the body as Gherkin Features and Scenarios
+ And each Feature covers a logical group of operations
+ And each Scenario describes a specific workflow or decision point
+ And use Given/When/Then for step-by-step procedures
+ And keep SKILL.md under 500 lines — split into references if needed
+
+ Scenario: Step 3 — Create resource.json
+ Given SKILL.md is written
+ When you create the resource manifest
+ Then include name, type "skill", and tag version
+ And example:
+ """
+ {
+ "name": "my-skill",
+ "type": "skill",
+ "tag": "0.1.0"
+ }
+ """
+
+ Scenario: Step 4 — Train the procedure
+ Given the skill directory is ready
+ When you train the procedure to a role
+ Then use the train operation with Gherkin source
+ And the Feature description MUST be the ResourceX locator for the skill
+ And the Feature body summarizes capabilities for activate-time awareness
+ And example:
+ """
+ use("!individual.train", {
+ individual: "sean",
+ id: "my-skill",
+ content: "Feature: My Skill\n https://github.com/org/repo/tree/main/skills/my-skill\n\n Scenario: When to use\n Given I need to do X\n Then load this skill"
+ })
+ """
+
+ Scenario: Step 5 — Test the skill
+ Given the procedure is trained
+ When you activate the role
+ Then the procedure summary should appear in the activation output
+ And calling skill with the locator should load the full SKILL.md
+ And the loaded content should be actionable and complete
+
+Feature: SKILL.md Writing Guidelines
+ Rules for writing effective skill content in Gherkin.
+
+ Scenario: Context window is a shared resource
+ Given the SKILL.md is loaded into the AI's context window
+ Then keep content concise — only include what the role cannot infer
+ And prefer concrete examples over verbose explanations
+ And use Gherkin structure to organize — not to pad length
+
+ Scenario: Gherkin as instruction format
+ Given RoleX uses Gherkin as its universal language
+ When writing SKILL.md body content
+ Then use Feature for logical groups of related operations
+ And use Scenario for specific workflows or procedures
+ And use Given for preconditions and context
+ And use When for actions and triggers
+ And use Then for expected outcomes and next steps
+ And use doc strings (triple quotes) for command examples and templates
+
+ Scenario: Progressive disclosure within a skill
+ Given a skill may cover many operations
+ When some details are only needed in specific situations
+ Then keep core workflows in SKILL.md
+ And move detailed reference material to references/ files
+ And reference them from SKILL.md with clear "when to read" guidance
+
+ Scenario: Frontmatter requirements
+ Given the frontmatter is the triggering mechanism
+ Then name must match the procedure name
+ And description must explain what the skill does AND when to use it
+ And do not include other fields in frontmatter
diff --git a/prototypes/skills/skill-creator/resource.json b/skills/skill-creator/resource.json
similarity index 93%
rename from prototypes/skills/skill-creator/resource.json
rename to skills/skill-creator/resource.json
index 2396ea4..d3ec514 100644
--- a/prototypes/skills/skill-creator/resource.json
+++ b/skills/skill-creator/resource.json
@@ -1,7 +1,6 @@
{
"name": "skill-creator",
"type": "skill",
- "tag": "0.1.0",
"description": "Guide for creating RoleX skills — directory structure, SKILL.md format, procedure contract",
"author": "deepractice",
"keywords": ["rolex", "skill", "creator", "guide"]
diff --git a/skills/version-migration/SKILL.md b/skills/version-migration/SKILL.md
new file mode 100644
index 0000000..5bd564c
--- /dev/null
+++ b/skills/version-migration/SKILL.md
@@ -0,0 +1,242 @@
+---
+name: version-migration
+description: Migrate legacy RoleX data (pre-1.0) to RoleX 1.0. Use when a user has old data in ~/.rolex and needs to migrate individuals, organizations, and positions to the new version.
+---
+
+Feature: Version Migration Overview
+ Migrate pre-1.0 RoleX data to the current version.
+ The old format stores data as Gherkin feature files in ~/.rolex/.
+ The new format uses a structured graph via RoleX runtime commands.
+
+ Scenario: When to use this skill
+ Given a user mentions migrating from an old RoleX version
+ Or a user has data in ~/.rolex that needs to be brought forward
+ When they want to preserve their roles, organizations, and knowledge
+ Then load this skill and follow the migration process
+
+ Scenario: Migration is non-destructive
+ Given the old data lives in ~/.rolex
+ When migration runs
+ Then old files are only read, never modified or deleted
+ And new entities are created in the current RoleX runtime
+ And the user can verify before cleaning up old data
+
+Feature: Legacy Data Format (pre-1.0)
+ Understanding the old directory structure and file conventions.
+
+ Scenario: Root directory structure
+ Given the legacy data lives at ~/.rolex/
+ Then the structure is:
+ """
+ ~/.rolex/
+ ├── rolex.json # Root manifest
+ ├── .seed-version # Version string (e.g. "0.11.0")
+ └── roles/
+ ├── /
+ │ ├── identity/
+ │ │ ├── persona.identity.feature # Who this role is
+ │ │ └── *.knowledge.identity.feature # Knowledge files
+ │ └── goals/ # Goal files (if any)
+ └── /
+ └── ...
+ """
+
+ Scenario: rolex.json manifest
+ Given rolex.json is the root manifest
+ Then it contains:
+ """
+ {
+ "roles": ["nuwa", "waiter"], // List of role names
+ "organizations": {}, // Organization definitions
+ "assignments": {} // Role-to-org assignments
+ }
+ """
+ And roles array lists all individuals to migrate
+ And organizations object may contain org definitions with charters
+ And assignments object may contain role-to-position mappings
+
+ Scenario: Persona files
+ Given a file named persona.identity.feature exists per role
+ Then it contains a Gherkin Feature defining who the role is
+ And the Feature title is the role's name
+ And Scenarios describe personality, thinking style, and behavior
+ And this maps to the individual's identity content in new format
+
+ Scenario: Knowledge files
+ Given files named *.knowledge.identity.feature exist per role
+ Then each file contains a Gherkin Feature with domain knowledge
+ And the filename prefix (before .knowledge) is the knowledge topic
+ And these map to principles (teach) in the new format
+ And the knowledge id is derived from the filename prefix
+
+ Scenario: Goal files
+ Given a goals/ directory may exist per role
+ Then it may contain Gherkin files representing active goals
+ And goals can be recreated via want in the new format
+ And goals are optional — many roles have empty goals directories
+
+Feature: Migration Process
+ Step-by-step process to migrate legacy data.
+
+ Scenario: Step 1 — Scan and analyze
+ Given the user wants to migrate
+ When you begin the migration process
+ Then read ~/.rolex/rolex.json to get the manifest
+ And list all roles from the roles array
+ And for each role, read all files under identity/ and goals/
+ And present a summary to the user:
+ """
+ Found X roles to migrate:
+ - : persona + N knowledge files + M goals
+ - : persona + N knowledge files + M goals
+
+ Organizations: X
+ Assignments: X
+ """
+ And ask the user to confirm before proceeding
+
+ Scenario: Step 2 — Migrate individuals
+ Given the user confirmed migration
+ When migrating each role from the manifest
+ Then for each role:
+ """
+ 1. Read persona.identity.feature → use as born content
+ 2. Call: use("!individual.born", {
+ id: "",
+ content: ""
+ })
+ """
+ And the role-name from the directory becomes the individual id
+
+ Scenario: Step 3 — Migrate knowledge to principles
+ Given an individual has been born
+ When migrating their knowledge files
+ Then for each *.knowledge.identity.feature file:
+ """
+ 1. Extract topic from filename: "role-creation.knowledge.identity.feature" → "role-creation"
+ 2. Read the file content (Gherkin Feature)
+ 3. Call: use("!individual.teach", {
+ individual: "",
+ content: "",
+ id: ""
+ })
+ """
+ And each knowledge file becomes one principle
+
+ Scenario: Step 4 — Migrate organizations
+ Given rolex.json contains organization definitions
+ When the organizations object is not empty
+ Then for each organization:
+ """
+ 1. Call: use("!org.found", {
+ id: "",
+ content: ""
+ })
+ 2. If charter exists:
+ Call: use("!org.charter", {
+ org: "",
+ content: ""
+ })
+ """
+
+ Scenario: Step 5 — Migrate assignments (membership + appointments)
+ Given rolex.json contains assignment mappings
+ When the assignments object is not empty
+ Then for each assignment:
+ """
+ 1. Call: use("!org.hire", {
+ org: "",
+ individual: ""
+ })
+ 2. If position exists:
+ Call: use("!position.appoint", {
+ position: "",
+ individual: ""
+ })
+ """
+
+ Scenario: Step 6 — Migrate goals (optional)
+ Given a role has files in goals/ directory
+ When the user wants to preserve active goals
+ Then for each goal file:
+ """
+ 1. Read the goal Gherkin content
+ 2. Activate the individual: activate({ roleId: "" })
+ 3. Call: want({ goal: "", id: "" })
+ """
+ And goal ids are derived from the goal filename
+
+ Scenario: Step 7 — Verify migration
+ Given all entities have been migrated
+ When verification is needed
+ Then activate each migrated individual and check:
+ """
+ 1. activate({ roleId: "" })
+ 2. Verify identity content matches the old persona
+ 3. Verify principles match the old knowledge files
+ 4. Verify organization memberships if applicable
+ """
+ And present the verification results to the user
+
+Feature: Entity Mapping Reference
+ How old format entities map to new format commands.
+
+ Scenario: Individual mapping
+ Given an old role directory exists
+ Then the mapping is:
+ """
+ Old: roles//identity/persona.identity.feature
+ New: use("!individual.born", { id: "", content: "" })
+ """
+
+ Scenario: Knowledge mapping
+ Given old knowledge files exist
+ Then the mapping is:
+ """
+ Old: roles//identity/.knowledge.identity.feature
+ New: use("!individual.teach", { individual: "", content: "", id: "" })
+ """
+
+ Scenario: Organization mapping
+ Given old organization definitions exist in rolex.json
+ Then the mapping is:
+ """
+ Old: rolex.json → organizations.
+ New: use("!org.found", { id: "", content: "" })
+ use("!org.charter", { org: "", content: "" })
+ """
+
+ Scenario: Assignment mapping
+ Given old assignments exist in rolex.json
+ Then the mapping is:
+ """
+ Old: rolex.json → assignments.