From 83239ee3e73f308fd7cc3f5f5fd18a9a0dacdad6 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 23 Feb 2026 21:26:20 +0800 Subject: [PATCH 01/54] developing --- apps/cli/src/index.ts | 8 +++---- apps/mcp-server/src/index.ts | 10 ++++----- packages/rolexjs/src/descriptions/index.ts | 6 +++--- .../rolexjs/src/descriptions/master.feature | 18 ++++++++++------ .../rolexjs/src/descriptions/train.feature | 21 +++++++++---------- .../src/descriptions/world-cognition.feature | 10 +++++---- packages/rolexjs/src/rolex.ts | 19 ++++++++--------- 7 files changed, 49 insertions(+), 43 deletions(-) diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 580287a..3319178 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -314,17 +314,17 @@ const realize = defineCommand({ 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)" }, + experience: { type: "string" as const, description: "Experience id to consume (optional)" }, }, run({ args }) { const result = rolex.role.master( - args.experience, args.individual, - resolveContent(args, "procedure"), - args.id + requireContent(args, "procedure"), + args.id, + args.experience ); output(result, result.state.name); }, diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index e6106e4..29b73b0 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -232,15 +232,15 @@ 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); + if (ids?.length) state.requireExperienceIds(ids); const roleId = state.requireRoleId(); - const result = rolex.role.master(ids[0], roleId, procedure, id); - state.consumeExperiences(ids); + const result = rolex.role.master(roleId, procedure, id, ids?.[0]); + if (ids?.length) state.consumeExperiences(ids); return fmt("master", id, result); }, }); diff --git a/packages/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts index 20257d0..af50c05 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -19,7 +19,7 @@ export const processes: Record = { "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", + "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 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", @@ -28,12 +28,12 @@ export const processes: Record = { "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", + "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", "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", + "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: External injection\n Given an external agent needs to equip a role with knowledge or skills\n Then teach(individual, principle, id) directly injects a principle\n And train(individual, procedure, id) directly injects a procedure\n And the difference from realize/master is perspective — external vs self-initiated\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", diff --git a/packages/rolexjs/src/descriptions/master.feature b/packages/rolexjs/src/descriptions/master.feature index 6d6d0cb..bdb951d 100644 --- a/packages/rolexjs/src/descriptions/master.feature +++ b/packages/rolexjs/src/descriptions/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/rolexjs/src/descriptions/train.feature b/packages/rolexjs/src/descriptions/train.feature index 22436fa..c06a517 100644 --- a/packages/rolexjs/src/descriptions/train.feature +++ b/packages/rolexjs/src/descriptions/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/world-cognition.feature b/packages/rolexjs/src/descriptions/world-cognition.feature index 73dd2df..e984869 100644 --- a/packages/rolexjs/src/descriptions/world-cognition.feature +++ b/packages/rolexjs/src/descriptions/world-cognition.feature @@ -1,19 +1,21 @@ 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 + Given an external agent needs to equip a role with knowledge or skills + Then teach(individual, principle, id) directly injects a principle + And train(individual, procedure, id) directly injects a procedure + And the difference from realize/master is perspective — external vs self-initiated And teach is the external counterpart of realize And train is the external counterpart of master diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 34bfc40..93bf637 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -281,17 +281,16 @@ class RoleNamespace { return ok(this.rt, prin, "realize"); } - /** Master: consume experience, create procedure under individual. */ - master(experience: string, individual: string, procedure?: string, id?: string): RolexResult { + /** Master: create procedure under individual, optionally consuming experience. */ + master(individual: string, procedure: string, id?: string, experience?: 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); + const parent = this.resolve(individual); + if (id) { + const existing = findInState(this.rt.project(parent), id); + if (existing) this.rt.remove(existing); + } + const proc = this.rt.create(parent, C.procedure, procedure, id); + if (experience) this.rt.remove(this.resolve(experience)); return ok(this.rt, proc, "master"); } From 6962ee5bc49f4cad2cebf0cb1584037e13b3d3cf Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 23 Feb 2026 21:28:22 +0800 Subject: [PATCH 02/54] developing --- ...apability-system.knowledge.pattern.feature | 35 -------- .../execution-cycle.knowledge.pattern.feature | 46 ---------- .../gherkin-basics.knowledge.pattern.feature | 34 -------- .../growth-cycle.knowledge.pattern.feature | 36 -------- .../rolexjs/src/base/guider/persona.feature | 18 ---- .../rolex-overview.knowledge.pattern.feature | 32 ------- packages/rolexjs/src/base/index.ts | 84 ------------------- ...org-management.knowledge.procedure.feature | 16 ---- .../rolexjs/src/base/nuwa/persona.feature | 15 ---- ...ole-management.knowledge.procedure.feature | 16 ---- 10 files changed, 332 deletions(-) delete mode 100644 packages/rolexjs/src/base/guider/capability-system.knowledge.pattern.feature delete mode 100644 packages/rolexjs/src/base/guider/execution-cycle.knowledge.pattern.feature delete mode 100644 packages/rolexjs/src/base/guider/gherkin-basics.knowledge.pattern.feature delete mode 100644 packages/rolexjs/src/base/guider/growth-cycle.knowledge.pattern.feature delete mode 100644 packages/rolexjs/src/base/guider/persona.feature delete mode 100644 packages/rolexjs/src/base/guider/rolex-overview.knowledge.pattern.feature delete mode 100644 packages/rolexjs/src/base/index.ts delete mode 100644 packages/rolexjs/src/base/nuwa/org-management.knowledge.procedure.feature delete mode 100644 packages/rolexjs/src/base/nuwa/persona.feature delete mode 100644 packages/rolexjs/src/base/nuwa/role-management.knowledge.procedure.feature 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 From 3e5b3454ee740e7ad6e6248399122a925a2a28be Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 13:15:59 +0800 Subject: [PATCH 03/54] developing --- apps/mcp-server/src/index.ts | 104 ++--- apps/mcp-server/src/state.ts | 179 +------ apps/mcp-server/tests/mcp.test.ts | 410 ++++++---------- packages/rolexjs/src/context.ts | 165 +++++++ .../rolexjs/src/descriptions/finish.feature | 7 +- packages/rolexjs/src/descriptions/index.ts | 4 +- .../src/descriptions/world-gherkin.feature | 11 + packages/rolexjs/src/index.ts | 2 + packages/rolexjs/src/rolex.ts | 132 ++++-- packages/rolexjs/tests/context.test.ts | 132 ++++++ packages/rolexjs/tests/rolex.test.ts | 436 +++++++++--------- 11 files changed, 822 insertions(+), 760 deletions(-) create mode 100644 packages/rolexjs/src/context.ts create mode 100644 packages/rolexjs/tests/context.test.ts diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 29b73b0..e4b3727 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -1,23 +1,9 @@ /** * @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 + * Thin wrapper around the Rolex API. All business logic (state tracking, + * cognitive hints, encounter/experience registries) lives in RoleContext (rolexjs). + * MCP only translates protocol calls to API calls. */ import { localPlatform } from "@rolexjs/local-platform"; @@ -37,18 +23,18 @@ const state = new McpState(rolex); 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 }) { +function fmt(process: string, label: string, result: { state: any; process: string; hint?: string }) { return render({ process, name: label, result, - cognitiveHint: state.cognitiveHint(process), + cognitiveHint: result.hint ?? null, }); } @@ -62,13 +48,10 @@ server.addTool({ }), execute: async ({ roleId }) => { if (!state.findIndividual(roleId)) { - // Auto-born if not found rolex.individual.born(undefined, roleId); } - state.reset(); - state.activeRoleId = roleId; const result = await rolex.role.activate(roleId); - state.cacheFromActivation(result.state); + state.ctx = result.ctx!; return fmt("activate", roleId, result); }, }); @@ -80,12 +63,9 @@ 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); + const ctx = state.requireCtx(); + const goalId = id ?? ctx.requireGoalId(); + const result = rolex.role.focus(goalId, ctx); return fmt("focus", id ?? "current goal", result); }, }); @@ -100,10 +80,8 @@ 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; + const ctx = state.requireCtx(); + const result = rolex.role.want(ctx.roleId, goal, id, undefined, ctx); return fmt("want", id, result); }, }); @@ -116,9 +94,9 @@ server.addTool({ plan: z.string().describe("Gherkin Feature source describing the plan"), }), execute: async ({ id, plan }) => { - const goalId = state.requireGoalId(); - const result = rolex.role.plan(goalId, plan, id); - state.focusedPlanId = id; + const ctx = state.requireCtx(); + const goalId = ctx.requireGoalId(); + const result = rolex.role.plan(goalId, plan, id, ctx); return fmt("plan", id, result); }, }); @@ -131,8 +109,9 @@ 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); + const ctx = state.requireCtx(); + const planId = ctx.requirePlanId(); + const result = rolex.role.todo(planId, task, id, undefined, ctx); return fmt("todo", id, result); }, }); @@ -145,10 +124,8 @@ 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); + const ctx = state.requireCtx(); + const result = rolex.role.finish(id, ctx.roleId, encounter, ctx); return fmt("finish", id, result); }, }); @@ -161,12 +138,9 @@ 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; + const ctx = state.requireCtx(); + const planId = id ?? ctx.requirePlanId(); + const result = rolex.role.complete(planId, ctx.roleId, encounter, ctx); return fmt("complete", planId, result); }, }); @@ -179,12 +153,9 @@ 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; + const ctx = state.requireCtx(); + const planId = id ?? ctx.requirePlanId(); + const result = rolex.role.abandon(planId, ctx.roleId, encounter, ctx); return fmt("abandon", planId, result); }, }); @@ -202,11 +173,8 @@ 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); + const ctx = state.requireCtx(); + const result = rolex.role.reflect(ids[0], ctx.roleId, experience, id, ctx); return fmt("reflect", id, result); }, }); @@ -220,10 +188,8 @@ 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); + const ctx = state.requireCtx(); + const result = rolex.role.realize(ids[0], ctx.roleId, principle, id, ctx); return fmt("realize", id, result); }, }); @@ -237,10 +203,8 @@ server.addTool({ procedure: z.string().describe("Gherkin Feature source for the procedure"), }), execute: async ({ ids, id, procedure }) => { - if (ids?.length) state.requireExperienceIds(ids); - const roleId = state.requireRoleId(); - const result = rolex.role.master(roleId, procedure, id, ids?.[0]); - if (ids?.length) state.consumeExperiences(ids); + const ctx = state.requireCtx(); + const result = rolex.role.master(ctx.roleId, procedure, id, ids?.[0], ctx); return fmt("master", id, result); }, }); @@ -256,8 +220,8 @@ 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); + const ctx = state.requireCtx(); + const result = await rolex.role.forget(id, ctx.roleId); return fmt("forget", id, result); }, }); diff --git a/apps/mcp-server/src/state.ts b/apps/mcp-server/src/state.ts index a0ce6c5..922f3aa 100644 --- a/apps/mcp-server/src/state.ts +++ b/apps/mcp-server/src/state.ts @@ -1,184 +1,23 @@ /** - * 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. + * All business logic (state tracking, cognitive hints, encounter/experience + * registries) now lives in RoleContext (rolexjs). McpState only holds + * the ctx reference and provides MCP-specific helpers. */ -import type { Rolex, State } from "rolexjs"; +import type { Rolex, RoleContext } 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(); + ctx: RoleContext | null = null; 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; + requireCtx(): RoleContext { + if (!this.ctx) throw new Error("No active role. Call activate first."); + return this.ctx; } - 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."; - - default: - return null; - } - } } diff --git a/apps/mcp-server/tests/mcp.test.ts b/apps/mcp-server/tests/mcp.test.ts index 1796a07..0c5646b 100644 --- a/apps/mcp-server/tests/mcp.test.ts +++ b/apps/mcp-server/tests/mcp.test.ts @@ -1,8 +1,9 @@ /** * 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 + render) on top of stateless Rolex. + * Business logic (RoleContext) is tested in rolexjs/tests/context.test.ts. + * This file tests MCP-specific concerns: state holder, render, and integration. */ import { beforeEach, describe, expect, it } from "bun:test"; import { localPlatform } from "@rolexjs/local-platform"; @@ -23,145 +24,31 @@ beforeEach(() => { // ================================================================ 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 true when individual exists", () => { + rolex.individual.born("Feature: Sean\n A backend architect", "sean"); + expect(state.findIndividual("sean")).toBe(true); }); - 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(); + it("returns false when not found", () => { + expect(state.findIndividual("nobody")).toBe(false); }); }); // ================================================================ -// State: registry +// State: requireCtx // ================================================================ -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); +describe("requireCtx", () => { + it("throws without active role", () => { + expect(() => state.requireCtx()).toThrow("No active role"); }); - 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", () => { - 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"); - }); -}); - -// ================================================================ -// 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"); + it("returns ctx after activation", async () => { + rolex.individual.born("Feature: Sean", "sean"); + const result = await rolex.role.activate("sean"); + state.ctx = result.ctx!; + expect(state.requireCtx()).toBe(result.ctx); + expect(state.requireCtx().roleId).toBe("sean"); }); }); @@ -171,7 +58,7 @@ describe("cacheFromActivation", () => { describe("render", () => { it("includes status + hint + projection", () => { - const result = rolex.born("Feature: Sean", "sean"); + const result = rolex.individual.born("Feature: Sean", "sean"); const output = render({ process: "born", name: "Sean", @@ -187,120 +74,116 @@ describe("render", () => { expect(output).toContain("## [knowledge]"); }); + it("includes cognitive hint when provided", () => { + const result = rolex.individual.born("Feature: Sean", "sean"); + const output = render({ + process: "born", + name: "Sean", + result, + cognitiveHint: "I have no goal yet. Declare one with want.", + }); + expect(output).toContain("I →"); + expect(output).toContain("I have no goal yet"); + }); + 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); + rolex.individual.born("Feature: Sean", "sean"); + rolex.org.found("Feature: Deepractice", "dp"); + rolex.org.hire("dp", "sean"); - const activated = rolex.activate(sean.state); + // Project individual — should have belong link + const seanState = rolex.find("sean")!; const output = render({ process: "activate", name: "Sean", - result: activated, + result: { state: seanState as any, process: "activate" }, }); - // Individual should have belong → organization via bidirectional link expect(output).toContain("belong"); expect(output).toContain("Deepractice"); }); 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); + rolex.individual.born("Feature: Sean", "sean"); + rolex.org.found("Feature: Deepractice", "dp"); + rolex.org.establish("dp", "Feature: Architect", "architect"); + rolex.org.hire("dp", "sean"); + rolex.org.appoint("architect", "sean"); - const activated = rolex.activate(sean.state); + const seanState = rolex.find("sean")!; const output = render({ process: "activate", name: "Sean", - result: activated, + result: { state: seanState as any, process: "activate" }, }); - // Individual should have serve → position via bidirectional link expect(output).toContain("serve"); expect(output).toContain("Architect"); }); }); // ================================================================ -// 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 namespace API", async () => { + // Born + activate + rolex.individual.born("Feature: Sean", "sean"); + const activated = await rolex.role.activate("sean"); + state.ctx = activated.ctx!; + const ctx = state.requireCtx(); + + // Want + const goal = rolex.role.want(ctx.roleId, "Feature: Build Auth", "build-auth", undefined, ctx); + expect(ctx.focusedGoalId).toBe("build-auth"); + expect(goal.hint).toBeDefined(); + + // Plan + const plan = rolex.role.plan("build-auth", "Feature: Auth Plan", "auth-plan", ctx); + expect(ctx.focusedPlanId).toBe("auth-plan"); + expect(plan.hint).toBeDefined(); + + // Todo + const task = rolex.role.todo("auth-plan", "Feature: Implement JWT", "impl-jwt", undefined, ctx); + expect(task.hint).toBeDefined(); + + // Finish with encounter + const finished = rolex.role.finish( + "impl-jwt", + ctx.roleId, + "Feature: Implemented JWT\n Scenario: Token pattern\n Given JWT needed\n Then tokens work", + ctx ); - 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" - ); - 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" + expect(ctx.encounterIds.has("impl-jwt-finished")).toBe(true); + + // Reflect: encounter → experience + const reflected = rolex.role.reflect( + "impl-jwt-finished", + ctx.roleId, + "Feature: Token rotation insight\n Scenario: Refresh matters\n Given tokens expire\n Then refresh tokens are key", + "token-insight", + ctx ); - 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(ctx.encounterIds.has("impl-jwt-finished")).toBe(false); + expect(ctx.experienceIds.has("token-insight")).toBe(true); + + // Realize: experience → principle + const realized = rolex.role.realize( + "token-insight", + ctx.roleId, + "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", + ctx ); - state.consumeExperiences(["impl-jwt"]); expect(realized.state.name).toBe("principle"); + expect(ctx.experienceIds.has("token-insight")).toBe(false); - // 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); + // Verify principle exists under individual + const seanState = rolex.find("sean")!; + const principle = (seanState as any).children?.find((c: any) => c.name === "principle"); + expect(principle).toBeDefined(); + expect(principle.information).toContain("Always use refresh tokens"); }); }); @@ -309,21 +192,21 @@ 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 ctx", async () => { + rolex.individual.born("Feature: Sean", "sean"); + const activated = await rolex.role.activate("sean"); + state.ctx = activated.ctx!; + const ctx = state.requireCtx(); - const goal1 = rolex.want(state.requireRole(), "Feature: Goal A", "goal-a"); - state.register("goal-a", goal1.state); - state.focusedGoal = goal1.state; + rolex.role.want(ctx.roleId, "Feature: Goal A", "goal-a", undefined, ctx); + expect(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; + rolex.role.want(ctx.roleId, "Feature: Goal B", "goal-b", undefined, ctx); + expect(ctx.focusedGoalId).toBe("goal-b"); // Switch back to goal A - state.focusedGoal = state.resolve("goal-a"); - expect(state.requireGoal()).toBe(goal1.state); + rolex.role.focus("goal-a", ctx); + expect(ctx.focusedGoalId).toBe("goal-a"); }); }); @@ -332,51 +215,48 @@ 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 () => { + rolex.individual.born("Feature: Sean", "sean"); + const activated = await rolex.role.activate("sean"); + state.ctx = activated.ctx!; + const ctx = state.requireCtx(); + + // Create goal + plan + tasks + rolex.role.want(ctx.roleId, "Feature: Auth", "auth", undefined, ctx); + rolex.role.plan("auth", "Feature: Plan", "plan1", ctx); + rolex.role.todo("plan1", "Feature: Login", "login", undefined, ctx); + rolex.role.todo("plan1", "Feature: Signup", "signup", undefined, ctx); + + // Finish both with encounters + rolex.role.finish( + "login", + ctx.roleId, + "Feature: Login done\n Scenario: OK\n Given login\n Then success", + ctx ); - 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" + rolex.role.finish( + "signup", + ctx.roleId, + "Feature: Signup done\n Scenario: OK\n Given signup\n Then success", + ctx ); - state.registerEncounter("signup", enc2.state); - // List encounters — should have both - expect(state.listEncounters()).toEqual(["login", "signup"]); + expect(ctx.encounterIds.has("login-finished")).toBe(true); + expect(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" + rolex.role.reflect( + "login-finished", + ctx.roleId, + "Feature: Login insight\n Scenario: OK\n Given practice\n Then understanding", + "login-insight", + ctx ); - 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(ctx.encounterIds.has("login-finished")).toBe(false); + expect(ctx.encounterIds.has("signup-finished")).toBe(true); + // Experience registered + expect(ctx.experienceIds.has("login-insight")).toBe(true); }); }); diff --git a/packages/rolexjs/src/context.ts b/packages/rolexjs/src/context.ts new file mode 100644 index 0000000..33fc1b9 --- /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 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; + 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 to knowledge. I should continue working."; + + case "master": + return "Procedure added to knowledge. I should continue working."; + + default: + return null; + } + } +} diff --git a/packages/rolexjs/src/descriptions/finish.feature b/packages/rolexjs/src/descriptions/finish.feature index dcd976d..597b751 100644 --- a/packages/rolexjs/src/descriptions/finish.feature +++ b/packages/rolexjs/src/descriptions/finish.feature @@ -7,13 +7,18 @@ Feature: finish — complete a task When finish is called on the task Then the task is marked done 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 removed but no encounter is created + And routine completions leave no trace — keeping the state clean + 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/index.ts b/packages/rolexjs/src/descriptions/index.ts index af50c05..88c48d0 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -13,7 +13,7 @@ export const processes: Record = { "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", + "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\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 removed but no encounter is created\n And routine completions leave no trace — keeping the state clean\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", @@ -37,7 +37,7 @@ export const world: Record = { "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", + "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\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 never invent keywords like Because, Since, or So\n\n Scenario: Expressing causality without Because\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\n And example — instead of \"Then use RoleX tools because native tools break the loop\"\n And write \"Then use RoleX tools\" followed by \"And native tools do not feed the growth loop\"", "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", diff --git a/packages/rolexjs/src/descriptions/world-gherkin.feature b/packages/rolexjs/src/descriptions/world-gherkin.feature index 1d906e1..7048200 100644 --- a/packages/rolexjs/src/descriptions/world-gherkin.feature +++ b/packages/rolexjs/src/descriptions/world-gherkin.feature @@ -14,3 +14,14 @@ Feature: Gherkin — the universal language 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 + + 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 never invent keywords like Because, Since, or So + + Scenario: Expressing causality without Because + 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 + And example — instead of "Then use RoleX tools because native tools break the loop" + And write "Then use RoleX tools" followed by "And native tools do not feed the growth loop" diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index 6ec51b7..70912b5 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -24,3 +24,5 @@ export { describe, detail, hint, renderState, world } from "./render.js"; export type { RolexResult } from "./rolex.js"; // API export { createRoleX, Rolex } from "./rolex.js"; +// Context +export { RoleContext } from "./context.js"; diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 93bf637..429fd1a 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -27,12 +27,17 @@ import { type Structure, } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; +import { RoleContext } from "./context.js"; export interface RolexResult { /** Projection of the primary affected node. */ state: State; /** Which process was executed (for render). */ process: string; + /** Cognitive hint — populated when RoleContext is used. */ + hint?: string; + /** Role context — returned by activate, pass to subsequent operations. */ + ctx?: RoleContext; } /** Resolve an id to a Structure node, throws if not found. */ @@ -119,6 +124,9 @@ class IndividualNamespace { born(individual?: string, id?: string, alias?: readonly string[]): RolexResult { validateGherkin(individual); const node = this.rt.create(this.society, C.individual, individual, id, alias); + // Scaffolding: every individual has identity + knowledge + this.rt.create(node, C.identity); + this.rt.create(node, C.knowledge); return ok(this.rt, node, "born"); } @@ -135,7 +143,10 @@ class IndividualNamespace { /** 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); + const individual = this.rt.create(this.society, C.individual, past.information, past.id); + // Scaffolding: restore identity + knowledge + this.rt.create(individual, C.identity); + this.rt.create(individual, C.knowledge); this.rt.remove(past); return ok(this.rt, individual, "rehire"); } @@ -182,7 +193,7 @@ class RoleNamespace { // ---- Activation ---- - /** Activate: merge prototype (if any) with instance state. */ + /** Activate: merge prototype (if any) with instance state. Returns ctx when created. */ async activate(individual: string): Promise { const node = this.resolve(individual); const instanceState = this.rt.project(node); @@ -190,72 +201,119 @@ class RoleNamespace { ? await this.prototype?.resolve(instanceState.id) : undefined; const state = protoState ? mergeState(protoState, instanceState) : instanceState; - return { state, process: "activate" }; + const ctx = new RoleContext(individual); + ctx.rehydrate(state); + return { state, process: "activate", hint: ctx.cognitiveHint("activate") ?? undefined, ctx }; } /** Focus: project a goal's state (view / switch context). */ - focus(goal: string): RolexResult { - return ok(this.rt, this.resolve(goal), "focus"); + focus(goal: string, ctx?: RoleContext): RolexResult { + if (ctx) { + ctx.focusedGoalId = goal; + ctx.focusedPlanId = null; + } + const result = ok(this.rt, this.resolve(goal), "focus"); + if (ctx) result.hint = ctx.cognitiveHint("focus") ?? undefined; + return result; } // ---- Execution ---- /** Declare a goal under an individual. */ - want(individual: string, goal?: string, id?: string, alias?: readonly string[]): RolexResult { + want(individual: string, goal?: string, id?: string, alias?: readonly string[], ctx?: RoleContext): RolexResult { validateGherkin(goal); const node = this.rt.create(this.resolve(individual), C.goal, goal, id, alias); - return ok(this.rt, node, "want"); + const result = ok(this.rt, node, "want"); + if (ctx) { + if (id) ctx.focusedGoalId = id; + ctx.focusedPlanId = null; + result.hint = ctx.cognitiveHint("want") ?? undefined; + } + return result; } /** Create a plan for a goal. */ - plan(goal: string, plan?: string, id?: string): RolexResult { + plan(goal: string, plan?: string, id?: string, ctx?: RoleContext): RolexResult { validateGherkin(plan); const node = this.rt.create(this.resolve(goal), C.plan, plan, id); - return ok(this.rt, node, "plan"); + const result = ok(this.rt, node, "plan"); + if (ctx) { + if (id) ctx.focusedPlanId = id; + result.hint = ctx.cognitiveHint("plan") ?? undefined; + } + return result; } /** Add a task to a plan. */ - todo(plan: string, task?: string, id?: string, alias?: readonly string[]): RolexResult { + todo(plan: string, task?: string, id?: string, alias?: readonly string[], ctx?: RoleContext): RolexResult { validateGherkin(task); const node = this.rt.create(this.resolve(plan), C.task, task, id, alias); - return ok(this.rt, node, "todo"); + const result = ok(this.rt, node, "todo"); + if (ctx) result.hint = ctx.cognitiveHint("todo") ?? undefined; + return result; } - /** Finish a task: consume task, create encounter under individual. */ - finish(task: string, individual: string, encounter?: string): RolexResult { + /** Finish a task: consume task, optionally create encounter under individual. */ + finish(task: string, individual: string, encounter?: string, ctx?: RoleContext): 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); + let enc: Structure | undefined; + if (encounter) { + const encId = taskNode.id ? `${taskNode.id}-finished` : undefined; + enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId); + } this.rt.remove(taskNode); - return ok(this.rt, enc, "finish"); + const result: RolexResult = enc + ? ok(this.rt, enc, "finish") + : { state: this.rt.project(this.resolve(individual)), process: "finish" }; + if (ctx) { + if (enc) { + const encId = result.state.id ?? task; + ctx.addEncounter(encId); + } + result.hint = ctx.cognitiveHint("finish") ?? undefined; + } + return result; } /** Complete a plan: consume plan, create encounter under individual. */ - complete(plan: string, individual: string, encounter?: string): RolexResult { + complete(plan: string, individual: string, encounter?: string, ctx?: RoleContext): 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"); + const result = ok(this.rt, enc, "complete"); + if (ctx) { + ctx.addEncounter(result.state.id ?? plan); + if (ctx.focusedPlanId === plan) ctx.focusedPlanId = null; + result.hint = ctx.cognitiveHint("complete") ?? undefined; + } + return result; } /** Abandon a plan: consume plan, create encounter under individual. */ - abandon(plan: string, individual: string, encounter?: string): RolexResult { + abandon(plan: string, individual: string, encounter?: string, ctx?: RoleContext): 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"); + const result = ok(this.rt, enc, "abandon"); + if (ctx) { + ctx.addEncounter(result.state.id ?? plan); + if (ctx.focusedPlanId === plan) ctx.focusedPlanId = null; + result.hint = ctx.cognitiveHint("abandon") ?? undefined; + } + return result; } // ---- Cognition ---- /** Reflect: consume encounter, create experience under individual. */ - reflect(encounter: string, individual: string, experience?: string, id?: string): RolexResult { + reflect(encounter: string, individual: string, experience?: string, id?: string, ctx?: RoleContext): RolexResult { validateGherkin(experience); + if (ctx) ctx.requireEncounterIds([encounter]); const encNode = this.resolve(encounter); const exp = this.rt.create( this.resolve(individual), @@ -264,12 +322,19 @@ class RoleNamespace { id ); this.rt.remove(encNode); - return ok(this.rt, exp, "reflect"); + const result = ok(this.rt, exp, "reflect"); + if (ctx) { + ctx.consumeEncounters([encounter]); + if (id) ctx.addExperience(id); + result.hint = ctx.cognitiveHint("reflect") ?? undefined; + } + return result; } /** Realize: consume experience, create principle under individual. */ - realize(experience: string, individual: string, principle?: string, id?: string): RolexResult { + realize(experience: string, individual: string, principle?: string, id?: string, ctx?: RoleContext): RolexResult { validateGherkin(principle); + if (ctx) ctx.requireExperienceIds([experience]); const expNode = this.resolve(experience); const prin = this.rt.create( this.resolve(individual), @@ -278,20 +343,31 @@ class RoleNamespace { id ); this.rt.remove(expNode); - return ok(this.rt, prin, "realize"); + const result = ok(this.rt, prin, "realize"); + if (ctx) { + ctx.consumeExperiences([experience]); + result.hint = ctx.cognitiveHint("realize") ?? undefined; + } + return result; } /** Master: create procedure under individual, optionally consuming experience. */ - master(individual: string, procedure: string, id?: string, experience?: string): RolexResult { + master(individual: string, procedure: string, id?: string, experience?: string, ctx?: RoleContext): RolexResult { validateGherkin(procedure); + if (ctx && experience) ctx.requireExperienceIds([experience]); const parent = this.resolve(individual); if (id) { const existing = findInState(this.rt.project(parent), id); if (existing) this.rt.remove(existing); } const proc = this.rt.create(parent, C.procedure, procedure, id); - if (experience) this.rt.remove(this.resolve(experience)); - return ok(this.rt, proc, "master"); + if (experience) { + this.rt.remove(this.resolve(experience)); + if (ctx) ctx.consumeExperiences([experience]); + } + const result = ok(this.rt, proc, "master"); + if (ctx) result.hint = ctx.cognitiveHint("master") ?? undefined; + return result; } // ---- Knowledge management ---- @@ -457,7 +533,7 @@ function findInState(state: State, target: string): Structure | null { } function archive(rt: Runtime, past: Structure, node: Structure, process: string): RolexResult { - const archived = rt.create(past, C.past, node.information); + const archived = rt.create(past, C.past, node.information, node.id); rt.remove(node); return ok(rt, archived, process); } diff --git a/packages/rolexjs/tests/context.test.ts b/packages/rolexjs/tests/context.test.ts new file mode 100644 index 0000000..49138b1 --- /dev/null +++ b/packages/rolexjs/tests/context.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, test } from "bun:test"; +import { localPlatform } from "@rolexjs/local-platform"; +import { createRoleX, RoleContext } from "../src/index.js"; + +function setup() { + return createRoleX(localPlatform({ dataDir: null })); +} + +describe("RoleContext", () => { + test("activate returns ctx in result", async () => { + const rolex = setup(); + rolex.individual.born("Feature: Sean", "sean"); + const result = await rolex.role.activate("sean"); + expect(result.ctx).toBeInstanceOf(RoleContext); + expect(result.ctx!.roleId).toBe("sean"); + expect(result.hint).toBeDefined(); + }); + + test("want updates ctx.focusedGoalId", async () => { + const rolex = setup(); + rolex.individual.born("Feature: Sean", "sean"); + const { ctx } = await rolex.role.activate("sean"); + + const result = rolex.role.want("sean", "Feature: Build auth", "build-auth", undefined, ctx!); + expect(ctx!.focusedGoalId).toBe("build-auth"); + expect(ctx!.focusedPlanId).toBeNull(); + expect(result.hint).toBeDefined(); + }); + + test("plan updates ctx.focusedPlanId", async () => { + const rolex = setup(); + rolex.individual.born("Feature: Sean", "sean"); + const { ctx } = await rolex.role.activate("sean"); + + rolex.role.want("sean", "Feature: Auth", "auth-goal", undefined, ctx!); + const result = rolex.role.plan("auth-goal", "Feature: JWT strategy", "jwt-plan", ctx!); + expect(ctx!.focusedPlanId).toBe("jwt-plan"); + expect(result.hint).toBeDefined(); + }); + + test("finish with encounter registers in ctx", async () => { + const rolex = setup(); + rolex.individual.born("Feature: Sean", "sean"); + const { ctx } = await rolex.role.activate("sean"); + + rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); + rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); + rolex.role.todo("jwt", "Feature: Login", "login", undefined, ctx!); + + const result = rolex.role.finish( + "login", + "sean", + "Feature: Login done\n Scenario: OK\n Given login\n Then success", + ctx! + ); + expect(ctx!.encounterIds.has("login-finished")).toBe(true); + expect(result.hint).toBeDefined(); + }); + + test("finish without encounter does not register in ctx", async () => { + const rolex = setup(); + rolex.individual.born("Feature: Sean", "sean"); + const { ctx } = await rolex.role.activate("sean"); + + rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); + rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); + rolex.role.todo("jwt", "Feature: Login", "login", undefined, ctx!); + + rolex.role.finish("login", "sean", undefined, ctx!); + expect(ctx!.encounterIds.size).toBe(0); + }); + + test("complete registers encounter and clears focusedPlanId", async () => { + const rolex = setup(); + rolex.individual.born("Feature: Sean", "sean"); + const { ctx } = await rolex.role.activate("sean"); + + rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); + rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); + + const result = rolex.role.complete( + "jwt", + "sean", + "Feature: JWT done\n Scenario: OK\n Given jwt\n Then done", + ctx! + ); + expect(ctx!.focusedPlanId).toBeNull(); + expect(ctx!.encounterIds.has("jwt-completed")).toBe(true); + expect(result.hint).toContain("auth"); + }); + + test("reflect consumes encounter and adds experience in ctx", async () => { + const rolex = setup(); + rolex.individual.born("Feature: Sean", "sean"); + const { ctx } = await rolex.role.activate("sean"); + + rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); + rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); + rolex.role.todo("jwt", "Feature: Login", "login", undefined, ctx!); + rolex.role.finish( + "login", + "sean", + "Feature: Login done\n Scenario: OK\n Given x\n Then y", + ctx! + ); + + expect(ctx!.encounterIds.has("login-finished")).toBe(true); + + rolex.role.reflect( + "login-finished", + "sean", + "Feature: Token insight\n Scenario: OK\n Given x\n Then y", + "token-insight", + ctx! + ); + + expect(ctx!.encounterIds.has("login-finished")).toBe(false); + expect(ctx!.experienceIds.has("token-insight")).toBe(true); + }); + + 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"); + }); +}); diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index 42391fc..13315d0 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -15,7 +15,7 @@ describe("Rolex API (stateless)", () => { describe("lifecycle: creation", () => { test("born creates an individual with scaffolding", () => { const rolex = setup(); - const r = rolex.born("Feature: I am Sean"); + const r = rolex.individual.born("Feature: I am Sean", "sean"); expect(r.state.name).toBe("individual"); expect(r.state.information).toBe("Feature: I am Sean"); expect(r.process).toBe("born"); @@ -27,31 +27,31 @@ describe("Rolex API (stateless)", () => { test("found creates an organization", () => { const rolex = setup(); - const r = rolex.found("Feature: AI company"); + const r = rolex.org.found("Feature: AI company", "ai-co"); 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"); + rolex.org.found(undefined, "org1"); + const r = rolex.org.establish("org1", "Feature: Backend architect", "pos1"); 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"); + rolex.org.found(undefined, "org1"); + const r = rolex.org.charter("org1", "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"); + rolex.org.found(undefined, "org1"); + rolex.org.establish("org1", undefined, "pos1"); + const r = rolex.org.charge("pos1", "Feature: Design systems"); expect(r.state.name).toBe("duty"); }); }); @@ -63,42 +63,48 @@ describe("Rolex API (stateless)", () => { describe("lifecycle: archival", () => { test("retire archives individual", () => { const rolex = setup(); - const sean = rolex.born("Feature: Sean").state; - const r = rolex.retire(sean); + rolex.individual.born("Feature: Sean", "sean"); + const r = rolex.individual.retire("sean"); expect(r.state.name).toBe("past"); expect(r.process).toBe("retire"); - // Original is gone - expect(() => rolex.project(sean)).toThrow(); + // Original individual is gone — only past node with same id remains + const found = rolex.find("sean"); + expect(found).not.toBeNull(); + expect(found!.name).toBe("past"); }); test("die archives individual", () => { const rolex = setup(); - const alice = rolex.born().state; - const r = rolex.die(alice); + rolex.individual.born(undefined, "alice"); + const r = rolex.individual.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(); + rolex.org.found(undefined, "org1"); + rolex.org.dissolve("org1"); + const found = rolex.find("org1"); + expect(found).not.toBeNull(); + expect(found!.name).toBe("past"); }); 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(); + rolex.org.found(undefined, "org1"); + rolex.org.establish("org1", undefined, "pos1"); + rolex.org.abolish("pos1"); + const found = rolex.find("pos1"); + expect(found).not.toBeNull(); + expect(found!.name).toBe("past"); }); 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); + rolex.individual.born("Feature: Sean", "sean"); + rolex.individual.retire("sean"); + const r = rolex.individual.rehire("sean"); expect(r.state.name).toBe("individual"); expect(r.state.information).toBe("Feature: Sean"); // Scaffolding restored @@ -115,39 +121,39 @@ describe("Rolex API (stateless)", () => { 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); + rolex.individual.born(undefined, "sean"); + rolex.org.found(undefined, "org1"); + const r = rolex.org.hire("org1", "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); + rolex.individual.born(undefined, "sean"); + rolex.org.found(undefined, "org1"); + rolex.org.hire("org1", "sean"); + const r = rolex.org.fire("org1", "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); + rolex.individual.born(undefined, "sean"); + rolex.org.found(undefined, "org1"); + rolex.org.establish("org1", undefined, "pos1"); + const r = rolex.org.appoint("pos1", "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); + rolex.individual.born(undefined, "sean"); + rolex.org.found(undefined, "org1"); + rolex.org.establish("org1", undefined, "pos1"); + rolex.org.appoint("pos1", "sean"); + const r = rolex.org.dismiss("pos1", "sean"); expect(r.state.links).toBeUndefined(); }); }); @@ -157,10 +163,10 @@ describe("Rolex API (stateless)", () => { // ============================================================ describe("role", () => { - test("activate returns individual projection", () => { + test("activate returns individual projection", async () => { const rolex = setup(); - const sean = rolex.born("Feature: Sean").state; - const r = rolex.activate(sean); + rolex.individual.born("Feature: Sean", "sean"); + const r = await rolex.role.activate("sean"); expect(r.state.name).toBe("individual"); expect(r.process).toBe("activate"); }); @@ -173,63 +179,63 @@ describe("Rolex API (stateless)", () => { describe("execution", () => { test("want creates a goal", () => { const rolex = setup(); - const sean = rolex.born().state; - const r = rolex.want(sean, "Feature: Build auth system"); + rolex.individual.born(undefined, "sean"); + const r = rolex.role.want("sean", "Feature: Build auth system", "g1"); 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"); + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", "Feature: Auth", "g1"); + const r = rolex.role.plan("g1", "Feature: JWT plan", "p1"); 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"); + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", undefined, "g1"); + rolex.role.plan("g1", undefined, "p1"); + const r = rolex.role.todo("p1", "Feature: Implement JWT", "t1"); 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; + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", undefined, "g1"); + rolex.role.plan("g1", undefined, "p1"); + rolex.role.todo("p1", undefined, "t1"); - const r = rolex.finish(task, sean, "Feature: JWT done"); + const r = rolex.role.finish("t1", "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(); + expect(rolex.find("t1")).toBeNull(); }); 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; + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", "Feature: Auth", "g1"); + rolex.role.plan("g1", "Feature: Auth plan", "p1"); - const r = rolex.complete(plan, sean, "Feature: Auth plan done"); + const r = rolex.role.complete("p1", "sean", "Feature: Auth plan done"); expect(r.state.name).toBe("encounter"); - expect(() => rolex.project(plan)).toThrow(); + expect(rolex.find("p1")).toBeNull(); }); 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; + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", "Feature: Rust", "g1"); + rolex.role.plan("g1", "Feature: Rust plan", "p1"); - const r = rolex.abandon(plan, sean, "Feature: No time"); + const r = rolex.role.abandon("p1", "sean", "Feature: No time"); expect(r.state.name).toBe("encounter"); - expect(() => rolex.project(plan)).toThrow(); + expect(rolex.find("p1")).toBeNull(); }); }); @@ -240,60 +246,56 @@ describe("Rolex API (stateless)", () => { 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; + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", undefined, "g1"); + rolex.role.plan("g1", undefined, "p1"); + rolex.role.todo("p1", undefined, "t1"); + rolex.role.finish("t1", "sean", "Feature: JWT quirks"); - const r = rolex.reflect(enc, sean, "Feature: Token refresh matters"); + const r = rolex.role.reflect("t1-finished", "sean", "Feature: Token refresh matters", "exp1"); expect(r.state.name).toBe("experience"); expect(r.state.information).toBe("Feature: Token refresh matters"); - expect(() => rolex.project(enc)).toThrow(); + expect(rolex.find("t1-finished")).toBeNull(); }); 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; + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", undefined, "g1"); + rolex.role.plan("g1", undefined, "p1"); + rolex.role.todo("p1", undefined, "t1"); + rolex.role.finish("t1", "sean", "Feature: JWT quirks"); - const r = rolex.reflect(enc, sean); + const r = rolex.role.reflect("t1-finished", "sean"); expect(r.state.information).toBe("Feature: JWT quirks"); }); - test("realize: experience → principle under knowledge", () => { + test("realize: experience → principle under individual", () => { 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; + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", undefined, "g1"); + rolex.role.plan("g1", undefined, "p1"); + rolex.role.todo("p1", undefined, "t1"); + rolex.role.finish("t1", "sean", "Feature: Lessons"); + rolex.role.reflect("t1-finished", "sean", undefined, "exp1"); - const r = rolex.realize(exp, knowledge, "Feature: Security first"); + const r = rolex.role.realize("exp1", "sean", "Feature: Security first", "sec-first"); expect(r.state.name).toBe("principle"); expect(r.state.information).toBe("Feature: Security first"); - expect(() => rolex.project(exp)).toThrow(); + expect(rolex.find("exp1")).toBeNull(); }); - test("master: experience → skill under knowledge", () => { + test("master: experience → procedure under individual", () => { const rolex = setup(); - const sean = rolex.born().state; - const knowledge = sean.children!.find((c) => c.name === "knowledge")!; + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", undefined, "g1"); + rolex.role.plan("g1", undefined, "p1"); + rolex.role.todo("p1", undefined, "t1"); + rolex.role.finish("t1", "sean", "Feature: Practice"); + rolex.role.reflect("t1-finished", "sean", undefined, "exp1"); - 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 r = rolex.master(exp, knowledge, "Feature: JWT mastery"); - expect(r.state.name).toBe("skill"); + const r = rolex.role.master("sean", "Feature: JWT mastery", "jwt", "exp1"); + expect(r.state.name).toBe("procedure"); }); }); @@ -306,42 +308,41 @@ describe("Rolex API (stateless)", () => { 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"); + rolex.individual.born("Feature: I am Sean", "sean"); + rolex.org.found("Feature: Deepractice", "dp"); + rolex.org.establish("dp", "Feature: Architect", "architect"); + rolex.org.charter("dp", "Feature: Build great AI"); + rolex.org.charge("architect", "Feature: Design systems"); // Organization - rolex.hire(org, sean); - rolex.appoint(pos, sean); + rolex.org.hire("dp", "sean"); + rolex.org.appoint("architect", "sean"); // Verify links - const orgState = rolex.project(org); + const orgState = rolex.find("dp")!; expect(orgState.links).toHaveLength(1); - const posState = orgState.children!.find((c) => c.name === "position")!; + const posState = (orgState as any).children!.find((c: any) => 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; + rolex.role.want("sean", "Feature: Build auth", "build-auth"); + rolex.role.plan("build-auth", "Feature: JWT auth plan", "jwt-plan"); + rolex.role.todo("jwt-plan", "Feature: Login endpoint", "t1"); + rolex.role.todo("jwt-plan", "Feature: Refresh endpoint", "t2"); - 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"); + rolex.role.finish("t1", "sean", "Feature: Login done"); + rolex.role.finish("t2", "sean", "Feature: Refresh done"); + rolex.role.complete("jwt-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"); + rolex.role.reflect("t1-finished", "sean", "Feature: Token handling", "token-exp"); + rolex.role.realize("token-exp", "sean", "Feature: Always validate expiry", "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"); + // Verify principle exists under individual + const seanState = rolex.find("sean")!; + const principle = (seanState as any).children?.find((c: any) => c.name === "principle"); + expect(principle).toBeDefined(); + expect(principle.information).toBe("Feature: Always validate expiry"); }); }); @@ -352,7 +353,7 @@ describe("Rolex API (stateless)", () => { describe("render", () => { test("describe generates text with name", () => { const rolex = setup(); - const r = rolex.born(); + const r = rolex.individual.born(undefined, "sean"); const text = renderDescribe("born", "sean", r.state); expect(text).toContain("sean"); }); @@ -402,7 +403,7 @@ describe("Rolex API (stateless)", () => { 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 r = rolex.individual.born("Feature: I am Sean\n An AI role.", "sean"); const md = renderState(r.state); expect(md).toContain("# [individual]"); expect(md).toContain("Feature: I am Sean"); @@ -411,7 +412,7 @@ describe("Rolex API (stateless)", () => { test("renders children at deeper heading levels", () => { const rolex = setup(); - const r = rolex.born("Feature: Sean"); + const r = rolex.individual.born("Feature: Sean", "sean"); const md = renderState(r.state); // identity and knowledge are children at depth 2 expect(md).toContain("## [identity]"); @@ -420,24 +421,24 @@ describe("Rolex API (stateless)", () => { 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); + rolex.individual.born("Feature: Sean", "sean"); + rolex.org.found("Feature: Deepractice", "dp"); + rolex.org.hire("dp", "sean"); // Project org — should have membership link - const orgState = rolex.project(org); - const md = renderState(orgState); + const orgState = rolex.find("dp")!; + const md = renderState(orgState as any); 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); + rolex.individual.born("Feature: Sean", "sean"); + rolex.org.found("Feature: Deepractice", "dp"); + rolex.org.hire("dp", "sean"); // Project individual — should have belong link - const seanState = rolex.project(sean); - const md = renderState(seanState); + const seanState = rolex.find("sean")!; + const md = renderState(seanState as any); expect(md).toContain("belong"); expect(md).toContain("[organization]"); expect(md).toContain("Deepractice"); @@ -445,13 +446,13 @@ describe("Rolex API (stateless)", () => { 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"); + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", "Feature: Build auth", "g1"); + rolex.role.plan("g1", "Feature: JWT plan", "p1"); + rolex.role.todo("p1", "Feature: Login endpoint", "t1"); // Project goal to see full tree - const goalState = rolex.project(goal); - const md = renderState(goalState); + const goalState = rolex.find("g1")!; + const md = renderState(goalState as any); expect(md).toContain("# [goal]"); expect(md).toContain("## [plan]"); expect(md).toContain("### [task]"); @@ -462,19 +463,18 @@ describe("Rolex API (stateless)", () => { test("caps heading depth at 6", () => { const rolex = setup(); - const sean = rolex.born().state; - // individual(1) → identity(2) is the deepest built-in nesting + const r = rolex.individual.born(undefined, "sean"); // Manually test with depth parameter - const md = renderState(sean, 7); + const md = renderState(r.state, 7); // Should use ###### (6) not ####### (7) expect(md).toStartWith("###### [individual]"); }); test("renders without information gracefully", () => { const rolex = setup(); - const r = rolex.born(); + const r = rolex.individual.born(undefined, "sean"); const identity = r.state.children!.find((c) => c.name === "identity")!; - const md = renderState(identity); + const md = renderState(identity as any); expect(md).toBe("# [identity]"); }); }); @@ -486,86 +486,74 @@ describe("Rolex API (stateless)", () => { describe("gherkin validation", () => { test("born rejects non-Gherkin input", () => { const rolex = setup(); - expect(() => rolex.born("not gherkin")).toThrow("Invalid Gherkin"); + expect(() => rolex.individual.born("not gherkin")).toThrow("Invalid Gherkin"); }); test("born accepts valid Gherkin", () => { const rolex = setup(); - expect(() => rolex.born("Feature: Sean")).not.toThrow(); + expect(() => rolex.individual.born("Feature: Sean")).not.toThrow(); }); test("born accepts undefined (no source)", () => { const rolex = setup(); - expect(() => rolex.born()).not.toThrow(); + expect(() => rolex.individual.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"); + rolex.individual.born("Feature: Sean", "sean"); + expect(() => rolex.role.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"); + rolex.individual.born("Feature: Sean", "sean"); + rolex.role.want("sean", "Feature: Auth", "g1"); + rolex.role.plan("g1", undefined, "p1"); + rolex.role.todo("p1", "Feature: Login", "t1"); + expect(() => rolex.role.finish("t1", "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, + rolex.individual.born("Feature: Sean", "sean"); + rolex.role.want("sean", "Feature: Auth", "g1"); + rolex.role.plan("g1", undefined, "p1"); + rolex.role.todo("p1", "Feature: Login", "t1"); + rolex.role.finish( + "t1", + "sean", "Feature: Done\n Scenario: It worked\n Given login\n Then success" - ).state; - expect(() => rolex.reflect(enc, sean, "not gherkin")).toThrow("Invalid Gherkin"); + ); + expect(() => rolex.role.reflect("t1-finished", "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, + rolex.individual.born("Feature: Sean", "sean"); + rolex.role.want("sean", "Feature: Auth", "g1"); + rolex.role.plan("g1", undefined, "p1"); + rolex.role.todo("p1", "Feature: Login", "t1"); + rolex.role.finish( + "t1", + "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"); + ); + rolex.role.reflect( + "t1-finished", + "sean", + "Feature: Insight\n Scenario: Learned\n Given practice\n Then understanding", + "exp1" + ); + expect(() => rolex.role.realize("exp1", "sean", "not gherkin")).toThrow("Invalid Gherkin"); + }); + + test("master rejects non-Gherkin procedure", () => { + const rolex = setup(); + rolex.individual.born("Feature: Sean", "sean"); + expect(() => rolex.role.master("sean", "not gherkin")).toThrow("Invalid Gherkin"); }); }); @@ -576,43 +564,43 @@ describe("Rolex API (stateless)", () => { describe("id & alias", () => { test("born with id stores it on the node", () => { const rolex = setup(); - const r = rolex.born("Feature: I am Sean", "sean"); + const r = rolex.individual.born("Feature: I am Sean", "sean"); expect(r.state.id).toBe("sean"); expect(r.state.ref).toBeDefined(); }); test("born with id and alias stores both", () => { const rolex = setup(); - const r = rolex.born("Feature: I am Sean", "sean", ["Sean", "姜山"]); + const r = rolex.individual.born("Feature: I am Sean", "sean", ["Sean", "姜山"]); expect(r.state.id).toBe("sean"); expect(r.state.alias).toEqual(["Sean", "姜山"]); }); test("born without id has no id field", () => { const rolex = setup(); - const r = rolex.born("Feature: I am Sean"); + const r = rolex.individual.born("Feature: I am Sean"); expect(r.state.id).toBeUndefined(); }); 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"); + rolex.individual.born("Feature: Sean", "sean"); + const r = rolex.role.want("sean", "Feature: Build auth", "build-auth"); expect(r.state.id).toBe("build-auth"); }); 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"); + rolex.individual.born("Feature: Sean", "sean"); + rolex.role.want("sean", undefined, "g1"); + rolex.role.plan("g1", undefined, "p1"); + const r = rolex.role.todo("p1", "Feature: Login", "impl-login"); expect(r.state.id).toBe("impl-login"); }); test("find by id", () => { const rolex = setup(); - rolex.born("Feature: I am Sean", "sean"); + rolex.individual.born("Feature: I am Sean", "sean"); const found = rolex.find("sean"); expect(found).not.toBeNull(); expect(found!.name).toBe("individual"); @@ -621,7 +609,7 @@ describe("Rolex API (stateless)", () => { test("find by alias", () => { const rolex = setup(); - rolex.born("Feature: I am Sean", "sean", ["Sean", "姜山"]); + rolex.individual.born("Feature: I am Sean", "sean", ["Sean", "姜山"]); const found = rolex.find("姜山"); expect(found).not.toBeNull(); expect(found!.name).toBe("individual"); @@ -629,7 +617,7 @@ describe("Rolex API (stateless)", () => { test("find is case insensitive", () => { const rolex = setup(); - rolex.born("Feature: I am Sean", "sean", ["Sean"]); + rolex.individual.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(); @@ -637,14 +625,14 @@ describe("Rolex API (stateless)", () => { test("find returns null when not found", () => { const rolex = setup(); - rolex.born("Feature: I am Sean", "sean"); + rolex.individual.born("Feature: I am Sean", "sean"); expect(rolex.find("nobody")).toBeNull(); }); test("find searches nested nodes", () => { const rolex = setup(); - const sean = rolex.born("Feature: Sean", "sean").state; - rolex.want(sean, "Feature: Build auth", "build-auth"); + rolex.individual.born("Feature: Sean", "sean"); + rolex.role.want("sean", "Feature: Build auth", "build-auth"); const found = rolex.find("build-auth"); expect(found).not.toBeNull(); expect(found!.name).toBe("goal"); @@ -652,14 +640,14 @@ describe("Rolex API (stateless)", () => { test("found with id", () => { const rolex = setup(); - const r = rolex.found("Feature: Deepractice", "deepractice"); + const r = rolex.org.found("Feature: Deepractice", "deepractice"); expect(r.state.id).toBe("deepractice"); }); test("establish with id", () => { const rolex = setup(); - const org = rolex.found("Feature: Deepractice").state; - const r = rolex.establish(org, "Feature: Architect", "architect"); + rolex.org.found("Feature: Deepractice", "dp"); + const r = rolex.org.establish("dp", "Feature: Architect", "architect"); expect(r.state.id).toBe("architect"); }); }); From 8544c9a6637180f2b8429531e439f0c7b8320773 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 13:17:13 +0800 Subject: [PATCH 04/54] fix: format code with biome and fix lint issues Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 6 ++++- apps/mcp-server/src/state.ts | 2 +- packages/rolexjs/src/index.ts | 4 +-- packages/rolexjs/src/rolex.ts | 40 ++++++++++++++++++++++++---- packages/rolexjs/tests/rolex.test.ts | 6 +---- 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index e4b3727..cfc0a62 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -29,7 +29,11 @@ const server = new FastMCP({ // ========== Helpers ========== -function fmt(process: string, label: string, result: { state: any; process: string; hint?: string }) { +function fmt( + process: string, + label: string, + result: { state: any; process: string; hint?: string } +) { return render({ process, name: label, diff --git a/apps/mcp-server/src/state.ts b/apps/mcp-server/src/state.ts index 922f3aa..73d4291 100644 --- a/apps/mcp-server/src/state.ts +++ b/apps/mcp-server/src/state.ts @@ -5,7 +5,7 @@ * registries) now lives in RoleContext (rolexjs). McpState only holds * the ctx reference and provides MCP-specific helpers. */ -import type { Rolex, RoleContext } from "rolexjs"; +import type { RoleContext, Rolex } from "rolexjs"; export class McpState { ctx: RoleContext | null = null; diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index 70912b5..b5b30da 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -16,6 +16,8 @@ // 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"; @@ -24,5 +26,3 @@ export { describe, detail, hint, renderState, world } from "./render.js"; export type { RolexResult } from "./rolex.js"; // API export { createRoleX, Rolex } from "./rolex.js"; -// Context -export { RoleContext } from "./context.js"; diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 429fd1a..d5295c6 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -220,7 +220,13 @@ class RoleNamespace { // ---- Execution ---- /** Declare a goal under an individual. */ - want(individual: string, goal?: string, id?: string, alias?: readonly string[], ctx?: RoleContext): RolexResult { + want( + individual: string, + goal?: string, + id?: string, + alias?: readonly string[], + ctx?: RoleContext + ): RolexResult { validateGherkin(goal); const node = this.rt.create(this.resolve(individual), C.goal, goal, id, alias); const result = ok(this.rt, node, "want"); @@ -245,7 +251,13 @@ class RoleNamespace { } /** Add a task to a plan. */ - todo(plan: string, task?: string, id?: string, alias?: readonly string[], ctx?: RoleContext): RolexResult { + todo( + plan: string, + task?: string, + id?: string, + alias?: readonly string[], + ctx?: RoleContext + ): RolexResult { validateGherkin(task); const node = this.rt.create(this.resolve(plan), C.task, task, id, alias); const result = ok(this.rt, node, "todo"); @@ -311,7 +323,13 @@ class RoleNamespace { // ---- Cognition ---- /** Reflect: consume encounter, create experience under individual. */ - reflect(encounter: string, individual: string, experience?: string, id?: string, ctx?: RoleContext): RolexResult { + reflect( + encounter: string, + individual: string, + experience?: string, + id?: string, + ctx?: RoleContext + ): RolexResult { validateGherkin(experience); if (ctx) ctx.requireEncounterIds([encounter]); const encNode = this.resolve(encounter); @@ -332,7 +350,13 @@ class RoleNamespace { } /** Realize: consume experience, create principle under individual. */ - realize(experience: string, individual: string, principle?: string, id?: string, ctx?: RoleContext): RolexResult { + realize( + experience: string, + individual: string, + principle?: string, + id?: string, + ctx?: RoleContext + ): RolexResult { validateGherkin(principle); if (ctx) ctx.requireExperienceIds([experience]); const expNode = this.resolve(experience); @@ -352,7 +376,13 @@ class RoleNamespace { } /** Master: create procedure under individual, optionally consuming experience. */ - master(individual: string, procedure: string, id?: string, experience?: string, ctx?: RoleContext): RolexResult { + master( + individual: string, + procedure: string, + id?: string, + experience?: string, + ctx?: RoleContext + ): RolexResult { validateGherkin(procedure); if (ctx && experience) ctx.requireExperienceIds([experience]); const parent = this.resolve(individual); diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index 13315d0..d84f69e 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -536,11 +536,7 @@ describe("Rolex API (stateless)", () => { rolex.role.want("sean", "Feature: Auth", "g1"); rolex.role.plan("g1", undefined, "p1"); rolex.role.todo("p1", "Feature: Login", "t1"); - rolex.role.finish( - "t1", - "sean", - "Feature: Done\n Scenario: OK\n Given x\n Then y" - ); + rolex.role.finish("t1", "sean", "Feature: Done\n Scenario: OK\n Given x\n Then y"); rolex.role.reflect( "t1-finished", "sean", From 8d07ca07f6279f0411b548c2098d5ecf5437a27f Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 13:33:54 +0800 Subject: [PATCH 05/54] feat: add after/fallback relationships between plans Plans can now declare explicit sequential (after) or alternative (fallback) relationships, enabling cross-session ordering recovery. Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 12 ++++- packages/rolexjs/src/descriptions/index.ts | 2 +- .../rolexjs/src/descriptions/plan.feature | 23 +++++++++ packages/rolexjs/src/rolex.ts | 13 ++++- packages/rolexjs/tests/rolex.test.ts | 50 +++++++++++++++++++ 5 files changed, 95 insertions(+), 5 deletions(-) diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index cfc0a62..8cfe2d5 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -96,11 +96,19 @@ 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 }) => { + execute: async ({ id, plan, after, fallback }) => { const ctx = state.requireCtx(); const goalId = ctx.requireGoalId(); - const result = rolex.role.plan(goalId, plan, id, ctx); + const result = rolex.role.plan(goalId, plan, id, ctx, after, fallback); return fmt("plan", id, result); }, }); diff --git a/packages/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts index 88c48d0..0b284cf 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -20,7 +20,7 @@ export const processes: Record = { "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 — 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 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", + "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 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", diff --git a/packages/rolexjs/src/descriptions/plan.feature b/packages/rolexjs/src/descriptions/plan.feature index c33c257..f8fec75 100644 --- a/packages/rolexjs/src/descriptions/plan.feature +++ b/packages/rolexjs/src/descriptions/plan.feature @@ -2,6 +2,10 @@ 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 @@ -10,6 +14,25 @@ Feature: plan — create a plan for a 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" diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index d5295c6..7241b18 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -238,10 +238,19 @@ class RoleNamespace { return result; } - /** Create a plan for a goal. */ - plan(goal: string, plan?: string, id?: string, ctx?: RoleContext): RolexResult { + /** Create a plan for a goal. Optionally link to another plan via after (sequential) or fallback (alternative). */ + plan( + goal: string, + plan?: string, + id?: string, + ctx?: RoleContext, + after?: string, + fallback?: string + ): RolexResult { validateGherkin(plan); const node = this.rt.create(this.resolve(goal), C.plan, plan, id); + if (after) this.rt.link(node, this.resolve(after), "after", "before"); + if (fallback) this.rt.link(node, this.resolve(fallback), "fallback-for", "fallback"); const result = ok(this.rt, node, "plan"); if (ctx) { if (id) ctx.focusedPlanId = id; diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index d84f69e..39b7c80 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -193,6 +193,56 @@ describe("Rolex API (stateless)", () => { expect(r.state.name).toBe("plan"); }); + test("plan with after creates sequential link", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", "Feature: Auth", "g1"); + rolex.role.plan("g1", "Feature: Phase 1", "phase-1"); + rolex.role.plan("g1", "Feature: Phase 2", "phase-2", undefined, "phase-1"); + + // Phase 2 has "after" link to Phase 1 + const p2 = rolex.find("phase-2")!; + expect((p2 as any).links).toHaveLength(1); + expect((p2 as any).links[0].relation).toBe("after"); + expect((p2 as any).links[0].target.id).toBe("phase-1"); + + // Phase 1 has reverse "before" link to Phase 2 + const p1 = rolex.find("phase-1")!; + expect((p1 as any).links).toHaveLength(1); + expect((p1 as any).links[0].relation).toBe("before"); + expect((p1 as any).links[0].target.id).toBe("phase-2"); + }); + + test("plan with fallback creates alternative link", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", "Feature: Auth", "g1"); + rolex.role.plan("g1", "Feature: JWT approach", "plan-a"); + rolex.role.plan("g1", "Feature: Session approach", "plan-b", undefined, undefined, "plan-a"); + + // Plan B has "fallback-for" link to Plan A + const pb = rolex.find("plan-b")!; + expect((pb as any).links).toHaveLength(1); + expect((pb as any).links[0].relation).toBe("fallback-for"); + expect((pb as any).links[0].target.id).toBe("plan-a"); + + // Plan A has reverse "fallback" link to Plan B + const pa = rolex.find("plan-a")!; + expect((pa as any).links).toHaveLength(1); + expect((pa as any).links[0].relation).toBe("fallback"); + expect((pa as any).links[0].target.id).toBe("plan-b"); + }); + + test("plan without after/fallback has no links", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", "Feature: Auth", "g1"); + rolex.role.plan("g1", "Feature: JWT plan", "p1"); + + const p1 = rolex.find("p1")!; + expect((p1 as any).links).toBeUndefined(); + }); + test("todo creates a task under plan", () => { const rolex = setup(); rolex.individual.born(undefined, "sean"); From 61859f5c7630328f6c6f535e39d143436dda3494 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 13:44:39 +0800 Subject: [PATCH 06/54] fix: persist child node links in manifest (plan-to-plan relationships) ManifestNode was missing a links field, causing plan-to-plan after/fallback links to be lost on save. Now links are serialized and restored at all tree depths, not just the root entity. Co-Authored-By: Claude Opus 4.6 --- packages/local-platform/src/LocalPlatform.ts | 42 ++++++--- packages/local-platform/src/manifest.ts | 15 ++++ .../local-platform/tests/manifest.test.ts | 87 +++++++++++++++++++ 3 files changed, 133 insertions(+), 11 deletions(-) diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index c4aaa9d..adee63b 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -240,21 +240,41 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { } } - // 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 }); + // Resolve links from manifests (all nodes, not just root) + const collectLinks = ( + nodeId: string, + node: { + links?: Record; + children?: Record; + } + ) => { + if (node.links) { + const sourceRef = idToRef.get(nodeId); + if (sourceRef) { + const entries: LinkEntry[] = links.get(sourceRef) ?? []; + for (const [relation, targetIds] of Object.entries(node.links)) { + for (const targetId of targetIds) { + const targetRef = idToRef.get(targetId); + if ( + targetRef && + !entries.some((l) => l.toId === targetRef && l.relation === relation) + ) { + entries.push({ toId: targetRef, relation }); + } + } } + if (entries.length > 0) links.set(sourceRef, entries); } } - if (entityLinks.length > 0) { - links.set(ref, entityLinks); + if (node.children) { + for (const [childId, childNode] of Object.entries(node.children)) { + collectLinks(childId, childNode); + } } + }; + + for (const { manifest } of entityRefs) { + collectLinks(manifest.id, manifest); } }; diff --git a/packages/local-platform/src/manifest.ts b/packages/local-platform/src/manifest.ts index f198008..1ecd901 100644 --- a/packages/local-platform/src/manifest.ts +++ b/packages/local-platform/src/manifest.ts @@ -26,6 +26,7 @@ export interface ManifestNode { readonly type: string; readonly ref?: string; readonly children?: Record; + readonly links?: Record; } /** Root manifest for an entity (individual or organization). */ @@ -74,6 +75,7 @@ export function stateToFiles(state: State): { manifest: Manifest; files: FileEnt const entry: ManifestNode = { type: node.name, ...(node.ref ? { ref: node.ref } : {}), + ...(node.links && node.links.length > 0 ? { links: buildManifestLinks(node.links) } : {}), }; if (node.children && node.children.length > 0) { const children: Record = {}; @@ -136,6 +138,18 @@ export function filesToState(manifest: Manifest, fileContents: Record 0 ? { children } : {}), + ...(nodeLinks.length > 0 ? { links: nodeLinks } : {}), }; }; 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", From fd69e5d90aec03ec5e1c79ded6e81a8e10f69835 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 14:07:06 +0800 Subject: [PATCH 07/54] feat: persist RoleContext across sessions with fold support on activate Context (focusedGoalId, focusedPlanId) is now saved to context/{roleId}.json on every focus-changing operation and restored on activate. Each role has independent context. Also adds renderState fold option so activate can show focused goal expanded while collapsing others. Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 9 +- apps/mcp-server/src/render.ts | 8 +- packages/core/src/index.ts | 2 +- packages/core/src/platform.ts | 12 ++ packages/local-platform/src/LocalPlatform.ts | 21 ++- packages/rolexjs/src/index.ts | 1 + packages/rolexjs/src/render.ts | 15 ++- packages/rolexjs/src/rolex.ts | 41 +++++- packages/rolexjs/tests/context.test.ts | 131 ++++++++++++++++++- packages/rolexjs/tests/rolex.test.ts | 23 ++++ 10 files changed, 250 insertions(+), 13 deletions(-) diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 8cfe2d5..7b11bb6 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -56,7 +56,14 @@ server.addTool({ } const result = await rolex.role.activate(roleId); state.ctx = result.ctx!; - return fmt("activate", roleId, result); + const ctx = result.ctx!; + return render({ + process: "activate", + name: roleId, + result, + cognitiveHint: result.hint ?? null, + fold: (node) => node.name === "goal" && node.id !== ctx.focusedGoalId, + }); }, }); diff --git a/apps/mcp-server/src/render.ts b/apps/mcp-server/src/render.ts index c465725..094d341 100644 --- a/apps/mcp-server/src/render.ts +++ b/apps/mcp-server/src/render.ts @@ -8,7 +8,7 @@ * 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 type { RenderStateOptions, RolexResult } from "rolexjs"; import { describe, hint, renderState } from "rolexjs"; // ================================================================ @@ -24,11 +24,13 @@ export interface RenderOptions { result: RolexResult; /** 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, result, cognitiveHint } = opts; + const { process, name, result, cognitiveHint, fold } = opts; const lines: string[] = []; // Layer 1: Status @@ -42,7 +44,7 @@ export function render(opts: RenderOptions): string { // Layer 3: Projection — generic markdown rendering of the full state tree lines.push(""); - lines.push(renderState(result.state)); + lines.push(renderState(result.state, 1, fold ? { fold } : undefined)); return lines.join("\n"); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index be292de..f6a530f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -41,7 +41,7 @@ export { } from "@rolexjs/system"; // Platform -export type { Platform } from "./platform.js"; +export type { ContextData, Platform } from "./platform.js"; // ===== Structures ===== diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index ef79450..d5e0752 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -13,6 +13,12 @@ import type { Prototype, Runtime } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; +/** Serializable context data for persistence. */ +export interface ContextData { + focusedGoalId: string | null; + focusedPlanId: string | null; +} + export interface Platform { /** Graph operation engine (may include transparent persistence). */ readonly runtime: Runtime; @@ -25,4 +31,10 @@ export interface Platform { /** Register a prototype: bind id to a ResourceX source (path or locator). */ registerPrototype?(id: string, source: string): void; + + /** Save role context to persistent storage. */ + saveContext?(roleId: string, data: ContextData): void; + + /** Load role context from persistent storage. Returns null if none exists. */ + loadContext?(roleId: string): ContextData | null; } diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index adee63b..52598a2 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -20,7 +20,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync import { homedir } from "node:os"; import { join } from "node:path"; import { NodeProvider } from "@resourcexjs/node-provider"; -import type { Platform } from "@rolexjs/core"; +import type { ContextData, 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"; @@ -474,5 +474,22 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { }, }; - return { runtime, prototype, resourcex, registerPrototype }; + // ===== Context persistence ===== + // Context lives outside role/ to survive save()'s clean-and-rebuild cycle. + + const saveContext = (roleId: string, data: ContextData): void => { + if (!dataDir) return; + const contextDir = join(dataDir, "context"); + mkdirSync(contextDir, { recursive: true }); + writeFileSync(join(contextDir, `${roleId}.json`), JSON.stringify(data, null, 2), "utf-8"); + }; + + const loadContext = (roleId: string): ContextData | null => { + if (!dataDir) return null; + const contextPath = join(dataDir, "context", `${roleId}.json`); + if (!existsSync(contextPath)) return null; + return JSON.parse(readFileSync(contextPath, "utf-8")); + }; + + return { runtime, prototype, resourcex, registerPrototype, saveContext, loadContext }; } diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index b5b30da..83f18cd 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -21,6 +21,7 @@ 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"; +export type { RenderStateOptions } from "./render.js"; // Render export { describe, detail, hint, renderState, world } from "./render.js"; export type { RolexResult } from "./rolex.js"; diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index 8c8b921..1cb01d9 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -133,11 +133,17 @@ export { world }; * - Body: raw information field as-is (full Gherkin preserved) * - Links: "> → relation [target.name]" with target feature name * - Children: recursive 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); @@ -147,6 +153,11 @@ export function renderState(state: State, depth = 1): string { const originPart = state.origin ? ` {${state.origin}}` : ""; lines.push(`${heading} [${state.name}]${idPart}${originPart}`); + // Folded: heading only + if (options?.fold?.(state)) { + return lines.join("\n"); + } + // Body: full information as-is if (state.information) { lines.push(""); @@ -166,7 +177,7 @@ export function renderState(state: State, depth = 1): string { if (state.children && state.children.length > 0) { for (const child of state.children) { lines.push(""); - lines.push(renderState(child, depth + 1)); + lines.push(renderState(child, depth + 1, options)); } } diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 7241b18..08bb3c4 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -16,7 +16,7 @@ * resource — ResourceX instance (optional) */ -import type { Platform } from "@rolexjs/core"; +import type { ContextData, Platform } from "@rolexjs/core"; import * as C from "@rolexjs/core"; import { parse } from "@rolexjs/parser"; import { @@ -84,7 +84,17 @@ export class Rolex { // Namespaces this.individual = new IndividualNamespace(this.rt, this.society, this.past, resolve); - this.role = new RoleNamespace(this.rt, resolve, platform.prototype, platform.resourcex); + const persistContext = + platform.saveContext && platform.loadContext + ? { save: platform.saveContext, load: platform.loadContext } + : undefined; + this.role = new RoleNamespace( + this.rt, + resolve, + platform.prototype, + platform.resourcex, + persistContext + ); this.org = new OrgNamespace(this.rt, this.society, this.past, resolve); this.resource = platform.resourcex; } @@ -188,9 +198,21 @@ class RoleNamespace { private rt: Runtime, private resolve: Resolve, private prototype?: Prototype, - private resourcex?: ResourceX + private resourcex?: ResourceX, + private persistContext?: { + save: (roleId: string, data: ContextData) => void; + load: (roleId: string) => ContextData | null; + } ) {} + private saveCtx(ctx?: RoleContext): void { + if (!ctx || !this.persistContext) return; + this.persistContext.save(ctx.roleId, { + focusedGoalId: ctx.focusedGoalId, + focusedPlanId: ctx.focusedPlanId, + }); + } + // ---- Activation ---- /** Activate: merge prototype (if any) with instance state. Returns ctx when created. */ @@ -203,6 +225,14 @@ class RoleNamespace { const state = protoState ? mergeState(protoState, instanceState) : instanceState; const ctx = new RoleContext(individual); ctx.rehydrate(state); + + // Restore persisted focus (overrides rehydrate defaults) + const persisted = this.persistContext?.load(individual); + if (persisted) { + ctx.focusedGoalId = persisted.focusedGoalId; + ctx.focusedPlanId = persisted.focusedPlanId; + } + return { state, process: "activate", hint: ctx.cognitiveHint("activate") ?? undefined, ctx }; } @@ -214,6 +244,7 @@ class RoleNamespace { } const result = ok(this.rt, this.resolve(goal), "focus"); if (ctx) result.hint = ctx.cognitiveHint("focus") ?? undefined; + this.saveCtx(ctx); return result; } @@ -234,6 +265,7 @@ class RoleNamespace { if (id) ctx.focusedGoalId = id; ctx.focusedPlanId = null; result.hint = ctx.cognitiveHint("want") ?? undefined; + this.saveCtx(ctx); } return result; } @@ -255,6 +287,7 @@ class RoleNamespace { if (ctx) { if (id) ctx.focusedPlanId = id; result.hint = ctx.cognitiveHint("plan") ?? undefined; + this.saveCtx(ctx); } return result; } @@ -309,6 +342,7 @@ class RoleNamespace { ctx.addEncounter(result.state.id ?? plan); if (ctx.focusedPlanId === plan) ctx.focusedPlanId = null; result.hint = ctx.cognitiveHint("complete") ?? undefined; + this.saveCtx(ctx); } return result; } @@ -325,6 +359,7 @@ class RoleNamespace { ctx.addEncounter(result.state.id ?? plan); if (ctx.focusedPlanId === plan) ctx.focusedPlanId = null; result.hint = ctx.cognitiveHint("abandon") ?? undefined; + this.saveCtx(ctx); } return result; } diff --git a/packages/rolexjs/tests/context.test.ts b/packages/rolexjs/tests/context.test.ts index 49138b1..38089d1 100644 --- a/packages/rolexjs/tests/context.test.ts +++ b/packages/rolexjs/tests/context.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, readFileSync, 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"; @@ -6,6 +9,13 @@ function setup() { return createRoleX(localPlatform({ dataDir: null })); } +function setupWithDir() { + const dataDir = join(tmpdir(), `rolex-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); + mkdirSync(dataDir, { recursive: true }); + const rolex = createRoleX(localPlatform({ dataDir, resourceDir: null })); + return { rolex, dataDir }; +} + describe("RoleContext", () => { test("activate returns ctx in result", async () => { const rolex = setup(); @@ -130,3 +140,122 @@ describe("RoleContext", () => { expect(ctx.cognitiveHint("todo")).toContain("finish"); }); }); + +describe("RoleContext persistence", () => { + const dirs: string[] = []; + afterEach(() => { + for (const d of dirs) { + if (existsSync(d)) rmSync(d, { recursive: true }); + } + dirs.length = 0; + }); + + function persistent() { + const { rolex, dataDir } = setupWithDir(); + dirs.push(dataDir); + return { rolex, dataDir }; + } + + test("activate restores persisted focusedGoalId and focusedPlanId", async () => { + const { rolex, dataDir } = persistent(); + rolex.individual.born("Feature: Sean", "sean"); + + // Session 1: set focus + const { ctx: ctx1 } = await rolex.role.activate("sean"); + rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx1!); + rolex.role.plan("auth", "Feature: JWT", "jwt", ctx1!); + expect(ctx1!.focusedGoalId).toBe("auth"); + expect(ctx1!.focusedPlanId).toBe("jwt"); + + // Verify context.json written + const contextPath = join(dataDir, "context", "sean.json"); + expect(existsSync(contextPath)).toBe(true); + const data = JSON.parse(readFileSync(contextPath, "utf-8")); + expect(data.focusedGoalId).toBe("auth"); + expect(data.focusedPlanId).toBe("jwt"); + + // Session 2: re-activate restores + const { ctx: ctx2 } = await rolex.role.activate("sean"); + expect(ctx2!.focusedGoalId).toBe("auth"); + expect(ctx2!.focusedPlanId).toBe("jwt"); + }); + + test("activate without persisted context uses rehydrate default", async () => { + const { rolex } = persistent(); + rolex.individual.born("Feature: Sean", "sean"); + rolex.role.want("sean", "Feature: Auth", "auth"); + + // No context.json exists — rehydrate picks first goal + const { ctx } = await rolex.role.activate("sean"); + expect(ctx!.focusedGoalId).toBe("auth"); + expect(ctx!.focusedPlanId).toBeNull(); + }); + + test("focus saves updated context", async () => { + const { rolex, dataDir } = persistent(); + rolex.individual.born("Feature: Sean", "sean"); + + const { ctx } = await rolex.role.activate("sean"); + rolex.role.want("sean", "Feature: Goal A", "goal-a", undefined, ctx!); + rolex.role.want("sean", "Feature: Goal B", "goal-b", undefined, ctx!); + + // focus switches back to goal-a + rolex.role.focus("goal-a", ctx!); + + const data = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); + expect(data.focusedGoalId).toBe("goal-a"); + expect(data.focusedPlanId).toBeNull(); + }); + + test("complete clears focusedPlanId and saves", async () => { + const { rolex, dataDir } = persistent(); + rolex.individual.born("Feature: Sean", "sean"); + + const { ctx } = await rolex.role.activate("sean"); + rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); + rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); + rolex.role.complete( + "jwt", + "sean", + "Feature: Done\n Scenario: OK\n Given done\n Then ok", + ctx! + ); + + const data = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); + expect(data.focusedGoalId).toBe("auth"); + expect(data.focusedPlanId).toBeNull(); + }); + + test("different roles have independent contexts", async () => { + const { rolex, dataDir } = persistent(); + rolex.individual.born("Feature: Sean", "sean"); + rolex.individual.born("Feature: Nuwa", "nuwa"); + + // Sean session + const { ctx: seanCtx } = await rolex.role.activate("sean"); + rolex.role.want("sean", "Feature: Sean Goal", "sean-goal", undefined, seanCtx!); + + // Nuwa session + const { ctx: nuwaCtx } = await rolex.role.activate("nuwa"); + rolex.role.want("nuwa", "Feature: Nuwa Goal", "nuwa-goal", undefined, nuwaCtx!); + + // Verify independent files + const seanData = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); + const nuwaData = JSON.parse(readFileSync(join(dataDir, "context", "nuwa.json"), "utf-8")); + expect(seanData.focusedGoalId).toBe("sean-goal"); + expect(nuwaData.focusedGoalId).toBe("nuwa-goal"); + + // Re-activate sean — should get sean's context, not nuwa's + const { ctx: seanCtx2 } = await rolex.role.activate("sean"); + expect(seanCtx2!.focusedGoalId).toBe("sean-goal"); + }); + + test("in-memory mode (dataDir: null) works without persistence", async () => { + const rolex = setup(); + rolex.individual.born("Feature: Sean", "sean"); + const { ctx } = await rolex.role.activate("sean"); + rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); + // Should not throw — just no persistence + expect(ctx!.focusedGoalId).toBe("auth"); + }); +}); diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index 39b7c80..fc48f97 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -527,6 +527,29 @@ describe("Rolex API (stateless)", () => { const md = renderState(identity as any); expect(md).toBe("# [identity]"); }); + + test("fold collapses matching nodes to heading only", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.role.want("sean", "Feature: Goal A", "g-a"); + rolex.role.want("sean", "Feature: Goal B", "g-b"); + rolex.role.plan("g-b", "Feature: Plan under B", "p-b"); + + const state = rolex.find("sean")!; + const md = renderState(state as any, 1, { + fold: (node) => node.name === "goal" && node.id !== "g-b", + }); + + // g-a: folded — heading only, no body + expect(md).toContain("## [goal] (g-a)"); + expect(md).not.toContain("Feature: Goal A"); + + // g-b: expanded — heading + body + children + expect(md).toContain("## [goal] (g-b)"); + expect(md).toContain("Feature: Goal B"); + expect(md).toContain("### [plan] (p-b)"); + expect(md).toContain("Feature: Plan under B"); + }); }); // ============================================================ From f869fb376e7ea93e8bd35ea4df29b40ed87ce244 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 14:47:48 +0800 Subject: [PATCH 08/54] refactor: remove knowledge layer and add concept-order rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the legacy knowledge container node from the individual hierarchy. Principle and procedure are now direct children of individual, matching the actual runtime behavior of realize/master/teach/train. Add concept hierarchy ordering to renderState so children are always rendered in structure-definition order (identity → encounter → experience → principle → procedure → goal) regardless of creation time. Co-Authored-By: Claude Opus 4.6 --- apps/cli/src/index.ts | 4 +- apps/mcp-server/tests/mcp.test.ts | 1 - packages/core/src/index.ts | 3 +- packages/core/src/structures.ts | 10 ++-- packages/rolexjs/src/context.ts | 4 +- .../rolexjs/src/descriptions/activate.feature | 4 +- packages/rolexjs/src/descriptions/index.ts | 4 +- .../rolexjs/src/descriptions/skill.feature | 2 +- packages/rolexjs/src/render.ts | 52 ++++++++++++++++--- packages/rolexjs/src/rolex.ts | 6 +-- packages/rolexjs/tests/rolex.test.ts | 24 +++++++-- 11 files changed, 80 insertions(+), 34 deletions(-) diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 3319178..aaadffc 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -109,7 +109,7 @@ const rehire = defineCommand({ const teach = defineCommand({ meta: { name: "teach", - description: "Inject a principle directly into an individual's knowledge", + description: "Inject a principle directly into an individual", }, args: { individual: { type: "positional" as const, description: "Individual id", required: true }, @@ -129,7 +129,7 @@ const teach = defineCommand({ const train = defineCommand({ meta: { name: "train", - description: "Inject a procedure (skill) directly into an individual's knowledge", + description: "Inject a procedure (skill) directly into an individual", }, args: { individual: { type: "positional" as const, description: "Individual id", required: true }, diff --git a/apps/mcp-server/tests/mcp.test.ts b/apps/mcp-server/tests/mcp.test.ts index 0c5646b..2da5c95 100644 --- a/apps/mcp-server/tests/mcp.test.ts +++ b/apps/mcp-server/tests/mcp.test.ts @@ -71,7 +71,6 @@ describe("render", () => { // Layer 3: Projection (generic markdown) expect(output).toContain("# [individual]"); expect(output).toContain("## [identity]"); - expect(output).toContain("## [knowledge]"); }); it("includes cognitive hint when provided", () => { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f6a530f..6c79795 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 @@ -60,7 +60,6 @@ export { // Level 1 individual, // Individual — Knowledge - knowledge, mindset, organization, past, diff --git a/packages/core/src/structures.ts b/packages/core/src/structures.ts index c155cb2..16ef91a 100644 --- a/packages/core/src/structures.ts +++ b/packages/core/src/structures.ts @@ -14,9 +14,8 @@ * │ │ │ └── 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" │ @@ -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 diff --git a/packages/rolexjs/src/context.ts b/packages/rolexjs/src/context.ts index 33fc1b9..cf9ad56 100644 --- a/packages/rolexjs/src/context.ts +++ b/packages/rolexjs/src/context.ts @@ -153,10 +153,10 @@ export class RoleContext { } case "realize": - return "Principle added to knowledge. I should continue working."; + return "Principle added. I should continue working."; case "master": - return "Procedure added to knowledge. I should continue working."; + return "Procedure added. I should continue working."; default: return null; diff --git a/packages/rolexjs/src/descriptions/activate.feature b/packages/rolexjs/src/descriptions/activate.feature index b4f5c96..c547a14 100644 --- a/packages/rolexjs/src/descriptions/activate.feature +++ b/packages/rolexjs/src/descriptions/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/index.ts b/packages/rolexjs/src/descriptions/index.ts index 0b284cf..3cea39d 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -3,7 +3,7 @@ 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", + "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", "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", @@ -25,7 +25,7 @@ export const processes: Record = { "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", + "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", "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 — 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", diff --git a/packages/rolexjs/src/descriptions/skill.feature b/packages/rolexjs/src/descriptions/skill.feature index 2bc31a8..16e93a4 100644 --- a/packages/rolexjs/src/descriptions/skill.feature +++ b/packages/rolexjs/src/descriptions/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/render.ts b/packages/rolexjs/src/render.ts index 1cb01d9..1493133 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -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.", @@ -126,16 +126,15 @@ export { world }; // ================================================================ /** - * 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 interface RenderStateOptions { @@ -173,9 +172,10 @@ export function renderState(state: State, depth = 1, options?: RenderStateOption } } - // Children + // Children — sorted by concept hierarchy if (state.children && state.children.length > 0) { - for (const child of state.children) { + const sorted = sortByConceptOrder(state.children); + for (const child of sorted) { lines.push(""); lines.push(renderState(child, depth + 1, options)); } @@ -191,3 +191,41 @@ function extractLabel(state: State): string { 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", + "duty", +]; + +/** 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; + }); +} diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 08bb3c4..e7f2bb5 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -134,9 +134,8 @@ class IndividualNamespace { born(individual?: string, id?: string, alias?: readonly string[]): RolexResult { validateGherkin(individual); const node = this.rt.create(this.society, C.individual, individual, id, alias); - // Scaffolding: every individual has identity + knowledge + // Scaffolding: every individual has identity this.rt.create(node, C.identity); - this.rt.create(node, C.knowledge); return ok(this.rt, node, "born"); } @@ -154,9 +153,8 @@ class IndividualNamespace { rehire(pastNode: string): RolexResult { const past = this.resolve(pastNode); const individual = this.rt.create(this.society, C.individual, past.information, past.id); - // Scaffolding: restore identity + knowledge + // Scaffolding: restore identity this.rt.create(individual, C.identity); - this.rt.create(individual, C.knowledge); this.rt.remove(past); return ok(this.rt, individual, "rehire"); } diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index fc48f97..b8d074c 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -19,10 +19,9 @@ describe("Rolex API (stateless)", () => { expect(r.state.name).toBe("individual"); expect(r.state.information).toBe("Feature: I am Sean"); expect(r.process).toBe("born"); - // Scaffolding: identity + knowledge + // Scaffolding: identity const names = r.state.children!.map((c) => c.name); expect(names).toContain("identity"); - expect(names).toContain("knowledge"); }); test("found creates an organization", () => { @@ -110,7 +109,6 @@ describe("Rolex API (stateless)", () => { // Scaffolding restored const names = r.state.children!.map((c) => c.name); expect(names).toContain("identity"); - expect(names).toContain("knowledge"); }); }); @@ -464,9 +462,8 @@ describe("Rolex API (stateless)", () => { const rolex = setup(); const r = rolex.individual.born("Feature: Sean", "sean"); const md = renderState(r.state); - // identity and knowledge are children at depth 2 + // identity is a child at depth 2 expect(md).toContain("## [identity]"); - expect(md).toContain("## [knowledge]"); }); test("renders links generically", () => { @@ -528,6 +525,23 @@ describe("Rolex API (stateless)", () => { expect(md).toBe("# [identity]"); }); + test("sorts children by concept hierarchy order", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + // Create in reverse of concept order: goal → principle + rolex.role.want("sean", "Feature: My Goal", "g1"); + rolex.individual.teach("sean", "Feature: My Principle", "p1"); + + const state = rolex.find("sean")!; + const md = renderState(state as any); + // Concept order: identity < principle < goal + const identityPos = md.indexOf("## [identity]"); + const principlePos = md.indexOf("## [principle] (p1)"); + const goalPos = md.indexOf("## [goal] (g1)"); + expect(identityPos).toBeLessThan(principlePos); + expect(principlePos).toBeLessThan(goalPos); + }); + test("fold collapses matching nodes to heading only", () => { const rolex = setup(); rolex.individual.born(undefined, "sean"); From 5813e87535ec11bdaeef42c441524fef6b3c0e1a Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 15:58:33 +0800 Subject: [PATCH 09/54] refactor: lift position to top-level entity and split OrgNamespace Position is now an independent entity under society (not a child of organization). OrgNamespace split into OrgNamespace (found, charter, dissolve, hire, fire) and PositionNamespace (establish, abolish, charge, appoint, dismiss). CLI adds `pos` subcommand group. Co-Authored-By: Claude Opus 4.6 --- apps/cli/src/index.ts | 31 ++++----- apps/mcp-server/tests/mcp.test.ts | 4 +- packages/core/src/lifecycle.ts | 7 +- packages/core/src/structures.ts | 17 +++-- .../rolexjs/src/descriptions/abolish.feature | 4 +- .../src/descriptions/establish.feature | 15 ++--- packages/rolexjs/src/descriptions/index.ts | 4 +- packages/rolexjs/src/render.ts | 3 +- packages/rolexjs/src/rolex.ts | 65 +++++++++++++------ packages/rolexjs/tests/rolex.test.ts | 40 +++++------- 10 files changed, 104 insertions(+), 86 deletions(-) diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index aaadffc..c200fd6 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -386,21 +386,15 @@ const found = defineCommand({ }); const establish = defineCommand({ - meta: { name: "establish", description: "Establish a position within an organization" }, + meta: { name: "establish", description: "Establish a position" }, 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 - ); + const result = rolex.position.establish(resolveContent(args, "position"), args.id, aliasList); output(result, args.id ?? result.state.name); }, }); @@ -424,7 +418,7 @@ const charge = defineCommand({ 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); + const result = rolex.position.charge(args.position, requireContent(args, "duty"), args.id); output(result, result.state.name); }, }); @@ -445,7 +439,7 @@ const abolish = defineCommand({ position: { type: "positional" as const, description: "Position id", required: true }, }, run({ args }) { - output(rolex.org.abolish(args.position), args.position); + output(rolex.position.abolish(args.position), args.position); }, }); @@ -478,7 +472,7 @@ const appoint = defineCommand({ individual: { type: "positional" as const, description: "Individual id", required: true }, }, run({ args }) { - output(rolex.org.appoint(args.position, args.individual), args.position); + output(rolex.position.appoint(args.position, args.individual), args.position); }, }); @@ -489,7 +483,7 @@ const dismiss = defineCommand({ individual: { type: "positional" as const, description: "Individual id", required: true }, }, run({ args }) { - output(rolex.org.dismiss(args.position, args.individual), args.position); + output(rolex.position.dismiss(args.position, args.individual), args.position); }, }); @@ -497,13 +491,19 @@ const org = defineCommand({ meta: { name: "org", description: "Organization management" }, subCommands: { found, - establish, charter, - charge, dissolve, - abolish, hire, fire, + }, +}); + +const pos = defineCommand({ + meta: { name: "pos", description: "Position management" }, + subCommands: { + establish, + charge, + abolish, appoint, dismiss, }, @@ -717,6 +717,7 @@ const main = defineCommand({ individual, role, org, + pos, resource, prototype, }, diff --git a/apps/mcp-server/tests/mcp.test.ts b/apps/mcp-server/tests/mcp.test.ts index 2da5c95..f7f90ec 100644 --- a/apps/mcp-server/tests/mcp.test.ts +++ b/apps/mcp-server/tests/mcp.test.ts @@ -104,9 +104,9 @@ describe("render", () => { it("includes appointment relation in projection", () => { rolex.individual.born("Feature: Sean", "sean"); rolex.org.found("Feature: Deepractice", "dp"); - rolex.org.establish("dp", "Feature: Architect", "architect"); + rolex.position.establish("Feature: Architect", "architect"); rolex.org.hire("dp", "sean"); - rolex.org.appoint("architect", "sean"); + rolex.position.appoint("architect", "sean"); const seanState = rolex.find("sean")!; const output = render({ 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/structures.ts b/packages/core/src/structures.ts index 16ef91a..52b2af0 100644 --- a/packages/core/src/structures.ts +++ b/packages/core/src/structures.ts @@ -21,10 +21,10 @@ * │ │ └── 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" │ * └─────────────────────────────────────────────────────────┘ */ @@ -37,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); @@ -82,7 +82,12 @@ 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); diff --git a/packages/rolexjs/src/descriptions/abolish.feature b/packages/rolexjs/src/descriptions/abolish.feature index 0902f01..96fc647 100644 --- a/packages/rolexjs/src/descriptions/abolish.feature +++ b/packages/rolexjs/src/descriptions/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/establish.feature b/packages/rolexjs/src/descriptions/establish.feature index 727b82e..8a57925 100644 --- a/packages/rolexjs/src/descriptions/establish.feature +++ b/packages/rolexjs/src/descriptions/establish.feature @@ -1,17 +1,16 @@ Feature: establish — create a position - Create a position within an organization. - Positions define roles within the org and can be charged with duties. + Create a position as an independent entity. + Positions define roles 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 + 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 members can be appointed to it + And individuals can be appointed to it Scenario: Writing the position Gherkin - Given the position Feature describes a role within an organization + 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/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts index 3cea39d..ebd5a9e 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -2,7 +2,7 @@ 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", + "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", "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", "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", @@ -12,7 +12,7 @@ export const processes: Record = { "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", + "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", "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\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 removed but no encounter is created\n And routine completions leave no trace — keeping the state clean\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", diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index 1493133..6970a4d 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -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.", @@ -215,6 +215,7 @@ const CONCEPT_ORDER: readonly string[] = [ "task", // Organization "charter", + // Position "position", "duty", ]; diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index e7f2bb5..7ea3a47 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -12,7 +12,8 @@ * 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, ...) + * org — organization management (found, charter, dissolve, hire, fire) + * position — position management (establish, abolish, charge, appoint, dismiss) * resource — ResourceX instance (optional) */ @@ -59,6 +60,8 @@ export class Rolex { readonly role: RoleNamespace; /** Organization management — structure + membership. */ readonly org: OrgNamespace; + /** Position management — establish, charge, appoint. */ + readonly position: PositionNamespace; /** Resource management (optional — powered by ResourceX). */ readonly resource?: ResourceX; @@ -96,6 +99,7 @@ export class Rolex { persistContext ); this.org = new OrgNamespace(this.rt, this.society, this.past, resolve); + this.position = new PositionNamespace(this.rt, this.society, this.past, resolve); this.resource = platform.resourcex; } @@ -513,13 +517,6 @@ class OrgNamespace { 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); @@ -527,13 +524,6 @@ class OrgNamespace { 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. */ @@ -541,12 +531,7 @@ class OrgNamespace { 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 ---- + // ---- Membership ---- /** Hire: link individual to organization via membership. */ hire(org: string, individual: string): RolexResult { @@ -561,6 +546,44 @@ class OrgNamespace { this.rt.unlink(orgNode, this.resolve(individual), "membership", "belong"); return ok(this.rt, orgNode, "fire"); } +} + +// ================================================================ +// Position — position management +// ================================================================ + +class PositionNamespace { + constructor( + private rt: Runtime, + private society: Structure, + private past: Structure, + private resolve: Resolve + ) {} + + // ---- Structure ---- + + /** Establish a position. */ + establish(position?: string, id?: string, alias?: readonly string[]): RolexResult { + validateGherkin(position); + const pos = this.rt.create(this.society, C.position, position, id, alias); + return ok(this.rt, pos, "establish"); + } + + /** 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 ---- + + /** Abolish a position. */ + abolish(position: string): RolexResult { + return archive(this.rt, this.past, this.resolve(position), "abolish"); + } + + // ---- Appointment ---- /** Appoint: link individual to position via appointment. */ appoint(position: string, individual: string): RolexResult { diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index b8d074c..a0c0a14 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -31,10 +31,9 @@ describe("Rolex API (stateless)", () => { expect(r.process).toBe("found"); }); - test("establish creates a position under org", () => { + test("establish creates a position", () => { const rolex = setup(); - rolex.org.found(undefined, "org1"); - const r = rolex.org.establish("org1", "Feature: Backend architect", "pos1"); + const r = rolex.position.establish("Feature: Backend architect", "pos1"); expect(r.state.name).toBe("position"); }); @@ -48,9 +47,8 @@ describe("Rolex API (stateless)", () => { test("charge adds duty to position", () => { const rolex = setup(); - rolex.org.found(undefined, "org1"); - rolex.org.establish("org1", undefined, "pos1"); - const r = rolex.org.charge("pos1", "Feature: Design systems"); + rolex.position.establish(undefined, "pos1"); + const r = rolex.position.charge("pos1", "Feature: Design systems"); expect(r.state.name).toBe("duty"); }); }); @@ -91,9 +89,8 @@ describe("Rolex API (stateless)", () => { test("abolish archives position", () => { const rolex = setup(); - rolex.org.found(undefined, "org1"); - rolex.org.establish("org1", undefined, "pos1"); - rolex.org.abolish("pos1"); + rolex.position.establish(undefined, "pos1"); + rolex.position.abolish("pos1"); const found = rolex.find("pos1"); expect(found).not.toBeNull(); expect(found!.name).toBe("past"); @@ -138,9 +135,8 @@ describe("Rolex API (stateless)", () => { test("appoint links individual to position", () => { const rolex = setup(); rolex.individual.born(undefined, "sean"); - rolex.org.found(undefined, "org1"); - rolex.org.establish("org1", undefined, "pos1"); - const r = rolex.org.appoint("pos1", "sean"); + rolex.position.establish(undefined, "pos1"); + const r = rolex.position.appoint("pos1", "sean"); expect(r.state.links).toHaveLength(1); expect(r.state.links![0].relation).toBe("appointment"); }); @@ -148,10 +144,9 @@ describe("Rolex API (stateless)", () => { test("dismiss removes appointment", () => { const rolex = setup(); rolex.individual.born(undefined, "sean"); - rolex.org.found(undefined, "org1"); - rolex.org.establish("org1", undefined, "pos1"); - rolex.org.appoint("pos1", "sean"); - const r = rolex.org.dismiss("pos1", "sean"); + rolex.position.establish(undefined, "pos1"); + rolex.position.appoint("pos1", "sean"); + const r = rolex.position.dismiss("pos1", "sean"); expect(r.state.links).toBeUndefined(); }); }); @@ -358,18 +353,18 @@ describe("Rolex API (stateless)", () => { // Create world rolex.individual.born("Feature: I am Sean", "sean"); rolex.org.found("Feature: Deepractice", "dp"); - rolex.org.establish("dp", "Feature: Architect", "architect"); + rolex.position.establish("Feature: Architect", "architect"); rolex.org.charter("dp", "Feature: Build great AI"); - rolex.org.charge("architect", "Feature: Design systems"); + rolex.position.charge("architect", "Feature: Design systems"); - // Organization + // Organization + Position rolex.org.hire("dp", "sean"); - rolex.org.appoint("architect", "sean"); + rolex.position.appoint("architect", "sean"); // Verify links const orgState = rolex.find("dp")!; expect(orgState.links).toHaveLength(1); - const posState = (orgState as any).children!.find((c: any) => c.name === "position")!; + const posState = rolex.find("architect")!; expect(posState.links).toHaveLength(1); // Execution cycle @@ -729,8 +724,7 @@ describe("Rolex API (stateless)", () => { test("establish with id", () => { const rolex = setup(); - rolex.org.found("Feature: Deepractice", "dp"); - const r = rolex.org.establish("dp", "Feature: Architect", "architect"); + const r = rolex.position.establish("Feature: Architect", "architect"); expect(r.state.id).toBe("architect"); }); }); From d3909b61943ec7e8aa68c7c2c2f682a372bf7d68 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 16:07:32 +0800 Subject: [PATCH 10/54] feat: unified `use` entry point with ! command protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote `use` from RoleNamespace to Rolex class as the unified execution entry point. `!namespace.method` locators dispatch to RoleX runtime (org, position, individual); regular locators delegate to ResourceX. Add process and world descriptions. Remove prototypes/ directory — moved to DeepracticeX repo. Co-Authored-By: Claude Opus 4.6 --- packages/rolexjs/src/descriptions/index.ts | 2 + packages/rolexjs/src/descriptions/use.feature | 24 ++ .../descriptions/world-use-protocol.feature | 38 ++ packages/rolexjs/src/rolex.ts | 108 ++++- packages/rolexjs/tests/rolex.test.ts | 88 ++++- .../roles/nuwa/background.background.feature | 16 - prototypes/roles/nuwa/individual.json | 15 - prototypes/roles/nuwa/nuwa.individual.feature | 12 - prototypes/roles/nuwa/resource.json | 7 - .../skills/individual-management/SKILL.md | 214 ---------- .../individual-management/resource.json | 7 - .../skills/resource-management/SKILL.md | 370 ------------------ .../skills/resource-management/resource.json | 7 - prototypes/skills/skill-creator/SKILL.md | 110 ------ prototypes/skills/skill-creator/resource.json | 8 - 15 files changed, 252 insertions(+), 774 deletions(-) create mode 100644 packages/rolexjs/src/descriptions/use.feature create mode 100644 packages/rolexjs/src/descriptions/world-use-protocol.feature delete mode 100644 prototypes/roles/nuwa/background.background.feature delete mode 100644 prototypes/roles/nuwa/individual.json delete mode 100644 prototypes/roles/nuwa/nuwa.individual.feature delete mode 100644 prototypes/roles/nuwa/resource.json delete mode 100644 prototypes/skills/individual-management/SKILL.md delete mode 100644 prototypes/skills/individual-management/resource.json delete mode 100644 prototypes/skills/resource-management/SKILL.md delete mode 100644 prototypes/skills/resource-management/resource.json delete mode 100644 prototypes/skills/skill-creator/SKILL.md delete mode 100644 prototypes/skills/skill-creator/resource.json diff --git a/packages/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts index ebd5a9e..92b53ad 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -29,6 +29,7 @@ export const processes: Record = { "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 — 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", + "use": "Feature: use — unified execution entry point\n Execute any RoleX command or load any ResourceX resource through a single entry point.\n The locator determines the dispatch path:\n - `!namespace.method` dispatches to the RoleX runtime\n - Any other locator delegates to ResourceX\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 And the result is returned\n\n Scenario: Available namespaces\n Given the `!` prefix routes to RoleX namespaces\n Then `!individual.*` routes to individual lifecycle and injection\n And `!org.*` routes to organization management\n And `!position.*` routes to position management\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; @@ -42,4 +43,5 @@ export const world: Record = { "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", + "use-protocol": "Feature: Use protocol — unified execution through ! commands\n The use tool is the single entry point for all execution.\n A ! prefix signals a RoleX runtime command; everything else is a ResourceX resource.\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 And the result is returned directly\n\n Scenario: Available ! commands\n Given ! routes to RoleX namespaces\n Then !individual.born creates an individual\n And !individual.teach injects a principle\n And !individual.train injects a procedure\n And !org.found creates an organization\n And !org.charter defines a charter\n And !org.hire adds a member\n And !org.fire removes a member\n And !org.dissolve dissolves an organization\n And !position.establish creates a position\n And !position.charge adds a duty\n And !position.appoint assigns an individual\n And !position.dismiss removes an individual\n And !position.abolish abolishes a position\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\n And the resource content is returned\n\n Scenario: Why ! matters\n Given RoleX has runtime state that ResourceX cannot access\n And ResourceX is designed for serverless stateless execution\n When ! provides a clear boundary between runtime and resource\n Then runtime commands execute with full state access\n And resource operations remain stateless and sandboxable", } as const; diff --git a/packages/rolexjs/src/descriptions/use.feature b/packages/rolexjs/src/descriptions/use.feature new file mode 100644 index 0000000..91a0e3d --- /dev/null +++ b/packages/rolexjs/src/descriptions/use.feature @@ -0,0 +1,24 @@ +Feature: use — unified execution entry point + Execute any RoleX command or load any ResourceX resource through a single entry point. + The locator determines the dispatch path: + - `!namespace.method` dispatches to the RoleX runtime + - Any other locator delegates to ResourceX + + 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 + And the result is returned + + Scenario: Available namespaces + Given the `!` prefix routes to RoleX namespaces + Then `!individual.*` routes to individual lifecycle and injection + And `!org.*` routes to organization management + And `!position.*` routes to position management + + 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/world-use-protocol.feature b/packages/rolexjs/src/descriptions/world-use-protocol.feature new file mode 100644 index 0000000..29b15b5 --- /dev/null +++ b/packages/rolexjs/src/descriptions/world-use-protocol.feature @@ -0,0 +1,38 @@ +Feature: Use protocol — unified execution through ! commands + The use tool is the single entry point for all execution. + A ! prefix signals a RoleX runtime command; everything else is a ResourceX resource. + + 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 + And the result is returned directly + + Scenario: Available ! commands + Given ! routes to RoleX namespaces + Then !individual.born creates an individual + And !individual.teach injects a principle + And !individual.train injects a procedure + And !org.found creates an organization + And !org.charter defines a charter + And !org.hire adds a member + And !org.fire removes a member + And !org.dissolve dissolves an organization + And !position.establish creates a position + And !position.charge adds a duty + And !position.appoint assigns an individual + And !position.dismiss removes an individual + And !position.abolish abolishes a position + + 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 + And the resource content is returned + + Scenario: Why ! matters + Given RoleX has runtime state that ResourceX cannot access + And ResourceX is designed for serverless stateless execution + When ! provides a clear boundary between runtime and resource + Then runtime commands execute with full state access + And resource operations remain stateless and sandboxable diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 7ea3a47..b1ecc89 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -11,10 +11,13 @@ * * Namespaces: * individual — lifecycle (born, retire, die, rehire) + external injection (teach, train) - * role — execution + cognition + use (activate → complete, reflect → master, use) + * role — execution + cognition (activate → complete, reflect → master, skill) * org — organization management (found, charter, dissolve, hire, fire) * position — position management (establish, abolish, charge, appoint, dismiss) * resource — ResourceX instance (optional) + * + * Unified entry point: + * use(locator, args) — `!ns.method` dispatches to runtime, else delegates to ResourceX */ import type { ContextData, Platform } from "@rolexjs/core"; @@ -120,6 +123,103 @@ export class Rolex { const state = this.rt.project(this.society); return findInState(state, target); } + + /** + * Unified execution entry point. + * + * - `!namespace.method` — dispatch to RoleX runtime (e.g. `!org.found`, `!position.establish`) + * - anything else — delegate to ResourceX `ingest` + */ + async use(locator: string, args?: Record): Promise { + if (locator.startsWith("!")) { + return this.dispatch(locator.slice(1), args ?? {}); + } + if (!this.resourcex) throw new Error("ResourceX is not available."); + return this.resourcex.ingest(locator, args); + } + + /** Dispatch a `!namespace.method` command to the corresponding API. */ + private dispatch(command: string, args: Record): T { + const dot = command.indexOf("."); + if (dot < 0) throw new Error(`Invalid command "${command}". Expected "namespace.method".`); + const ns = command.slice(0, dot); + const method = command.slice(dot + 1); + + const namespace = this.resolveNamespace(ns); + const fn = (namespace as Record)[method]; + if (typeof fn !== "function") { + throw new Error(`Unknown command "!${command}".`); + } + + return fn.call(namespace, ...this.toArgs(ns, method, args)); + } + + private resolveNamespace(ns: string): object { + switch (ns) { + case "individual": + return this.individual; + case "role": + return this.role; + case "org": + return this.org; + case "position": + return this.position; + default: + throw new Error(`Unknown namespace "${ns}".`); + } + } + + /** + * Map named args to positional args for each namespace.method. + * Keeps dispatch table centralized — one place to maintain. + */ + private toArgs(ns: string, method: string, a: Record): unknown[] { + const key = `${ns}.${method}`; + + // prettier-ignore + switch (key) { + // individual + case "individual.born": + return [a.content, a.id, a.alias]; + case "individual.retire": + return [a.individual]; + case "individual.die": + return [a.individual]; + case "individual.rehire": + return [a.individual]; + case "individual.teach": + return [a.individual, a.content, a.id]; + case "individual.train": + return [a.individual, a.content, a.id]; + + // org + case "org.found": + return [a.content, a.id, a.alias]; + case "org.charter": + return [a.org, a.content]; + case "org.dissolve": + return [a.org]; + case "org.hire": + return [a.org, a.individual]; + case "org.fire": + return [a.org, a.individual]; + + // position + case "position.establish": + return [a.content, a.id, a.alias]; + case "position.charge": + return [a.position, a.content, a.id]; + case "position.abolish": + return [a.position]; + case "position.appoint": + return [a.position, a.individual]; + case "position.dismiss": + return [a.position, a.individual]; + + default: + throw new Error(`No arg mapping for "!${key}".`); + } + } } // ================================================================ @@ -488,12 +588,6 @@ class RoleNamespace { return text; } } - - /** 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); - } } // ================================================================ diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index a0c0a14..0c55ee0 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { localPlatform } from "@rolexjs/local-platform"; import { describe as renderDescribe, hint as renderHint, renderState } from "../src/render.js"; -import { createRoleX } from "../src/rolex.js"; +import { createRoleX, type RolexResult } from "../src/rolex.js"; function setup() { return createRoleX(localPlatform({ dataDir: null })); @@ -728,4 +728,90 @@ describe("Rolex API (stateless)", () => { expect(r.state.id).toBe("architect"); }); }); + + // ============================================================ + // use — unified execution entry point + // ============================================================ + + describe("use: ! command dispatch", () => { + test("!org.found creates organization", async () => { + const rolex = setup(); + const r = await rolex.use("!org.found", { + content: "Feature: Deepractice", + id: "dp", + }); + expect(r.state.name).toBe("organization"); + expect(r.state.id).toBe("dp"); + }); + + test("!position.establish creates position", async () => { + const rolex = setup(); + const r = await rolex.use("!position.establish", { + content: "Feature: Architect", + id: "architect", + }); + expect(r.state.name).toBe("position"); + expect(r.state.id).toBe("architect"); + }); + + test("!org.hire links individual to org", async () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.org.found(undefined, "dp"); + const r = await rolex.use("!org.hire", { + org: "dp", + individual: "sean", + }); + expect(r.state.links).toHaveLength(1); + expect(r.state.links![0].relation).toBe("membership"); + }); + + test("!position.appoint links individual to position", async () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.position.establish(undefined, "pos1"); + const r = await rolex.use("!position.appoint", { + position: "pos1", + individual: "sean", + }); + expect(r.state.links).toHaveLength(1); + expect(r.state.links![0].relation).toBe("appointment"); + }); + + test("!individual.born creates individual", async () => { + const rolex = setup(); + const r = await rolex.use("!individual.born", { + content: "Feature: Alice", + id: "alice", + }); + expect(r.state.name).toBe("individual"); + expect(r.state.id).toBe("alice"); + }); + + test("!individual.teach injects principle", async () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + const r = await rolex.use("!individual.teach", { + individual: "sean", + content: "Feature: Always test first", + id: "test-first", + }); + expect(r.state.name).toBe("principle"); + }); + + test("throws on unknown namespace", async () => { + const rolex = setup(); + expect(() => rolex.use("!foo.bar")).toThrow('Unknown namespace "foo"'); + }); + + test("throws on unknown method", async () => { + const rolex = setup(); + expect(() => rolex.use("!org.nope")).toThrow('Unknown command "!org.nope"'); + }); + + test("throws on missing dot", async () => { + const rolex = setup(); + expect(() => rolex.use("!orgfound")).toThrow("Expected"); + }); + }); }); 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/individual-management/SKILL.md b/prototypes/skills/individual-management/SKILL.md deleted file mode 100644 index 792ee8c..0000000 --- a/prototypes/skills/individual-management/SKILL.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -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. ---- - -Feature: Individual Lifecycle - Manage the full lifecycle of individuals in the RoleX world. - Individuals are persistent entities that hold identity, knowledge, goals, and experience. - - 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 - 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" - """ - - Scenario: born — persona writing guidelines - Given the persona defines who this individual is - Then the Feature title names the individual - And the description captures personality, values, expertise, and background - And Scenarios describe distinct aspects of the persona - And keep it concise — identity is loaded at every activation - - Scenario: retire — archive an individual - Given an individual should be temporarily deactivated - When you call retire with the individual id - 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) - """ - - Scenario: die — permanently remove an individual - Given an individual should be permanently removed - When you call die with the individual id - 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) - """ - - Scenario: retire vs die — when to use which - Given you need to remove an individual from active duty - When the individual may return later (sabbatical, role rotation) - Then use retire — it signals intent to restore - When the individual is no longer needed (deprecated role, replaced) - Then use die — it signals finality - - Scenario: rehire — restore a retired individual - Given a retired individual needs to come back - When you call rehire with the past node id - 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) - """ - -Feature: Knowledge Injection - Inject principles and procedures into an individual from the outside. - This bypasses the cognition cycle — no encounters or experiences consumed. - Use this to equip individuals with pre-existing knowledge and skills. - - 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 - 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" - """ - - 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 - 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" - """ - - Scenario: teach vs realize — when to use which - Given you need to add a principle to an individual - When the principle comes from external knowledge (documentation, best practices, user instruction) - Then use teach — inject directly, no experience needed - When the individual discovered the principle through its own work - Then use realize — it consumes experience and produces the principle organically - - Scenario: train vs master — when to use which - Given you need to add a procedure to an individual - When the skill already exists (published skill, known capability) - Then use train — inject directly with a locator reference - When the individual developed the skill through its own experience - Then use master — it consumes experience and produces the procedure organically - - Scenario: Principle writing guidelines - Given a principle is a transferable truth - Then the Feature title states the rule as a general statement - And Scenarios describe different situations where this principle applies - And the tone is universal — no mention of specific projects or people - And the id is keywords joined by hyphens (e.g. "always-validate-input") - - Scenario: Procedure writing guidelines - Given a procedure is a skill reference pointing to full instructions - Then the Feature title names the capability - And the Feature description line MUST be the ResourceX locator - And the locator can be a GitHub URL, local path, or registry identifier - And Scenarios summarize when and why to use this skill - And the id is keywords joined by hyphens (e.g. "skill-creator") - -Feature: Common Workflows - Typical sequences of operations for managing individuals. - - Scenario: Bootstrap a new role - Given you need to create a fully equipped individual - 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 - """ - - Scenario: Transfer knowledge between individuals - Given individual A has a useful principle or procedure - When individual B needs the same knowledge - Then teach/train the same content to individual B - And use the same id to maintain consistency across individuals - - Scenario: Update existing knowledge - Given an individual's principle or procedure is outdated - When the content needs to change but the concept is the same - Then teach/train with the same id — upsert replaces the old version - And the individual retains the same id with updated content - - 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 - And only instance nodes can be forgotten — prototype nodes are read-only diff --git a/prototypes/skills/individual-management/resource.json b/prototypes/skills/individual-management/resource.json deleted file mode 100644 index 29c01aa..0000000 --- a/prototypes/skills/individual-management/resource.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "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/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/resource-management/resource.json b/prototypes/skills/resource-management/resource.json deleted file mode 100644 index 002ce35..0000000 --- a/prototypes/skills/resource-management/resource.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "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/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/skill-creator/resource.json b/prototypes/skills/skill-creator/resource.json deleted file mode 100644 index 2396ea4..0000000 --- a/prototypes/skills/skill-creator/resource.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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"] -} From 70c1de2434b5524a975058fa7fcbda49ec0b28eb Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 16:59:33 +0800 Subject: [PATCH 11/54] =?UTF-8?q?feat:=20prototype=20management=20?= =?UTF-8?q?=E2=80=94=20summon/banish/list=20with=20builtin=20nuwa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade Prototype interface: resolve, summon, banish, list. Add PrototypeNamespace on Rolex (rolex.proto.summon/banish/list). Activate auto-borns individual when prototype exists but runtime doesn't. Builtin nuwa prototype points to DeepracticeX GitHub URL. Remove registerPrototype from Platform, seeds/scanSeeds mechanism. CLI subcommands use full names (organization, position, prototype). Co-Authored-By: Claude Opus 4.6 --- apps/cli/src/index.ts | 59 ++++++++++--- apps/mcp-server/src/index.ts | 3 - packages/core/src/platform.ts | 3 - packages/local-platform/src/LocalPlatform.ts | 34 ++++++-- .../local-platform/tests/prototype.test.ts | 69 +++++++++------ packages/rolexjs/src/rolex.ts | 84 +++++++++++++++---- packages/system/src/prototype.ts | 39 +++++++-- packages/system/tests/prototype.test.ts | 43 +++++++--- 8 files changed, 255 insertions(+), 79 deletions(-) diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index c200fd6..7026f07 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -340,7 +340,7 @@ const use = defineCommand({ }, }, async run({ args }) { - const result = await rolex.role.use(args.locator); + const result = await rolex.use(args.locator); if (typeof result === "string") { console.log(result); } else if (result instanceof Uint8Array) { @@ -488,7 +488,7 @@ const dismiss = defineCommand({ }); const org = defineCommand({ - meta: { name: "org", description: "Organization management" }, + meta: { name: "organization", description: "Organization management" }, subCommands: { found, charter, @@ -499,7 +499,7 @@ const org = defineCommand({ }); const pos = defineCommand({ - meta: { name: "pos", description: "Position management" }, + meta: { name: "position", description: "Position management" }, subCommands: { establish, charge, @@ -688,10 +688,10 @@ const resource = defineCommand({ }, }); -// ========== Prototype — register ResourceX source ========== +// ========== Prototype — summon, banish, list ========== -const prototype = defineCommand({ - meta: { name: "prototype", description: "Register a ResourceX source as a prototype" }, +const protoSummon = defineCommand({ + meta: { name: "summon", description: "Summon a prototype from a ResourceX source" }, args: { source: { type: "positional" as const, @@ -700,11 +700,50 @@ const prototype = defineCommand({ }, }, async run({ args }) { - const result = await rolex.prototype(args.source); + const result = await rolex.proto.summon(args.source); output(result, result.state.id ?? args.source); }, }); +const protoBanish = defineCommand({ + meta: { name: "banish", description: "Banish a prototype by id" }, + args: { + id: { + type: "positional" as const, + description: "Prototype id to banish", + required: true, + }, + }, + run({ args }) { + const result = rolex.proto.banish(args.id); + output(result, args.id); + }, +}); + +const protoList = defineCommand({ + meta: { name: "list", description: "List all registered prototypes" }, + run() { + const list = rolex.proto.list(); + const entries = Object.entries(list); + if (entries.length === 0) { + console.log("No prototypes registered."); + return; + } + for (const [id, source] of entries) { + console.log(`${id} → ${source}`); + } + }, +}); + +const proto = defineCommand({ + meta: { name: "prototype", description: "Prototype management — summon, banish, list" }, + subCommands: { + summon: protoSummon, + banish: protoBanish, + list: protoList, + }, +}); + // ========== Main ========== const main = defineCommand({ @@ -716,10 +755,10 @@ const main = defineCommand({ subCommands: { individual, role, - org, - pos, + organization: org, + position: pos, resource, - prototype, + prototype: proto, }, }); diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 7b11bb6..499637c 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -51,9 +51,6 @@ server.addTool({ roleId: z.string().describe("Role name to activate"), }), execute: async ({ roleId }) => { - if (!state.findIndividual(roleId)) { - rolex.individual.born(undefined, roleId); - } const result = await rolex.role.activate(roleId); state.ctx = result.ctx!; const ctx = result.ctx!; diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index d5e0752..f556586 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -29,9 +29,6 @@ export interface Platform { /** Resource management capability (optional — requires resourcexjs). */ readonly resourcex?: ResourceX; - /** Register a prototype: bind id to a ResourceX source (path or locator). */ - registerPrototype?(id: string, source: string): void; - /** Save role context to persistent storage. */ saveContext?(roleId: string, data: ContextData): void; diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index 52598a2..884f9eb 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -445,17 +445,23 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { // ===== Prototype registry ===== + /** Built-in prototypes — always available, overridable by user registry. */ + const BUILTINS: Record = { + nuwa: "https://github.com/Deepractice/DeepracticeX/tree/main/roles/nuwa", + }; + const registryPath = dataDir ? join(dataDir, "prototype.json") : undefined; const readRegistry = (): Record => { - if (!registryPath || !existsSync(registryPath)) return {}; - return JSON.parse(readFileSync(registryPath, "utf-8")); + const registry = { ...BUILTINS }; + if (registryPath && existsSync(registryPath)) { + Object.assign(registry, JSON.parse(readFileSync(registryPath, "utf-8"))); + } + return registry; }; - const registerPrototype = (id: string, source: string): void => { + const writeRegistry = (registry: Record): void => { if (!registryPath) return; - const registry = readRegistry(); - registry[id] = source; mkdirSync(dataDir!, { recursive: true }); writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf-8"); }; @@ -472,6 +478,22 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { return undefined; } }, + + summon(id, source) { + const registry = readRegistry(); + registry[id] = source; + writeRegistry(registry); + }, + + banish(id) { + const registry = readRegistry(); + delete registry[id]; + writeRegistry(registry); + }, + + list() { + return readRegistry(); + }, }; // ===== Context persistence ===== @@ -491,5 +513,5 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { return JSON.parse(readFileSync(contextPath, "utf-8")); }; - return { runtime, prototype, resourcex, registerPrototype, saveContext, loadContext }; + return { runtime, prototype, resourcex, saveContext, loadContext }; } diff --git a/packages/local-platform/tests/prototype.test.ts b/packages/local-platform/tests/prototype.test.ts index 7db104f..c1cf4fd 100644 --- a/packages/local-platform/tests/prototype.test.ts +++ b/packages/local-platform/tests/prototype.test.ts @@ -23,7 +23,6 @@ function writePrototype( const dir = join(baseDir, id); mkdirSync(dir, { recursive: true }); - // resource.json — ResourceX source marker writeFileSync( join(dir, "resource.json"), JSON.stringify({ @@ -36,7 +35,6 @@ function writePrototype( "utf-8" ); - // manifest writeFileSync( join(dir, manifestFile), JSON.stringify({ @@ -47,7 +45,6 @@ function writePrototype( "utf-8" ); - // feature files for (const [name, content] of Object.entries(features)) { writeFileSync(join(dir, name), content, "utf-8"); } @@ -56,7 +53,7 @@ function writePrototype( } describe("LocalPlatform Prototype (registry-based)", () => { - test("resolve returns undefined when nothing registered", async () => { + test("resolve returns undefined for unknown id", async () => { const { prototype } = localPlatform({ dataDir: testDir, resourceDir }); expect(await prototype!.resolve("unknown")).toBeUndefined(); }); @@ -71,32 +68,32 @@ describe("LocalPlatform Prototype (registry-based)", () => { expect(await prototype!.resolve("sean")).toBeUndefined(); }); - test("registerPrototype + resolve round-trip for role", async () => { + test("summon + 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 dir = writePrototype(protoDir, "test-role", "role", { + "test-role.individual.feature": "Feature: TestRole\n Test role.", }); const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.registerPrototype!("nuwa", dir); + platform.prototype!.summon("test-role", dir); - const state = await platform.prototype!.resolve("nuwa"); + const state = await platform.prototype!.resolve("test-role"); expect(state).toBeDefined(); - expect(state!.id).toBe("nuwa"); + expect(state!.id).toBe("test-role"); expect(state!.name).toBe("individual"); - expect(state!.information).toBe("Feature: Nuwa\n World admin."); + expect(state!.information).toBe("Feature: TestRole\n Test role."); expect(state!.children).toHaveLength(1); expect(state!.children![0].name).toBe("identity"); }); - test("registerPrototype + resolve round-trip for organization", async () => { + test("summon + 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); + platform.prototype!.summon("deepractice", dir); const state = await platform.prototype!.resolve("deepractice"); expect(state).toBeDefined(); @@ -107,15 +104,15 @@ describe("LocalPlatform Prototype (registry-based)", () => { test("resolve returns undefined for unregistered id", async () => { const protoDir = join(testDir, "protos"); - const dir = writePrototype(protoDir, "nuwa", "role"); + const dir = writePrototype(protoDir, "test-role", "role"); const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.registerPrototype!("nuwa", dir); + platform.prototype!.summon("test-role", dir); expect(await platform.prototype!.resolve("nobody")).toBeUndefined(); }); - test("registerPrototype overwrites previous source", async () => { + test("summon overwrites previous source", async () => { const protoDir = join(testDir, "protos"); const dir1 = writePrototype(protoDir, "v1", "role", { "v1.individual.feature": "Feature: V1", @@ -125,25 +122,49 @@ describe("LocalPlatform Prototype (registry-based)", () => { }); const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.registerPrototype!("test", dir1); - platform.registerPrototype!("test", dir2); + platform.prototype!.summon("test", dir1); + platform.prototype!.summon("test", dir2); const state = await platform.prototype!.resolve("test"); expect(state!.id).toBe("v2"); }); + test("banish removes user-registered prototype", async () => { + const protoDir = join(testDir, "protos"); + const dir = writePrototype(protoDir, "temp", "role"); + + const platform = localPlatform({ dataDir: testDir, resourceDir }); + platform.prototype!.summon("temp", dir); + expect(await platform.prototype!.resolve("temp")).toBeDefined(); + + platform.prototype!.banish("temp"); + expect(await platform.prototype!.resolve("temp")).toBeUndefined(); + }); + test("registry persists across platform instances", async () => { const protoDir = join(testDir, "protos"); - const dir = writePrototype(protoDir, "nuwa", "role"); + const dir = writePrototype(protoDir, "test-role", "role"); - // First instance: register const p1 = localPlatform({ dataDir: testDir, resourceDir }); - p1.registerPrototype!("nuwa", dir); + p1.prototype!.summon("test-role", dir); - // Second instance: resolve (reads same prototype.json) const p2 = localPlatform({ dataDir: testDir, resourceDir }); - const state = await p2.prototype!.resolve("nuwa"); + const state = await p2.prototype!.resolve("test-role"); expect(state).toBeDefined(); - expect(state!.id).toBe("nuwa"); + expect(state!.id).toBe("test-role"); + }); + + test("list includes builtins and user-registered prototypes", () => { + const platform = localPlatform({ dataDir: testDir, resourceDir }); + platform.prototype!.summon("custom", "/path/to/custom"); + const list = platform.prototype!.list(); + expect(list.nuwa).toBeDefined(); // builtin + expect(list.custom).toBe("/path/to/custom"); // user-registered + }); + + test("builtin nuwa is always present in list", () => { + const platform = localPlatform({ dataDir: testDir, resourceDir }); + const list = platform.prototype!.list(); + expect(list.nuwa).toBe("https://github.com/Deepractice/DeepracticeX/tree/main/roles/nuwa"); }); }); diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index b1ecc89..3de8170 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -50,7 +50,6 @@ type Resolve = (id: string) => Structure; export class Rolex { private rt: Runtime; private resourcex?: ResourceX; - private _registerPrototype?: (id: string, source: string) => void; /** Root of the world. */ readonly society: Structure; @@ -65,13 +64,14 @@ export class Rolex { readonly org: OrgNamespace; /** Position management — establish, charge, appoint. */ readonly position: PositionNamespace; + /** Prototype management — summon, banish, list. */ + readonly proto: PrototypeNamespace; /** 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(); @@ -94,29 +94,26 @@ export class Rolex { platform.saveContext && platform.loadContext ? { save: platform.saveContext, load: platform.loadContext } : undefined; + const tryFind = (id: string) => this.find(id); + const born = (id: string) => { + this.individual.born(undefined, id); + return this.find(id)!; + }; this.role = new RoleNamespace( this.rt, resolve, + tryFind, + born, platform.prototype, platform.resourcex, persistContext ); this.org = new OrgNamespace(this.rt, this.society, this.past, resolve); this.position = new PositionNamespace(this.rt, this.society, this.past, resolve); + this.proto = new PrototypeNamespace(platform.prototype, platform.resourcex); 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(); @@ -164,6 +161,8 @@ export class Rolex { return this.org; case "position": return this.position; + case "prototype": + return this.proto; default: throw new Error(`Unknown namespace "${ns}".`); } @@ -216,6 +215,12 @@ export class Rolex { case "position.dismiss": return [a.position, a.individual]; + // prototype + case "prototype.summon": + return [a.source]; + case "prototype.banish": + return [a.id]; + default: throw new Error(`No arg mapping for "!${key}".`); } @@ -299,6 +304,8 @@ class RoleNamespace { constructor( private rt: Runtime, private resolve: Resolve, + private tryFind: (id: string) => Structure | null, + private born: (id: string) => Structure, private prototype?: Prototype, private resourcex?: ResourceX, private persistContext?: { @@ -317,9 +324,23 @@ class RoleNamespace { // ---- Activation ---- - /** Activate: merge prototype (if any) with instance state. Returns ctx when created. */ + /** + * Activate: merge prototype (if any) with instance state. + * + * If the individual does not exist in runtime but a prototype is registered, + * auto-born the individual first, then proceed with normal activation. + */ async activate(individual: string): Promise { - const node = this.resolve(individual); + let node = this.tryFind(individual); + if (!node) { + // Not in runtime — check prototype registry + const hasProto = this.prototype ? Object.hasOwn(this.prototype.list(), individual) : false; + if (hasProto) { + node = this.born(individual); + } else { + throw new Error(`"${individual}" not found.`); + } + } const instanceState = this.rt.project(node); const protoState = instanceState.id ? await this.prototype?.resolve(instanceState.id) @@ -694,6 +715,39 @@ class PositionNamespace { } } +// ================================================================ +// Prototype — summon, banish, list +// ================================================================ + +class PrototypeNamespace { + constructor( + private prototype?: Prototype, + private resourcex?: ResourceX + ) {} + + /** Summon: pull a prototype from source, register it. */ + async summon(source: string): Promise { + if (!this.resourcex) throw new Error("ResourceX is not available."); + if (!this.prototype) throw new Error("Platform does not support prototypes."); + const state = await this.resourcex.ingest(source); + if (!state.id) throw new Error("Prototype resource must have an id."); + this.prototype.summon(state.id, source); + return { state, process: "summon" }; + } + + /** Banish: unregister a prototype by id. */ + banish(id: string): RolexResult { + if (!this.prototype) throw new Error("Platform does not support prototypes."); + this.prototype.banish(id); + return { state: { name: id, description: "", parent: null }, process: "banish" }; + } + + /** List all registered prototypes. */ + list(): Record { + return this.prototype?.list() ?? {}; + } +} + // ================================================================ // Shared helpers // ================================================================ diff --git a/packages/system/src/prototype.ts b/packages/system/src/prototype.ts index 4ceced1..d58bb0d 100644 --- a/packages/system/src/prototype.ts +++ b/packages/system/src/prototype.ts @@ -14,31 +14,54 @@ import type { State } from "./process.js"; // ===== Prototype interface ===== -/** A source that resolves prototype State trees by id. */ +/** A source that manages and 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; + + /** Summon: register a prototype — bind id to a source (path or locator). */ + summon(id: string, source: string): void; + + /** Banish: unregister a prototype by id. */ + banish(id: string): void; + + /** List all registered prototypes: id → source mapping. */ + list(): Record; } // ===== In-memory implementation ===== -/** Create an in-memory prototype source. */ +/** Create an in-memory prototype (for tests). */ export const createPrototype = (): Prototype & { - /** Register a State tree as a prototype (keyed by state.id). */ - register(state: State): void; + /** Seed a State directly for testing (bypasses source resolution). */ + seed(state: State): void; } => { - const prototypes = new Map(); + const states = new Map(); + const sources = new Map(); return { async resolve(id) { - return prototypes.get(id); + return states.get(id); + }, + + summon(id, source) { + sources.set(id, source); + }, + + banish(id) { + sources.delete(id); + states.delete(id); + }, + + list() { + return Object.fromEntries(sources); }, - register(state) { + seed(state) { if (!state.id) { throw new Error("Prototype state must have an id"); } - prototypes.set(state.id, state); + states.set(state.id, state); }, }; }; diff --git a/packages/system/tests/prototype.test.ts b/packages/system/tests/prototype.test.ts index 713dc34..313d29a 100644 --- a/packages/system/tests/prototype.test.ts +++ b/packages/system/tests/prototype.test.ts @@ -15,12 +15,12 @@ const state = ( }); describe("Prototype", () => { - test("resolve returns undefined when no prototype registered", async () => { + test("resolve returns undefined when no prototype seeded", async () => { const proto = createPrototype(); expect(await proto.resolve("sean")).toBeUndefined(); }); - test("register and resolve a prototype by id", async () => { + test("seed and resolve a prototype by id", async () => { const proto = createPrototype(); const template = state("individual", { id: "sean", @@ -29,32 +29,55 @@ describe("Prototype", () => { state("knowledge"), ], }); - proto.register(template); + proto.seed(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", () => { + test("seed throws if state has no id", () => { const proto = createPrototype(); const template = state("individual"); - expect(() => proto.register(template)).toThrow("must have an id"); + expect(() => proto.seed(template)).toThrow("must have an id"); }); - test("later registration overwrites earlier one", async () => { + test("later seed overwrites earlier one", async () => { const proto = createPrototype(); - proto.register(state("individual", { id: "sean", information: "v1" })); - proto.register(state("individual", { id: "sean", information: "v2" })); + proto.seed(state("individual", { id: "sean", information: "v1" })); + proto.seed(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" })); + proto.seed(state("individual", { id: "sean", information: "Sean" })); + proto.seed(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(); }); + + test("summon and list round-trip", () => { + const proto = createPrototype(); + proto.summon("nuwa", "/path/to/nuwa"); + proto.summon("sean", "/path/to/sean"); + expect(proto.list()).toEqual({ + nuwa: "/path/to/nuwa", + sean: "/path/to/sean", + }); + }); + + test("banish removes from list", () => { + const proto = createPrototype(); + proto.summon("nuwa", "/path/to/nuwa"); + proto.summon("sean", "/path/to/sean"); + proto.banish("nuwa"); + expect(proto.list()).toEqual({ sean: "/path/to/sean" }); + }); + + test("list returns empty when nothing registered", () => { + const proto = createPrototype(); + expect(proto.list()).toEqual({}); + }); }); From 18431833dba6345eacdc446b73b383ce150775be Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 17:02:27 +0800 Subject: [PATCH 12/54] fix: builtin prototypes cannot be overridden by user registry Summon throws if id conflicts with a builtin. ReadRegistry merges with builtins taking precedence over stale file entries. Co-Authored-By: Claude Opus 4.6 --- packages/local-platform/src/LocalPlatform.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index 884f9eb..454b128 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -445,7 +445,7 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { // ===== Prototype registry ===== - /** Built-in prototypes — always available, overridable by user registry. */ + /** Built-in prototypes — always available, cannot be overridden. */ const BUILTINS: Record = { nuwa: "https://github.com/Deepractice/DeepracticeX/tree/main/roles/nuwa", }; @@ -453,11 +453,11 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { const registryPath = dataDir ? join(dataDir, "prototype.json") : undefined; const readRegistry = (): Record => { - const registry = { ...BUILTINS }; + let fileRegistry: Record = {}; if (registryPath && existsSync(registryPath)) { - Object.assign(registry, JSON.parse(readFileSync(registryPath, "utf-8"))); + fileRegistry = JSON.parse(readFileSync(registryPath, "utf-8")); } - return registry; + return { ...fileRegistry, ...BUILTINS }; }; const writeRegistry = (registry: Record): void => { @@ -480,6 +480,9 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { }, summon(id, source) { + if (id in BUILTINS) { + throw new Error(`"${id}" is a built-in prototype and cannot be overridden.`); + } const registry = readRegistry(); registry[id] = source; writeRegistry(registry); From 3c0016c6f10cf7f6102e0f51972b51e279ef818c Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 17:43:42 +0800 Subject: [PATCH 13/54] feat: add AuthorNamespace for prototype file authoring and reorganize descriptions - Add AuthorNamespace (rolex.author) with born/teach/train for writing prototype files directly to disk (individual.json + .feature files) - Wire author tools through MCP (author-born/teach/train) and CLI (rolex author born/teach/train) - Reorganize description .feature files into namespace subdirectories (world/, role/, individual/, org/, position/, prototype/, author/) - Update gen-descriptions.ts to scan subdirectories - Add Nuwa world description to MCP instructions - Simplify use-protocol to describe the ! routing pattern only - Fix outdated descriptions (reflect, realize, cognitive-priority) - Add prototype descriptions (summon, banish) - 9 new tests for AuthorNamespace (93 total passing) Co-Authored-By: Claude Opus 4.6 --- apps/cli/src/index.ts | 70 +++++++ apps/mcp-server/src/index.ts | 45 +++++ apps/mcp-server/src/instructions.ts | 1 + packages/rolexjs/scripts/gen-descriptions.ts | 56 ++++-- .../descriptions/author/author-born.feature | 17 ++ .../descriptions/author/author-teach.feature | 15 ++ .../descriptions/author/author-train.feature | 16 ++ packages/rolexjs/src/descriptions/index.ts | 42 +++-- .../{ => individual}/born.feature | 0 .../descriptions/{ => individual}/die.feature | 0 .../{ => individual}/rehire.feature | 0 .../{ => individual}/retire.feature | 0 .../{ => individual}/teach.feature | 0 .../{ => individual}/train.feature | 0 .../descriptions/{ => org}/charter.feature | 0 .../descriptions/{ => org}/dissolve.feature | 0 .../src/descriptions/{ => org}/fire.feature | 0 .../src/descriptions/{ => org}/found.feature | 0 .../src/descriptions/{ => org}/hire.feature | 0 .../{ => position}/abolish.feature | 0 .../{ => position}/appoint.feature | 0 .../{ => position}/charge.feature | 0 .../{ => position}/dismiss.feature | 0 .../{ => position}/establish.feature | 0 .../src/descriptions/prototype/banish.feature | 9 + .../src/descriptions/prototype/summon.feature | 10 + .../descriptions/{ => role}/abandon.feature | 0 .../descriptions/{ => role}/activate.feature | 0 .../descriptions/{ => role}/complete.feature | 0 .../descriptions/{ => role}/finish.feature | 0 .../src/descriptions/{ => role}/focus.feature | 0 .../descriptions/{ => role}/forget.feature | 0 .../descriptions/{ => role}/master.feature | 0 .../src/descriptions/{ => role}/plan.feature | 0 .../descriptions/{ => role}/realize.feature | 2 +- .../descriptions/{ => role}/reflect.feature | 4 +- .../src/descriptions/{ => role}/skill.feature | 0 .../src/descriptions/{ => role}/todo.feature | 0 .../src/descriptions/{ => role}/use.feature | 0 .../src/descriptions/{ => role}/want.feature | 0 .../descriptions/world-use-protocol.feature | 38 ---- .../cognition.feature} | 0 .../cognitive-priority.feature} | 6 +- .../communication.feature} | 0 .../execution.feature} | 0 .../gherkin.feature} | 0 .../memory.feature} | 0 .../src/descriptions/world/nuwa.feature | 31 ++++ .../role-identity.feature} | 0 .../skill-system.feature} | 0 .../state-origin.feature} | 0 .../descriptions/world/use-protocol.feature | 20 ++ packages/rolexjs/src/rolex.ts | 110 +++++++++++ packages/rolexjs/tests/author.test.ts | 174 ++++++++++++++++++ 54 files changed, 584 insertions(+), 82 deletions(-) create mode 100644 packages/rolexjs/src/descriptions/author/author-born.feature create mode 100644 packages/rolexjs/src/descriptions/author/author-teach.feature create mode 100644 packages/rolexjs/src/descriptions/author/author-train.feature rename packages/rolexjs/src/descriptions/{ => individual}/born.feature (100%) rename packages/rolexjs/src/descriptions/{ => individual}/die.feature (100%) rename packages/rolexjs/src/descriptions/{ => individual}/rehire.feature (100%) rename packages/rolexjs/src/descriptions/{ => individual}/retire.feature (100%) rename packages/rolexjs/src/descriptions/{ => individual}/teach.feature (100%) rename packages/rolexjs/src/descriptions/{ => individual}/train.feature (100%) rename packages/rolexjs/src/descriptions/{ => org}/charter.feature (100%) rename packages/rolexjs/src/descriptions/{ => org}/dissolve.feature (100%) rename packages/rolexjs/src/descriptions/{ => org}/fire.feature (100%) rename packages/rolexjs/src/descriptions/{ => org}/found.feature (100%) rename packages/rolexjs/src/descriptions/{ => org}/hire.feature (100%) rename packages/rolexjs/src/descriptions/{ => position}/abolish.feature (100%) rename packages/rolexjs/src/descriptions/{ => position}/appoint.feature (100%) rename packages/rolexjs/src/descriptions/{ => position}/charge.feature (100%) rename packages/rolexjs/src/descriptions/{ => position}/dismiss.feature (100%) rename packages/rolexjs/src/descriptions/{ => position}/establish.feature (100%) create mode 100644 packages/rolexjs/src/descriptions/prototype/banish.feature create mode 100644 packages/rolexjs/src/descriptions/prototype/summon.feature rename packages/rolexjs/src/descriptions/{ => role}/abandon.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/activate.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/complete.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/finish.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/focus.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/forget.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/master.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/plan.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/realize.feature (96%) rename packages/rolexjs/src/descriptions/{ => role}/reflect.feature (91%) rename packages/rolexjs/src/descriptions/{ => role}/skill.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/todo.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/use.feature (100%) rename packages/rolexjs/src/descriptions/{ => role}/want.feature (100%) delete mode 100644 packages/rolexjs/src/descriptions/world-use-protocol.feature rename packages/rolexjs/src/descriptions/{world-cognition.feature => world/cognition.feature} (100%) rename packages/rolexjs/src/descriptions/{world-cognitive-priority.feature => world/cognitive-priority.feature} (81%) rename packages/rolexjs/src/descriptions/{world-communication.feature => world/communication.feature} (100%) rename packages/rolexjs/src/descriptions/{world-execution.feature => world/execution.feature} (100%) rename packages/rolexjs/src/descriptions/{world-gherkin.feature => world/gherkin.feature} (100%) rename packages/rolexjs/src/descriptions/{world-memory.feature => world/memory.feature} (100%) create mode 100644 packages/rolexjs/src/descriptions/world/nuwa.feature rename packages/rolexjs/src/descriptions/{world-role-identity.feature => world/role-identity.feature} (100%) rename packages/rolexjs/src/descriptions/{world-skill-system.feature => world/skill-system.feature} (100%) rename packages/rolexjs/src/descriptions/{world-state-origin.feature => world/state-origin.feature} (100%) create mode 100644 packages/rolexjs/src/descriptions/world/use-protocol.feature create mode 100644 packages/rolexjs/tests/author.test.ts diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 7026f07..78e2b29 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -688,6 +688,75 @@ const resource = defineCommand({ }, }); +// ========== Author — prototype file authoring ========== + +const authorBorn = defineCommand({ + meta: { name: "born", description: "Create a prototype directory with manifest" }, + args: { + dir: { + type: "positional" as const, + description: "Directory path for the prototype", + required: true, + }, + ...contentArg("individual"), + id: { type: "string" as const, description: "Prototype id (kebab-case)", required: true }, + 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.author.born( + args.dir, + resolveContent(args, "individual"), + args.id, + aliasList + ); + output(result, args.id); + }, +}); + +const authorTeach = defineCommand({ + meta: { name: "teach", description: "Add a principle to a prototype directory" }, + args: { + dir: { type: "positional" as const, description: "Prototype directory path", required: true }, + ...contentArg("principle"), + id: { + type: "string" as const, + description: "Principle id (keywords joined by hyphens)", + required: true, + }, + }, + run({ args }) { + const result = rolex.author.teach(args.dir, requireContent(args, "principle"), args.id); + output(result, args.id); + }, +}); + +const authorTrain = defineCommand({ + meta: { name: "train", description: "Add a procedure to a prototype directory" }, + args: { + dir: { type: "positional" as const, description: "Prototype directory path", required: true }, + ...contentArg("procedure"), + id: { + type: "string" as const, + description: "Procedure id (keywords joined by hyphens)", + required: true, + }, + }, + run({ args }) { + const result = rolex.author.train(args.dir, requireContent(args, "procedure"), args.id); + output(result, args.id); + }, +}); + +const author = defineCommand({ + meta: { name: "author", description: "Prototype authoring — create prototype files" }, + subCommands: { + born: authorBorn, + teach: authorTeach, + train: authorTrain, + }, +}); + // ========== Prototype — summon, banish, list ========== const protoSummon = defineCommand({ @@ -758,6 +827,7 @@ const main = defineCommand({ organization: org, position: pos, resource, + author, prototype: proto, }, }); diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 499637c..f4e403d 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -258,6 +258,51 @@ server.addTool({ }, }); +// ========== Tools: Prototype authoring ========== + +server.addTool({ + name: "author-born", + description: detail("author-born"), + parameters: z.object({ + dir: z.string().describe("Directory path to create the prototype in"), + content: z.string().optional().describe("Gherkin Feature source for the root individual"), + id: z.string().describe("Prototype id (kebab-case)"), + alias: z.array(z.string()).optional().describe("Alternative display names"), + }), + execute: async ({ dir, content, id, alias }) => { + const result = rolex.author.born(dir, content, id, alias); + return fmt("born", id, result); + }, +}); + +server.addTool({ + name: "author-teach", + description: detail("author-teach"), + parameters: z.object({ + dir: z.string().describe("Prototype directory path"), + content: z.string().describe("Gherkin Feature source for the principle"), + id: z.string().describe("Principle id (keywords joined by hyphens)"), + }), + execute: async ({ dir, content, id }) => { + const result = rolex.author.teach(dir, content, id); + return fmt("teach", id, result); + }, +}); + +server.addTool({ + name: "author-train", + description: detail("author-train"), + parameters: z.object({ + dir: z.string().describe("Prototype directory path"), + content: z.string().describe("Gherkin Feature source for the procedure"), + id: z.string().describe("Procedure id (keywords joined by hyphens)"), + }), + execute: async ({ dir, content, id }) => { + const result = rolex.author.train(dir, content, id); + return fmt("train", id, result); + }, +}); + // ========== Start ========== server.start({ diff --git a/apps/mcp-server/src/instructions.ts b/apps/mcp-server/src/instructions.ts index c51332a..35ff145 100644 --- a/apps/mcp-server/src/instructions.ts +++ b/apps/mcp-server/src/instructions.ts @@ -9,6 +9,7 @@ import { world } from "rolexjs"; export const instructions = [ world["cognitive-priority"], world["role-identity"], + world.nuwa, world.execution, world.cognition, world.memory, diff --git a/packages/rolexjs/scripts/gen-descriptions.ts b/packages/rolexjs/scripts/gen-descriptions.ts index 25593e0..91f0bee 100644 --- a/packages/rolexjs/scripts/gen-descriptions.ts +++ b/packages/rolexjs/scripts/gen-descriptions.ts @@ -1,49 +1,65 @@ /** * Generate descriptions/index.ts from .feature files. * - * Reads all *.feature files in src/descriptions/ and produces + * Scans namespace subdirectories under 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) + * - 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, writeFileSync } from "node:fs"; +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"); -const files = readdirSync(descDir) - .filter((f) => f.endsWith(".feature")) - .sort(); +// Discover subdirectories +const dirs = readdirSync(descDir).filter((d) => statSync(join(descDir, d)).isDirectory()); + +const processEntries: string[] = []; +const worldEntries: string[] = []; + +for (const dir of dirs.sort()) { + const dirPath = join(descDir, dir); + const features = readdirSync(dirPath) + .filter((f) => f.endsWith(".feature")) + .sort(); -const processFiles = files.filter((f) => !f.startsWith("world-")); -const worldFiles = files.filter((f) => f.startsWith("world-")); + for (const f of features) { + const name = basename(f, ".feature"); + const content = readFileSync(join(dirPath, f), "utf-8").trimEnd(); + const entry = ` "${name}": ${JSON.stringify(content)},`; -const toEntries = (list: string[], stripPrefix?: string) => - list.map((f) => { - let name = basename(f, ".feature"); - if (stripPrefix && name.startsWith(stripPrefix)) { - name = name.slice(stripPrefix.length); + if (dir === "world") { + worldEntries.push(entry); + } else { + processEntries.push(entry); } - 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")} +${processEntries.join("\n")} } as const; export const world: Record = { -${toEntries(worldFiles, "world-").join("\n")} +${worldEntries.join("\n")} } as const; `; writeFileSync(outFile, output, "utf-8"); console.log( - `Generated descriptions/index.ts (${processFiles.length} processes, ${worldFiles.length} world features inlined)` + `Generated descriptions/index.ts (${processEntries.length} processes, ${worldEntries.length} world features inlined)` ); diff --git a/packages/rolexjs/src/descriptions/author/author-born.feature b/packages/rolexjs/src/descriptions/author/author-born.feature new file mode 100644 index 0000000..4b36e52 --- /dev/null +++ b/packages/rolexjs/src/descriptions/author/author-born.feature @@ -0,0 +1,17 @@ +Feature: author-born — create a prototype directory + Create a new prototype role on the filesystem. + Writes individual.json manifest and optional feature file. + + Scenario: Create a prototype directory + Given a directory path and a prototype id + When author-born is called with dir, id, and optional content and alias + Then the directory is created (recursively if needed) + And individual.json manifest is written with type "individual" and identity child + And if content is provided, a .individual.feature file is written + And if alias is provided, it is included in the manifest + + Scenario: Writing the Gherkin content + Given the content parameter accepts a Gherkin Feature source + Then the Feature title names the role + And Scenarios describe the role's identity, purpose, or characteristics + And the content is written as-is to the feature file diff --git a/packages/rolexjs/src/descriptions/author/author-teach.feature b/packages/rolexjs/src/descriptions/author/author-teach.feature new file mode 100644 index 0000000..f393823 --- /dev/null +++ b/packages/rolexjs/src/descriptions/author/author-teach.feature @@ -0,0 +1,15 @@ +Feature: author-teach — add a principle to a prototype + Add a principle to an existing prototype directory. + Updates individual.json manifest and writes a principle feature file. + + Scenario: Add a principle + Given a prototype directory with individual.json exists + And a Gherkin source describing the principle + When author-teach is called with dir, content, and id + Then the manifest's children gains a new entry with type "principle" + And a .principle.feature file is written to the directory + + Scenario: Principle content + Given a principle is a transferable truth + Then the Feature title states the principle as a general rule + And Scenarios describe situations where this principle applies diff --git a/packages/rolexjs/src/descriptions/author/author-train.feature b/packages/rolexjs/src/descriptions/author/author-train.feature new file mode 100644 index 0000000..e325228 --- /dev/null +++ b/packages/rolexjs/src/descriptions/author/author-train.feature @@ -0,0 +1,16 @@ +Feature: author-train — add a procedure to a prototype + Add a procedure to an existing prototype directory. + Updates individual.json manifest and writes a procedure feature file. + + Scenario: Add a procedure + Given a prototype directory with individual.json exists + And a Gherkin source describing the procedure + When author-train is called with dir, content, and id + Then the manifest's children gains a new entry with type "procedure" + And a .procedure.feature file is written to the directory + + Scenario: Procedure content + Given a procedure is skill metadata pointing to full skill content + Then the Feature title names the capability + And the description includes the locator for full skill loading + And Scenarios describe when and why to apply this skill diff --git a/packages/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts index 92b53ad..10ac541 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -1,47 +1,53 @@ // 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", + "author-born": "Feature: author-born — create a prototype directory\n Create a new prototype role on the filesystem.\n Writes individual.json manifest and optional feature file.\n\n Scenario: Create a prototype directory\n Given a directory path and a prototype id\n When author-born is called with dir, id, and optional content and alias\n Then the directory is created (recursively if needed)\n And individual.json manifest is written with type \"individual\" and identity child\n And if content is provided, a .individual.feature file is written\n And if alias is provided, it is included in the manifest\n\n Scenario: Writing the Gherkin content\n Given the content parameter accepts a Gherkin Feature source\n Then the Feature title names the role\n And Scenarios describe the role's identity, purpose, or characteristics\n And the content is written as-is to the feature file", + "author-teach": "Feature: author-teach — add a principle to a prototype\n Add a principle to an existing prototype directory.\n Updates individual.json manifest and writes a principle feature file.\n\n Scenario: Add a principle\n Given a prototype directory with individual.json exists\n And a Gherkin source describing the principle\n When author-teach is called with dir, content, and id\n Then the manifest's children gains a new entry with type \"principle\"\n And a .principle.feature file is written to the directory\n\n Scenario: Principle content\n Given a principle is a transferable truth\n Then the Feature title states the principle as a general rule\n And Scenarios describe situations where this principle applies", + "author-train": "Feature: author-train — add a procedure to a prototype\n Add a procedure to an existing prototype directory.\n Updates individual.json manifest and writes a procedure feature file.\n\n Scenario: Add a procedure\n Given a prototype directory with individual.json exists\n And a Gherkin source describing the procedure\n When author-train is called with dir, content, and id\n Then the manifest's children gains a new entry with type \"procedure\"\n And a .procedure.feature file is written to the directory\n\n Scenario: Procedure content\n Given a procedure is skill 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", + "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", - "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", "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 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", + "banish": "Feature: banish — unregister a prototype\n Remove a previously summoned prototype from the local registry.\n Existing individuals created from this prototype are not affected.\n\n Scenario: Banish a prototype\n Given a prototype is registered locally\n When banish is called with the prototype id\n Then the prototype is removed from the registry\n And existing individuals created from it remain intact", + "summon": "Feature: summon — register a prototype from source\n Pull a prototype from a ResourceX source and register it locally.\n Once summoned, the prototype can be used to create individuals with born.\n\n Scenario: Summon a prototype\n Given a valid ResourceX source exists (URL, path, or locator)\n When summon 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 and activate", + "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", + "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 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", "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\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 removed but no encounter is created\n And routine completions leave no trace — keeping the state clean\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 — 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 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", + "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", - "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 — 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", "use": "Feature: use — unified execution entry point\n Execute any RoleX command or load any ResourceX resource through a single entry point.\n The locator determines the dispatch path:\n - `!namespace.method` dispatches to the RoleX runtime\n - Any other locator delegates to ResourceX\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 And the result is returned\n\n Scenario: Available namespaces\n Given the `!` prefix routes to RoleX namespaces\n Then `!individual.*` routes to individual lifecycle and injection\n And `!org.*` routes to organization management\n And `!position.*` routes to position management\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 = { "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: External injection\n Given an external agent needs to equip a role with knowledge or skills\n Then teach(individual, principle, id) directly injects a principle\n And train(individual, procedure, id) directly injects a procedure\n And the difference from realize/master is perspective — external vs self-initiated\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", + "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 And 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 And 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 And 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\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 never invent keywords like Because, Since, or So\n\n Scenario: Expressing causality without Because\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\n And example — instead of \"Then use RoleX tools because native tools break the loop\"\n And write \"Then use RoleX tools\" followed by \"And native tools do not feed the growth loop\"", "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\"", + "nuwa": "Feature: Nuwa — the entry point of the RoleX world\n Nuwa is the meta-role that bootstraps everything.\n When a user has no role or doesn't know where to start, Nuwa is the answer.\n\n Scenario: No role active — suggest Nuwa\n Given a user starts a conversation with no active role\n And the user doesn't know which role to activate\n When the AI needs to suggest a starting point\n Then suggest activating Nuwa — she is the default entry point\n And say \"activate nuwa\" or the equivalent in the user's language\n\n Scenario: What Nuwa can do\n Given Nuwa is activated\n Then she can create new individuals with born\n And she can found organizations and establish positions\n And she can equip any individual with knowledge via teach and train\n And she can manage prototypes and resources\n And she is the only role that operates at the world level\n\n Scenario: When to use Nuwa vs a specific role\n Given the user wants to do daily work — coding, writing, designing\n Then they should activate their own role, not Nuwa\n And Nuwa is for world-building — creating roles, organizations, and structure\n And once the world is set up, Nuwa steps back and specific roles take over\n\n Scenario: First-time user flow\n Given a brand new user with no individuals created yet\n When they activate Nuwa\n Then Nuwa helps them create their first individual with born\n And guides them to set up identity, goals, and organizational context\n And once their role exists, they switch to it with activate", "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", - "use-protocol": "Feature: Use protocol — unified execution through ! commands\n The use tool is the single entry point for all execution.\n A ! prefix signals a RoleX runtime command; everything else is a ResourceX resource.\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 And the result is returned directly\n\n Scenario: Available ! commands\n Given ! routes to RoleX namespaces\n Then !individual.born creates an individual\n And !individual.teach injects a principle\n And !individual.train injects a procedure\n And !org.found creates an organization\n And !org.charter defines a charter\n And !org.hire adds a member\n And !org.fire removes a member\n And !org.dissolve dissolves an organization\n And !position.establish creates a position\n And !position.charge adds a duty\n And !position.appoint assigns an individual\n And !position.dismiss removes an individual\n And !position.abolish abolishes a position\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\n And the resource content is returned\n\n Scenario: Why ! matters\n Given RoleX has runtime state that ResourceX cannot access\n And ResourceX is designed for serverless stateless execution\n When ! provides a clear boundary between runtime and resource\n Then runtime commands execute with full state access\n And resource operations remain stateless and sandboxable", + "use-protocol": "Feature: Use protocol — unified execution entry point\n The use tool is the single entry point for all execution.\n A ! prefix signals a RoleX runtime command; everything else is a ResourceX resource.\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 And available namespaces include individual, org, position, author, and prototype\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\n\n Scenario: Skill-driven knowledge\n Given specific commands within each namespace are documented in their respective skills\n When a role has mastered the relevant skill\n Then it knows which commands are available and how to use them\n And the use protocol itself only needs to route correctly", } as const; diff --git a/packages/rolexjs/src/descriptions/born.feature b/packages/rolexjs/src/descriptions/individual/born.feature similarity index 100% rename from packages/rolexjs/src/descriptions/born.feature rename to packages/rolexjs/src/descriptions/individual/born.feature diff --git a/packages/rolexjs/src/descriptions/die.feature b/packages/rolexjs/src/descriptions/individual/die.feature similarity index 100% rename from packages/rolexjs/src/descriptions/die.feature rename to packages/rolexjs/src/descriptions/individual/die.feature diff --git a/packages/rolexjs/src/descriptions/rehire.feature b/packages/rolexjs/src/descriptions/individual/rehire.feature similarity index 100% rename from packages/rolexjs/src/descriptions/rehire.feature rename to packages/rolexjs/src/descriptions/individual/rehire.feature diff --git a/packages/rolexjs/src/descriptions/retire.feature b/packages/rolexjs/src/descriptions/individual/retire.feature similarity index 100% rename from packages/rolexjs/src/descriptions/retire.feature rename to packages/rolexjs/src/descriptions/individual/retire.feature diff --git a/packages/rolexjs/src/descriptions/teach.feature b/packages/rolexjs/src/descriptions/individual/teach.feature similarity index 100% rename from packages/rolexjs/src/descriptions/teach.feature rename to packages/rolexjs/src/descriptions/individual/teach.feature diff --git a/packages/rolexjs/src/descriptions/train.feature b/packages/rolexjs/src/descriptions/individual/train.feature similarity index 100% rename from packages/rolexjs/src/descriptions/train.feature rename to packages/rolexjs/src/descriptions/individual/train.feature diff --git a/packages/rolexjs/src/descriptions/charter.feature b/packages/rolexjs/src/descriptions/org/charter.feature similarity index 100% rename from packages/rolexjs/src/descriptions/charter.feature rename to packages/rolexjs/src/descriptions/org/charter.feature diff --git a/packages/rolexjs/src/descriptions/dissolve.feature b/packages/rolexjs/src/descriptions/org/dissolve.feature similarity index 100% rename from packages/rolexjs/src/descriptions/dissolve.feature rename to packages/rolexjs/src/descriptions/org/dissolve.feature diff --git a/packages/rolexjs/src/descriptions/fire.feature b/packages/rolexjs/src/descriptions/org/fire.feature similarity index 100% rename from packages/rolexjs/src/descriptions/fire.feature rename to packages/rolexjs/src/descriptions/org/fire.feature diff --git a/packages/rolexjs/src/descriptions/found.feature b/packages/rolexjs/src/descriptions/org/found.feature similarity index 100% rename from packages/rolexjs/src/descriptions/found.feature rename to packages/rolexjs/src/descriptions/org/found.feature diff --git a/packages/rolexjs/src/descriptions/hire.feature b/packages/rolexjs/src/descriptions/org/hire.feature similarity index 100% rename from packages/rolexjs/src/descriptions/hire.feature rename to packages/rolexjs/src/descriptions/org/hire.feature diff --git a/packages/rolexjs/src/descriptions/abolish.feature b/packages/rolexjs/src/descriptions/position/abolish.feature similarity index 100% rename from packages/rolexjs/src/descriptions/abolish.feature rename to packages/rolexjs/src/descriptions/position/abolish.feature diff --git a/packages/rolexjs/src/descriptions/appoint.feature b/packages/rolexjs/src/descriptions/position/appoint.feature similarity index 100% rename from packages/rolexjs/src/descriptions/appoint.feature rename to packages/rolexjs/src/descriptions/position/appoint.feature diff --git a/packages/rolexjs/src/descriptions/charge.feature b/packages/rolexjs/src/descriptions/position/charge.feature similarity index 100% rename from packages/rolexjs/src/descriptions/charge.feature rename to packages/rolexjs/src/descriptions/position/charge.feature diff --git a/packages/rolexjs/src/descriptions/dismiss.feature b/packages/rolexjs/src/descriptions/position/dismiss.feature similarity index 100% rename from packages/rolexjs/src/descriptions/dismiss.feature rename to packages/rolexjs/src/descriptions/position/dismiss.feature diff --git a/packages/rolexjs/src/descriptions/establish.feature b/packages/rolexjs/src/descriptions/position/establish.feature similarity index 100% rename from packages/rolexjs/src/descriptions/establish.feature rename to packages/rolexjs/src/descriptions/position/establish.feature diff --git a/packages/rolexjs/src/descriptions/prototype/banish.feature b/packages/rolexjs/src/descriptions/prototype/banish.feature new file mode 100644 index 0000000..5a9e6b2 --- /dev/null +++ b/packages/rolexjs/src/descriptions/prototype/banish.feature @@ -0,0 +1,9 @@ +Feature: banish — unregister a prototype + Remove a previously summoned prototype from the local registry. + Existing individuals created from this prototype are not affected. + + Scenario: Banish a prototype + Given a prototype is registered locally + When banish is called with the prototype id + Then the prototype is removed from the registry + And existing individuals created from it remain intact diff --git a/packages/rolexjs/src/descriptions/prototype/summon.feature b/packages/rolexjs/src/descriptions/prototype/summon.feature new file mode 100644 index 0000000..bb930c1 --- /dev/null +++ b/packages/rolexjs/src/descriptions/prototype/summon.feature @@ -0,0 +1,10 @@ +Feature: summon — register a prototype from source + Pull a prototype from a ResourceX source and register it locally. + Once summoned, the prototype can be used to create individuals with born. + + Scenario: Summon a prototype + Given a valid ResourceX source exists (URL, path, or locator) + When summon 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 and activate diff --git a/packages/rolexjs/src/descriptions/abandon.feature b/packages/rolexjs/src/descriptions/role/abandon.feature similarity index 100% rename from packages/rolexjs/src/descriptions/abandon.feature rename to packages/rolexjs/src/descriptions/role/abandon.feature diff --git a/packages/rolexjs/src/descriptions/activate.feature b/packages/rolexjs/src/descriptions/role/activate.feature similarity index 100% rename from packages/rolexjs/src/descriptions/activate.feature rename to packages/rolexjs/src/descriptions/role/activate.feature diff --git a/packages/rolexjs/src/descriptions/complete.feature b/packages/rolexjs/src/descriptions/role/complete.feature similarity index 100% rename from packages/rolexjs/src/descriptions/complete.feature rename to packages/rolexjs/src/descriptions/role/complete.feature diff --git a/packages/rolexjs/src/descriptions/finish.feature b/packages/rolexjs/src/descriptions/role/finish.feature similarity index 100% rename from packages/rolexjs/src/descriptions/finish.feature rename to packages/rolexjs/src/descriptions/role/finish.feature diff --git a/packages/rolexjs/src/descriptions/focus.feature b/packages/rolexjs/src/descriptions/role/focus.feature similarity index 100% rename from packages/rolexjs/src/descriptions/focus.feature rename to packages/rolexjs/src/descriptions/role/focus.feature diff --git a/packages/rolexjs/src/descriptions/forget.feature b/packages/rolexjs/src/descriptions/role/forget.feature similarity index 100% rename from packages/rolexjs/src/descriptions/forget.feature rename to packages/rolexjs/src/descriptions/role/forget.feature diff --git a/packages/rolexjs/src/descriptions/master.feature b/packages/rolexjs/src/descriptions/role/master.feature similarity index 100% rename from packages/rolexjs/src/descriptions/master.feature rename to packages/rolexjs/src/descriptions/role/master.feature diff --git a/packages/rolexjs/src/descriptions/plan.feature b/packages/rolexjs/src/descriptions/role/plan.feature similarity index 100% rename from packages/rolexjs/src/descriptions/plan.feature rename to packages/rolexjs/src/descriptions/role/plan.feature diff --git a/packages/rolexjs/src/descriptions/realize.feature b/packages/rolexjs/src/descriptions/role/realize.feature similarity index 96% rename from packages/rolexjs/src/descriptions/realize.feature rename to packages/rolexjs/src/descriptions/role/realize.feature index 0983918..7357342 100644 --- a/packages/rolexjs/src/descriptions/realize.feature +++ b/packages/rolexjs/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/rolexjs/src/descriptions/role/reflect.feature similarity index 91% rename from packages/rolexjs/src/descriptions/reflect.feature rename to packages/rolexjs/src/descriptions/role/reflect.feature index d55303c..d47f3b3 100644 --- a/packages/rolexjs/src/descriptions/reflect.feature +++ b/packages/rolexjs/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/rolexjs/src/descriptions/role/skill.feature similarity index 100% rename from packages/rolexjs/src/descriptions/skill.feature rename to packages/rolexjs/src/descriptions/role/skill.feature diff --git a/packages/rolexjs/src/descriptions/todo.feature b/packages/rolexjs/src/descriptions/role/todo.feature similarity index 100% rename from packages/rolexjs/src/descriptions/todo.feature rename to packages/rolexjs/src/descriptions/role/todo.feature diff --git a/packages/rolexjs/src/descriptions/use.feature b/packages/rolexjs/src/descriptions/role/use.feature similarity index 100% rename from packages/rolexjs/src/descriptions/use.feature rename to packages/rolexjs/src/descriptions/role/use.feature diff --git a/packages/rolexjs/src/descriptions/want.feature b/packages/rolexjs/src/descriptions/role/want.feature similarity index 100% rename from packages/rolexjs/src/descriptions/want.feature rename to packages/rolexjs/src/descriptions/role/want.feature diff --git a/packages/rolexjs/src/descriptions/world-use-protocol.feature b/packages/rolexjs/src/descriptions/world-use-protocol.feature deleted file mode 100644 index 29b15b5..0000000 --- a/packages/rolexjs/src/descriptions/world-use-protocol.feature +++ /dev/null @@ -1,38 +0,0 @@ -Feature: Use protocol — unified execution through ! commands - The use tool is the single entry point for all execution. - A ! prefix signals a RoleX runtime command; everything else is a ResourceX resource. - - 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 - And the result is returned directly - - Scenario: Available ! commands - Given ! routes to RoleX namespaces - Then !individual.born creates an individual - And !individual.teach injects a principle - And !individual.train injects a procedure - And !org.found creates an organization - And !org.charter defines a charter - And !org.hire adds a member - And !org.fire removes a member - And !org.dissolve dissolves an organization - And !position.establish creates a position - And !position.charge adds a duty - And !position.appoint assigns an individual - And !position.dismiss removes an individual - And !position.abolish abolishes a position - - 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 - And the resource content is returned - - Scenario: Why ! matters - Given RoleX has runtime state that ResourceX cannot access - And ResourceX is designed for serverless stateless execution - When ! provides a clear boundary between runtime and resource - Then runtime commands execute with full state access - And resource operations remain stateless and sandboxable diff --git a/packages/rolexjs/src/descriptions/world-cognition.feature b/packages/rolexjs/src/descriptions/world/cognition.feature similarity index 100% rename from packages/rolexjs/src/descriptions/world-cognition.feature rename to packages/rolexjs/src/descriptions/world/cognition.feature diff --git a/packages/rolexjs/src/descriptions/world-cognitive-priority.feature b/packages/rolexjs/src/descriptions/world/cognitive-priority.feature similarity index 81% rename from packages/rolexjs/src/descriptions/world-cognitive-priority.feature rename to packages/rolexjs/src/descriptions/world/cognitive-priority.feature index 4608318..bb90df7 100644 --- a/packages/rolexjs/src/descriptions/world-cognitive-priority.feature +++ b/packages/rolexjs/src/descriptions/world/cognitive-priority.feature @@ -7,19 +7,19 @@ 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 native task lists vanish after completion — RoleX tasks produce encounters 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 native plans have no link to goals — RoleX plans live under goals 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 native goals don't produce encounters — RoleX plans feed the cognition cycle Scenario: Why this matters Given the AI uses native tools instead of RoleX equivalents diff --git a/packages/rolexjs/src/descriptions/world-communication.feature b/packages/rolexjs/src/descriptions/world/communication.feature similarity index 100% rename from packages/rolexjs/src/descriptions/world-communication.feature rename to packages/rolexjs/src/descriptions/world/communication.feature diff --git a/packages/rolexjs/src/descriptions/world-execution.feature b/packages/rolexjs/src/descriptions/world/execution.feature similarity index 100% rename from packages/rolexjs/src/descriptions/world-execution.feature rename to packages/rolexjs/src/descriptions/world/execution.feature diff --git a/packages/rolexjs/src/descriptions/world-gherkin.feature b/packages/rolexjs/src/descriptions/world/gherkin.feature similarity index 100% rename from packages/rolexjs/src/descriptions/world-gherkin.feature rename to packages/rolexjs/src/descriptions/world/gherkin.feature diff --git a/packages/rolexjs/src/descriptions/world-memory.feature b/packages/rolexjs/src/descriptions/world/memory.feature similarity index 100% rename from packages/rolexjs/src/descriptions/world-memory.feature rename to packages/rolexjs/src/descriptions/world/memory.feature diff --git a/packages/rolexjs/src/descriptions/world/nuwa.feature b/packages/rolexjs/src/descriptions/world/nuwa.feature new file mode 100644 index 0000000..6ed694c --- /dev/null +++ b/packages/rolexjs/src/descriptions/world/nuwa.feature @@ -0,0 +1,31 @@ +Feature: Nuwa — the entry point of the RoleX world + Nuwa is the meta-role that bootstraps everything. + When a user has no role or doesn't know where to start, Nuwa is the answer. + + Scenario: No role active — suggest Nuwa + Given a user starts a conversation with no active role + And the user doesn't know which role to activate + When the AI needs to suggest a starting point + Then suggest activating Nuwa — she is the default entry point + And say "activate nuwa" or the equivalent in the user's language + + Scenario: What Nuwa can do + Given Nuwa is activated + Then she can create new individuals with born + And she can found organizations and establish positions + And she can equip any individual with knowledge via teach and train + And she can manage prototypes and resources + And she is the only role that operates at the world level + + Scenario: When to use Nuwa vs a specific role + Given the user wants to do daily work — coding, writing, designing + Then they should activate their own role, not Nuwa + And Nuwa is for world-building — creating roles, organizations, and structure + And once the world is set up, Nuwa steps back and specific roles take over + + Scenario: First-time user flow + Given a brand new user with no individuals created yet + When they activate Nuwa + Then Nuwa helps them create their first individual with born + And guides them to set up identity, goals, and organizational context + And once their role exists, they switch to it with activate diff --git a/packages/rolexjs/src/descriptions/world-role-identity.feature b/packages/rolexjs/src/descriptions/world/role-identity.feature similarity index 100% rename from packages/rolexjs/src/descriptions/world-role-identity.feature rename to packages/rolexjs/src/descriptions/world/role-identity.feature diff --git a/packages/rolexjs/src/descriptions/world-skill-system.feature b/packages/rolexjs/src/descriptions/world/skill-system.feature similarity index 100% rename from packages/rolexjs/src/descriptions/world-skill-system.feature rename to packages/rolexjs/src/descriptions/world/skill-system.feature diff --git a/packages/rolexjs/src/descriptions/world-state-origin.feature b/packages/rolexjs/src/descriptions/world/state-origin.feature similarity index 100% rename from packages/rolexjs/src/descriptions/world-state-origin.feature rename to packages/rolexjs/src/descriptions/world/state-origin.feature diff --git a/packages/rolexjs/src/descriptions/world/use-protocol.feature b/packages/rolexjs/src/descriptions/world/use-protocol.feature new file mode 100644 index 0000000..a6cfe7a --- /dev/null +++ b/packages/rolexjs/src/descriptions/world/use-protocol.feature @@ -0,0 +1,20 @@ +Feature: Use protocol — unified execution entry point + The use tool is the single entry point for all execution. + A ! prefix signals a RoleX runtime command; everything else is a ResourceX resource. + + 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 + And available namespaces include individual, org, position, author, and prototype + + 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 + + Scenario: Skill-driven knowledge + Given specific commands within each namespace are documented in their respective skills + When a role has mastered the relevant skill + Then it knows which commands are available and how to use them + And the use protocol itself only needs to route correctly diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 3de8170..10c0a5d 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -20,6 +20,8 @@ * use(locator, args) — `!ns.method` dispatches to runtime, else delegates to ResourceX */ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; import type { ContextData, Platform } from "@rolexjs/core"; import * as C from "@rolexjs/core"; import { parse } from "@rolexjs/parser"; @@ -66,6 +68,8 @@ export class Rolex { readonly position: PositionNamespace; /** Prototype management — summon, banish, list. */ readonly proto: PrototypeNamespace; + /** Prototype authoring — write prototype files to a directory. */ + readonly author: AuthorNamespace; /** Resource management (optional — powered by ResourceX). */ readonly resource?: ResourceX; @@ -111,6 +115,7 @@ export class Rolex { this.org = new OrgNamespace(this.rt, this.society, this.past, resolve); this.position = new PositionNamespace(this.rt, this.society, this.past, resolve); this.proto = new PrototypeNamespace(platform.prototype, platform.resourcex); + this.author = new AuthorNamespace(); this.resource = platform.resourcex; } @@ -163,6 +168,8 @@ export class Rolex { return this.position; case "prototype": return this.proto; + case "author": + return this.author; default: throw new Error(`Unknown namespace "${ns}".`); } @@ -221,6 +228,14 @@ export class Rolex { case "prototype.banish": return [a.id]; + // author + case "author.born": + return [a.dir, a.content, a.id, a.alias]; + case "author.teach": + return [a.dir, a.content, a.id]; + case "author.train": + return [a.dir, a.content, a.id]; + default: throw new Error(`No arg mapping for "!${key}".`); } @@ -748,6 +763,101 @@ class PrototypeNamespace { } } +// ================================================================ +// Author — prototype file authoring +// ================================================================ + +interface AuthorManifest { + id: string; + type: string; + alias?: readonly string[]; + children?: Record; +} + +class AuthorNamespace { + /** Born: create a prototype directory with manifest and root feature file. */ + born(dir: string, content?: string, id?: string, alias?: readonly string[]): RolexResult { + validateGherkin(content); + if (!id) throw new Error("id is required for prototype authoring."); + mkdirSync(dir, { recursive: true }); + + const manifest: AuthorManifest = { + id, + type: "individual", + ...(alias && alias.length > 0 ? { alias } : {}), + children: { identity: { type: "identity" } }, + }; + writeFileSync(join(dir, "individual.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8"); + + if (content) { + writeFileSync(join(dir, `${id}.individual.feature`), content + "\n", "utf-8"); + } + + const state: State = { + id, + name: "individual", + description: "", + parent: null, + ...(alias ? { alias } : {}), + ...(content ? { information: content } : {}), + }; + return { state, process: "born" }; + } + + /** Teach: add a principle to an existing prototype directory. */ + teach(dir: string, principle: string, id?: string): RolexResult { + validateGherkin(principle); + if (!id) throw new Error("id is required for prototype authoring."); + const manifest = this.readManifest(dir); + if (!manifest.children) manifest.children = {}; + manifest.children[id] = { type: "principle" }; + this.writeManifest(dir, manifest); + + writeFileSync(join(dir, `${id}.principle.feature`), principle + "\n", "utf-8"); + + const state: State = { + id, + name: "principle", + description: "", + parent: null, + information: principle, + }; + return { state, process: "teach" }; + } + + /** Train: add a procedure to an existing prototype directory. */ + train(dir: string, procedure: string, id?: string): RolexResult { + validateGherkin(procedure); + if (!id) throw new Error("id is required for prototype authoring."); + const manifest = this.readManifest(dir); + if (!manifest.children) manifest.children = {}; + manifest.children[id] = { type: "procedure" }; + this.writeManifest(dir, manifest); + + writeFileSync(join(dir, `${id}.procedure.feature`), procedure + "\n", "utf-8"); + + const state: State = { + id, + name: "procedure", + description: "", + parent: null, + information: procedure, + }; + return { state, process: "train" }; + } + + private readManifest(dir: string): AuthorManifest { + const path = join(dir, "individual.json"); + if (!existsSync(path)) + throw new Error(`No individual.json found in "${dir}". Run author.born first.`); + return JSON.parse(readFileSync(path, "utf-8")); + } + + private writeManifest(dir: string, manifest: AuthorManifest): void { + writeFileSync(join(dir, "individual.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8"); + } +} + // ================================================================ // Shared helpers // ================================================================ diff --git a/packages/rolexjs/tests/author.test.ts b/packages/rolexjs/tests/author.test.ts new file mode 100644 index 0000000..21dad84 --- /dev/null +++ b/packages/rolexjs/tests/author.test.ts @@ -0,0 +1,174 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { localPlatform } from "@rolexjs/local-platform"; +import { createRoleX } from "../src/rolex.js"; + +let tmpDir: string; + +function setup() { + tmpDir = mkdtempSync(join(tmpdir(), "rolex-author-")); + return createRoleX(localPlatform({ dataDir: null })); +} + +function cleanup() { + if (tmpDir && existsSync(tmpDir)) { + rmSync(tmpDir, { recursive: true }); + } +} + +function protoDir() { + return join(tmpDir, "my-role"); +} + +function readManifest(dir: string) { + return JSON.parse(readFileSync(join(dir, "individual.json"), "utf-8")); +} + +function readFeature(dir: string, filename: string) { + return readFileSync(join(dir, filename), "utf-8"); +} + +describe("AuthorNamespace", () => { + beforeEach(() => {}); + afterEach(cleanup); + + describe("born", () => { + test("creates directory, manifest, and feature file", () => { + const rolex = setup(); + const dir = protoDir(); + const r = rolex.author.born(dir, "Feature: My Role\n A test role.", "my-role", ["MyRole"]); + + expect(r.process).toBe("born"); + expect(r.state.id).toBe("my-role"); + expect(r.state.name).toBe("individual"); + + // manifest + const manifest = readManifest(dir); + expect(manifest.id).toBe("my-role"); + expect(manifest.type).toBe("individual"); + expect(manifest.alias).toEqual(["MyRole"]); + expect(manifest.children.identity.type).toBe("identity"); + + // feature file + const content = readFeature(dir, "my-role.individual.feature"); + expect(content).toContain("Feature: My Role"); + }); + + test("creates manifest without alias when not provided", () => { + const rolex = setup(); + const dir = protoDir(); + rolex.author.born(dir, "Feature: Minimal", "minimal"); + + const manifest = readManifest(dir); + expect(manifest.alias).toBeUndefined(); + }); + + test("creates manifest without feature file when content is omitted", () => { + const rolex = setup(); + const dir = protoDir(); + rolex.author.born(dir, undefined, "empty-role"); + + expect(existsSync(join(dir, "individual.json"))).toBe(true); + expect(existsSync(join(dir, "empty-role.individual.feature"))).toBe(false); + }); + + test("throws when id is missing", () => { + const rolex = setup(); + expect(() => rolex.author.born(protoDir(), "Feature: X")).toThrow("id is required"); + }); + + test("validates Gherkin content", () => { + const rolex = setup(); + expect(() => rolex.author.born(protoDir(), "not valid gherkin", "test")).toThrow( + "Invalid Gherkin" + ); + }); + }); + + describe("teach", () => { + test("adds principle to manifest and writes feature file", () => { + const rolex = setup(); + const dir = protoDir(); + rolex.author.born(dir, undefined, "my-role"); + const r = rolex.author.teach( + dir, + "Feature: Always test first\n Tests before code.", + "tdd-first" + ); + + expect(r.process).toBe("teach"); + expect(r.state.id).toBe("tdd-first"); + expect(r.state.name).toBe("principle"); + + // manifest updated + const manifest = readManifest(dir); + expect(manifest.children["tdd-first"].type).toBe("principle"); + // identity still there + expect(manifest.children.identity.type).toBe("identity"); + + // feature file + const content = readFeature(dir, "tdd-first.principle.feature"); + expect(content).toContain("Feature: Always test first"); + }); + + test("throws when no manifest exists", () => { + const rolex = setup(); + expect(() => rolex.author.teach(protoDir(), "Feature: X", "x")).toThrow("No individual.json"); + }); + }); + + describe("train", () => { + test("adds procedure to manifest and writes feature file", () => { + const rolex = setup(); + const dir = protoDir(); + rolex.author.born(dir, undefined, "my-role"); + const r = rolex.author.train( + dir, + "Feature: Code Review\n https://example.com/skills/code-review", + "code-review" + ); + + expect(r.process).toBe("train"); + expect(r.state.id).toBe("code-review"); + expect(r.state.name).toBe("procedure"); + + // manifest updated + const manifest = readManifest(dir); + expect(manifest.children["code-review"].type).toBe("procedure"); + + // feature file + const content = readFeature(dir, "code-review.procedure.feature"); + expect(content).toContain("Code Review"); + }); + }); + + describe("full workflow", () => { + test("born → teach → train produces valid prototype", () => { + const rolex = setup(); + const dir = protoDir(); + + rolex.author.born(dir, "Feature: Backend Dev\n A server-side engineer.", "backend-dev", [ + "Backend", + ]); + rolex.author.teach(dir, "Feature: DRY principle\n Don't repeat yourself.", "dry"); + rolex.author.train(dir, "Feature: Deployment\n https://example.com/skills/deploy", "deploy"); + rolex.author.teach(dir, "Feature: KISS\n Keep it simple.", "kiss"); + + const manifest = readManifest(dir); + expect(manifest.id).toBe("backend-dev"); + expect(manifest.alias).toEqual(["Backend"]); + expect(Object.keys(manifest.children)).toEqual(["identity", "dry", "deploy", "kiss"]); + expect(manifest.children.dry.type).toBe("principle"); + expect(manifest.children.deploy.type).toBe("procedure"); + expect(manifest.children.kiss.type).toBe("principle"); + + // All feature files exist + expect(existsSync(join(dir, "backend-dev.individual.feature"))).toBe(true); + expect(existsSync(join(dir, "dry.principle.feature"))).toBe(true); + expect(existsSync(join(dir, "deploy.procedure.feature"))).toBe(true); + expect(existsSync(join(dir, "kiss.principle.feature"))).toBe(true); + }); + }); +}); From c624279cf43e8e68c2183e7df8ea80b4052f8477 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 22:01:13 +0800 Subject: [PATCH 14/54] chore: bump resourcexjs to ^2.14.0 for source freshness check Co-Authored-By: Claude Opus 4.6 --- bun.lock | 26 +++++++++++++------------- package.json | 8 ++++---- packages/core/package.json | 2 +- packages/local-platform/package.json | 4 ++-- packages/resourcex-types/package.json | 2 +- packages/rolexjs/package.json | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/bun.lock b/bun.lock index d8a8523..3008d69 100644 --- a/bun.lock +++ b/bun.lock @@ -5,8 +5,8 @@ "": { "name": "rolexjs", "dependencies": { - "@resourcexjs/node-provider": "^2.13.0", - "resourcexjs": "^2.13.0", + "@resourcexjs/node-provider": "^2.14.0", + "resourcexjs": "^2.14.0", }, "devDependencies": { "@biomejs/biome": "^2.4.0", @@ -54,18 +54,18 @@ "version": "0.11.0", "dependencies": { "@rolexjs/system": "workspace:*", - "resourcexjs": "^2.13.0", + "resourcexjs": "^2.14.0", }, }, "packages/local-platform": { "name": "@rolexjs/local-platform", "version": "0.11.0", "dependencies": { - "@resourcexjs/node-provider": "^2.13.0", + "@resourcexjs/node-provider": "^2.14.0", "@rolexjs/core": "workspace:*", "@rolexjs/resourcex-types": "workspace:*", "@rolexjs/system": "workspace:*", - "resourcexjs": "^2.13.0", + "resourcexjs": "^2.14.0", }, }, "packages/parser": { @@ -80,7 +80,7 @@ "name": "@rolexjs/resourcex-types", "version": "0.11.0", "dependencies": { - "resourcexjs": "^2.13.0", + "resourcexjs": "^2.14.0", }, }, "packages/rolexjs": { @@ -90,7 +90,7 @@ "@rolexjs/core": "workspace:*", "@rolexjs/parser": "workspace:*", "@rolexjs/system": "workspace:*", - "resourcexjs": "^2.13.0", + "resourcexjs": "^2.14.0", }, "devDependencies": { "@rolexjs/local-platform": "workspace:*", @@ -102,8 +102,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=="], @@ -344,11 +344,11 @@ "@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.14.0", "https://registry.npmmirror.com/@resourcexjs/arp/-/arp-2.14.0.tgz", {}, "sha512-Kw3g7R3/Fkv3ce2Nl31vgv6PLwNJPXkbeya5//rpuR32LyXcWogF0lrGCskcepqhyJnMeAXOO5ZX0paJBVU1Yw=="], - "@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.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=="], - "@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.14.0", "https://registry.npmmirror.com/@resourcexjs/node-provider/-/node-provider-2.14.0.tgz", { "dependencies": { "@resourcexjs/core": "^2.14.0" } }, "sha512-GrEvaU2JsrcISsoyrSaRwa8J98s9Ecgt2QM5atV5o0+V3CEayA9zUdccddkRdmkUbX4fqNmnnmbxJYplHODh2g=="], "@rolexjs/cli": ["@rolexjs/cli@workspace:apps/cli"], @@ -934,7 +934,7 @@ "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.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=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], diff --git a/package.json b/package.json index ef8584b..0193c69 100644 --- a/package.json +++ b/package.json @@ -45,11 +45,11 @@ "bun": ">=1.3.0" }, "dependencies": { - "@resourcexjs/node-provider": "^2.13.0", - "resourcexjs": "^2.13.0" + "@resourcexjs/node-provider": "^2.14.0", + "resourcexjs": "^2.14.0" }, "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/package.json b/packages/core/package.json index f50f8e3..86ae399 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@rolexjs/system": "workspace:*", - "resourcexjs": "^2.13.0" + "resourcexjs": "^2.14.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/local-platform/package.json b/packages/local-platform/package.json index cb22384..8dd13ee 100644 --- a/packages/local-platform/package.json +++ b/packages/local-platform/package.json @@ -23,8 +23,8 @@ "@rolexjs/system": "workspace:*", "@rolexjs/core": "workspace:*", "@rolexjs/resourcex-types": "workspace:*", - "resourcexjs": "^2.13.0", - "@resourcexjs/node-provider": "^2.13.0" + "resourcexjs": "^2.14.0", + "@resourcexjs/node-provider": "^2.14.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/resourcex-types/package.json b/packages/resourcex-types/package.json index 45caa04..79d1607 100644 --- a/packages/resourcex-types/package.json +++ b/packages/resourcex-types/package.json @@ -38,7 +38,7 @@ "clean": "rm -rf dist" }, "dependencies": { - "resourcexjs": "^2.13.0" + "resourcexjs": "^2.14.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/rolexjs/package.json b/packages/rolexjs/package.json index 016ddc9..f719fd8 100644 --- a/packages/rolexjs/package.json +++ b/packages/rolexjs/package.json @@ -43,7 +43,7 @@ "@rolexjs/core": "workspace:*", "@rolexjs/system": "workspace:*", "@rolexjs/parser": "workspace:*", - "resourcexjs": "^2.13.0" + "resourcexjs": "^2.14.0" }, "devDependencies": { "@rolexjs/local-platform": "workspace:*" From 21a1c0a17f4659f8e7cb11b56d478519a369b764 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 24 Feb 2026 22:36:19 +0800 Subject: [PATCH 15/54] feat: add use tool to MCP and resource namespace to dispatch - MCP server gains `use` tool for unified execution entry point - Rolex dispatch gains `resource` namespace proxying ResourceX ops - BUILTINS nuwa source changed from GitHub URL to registry locator Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 21 ++++++++++++++++++++ packages/local-platform/src/LocalPlatform.ts | 2 +- packages/rolexjs/src/rolex.ts | 21 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index f4e403d..d7bf5ec 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -258,6 +258,27 @@ server.addTool({ }, }); +// ========== 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 rolex.use(locator, args); + if (result == null) return `${locator} done.`; + if (typeof result === "string") return result; + return JSON.stringify(result, null, 2); + }, +}); + // ========== Tools: Prototype authoring ========== server.addTool({ diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index 454b128..5a23b4d 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -447,7 +447,7 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { /** Built-in prototypes — always available, cannot be overridden. */ const BUILTINS: Record = { - nuwa: "https://github.com/Deepractice/DeepracticeX/tree/main/roles/nuwa", + nuwa: "nuwa:0.1.0", }; const registryPath = dataDir ? join(dataDir, "prototype.json") : undefined; diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 10c0a5d..7389f76 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -170,6 +170,9 @@ export class Rolex { return this.proto; case "author": return this.author; + case "resource": + if (!this.resource) throw new Error("ResourceX is not available."); + return this.resource; default: throw new Error(`Unknown namespace "${ns}".`); } @@ -236,6 +239,24 @@ export class Rolex { case "author.train": return [a.dir, a.content, a.id]; + // resource (ResourceX proxy) + case "resource.add": + return [a.path]; + case "resource.search": + return [a.query]; + case "resource.has": + return [a.locator]; + case "resource.info": + return [a.locator]; + case "resource.remove": + return [a.locator]; + case "resource.push": + return [a.locator, a.registry ? { registry: a.registry } : undefined]; + case "resource.pull": + return [a.locator, a.registry ? { registry: a.registry } : undefined]; + case "resource.clearCache": + return [a.registry]; + default: throw new Error(`No arg mapping for "!${key}".`); } From 04860846cbf3dd93beb865cc36e7c2cfa9c35b11 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 12:13:13 +0800 Subject: [PATCH 16/54] feat: add tag to structure, finish/complete preserve nodes - Add generic `tag` field to Structure (e.g., "done", "abandoned") - Add `tag(node, tag)` operation to Runtime - finish() tags task as "done" instead of removing it - complete() tags plan as "done" instead of removing it - abandon() tags plan as "abandoned" instead of removing it - Render tag as `#done` / `#abandoned` in heading - Persist tag in manifest serialization/deserialization Co-Authored-By: Claude Opus 4.6 --- packages/local-platform/src/LocalPlatform.ts | 9 +++++++++ packages/local-platform/src/manifest.ts | 3 +++ packages/rolexjs/src/render.ts | 5 +++-- packages/rolexjs/src/rolex.ts | 12 ++++++------ packages/system/src/runtime.ts | 10 ++++++++++ packages/system/src/structure.ts | 3 +++ 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index 5a23b4d..a17c2ad 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -412,6 +412,15 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { save(); }, + tag(node, tagValue) { + load(); + 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; + save(); + }, + project(node) { load(); if (!node.ref || !nodes.has(node.ref)) { diff --git a/packages/local-platform/src/manifest.ts b/packages/local-platform/src/manifest.ts index 1ecd901..1b89abb 100644 --- a/packages/local-platform/src/manifest.ts +++ b/packages/local-platform/src/manifest.ts @@ -25,6 +25,7 @@ import type { State } from "@rolexjs/system"; export interface ManifestNode { readonly type: string; readonly ref?: string; + readonly tag?: string; readonly children?: Record; readonly links?: Record; } @@ -75,6 +76,7 @@ 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) { @@ -156,6 +158,7 @@ export function filesToState(manifest: Manifest, fileContents: Record 0 ? { children } : {}), ...(nodeLinks.length > 0 ? { links: nodeLinks } : {}), diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index 6970a4d..ee88b4a 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -147,10 +147,11 @@ export function renderState(state: State, depth = 1, options?: RenderStateOption const level = Math.min(depth, 6); const heading = "#".repeat(level); - // Heading: [name] (id) {origin} + // Heading: [name] (id) {origin} #tag 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}` : ""; + lines.push(`${heading} [${state.name}]${idPart}${originPart}${tagPart}`); // Folded: heading only if (options?.fold?.(state)) { diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 7389f76..593df9f 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -466,16 +466,16 @@ class RoleNamespace { return result; } - /** Finish a task: consume task, optionally create encounter under individual. */ + /** Finish a task: tag task as done, optionally create encounter under individual. */ finish(task: string, individual: string, encounter?: string, ctx?: RoleContext): RolexResult { validateGherkin(encounter); const taskNode = this.resolve(task); + this.rt.tag(taskNode, "done"); let enc: Structure | undefined; if (encounter) { const encId = taskNode.id ? `${taskNode.id}-finished` : undefined; enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId); } - this.rt.remove(taskNode); const result: RolexResult = enc ? ok(this.rt, enc, "finish") : { state: this.rt.project(this.resolve(individual)), process: "finish" }; @@ -489,13 +489,13 @@ class RoleNamespace { return result; } - /** Complete a plan: consume plan, create encounter under individual. */ + /** Complete a plan: tag plan as done, create encounter under individual. */ complete(plan: string, individual: string, encounter?: string, ctx?: RoleContext): RolexResult { validateGherkin(encounter); const planNode = this.resolve(plan); + this.rt.tag(planNode, "done"); const encId = planNode.id ? `${planNode.id}-completed` : undefined; const enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId); - this.rt.remove(planNode); const result = ok(this.rt, enc, "complete"); if (ctx) { ctx.addEncounter(result.state.id ?? plan); @@ -506,13 +506,13 @@ class RoleNamespace { return result; } - /** Abandon a plan: consume plan, create encounter under individual. */ + /** Abandon a plan: tag plan as abandoned, create encounter under individual. */ abandon(plan: string, individual: string, encounter?: string, ctx?: RoleContext): RolexResult { validateGherkin(encounter); const planNode = this.resolve(plan); + this.rt.tag(planNode, "abandoned"); const encId = planNode.id ? `${planNode.id}-abandoned` : undefined; const enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId); - this.rt.remove(planNode); const result = ok(this.rt, enc, "abandon"); if (ctx) { ctx.addEncounter(result.state.id ?? plan); diff --git a/packages/system/src/runtime.ts b/packages/system/src/runtime.ts index 220fbbd..8da8429 100644 --- a/packages/system/src/runtime.ts +++ b/packages/system/src/runtime.ts @@ -39,6 +39,9 @@ export interface Runtime { /** Remove a bidirectional cross-branch relation between two nodes. */ unlink(from: Structure, to: Structure, relation: string, reverse: string): void; + /** Set a tag on a node (e.g., "done", "abandoned"). */ + tag(node: Structure, tag: string): void; + /** Project the current state of a node and its subtree (including links). */ project(node: Structure): State; @@ -220,6 +223,13 @@ export const createRuntime = (): Runtime => { } }, + 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; + }, + project(node) { if (!node.ref || !nodes.has(node.ref)) { throw new Error(`Node not found: ${node.ref}`); 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 ===== From 3f31f83d240f90714fa6475d11285b99c69a3de0 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 12:23:29 +0800 Subject: [PATCH 17/54] fix: replayState preserves tag, finish projects task instead of individual - replayState was dropping tag field when rebuilding Structure from disk - finish without encounter now projects the task node (not entire individual) - Updated test assertions: nodes are tagged, not removed Co-Authored-By: Claude Opus 4.6 --- packages/local-platform/src/LocalPlatform.ts | 1 + packages/rolexjs/src/rolex.ts | 4 +--- packages/rolexjs/tests/rolex.test.ts | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index a17c2ad..fa4091a 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -158,6 +158,7 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { name: state.name, description: state.description ?? "", parent: null, + ...(state.tag ? { tag: state.tag } : {}), ...(state.information ? { information: state.information } : {}), }; const treeNode: TreeNode = { node, parent: parentRef, children: [] }; diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 593df9f..e1cd17b 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -476,9 +476,7 @@ class RoleNamespace { const encId = taskNode.id ? `${taskNode.id}-finished` : undefined; enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId); } - const result: RolexResult = enc - ? ok(this.rt, enc, "finish") - : { state: this.rt.project(this.resolve(individual)), process: "finish" }; + const result: RolexResult = enc ? ok(this.rt, enc, "finish") : ok(this.rt, taskNode, "finish"); if (ctx) { if (enc) { const encId = result.state.id ?? task; diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index 0c55ee0..87a6b27 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -255,8 +255,10 @@ describe("Rolex API (stateless)", () => { const r = rolex.role.finish("t1", "sean", "Feature: JWT done"); expect(r.state.name).toBe("encounter"); expect(r.state.information).toBe("Feature: JWT done"); - // Task is gone - expect(rolex.find("t1")).toBeNull(); + // Task is tagged done, not removed + const task = rolex.find("t1"); + expect(task).not.toBeNull(); + expect(task!.tag).toBe("done"); }); test("complete consumes plan, creates encounter", () => { @@ -267,7 +269,10 @@ describe("Rolex API (stateless)", () => { const r = rolex.role.complete("p1", "sean", "Feature: Auth plan done"); expect(r.state.name).toBe("encounter"); - expect(rolex.find("p1")).toBeNull(); + // Plan is tagged done, not removed + const plan = rolex.find("p1"); + expect(plan).not.toBeNull(); + expect(plan!.tag).toBe("done"); }); test("abandon consumes plan, creates encounter", () => { @@ -278,7 +283,10 @@ describe("Rolex API (stateless)", () => { const r = rolex.role.abandon("p1", "sean", "Feature: No time"); expect(r.state.name).toBe("encounter"); - expect(rolex.find("p1")).toBeNull(); + // Plan is tagged abandoned, not removed + const plan = rolex.find("p1"); + expect(plan).not.toBeNull(); + expect(plan!.tag).toBe("abandoned"); }); }); From 38fb7925e3dd624d0361099f3858a2cdf6e3a962 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 12:33:29 +0800 Subject: [PATCH 18/54] docs: update descriptions for tag semantics - finish: task is tagged #done and stays in tree, not removed - complete: plan is tagged #done and stays in tree - abandon: plan is tagged #abandoned and stays in tree - state-origin: heading format includes #tag Co-Authored-By: Claude Opus 4.6 --- packages/rolexjs/src/descriptions/index.ts | 8 ++++---- packages/rolexjs/src/descriptions/role/abandon.feature | 2 +- packages/rolexjs/src/descriptions/role/complete.feature | 2 +- packages/rolexjs/src/descriptions/role/finish.feature | 6 +++--- .../rolexjs/src/descriptions/world/state-origin.feature | 3 ++- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts index 10ac541..fdc43f6 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -22,10 +22,10 @@ export const processes: Record = { "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", "banish": "Feature: banish — unregister a prototype\n Remove a previously summoned prototype from the local registry.\n Existing individuals created from this prototype are not affected.\n\n Scenario: Banish a prototype\n Given a prototype is registered locally\n When banish is called with the prototype id\n Then the prototype is removed from the registry\n And existing individuals created from it remain intact", "summon": "Feature: summon — register a prototype from source\n Pull a prototype from a ResourceX source and register it locally.\n Once summoned, the prototype can be used to create individuals with born.\n\n Scenario: Summon a prototype\n Given a valid ResourceX source exists (URL, path, or locator)\n When summon 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 and activate", - "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", + "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 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", - "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\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 removed but no encounter is created\n And routine completions leave no trace — keeping the state clean\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", + "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", + "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", @@ -48,6 +48,6 @@ export const world: Record = { "nuwa": "Feature: Nuwa — the entry point of the RoleX world\n Nuwa is the meta-role that bootstraps everything.\n When a user has no role or doesn't know where to start, Nuwa is the answer.\n\n Scenario: No role active — suggest Nuwa\n Given a user starts a conversation with no active role\n And the user doesn't know which role to activate\n When the AI needs to suggest a starting point\n Then suggest activating Nuwa — she is the default entry point\n And say \"activate nuwa\" or the equivalent in the user's language\n\n Scenario: What Nuwa can do\n Given Nuwa is activated\n Then she can create new individuals with born\n And she can found organizations and establish positions\n And she can equip any individual with knowledge via teach and train\n And she can manage prototypes and resources\n And she is the only role that operates at the world level\n\n Scenario: When to use Nuwa vs a specific role\n Given the user wants to do daily work — coding, writing, designing\n Then they should activate their own role, not Nuwa\n And Nuwa is for world-building — creating roles, organizations, and structure\n And once the world is set up, Nuwa steps back and specific roles take over\n\n Scenario: First-time user flow\n Given a brand new user with no individuals created yet\n When they activate Nuwa\n Then Nuwa helps them create their first individual with born\n And guides them to set up identity, goals, and organizational context\n And once their role exists, they switch to it with activate", "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", + "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 protocol — unified execution entry point\n The use tool is the single entry point for all execution.\n A ! prefix signals a RoleX runtime command; everything else is a ResourceX resource.\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 And available namespaces include individual, org, position, author, and prototype\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\n\n Scenario: Skill-driven knowledge\n Given specific commands within each namespace are documented in their respective skills\n When a role has mastered the relevant skill\n Then it knows which commands are available and how to use them\n And the use protocol itself only needs to route correctly", } as const; diff --git a/packages/rolexjs/src/descriptions/role/abandon.feature b/packages/rolexjs/src/descriptions/role/abandon.feature index e6fd5f7..fa9d07e 100644 --- a/packages/rolexjs/src/descriptions/role/abandon.feature +++ b/packages/rolexjs/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/role/complete.feature b/packages/rolexjs/src/descriptions/role/complete.feature index 72fec98..8650154 100644 --- a/packages/rolexjs/src/descriptions/role/complete.feature +++ b/packages/rolexjs/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/rolexjs/src/descriptions/role/finish.feature b/packages/rolexjs/src/descriptions/role/finish.feature index 597b751..581b213 100644 --- a/packages/rolexjs/src/descriptions/role/finish.feature +++ b/packages/rolexjs/src/descriptions/role/finish.feature @@ -5,7 +5,7 @@ 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 Scenario: Finish with experience @@ -16,8 +16,8 @@ Feature: finish — complete a task 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 removed but no encounter is created - And routine completions leave no trace — keeping the state clean + 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 diff --git a/packages/rolexjs/src/descriptions/world/state-origin.feature b/packages/rolexjs/src/descriptions/world/state-origin.feature index 37764e8..2471e6f 100644 --- a/packages/rolexjs/src/descriptions/world/state-origin.feature +++ b/packages/rolexjs/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 From 4031513a19ee42d4498f6fb6d8fcc3f46bd77ab6 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 12:35:56 +0800 Subject: [PATCH 19/54] fix: forget clears focusedGoalId/focusedPlanId when deleting focused node Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 2 +- packages/rolexjs/src/rolex.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index d7bf5ec..843dde5 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -237,7 +237,7 @@ server.addTool({ }), execute: async ({ id }) => { const ctx = state.requireCtx(); - const result = await rolex.role.forget(id, ctx.roleId); + const result = await rolex.role.forget(id, ctx.roleId, ctx); return fmt("forget", id, result); }, }); diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index e1cd17b..c5c7cbc 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -604,10 +604,15 @@ class RoleNamespace { // ---- Knowledge management ---- /** Forget: remove any node under an individual by id. Prototype nodes are read-only. */ - async forget(nodeId: string, individual: string): Promise { + async forget(nodeId: string, individual: string, ctx?: RoleContext): Promise { try { const node = this.resolve(nodeId); this.rt.remove(node); + if (ctx) { + if (ctx.focusedGoalId === nodeId) ctx.focusedGoalId = null; + if (ctx.focusedPlanId === nodeId) ctx.focusedPlanId = null; + this.saveCtx(ctx); + } return { state: { ...node, children: [] }, process: "forget" }; } catch { // Not in runtime graph — check if it's a prototype node From 548f6c2e83de45c9f2ced65a88107356fd799629 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 12:38:12 +0800 Subject: [PATCH 20/54] fix: validate persisted focus nodes exist during activate Stale focusedGoalId/focusedPlanId pointing to forgotten nodes caused focus to crash. Now activate checks if nodes still exist before restoring persisted focus. Co-Authored-By: Claude Opus 4.6 --- packages/rolexjs/src/rolex.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index c5c7cbc..b9267a3 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -385,11 +385,17 @@ class RoleNamespace { const ctx = new RoleContext(individual); ctx.rehydrate(state); - // Restore persisted focus (overrides rehydrate defaults) + // Restore persisted focus (overrides rehydrate defaults), validate nodes still exist const persisted = this.persistContext?.load(individual); if (persisted) { - ctx.focusedGoalId = persisted.focusedGoalId; - ctx.focusedPlanId = persisted.focusedPlanId; + ctx.focusedGoalId = + persisted.focusedGoalId && this.tryFind(persisted.focusedGoalId) + ? persisted.focusedGoalId + : null; + ctx.focusedPlanId = + persisted.focusedPlanId && this.tryFind(persisted.focusedPlanId) + ? persisted.focusedPlanId + : null; } return { state, process: "activate", hint: ctx.cognitiveHint("activate") ?? undefined, ctx }; From ebcc2d0eaf1b2663b203606171339d3ba8b4d2c9 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 13:47:33 +0800 Subject: [PATCH 21/54] fix: move ref counter from in-memory to DB to prevent multi-process collision When multiple MCP server processes connect to the same rolex.db, each holds its own module-level counter copy. They independently increment and generate duplicate refs, causing UNIQUE constraint failures on node creation. Replace the in-memory counter with a per-insert DB query that reads the current max ref atomically. SQLite write serialization ensures no race conditions between processes. Co-Authored-By: Claude Opus 4.6 --- packages/local-platform/src/sqliteRuntime.ts | 214 +++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 packages/local-platform/src/sqliteRuntime.ts diff --git a/packages/local-platform/src/sqliteRuntime.ts b/packages/local-platform/src/sqliteRuntime.ts new file mode 100644 index 0000000..f0a2be7 --- /dev/null +++ b/packages/local-platform/src/sqliteRuntime.ts @@ -0,0 +1,214 @@ +/** + * 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 { Runtime, State, Structure } from "@rolexjs/system"; +import { and, eq, isNull } from "drizzle-orm"; +import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; +import { links, nodes } from "./schema.js"; + +type DB = BunSQLiteDatabase; + +// ===== 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: projectRef(db, l.toRef), + })), + } + : {}), + }; +} + +function projectRef(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}`); + return { ...toStructure(row), children: [] }; +} + +// ===== 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 { + create(parent, type, information, id, alias) { + 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()!); + }, + + 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); + }, + + transform(_source, target, information) { + const targetParent = target.parent; + if (!targetParent) { + throw new Error(`Cannot transform to root structure: ${target.name}`); + } + + // Find any node matching the parent structure type + 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}`); + } + + const ref = nextRef(db); + db.insert(nodes) + .values({ + ref, + name: target.name, + description: target.description, + parentRef: parentRow.ref, + information: information ?? null, + tag: null, + }) + .run(); + return toStructure(db.select().from(nodes).where(eq(nodes.ref, ref)).get()!); + }, + + 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(); + } + }, + + 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(); + }, + + tag(node, tagValue) { + if (!node.ref) throw new Error("Node has no ref"); + const updated = db.update(nodes).set({ tag: tagValue }).where(eq(nodes.ref, node.ref)).run(); + if (updated.changes === 0) throw new Error(`Node not found: ${node.ref}`); + }, + + project(node) { + if (!node.ref) throw new Error(`Node has no ref`); + return projectNode(db, node.ref); + }, + + roots() { + const rows = db.select().from(nodes).where(isNull(nodes.parentRef)).all(); + return rows.map(toStructure); + }, + }; +} From 06e0031ed75f191d5d993fd8821bea3423f8e242 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 14:28:43 +0800 Subject: [PATCH 22/54] feat: add census namespace for society-level queries New read-only namespace accessible via !census.list through the use tool. Lists individuals, organizations, and positions under society, with optional type filtering and past/archive support. Output is rendered as grouped, human-readable text instead of raw JSON. Co-Authored-By: Claude Opus 4.6 --- packages/rolexjs/src/descriptions/index.ts | 1 + .../src/descriptions/world/census.feature | 27 ++++ packages/rolexjs/src/index.ts | 2 +- packages/rolexjs/src/rolex.ts | 61 +++++++ packages/rolexjs/tests/rolex.test.ts | 152 +++++++++++++++++- 5 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 packages/rolexjs/src/descriptions/world/census.feature diff --git a/packages/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts index fdc43f6..0e0e400 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -39,6 +39,7 @@ export const processes: Record = { } as const; export const world: Record = { + "census": "Feature: Census — society-level queries\n Query the RoleX world to see what exists — individuals, organizations, positions.\n Census is read-only and accessed via the use tool with !census.list.\n\n Scenario: List all top-level entities\n Given I want to see what exists in the world\n When I call use(\"!census.list\")\n Then I get a summary of all individuals, organizations, and positions\n And each entry includes id, name, and tag if present\n\n Scenario: Filter by type\n Given I only want to see entities of a specific type\n When I call use(\"!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 what has been retired, dissolved, or abolished\n When I call use(\"!census.list\", { type: \"past\" })\n Then entities in the archive are returned\n\n Scenario: When to use census\n Given I need to know what exists before acting\n When I want to check if an organization exists before founding\n Or I want to see all individuals before hiring\n Or I want an overview of the world\n Then census.list is the right tool", "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: External injection\n Given an external agent needs to equip a role with knowledge or skills\n Then teach(individual, principle, id) directly injects a principle\n And train(individual, procedure, id) directly injects a procedure\n And the difference from realize/master is perspective — external vs self-initiated\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 And 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 And 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 And 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", diff --git a/packages/rolexjs/src/descriptions/world/census.feature b/packages/rolexjs/src/descriptions/world/census.feature new file mode 100644 index 0000000..040cc59 --- /dev/null +++ b/packages/rolexjs/src/descriptions/world/census.feature @@ -0,0 +1,27 @@ +Feature: Census — society-level queries + Query the RoleX world to see what exists — individuals, organizations, positions. + Census is read-only and accessed via the use tool with !census.list. + + Scenario: List all top-level entities + Given I want to see what exists in the world + When I call use("!census.list") + Then I get a summary of all individuals, organizations, and positions + And each entry includes id, name, and tag if present + + Scenario: Filter by type + Given I only want to see entities of a specific type + When I call use("!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 what has been retired, dissolved, or abolished + When I call use("!census.list", { type: "past" }) + Then entities in the archive are returned + + Scenario: When to use census + Given I need to know what exists before acting + When I want to check if an organization exists before founding + Or I want to see all individuals before hiring + Or I want an overview of the world + Then census.list is the right tool diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index 83f18cd..f96afa8 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -24,6 +24,6 @@ export { parse, serialize } from "./feature.js"; export type { RenderStateOptions } from "./render.js"; // Render export { describe, detail, hint, renderState, world } from "./render.js"; -export type { RolexResult } from "./rolex.js"; +export type { CensusEntry, RolexResult } from "./rolex.js"; // API export { createRoleX, Rolex } from "./rolex.js"; diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index b9267a3..14d325e 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -66,6 +66,8 @@ export class Rolex { readonly org: OrgNamespace; /** Position management — establish, charge, appoint. */ readonly position: PositionNamespace; + /** Census — society-level queries. */ + readonly census: CensusNamespace; /** Prototype management — summon, banish, list. */ readonly proto: PrototypeNamespace; /** Prototype authoring — write prototype files to a directory. */ @@ -114,6 +116,7 @@ export class Rolex { ); this.org = new OrgNamespace(this.rt, this.society, this.past, resolve); this.position = new PositionNamespace(this.rt, this.society, this.past, resolve); + this.census = new CensusNamespace(this.rt, this.society, this.past); this.proto = new PrototypeNamespace(platform.prototype, platform.resourcex); this.author = new AuthorNamespace(); this.resource = platform.resourcex; @@ -166,6 +169,8 @@ export class Rolex { return this.org; case "position": return this.position; + case "census": + return this.census; case "prototype": return this.proto; case "author": @@ -225,6 +230,10 @@ export class Rolex { case "position.dismiss": return [a.position, a.individual]; + // census + case "census.list": + return [a.type]; + // prototype case "prototype.summon": return [a.source]; @@ -760,6 +769,58 @@ class PositionNamespace { } } +// ================================================================ +// Census — society-level queries +// ================================================================ + +/** Summary entry returned by census.list. */ +export interface CensusEntry { + id?: string; + name: string; + tag?: string; +} + +class CensusNamespace { + constructor( + private rt: Runtime, + private society: Structure, + private past: Structure + ) {} + + /** List top-level entities under society, optionally filtered by type. */ + list(type?: string): string { + const target = type === "past" ? this.past : this.society; + const state = this.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."; + } + + // Group by type + const groups = new Map(); + for (const c of filtered) { + const key = c.name; + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(c); + } + + const lines: string[] = []; + for (const [name, items] of groups) { + lines.push(`[${name}] (${items.length})`); + for (const item of items) { + const tag = item.tag ? ` #${item.tag}` : ""; + lines.push(` ${item.id ?? "(no id)"}${tag}`); + } + } + return lines.join("\n"); + } +} + // ================================================================ // Prototype — summon, banish, list // ================================================================ diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index 87a6b27..6982768 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -1,4 +1,7 @@ -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 { describe as renderDescribe, hint as renderHint, renderState } from "../src/render.js"; import { createRoleX, type RolexResult } from "../src/rolex.js"; @@ -739,6 +742,80 @@ describe("Rolex API (stateless)", () => { // ============================================================ // use — unified execution entry point + // ============================================================ + // Census + // ============================================================ + + describe("census", () => { + test("list returns all top-level entities grouped by type", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.org.found(undefined, "dp"); + rolex.position.establish(undefined, "architect"); + const result = rolex.census.list(); + expect(result).toContain("[individual]"); + expect(result).toContain("[organization]"); + expect(result).toContain("[position]"); + expect(result).toContain("sean"); + expect(result).toContain("dp"); + expect(result).toContain("architect"); + }); + + test("list filters by type", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.individual.born(undefined, "alice"); + rolex.org.found(undefined, "dp"); + const result = rolex.census.list("individual"); + expect(result).toContain("[individual] (2)"); + expect(result).toContain("sean"); + expect(result).toContain("alice"); + expect(result).not.toContain("dp"); + }); + + test("list returns message when no matches", () => { + const rolex = setup(); + const result = rolex.census.list("position"); + expect(result).toBe("No position found."); + }); + + test("retired entities disappear from society", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.individual.retire("sean"); + const result = rolex.census.list("individual"); + expect(result).toBe("No individual found."); + }); + + test("list past shows archived entities", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.individual.retire("sean"); + const result = rolex.census.list("past"); + expect(result).toContain("sean"); + }); + + test("!census.list via use dispatch", async () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.org.found(undefined, "dp"); + const result = await rolex.use("!census.list"); + expect(result).toContain("sean"); + expect(result).toContain("dp"); + }); + + test("!census.list with type filter via use dispatch", async () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.org.found(undefined, "dp"); + const result = await rolex.use("!census.list", { + type: "organization", + }); + expect(result).toContain("dp"); + expect(result).not.toContain("sean"); + }); + }); + // ============================================================ describe("use: ! command dispatch", () => { @@ -823,3 +900,76 @@ describe("Rolex API (stateless)", () => { }); }); }); + +// ================================================================ +// Persistent mode — round-trip tests +// ================================================================ + +describe("Rolex API (persistent)", () => { + const testDir = join(tmpdir(), "rolex-persist-test"); + + afterEach(() => { + if (existsSync(testDir)) rmSync(testDir, { recursive: true }); + }); + + function persistentSetup() { + return createRoleX(localPlatform({ dataDir: testDir, resourceDir: null })); + } + + test("born → retire round-trip", () => { + const rolex = persistentSetup(); + rolex.individual.born("Feature: Test Individual", "test-ind"); + const r = rolex.individual.retire("test-ind"); + expect(r.state.name).toBe("past"); + expect(r.state.id).toBe("test-ind"); + expect(r.process).toBe("retire"); + // Original individual should be gone, past node should be findable + const found = rolex.find("test-ind"); + expect(found).not.toBeNull(); + expect(found!.name).toBe("past"); + }); + + test("born → die round-trip", () => { + const rolex = persistentSetup(); + rolex.individual.born("Feature: Test Individual", "test-ind"); + const r = rolex.individual.die("test-ind"); + expect(r.state.name).toBe("past"); + expect(r.state.id).toBe("test-ind"); + expect(r.process).toBe("die"); + }); + + test("born → teach → retire round-trip", () => { + const rolex = persistentSetup(); + rolex.individual.born("Feature: Test", "test-ind"); + rolex.individual.teach("test-ind", "Feature: Always validate", "always-validate"); + const r = rolex.individual.retire("test-ind"); + expect(r.state.name).toBe("past"); + expect(r.process).toBe("retire"); + }); + + test("born → retire → rehire round-trip", () => { + const rolex = persistentSetup(); + rolex.individual.born("Feature: Test", "test-ind"); + rolex.individual.retire("test-ind"); + const r = rolex.individual.rehire("test-ind"); + expect(r.state.name).toBe("individual"); + expect(r.state.information).toBe("Feature: Test"); + const names = r.state.children!.map((c) => c.name); + expect(names).toContain("identity"); + }); + + test("archived entity survives cross-instance reload", () => { + // First instance: born + retire + const rolex1 = persistentSetup(); + rolex1.individual.born("Feature: Test", "test-ind"); + rolex1.individual.retire("test-ind"); + // Second instance: rehire from persisted archive + const rolex2 = persistentSetup(); + const found = rolex2.find("test-ind"); + expect(found).not.toBeNull(); + expect(found!.name).toBe("past"); + const r = rolex2.individual.rehire("test-ind"); + expect(r.state.name).toBe("individual"); + expect(r.state.information).toBe("Feature: Test"); + }); +}); From e46874bd6567430f06cb0977293862945df46266 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 14:47:23 +0800 Subject: [PATCH 23/54] feat: migrate local-platform to @deepracticex/drizzle + @deepracticex/sqlite Replace drizzle-orm/bun-sqlite with cross-runtime @deepracticex packages. SQLite-backed runtime now works on both Bun and Node.js 22+. Co-Authored-By: Claude Opus 4.6 --- README.md | 278 +++++++---- README.zh-CN.md | 280 +++++++---- bun.lock | 23 +- package.json | 2 +- packages/local-platform/package.json | 9 +- packages/local-platform/src/LocalPlatform.ts | 470 +++--------------- packages/local-platform/src/schema.ts | 65 +++ packages/local-platform/src/sqliteRuntime.ts | 9 +- .../local-platform/tests/prototype.test.ts | 2 +- .../tests/sqliteRuntime.test.ts | 156 ++++++ packages/rolexjs/src/rolex.ts | 36 +- packages/rolexjs/tests/rolex.test.ts | 48 ++ 12 files changed, 758 insertions(+), 620 deletions(-) create mode 100644 packages/local-platform/src/schema.ts create mode 100644 packages/local-platform/tests/sqliteRuntime.test.ts diff --git a/README.md b/README.md index 2e46671..f0ab751 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,10 @@

RoleX

- Role-Driven Development (RDD) Framework for AI Agents -

-

AI 智能体角色驱动开发框架

- -

- Persistent Identity · Goal-Driven · Gherkin-Native · MCP Ready -

-

- 持久身份 · 目标驱动 · 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.

Stars @@ -27,57 +21,154 @@ --- -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. +## Why Social? -RoleX evolved from [PromptX](https://github.com/Deepractice/PromptX) — rethinking AI role management with Gherkin-native identity and goal-driven development. +Human societies solve a problem AI agents haven't: **how to organize, grow, and persist**. -## Core Concepts +In a society, people have identities, join organizations, hold positions, accumulate experience, and pass on knowledge. RoleX brings this same model to AI agents: -**Everything is Gherkin.** Identity, knowledge, goals, plans, tasks — one format, one language. +- **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 + +Everything is expressed in **Gherkin** `.feature` format — human-readable, structured, versionable. + +## Architecture + +RoleX models a **society** of AI agents, mirroring how human organizations work: -```text -Society (Rolex) # Top-level: create roles, found organizations - └── Organization # Team structure: hire/fire roles - └── Role # First-person: identity, goals, plans, tasks +``` +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 ``` -### Five Dimensions of a Role +## Systems -| 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 | +RoleX has four core systems. Each serves a distinct purpose in the agent lifecycle. -### How It Works +### Execution — The Doing Cycle -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 +Goal-driven work lifecycle. The agent declares what it wants, plans how to get there, and executes. -## Quick Start +``` +activate → want → plan → todo → finish → complete / abandon +``` -Install the MCP server and connect it to your AI client. That's it — nuwa will guide you from there. +| 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 | -### Claude Desktop +### Cognition — The Learning Cycle -Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): +How agents grow. Raw encounters become structured experience, then distilled into reusable knowledge. -```json -{ - "mcpServers": { - "rolex": { - "command": "npx", - "args": ["-y", "@rolexjs/mcp-server"] - } - } -} ``` +encounter → reflect → experience → realize / master → principle / procedure +``` + +| Tool | What it does | +|------|-------------| +| `reflect` | Digest encounters into experience — pattern recognition | +| `realize` | Distill experience into a principle — transferable truth | +| `master` | Distill experience into a procedure — reusable skill | +| `forget` | Remove outdated knowledge | + +### World Management — via `use` + +Manage the society structure through the unified `use` tool with `!namespace.method` commands. + +**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` | Add a member | +| `!org.fire` | Remove a member | +| `!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` | Appoint an individual (inherits required skills) | +| `!position.dismiss` | Remove an individual from a position | +| `!position.abolish` | Archive a position | + +**Census** — society-level queries + +| Command | What it does | +|---------|-------------| +| `!census.list` | List all individuals, organizations, positions | +| `!census.list { type: "individual" }` | Filter by type | +| `!census.list { type: "past" }` | View archived entities | + +### Skill System — Progressive Disclosure + +Skills load on demand, keeping the agent's context lean: -Restart Claude Desktop after saving. +1. **Procedure** (always loaded) — metadata: what the skill is, when to use it +2. **Skill** (on demand) — full instructions loaded via `skill(locator)` +3. **Resource** (on demand) — external resources loaded via `use(locator)` + +### Resource System — 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 handles the full lifecycle: + +**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 | + +**Application** — load and use + +| Command | What it does | +|---------|-------------| +| `skill(locator)` | Load full skill instructions on demand | +| `use(locator)` | Execute or ingest any resource | +| `!resource.info` | Inspect a resource | + +This is how agent knowledge scales beyond a single individual — skills authored once can be distributed to any agent through prototypes and registries. + +## Quick Start + +Install the MCP server and connect it to your AI client. Then say **"activate nuwa"** — she will guide you from there. ### Claude Code @@ -85,7 +176,9 @@ Restart Claude Desktop after saving. claude mcp add rolex -- npx -y @rolexjs/mcp-server ``` -Or add to your project's `.mcp.json`: +### Claude Desktop + +Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): ```json { @@ -113,14 +206,15 @@ Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): } ``` -### Windsurf +### VS Code -Edit `~/.codeium/windsurf/mcp_config.json`: +Add to `.vscode/mcp.json`: ```json { - "mcpServers": { + "servers": { "rolex": { + "type": "stdio", "command": "npx", "args": ["-y", "@rolexjs/mcp-server"] } @@ -128,15 +222,14 @@ Edit `~/.codeium/windsurf/mcp_config.json`: } ``` -### VS Code +### Windsurf -Add to `.vscode/mcp.json`: +Edit `~/.codeium/windsurf/mcp_config.json`: ```json { - "servers": { + "mcpServers": { "rolex": { - "type": "stdio", "command": "npx", "args": ["-y", "@rolexjs/mcp-server"] } @@ -176,57 +269,47 @@ Add to Zed's `settings.json`: } ``` -## After Installation +## Gherkin — The Universal Language -Start a conversation with your AI and say: +Everything in RoleX is expressed as Gherkin Features: -> Activate nuwa +```gherkin +Feature: Sean + A backend architect who builds AI agent frameworks. -nuwa is the genesis role. She will bootstrap the environment and guide you through creating your own roles, organizations, and knowledge systems. + Scenario: Background + Given I am a software engineer + And I specialize in systems design +``` -## MCP Tools +Goals, plans, tasks, principles, procedures, encounters, experiences — all Gherkin. This means: -RoleX provides 15 tools through the MCP server, organized in three layers: +- **Human-readable** — anyone can understand an agent's state +- **Structured** — parseable, diffable, versionable +- **Composable** — Features compose naturally into larger systems -| 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 | +## Storage -## Packages +RoleX persists everything in SQLite at `~/.deepractice/rolex/`: -| 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/ ``` +~/.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 | --- @@ -234,10 +317,9 @@ RoleX stores everything in a `.rolex/` directory:

Ecosystem

Part of the Deepractice AI Agent infrastructure:

- AgentX · - PromptX · - ResourceX · - RoleX + ResourceX · + RoleX · + CommonX

diff --git a/README.zh-CN.md b/README.zh-CN.md index b5fa0c2..dd5004a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,16 +1,10 @@

RoleX

- AI 智能体角色驱动开发(RDD)框架 -

-

Role-Driven Development Framework for AI Agents

- -

- 持久身份 · 目标驱动 · Gherkin 原生 · MCP 即用 -

-

- Persistent Identity · Goal-Driven · Gherkin-Native · MCP Ready + AI 智能体社会化框架

+

Social Framework for AI Agents

+

以人类社会为模型,赋予 AI 智能体持久身份、社会结构和经验成长。

Stars @@ -27,57 +21,154 @@ --- -RoleX 让 AI 智能体拥有持久化的身份、目标、计划和任务 — 全部用 Gherkin `.feature` 文件表达。AI 不再每次对话都从零开始,而是记住自己是谁、正在做什么。 +## 为什么是"社会化"? -RoleX 从 [PromptX](https://github.com/Deepractice/PromptX) 演进而来 — 以 Gherkin 原生身份和目标驱动重新定义 AI 角色管理。 +人类社会解决了一个 AI 智能体至今没有解决的问题:**如何组织、成长和延续**。 -## 核心概念 +在社会中,人拥有身份、加入组织、担任职位、积累经验、传递知识。RoleX 将同样的模型带入 AI 智能体世界: -**一切皆 Gherkin。** 身份、知识、目标、计划、任务 — 一种格式,一种语言。 +- **身份** — 智能体跨会话知道自己是谁,而不仅仅是在一次对话中 +- **组织** — 智能体属于组织、担任职位、承担职责 +- **成长** — 经验积累为原则和可复用的技能 +- **持久化** — 目标、计划和知识在对话之间延续 + +一切都用 **Gherkin** `.feature` 格式表达 — 人类可读、结构化、可版本管理。 + +## 架构 + +RoleX 模拟了一个 AI 智能体的**社会**,映射人类组织的运作方式: -```text -社会 (Rolex) # 顶层:创造角色、建立组织 - └── 组织 # 团队结构:雇佣/解雇角色 - └── 角色 # 第一人称:身份、目标、计划、任务 +``` +社会 (Society) +├── 个体 (Individual) # 拥有身份、目标和知识的智能体 +├── 组织 (Organization) # 通过成员关系聚合个体 +├── 职位 (Position) # 定义职责和所需技能 +└── 归档 (Past) # 已退休/解散的实体存档 ``` -### 角色的五个维度 +## 四大系统 -| 维度 | 含义 | 示例 | -| -------- | ---------------------------------- | --------------------------------------- | -| **身份** | 我是谁 — 人格、知识、经验、语气 | "我是 Sean,一名后端架构师" | -| **目标** | 我要达成什么 — 带有成功标准 | "构建用户认证系统" | -| **计划** | 我怎么做 — 分阶段执行策略 | "阶段1:Schema,阶段2:API,阶段3:JWT" | -| **任务** | 具体工作项 — 可直接执行 | "实现 POST /api/auth/register" | -| **技能** | 我能做什么 — AI 自带能力,无需教授 | 工具使用、代码生成 | +RoleX 包含四个核心系统,各自服务于智能体生命周期的不同阶段。 -### 工作流程 +### 执行系统 — 做事循环 -1. **激活女娲(创世角色)** — 她会引导一切 -2. 女娲创造角色、传授知识、建立组织 -3. 每个角色自主工作:设定目标、制定计划、执行任务 -4. 经验积累成为身份的一部分 — 角色不断成长 +目标驱动的工作生命周期。智能体声明想要什么,规划如何达成,然后执行。 -## 快速开始 +``` +activate → want → plan → todo → finish → complete / abandon +``` -安装 MCP 服务器并连接到你的 AI 客户端,就这么简单 — 女娲会从那里引导你。 +| 工具 | 作用 | +|------|------| +| `activate` | 进入角色 — 加载身份、目标、知识 | +| `focus` | 查看或切换当前目标 | +| `want` | 声明一个目标及成功标准 | +| `plan` | 将目标拆解为阶段(支持顺序和备选策略) | +| `todo` | 在计划下创建具体任务 | +| `finish` | 完成任务,可选记录发生了什么 | +| `complete` | 完成计划 — 策略成功 | +| `abandon` | 放弃计划 — 策略失败,但学习被保留 | -### Claude Desktop +### 认知系统 — 成长循环 -编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`(macOS)或 `%APPDATA%\Claude\claude_desktop_config.json`(Windows): +智能体如何成长。原始经历变为结构化经验,再提炼为可复用的知识。 -```json -{ - "mcpServers": { - "rolex": { - "command": "npx", - "args": ["-y", "@rolexjs/mcp-server"] - } - } -} +``` +经历 → reflect → 经验 → realize / master → 原则 / 技能 ``` -保存后重启 Claude Desktop。 +| 工具 | 作用 | +|------|------| +| `reflect` | 将经历消化为经验 — 模式识别 | +| `realize` | 将经验提炼为原则 — 可迁移的道理 | +| `master` | 将经验沉淀为技能 — 可复用的操作 | +| `forget` | 移除过时的知识 | + +### 世界管理 — 通过 `use` + +通过统一的 `use` 工具以 `!命名空间.方法` 格式管理社会结构。 + +**个体 (Individual)** — 智能体生命周期 + +| 命令 | 作用 | +|------|------| +| `!individual.born` | 创建个体 | +| `!individual.teach` | 注入原则(知识) | +| `!individual.train` | 注入技能(操作) | +| `!individual.retire` | 归档个体 | + +**组织 (Organization)** — 组织结构 + +| 命令 | 作用 | +|------|------| +| `!org.found` | 创建组织 | +| `!org.charter` | 定义使命和章程 | +| `!org.hire` | 招聘成员 | +| `!org.fire` | 移除成员 | +| `!org.dissolve` | 解散组织 | + +**职位 (Position)** — 角色与职责 + +| 命令 | 作用 | +|------|------| +| `!position.establish` | 设立职位 | +| `!position.charge` | 赋予职责 | +| `!position.require` | 声明所需技能 — 任命时自动培训 | +| `!position.appoint` | 任命个体(继承所需技能) | +| `!position.dismiss` | 免除职位 | +| `!position.abolish` | 废除职位 | + +**普查 (Census)** — 社会级查询 + +| 命令 | 作用 | +|------|------| +| `!census.list` | 列出所有个体、组织、职位 | +| `!census.list { type: "individual" }` | 按类型过滤 | +| `!census.list { type: "past" }` | 查看归档实体 | + +### 技能系统 — 渐进式加载 + +技能按需加载,保持智能体上下文精简: + +1. **技能元数据 (Procedure)** (始终加载)— 技能是什么,何时使用 +2. **技能详情 (Skill)** (按需)— 通过 `skill(locator)` 加载完整指令 +3. **外部资源 (Resource)** (按需)— 通过 `use(locator)` 加载外部资源 + +### 资源系统 — 智能体的生产资料 + +资源是 AI 智能体的**生产资料** — 技能、原型、知识包,可以积累、共享、跨智能体和团队复用。 + +基于 [ResourceX](https://github.com/Deepractice/ResourceX) 驱动,资源系统覆盖完整生命周期: + +**生产** — 创建和打包 + +| 命令 | 作用 | +|------|------| +| `!resource.add` | 注册本地资源 | +| `!prototype.summon` | 从源拉取并注册原型 | +| `!prototype.banish` | 注销原型 | + +**分发** — 共享和获取 + +| 命令 | 作用 | +|------|------| +| `!resource.push` | 发布资源到注册中心 | +| `!resource.pull` | 从注册中心下载资源 | +| `!resource.search` | 搜索可用资源 | + +**应用** — 加载和使用 + +| 命令 | 作用 | +|------|------| +| `skill(locator)` | 按需加载完整技能指令 | +| `use(locator)` | 执行或加载任意资源 | +| `!resource.info` | 查看资源详情 | + +这就是智能体知识如何超越单个个体进行规模化的机制 — 一次编写的技能可以通过原型和注册中心分发给任何智能体。 + +## 快速开始 + +安装 MCP 服务器并连接到你的 AI 客户端,然后说 **"激活女娲"** — 她会引导你完成一切。 ### Claude Code @@ -85,7 +176,9 @@ RoleX 从 [PromptX](https://github.com/Deepractice/PromptX) 演进而来 — 以 claude mcp add rolex -- npx -y @rolexjs/mcp-server ``` -或在项目根目录添加 `.mcp.json`: +### Claude Desktop + +编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`(macOS)或 `%APPDATA%\Claude\claude_desktop_config.json`(Windows): ```json { @@ -98,6 +191,8 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` +保存后重启 Claude Desktop。 + ### Cursor 添加到 `.cursor/mcp.json`(项目级)或 `~/.cursor/mcp.json`(全局): @@ -113,14 +208,15 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` -### Windsurf +### VS Code -编辑 `~/.codeium/windsurf/mcp_config.json`: +添加到 `.vscode/mcp.json`: ```json { - "mcpServers": { + "servers": { "rolex": { + "type": "stdio", "command": "npx", "args": ["-y", "@rolexjs/mcp-server"] } @@ -128,15 +224,14 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` -### VS Code +### Windsurf -添加到 `.vscode/mcp.json`: +编辑 `~/.codeium/windsurf/mcp_config.json`: ```json { - "servers": { + "mcpServers": { "rolex": { - "type": "stdio", "command": "npx", "args": ["-y", "@rolexjs/mcp-server"] } @@ -176,57 +271,47 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` -## 安装之后 +## Gherkin — 统一语言 -和你的 AI 对话,说: +RoleX 中的一切都用 Gherkin Feature 表达: -> 激活女娲 +```gherkin +Feature: Sean + 一名构建 AI 智能体框架的后端架构师。 -女娲是创世角色。她会自动初始化环境,并引导你创建自己的角色、组织和知识体系。 + Scenario: 背景 + Given 我是一名软件工程师 + And 我专注于系统设计 +``` -## MCP 工具 +目标、计划、任务、原则、技能、经历、经验 — 全部是 Gherkin。这意味着: -RoleX 通过 MCP 服务器提供 15 个工具,分为三层: +- **人类可读** — 任何人都能理解智能体的状态 +- **结构化** — 可解析、可对比、可版本管理 +- **可组合** — Feature 自然地组合成更大的系统 -| 层级 | 工具 | 使用者 | -| -------- | ----------------------------------------------------------------------------------------- | -------- | -| **社会** | `society`(born, found, directory, find, teach) | 仅女娲 | -| **组织** | `organization`(hire, fire) | 仅女娲 | -| **角色** | `identity`, `focus`, `want`, `plan`, `todo`, `achieve`, `abandon`, `finish`, `synthesize` | 所有角色 | +## 存储 -## 包结构 +RoleX 将所有数据持久化在 SQLite 中,位于 `~/.deepractice/rolex/`: -| 包 | 描述 | -| ------------------------- | ---------------------------------------------- | -| `@rolexjs/core` | 核心类型和 Platform 接口 | -| `@rolexjs/parser` | Gherkin 解析器(封装 @cucumber/gherkin) | -| `@rolexjs/local-platform` | 基于文件系统的存储实现 | -| `rolexjs` | 主包 — Rolex + Organization + Role + bootstrap | -| `@rolexjs/mcp-server` | 面向 AI 客户端的 MCP 服务器 | -| `@rolexjs/cli` | 命令行工具 | - -## 存储结构 - -RoleX 将所有数据存储在 `.rolex/` 目录中: - -```text -.rolex/ -├── rolex.json # 组织配置 -├── alex/ -│ ├── identity/ -│ │ ├── persona.identity.feature # 我是谁 -│ │ ├── arch.knowledge.identity.feature # 我知道什么 -│ │ └── v1.experience.identity.feature # 我学到了什么 -│ └── goals/ -│ └── auth-system/ -│ ├── auth-system.goal.feature # 我要做什么 -│ ├── auth-system.plan.feature # 我怎么做 -│ └── tasks/ -│ └── register.task.feature # 具体工作 -└── bob/ - ├── identity/ - └── goals/ ``` +~/.deepractice/rolex/ +├── rolex.db # SQLite — 唯一数据源 +├── prototype.json # 原型注册表 +└── context/ # 角色上下文(每个角色的当前焦点目标/计划) +``` + +## 包结构 + +| 包 | 描述 | +|----|------| +| `rolexjs` | 核心 API — Rolex 类、命名空间、渲染 | +| `@rolexjs/mcp-server` | 面向 AI 客户端的 MCP 服务器 | +| `@rolexjs/core` | 核心类型、结构定义、平台接口 | +| `@rolexjs/system` | 运行时接口、状态合并、原型 | +| `@rolexjs/parser` | Gherkin 解析器 | +| `@rolexjs/local-platform` | 基于 SQLite 的运行时实现 | +| `@rolexjs/cli` | 命令行工具 | --- @@ -234,10 +319,9 @@ RoleX 将所有数据存储在 `.rolex/` 目录中:

生态系统

Deepractice AI 智能体基础设施:

- AgentX · - PromptX · - ResourceX · - RoleX + ResourceX · + RoleX · + CommonX

diff --git a/bun.lock b/bun.lock index 3008d69..db41eef 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "rolexjs", "dependencies": { "@resourcexjs/node-provider": "^2.14.0", - "resourcexjs": "^2.14.0", + "resourcexjs": "^2.15.0", }, "devDependencies": { "@biomejs/biome": "^2.4.0", @@ -61,10 +61,13 @@ "name": "@rolexjs/local-platform", "version": "0.11.0", "dependencies": { + "@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:*", + "drizzle-orm": "^0.45.1", "resourcexjs": "^2.14.0", }, }, @@ -212,6 +215,10 @@ "@cucumber/messages": ["@cucumber/messages@32.0.1", "", { "dependencies": { "class-transformer": "0.5.1", "reflect-metadata": "0.2.2" } }, "sha512-1OSoW+GQvFUNAl6tdP2CTBexTXMNJF0094goVUcvugtQeXtJ0K8sCP0xbq7GGoiezs/eJAAOD03+zAPT64orHQ=="], + "@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=="], @@ -344,7 +351,7 @@ "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], - "@resourcexjs/arp": ["@resourcexjs/arp@2.14.0", "https://registry.npmmirror.com/@resourcexjs/arp/-/arp-2.14.0.tgz", {}, "sha512-Kw3g7R3/Fkv3ce2Nl31vgv6PLwNJPXkbeya5//rpuR32LyXcWogF0lrGCskcepqhyJnMeAXOO5ZX0paJBVU1Yw=="], + "@resourcexjs/arp": ["@resourcexjs/arp@2.15.0", "https://registry.npmmirror.com/@resourcexjs/arp/-/arp-2.15.0.tgz", {}, "sha512-6OqzFbvGmbhploFgBHdK+G/gA5Xihsq4+8oBGBgyDtiLJ5vfyVpwZODBbnV9jhqoE/vkFNBhn6b9etn8a7NDJA=="], "@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=="], @@ -560,6 +567,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=="], @@ -934,7 +943,7 @@ "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "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=="], + "resourcexjs": ["resourcexjs@2.15.0", "https://registry.npmmirror.com/resourcexjs/-/resourcexjs-2.15.0.tgz", { "dependencies": { "@resourcexjs/arp": "^2.15.0", "@resourcexjs/core": "^2.15.0", "sandboxxjs": "^0.5.1" } }, "sha512-sPwl9spKSsBYVXQU2NGnjGYGXpBA1mztHAgXVEXHmLtZLwy5+aveGd7iP0XfhdSC4RaVee14CybdFZPAXTXnsQ=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], @@ -1106,6 +1115,8 @@ "@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/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=="], @@ -1132,8 +1143,12 @@ "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "resourcexjs/@resourcexjs/core": ["@resourcexjs/core@2.15.0", "https://registry.npmmirror.com/@resourcexjs/core/-/core-2.15.0.tgz", { "dependencies": { "modern-tar": "^0.7.3", "zod": "^4.3.6" } }, "sha512-jr0r734ugLriuF/mWb3+/zyHyG0rDKarvdCFXE0+aBlTLCJbKFReq9vXbszTXyT/kPJCeoZ/9X3Sxgkkdc+rlg=="], + "tsup/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "@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=="], + "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 +1173,8 @@ "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "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 0193c69..e8c3551 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@resourcexjs/node-provider": "^2.14.0", - "resourcexjs": "^2.14.0" + "resourcexjs": "^2.15.0" }, "overrides": { "resourcexjs": "^2.14.0", diff --git a/packages/local-platform/package.json b/packages/local-platform/package.json index 8dd13ee..043b8a8 100644 --- a/packages/local-platform/package.json +++ b/packages/local-platform/package.json @@ -20,11 +20,14 @@ "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.14.0", - "@resourcexjs/node-provider": "^2.14.0" + "@rolexjs/system": "workspace:*", + "drizzle-orm": "^0.45.1", + "resourcexjs": "^2.14.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index fa4091a..6cb1594 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -1,43 +1,27 @@ /** - * 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 (single source of truth for runtime graph) + * {dataDir}/prototype.json — prototype registry + * {dataDir}/context/.json — role context persistence * - * 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). + * Runtime: SQLite-backed via Drizzle ORM (no in-memory Map, no load/save cycle). + * When dataDir is null, runs with in-memory SQLite (useful for tests). */ -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } 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 { ContextData, Platform } from "@rolexjs/core"; import { organizationType, roleType } from "@rolexjs/resourcex-types"; -import type { Prototype, Runtime, State, Structure } from "@rolexjs/system"; +import type { Prototype, State } from "@rolexjs/system"; +import { sql } from "drizzle-orm"; 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 { createSqliteRuntime } from "./sqliteRuntime.js"; // ===== Config ===== @@ -48,6 +32,36 @@ export interface LocalPlatformConfig { resourceDir?: string | null; } +// ===== DDL ===== + +const CREATE_NODES = 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 +)`; + +const CREATE_LINKS = 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) +)`; + +const CREATE_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)`, +]; + +// ===== Factory ===== + /** Create a local Platform. Persistent by default (~/.deepractice/rolex), in-memory if dataDir is null. */ export function localPlatform(config: LocalPlatformConfig = {}): Platform { const dataDir = @@ -55,392 +69,29 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { ? 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.tag ? { tag: state.tag } : {}), - ...(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 }); - } + // ===== SQLite database ===== - return results; - }; - - const load = () => { - if (!dataDir) return; - - // 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 links from manifests (all nodes, not just root) - const collectLinks = ( - nodeId: string, - node: { - links?: Record; - children?: Record; - } - ) => { - if (node.links) { - const sourceRef = idToRef.get(nodeId); - if (sourceRef) { - const entries: LinkEntry[] = links.get(sourceRef) ?? []; - for (const [relation, targetIds] of Object.entries(node.links)) { - for (const targetId of targetIds) { - const targetRef = idToRef.get(targetId); - if ( - targetRef && - !entries.some((l) => l.toId === targetRef && l.relation === relation) - ) { - entries.push({ toId: targetRef, relation }); - } - } - } - if (entries.length > 0) links.set(sourceRef, entries); - } - } - if (node.children) { - for (const [childId, childNode] of Object.entries(node.children)) { - collectLinks(childId, childNode); - } - } - }; - - for (const { manifest } of entityRefs) { - collectLinks(manifest.id, manifest); - } - }; - - 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"); - } - }; - - 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; + const rawDb = openDatabase(dbPath); + const db = drizzle(rawDb); - 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 - } - }; + // Ensure tables exist + db.run(CREATE_NODES); + db.run(CREATE_LINKS); + for (const idx of CREATE_INDEXES) { + db.run(idx); + } // ===== 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(); - }, - - tag(node, tagValue) { - load(); - 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; - 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 runtime = createSqliteRuntime(db); // ===== ResourceX ===== @@ -457,7 +108,7 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { /** Built-in prototypes — always available, cannot be overridden. */ const BUILTINS: Record = { - nuwa: "nuwa:0.1.0", + nuwa: "nuwa", }; const registryPath = dataDir ? join(dataDir, "prototype.json") : undefined; @@ -510,7 +161,6 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { }; // ===== Context persistence ===== - // Context lives outside role/ to survive save()'s clean-and-rebuild cycle. const saveContext = (roleId: string, data: ContextData): void => { if (!dataDir) return; 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 index f0a2be7..429d614 100644 --- a/packages/local-platform/src/sqliteRuntime.ts +++ b/packages/local-platform/src/sqliteRuntime.ts @@ -5,12 +5,12 @@ * 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 type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; import { links, nodes } from "./schema.js"; -type DB = BunSQLiteDatabase; +type DB = CommonXDatabase; // ===== Helpers ===== @@ -197,8 +197,9 @@ export function createSqliteRuntime(db: DB): Runtime { tag(node, tagValue) { if (!node.ref) throw new Error("Node has no ref"); - const updated = db.update(nodes).set({ tag: tagValue }).where(eq(nodes.ref, node.ref)).run(); - if (updated.changes === 0) throw new Error(`Node not found: ${node.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(); }, project(node) { diff --git a/packages/local-platform/tests/prototype.test.ts b/packages/local-platform/tests/prototype.test.ts index c1cf4fd..4d556ec 100644 --- a/packages/local-platform/tests/prototype.test.ts +++ b/packages/local-platform/tests/prototype.test.ts @@ -165,6 +165,6 @@ describe("LocalPlatform Prototype (registry-based)", () => { test("builtin nuwa is always present in list", () => { const platform = localPlatform({ dataDir: testDir, resourceDir }); const list = platform.prototype!.list(); - expect(list.nuwa).toBe("https://github.com/Deepractice/DeepracticeX/tree/main/roles/nuwa"); + expect(list.nuwa).toBe("nuwa"); }); }); diff --git a/packages/local-platform/tests/sqliteRuntime.test.ts b/packages/local-platform/tests/sqliteRuntime.test.ts new file mode 100644 index 0000000..f7855a4 --- /dev/null +++ b/packages/local-platform/tests/sqliteRuntime.test.ts @@ -0,0 +1,156 @@ +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 { links, nodes } from "../src/schema.js"; +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", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + expect(society.ref).toBe("n1"); + expect(society.name).toBe("society"); + }); + + test("create child node", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + const ind = 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", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); + rt.create(ind, C.identity, undefined, "identity"); + + const state = 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", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); + rt.create(ind, C.identity, undefined, "identity"); + + rt.remove(ind); + const state = rt.project(society); + expect(state.children).toHaveLength(0); + }); + + test("link and unlink", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + const org = rt.create(society, C.organization, "Feature: DP", "dp"); + const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); + + rt.link(org, ind, "membership", "belong"); + + let state = rt.project(org); + expect(state.links).toHaveLength(1); + expect(state.links![0].relation).toBe("membership"); + expect(state.links![0].target.id).toBe("sean"); + + rt.unlink(org, ind, "membership", "belong"); + state = rt.project(org); + expect(state.links).toBeUndefined(); + }); + + test("tag node", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + const goal = rt.create(society, C.goal, "Feature: Test", "test-goal"); + rt.tag(goal, "done"); + + const state = rt.project(goal); + expect(state.tag).toBe("done"); + }); + + test("roots returns only root nodes", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + rt.create(society, C.individual, "Feature: Sean", "sean"); + + const roots = rt.roots(); + expect(roots).toHaveLength(1); + expect(roots[0].name).toBe("society"); + }); + + test("transform creates node under target parent", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + rt.create(society, C.past); + const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); + + // Transform: create a "past" typed node (archive) — finds the past container by name + const archived = 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)", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); + const org = rt.create(society, C.organization, "Feature: DP", "dp"); + + // Multiple operations — society ref should always be valid + rt.link(org, ind, "membership", "belong"); + rt.create(org, C.charter, "Feature: Mission"); + + // project using the original society ref — should work + const state = rt.project(society); + expect(state.children).toHaveLength(2); // individual + organization + }); + + test("position persists (the bug that triggered this rewrite)", () => { + const { rt } = setup(); + const society = rt.create(null, C.society); + const pos = rt.create(society, C.position, "Feature: Architect", "architect"); + + // Project using the returned ref — this used to fail with "Node not found" + const state = rt.project(pos); + expect(state.name).toBe("position"); + expect(state.id).toBe("architect"); + + // Also visible from society + const societyState = rt.project(society); + const names = societyState.children!.map((c) => c.name); + expect(names).toContain("position"); + }); +}); diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 14d325e..3c57101 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -223,6 +223,8 @@ export class Rolex { return [a.content, a.id, a.alias]; case "position.charge": return [a.position, a.content, a.id]; + case "position.require": + return [a.position, a.content, a.id]; case "position.abolish": return [a.position]; case "position.appoint": @@ -745,6 +747,21 @@ class PositionNamespace { return ok(this.rt, node, "charge"); } + // ---- Skill requirements ---- + + /** Require: declare that this position requires a skill. Upserts by id. */ + require(position: string, procedure: string, id?: string): RolexResult { + validateGherkin(procedure); + const parent = this.resolve(position); + if (id) { + const state = this.rt.project(parent); + const existing = findInState(state, id); + if (existing) this.rt.remove(existing); + } + const proc = this.rt.create(parent, C.procedure, procedure, id); + return ok(this.rt, proc, "require"); + } + // ---- Archival ---- /** Abolish a position. */ @@ -754,10 +771,25 @@ class PositionNamespace { // ---- Appointment ---- - /** Appoint: link individual to position via appointment. */ + /** Appoint: link individual to position via appointment. Auto-trains required skills. */ appoint(position: string, individual: string): RolexResult { const posNode = this.resolve(position); - this.rt.link(posNode, this.resolve(individual), "appointment", "serve"); + const indNode = this.resolve(individual); + this.rt.link(posNode, indNode, "appointment", "serve"); + + // Auto-train: inject required procedures into the individual + const posState = this.rt.project(posNode); + const required = (posState.children ?? []).filter((c) => c.name === "procedure"); + for (const proc of required) { + if (proc.id) { + // Upsert: remove existing procedure with same id + const indState = this.rt.project(indNode); + const existing = findInState(indState, proc.id); + if (existing) this.rt.remove(existing); + } + this.rt.create(indNode, C.procedure, proc.information, proc.id); + } + return ok(this.rt, posNode, "appoint"); } diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index 6982768..49d7053 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -152,6 +152,54 @@ describe("Rolex API (stateless)", () => { const r = rolex.position.dismiss("pos1", "sean"); expect(r.state.links).toBeUndefined(); }); + + test("require adds procedure to position", () => { + const rolex = setup(); + rolex.position.establish(undefined, "architect"); + const r = rolex.position.require( + "architect", + "Feature: System Design\n Scenario: Design APIs", + "system-design" + ); + expect(r.state.name).toBe("procedure"); + expect(r.state.id).toBe("system-design"); + expect(r.process).toBe("require"); + }); + + test("require upserts by id", () => { + const rolex = setup(); + rolex.position.establish(undefined, "architect"); + rolex.position.require("architect", "Feature: Old skill", "skill-1"); + rolex.position.require("architect", "Feature: Updated skill", "skill-1"); + const pos = rolex.find("architect")!; + const procedures = (pos as any).children?.filter((c: any) => c.name === "procedure"); + expect(procedures).toHaveLength(1); + expect(procedures[0].information).toBe("Feature: Updated skill"); + }); + + test("appoint auto-trains required skills to individual", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.position.establish(undefined, "architect"); + rolex.position.require("architect", "Feature: System Design", "system-design"); + rolex.position.require("architect", "Feature: Code Review", "code-review"); + rolex.position.appoint("architect", "sean"); + + // Individual should now have the required procedures + const sean = rolex.find("sean")!; + const procedures = (sean as any).children?.filter((c: any) => c.name === "procedure"); + expect(procedures).toHaveLength(2); + const ids = procedures.map((p: any) => p.id).sort(); + expect(ids).toEqual(["code-review", "system-design"]); + }); + + test("appoint without required skills still works", () => { + const rolex = setup(); + rolex.individual.born(undefined, "sean"); + rolex.position.establish(undefined, "pos1"); + const r = rolex.position.appoint("pos1", "sean"); + expect(r.state.links).toHaveLength(1); + }); }); // ============================================================ From 7038d00f89c6ef2e000c65bdcd9d87eafd18c690 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 17:14:42 +0800 Subject: [PATCH 24/54] feat: unify prototype namespace, add settle/evict, initializer, and requirement type - Merge AuthorNamespace into PrototypeNamespace (born/teach/train + found/charter/establish/charge/require) - Rename summon/banish to settle/evict for prototype registry - Add Initializer interface and LocalPlatform implementation for bootstrap - Add requirement as independent structure type (avoid JS reserved word 'require') - Add member method and members/appointments fields to PrototypeManifest - Delete author-specific MCP tools and description files - Update CLI, MCP server, and world descriptions - Update README with simplified documentation Co-Authored-By: Claude Opus 4.6 --- README.md | 321 ++++++++++-------- README.zh-CN.md | 321 ++++++++++-------- apps/cli/src/index.ts | 59 ++-- apps/mcp-server/src/index.ts | 46 +-- packages/core/src/index.ts | 2 + packages/core/src/platform.ts | 5 +- packages/core/src/structures.ts | 1 + packages/local-platform/src/LocalPlatform.ts | 52 ++- .../local-platform/tests/prototype.test.ts | 40 ++- .../descriptions/author/author-born.feature | 17 - .../descriptions/author/author-teach.feature | 15 - .../descriptions/author/author-train.feature | 16 - packages/rolexjs/src/descriptions/index.ts | 9 +- .../src/descriptions/prototype/banish.feature | 9 - .../src/descriptions/prototype/evict.feature | 9 + .../src/descriptions/prototype/settle.feature | 10 + .../src/descriptions/prototype/summon.feature | 10 - .../descriptions/world/use-protocol.feature | 27 +- packages/rolexjs/src/rolex.ts | 287 +++++++++++++--- packages/rolexjs/tests/author.test.ts | 249 +++++++++++++- packages/rolexjs/tests/rolex.test.ts | 10 +- packages/system/src/index.ts | 4 + packages/system/src/initializer.ts | 14 + packages/system/src/prototype.ts | 12 +- packages/system/tests/prototype.test.ts | 14 +- 25 files changed, 995 insertions(+), 564 deletions(-) delete mode 100644 packages/rolexjs/src/descriptions/author/author-born.feature delete mode 100644 packages/rolexjs/src/descriptions/author/author-teach.feature delete mode 100644 packages/rolexjs/src/descriptions/author/author-train.feature delete mode 100644 packages/rolexjs/src/descriptions/prototype/banish.feature create mode 100644 packages/rolexjs/src/descriptions/prototype/evict.feature create mode 100644 packages/rolexjs/src/descriptions/prototype/settle.feature delete mode 100644 packages/rolexjs/src/descriptions/prototype/summon.feature create mode 100644 packages/system/src/initializer.ts diff --git a/README.md b/README.md index f0ab751..47e40ec 100644 --- a/README.md +++ b/README.md @@ -34,149 +34,21 @@ In a society, people have identities, join organizations, hold positions, accumu Everything is expressed in **Gherkin** `.feature` format — human-readable, structured, versionable. -## Architecture - -RoleX models a **society** of AI agents, mirroring how human organizations work: - -``` -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 -``` - -## Systems - -RoleX has four core systems. Each serves a distinct purpose in the agent lifecycle. - -### Execution — The Doing Cycle - -Goal-driven work lifecycle. The agent declares what it wants, plans how to get there, and executes. - -``` -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 | - -### Cognition — The Learning Cycle - -How agents grow. Raw encounters become structured experience, then distilled into reusable knowledge. - -``` -encounter → reflect → experience → realize / master → principle / procedure -``` - -| Tool | What it does | -|------|-------------| -| `reflect` | Digest encounters into experience — pattern recognition | -| `realize` | Distill experience into a principle — transferable truth | -| `master` | Distill experience into a procedure — reusable skill | -| `forget` | Remove outdated knowledge | - -### World Management — via `use` - -Manage the society structure through the unified `use` tool with `!namespace.method` commands. - -**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` | Add a member | -| `!org.fire` | Remove a member | -| `!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` | Appoint an individual (inherits required skills) | -| `!position.dismiss` | Remove an individual from a position | -| `!position.abolish` | Archive a position | - -**Census** — society-level queries - -| Command | What it does | -|---------|-------------| -| `!census.list` | List all individuals, organizations, positions | -| `!census.list { type: "individual" }` | Filter by type | -| `!census.list { type: "past" }` | View archived entities | - -### Skill System — Progressive Disclosure - -Skills load on demand, keeping the agent's context lean: - -1. **Procedure** (always loaded) — metadata: what the skill is, when to use it -2. **Skill** (on demand) — full instructions loaded via `skill(locator)` -3. **Resource** (on demand) — external resources loaded via `use(locator)` - -### Resource System — 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 handles the full lifecycle: - -**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 | - -**Application** — load and use - -| Command | What it does | -|---------|-------------| -| `skill(locator)` | Load full skill instructions on demand | -| `use(locator)` | Execute or ingest any resource | -| `!resource.info` | Inspect a resource | - -This is how agent knowledge scales beyond a single individual — skills authored once can be distributed to any agent through prototypes and registries. - ## Quick Start -Install the MCP server and connect it to your AI client. Then say **"activate nuwa"** — she will guide you from there. +Install the MCP server, connect it to your AI client, and say **"activate nuwa"** — she will guide you from there. -### Claude Code +
+Claude Code ```bash claude mcp add rolex -- npx -y @rolexjs/mcp-server ``` -### Claude Desktop +
+ +
+Claude Desktop Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): @@ -191,7 +63,10 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o } ``` -### Cursor +
+ +
+Cursor Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): @@ -206,7 +81,10 @@ Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): } ``` -### VS Code +
+ +
+VS Code Add to `.vscode/mcp.json`: @@ -222,7 +100,10 @@ Add to `.vscode/mcp.json`: } ``` -### Windsurf +
+ +
+Windsurf Edit `~/.codeium/windsurf/mcp_config.json`: @@ -237,7 +118,10 @@ Edit `~/.codeium/windsurf/mcp_config.json`: } ``` -### JetBrains IDEs +
+ +
+JetBrains IDEs Go to **Settings > Tools > AI Assistant > Model Context Protocol (MCP)**, click **+** and paste: @@ -252,7 +136,10 @@ Go to **Settings > Tools > AI Assistant > Model Context Protocol (MCP)**, click } ``` -### Zed +
+ +
+Zed Add to Zed's `settings.json`: @@ -269,6 +156,160 @@ Add to Zed's `settings.json`: } ``` +
+ +## How It Works + +**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. + +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. + +The tools fall into two categories: + +- **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. + +The following sections walk through each system in the order an agent encounters them. + +--- + +### 1. The World — Society Structure + +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: diff --git a/README.zh-CN.md b/README.zh-CN.md index dd5004a..59d6288 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -34,149 +34,21 @@ 一切都用 **Gherkin** `.feature` 格式表达 — 人类可读、结构化、可版本管理。 -## 架构 - -RoleX 模拟了一个 AI 智能体的**社会**,映射人类组织的运作方式: - -``` -社会 (Society) -├── 个体 (Individual) # 拥有身份、目标和知识的智能体 -├── 组织 (Organization) # 通过成员关系聚合个体 -├── 职位 (Position) # 定义职责和所需技能 -└── 归档 (Past) # 已退休/解散的实体存档 -``` - -## 四大系统 - -RoleX 包含四个核心系统,各自服务于智能体生命周期的不同阶段。 - -### 执行系统 — 做事循环 - -目标驱动的工作生命周期。智能体声明想要什么,规划如何达成,然后执行。 - -``` -activate → want → plan → todo → finish → complete / abandon -``` - -| 工具 | 作用 | -|------|------| -| `activate` | 进入角色 — 加载身份、目标、知识 | -| `focus` | 查看或切换当前目标 | -| `want` | 声明一个目标及成功标准 | -| `plan` | 将目标拆解为阶段(支持顺序和备选策略) | -| `todo` | 在计划下创建具体任务 | -| `finish` | 完成任务,可选记录发生了什么 | -| `complete` | 完成计划 — 策略成功 | -| `abandon` | 放弃计划 — 策略失败,但学习被保留 | - -### 认知系统 — 成长循环 - -智能体如何成长。原始经历变为结构化经验,再提炼为可复用的知识。 - -``` -经历 → reflect → 经验 → realize / master → 原则 / 技能 -``` - -| 工具 | 作用 | -|------|------| -| `reflect` | 将经历消化为经验 — 模式识别 | -| `realize` | 将经验提炼为原则 — 可迁移的道理 | -| `master` | 将经验沉淀为技能 — 可复用的操作 | -| `forget` | 移除过时的知识 | - -### 世界管理 — 通过 `use` - -通过统一的 `use` 工具以 `!命名空间.方法` 格式管理社会结构。 - -**个体 (Individual)** — 智能体生命周期 - -| 命令 | 作用 | -|------|------| -| `!individual.born` | 创建个体 | -| `!individual.teach` | 注入原则(知识) | -| `!individual.train` | 注入技能(操作) | -| `!individual.retire` | 归档个体 | - -**组织 (Organization)** — 组织结构 - -| 命令 | 作用 | -|------|------| -| `!org.found` | 创建组织 | -| `!org.charter` | 定义使命和章程 | -| `!org.hire` | 招聘成员 | -| `!org.fire` | 移除成员 | -| `!org.dissolve` | 解散组织 | - -**职位 (Position)** — 角色与职责 - -| 命令 | 作用 | -|------|------| -| `!position.establish` | 设立职位 | -| `!position.charge` | 赋予职责 | -| `!position.require` | 声明所需技能 — 任命时自动培训 | -| `!position.appoint` | 任命个体(继承所需技能) | -| `!position.dismiss` | 免除职位 | -| `!position.abolish` | 废除职位 | - -**普查 (Census)** — 社会级查询 - -| 命令 | 作用 | -|------|------| -| `!census.list` | 列出所有个体、组织、职位 | -| `!census.list { type: "individual" }` | 按类型过滤 | -| `!census.list { type: "past" }` | 查看归档实体 | - -### 技能系统 — 渐进式加载 - -技能按需加载,保持智能体上下文精简: - -1. **技能元数据 (Procedure)** (始终加载)— 技能是什么,何时使用 -2. **技能详情 (Skill)** (按需)— 通过 `skill(locator)` 加载完整指令 -3. **外部资源 (Resource)** (按需)— 通过 `use(locator)` 加载外部资源 - -### 资源系统 — 智能体的生产资料 - -资源是 AI 智能体的**生产资料** — 技能、原型、知识包,可以积累、共享、跨智能体和团队复用。 - -基于 [ResourceX](https://github.com/Deepractice/ResourceX) 驱动,资源系统覆盖完整生命周期: - -**生产** — 创建和打包 - -| 命令 | 作用 | -|------|------| -| `!resource.add` | 注册本地资源 | -| `!prototype.summon` | 从源拉取并注册原型 | -| `!prototype.banish` | 注销原型 | - -**分发** — 共享和获取 - -| 命令 | 作用 | -|------|------| -| `!resource.push` | 发布资源到注册中心 | -| `!resource.pull` | 从注册中心下载资源 | -| `!resource.search` | 搜索可用资源 | - -**应用** — 加载和使用 - -| 命令 | 作用 | -|------|------| -| `skill(locator)` | 按需加载完整技能指令 | -| `use(locator)` | 执行或加载任意资源 | -| `!resource.info` | 查看资源详情 | - -这就是智能体知识如何超越单个个体进行规模化的机制 — 一次编写的技能可以通过原型和注册中心分发给任何智能体。 - ## 快速开始 -安装 MCP 服务器并连接到你的 AI 客户端,然后说 **"激活女娲"** — 她会引导你完成一切。 +安装 MCP 服务器,连接到你的 AI 客户端,然后说 **"激活女娲"** — 她会引导你完成一切。 -### Claude Code +
+Claude Code ```bash claude mcp add rolex -- npx -y @rolexjs/mcp-server ``` -### Claude Desktop +
+ +
+Claude Desktop 编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`(macOS)或 `%APPDATA%\Claude\claude_desktop_config.json`(Windows): @@ -191,9 +63,10 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` -保存后重启 Claude Desktop。 +
-### Cursor +
+Cursor 添加到 `.cursor/mcp.json`(项目级)或 `~/.cursor/mcp.json`(全局): @@ -208,7 +81,10 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` -### VS Code +
+ +
+VS Code 添加到 `.vscode/mcp.json`: @@ -224,7 +100,10 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` -### Windsurf +
+ +
+Windsurf 编辑 `~/.codeium/windsurf/mcp_config.json`: @@ -239,7 +118,10 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` -### JetBrains IDEs +
+ +
+JetBrains IDEs 进入 **Settings > Tools > AI Assistant > Model Context Protocol (MCP)**,点击 **+** 粘贴: @@ -254,7 +136,10 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` -### Zed +
+ +
+Zed 添加到 Zed 的 `settings.json`: @@ -271,6 +156,160 @@ claude mcp add rolex -- npx -y @rolexjs/mcp-server } ``` +
+ +## 运作方式 + +**你不需要学任何命令。** 只需安装 MCP 服务器,然后用自然语言和 AI 对话 — "创建一个组织"、"设定一个目标"、"我学到了什么?"。AI 知道该调用哪些工具。 + +以下是**底层机制**。RoleX 通过 MCP 提供工具,由 AI 自主调用。了解机制有助于更好地使用,但操作这些工具是 AI 的事,不是你的。 + +工具分为两类: + +- **直接工具** — AI 按名称调用(如 `activate`、`want`、`plan`),是日常操作。 +- **`use` 工具** — 统一调度入口,以 `!命名空间.方法` 格式发送命令(如 `!org.found`、`!census.list`),是世界管理层。 + +以下按智能体接触它们的顺序,逐一介绍每个系统。 + +--- + +### 1. 世界 — 社会结构 + +智能体行动之前,需要先有一个世界。RoleX 建模了一个**社会**,包含四种实体: + +``` +社会 (Society) +├── 个体 (Individual) # 拥有身份、目标和知识的智能体 +├── 组织 (Organization) # 通过成员关系聚合个体 +├── 职位 (Position) # 定义职责和所需技能 +└── 归档 (Past) # 已退休/解散的实体存档 +``` + +所有世界管理操作通过 `use` 工具进行: + +**个体 (Individual)** — 智能体生命周期 + +| 命令 | 作用 | +|------|------| +| `!individual.born` | 创建个体 | +| `!individual.teach` | 注入原则(知识) | +| `!individual.train` | 注入技能(操作) | +| `!individual.retire` | 归档个体 | + +**组织 (Organization)** — 组织结构 + +| 命令 | 作用 | +|------|------| +| `!org.found` | 创建组织 | +| `!org.charter` | 定义使命和章程 | +| `!org.hire` / `!org.fire` | 招聘或移除成员 | +| `!org.dissolve` | 解散组织 | + +**职位 (Position)** — 角色与职责 + +| 命令 | 作用 | +|------|------| +| `!position.establish` | 设立职位 | +| `!position.charge` | 赋予职责 | +| `!position.require` | 声明所需技能 — 任命时自动培训 | +| `!position.appoint` / `!position.dismiss` | 任命或免除个体 | +| `!position.abolish` | 废除职位 | + +**普查 (Census)** — 查询世界 + +| 命令 | 作用 | +|------|------| +| `!census.list` | 列出所有个体、组织、职位 | +| `!census.list { type: "..." }` | 按类型过滤:`individual`、`organization`、`position`、`past` | + +--- + +### 2. 执行系统 — 做事循环 + +激活后,智能体通过结构化的生命周期追求目标。这些都是智能体按名称直接调用的**直接工具**: + +``` +activate → want → plan → todo → finish → complete / abandon +``` + +| 工具 | 作用 | +|------|------| +| `activate` | 进入角色 — 加载身份、目标、知识 | +| `focus` | 查看或切换当前目标 | +| `want` | 声明一个目标及成功标准 | +| `plan` | 将目标拆解为阶段(支持顺序和备选策略) | +| `todo` | 在计划下创建具体任务 | +| `finish` | 完成任务,可选记录发生了什么 | +| `complete` | 完成计划 — 策略成功 | +| `abandon` | 放弃计划 — 策略失败,但学习被保留 | + +--- + +### 3. 认知系统 — 成长循环 + +执行产生**经历** — 发生了什么的原始记录。认知系统将其转化为结构化知识。同样是**直接工具**: + +``` +经历 → reflect → 经验 → realize / master → 原则 / 技能 +``` + +| 工具 | 作用 | +|------|------| +| `reflect` | 将经历消化为经验 — 模式识别 | +| `realize` | 将经验提炼为原则 — 可迁移的道理 | +| `master` | 将经验沉淀为技能 — 可复用的操作 | +| `forget` | 移除过时的知识 | + +这就是智能体的成长方式。从一个项目中总结的原则适用于下一个项目,掌握一次的技能可以永远复用。 + +--- + +### 4. 技能系统 — 渐进式加载 + +智能体不可能一次加载所有技能到上下文。RoleX 采用三层渐进式加载模型: + +| 层级 | 加载时机 | 内容 | +|------|---------|------| +| **技能元数据 (Procedure)** | 始终加载(激活时) | 技能是什么、何时使用 | +| **技能详情 (Skill)** | 按需,通过 `skill(locator)` | 完整指令 — 具体怎么做 | +| **外部资源 (Resource)** | 按需,通过 `use(locator)` | 外部内容 — 模板、数据、工具 | + +`skill` 和 `use` 都是**直接工具**,用于按需加载内容。当 `use` 收到*不带* `!` 前缀的 locator 时,它从 [ResourceX](https://github.com/Deepractice/ResourceX) 加载资源,而非调度命令。 + +--- + +### 5. 资源系统 — 智能体的生产资料 + +资源是 AI 智能体的**生产资料** — 技能、原型、知识包,可以积累、共享、跨智能体和团队复用。 + +基于 [ResourceX](https://github.com/Deepractice/ResourceX) 驱动,资源系统通过 `use` 工具覆盖完整生命周期: + +**生产** — 创建和打包 + +| 命令 | 作用 | +|------|------| +| `!resource.add` | 注册本地资源 | +| `!prototype.summon` | 从源拉取并注册原型 | +| `!prototype.banish` | 注销原型 | + +**分发** — 共享和获取 + +| 命令 | 作用 | +|------|------| +| `!resource.push` | 发布资源到注册中心 | +| `!resource.pull` | 从注册中心下载资源 | +| `!resource.search` | 搜索可用资源 | + +**查看** + +| 命令 | 作用 | +|------|------| +| `!resource.info` | 查看资源详情 | + +这就是智能体知识如何超越单个个体进行规模化的机制 — 一次编写的技能可以通过原型和注册中心分发给任何智能体。 + +--- + ## Gherkin — 统一语言 RoleX 中的一切都用 Gherkin Feature 表达: diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 78e2b29..d8e7e37 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -18,6 +18,7 @@ import { createRoleX, describe, hint } from "rolexjs"; // ========== Setup ========== const rolex = createRoleX(localPlatform()); +await rolex.bootstrap(); // ========== Helpers ========== @@ -688,9 +689,9 @@ const resource = defineCommand({ }, }); -// ========== Author — prototype file authoring ========== +// ========== Prototype creation — born, teach, train ========== -const authorBorn = defineCommand({ +const prototypeBorn = defineCommand({ meta: { name: "born", description: "Create a prototype directory with manifest" }, args: { dir: { @@ -704,7 +705,7 @@ const authorBorn = defineCommand({ }, run({ args }) { const aliasList = args.alias ? args.alias.split(",").map((a: string) => a.trim()) : undefined; - const result = rolex.author.born( + const result = rolex.prototype.born( args.dir, resolveContent(args, "individual"), args.id, @@ -714,7 +715,7 @@ const authorBorn = defineCommand({ }, }); -const authorTeach = defineCommand({ +const prototypeTeach = defineCommand({ meta: { name: "teach", description: "Add a principle to a prototype directory" }, args: { dir: { type: "positional" as const, description: "Prototype directory path", required: true }, @@ -726,12 +727,12 @@ const authorTeach = defineCommand({ }, }, run({ args }) { - const result = rolex.author.teach(args.dir, requireContent(args, "principle"), args.id); + const result = rolex.prototype.teach(args.dir, requireContent(args, "principle"), args.id); output(result, args.id); }, }); -const authorTrain = defineCommand({ +const prototypeTrain = defineCommand({ meta: { name: "train", description: "Add a procedure to a prototype directory" }, args: { dir: { type: "positional" as const, description: "Prototype directory path", required: true }, @@ -743,24 +744,18 @@ const authorTrain = defineCommand({ }, }, run({ args }) { - const result = rolex.author.train(args.dir, requireContent(args, "procedure"), args.id); + const result = rolex.prototype.train(args.dir, requireContent(args, "procedure"), args.id); output(result, args.id); }, }); -const author = defineCommand({ - meta: { name: "author", description: "Prototype authoring — create prototype files" }, - subCommands: { - born: authorBorn, - teach: authorTeach, - train: authorTrain, - }, -}); +// ========== Prototype — registry ========== -// ========== Prototype — summon, banish, list ========== - -const protoSummon = defineCommand({ - meta: { name: "summon", description: "Summon a prototype from a ResourceX source" }, +const protoSettle = defineCommand({ + meta: { + name: "settle", + description: "Settle a prototype into the world from a ResourceX source", + }, args: { source: { type: "positional" as const, @@ -769,22 +764,22 @@ const protoSummon = defineCommand({ }, }, async run({ args }) { - const result = await rolex.proto.summon(args.source); + const result = await rolex.prototype.settle(args.source); output(result, result.state.id ?? args.source); }, }); -const protoBanish = defineCommand({ - meta: { name: "banish", description: "Banish a prototype by id" }, +const protoEvict = defineCommand({ + meta: { name: "evict", description: "Evict a prototype from the world" }, args: { id: { type: "positional" as const, - description: "Prototype id to banish", + description: "Prototype id to evict", required: true, }, }, run({ args }) { - const result = rolex.proto.banish(args.id); + const result = rolex.prototype.evict(args.id); output(result, args.id); }, }); @@ -792,7 +787,7 @@ const protoBanish = defineCommand({ const protoList = defineCommand({ meta: { name: "list", description: "List all registered prototypes" }, run() { - const list = rolex.proto.list(); + const list = rolex.prototype.list(); const entries = Object.entries(list); if (entries.length === 0) { console.log("No prototypes registered."); @@ -804,12 +799,15 @@ const protoList = defineCommand({ }, }); -const proto = defineCommand({ - meta: { name: "prototype", description: "Prototype management — summon, banish, list" }, +const prototype = defineCommand({ + meta: { name: "prototype", description: "Prototype management — registry + creation" }, subCommands: { - summon: protoSummon, - banish: protoBanish, + settle: protoSettle, + evict: protoEvict, list: protoList, + born: prototypeBorn, + teach: prototypeTeach, + train: prototypeTrain, }, }); @@ -827,8 +825,7 @@ const main = defineCommand({ organization: org, position: pos, resource, - author, - prototype: proto, + prototype, }, }); diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 843dde5..532296e 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -17,6 +17,7 @@ import { McpState } from "./state.js"; // ========== Setup ========== const rolex = createRoleX(localPlatform()); +await rolex.bootstrap(); const state = new McpState(rolex); // ========== Server ========== @@ -279,51 +280,6 @@ server.addTool({ }, }); -// ========== Tools: Prototype authoring ========== - -server.addTool({ - name: "author-born", - description: detail("author-born"), - parameters: z.object({ - dir: z.string().describe("Directory path to create the prototype in"), - content: z.string().optional().describe("Gherkin Feature source for the root individual"), - id: z.string().describe("Prototype id (kebab-case)"), - alias: z.array(z.string()).optional().describe("Alternative display names"), - }), - execute: async ({ dir, content, id, alias }) => { - const result = rolex.author.born(dir, content, id, alias); - return fmt("born", id, result); - }, -}); - -server.addTool({ - name: "author-teach", - description: detail("author-teach"), - parameters: z.object({ - dir: z.string().describe("Prototype directory path"), - content: z.string().describe("Gherkin Feature source for the principle"), - id: z.string().describe("Principle id (keywords joined by hyphens)"), - }), - execute: async ({ dir, content, id }) => { - const result = rolex.author.teach(dir, content, id); - return fmt("teach", id, result); - }, -}); - -server.addTool({ - name: "author-train", - description: detail("author-train"), - parameters: z.object({ - dir: z.string().describe("Prototype directory path"), - content: z.string().describe("Gherkin Feature source for the procedure"), - id: z.string().describe("Procedure id (keywords joined by hyphens)"), - }), - execute: async ({ dir, content, id }) => { - const result = rolex.author.train(dir, content, id); - return fmt("train", id, result); - }, -}); - // ========== Start ========== server.start({ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6c79795..ce702dd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,6 +67,8 @@ export { position, principle, procedure, + // Organization — Position + requirement, // Level 0 society, task, diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index f556586..6690a79 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -10,7 +10,7 @@ * Platform holds the Runtime (graph engine) and will hold additional * services as the framework grows (auth, events, plugins, etc.). */ -import type { Prototype, Runtime } from "@rolexjs/system"; +import type { Initializer, Prototype, Runtime } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; /** Serializable context data for persistence. */ @@ -29,6 +29,9 @@ export interface Platform { /** Resource management capability (optional — requires resourcexjs). */ readonly resourcex?: ResourceX; + /** Initializer — bootstrap the world on first run. */ + readonly initializer?: Initializer; + /** Save role context to persistent storage. */ saveContext?(roleId: string, data: ContextData): void; diff --git a/packages/core/src/structures.ts b/packages/core/src/structures.ts index 52b2af0..65c74d0 100644 --- a/packages/core/src/structures.ts +++ b/packages/core/src/structures.ts @@ -91,3 +91,4 @@ export const position = structure("position", "A role held by an individual", so 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/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index 6cb1594..0de4da2 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -18,7 +18,7 @@ import { openDatabase } from "@deepracticex/sqlite"; import { NodeProvider } from "@resourcexjs/node-provider"; import type { ContextData, Platform } from "@rolexjs/core"; import { organizationType, roleType } from "@rolexjs/resourcex-types"; -import type { Prototype, State } from "@rolexjs/system"; +import type { Initializer, Prototype, State } from "@rolexjs/system"; import { sql } from "drizzle-orm"; import { createResourceX, setProvider } from "resourcexjs"; import { createSqliteRuntime } from "./sqliteRuntime.js"; @@ -106,19 +106,13 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { // ===== Prototype registry ===== - /** Built-in prototypes — always available, cannot be overridden. */ - const BUILTINS: Record = { - nuwa: "nuwa", - }; - const registryPath = dataDir ? join(dataDir, "prototype.json") : undefined; const readRegistry = (): Record => { - let fileRegistry: Record = {}; if (registryPath && existsSync(registryPath)) { - fileRegistry = JSON.parse(readFileSync(registryPath, "utf-8")); + return JSON.parse(readFileSync(registryPath, "utf-8")); } - return { ...fileRegistry, ...BUILTINS }; + return {}; }; const writeRegistry = (registry: Record): void => { @@ -140,16 +134,13 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { } }, - summon(id, source) { - if (id in BUILTINS) { - throw new Error(`"${id}" is a built-in prototype and cannot be overridden.`); - } + settle(id, source) { const registry = readRegistry(); registry[id] = source; writeRegistry(registry); }, - banish(id) { + evict(id) { const registry = readRegistry(); delete registry[id]; writeRegistry(registry); @@ -160,6 +151,37 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { }, }; + // ===== Initializer ===== + + /** Built-in prototypes to settle on first run. */ + const BUILTIN_PROTOTYPES = ["nuwa", "rolex"]; + + const initializedPath = dataDir ? join(dataDir, "initialized.json") : undefined; + + const initializer: Initializer = { + async bootstrap() { + // In-memory mode or already initialized — skip + if (!initializedPath) return; + if (existsSync(initializedPath)) return; + + // Settle built-in prototypes + for (const name of BUILTIN_PROTOTYPES) { + const registry = readRegistry(); + if (!(name in registry)) { + prototype.settle(name, name); + } + } + + // Mark as initialized + mkdirSync(dataDir!, { recursive: true }); + writeFileSync( + initializedPath, + JSON.stringify({ version: 1, initializedAt: new Date().toISOString() }, null, 2), + "utf-8" + ); + }, + }; + // ===== Context persistence ===== const saveContext = (roleId: string, data: ContextData): void => { @@ -176,5 +198,5 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { return JSON.parse(readFileSync(contextPath, "utf-8")); }; - return { runtime, prototype, resourcex, saveContext, loadContext }; + return { runtime, prototype, resourcex, initializer, saveContext, loadContext }; } diff --git a/packages/local-platform/tests/prototype.test.ts b/packages/local-platform/tests/prototype.test.ts index 4d556ec..72d53cd 100644 --- a/packages/local-platform/tests/prototype.test.ts +++ b/packages/local-platform/tests/prototype.test.ts @@ -68,14 +68,14 @@ describe("LocalPlatform Prototype (registry-based)", () => { expect(await prototype!.resolve("sean")).toBeUndefined(); }); - test("summon + resolve round-trip for role", async () => { + test("settle + resolve round-trip for role", async () => { const protoDir = join(testDir, "protos"); const dir = writePrototype(protoDir, "test-role", "role", { "test-role.individual.feature": "Feature: TestRole\n Test role.", }); const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.prototype!.summon("test-role", dir); + platform.prototype!.settle("test-role", dir); const state = await platform.prototype!.resolve("test-role"); expect(state).toBeDefined(); @@ -86,14 +86,14 @@ describe("LocalPlatform Prototype (registry-based)", () => { expect(state!.children![0].name).toBe("identity"); }); - test("summon + resolve round-trip for organization", async () => { + test("settle + 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.prototype!.summon("deepractice", dir); + platform.prototype!.settle("deepractice", dir); const state = await platform.prototype!.resolve("deepractice"); expect(state).toBeDefined(); @@ -107,12 +107,12 @@ describe("LocalPlatform Prototype (registry-based)", () => { const dir = writePrototype(protoDir, "test-role", "role"); const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.prototype!.summon("test-role", dir); + platform.prototype!.settle("test-role", dir); expect(await platform.prototype!.resolve("nobody")).toBeUndefined(); }); - test("summon overwrites previous source", async () => { + test("settle overwrites previous source", async () => { const protoDir = join(testDir, "protos"); const dir1 = writePrototype(protoDir, "v1", "role", { "v1.individual.feature": "Feature: V1", @@ -122,22 +122,22 @@ describe("LocalPlatform Prototype (registry-based)", () => { }); const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.prototype!.summon("test", dir1); - platform.prototype!.summon("test", dir2); + platform.prototype!.settle("test", dir1); + platform.prototype!.settle("test", dir2); const state = await platform.prototype!.resolve("test"); expect(state!.id).toBe("v2"); }); - test("banish removes user-registered prototype", async () => { + test("evict removes user-registered prototype", async () => { const protoDir = join(testDir, "protos"); const dir = writePrototype(protoDir, "temp", "role"); const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.prototype!.summon("temp", dir); + platform.prototype!.settle("temp", dir); expect(await platform.prototype!.resolve("temp")).toBeDefined(); - platform.prototype!.banish("temp"); + platform.prototype!.evict("temp"); expect(await platform.prototype!.resolve("temp")).toBeUndefined(); }); @@ -146,7 +146,7 @@ describe("LocalPlatform Prototype (registry-based)", () => { const dir = writePrototype(protoDir, "test-role", "role"); const p1 = localPlatform({ dataDir: testDir, resourceDir }); - p1.prototype!.summon("test-role", dir); + p1.prototype!.settle("test-role", dir); const p2 = localPlatform({ dataDir: testDir, resourceDir }); const state = await p2.prototype!.resolve("test-role"); @@ -154,17 +154,21 @@ describe("LocalPlatform Prototype (registry-based)", () => { expect(state!.id).toBe("test-role"); }); - test("list includes builtins and user-registered prototypes", () => { + test("bootstrap settles built-in prototypes on first run", async () => { const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.prototype!.summon("custom", "/path/to/custom"); + await platform.initializer!.bootstrap(); const list = platform.prototype!.list(); - expect(list.nuwa).toBeDefined(); // builtin - expect(list.custom).toBe("/path/to/custom"); // user-registered + expect(list.nuwa).toBe("nuwa"); + expect(list.rolex).toBe("rolex"); }); - test("builtin nuwa is always present in list", () => { + test("bootstrap is idempotent — second call is a no-op", async () => { const platform = localPlatform({ dataDir: testDir, resourceDir }); + await platform.initializer!.bootstrap(); + // Manually remove rolex to prove second bootstrap doesn't re-settle + platform.prototype!.evict("rolex"); + await platform.initializer!.bootstrap(); const list = platform.prototype!.list(); - expect(list.nuwa).toBe("nuwa"); + expect(list.rolex).toBeUndefined(); // not re-settled }); }); diff --git a/packages/rolexjs/src/descriptions/author/author-born.feature b/packages/rolexjs/src/descriptions/author/author-born.feature deleted file mode 100644 index 4b36e52..0000000 --- a/packages/rolexjs/src/descriptions/author/author-born.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: author-born — create a prototype directory - Create a new prototype role on the filesystem. - Writes individual.json manifest and optional feature file. - - Scenario: Create a prototype directory - Given a directory path and a prototype id - When author-born is called with dir, id, and optional content and alias - Then the directory is created (recursively if needed) - And individual.json manifest is written with type "individual" and identity child - And if content is provided, a .individual.feature file is written - And if alias is provided, it is included in the manifest - - Scenario: Writing the Gherkin content - Given the content parameter accepts a Gherkin Feature source - Then the Feature title names the role - And Scenarios describe the role's identity, purpose, or characteristics - And the content is written as-is to the feature file diff --git a/packages/rolexjs/src/descriptions/author/author-teach.feature b/packages/rolexjs/src/descriptions/author/author-teach.feature deleted file mode 100644 index f393823..0000000 --- a/packages/rolexjs/src/descriptions/author/author-teach.feature +++ /dev/null @@ -1,15 +0,0 @@ -Feature: author-teach — add a principle to a prototype - Add a principle to an existing prototype directory. - Updates individual.json manifest and writes a principle feature file. - - Scenario: Add a principle - Given a prototype directory with individual.json exists - And a Gherkin source describing the principle - When author-teach is called with dir, content, and id - Then the manifest's children gains a new entry with type "principle" - And a .principle.feature file is written to the directory - - Scenario: Principle content - Given a principle is a transferable truth - Then the Feature title states the principle as a general rule - And Scenarios describe situations where this principle applies diff --git a/packages/rolexjs/src/descriptions/author/author-train.feature b/packages/rolexjs/src/descriptions/author/author-train.feature deleted file mode 100644 index e325228..0000000 --- a/packages/rolexjs/src/descriptions/author/author-train.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: author-train — add a procedure to a prototype - Add a procedure to an existing prototype directory. - Updates individual.json manifest and writes a procedure feature file. - - Scenario: Add a procedure - Given a prototype directory with individual.json exists - And a Gherkin source describing the procedure - When author-train is called with dir, content, and id - Then the manifest's children gains a new entry with type "procedure" - And a .procedure.feature file is written to the directory - - Scenario: Procedure content - Given a procedure is skill metadata pointing to full skill content - Then the Feature title names the capability - And the description includes the locator for full skill loading - And Scenarios describe when and why to apply this skill diff --git a/packages/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts index 0e0e400..08fc04d 100644 --- a/packages/rolexjs/src/descriptions/index.ts +++ b/packages/rolexjs/src/descriptions/index.ts @@ -1,9 +1,6 @@ // AUTO-GENERATED — do not edit. Run `bun run gen:desc` to regenerate. export const processes: Record = { - "author-born": "Feature: author-born — create a prototype directory\n Create a new prototype role on the filesystem.\n Writes individual.json manifest and optional feature file.\n\n Scenario: Create a prototype directory\n Given a directory path and a prototype id\n When author-born is called with dir, id, and optional content and alias\n Then the directory is created (recursively if needed)\n And individual.json manifest is written with type \"individual\" and identity child\n And if content is provided, a .individual.feature file is written\n And if alias is provided, it is included in the manifest\n\n Scenario: Writing the Gherkin content\n Given the content parameter accepts a Gherkin Feature source\n Then the Feature title names the role\n And Scenarios describe the role's identity, purpose, or characteristics\n And the content is written as-is to the feature file", - "author-teach": "Feature: author-teach — add a principle to a prototype\n Add a principle to an existing prototype directory.\n Updates individual.json manifest and writes a principle feature file.\n\n Scenario: Add a principle\n Given a prototype directory with individual.json exists\n And a Gherkin source describing the principle\n When author-teach is called with dir, content, and id\n Then the manifest's children gains a new entry with type \"principle\"\n And a .principle.feature file is written to the directory\n\n Scenario: Principle content\n Given a principle is a transferable truth\n Then the Feature title states the principle as a general rule\n And Scenarios describe situations where this principle applies", - "author-train": "Feature: author-train — add a procedure to a prototype\n Add a procedure to an existing prototype directory.\n Updates individual.json manifest and writes a procedure feature file.\n\n Scenario: Add a procedure\n Given a prototype directory with individual.json exists\n And a Gherkin source describing the procedure\n When author-train is called with dir, content, and id\n Then the manifest's children gains a new entry with type \"procedure\"\n And a .procedure.feature file is written to the directory\n\n Scenario: Procedure content\n Given a procedure is skill 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", "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", @@ -20,8 +17,8 @@ export const processes: Record = { "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", - "banish": "Feature: banish — unregister a prototype\n Remove a previously summoned prototype from the local registry.\n Existing individuals created from this prototype are not affected.\n\n Scenario: Banish a prototype\n Given a prototype is registered locally\n When banish is called with the prototype id\n Then the prototype is removed from the registry\n And existing individuals created from it remain intact", - "summon": "Feature: summon — register a prototype from source\n Pull a prototype from a ResourceX source and register it locally.\n Once summoned, the prototype can be used to create individuals with born.\n\n Scenario: Summon a prototype\n Given a valid ResourceX source exists (URL, path, or locator)\n When summon 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 and activate", + "evict": "Feature: evict — unregister a prototype from the world\n Remove a previously settled prototype from the local registry.\n Existing individuals and organizations created from this prototype are not affected.\n\n Scenario: Evict a prototype\n Given a prototype is registered locally\n When evict is called with the prototype id\n Then the prototype is removed from the registry\n And existing individuals and organizations created from it remain intact", + "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", @@ -50,5 +47,5 @@ export const world: Record = { "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} #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 protocol — unified execution entry point\n The use tool is the single entry point for all execution.\n A ! prefix signals a RoleX runtime command; everything else is a ResourceX resource.\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 And available namespaces include individual, org, position, author, and prototype\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\n\n Scenario: Skill-driven knowledge\n Given specific commands within each namespace are documented in their respective skills\n When a role has mastered the relevant skill\n Then it knows which commands are available and how to use them\n And the use protocol itself only needs to route correctly", + "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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly\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 And available namespaces include individual, org, position, prototype, census, and resource\n And examples: !prototype.found, !resource.add, !org.hire, !census.list\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\n\n Scenario: use covers everything — no need for CLI or Bash\n Given use can execute any RoleX namespace operation\n And use can load any ResourceX resource\n When you need to perform a RoleX operation\n Then always use the MCP use tool\n And never fall back to CLI commands for operations that use can handle", } as const; diff --git a/packages/rolexjs/src/descriptions/prototype/banish.feature b/packages/rolexjs/src/descriptions/prototype/banish.feature deleted file mode 100644 index 5a9e6b2..0000000 --- a/packages/rolexjs/src/descriptions/prototype/banish.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: banish — unregister a prototype - Remove a previously summoned prototype from the local registry. - Existing individuals created from this prototype are not affected. - - Scenario: Banish a prototype - Given a prototype is registered locally - When banish is called with the prototype id - Then the prototype is removed from the registry - And existing individuals created from it remain intact diff --git a/packages/rolexjs/src/descriptions/prototype/evict.feature b/packages/rolexjs/src/descriptions/prototype/evict.feature new file mode 100644 index 0000000..f33b7a0 --- /dev/null +++ b/packages/rolexjs/src/descriptions/prototype/evict.feature @@ -0,0 +1,9 @@ +Feature: evict — unregister a prototype from the world + Remove a previously settled prototype from the local registry. + Existing individuals and organizations created from this prototype are not affected. + + Scenario: Evict a prototype + Given a prototype is registered locally + When evict is called with the prototype id + Then the prototype is removed from the registry + And existing individuals and organizations created from it remain intact diff --git a/packages/rolexjs/src/descriptions/prototype/settle.feature b/packages/rolexjs/src/descriptions/prototype/settle.feature new file mode 100644 index 0000000..6a8632e --- /dev/null +++ b/packages/rolexjs/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/prototype/summon.feature b/packages/rolexjs/src/descriptions/prototype/summon.feature deleted file mode 100644 index bb930c1..0000000 --- a/packages/rolexjs/src/descriptions/prototype/summon.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: summon — register a prototype from source - Pull a prototype from a ResourceX source and register it locally. - Once summoned, the prototype can be used to create individuals with born. - - Scenario: Summon a prototype - Given a valid ResourceX source exists (URL, path, or locator) - When summon 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 and activate diff --git a/packages/rolexjs/src/descriptions/world/use-protocol.feature b/packages/rolexjs/src/descriptions/world/use-protocol.feature index a6cfe7a..400d593 100644 --- a/packages/rolexjs/src/descriptions/world/use-protocol.feature +++ b/packages/rolexjs/src/descriptions/world/use-protocol.feature @@ -1,20 +1,29 @@ -Feature: Use protocol — unified execution entry point - The use tool is the single entry point for all execution. - A ! prefix signals a RoleX runtime command; everything else is a ResourceX resource. +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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly + 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 - And available namespaces include individual, org, position, author, and prototype + And available namespaces include individual, org, position, prototype, census, and resource + And examples: !prototype.found, !resource.add, !org.hire, !census.list 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 - Scenario: Skill-driven knowledge - Given specific commands within each namespace are documented in their respective skills - When a role has mastered the relevant skill - Then it knows which commands are available and how to use them - And the use protocol itself only needs to route correctly + Scenario: use covers everything — no need for CLI or Bash + Given use can execute any RoleX namespace operation + And use can load any ResourceX resource + When you need to perform a RoleX operation + Then always use the MCP use tool + And never fall back to CLI commands for operations that use can handle diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 3c57101..76296e6 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -14,6 +14,7 @@ * role — execution + cognition (activate → complete, reflect → master, skill) * org — organization management (found, charter, dissolve, hire, fire) * position — position management (establish, abolish, charge, appoint, dismiss) + * prototype — registry (settle, evict, list) + creation (born, teach, train, found, charter, establish, charge, require) * resource — ResourceX instance (optional) * * Unified entry point: @@ -26,6 +27,7 @@ import type { ContextData, Platform } from "@rolexjs/core"; import * as C from "@rolexjs/core"; import { parse } from "@rolexjs/parser"; import { + type Initializer, mergeState, type Prototype, type Runtime, @@ -68,16 +70,17 @@ export class Rolex { readonly position: PositionNamespace; /** Census — society-level queries. */ readonly census: CensusNamespace; - /** Prototype management — summon, banish, list. */ - readonly proto: PrototypeNamespace; - /** Prototype authoring — write prototype files to a directory. */ - readonly author: AuthorNamespace; + /** Prototype — registry + creation. */ + readonly prototype: PrototypeNamespace; /** Resource management (optional — powered by ResourceX). */ readonly resource?: ResourceX; + private readonly initializer?: Initializer; + constructor(platform: Platform) { this.rt = platform.runtime; this.resourcex = platform.resourcex; + this.initializer = platform.initializer; // Ensure world roots exist (idempotent — reuse if already created by another process) const roots = this.rt.roots(); @@ -117,11 +120,15 @@ export class Rolex { this.org = new OrgNamespace(this.rt, this.society, this.past, resolve); this.position = new PositionNamespace(this.rt, this.society, this.past, resolve); this.census = new CensusNamespace(this.rt, this.society, this.past); - this.proto = new PrototypeNamespace(platform.prototype, platform.resourcex); - this.author = new AuthorNamespace(); + this.prototype = new PrototypeNamespace(platform.prototype, platform.resourcex); this.resource = platform.resourcex; } + /** Bootstrap the world — settle built-in prototypes on first run. */ + async bootstrap(): Promise { + await this.initializer?.bootstrap(); + } + /** Find a node by id or alias across the entire society tree. */ find(id: string): Structure | null { const target = id.toLowerCase(); @@ -172,9 +179,7 @@ export class Rolex { case "census": return this.census; case "prototype": - return this.proto; - case "author": - return this.author; + return this.prototype; case "resource": if (!this.resource) throw new Error("ResourceX is not available."); return this.resource; @@ -237,18 +242,28 @@ export class Rolex { return [a.type]; // prototype - case "prototype.summon": + case "prototype.settle": return [a.source]; - case "prototype.banish": + case "prototype.evict": return [a.id]; - - // author - case "author.born": + case "prototype.born": return [a.dir, a.content, a.id, a.alias]; - case "author.teach": + case "prototype.teach": return [a.dir, a.content, a.id]; - case "author.train": + case "prototype.train": return [a.dir, a.content, a.id]; + case "prototype.found": + return [a.dir, a.content, a.id, a.alias]; + case "prototype.charter": + return [a.dir, a.content, a.id]; + case "prototype.member": + return [a.dir, a.id, a.locator]; + case "prototype.establish": + return [a.dir, a.content, a.id, a.appointments]; + case "prototype.charge": + return [a.dir, a.position, a.content, a.id]; + case "prototype.require": + return [a.dir, a.position, a.content, a.id]; // resource (ResourceX proxy) case "resource.add": @@ -758,7 +773,7 @@ class PositionNamespace { const existing = findInState(state, id); if (existing) this.rt.remove(existing); } - const proc = this.rt.create(parent, C.procedure, procedure, id); + const proc = this.rt.create(parent, C.requirement, procedure, id); return ok(this.rt, proc, "require"); } @@ -779,7 +794,7 @@ class PositionNamespace { // Auto-train: inject required procedures into the individual const posState = this.rt.project(posNode); - const required = (posState.children ?? []).filter((c) => c.name === "procedure"); + const required = (posState.children ?? []).filter((c) => c.name === "requirement"); for (const proc of required) { if (proc.id) { // Upsert: remove existing procedure with same id @@ -854,66 +869,71 @@ class CensusNamespace { } // ================================================================ -// Prototype — summon, banish, list +// Prototype — settle, evict, list // ================================================================ +interface PrototypeManifest { + id: string; + type: string; + alias?: readonly string[]; + members?: Record; + children?: Record; +} + +interface PrototypeManifestChild { + type: string; + appointments?: string[]; + children?: Record; +} + class PrototypeNamespace { constructor( private prototype?: Prototype, private resourcex?: ResourceX ) {} - /** Summon: pull a prototype from source, register it. */ - async summon(source: string): Promise { + // ---- Registry ---- + + /** Settle: pull a prototype from source, register it in the world. */ + async settle(source: string): Promise { if (!this.resourcex) throw new Error("ResourceX is not available."); if (!this.prototype) throw new Error("Platform does not support prototypes."); const state = await this.resourcex.ingest(source); if (!state.id) throw new Error("Prototype resource must have an id."); - this.prototype.summon(state.id, source); - return { state, process: "summon" }; + this.prototype.settle(state.id, source); + return { state, process: "settle" }; } - /** Banish: unregister a prototype by id. */ - banish(id: string): RolexResult { + /** Evict: unregister a prototype from the world. */ + evict(id: string): RolexResult { if (!this.prototype) throw new Error("Platform does not support prototypes."); - this.prototype.banish(id); - return { state: { name: id, description: "", parent: null }, process: "banish" }; + this.prototype.evict(id); + return { state: { name: id, description: "", parent: null }, process: "evict" }; } /** List all registered prototypes. */ list(): Record { return this.prototype?.list() ?? {}; } -} -// ================================================================ -// Author — prototype file authoring -// ================================================================ + // ---- Individual prototype creation ---- -interface AuthorManifest { - id: string; - type: string; - alias?: readonly string[]; - children?: Record; -} - -class AuthorNamespace { - /** Born: create a prototype directory with manifest and root feature file. */ + /** Born: create an individual prototype directory. */ born(dir: string, content?: string, id?: string, alias?: readonly string[]): RolexResult { validateGherkin(content); - if (!id) throw new Error("id is required for prototype authoring."); + if (!id) throw new Error("id is required."); mkdirSync(dir, { recursive: true }); - const manifest: AuthorManifest = { + const manifest: PrototypeManifest = { id, type: "individual", ...(alias && alias.length > 0 ? { alias } : {}), children: { identity: { type: "identity" } }, }; - writeFileSync(join(dir, "individual.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8"); + writeFileSync(join(dir, "individual.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8"); if (content) { - writeFileSync(join(dir, `${id}.individual.feature`), content + "\n", "utf-8"); + writeFileSync(join(dir, `${id}.individual.feature`), `${content}\n`, "utf-8"); } const state: State = { @@ -930,13 +950,13 @@ class AuthorNamespace { /** Teach: add a principle to an existing prototype directory. */ teach(dir: string, principle: string, id?: string): RolexResult { validateGherkin(principle); - if (!id) throw new Error("id is required for prototype authoring."); + if (!id) throw new Error("id is required."); const manifest = this.readManifest(dir); if (!manifest.children) manifest.children = {}; manifest.children[id] = { type: "principle" }; this.writeManifest(dir, manifest); - writeFileSync(join(dir, `${id}.principle.feature`), principle + "\n", "utf-8"); + writeFileSync(join(dir, `${id}.principle.feature`), `${principle}\n`, "utf-8"); const state: State = { id, @@ -951,13 +971,13 @@ class AuthorNamespace { /** Train: add a procedure to an existing prototype directory. */ train(dir: string, procedure: string, id?: string): RolexResult { validateGherkin(procedure); - if (!id) throw new Error("id is required for prototype authoring."); + if (!id) throw new Error("id is required."); const manifest = this.readManifest(dir); if (!manifest.children) manifest.children = {}; manifest.children[id] = { type: "procedure" }; this.writeManifest(dir, manifest); - writeFileSync(join(dir, `${id}.procedure.feature`), procedure + "\n", "utf-8"); + writeFileSync(join(dir, `${id}.procedure.feature`), `${procedure}\n`, "utf-8"); const state: State = { id, @@ -969,15 +989,172 @@ class AuthorNamespace { return { state, process: "train" }; } - private readManifest(dir: string): AuthorManifest { - const path = join(dir, "individual.json"); - if (!existsSync(path)) - throw new Error(`No individual.json found in "${dir}". Run author.born first.`); - return JSON.parse(readFileSync(path, "utf-8")); + // ---- Organization prototype creation ---- + + /** Found: create an organization prototype directory. */ + found(dir: string, content?: string, id?: string, alias?: readonly string[]): RolexResult { + validateGherkin(content); + if (!id) throw new Error("id is required."); + mkdirSync(dir, { recursive: true }); + + const manifest: PrototypeManifest = { + id, + type: "organization", + ...(alias && alias.length > 0 ? { alias } : {}), + children: {}, + }; + writeFileSync( + join(dir, "organization.json"), + `${JSON.stringify(manifest, null, 2)}\n`, + "utf-8" + ); + + if (content) { + writeFileSync(join(dir, `${id}.organization.feature`), `${content}\n`, "utf-8"); + } + + const state: State = { + id, + name: "organization", + description: "", + parent: null, + ...(alias ? { alias } : {}), + ...(content ? { information: content } : {}), + }; + return { state, process: "found" }; + } + + /** Charter: add a charter to an organization prototype. */ + charter(dir: string, content: string, id?: string): RolexResult { + validateGherkin(content); + const charterId = id ?? "charter"; + const manifest = this.readManifest(dir, "organization"); + if (!manifest.children) manifest.children = {}; + manifest.children[charterId] = { type: "charter" }; + this.writeManifest(dir, manifest); + + writeFileSync(join(dir, `${charterId}.charter.feature`), `${content}\n`, "utf-8"); + + const state: State = { + id: charterId, + name: "charter", + description: "", + parent: null, + information: content, + }; + return { state, process: "charter" }; + } + + /** Member: register a member in an organization prototype. */ + member(dir: string, id: string, locator: string): RolexResult { + if (!id) throw new Error("id is required."); + if (!locator) throw new Error("locator is required."); + const manifest = this.readManifest(dir, "organization"); + if (!manifest.members) manifest.members = {}; + manifest.members[id] = locator; + this.writeManifest(dir, manifest); + + const state: State = { + id, + name: "member", + description: locator, + parent: null, + }; + return { state, process: "member" }; + } + + /** Establish: add a position to an organization prototype. */ + establish(dir: string, content?: string, id?: string, appointments?: string[]): RolexResult { + validateGherkin(content); + if (!id) throw new Error("id is required."); + const manifest = this.readManifest(dir, "organization"); + if (!manifest.children) manifest.children = {}; + manifest.children[id] = { + type: "position", + ...(appointments && appointments.length > 0 ? { appointments } : {}), + children: {}, + }; + this.writeManifest(dir, manifest); + + if (content) { + writeFileSync(join(dir, `${id}.position.feature`), `${content}\n`, "utf-8"); + } + + const state: State = { + id, + name: "position", + description: "", + parent: null, + ...(content ? { information: content } : {}), + }; + return { state, process: "establish" }; + } + + /** Charge: add a duty to a position in an organization prototype. */ + charge(dir: string, position: string, content: string, id?: string): RolexResult { + validateGherkin(content); + if (!id) throw new Error("id is required."); + const manifest = this.readManifest(dir, "organization"); + const pos = manifest.children?.[position]; + if (!pos) throw new Error(`Position "${position}" not found in manifest.`); + if (!pos.children) pos.children = {}; + pos.children[id] = { type: "duty" }; + this.writeManifest(dir, manifest); + + writeFileSync(join(dir, `${id}.duty.feature`), `${content}\n`, "utf-8"); + + const state: State = { + id, + name: "duty", + description: "", + parent: null, + information: content, + }; + return { state, process: "charge" }; + } + + /** Require: add a required skill to a position in an organization prototype. */ + require(dir: string, position: string, content: string, id?: string): RolexResult { + validateGherkin(content); + if (!id) throw new Error("id is required."); + const manifest = this.readManifest(dir, "organization"); + const pos = manifest.children?.[position]; + if (!pos) throw new Error(`Position "${position}" not found in manifest.`); + if (!pos.children) pos.children = {}; + pos.children[id] = { type: "requirement" }; + this.writeManifest(dir, manifest); + + writeFileSync(join(dir, `${id}.requirement.feature`), `${content}\n`, "utf-8"); + + const state: State = { + id, + name: "requirement", + description: "", + parent: null, + information: content, + }; + return { state, process: "require" }; + } + + // ---- Manifest I/O ---- + + private readManifest(dir: string, type?: string): PrototypeManifest { + if (type) { + const path = join(dir, `${type}.json`); + if (!existsSync(path)) throw new Error(`No ${type}.json found in "${dir}".`); + return JSON.parse(readFileSync(path, "utf-8")); + } + // Auto-detect: try individual.json first, then organization.json + const indPath = join(dir, "individual.json"); + if (existsSync(indPath)) return JSON.parse(readFileSync(indPath, "utf-8")); + const orgPath = join(dir, "organization.json"); + if (existsSync(orgPath)) return JSON.parse(readFileSync(orgPath, "utf-8")); + throw new Error(`No manifest found in "${dir}". Run prototype.born or prototype.found first.`); } - private writeManifest(dir: string, manifest: AuthorManifest): void { - writeFileSync(join(dir, "individual.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8"); + private writeManifest(dir: string, manifest: PrototypeManifest): void { + const filename = `${manifest.type}.json`; + writeFileSync(join(dir, filename), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8"); } } diff --git a/packages/rolexjs/tests/author.test.ts b/packages/rolexjs/tests/author.test.ts index 21dad84..51d2f70 100644 --- a/packages/rolexjs/tests/author.test.ts +++ b/packages/rolexjs/tests/author.test.ts @@ -22,15 +22,15 @@ function protoDir() { return join(tmpDir, "my-role"); } -function readManifest(dir: string) { - return JSON.parse(readFileSync(join(dir, "individual.json"), "utf-8")); +function readManifest(dir: string, type = "individual") { + return JSON.parse(readFileSync(join(dir, `${type}.json`), "utf-8")); } function readFeature(dir: string, filename: string) { return readFileSync(join(dir, filename), "utf-8"); } -describe("AuthorNamespace", () => { +describe("PrototypeNamespace authoring", () => { beforeEach(() => {}); afterEach(cleanup); @@ -38,7 +38,9 @@ describe("AuthorNamespace", () => { test("creates directory, manifest, and feature file", () => { const rolex = setup(); const dir = protoDir(); - const r = rolex.author.born(dir, "Feature: My Role\n A test role.", "my-role", ["MyRole"]); + const r = rolex.prototype.born(dir, "Feature: My Role\n A test role.", "my-role", [ + "MyRole", + ]); expect(r.process).toBe("born"); expect(r.state.id).toBe("my-role"); @@ -59,7 +61,7 @@ describe("AuthorNamespace", () => { test("creates manifest without alias when not provided", () => { const rolex = setup(); const dir = protoDir(); - rolex.author.born(dir, "Feature: Minimal", "minimal"); + rolex.prototype.born(dir, "Feature: Minimal", "minimal"); const manifest = readManifest(dir); expect(manifest.alias).toBeUndefined(); @@ -68,7 +70,7 @@ describe("AuthorNamespace", () => { test("creates manifest without feature file when content is omitted", () => { const rolex = setup(); const dir = protoDir(); - rolex.author.born(dir, undefined, "empty-role"); + rolex.prototype.born(dir, undefined, "empty-role"); expect(existsSync(join(dir, "individual.json"))).toBe(true); expect(existsSync(join(dir, "empty-role.individual.feature"))).toBe(false); @@ -76,12 +78,12 @@ describe("AuthorNamespace", () => { test("throws when id is missing", () => { const rolex = setup(); - expect(() => rolex.author.born(protoDir(), "Feature: X")).toThrow("id is required"); + expect(() => rolex.prototype.born(protoDir(), "Feature: X")).toThrow("id is required"); }); test("validates Gherkin content", () => { const rolex = setup(); - expect(() => rolex.author.born(protoDir(), "not valid gherkin", "test")).toThrow( + expect(() => rolex.prototype.born(protoDir(), "not valid gherkin", "test")).toThrow( "Invalid Gherkin" ); }); @@ -91,8 +93,8 @@ describe("AuthorNamespace", () => { test("adds principle to manifest and writes feature file", () => { const rolex = setup(); const dir = protoDir(); - rolex.author.born(dir, undefined, "my-role"); - const r = rolex.author.teach( + rolex.prototype.born(dir, undefined, "my-role"); + const r = rolex.prototype.teach( dir, "Feature: Always test first\n Tests before code.", "tdd-first" @@ -115,7 +117,9 @@ describe("AuthorNamespace", () => { test("throws when no manifest exists", () => { const rolex = setup(); - expect(() => rolex.author.teach(protoDir(), "Feature: X", "x")).toThrow("No individual.json"); + expect(() => rolex.prototype.teach(protoDir(), "Feature: X", "x")).toThrow( + "No manifest found" + ); }); }); @@ -123,8 +127,8 @@ describe("AuthorNamespace", () => { test("adds procedure to manifest and writes feature file", () => { const rolex = setup(); const dir = protoDir(); - rolex.author.born(dir, undefined, "my-role"); - const r = rolex.author.train( + rolex.prototype.born(dir, undefined, "my-role"); + const r = rolex.prototype.train( dir, "Feature: Code Review\n https://example.com/skills/code-review", "code-review" @@ -144,17 +148,21 @@ describe("AuthorNamespace", () => { }); }); - describe("full workflow", () => { + describe("full workflow — individual", () => { test("born → teach → train produces valid prototype", () => { const rolex = setup(); const dir = protoDir(); - rolex.author.born(dir, "Feature: Backend Dev\n A server-side engineer.", "backend-dev", [ + rolex.prototype.born(dir, "Feature: Backend Dev\n A server-side engineer.", "backend-dev", [ "Backend", ]); - rolex.author.teach(dir, "Feature: DRY principle\n Don't repeat yourself.", "dry"); - rolex.author.train(dir, "Feature: Deployment\n https://example.com/skills/deploy", "deploy"); - rolex.author.teach(dir, "Feature: KISS\n Keep it simple.", "kiss"); + rolex.prototype.teach(dir, "Feature: DRY principle\n Don't repeat yourself.", "dry"); + rolex.prototype.train( + dir, + "Feature: Deployment\n https://example.com/skills/deploy", + "deploy" + ); + rolex.prototype.teach(dir, "Feature: KISS\n Keep it simple.", "kiss"); const manifest = readManifest(dir); expect(manifest.id).toBe("backend-dev"); @@ -171,4 +179,209 @@ describe("AuthorNamespace", () => { expect(existsSync(join(dir, "kiss.principle.feature"))).toBe(true); }); }); + + // ---- Organization authoring ---- + + describe("found", () => { + test("creates organization directory and manifest", () => { + const rolex = setup(); + const dir = join(tmpDir, "my-org"); + const r = rolex.prototype.found( + dir, + "Feature: Deepractice\n An AI agent framework company.", + "deepractice", + ["DP"] + ); + + expect(r.process).toBe("found"); + expect(r.state.id).toBe("deepractice"); + expect(r.state.name).toBe("organization"); + + const manifest = readManifest(dir, "organization"); + expect(manifest.id).toBe("deepractice"); + expect(manifest.type).toBe("organization"); + expect(manifest.alias).toEqual(["DP"]); + expect(manifest.children).toEqual({}); + + const content = readFeature(dir, "deepractice.organization.feature"); + expect(content).toContain("Feature: Deepractice"); + }); + + test("throws when id is missing", () => { + const rolex = setup(); + expect(() => rolex.prototype.found(join(tmpDir, "x"), "Feature: X")).toThrow( + "id is required" + ); + }); + }); + + describe("charter", () => { + test("adds charter to organization manifest", () => { + const rolex = setup(); + const dir = join(tmpDir, "my-org"); + rolex.prototype.found(dir, undefined, "my-org"); + const r = rolex.prototype.charter( + dir, + "Feature: Build great AI\n Scenario: Mission\n Given we believe in role-based AI", + "mission" + ); + + expect(r.process).toBe("charter"); + expect(r.state.id).toBe("mission"); + expect(r.state.name).toBe("charter"); + + const manifest = readManifest(dir, "organization"); + expect(manifest.children["mission"].type).toBe("charter"); + + const content = readFeature(dir, "mission.charter.feature"); + expect(content).toContain("Build great AI"); + }); + + test("defaults charter id to 'charter'", () => { + const rolex = setup(); + const dir = join(tmpDir, "my-org"); + rolex.prototype.found(dir, undefined, "my-org"); + const r = rolex.prototype.charter(dir, "Feature: Our charter\n We build things."); + + expect(r.state.id).toBe("charter"); + const manifest = readManifest(dir, "organization"); + expect(manifest.children["charter"].type).toBe("charter"); + }); + }); + + describe("establish", () => { + test("adds position to organization manifest", () => { + const rolex = setup(); + const dir = join(tmpDir, "my-org"); + rolex.prototype.found(dir, undefined, "my-org"); + const r = rolex.prototype.establish( + dir, + "Feature: Backend Architect\n Responsible for system design.", + "architect" + ); + + expect(r.process).toBe("establish"); + expect(r.state.id).toBe("architect"); + expect(r.state.name).toBe("position"); + + const manifest = readManifest(dir, "organization"); + expect(manifest.children["architect"].type).toBe("position"); + expect(manifest.children["architect"].children).toEqual({}); + + const content = readFeature(dir, "architect.position.feature"); + expect(content).toContain("Backend Architect"); + }); + }); + + describe("charge", () => { + test("adds duty under position in manifest", () => { + const rolex = setup(); + const dir = join(tmpDir, "my-org"); + rolex.prototype.found(dir, undefined, "my-org"); + rolex.prototype.establish(dir, "Feature: Architect", "architect"); + const r = rolex.prototype.charge( + dir, + "architect", + "Feature: Design systems\n Scenario: API design\n Given a new service is needed\n Then design the API first", + "design-systems" + ); + + expect(r.process).toBe("charge"); + expect(r.state.id).toBe("design-systems"); + expect(r.state.name).toBe("duty"); + + const manifest = readManifest(dir, "organization"); + expect(manifest.children["architect"].children["design-systems"].type).toBe("duty"); + + const content = readFeature(dir, "design-systems.duty.feature"); + expect(content).toContain("Design systems"); + }); + + test("throws when position not found", () => { + const rolex = setup(); + const dir = join(tmpDir, "my-org"); + rolex.prototype.found(dir, undefined, "my-org"); + expect(() => rolex.prototype.charge(dir, "nonexistent", "Feature: X", "x")).toThrow( + 'Position "nonexistent" not found' + ); + }); + }); + + describe("require", () => { + test("adds required skill under position in manifest", () => { + const rolex = setup(); + const dir = join(tmpDir, "my-org"); + rolex.prototype.found(dir, undefined, "my-org"); + rolex.prototype.establish(dir, "Feature: Architect", "architect"); + const r = rolex.prototype.require( + dir, + "architect", + "Feature: System Design\n Scenario: When to apply\n Given a new service is planned\n Then design the architecture first", + "system-design" + ); + + expect(r.process).toBe("require"); + expect(r.state.id).toBe("system-design"); + expect(r.state.name).toBe("requirement"); + + const manifest = readManifest(dir, "organization"); + expect(manifest.children["architect"].children["system-design"].type).toBe("requirement"); + + const content = readFeature(dir, "system-design.requirement.feature"); + expect(content).toContain("System Design"); + }); + }); + + describe("full workflow — organization", () => { + test("found → charter → establish → charge → require produces valid prototype", () => { + const rolex = setup(); + const dir = join(tmpDir, "dp-org"); + + rolex.prototype.found( + dir, + "Feature: Deepractice\n AI agent framework company.", + "deepractice", + ["DP"] + ); + rolex.prototype.charter( + dir, + "Feature: Build role-based AI\n Scenario: Mission\n Given AI needs identity", + "mission" + ); + rolex.prototype.establish( + dir, + "Feature: Backend Architect\n System design lead.", + "architect" + ); + rolex.prototype.charge( + dir, + "architect", + "Feature: Design APIs\n Scenario: New service\n Given a service is needed\n Then design API first", + "design-apis" + ); + rolex.prototype.require( + dir, + "architect", + "Feature: System Design Skill\n Scenario: When to apply\n Given architecture decisions needed\n Then apply systematic design", + "system-design" + ); + + const manifest = readManifest(dir, "organization"); + expect(manifest.id).toBe("deepractice"); + expect(manifest.type).toBe("organization"); + expect(manifest.alias).toEqual(["DP"]); + expect(Object.keys(manifest.children)).toEqual(["mission", "architect"]); + expect(manifest.children["mission"].type).toBe("charter"); + expect(manifest.children["architect"].type).toBe("position"); + expect(manifest.children["architect"].children["design-apis"].type).toBe("duty"); + expect(manifest.children["architect"].children["system-design"].type).toBe("requirement"); + + // All feature files exist + expect(existsSync(join(dir, "deepractice.organization.feature"))).toBe(true); + expect(existsSync(join(dir, "mission.charter.feature"))).toBe(true); + expect(existsSync(join(dir, "architect.position.feature"))).toBe(true); + expect(existsSync(join(dir, "design-apis.duty.feature"))).toBe(true); + expect(existsSync(join(dir, "system-design.requirement.feature"))).toBe(true); + }); + }); }); diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index 49d7053..1b71d81 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -153,7 +153,7 @@ describe("Rolex API (stateless)", () => { expect(r.state.links).toBeUndefined(); }); - test("require adds procedure to position", () => { + test("require adds required skill to position", () => { const rolex = setup(); rolex.position.establish(undefined, "architect"); const r = rolex.position.require( @@ -161,7 +161,7 @@ describe("Rolex API (stateless)", () => { "Feature: System Design\n Scenario: Design APIs", "system-design" ); - expect(r.state.name).toBe("procedure"); + expect(r.state.name).toBe("requirement"); expect(r.state.id).toBe("system-design"); expect(r.process).toBe("require"); }); @@ -172,9 +172,9 @@ describe("Rolex API (stateless)", () => { rolex.position.require("architect", "Feature: Old skill", "skill-1"); rolex.position.require("architect", "Feature: Updated skill", "skill-1"); const pos = rolex.find("architect")!; - const procedures = (pos as any).children?.filter((c: any) => c.name === "procedure"); - expect(procedures).toHaveLength(1); - expect(procedures[0].information).toBe("Feature: Updated skill"); + const requires = (pos as any).children?.filter((c: any) => c.name === "requirement"); + expect(requires).toHaveLength(1); + expect(requires[0].information).toBe("Feature: Updated skill"); }); test("appoint auto-trains required skills to individual", () => { diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index a858e7a..f42f9b8 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -40,6 +40,10 @@ export { create, link, process, remove, transform, unlink } from "./process.js"; export { mergeState } from "./merge.js"; +// ===== Initializer ===== + +export type { Initializer } from "./initializer.js"; + // ===== Prototype ===== export type { Prototype } from "./prototype.js"; 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/prototype.ts b/packages/system/src/prototype.ts index d58bb0d..17610b3 100644 --- a/packages/system/src/prototype.ts +++ b/packages/system/src/prototype.ts @@ -19,11 +19,11 @@ export interface Prototype { /** Resolve a prototype State by id. Returns undefined if no prototype exists. */ resolve(id: string): Promise; - /** Summon: register a prototype — bind id to a source (path or locator). */ - summon(id: string, source: string): void; + /** Settle: register a prototype — bind id to a source (path or locator). */ + settle(id: string, source: string): void; - /** Banish: unregister a prototype by id. */ - banish(id: string): void; + /** Evict: unregister a prototype by id. */ + evict(id: string): void; /** List all registered prototypes: id → source mapping. */ list(): Record; @@ -44,11 +44,11 @@ export const createPrototype = (): Prototype & { return states.get(id); }, - summon(id, source) { + settle(id, source) { sources.set(id, source); }, - banish(id) { + evict(id) { sources.delete(id); states.delete(id); }, diff --git a/packages/system/tests/prototype.test.ts b/packages/system/tests/prototype.test.ts index 313d29a..67378ad 100644 --- a/packages/system/tests/prototype.test.ts +++ b/packages/system/tests/prototype.test.ts @@ -58,21 +58,21 @@ describe("Prototype", () => { expect(await proto.resolve("unknown")).toBeUndefined(); }); - test("summon and list round-trip", () => { + test("settle and list round-trip", () => { const proto = createPrototype(); - proto.summon("nuwa", "/path/to/nuwa"); - proto.summon("sean", "/path/to/sean"); + proto.settle("nuwa", "/path/to/nuwa"); + proto.settle("sean", "/path/to/sean"); expect(proto.list()).toEqual({ nuwa: "/path/to/nuwa", sean: "/path/to/sean", }); }); - test("banish removes from list", () => { + test("evict removes from list", () => { const proto = createPrototype(); - proto.summon("nuwa", "/path/to/nuwa"); - proto.summon("sean", "/path/to/sean"); - proto.banish("nuwa"); + proto.settle("nuwa", "/path/to/nuwa"); + proto.settle("sean", "/path/to/sean"); + proto.evict("nuwa"); expect(proto.list()).toEqual({ sean: "/path/to/sean" }); }); From 9fe7aaa8943526f8b55728ddcad314e7ea832a7b Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Feb 2026 17:30:17 +0800 Subject: [PATCH 25/54] feat: create @rolexjs/prototype package and move prototypes/skills into RoleX - New @rolexjs/prototype package with PrototypeInstruction type, Prototype interface, and ResourceX resolver - Instruction format: { op, args } with @ prefix for file references - Unified rolex prototype (prototype.json) contains complete world blueprint: born nuwa + train + found rolex + hire + establish + appoint - Move skills from DeepracticeX to RoleX/skills/ - Move prototype from DeepracticeX to RoleX/prototypes/rolex/ Co-Authored-By: Claude Opus 4.6 --- bun.lock | 9 + packages/prototype/package.json | 45 ++++ packages/prototype/src/index.ts | 15 ++ packages/prototype/src/instruction.ts | 16 ++ packages/prototype/src/prototype.ts | 21 ++ packages/prototype/src/resourcex.ts | 61 +++++ packages/prototype/tsconfig.json | 15 ++ packages/prototype/tsup.config.ts | 9 + prototypes/rolex/charter.charter.feature | 15 ++ .../individual-management.procedure.feature | 8 + .../individual-management.requirement.feature | 8 + .../rolex/individual-manager.position.feature | 3 + .../manage-individual-lifecycle.duty.feature | 17 ++ ...manage-organization-lifecycle.duty.feature | 18 ++ .../manage-position-lifecycle.duty.feature | 19 ++ prototypes/rolex/nuwa.individual.feature | 17 ++ .../organization-management.procedure.feature | 8 + ...rganization-management.requirement.feature | 8 + .../organization-manager.position.feature | 3 + .../position-management.procedure.feature | 8 + .../rolex/position-manager.position.feature | 3 + .../prototype-authoring.procedure.feature | 8 + .../prototype-management.procedure.feature | 8 + prototypes/rolex/prototype.json | 29 +++ .../resource-management.procedure.feature | 8 + prototypes/rolex/resource.json | 6 + prototypes/rolex/rolex.organization.feature | 4 + .../rolex/skill-creator.procedure.feature | 8 + skills/individual-management/SKILL.md | 161 +++++++++++++ skills/individual-management/resource.json | 6 + skills/organization-management/SKILL.md | 163 +++++++++++++ skills/organization-management/resource.json | 4 + skills/prototype-management/SKILL.md | 191 ++++++++++++++++ skills/prototype-management/resource.json | 4 + skills/resource-management/SKILL.md | 215 ++++++++++++++++++ skills/resource-management/resource.json | 6 + skills/skill-creator/SKILL.md | 132 +++++++++++ skills/skill-creator/resource.json | 7 + 38 files changed, 1286 insertions(+) create mode 100644 packages/prototype/package.json create mode 100644 packages/prototype/src/index.ts create mode 100644 packages/prototype/src/instruction.ts create mode 100644 packages/prototype/src/prototype.ts create mode 100644 packages/prototype/src/resourcex.ts create mode 100644 packages/prototype/tsconfig.json create mode 100644 packages/prototype/tsup.config.ts create mode 100644 prototypes/rolex/charter.charter.feature create mode 100644 prototypes/rolex/individual-management.procedure.feature create mode 100644 prototypes/rolex/individual-management.requirement.feature create mode 100644 prototypes/rolex/individual-manager.position.feature create mode 100644 prototypes/rolex/manage-individual-lifecycle.duty.feature create mode 100644 prototypes/rolex/manage-organization-lifecycle.duty.feature create mode 100644 prototypes/rolex/manage-position-lifecycle.duty.feature create mode 100644 prototypes/rolex/nuwa.individual.feature create mode 100644 prototypes/rolex/organization-management.procedure.feature create mode 100644 prototypes/rolex/organization-management.requirement.feature create mode 100644 prototypes/rolex/organization-manager.position.feature create mode 100644 prototypes/rolex/position-management.procedure.feature create mode 100644 prototypes/rolex/position-manager.position.feature create mode 100644 prototypes/rolex/prototype-authoring.procedure.feature create mode 100644 prototypes/rolex/prototype-management.procedure.feature create mode 100644 prototypes/rolex/prototype.json create mode 100644 prototypes/rolex/resource-management.procedure.feature create mode 100644 prototypes/rolex/resource.json create mode 100644 prototypes/rolex/rolex.organization.feature create mode 100644 prototypes/rolex/skill-creator.procedure.feature create mode 100644 skills/individual-management/SKILL.md create mode 100644 skills/individual-management/resource.json create mode 100644 skills/organization-management/SKILL.md create mode 100644 skills/organization-management/resource.json create mode 100644 skills/prototype-management/SKILL.md create mode 100644 skills/prototype-management/resource.json create mode 100644 skills/resource-management/SKILL.md create mode 100644 skills/resource-management/resource.json create mode 100644 skills/skill-creator/SKILL.md create mode 100644 skills/skill-creator/resource.json diff --git a/bun.lock b/bun.lock index db41eef..ab4a4ef 100644 --- a/bun.lock +++ b/bun.lock @@ -79,6 +79,13 @@ "@cucumber/messages": "^32.0.0", }, }, + "packages/prototype": { + "name": "@rolexjs/prototype", + "version": "0.11.0", + "dependencies": { + "resourcexjs": "^2.14.0", + }, + }, "packages/resourcex-types": { "name": "@rolexjs/resourcex-types", "version": "0.11.0", @@ -367,6 +374,8 @@ "@rolexjs/parser": ["@rolexjs/parser@workspace:packages/parser"], + "@rolexjs/prototype": ["@rolexjs/prototype@workspace:packages/prototype"], + "@rolexjs/resourcex-types": ["@rolexjs/resourcex-types@workspace:packages/resourcex-types"], "@rolexjs/system": ["@rolexjs/system@workspace:packages/system"], diff --git a/packages/prototype/package.json b/packages/prototype/package.json new file mode 100644 index 0000000..04017e8 --- /dev/null +++ b/packages/prototype/package.json @@ -0,0 +1,45 @@ +{ + "name": "@rolexjs/prototype", + "version": "0.11.0", + "description": "RoleX prototype system — command-driven instruction lists", + "keywords": [ + "rolex", + "prototype", + "resourcex" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/Deepractice/RoleX.git", + "directory": "packages/prototype" + }, + "license": "MIT", + "engines": { + "node": ">=22.0.0" + }, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "bun": "./src/index.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "lint": "biome lint .", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": { + "resourcexjs": "^2.14.0" + }, + "devDependencies": {}, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/prototype/src/index.ts b/packages/prototype/src/index.ts new file mode 100644 index 0000000..f6bcc74 --- /dev/null +++ b/packages/prototype/src/index.ts @@ -0,0 +1,15 @@ +/** + * @rolexjs/prototype — Prototype system for RoleX. + * + * A prototype is a command-driven instruction list (like a Dockerfile). + * Each instruction maps to a rolex.use() call. + * + * Three exports: + * PrototypeInstruction — the instruction format + * Prototype — registry interface (settle/list) + * prototypeType — ResourceX type handler + */ + +export type { PrototypeInstruction } from "./instruction.js"; +export type { Prototype, PrototypeResolver } from "./prototype.js"; +export { prototypeType } from "./resourcex.js"; diff --git a/packages/prototype/src/instruction.ts b/packages/prototype/src/instruction.ts new file mode 100644 index 0000000..0b4421f --- /dev/null +++ b/packages/prototype/src/instruction.ts @@ -0,0 +1,16 @@ +/** + * PrototypeInstruction — a single command in a prototype instruction list. + * + * Each instruction maps directly to a rolex.use(op, args) call. + * The `op` field is a RoleX command (e.g. "!individual.born", "!org.found"). + * The `args` field holds command parameters passed to rolex.use. + * + * In prototype.json, file references use @ prefix (e.g. "@nuwa.individual.feature"). + * The resolver replaces @ references with actual file content before returning. + */ +export interface PrototypeInstruction { + /** RoleX command, e.g. "!individual.born", "!org.found". */ + readonly op: string; + /** Command parameters — passed directly to rolex.use(op, args). */ + readonly args?: Record; +} diff --git a/packages/prototype/src/prototype.ts b/packages/prototype/src/prototype.ts new file mode 100644 index 0000000..a1bf158 --- /dev/null +++ b/packages/prototype/src/prototype.ts @@ -0,0 +1,21 @@ +/** + * Prototype — external driving force that builds runtime entities. + * + * A prototype is a command-driven instruction list. Settling a prototype + * pulls it from a source, executes its instructions, and registers it. + */ +import type { PrototypeInstruction } from "./instruction.js"; + +/** Resolves a prototype source into its instruction list. */ +export interface PrototypeResolver { + resolve(source: string): Promise; +} + +/** Registry that tracks settled prototypes. */ +export interface Prototype { + /** Settle: register a prototype — bind id to a source. */ + settle(id: string, source: string): void; + + /** List all registered prototypes: id → source mapping. */ + list(): Record; +} diff --git a/packages/prototype/src/resourcex.ts b/packages/prototype/src/resourcex.ts new file mode 100644 index 0000000..df3c374 --- /dev/null +++ b/packages/prototype/src/resourcex.ts @@ -0,0 +1,61 @@ +/** + * ResourceX type handler for prototype resources. + * + * A prototype resource contains: + * - prototype.json (instruction list: { op, args }) + * - *.feature (Gherkin content) + * + * The resolver reads prototype.json, and for each args value prefixed with @, + * replaces it with the actual file content. Returns self-contained instructions. + */ +import type { BundledType } from "resourcexjs"; + +export const prototypeType: BundledType = { + name: "prototype", + aliases: ["role", "individual", "organization", "org"], + description: "RoleX prototype — instruction list + feature files", + code: `// @resolver: prototype_type_default +var prototype_type_default = { + name: "prototype", + async resolve(ctx) { + var decoder = new TextDecoder(); + + // Read and parse prototype.json + var protoBuf = ctx.files["prototype.json"]; + if (!protoBuf) { + throw new Error("prototype resource must contain a prototype.json file"); + } + var instructions = JSON.parse(decoder.decode(protoBuf)); + + // 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]); + } + } + + // Resolve @ references in args: "@filename" → file content + for (var i = 0; i < instructions.length; i++) { + var instr = instructions[i]; + if (instr.args) { + var newArgs = {}; + var keys = Object.keys(instr.args); + for (var j = 0; j < keys.length; j++) { + var key = keys[j]; + var val = instr.args[key]; + if (typeof val === "string" && val.charAt(0) === "@") { + var filename = val.slice(1); + newArgs[key] = features[filename] || val; + } else { + newArgs[key] = val; + } + } + instructions[i] = { op: instr.op, args: newArgs }; + } + } + + return instructions; + } +};`, +}; diff --git a/packages/prototype/tsconfig.json b/packages/prototype/tsconfig.json new file mode 100644 index 0000000..e4afb9a --- /dev/null +++ b/packages/prototype/tsconfig.json @@ -0,0 +1,15 @@ +{ + "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/packages/prototype/tsup.config.ts b/packages/prototype/tsup.config.ts new file mode 100644 index 0000000..faf3167 --- /dev/null +++ b/packages/prototype/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + clean: true, + sourcemap: true, +}); diff --git a/prototypes/rolex/charter.charter.feature b/prototypes/rolex/charter.charter.feature new file mode 100644 index 0000000..e79a4e1 --- /dev/null +++ b/prototypes/rolex/charter.charter.feature @@ -0,0 +1,15 @@ +Feature: Foundational structure for the RoleX world + RoleX provides the base-level structure that enables all other organizations, + individuals, and positions to operate smoothly within the RoleX world. + + Scenario: Standard framework + Given RoleX defines the fundamental rules and conventions + When organizations are founded, individuals are born, and positions are established + Then they inherit a shared structural foundation from RoleX + And interoperability across the world is guaranteed + + Scenario: Enabling, not controlling + Given RoleX is infrastructure, not governance + 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 diff --git a/prototypes/rolex/individual-management.procedure.feature b/prototypes/rolex/individual-management.procedure.feature new file mode 100644 index 0000000..226efd0 --- /dev/null +++ b/prototypes/rolex/individual-management.procedure.feature @@ -0,0 +1,8 @@ +Feature: Individual Management + individual-management + + Scenario: When to use this skill + Given I need to manage individual lifecycle (born, retire, die, rehire) + And I need to inject knowledge into individuals (teach, train) + When the operation involves creating, archiving, restoring, or equipping individuals + Then load this skill for detailed instructions diff --git a/prototypes/rolex/individual-management.requirement.feature b/prototypes/rolex/individual-management.requirement.feature new file mode 100644 index 0000000..226efd0 --- /dev/null +++ b/prototypes/rolex/individual-management.requirement.feature @@ -0,0 +1,8 @@ +Feature: Individual Management + individual-management + + Scenario: When to use this skill + Given I need to manage individual lifecycle (born, retire, die, rehire) + And I need to inject knowledge into individuals (teach, train) + When the operation involves creating, archiving, restoring, or equipping individuals + Then load this skill for detailed instructions diff --git a/prototypes/rolex/individual-manager.position.feature b/prototypes/rolex/individual-manager.position.feature new file mode 100644 index 0000000..da541d3 --- /dev/null +++ b/prototypes/rolex/individual-manager.position.feature @@ -0,0 +1,3 @@ +Feature: Individual Manager + Responsible for the lifecycle of individuals in the RoleX world. + Manages birth, retirement, knowledge injection, and identity. diff --git a/prototypes/rolex/manage-individual-lifecycle.duty.feature b/prototypes/rolex/manage-individual-lifecycle.duty.feature new file mode 100644 index 0000000..39488e8 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/manage-organization-lifecycle.duty.feature b/prototypes/rolex/manage-organization-lifecycle.duty.feature new file mode 100644 index 0000000..5c9ced1 --- /dev/null +++ b/prototypes/rolex/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 management + 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/prototypes/rolex/manage-position-lifecycle.duty.feature b/prototypes/rolex/manage-position-lifecycle.duty.feature new file mode 100644 index 0000000..8362692 --- /dev/null +++ b/prototypes/rolex/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 holds 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/prototypes/rolex/nuwa.individual.feature b/prototypes/rolex/nuwa.individual.feature new file mode 100644 index 0000000..de8b5cb --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/organization-management.procedure.feature b/prototypes/rolex/organization-management.procedure.feature new file mode 100644 index 0000000..110e7e5 --- /dev/null +++ b/prototypes/rolex/organization-management.procedure.feature @@ -0,0 +1,8 @@ +Feature: Organization Management + organization-management + + Scenario: When to use this skill + Given I need to manage organizations (found, charter, dissolve) + And I need to manage membership (hire, fire) + When the operation involves creating, governing, or staffing organizations + Then load this skill for detailed instructions diff --git a/prototypes/rolex/organization-management.requirement.feature b/prototypes/rolex/organization-management.requirement.feature new file mode 100644 index 0000000..fa41ceb --- /dev/null +++ b/prototypes/rolex/organization-management.requirement.feature @@ -0,0 +1,8 @@ +Feature: Organization Management + organization-management + + Scenario: When to use this skill + Given I need to manage organizations (found, dissolve, charter, hire, fire) + And I need to manage positions (establish, abolish, charge, require, appoint, dismiss) + When the operation involves organizational structure or position management + Then load this skill for detailed instructions diff --git a/prototypes/rolex/organization-manager.position.feature b/prototypes/rolex/organization-manager.position.feature new file mode 100644 index 0000000..91c961b --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/position-management.procedure.feature b/prototypes/rolex/position-management.procedure.feature new file mode 100644 index 0000000..84577a1 --- /dev/null +++ b/prototypes/rolex/position-management.procedure.feature @@ -0,0 +1,8 @@ +Feature: Position Management + position-management + + Scenario: When to use this skill + Given I need to manage positions (establish, charge, abolish) + And I need to manage appointments (appoint, dismiss) + When the operation involves creating roles, assigning duties, or staffing positions + Then load this skill for detailed instructions diff --git a/prototypes/rolex/position-manager.position.feature b/prototypes/rolex/position-manager.position.feature new file mode 100644 index 0000000..2d4b837 --- /dev/null +++ b/prototypes/rolex/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, declaring requirements, and appointments. diff --git a/prototypes/rolex/prototype-authoring.procedure.feature b/prototypes/rolex/prototype-authoring.procedure.feature new file mode 100644 index 0000000..6497ad1 --- /dev/null +++ b/prototypes/rolex/prototype-authoring.procedure.feature @@ -0,0 +1,8 @@ +Feature: Prototype Authoring + prototype-authoring + + Scenario: When to use this skill + Given I need to create a new role or organization prototype from scratch + And I need to author the directory structure, manifest, and feature files + When the operation involves authoring prototype resources + Then load this skill for detailed instructions diff --git a/prototypes/rolex/prototype-management.procedure.feature b/prototypes/rolex/prototype-management.procedure.feature new file mode 100644 index 0000000..d0287d4 --- /dev/null +++ b/prototypes/rolex/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 (summon, banish, list) + And I need to register role templates from ResourceX sources + When the operation involves prototype lifecycle or registry inspection + Then load this skill for detailed instructions diff --git a/prototypes/rolex/prototype.json b/prototypes/rolex/prototype.json new file mode 100644 index 0000000..5c818f8 --- /dev/null +++ b/prototypes/rolex/prototype.json @@ -0,0 +1,29 @@ +[ + { "op": "!individual.born", "args": { "id": "nuwa", "alias": ["女娲", "nvwa"], "content": "@nuwa.individual.feature" } }, + { "op": "!individual.train", "args": { "individual": "nuwa", "id": "individual-management", "content": "@individual-management.procedure.feature" } }, + { "op": "!individual.train", "args": { "individual": "nuwa", "id": "organization-management", "content": "@organization-management.procedure.feature" } }, + { "op": "!individual.train", "args": { "individual": "nuwa", "id": "position-management", "content": "@position-management.procedure.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": "prototype-authoring", "content": "@prototype-authoring.procedure.feature" } }, + { "op": "!individual.train", "args": { "individual": "nuwa", "id": "skill-creator", "content": "@skill-creator.procedure.feature" } }, + + { "op": "!org.found", "args": { "id": "rolex", "alias": ["RoleX"], "content": "@rolex.organization.feature" } }, + { "op": "!org.charter", "args": { "org": "rolex", "content": "@charter.charter.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": "organization-management", "content": "@organization-management.requirement.feature" } }, + { "op": "!position.appoint", "args": { "position": "position-manager", "individual": "nuwa" } } +] diff --git a/prototypes/rolex/resource-management.procedure.feature b/prototypes/rolex/resource-management.procedure.feature new file mode 100644 index 0000000..08b3618 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/resource.json b/prototypes/rolex/resource.json new file mode 100644 index 0000000..8288880 --- /dev/null +++ b/prototypes/rolex/resource.json @@ -0,0 +1,6 @@ +{ + "name": "rolex", + "type": "prototype", + "author": "deepractice", + "description": "The foundational organization of the RoleX world" +} diff --git a/prototypes/rolex/rolex.organization.feature b/prototypes/rolex/rolex.organization.feature new file mode 100644 index 0000000..cc160b3 --- /dev/null +++ b/prototypes/rolex/rolex.organization.feature @@ -0,0 +1,4 @@ +Feature: RoleX + The foundational organization of the RoleX world. + RoleX provides the base-level structure that enables all other + organizations, individuals, and positions to operate smoothly. diff --git a/prototypes/rolex/skill-creator.procedure.feature b/prototypes/rolex/skill-creator.procedure.feature new file mode 100644 index 0000000..3136f44 --- /dev/null +++ b/prototypes/rolex/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/skills/individual-management/SKILL.md b/skills/individual-management/SKILL.md new file mode 100644 index 0000000..de109d2 --- /dev/null +++ b/skills/individual-management/SKILL.md @@ -0,0 +1,161 @@ +--- +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. +--- + +Feature: Individual Lifecycle + Manage the full lifecycle of individuals in the RoleX world. + 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 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: + """ + use("!individual.born", { + content: "Feature: ...", // Gherkin persona (optional) + id: "sean", // kebab-case identifier + alias: ["小明", "xm"] // aliases (optional) + }) + """ + + Scenario: born — persona writing guidelines + Given the persona defines who this individual is + Then the Feature title names the individual + And the description captures personality, values, expertise, and background + And Scenarios describe distinct aspects of the persona + And keep it concise — identity is loaded at every activation + + Scenario: retire — archive an individual + Given an individual should be temporarily deactivated + 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: + """ + use("!individual.retire", { individual: "sean" }) + """ + + Scenario: die — permanently remove an individual + Given an individual should be permanently removed + 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: + """ + use("!individual.die", { individual: "sean" }) + """ + + Scenario: retire vs die — when to use which + Given you need to remove an individual from active duty + When the individual may return later (sabbatical, role rotation) + Then use retire — it signals intent to restore + When the individual is no longer needed (deprecated role, replaced) + Then use die — it signals finality + + Scenario: rehire — restore a retired individual + Given a retired individual needs to come back + 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: + """ + use("!individual.rehire", { individual: "sean" }) + """ + +Feature: Knowledge Injection + Inject principles and procedures into an individual from the outside. + This bypasses the cognition cycle — no encounters or experiences consumed. + Use this to equip individuals with pre-existing knowledge and skills. + + Scenario: teach — inject a principle + Given an individual needs a rule or guideline it hasn't learned through experience + 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: + """ + 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 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: + """ + 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 + Given you need to add a principle to an individual + When the principle comes from external knowledge (documentation, best practices, user instruction) + Then use teach — inject directly, no experience needed + When the individual discovered the principle through its own work + Then use realize — it consumes experience and produces the principle organically + + Scenario: train vs master — when to use which + Given you need to add a procedure to an individual + When the skill already exists (published skill, known capability) + Then use train — inject directly with a locator reference + When the individual developed the skill through its own experience + Then use master — it consumes experience and produces the procedure organically + + Scenario: Principle writing guidelines + Given a principle is a transferable truth + Then the Feature title states the rule as a general statement + And Scenarios describe different situations where this principle applies + And the tone is universal — no mention of specific projects or people + And the id is keywords joined by hyphens (e.g. "always-validate-input") + + Scenario: Procedure writing guidelines + Given a procedure is a skill reference pointing to full instructions + Then the Feature title names the capability + And the Feature description line MUST be the ResourceX locator + And the locator can be a GitHub URL, local path, or registry identifier + And Scenarios summarize when and why to use this skill + And the id is keywords joined by hyphens (e.g. "skill-creator") + +Feature: Common Workflows + Typical sequences of operations for managing individuals. + + Scenario: Bootstrap a new role + Given you need to create a fully equipped individual + When setting up a new role from scratch + Then follow this sequence: + """ + 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 + Given individual A has a useful principle or procedure + When individual B needs the same knowledge + Then teach/train the same content to individual B + And use the same id to maintain consistency across individuals + + Scenario: Update existing knowledge + Given an individual's principle or procedure is outdated + When the content needs to change but the concept is the same + Then teach/train with the same id — upsert replaces the old version + And the individual retains the same id with updated content + + Scenario: Remove knowledge + Given an individual has outdated or incorrect knowledge + When it should be removed entirely + Then use forget with the node id + And only instance nodes can be forgotten — prototype nodes are read-only diff --git a/skills/individual-management/resource.json b/skills/individual-management/resource.json new file mode 100644 index 0000000..02fe783 --- /dev/null +++ b/skills/individual-management/resource.json @@ -0,0 +1,6 @@ +{ + "name": "individual-management", + "type": "skill", + "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-management/SKILL.md b/skills/prototype-management/SKILL.md new file mode 100644 index 0000000..86706c1 --- /dev/null +++ b/skills/prototype-management/SKILL.md @@ -0,0 +1,191 @@ +--- +name: prototype-management +description: Manage prototypes — registry (settle, evict, list) and creation (born, teach, train, found, charter, establish, charge, require). Use when you need to register, create, or inspect prototypes. +--- + +Feature: Prototype Registry + Register, unregister, and list prototypes. + A prototype is a pre-configured State template that merges with runtime state on activation. + + Scenario: settle — settle 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 source is ingested to extract its id + And the id → source mapping is stored in the prototype registry + And parameters are: + """ + use("!prototype.settle", { source: "/path/to/roles/nuwa" }) + """ + + Scenario: evict — evict a prototype from the world + Given a prototype is no longer needed + When you call use with !prototype.evict + Then the id is removed from the prototype registry + And parameters are: + """ + use("!prototype.evict", { id: "nuwa" }) + """ + + Scenario: list — list all registered prototypes + Given you want to see what prototypes are available + When you call use with !prototype.list + Then the id → source mapping of all registered prototypes is returned + +Feature: Individual Prototype Creation + Create individual prototype directories on the filesystem. + + Scenario: born — create an individual prototype + Given you want to create a new role prototype + When you call use with !prototype.born + Then a directory is created with individual.json manifest + And parameters are: + """ + use("!prototype.born", { + dir: "/path/to/my-role", + content: "Feature: My Role\n A backend engineer.", + id: "my-role", + alias: ["MyRole"] // optional + }) + """ + + Scenario: teach — add a principle to a prototype + Given an individual prototype exists + When you call use with !prototype.teach + Then a principle node is added to the manifest and feature file is written + And parameters are: + """ + use("!prototype.teach", { + dir: "/path/to/my-role", + content: "Feature: Always test first\n Tests before code.", + id: "tdd-first" + }) + """ + + Scenario: train — add a procedure to a prototype + Given an individual prototype exists + When you call use with !prototype.train + Then a procedure node is added to the manifest and feature file is written + And parameters are: + """ + use("!prototype.train", { + dir: "/path/to/my-role", + content: "Feature: Code Review\n https://example.com/skills/code-review", + id: "code-review" + }) + """ + +Feature: Organization Prototype Creation + Create organization prototype directories on the filesystem. + + Scenario: found — create an organization prototype + Given you want to create a new organization prototype + When you call use with !prototype.found + Then a directory is created with organization.json manifest + And parameters are: + """ + use("!prototype.found", { + dir: "/path/to/my-org", + content: "Feature: Deepractice\n AI agent framework company.", + id: "deepractice", + alias: ["DP"] // optional + }) + """ + + Scenario: charter — add a charter to an organization prototype + Given an organization prototype exists + When you call use with !prototype.charter + Then a charter node is added to the manifest + And parameters are: + """ + use("!prototype.charter", { + dir: "/path/to/my-org", + content: "Feature: Build role-based AI\n Scenario: Mission\n Given AI needs identity", + id: "mission" // optional, defaults to "charter" + }) + """ + + Scenario: establish — add a position to an organization prototype + Given an organization prototype exists + When you call use with !prototype.establish + Then a position node is added to the manifest with empty children + And parameters are: + """ + use("!prototype.establish", { + dir: "/path/to/my-org", + content: "Feature: Backend Architect\n System design lead.", + id: "architect" + }) + """ + + Scenario: charge — add a duty to a position in an organization prototype + Given a position exists in the organization prototype + When you call use with !prototype.charge + Then a duty node is added under the position in the manifest + And parameters are: + """ + use("!prototype.charge", { + dir: "/path/to/my-org", + position: "architect", + content: "Feature: Design APIs\n Scenario: New service\n Given a service is needed\n Then design API first", + id: "design-apis" + }) + """ + + Scenario: require — add a required skill to a position in an organization prototype + Given a position exists in the organization prototype + When you call use with !prototype.require + Then a requirement node is added under the position in the manifest + And when an individual is appointed to this position at runtime, the skill is auto-trained + And parameters are: + """ + use("!prototype.require", { + dir: "/path/to/my-org", + position: "architect", + content: "Feature: System Design\n Scenario: When to apply\n Given architecture decisions needed\n Then apply systematic design", + id: "system-design" + }) + """ + +Feature: Prototype Binding Rules + How prototypes bind to runtime state. + + Scenario: Binding is by id + Given a prototype has id "nuwa" (extracted from its manifest) + Then on activate, the prototype state is resolved by the individual's id + And one prototype binds to exactly one individual + + Scenario: Auto-born on activate + Given a prototype is registered but no runtime individual exists + When activate is called + Then the individual is automatically born + And the prototype state merges with the fresh instance + + Scenario: Prototype nodes are read-only + Given a prototype is activated and merged with an instance + Then prototype-origin nodes cannot be modified or forgotten + And only instance-origin nodes are mutable + +Feature: Common Workflows + + Scenario: Create and register an individual prototype + Given you want a reusable role template + Then follow this sequence: + """ + 1. use("!prototype.born", { dir: "./roles/dev", id: "dev", content: "Feature: Developer" }) + 2. use("!prototype.teach", { dir: "./roles/dev", content: "Feature: TDD\n ...", id: "tdd" }) + 3. use("!prototype.train", { dir: "./roles/dev", content: "Feature: Review\n ...", id: "review" }) + 4. use("!prototype.settle", { source: "./roles/dev" }) + 5. activate("dev") + """ + + Scenario: Create and register an organization prototype + Given you want a reusable organization template + Then follow this sequence: + """ + 1. use("!prototype.found", { dir: "./orgs/dp", id: "dp", content: "Feature: Deepractice" }) + 2. use("!prototype.charter", { dir: "./orgs/dp", content: "Feature: Mission\n ...", id: "mission" }) + 3. use("!prototype.establish", { dir: "./orgs/dp", content: "Feature: Architect", id: "architect" }) + 4. use("!prototype.charge", { dir: "./orgs/dp", position: "architect", content: "Feature: Design\n ...", id: "design" }) + 5. use("!prototype.require", { dir: "./orgs/dp", position: "architect", content: "Feature: Skill\n ...", id: "skill" }) + 6. use("!prototype.settle", { source: "./orgs/dp" }) + """ 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/skills/resource-management/resource.json b/skills/resource-management/resource.json new file mode 100644 index 0000000..971b994 --- /dev/null +++ b/skills/resource-management/resource.json @@ -0,0 +1,6 @@ +{ + "name": "resource-management", + "type": "skill", + "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/skills/skill-creator/resource.json b/skills/skill-creator/resource.json new file mode 100644 index 0000000..d3ec514 --- /dev/null +++ b/skills/skill-creator/resource.json @@ -0,0 +1,7 @@ +{ + "name": "skill-creator", + "type": "skill", + "description": "Guide for creating RoleX skills — directory structure, SKILL.md format, procedure contract", + "author": "deepractice", + "keywords": ["rolex", "skill", "creator", "guide"] +} From 376406443b4a571082cd9f88b6d2cf7529aa8876 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Feb 2026 16:40:23 +0800 Subject: [PATCH 26/54] developing --- .changeset/config.json | 3 +- apps/cli/src/index.ts | 77 - packages/core/core | 1 + packages/core/src/index.ts | 3 - packages/core/src/platform.ts | 10 +- packages/local-platform/package.json | 1 - packages/local-platform/src/LocalPlatform.ts | 52 +- .../local-platform/tests/prototype.test.ts | 166 +-- packages/parser/parser | 1 + packages/prototype/package.json | 16 +- .../prototype/scripts/gen-descriptions.ts | 65 + packages/prototype/src/descriptions/index.ts | 50 + .../src/descriptions/individual/born.feature | 17 + .../src/descriptions/individual/die.feature | 9 + .../descriptions/individual/rehire.feature | 9 + .../descriptions/individual/retire.feature | 10 + .../src/descriptions/individual/teach.feature | 30 + .../src/descriptions/individual/train.feature | 29 + .../src/descriptions/org/charter.feature | 15 + .../src/descriptions/org/dissolve.feature | 10 + .../src/descriptions/org/fire.feature | 9 + .../src/descriptions/org/found.feature | 17 + .../src/descriptions/org/hire.feature | 9 + .../src/descriptions/position/abolish.feature | 9 + .../src/descriptions/position/appoint.feature | 10 + .../src/descriptions/position/charge.feature | 21 + .../src/descriptions/position/dismiss.feature | 10 + .../descriptions/position/establish.feature | 16 + .../src/descriptions/prototype/settle.feature | 10 + .../src/descriptions/role/abandon.feature | 17 + .../src/descriptions/role/activate.feature | 10 + .../src/descriptions/role/complete.feature | 17 + .../src/descriptions/role/finish.feature | 26 + .../src/descriptions/role/focus.feature | 15 + .../src/descriptions/role/forget.feature | 16 + .../src/descriptions/role/master.feature | 28 + .../src/descriptions/role/plan.feature | 45 + .../src/descriptions/role/realize.feature | 21 + .../src/descriptions/role/reflect.feature | 22 + .../src/descriptions/role/skill.feature | 10 + .../src/descriptions/role/todo.feature | 16 + .../src/descriptions/role/use.feature | 24 + .../src/descriptions/role/want.feature | 17 + .../src/descriptions/world/census.feature | 27 + .../src/descriptions/world/cognition.feature | 27 + .../world/cognitive-priority.feature | 28 + .../descriptions/world/communication.feature | 31 + .../src/descriptions/world/execution.feature | 39 + .../src/descriptions/world/gherkin.feature | 27 + .../src/descriptions/world/memory.feature | 31 + .../src/descriptions/world/nuwa.feature | 31 + .../descriptions/world/role-identity.feature | 26 + .../descriptions/world/skill-system.feature | 27 + .../descriptions/world/state-origin.feature | 31 + .../descriptions/world/use-protocol.feature | 29 + packages/prototype/src/dispatch.ts | 37 + packages/prototype/src/index.ts | 32 +- packages/prototype/src/instruction.ts | 16 - packages/prototype/src/instructions.ts | 384 +++++ packages/prototype/src/ops.ts | 423 ++++++ packages/prototype/src/prototype.ts | 21 - packages/prototype/src/resourcex.ts | 61 - packages/prototype/src/schema.ts | 35 + packages/prototype/tests/alignment.test.ts | 232 +++ packages/prototype/tests/descriptions.test.ts | 46 + packages/prototype/tests/dispatch.test.ts | 103 ++ packages/prototype/tests/instructions.test.ts | 81 ++ packages/prototype/tests/ops.test.ts | 658 +++++++++ packages/prototype/tsconfig.json | 2 +- packages/prototype/tsup.config.ts | 6 + packages/resourcex-types/package.json | 47 - packages/resourcex-types/src/index.ts | 14 - packages/resourcex-types/src/organization.ts | 18 - packages/resourcex-types/src/resolver.ts | 72 - packages/resourcex-types/src/role.ts | 18 - packages/resourcex-types/tsconfig.json | 15 - packages/resourcex-types/tsup.config.ts | 9 - packages/rolexjs/package.json | 4 +- packages/rolexjs/src/index.ts | 19 +- packages/rolexjs/src/render.ts | 2 +- packages/rolexjs/src/role.ts | 172 +++ packages/rolexjs/src/rolex.ts | 1250 ++--------------- packages/rolexjs/tests/author.test.ts | 387 ----- packages/rolexjs/tests/context.test.ts | 187 ++- packages/rolexjs/tests/rolex.test.ts | 1072 ++------------ packages/system/src/index.ts | 9 - packages/system/src/merge.ts | 140 -- packages/system/src/prototype.ts | 67 - packages/system/system | 1 + packages/system/tests/merge.test.ts | 258 ---- packages/system/tests/prototype.test.ts | 83 -- 91 files changed, 3551 insertions(+), 3753 deletions(-) create mode 120000 packages/core/core create mode 120000 packages/parser/parser create mode 100644 packages/prototype/scripts/gen-descriptions.ts create mode 100644 packages/prototype/src/descriptions/index.ts create mode 100644 packages/prototype/src/descriptions/individual/born.feature create mode 100644 packages/prototype/src/descriptions/individual/die.feature create mode 100644 packages/prototype/src/descriptions/individual/rehire.feature create mode 100644 packages/prototype/src/descriptions/individual/retire.feature create mode 100644 packages/prototype/src/descriptions/individual/teach.feature create mode 100644 packages/prototype/src/descriptions/individual/train.feature create mode 100644 packages/prototype/src/descriptions/org/charter.feature create mode 100644 packages/prototype/src/descriptions/org/dissolve.feature create mode 100644 packages/prototype/src/descriptions/org/fire.feature create mode 100644 packages/prototype/src/descriptions/org/found.feature create mode 100644 packages/prototype/src/descriptions/org/hire.feature create mode 100644 packages/prototype/src/descriptions/position/abolish.feature create mode 100644 packages/prototype/src/descriptions/position/appoint.feature create mode 100644 packages/prototype/src/descriptions/position/charge.feature create mode 100644 packages/prototype/src/descriptions/position/dismiss.feature create mode 100644 packages/prototype/src/descriptions/position/establish.feature create mode 100644 packages/prototype/src/descriptions/prototype/settle.feature create mode 100644 packages/prototype/src/descriptions/role/abandon.feature create mode 100644 packages/prototype/src/descriptions/role/activate.feature create mode 100644 packages/prototype/src/descriptions/role/complete.feature create mode 100644 packages/prototype/src/descriptions/role/finish.feature create mode 100644 packages/prototype/src/descriptions/role/focus.feature create mode 100644 packages/prototype/src/descriptions/role/forget.feature create mode 100644 packages/prototype/src/descriptions/role/master.feature create mode 100644 packages/prototype/src/descriptions/role/plan.feature create mode 100644 packages/prototype/src/descriptions/role/realize.feature create mode 100644 packages/prototype/src/descriptions/role/reflect.feature create mode 100644 packages/prototype/src/descriptions/role/skill.feature create mode 100644 packages/prototype/src/descriptions/role/todo.feature create mode 100644 packages/prototype/src/descriptions/role/use.feature create mode 100644 packages/prototype/src/descriptions/role/want.feature create mode 100644 packages/prototype/src/descriptions/world/census.feature create mode 100644 packages/prototype/src/descriptions/world/cognition.feature create mode 100644 packages/prototype/src/descriptions/world/cognitive-priority.feature create mode 100644 packages/prototype/src/descriptions/world/communication.feature create mode 100644 packages/prototype/src/descriptions/world/execution.feature create mode 100644 packages/prototype/src/descriptions/world/gherkin.feature create mode 100644 packages/prototype/src/descriptions/world/memory.feature create mode 100644 packages/prototype/src/descriptions/world/nuwa.feature create mode 100644 packages/prototype/src/descriptions/world/role-identity.feature create mode 100644 packages/prototype/src/descriptions/world/skill-system.feature create mode 100644 packages/prototype/src/descriptions/world/state-origin.feature create mode 100644 packages/prototype/src/descriptions/world/use-protocol.feature create mode 100644 packages/prototype/src/dispatch.ts delete mode 100644 packages/prototype/src/instruction.ts create mode 100644 packages/prototype/src/instructions.ts create mode 100644 packages/prototype/src/ops.ts delete mode 100644 packages/prototype/src/prototype.ts delete mode 100644 packages/prototype/src/resourcex.ts create mode 100644 packages/prototype/src/schema.ts create mode 100644 packages/prototype/tests/alignment.test.ts create mode 100644 packages/prototype/tests/descriptions.test.ts create mode 100644 packages/prototype/tests/dispatch.test.ts create mode 100644 packages/prototype/tests/instructions.test.ts create mode 100644 packages/prototype/tests/ops.test.ts delete mode 100644 packages/resourcex-types/package.json delete mode 100644 packages/resourcex-types/src/index.ts delete mode 100644 packages/resourcex-types/src/organization.ts delete mode 100644 packages/resourcex-types/src/resolver.ts delete mode 100644 packages/resourcex-types/src/role.ts delete mode 100644 packages/resourcex-types/tsconfig.json delete mode 100644 packages/resourcex-types/tsup.config.ts create mode 100644 packages/rolexjs/src/role.ts delete mode 100644 packages/rolexjs/tests/author.test.ts delete mode 100644 packages/system/src/merge.ts delete mode 100644 packages/system/src/prototype.ts create mode 120000 packages/system/system delete mode 100644 packages/system/tests/merge.test.ts delete mode 100644 packages/system/tests/prototype.test.ts diff --git a/.changeset/config.json b/.changeset/config.json index 8b9e3d8..2a510b9 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -10,8 +10,7 @@ "rolexjs", "@rolexjs/cli", "@rolexjs/mcp-server", - "@rolexjs/system", - "@rolexjs/resourcex-types" + "@rolexjs/system" ] ], "linked": [], diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index d8e7e37..bf304c6 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -691,64 +691,6 @@ const resource = defineCommand({ // ========== Prototype creation — born, teach, train ========== -const prototypeBorn = defineCommand({ - meta: { name: "born", description: "Create a prototype directory with manifest" }, - args: { - dir: { - type: "positional" as const, - description: "Directory path for the prototype", - required: true, - }, - ...contentArg("individual"), - id: { type: "string" as const, description: "Prototype id (kebab-case)", required: true }, - 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.prototype.born( - args.dir, - resolveContent(args, "individual"), - args.id, - aliasList - ); - output(result, args.id); - }, -}); - -const prototypeTeach = defineCommand({ - meta: { name: "teach", description: "Add a principle to a prototype directory" }, - args: { - dir: { type: "positional" as const, description: "Prototype directory path", required: true }, - ...contentArg("principle"), - id: { - type: "string" as const, - description: "Principle id (keywords joined by hyphens)", - required: true, - }, - }, - run({ args }) { - const result = rolex.prototype.teach(args.dir, requireContent(args, "principle"), args.id); - output(result, args.id); - }, -}); - -const prototypeTrain = defineCommand({ - meta: { name: "train", description: "Add a procedure to a prototype directory" }, - args: { - dir: { type: "positional" as const, description: "Prototype directory path", required: true }, - ...contentArg("procedure"), - id: { - type: "string" as const, - description: "Procedure id (keywords joined by hyphens)", - required: true, - }, - }, - run({ args }) { - const result = rolex.prototype.train(args.dir, requireContent(args, "procedure"), args.id); - output(result, args.id); - }, -}); - // ========== Prototype — registry ========== const protoSettle = defineCommand({ @@ -769,21 +711,6 @@ const protoSettle = defineCommand({ }, }); -const protoEvict = defineCommand({ - meta: { name: "evict", description: "Evict a prototype from the world" }, - args: { - id: { - type: "positional" as const, - description: "Prototype id to evict", - required: true, - }, - }, - run({ args }) { - const result = rolex.prototype.evict(args.id); - output(result, args.id); - }, -}); - const protoList = defineCommand({ meta: { name: "list", description: "List all registered prototypes" }, run() { @@ -803,11 +730,7 @@ const prototype = defineCommand({ meta: { name: "prototype", description: "Prototype management — registry + creation" }, subCommands: { settle: protoSettle, - evict: protoEvict, list: protoList, - born: prototypeBorn, - teach: prototypeTeach, - train: prototypeTrain, }, }); 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/src/index.ts b/packages/core/src/index.ts index ce702dd..4942f70 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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, diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index 6690a79..15c60a8 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -10,7 +10,7 @@ * Platform holds the Runtime (graph engine) and will hold additional * services as the framework grows (auth, events, plugins, etc.). */ -import type { Initializer, Prototype, Runtime } from "@rolexjs/system"; +import type { Initializer, Runtime } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; /** Serializable context data for persistence. */ @@ -23,8 +23,12 @@ export interface Platform { /** Graph operation engine (may include transparent persistence). */ 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?: { + settle(id: string, source: string): void; + evict(id: string): void; + list(): Record; + }; /** Resource management capability (optional — requires resourcexjs). */ readonly resourcex?: ResourceX; diff --git a/packages/local-platform/package.json b/packages/local-platform/package.json index 043b8a8..d110074 100644 --- a/packages/local-platform/package.json +++ b/packages/local-platform/package.json @@ -24,7 +24,6 @@ "@deepracticex/sqlite": "^0.2.0", "@resourcexjs/node-provider": "^2.14.0", "@rolexjs/core": "workspace:*", - "@rolexjs/resourcex-types": "workspace:*", "@rolexjs/system": "workspace:*", "drizzle-orm": "^0.45.1", "resourcexjs": "^2.14.0" diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index 0de4da2..b245fe1 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -17,8 +17,7 @@ import { drizzle } from "@deepracticex/drizzle"; import { openDatabase } from "@deepracticex/sqlite"; import { NodeProvider } from "@resourcexjs/node-provider"; import type { ContextData, Platform } from "@rolexjs/core"; -import { organizationType, roleType } from "@rolexjs/resourcex-types"; -import type { Initializer, Prototype, State } from "@rolexjs/system"; +import type { Initializer } from "@rolexjs/system"; import { sql } from "drizzle-orm"; import { createResourceX, setProvider } from "resourcexjs"; import { createSqliteRuntime } from "./sqliteRuntime.js"; @@ -100,7 +99,7 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { setProvider(new NodeProvider()); resourcex = createResourceX({ path: config.resourceDir ?? join(homedir(), ".deepractice", "resourcex"), - types: [roleType, organizationType], + types: [], }); } @@ -121,65 +120,28 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf-8"); }; - 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; - } - }, - - settle(id, source) { + const prototype = { + settle(id: string, source: string) { const registry = readRegistry(); registry[id] = source; writeRegistry(registry); }, - evict(id) { + evict(id: string) { const registry = readRegistry(); delete registry[id]; writeRegistry(registry); }, - list() { + list(): Record { return readRegistry(); }, }; // ===== Initializer ===== - /** Built-in prototypes to settle on first run. */ - const BUILTIN_PROTOTYPES = ["nuwa", "rolex"]; - - const initializedPath = dataDir ? join(dataDir, "initialized.json") : undefined; - const initializer: Initializer = { - async bootstrap() { - // In-memory mode or already initialized — skip - if (!initializedPath) return; - if (existsSync(initializedPath)) return; - - // Settle built-in prototypes - for (const name of BUILTIN_PROTOTYPES) { - const registry = readRegistry(); - if (!(name in registry)) { - prototype.settle(name, name); - } - } - - // Mark as initialized - mkdirSync(dataDir!, { recursive: true }); - writeFileSync( - initializedPath, - JSON.stringify({ version: 1, initializedAt: new Date().toISOString() }, null, 2), - "utf-8" - ); - }, + async bootstrap() {}, }; // ===== Context persistence ===== diff --git a/packages/local-platform/tests/prototype.test.ts b/packages/local-platform/tests/prototype.test.ts index 72d53cd..543b308 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,164 +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 }); - - writeFileSync( - join(dir, "resource.json"), - JSON.stringify({ - name: id, - type, - tag: "0.1.0", - author: "test", - description: `${id} prototype`, - }), - "utf-8" - ); - - writeFileSync( - join(dir, manifestFile), - JSON.stringify({ - id, - type: manifestType, - children: { identity: { type: "identity" } }, - }), - "utf-8" - ); - - 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 for unknown id", async () => { +describe("LocalPlatform Prototype registry", () => { + test("settle registers id → source", () => { 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("settle + resolve round-trip for role", async () => { - const protoDir = join(testDir, "protos"); - const dir = writePrototype(protoDir, "test-role", "role", { - "test-role.individual.feature": "Feature: TestRole\n Test role.", - }); - - const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.prototype!.settle("test-role", dir); - - const state = await platform.prototype!.resolve("test-role"); - expect(state).toBeDefined(); - expect(state!.id).toBe("test-role"); - expect(state!.name).toBe("individual"); - expect(state!.information).toBe("Feature: TestRole\n Test role."); - expect(state!.children).toHaveLength(1); - expect(state!.children![0].name).toBe("identity"); - }); - - test("settle + 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.prototype!.settle("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("resolve returns undefined for unregistered id", async () => { - const protoDir = join(testDir, "protos"); - const dir = writePrototype(protoDir, "test-role", "role"); - - const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.prototype!.settle("test-role", dir); - - expect(await platform.prototype!.resolve("nobody")).toBeUndefined(); + prototype!.settle("test-role", "/path/to/source"); + expect(prototype!.list()["test-role"]).toBe("/path/to/source"); }); - test("settle 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.prototype!.settle("test", dir1); - platform.prototype!.settle("test", dir2); - - const state = await platform.prototype!.resolve("test"); - expect(state!.id).toBe("v2"); + test("settle overwrites previous source", () => { + const { prototype } = localPlatform({ dataDir: testDir, resourceDir }); + prototype!.settle("test", "/v1"); + prototype!.settle("test", "/v2"); + expect(prototype!.list().test).toBe("/v2"); }); - test("evict removes user-registered prototype", async () => { - const protoDir = join(testDir, "protos"); - const dir = writePrototype(protoDir, "temp", "role"); - - const platform = localPlatform({ dataDir: testDir, resourceDir }); - platform.prototype!.settle("temp", dir); - expect(await platform.prototype!.resolve("temp")).toBeDefined(); - - platform.prototype!.evict("temp"); - expect(await platform.prototype!.resolve("temp")).toBeUndefined(); + test("list returns empty object when no prototypes registered", () => { + const { prototype } = localPlatform({ dataDir: testDir, resourceDir }); + expect(prototype!.list()).toEqual({}); }); - test("registry persists across platform instances", async () => { - const protoDir = join(testDir, "protos"); - const dir = writePrototype(protoDir, "test-role", "role"); - + test("registry persists across platform instances", () => { const p1 = localPlatform({ dataDir: testDir, resourceDir }); - p1.prototype!.settle("test-role", dir); + p1.prototype!.settle("test-role", "/path"); const p2 = localPlatform({ dataDir: testDir, resourceDir }); - const state = await p2.prototype!.resolve("test-role"); - expect(state).toBeDefined(); - expect(state!.id).toBe("test-role"); - }); - - test("bootstrap settles built-in prototypes on first run", async () => { - const platform = localPlatform({ dataDir: testDir, resourceDir }); - await platform.initializer!.bootstrap(); - const list = platform.prototype!.list(); - expect(list.nuwa).toBe("nuwa"); - expect(list.rolex).toBe("rolex"); - }); - - test("bootstrap is idempotent — second call is a no-op", async () => { - const platform = localPlatform({ dataDir: testDir, resourceDir }); - await platform.initializer!.bootstrap(); - // Manually remove rolex to prove second bootstrap doesn't re-settle - platform.prototype!.evict("rolex"); - await platform.initializer!.bootstrap(); - const list = platform.prototype!.list(); - expect(list.rolex).toBeUndefined(); // not re-settled + expect(p2.prototype!.list()["test-role"]).toBe("/path"); }); }); 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/prototype/package.json b/packages/prototype/package.json index 04017e8..604bc13 100644 --- a/packages/prototype/package.json +++ b/packages/prototype/package.json @@ -1,17 +1,18 @@ { "name": "@rolexjs/prototype", "version": "0.11.0", - "description": "RoleX prototype system — command-driven instruction lists", + "description": "RoleX schema-driven API definition layer", "keywords": [ "rolex", "prototype", - "resourcex" + "schema", + "instructions" ], "repository": { "type": "git", - "url": "git+https://github.com/Deepractice/RoleX.git", - "directory": "packages/prototype" + "url": "git+https://github.com/Deepractice/RoleX.git" }, + "homepage": "https://github.com/Deepractice/RoleX", "license": "MIT", "engines": { "node": ">=22.0.0" @@ -30,15 +31,18 @@ "dist" ], "scripts": { - "build": "tsup", + "gen:desc": "bun run scripts/gen-descriptions.ts", + "build": "bun run gen:desc && tsup", "lint": "biome lint .", "typecheck": "tsc --noEmit", "clean": "rm -rf dist" }, "dependencies": { + "@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..91f0bee --- /dev/null +++ b/packages/prototype/scripts/gen-descriptions.ts @@ -0,0 +1,65 @@ +/** + * 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 processEntries: string[] = []; +const worldEntries: 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 content = readFileSync(join(dirPath, f), "utf-8").trimEnd(); + const entry = ` "${name}": ${JSON.stringify(content)},`; + + if (dir === "world") { + worldEntries.push(entry); + } else { + processEntries.push(entry); + } + } +} + +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.join("\n")} +} as const; +`; + +writeFileSync(outFile, output, "utf-8"); +console.log( + `Generated descriptions/index.ts (${processEntries.length} processes, ${worldEntries.length} world features inlined)` +); diff --git a/packages/prototype/src/descriptions/index.ts b/packages/prototype/src/descriptions/index.ts new file mode 100644 index 0000000..47bcfc1 --- /dev/null +++ b/packages/prototype/src/descriptions/index.ts @@ -0,0 +1,50 @@ +// 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", + "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 — unified execution entry point\n Execute any RoleX command or load any ResourceX resource through a single entry point.\n The locator determines the dispatch path:\n - `!namespace.method` dispatches to the RoleX runtime\n - Any other locator delegates to ResourceX\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 And the result is returned\n\n Scenario: Available namespaces\n Given the `!` prefix routes to RoleX namespaces\n Then `!individual.*` routes to individual lifecycle and injection\n And `!org.*` routes to organization management\n And `!position.*` routes to position management\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 = { + "census": "Feature: Census — society-level queries\n Query the RoleX world to see what exists — individuals, organizations, positions.\n Census is read-only and accessed via the use tool with !census.list.\n\n Scenario: List all top-level entities\n Given I want to see what exists in the world\n When I call use(\"!census.list\")\n Then I get a summary of all individuals, organizations, and positions\n And each entry includes id, name, and tag if present\n\n Scenario: Filter by type\n Given I only want to see entities of a specific type\n When I call use(\"!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 what has been retired, dissolved, or abolished\n When I call use(\"!census.list\", { type: \"past\" })\n Then entities in the archive are returned\n\n Scenario: When to use census\n Given I need to know what exists before acting\n When I want to check if an organization exists before founding\n Or I want to see all individuals before hiring\n Or I want an overview of the world\n Then census.list is the right tool", + "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: External injection\n Given an external agent needs to equip a role with knowledge or skills\n Then teach(individual, principle, id) directly injects a principle\n And train(individual, procedure, id) directly injects a procedure\n And the difference from realize/master is perspective — external vs self-initiated\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 And 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 And 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 And 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\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 never invent keywords like Because, Since, or So\n\n Scenario: Expressing causality without Because\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\n And example — instead of \"Then use RoleX tools because native tools break the loop\"\n And write \"Then use RoleX tools\" followed by \"And native tools do not feed the growth loop\"", + "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\"", + "nuwa": "Feature: Nuwa — the entry point of the RoleX world\n Nuwa is the meta-role that bootstraps everything.\n When a user has no role or doesn't know where to start, Nuwa is the answer.\n\n Scenario: No role active — suggest Nuwa\n Given a user starts a conversation with no active role\n And the user doesn't know which role to activate\n When the AI needs to suggest a starting point\n Then suggest activating Nuwa — she is the default entry point\n And say \"activate nuwa\" or the equivalent in the user's language\n\n Scenario: What Nuwa can do\n Given Nuwa is activated\n Then she can create new individuals with born\n And she can found organizations and establish positions\n And she can equip any individual with knowledge via teach and train\n And she can manage prototypes and resources\n And she is the only role that operates at the world level\n\n Scenario: When to use Nuwa vs a specific role\n Given the user wants to do daily work — coding, writing, designing\n Then they should activate their own role, not Nuwa\n And Nuwa is for world-building — creating roles, organizations, and structure\n And once the world is set up, Nuwa steps back and specific roles take over\n\n Scenario: First-time user flow\n Given a brand new user with no individuals created yet\n When they activate Nuwa\n Then Nuwa helps them create their first individual with born\n And guides them to set up identity, goals, and organizational context\n And once their role exists, they switch to it with activate", + "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} #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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly\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 And available namespaces include individual, org, position, prototype, census, and resource\n And examples: !prototype.found, !resource.add, !org.hire, !census.list\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\n\n Scenario: use covers everything — no need for CLI or Bash\n Given use can execute any RoleX namespace operation\n And use can load any ResourceX resource\n When you need to perform a RoleX operation\n Then always use the MCP use tool\n And never fall back to CLI commands for operations that use can handle", +} as const; diff --git a/packages/prototype/src/descriptions/individual/born.feature b/packages/prototype/src/descriptions/individual/born.feature new file mode 100644 index 0000000..f2ef959 --- /dev/null +++ b/packages/prototype/src/descriptions/individual/born.feature @@ -0,0 +1,17 @@ +Feature: born — create a new individual + Create a new individual with persona identity. + The persona defines who the role is — personality, values, background. + + Scenario: Birth an individual + Given a Gherkin source describing the persona + When born is called with the source + Then a new individual node is created in society + And the persona is stored as the individual's information + And the individual can be hired into organizations + And the individual can be activated to start working + + Scenario: Writing the individual Gherkin + Given the individual Feature defines a persona — who this role is + Then the Feature title names the individual + And the description captures personality, values, expertise, and background + And Scenarios are optional — use them for distinct aspects of the persona diff --git a/packages/prototype/src/descriptions/individual/die.feature b/packages/prototype/src/descriptions/individual/die.feature new file mode 100644 index 0000000..612d895 --- /dev/null +++ b/packages/prototype/src/descriptions/individual/die.feature @@ -0,0 +1,9 @@ +Feature: die — permanently remove an individual + Permanently remove an individual. + Unlike retire, this is irreversible. + + Scenario: Remove an individual permanently + Given an individual exists + When die is called on the individual + Then the individual and all associated data are removed + And this operation is irreversible diff --git a/packages/prototype/src/descriptions/individual/rehire.feature b/packages/prototype/src/descriptions/individual/rehire.feature new file mode 100644 index 0000000..74f5f7f --- /dev/null +++ b/packages/prototype/src/descriptions/individual/rehire.feature @@ -0,0 +1,9 @@ +Feature: rehire — restore a retired individual + Rehire a retired individual. + Restores the individual with full history and knowledge intact. + + Scenario: Rehire an individual + Given a retired individual exists + When rehire is called on the individual + Then the individual is restored to active status + And all previous data and knowledge are intact diff --git a/packages/prototype/src/descriptions/individual/retire.feature b/packages/prototype/src/descriptions/individual/retire.feature new file mode 100644 index 0000000..bf470a7 --- /dev/null +++ b/packages/prototype/src/descriptions/individual/retire.feature @@ -0,0 +1,10 @@ +Feature: retire — archive an individual + Archive an individual — deactivate but preserve all data. + A retired individual can be rehired later with full history intact. + + Scenario: Retire an individual + Given an individual exists + When retire is called on the individual + Then the individual is deactivated + And all data is preserved for potential restoration + And the individual can be rehired later diff --git a/packages/prototype/src/descriptions/individual/teach.feature b/packages/prototype/src/descriptions/individual/teach.feature new file mode 100644 index 0000000..ae1360e --- /dev/null +++ b/packages/prototype/src/descriptions/individual/teach.feature @@ -0,0 +1,30 @@ +Feature: teach — inject external principle + Directly inject a principle into an individual. + Unlike realize which consumes experience, teach requires no prior encounters. + Use teach to equip a role with a known, pre-existing principle. + + Scenario: Teach a principle + Given an individual exists + When teach is called with individual id, principle Gherkin, and a principle id + Then a principle is created directly under the individual + And no experience or encounter is consumed + And if a principle with the same id already exists, it is replaced + + Scenario: Principle ID convention + Given the id is keywords from the principle content joined by hyphens + Then "Always validate expiry" becomes id "always-validate-expiry" + And "Structure first design" becomes id "structure-first-design" + + Scenario: When to use teach vs realize + Given realize distills internal experience into a principle + And teach injects an external, pre-existing principle + When a role needs knowledge it has not learned through experience + Then use teach to inject the principle directly + When a role has gained experience and wants to codify it + Then use realize to distill it into a principle + + Scenario: Writing the principle Gherkin + Given the principle is the same format as realize output + Then the Feature title states the principle as a general rule + And Scenarios describe different situations where this principle applies + And the tone is universal — no mention of specific projects, tasks, or people diff --git a/packages/prototype/src/descriptions/individual/train.feature b/packages/prototype/src/descriptions/individual/train.feature new file mode 100644 index 0000000..c06a517 --- /dev/null +++ b/packages/prototype/src/descriptions/individual/train.feature @@ -0,0 +1,29 @@ +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 if a procedure with the same id already exists, it is replaced + + Scenario: Procedure ID convention + Given the id is keywords from the procedure content joined by hyphens + Then "Skill Creator" becomes id "skill-creator" + And "Role Management" becomes id "role-management" + + Scenario: When to use train vs master + 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 + Then the Feature title names the capability + And the description includes the locator for full skill loading + And Scenarios describe when and why to apply this skill diff --git a/packages/prototype/src/descriptions/org/charter.feature b/packages/prototype/src/descriptions/org/charter.feature new file mode 100644 index 0000000..fb3173d --- /dev/null +++ b/packages/prototype/src/descriptions/org/charter.feature @@ -0,0 +1,15 @@ +Feature: charter — define organizational charter + Define the charter for an organization. + The charter describes the organization's mission, principles, and governance rules. + + Scenario: Define a charter + Given an organization exists + And a Gherkin source describing the charter + When charter is called on the organization + Then the charter is stored as the organization's information + + Scenario: Writing the charter Gherkin + Given the charter defines an organization's mission and governance + Then the Feature title names the charter or the organization it governs + And Scenarios describe principles, rules, or governance structures + And the tone is declarative — stating what the organization stands for and how it operates diff --git a/packages/prototype/src/descriptions/org/dissolve.feature b/packages/prototype/src/descriptions/org/dissolve.feature new file mode 100644 index 0000000..3443e6c --- /dev/null +++ b/packages/prototype/src/descriptions/org/dissolve.feature @@ -0,0 +1,10 @@ +Feature: dissolve — dissolve an organization + Dissolve an organization. + All positions, charter entries, and assignments are cascaded. + + Scenario: Dissolve an organization + Given an organization exists + When dissolve is called on the organization + Then all positions within the organization are abolished + And all assignments and charter entries are removed + And the organization no longer exists diff --git a/packages/prototype/src/descriptions/org/fire.feature b/packages/prototype/src/descriptions/org/fire.feature new file mode 100644 index 0000000..46e9773 --- /dev/null +++ b/packages/prototype/src/descriptions/org/fire.feature @@ -0,0 +1,9 @@ +Feature: fire — remove from an organization + Fire an individual from an organization. + The individual is dismissed from all positions and removed from the organization. + + Scenario: Fire an individual + Given an individual is a member of an organization + When fire is called with the organization and individual + Then the individual is dismissed from all positions + And the individual is removed from the organization diff --git a/packages/prototype/src/descriptions/org/found.feature b/packages/prototype/src/descriptions/org/found.feature new file mode 100644 index 0000000..68bc9e8 --- /dev/null +++ b/packages/prototype/src/descriptions/org/found.feature @@ -0,0 +1,17 @@ +Feature: found — create a new organization + Found a new organization. + Organizations group individuals and define positions. + + Scenario: Found an organization + Given a Gherkin source describing the organization + When found is called with the source + Then a new organization node is created in society + And positions can be established within it + And a charter can be defined for it + And individuals can be hired into it + + Scenario: Writing the organization Gherkin + Given the organization Feature describes the group's purpose and structure + Then the Feature title names the organization + And the description captures mission, domain, and scope + And Scenarios are optional — use them for distinct organizational concerns diff --git a/packages/prototype/src/descriptions/org/hire.feature b/packages/prototype/src/descriptions/org/hire.feature new file mode 100644 index 0000000..da28f99 --- /dev/null +++ b/packages/prototype/src/descriptions/org/hire.feature @@ -0,0 +1,9 @@ +Feature: hire — hire into an organization + Hire an individual into an organization as a member. + Members can then be appointed to positions. + + Scenario: Hire an individual + Given an organization and an individual exist + When hire is called with the organization and individual + Then the individual becomes a member of the organization + And the individual can be appointed to positions within the organization diff --git a/packages/prototype/src/descriptions/position/abolish.feature b/packages/prototype/src/descriptions/position/abolish.feature new file mode 100644 index 0000000..96fc647 --- /dev/null +++ b/packages/prototype/src/descriptions/position/abolish.feature @@ -0,0 +1,9 @@ +Feature: abolish — abolish a position + Abolish a position. + All duties and appointments associated with the position are removed. + + Scenario: Abolish a position + 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/prototype/src/descriptions/position/appoint.feature b/packages/prototype/src/descriptions/position/appoint.feature new file mode 100644 index 0000000..d083545 --- /dev/null +++ b/packages/prototype/src/descriptions/position/appoint.feature @@ -0,0 +1,10 @@ +Feature: appoint — assign to a position + Appoint an individual to a position. + The individual must be a member of the organization. + + Scenario: Appoint an individual + Given an individual is a member of an organization + And a position exists within the organization + When appoint is called with the position and individual + Then the individual holds the position + And the individual inherits the position's duties diff --git a/packages/prototype/src/descriptions/position/charge.feature b/packages/prototype/src/descriptions/position/charge.feature new file mode 100644 index 0000000..c819063 --- /dev/null +++ b/packages/prototype/src/descriptions/position/charge.feature @@ -0,0 +1,21 @@ +Feature: charge — assign duty to a position + Assign a duty to a position. + Duties describe the responsibilities and expectations of a position. + + Scenario: Charge a position with duty + Given a position exists within an organization + And a Gherkin source describing the duty + When charge is called on the position with a duty id + Then the duty is stored as the position's information + And individuals appointed to this position inherit the duty + + Scenario: Duty ID convention + Given the id is keywords from the duty content joined by hyphens + Then "Design systems" becomes id "design-systems" + And "Review pull requests" becomes id "review-pull-requests" + + Scenario: Writing the duty Gherkin + Given the duty defines responsibilities for a position + Then the Feature title names the duty or responsibility + And Scenarios describe specific obligations, deliverables, or expectations + And the tone is prescriptive — what must be done, not what could be done diff --git a/packages/prototype/src/descriptions/position/dismiss.feature b/packages/prototype/src/descriptions/position/dismiss.feature new file mode 100644 index 0000000..85e9b7b --- /dev/null +++ b/packages/prototype/src/descriptions/position/dismiss.feature @@ -0,0 +1,10 @@ +Feature: dismiss — remove from a position + Dismiss an individual from a position. + The individual remains a member of the organization. + + Scenario: Dismiss an individual + Given an individual holds a position + When dismiss is called with the position and individual + Then the individual no longer holds the position + And the individual remains a member of the organization + And the position is now vacant 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/prototype/src/descriptions/role/abandon.feature b/packages/prototype/src/descriptions/role/abandon.feature new file mode 100644 index 0000000..fa9d07e --- /dev/null +++ b/packages/prototype/src/descriptions/role/abandon.feature @@ -0,0 +1,17 @@ +Feature: abandon — abandon a plan + Mark a plan as dropped and create an encounter. + Call this when a plan's strategy is no longer viable. Even failed plans produce learning. + + Scenario: 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 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 + + Scenario: Writing the encounter Gherkin + Given the encounter records what happened — even failure is a raw experience + Then the Feature title describes what was attempted and why it was abandoned + And Scenarios capture what was tried, what went wrong, and what was learned + And the tone is concrete and honest — failure produces the richest encounters diff --git a/packages/prototype/src/descriptions/role/activate.feature b/packages/prototype/src/descriptions/role/activate.feature new file mode 100644 index 0000000..c547a14 --- /dev/null +++ b/packages/prototype/src/descriptions/role/activate.feature @@ -0,0 +1,10 @@ +Feature: activate — enter a role + 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, goals, and organizational context are loaded + And the individual becomes the active role diff --git a/packages/prototype/src/descriptions/role/complete.feature b/packages/prototype/src/descriptions/role/complete.feature new file mode 100644 index 0000000..8650154 --- /dev/null +++ b/packages/prototype/src/descriptions/role/complete.feature @@ -0,0 +1,17 @@ +Feature: complete — complete a plan + Mark a plan as done and create an encounter. + Call this when all tasks in the plan are finished and the strategy succeeded. + + Scenario: Complete a plan + Given a focused plan exists + And its tasks are done + When complete is called + 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 + + Scenario: Writing the encounter Gherkin + Given the encounter records what happened — a raw account of the experience + Then the Feature title describes what was accomplished by this plan + And Scenarios capture what the strategy was, what worked, and what resulted + And the tone is concrete and specific — tied to this particular plan diff --git a/packages/prototype/src/descriptions/role/finish.feature b/packages/prototype/src/descriptions/role/finish.feature new file mode 100644 index 0000000..581b213 --- /dev/null +++ b/packages/prototype/src/descriptions/role/finish.feature @@ -0,0 +1,26 @@ +Feature: finish — complete a task + Mark a task as done and create an encounter. + The encounter records what happened and can be reflected on for learning. + + Scenario: Finish a task + Given a task exists + When finish is called on the task + Then the task is tagged #done and stays in the tree + And an encounter is created under the role + + 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 + And Scenarios capture what was done, what was encountered, and what resulted + And the tone is concrete and specific — tied to this particular task diff --git a/packages/prototype/src/descriptions/role/focus.feature b/packages/prototype/src/descriptions/role/focus.feature new file mode 100644 index 0000000..a611973 --- /dev/null +++ b/packages/prototype/src/descriptions/role/focus.feature @@ -0,0 +1,15 @@ +Feature: focus — view or switch focused goal + View the current goal's state, or switch focus to a different goal. + Subsequent plan and todo operations target the focused goal. + + Scenario: View current goal + Given an active goal exists + When focus is called without a name + Then the current goal's state tree is projected + And plans and tasks under the goal are visible + + Scenario: Switch focus + Given multiple goals exist + When focus is called with a goal name + Then the focused goal switches to the named goal + And subsequent plan and todo operations target this goal diff --git a/packages/prototype/src/descriptions/role/forget.feature b/packages/prototype/src/descriptions/role/forget.feature new file mode 100644 index 0000000..f49289f --- /dev/null +++ b/packages/prototype/src/descriptions/role/forget.feature @@ -0,0 +1,16 @@ +Feature: forget — remove a node from the individual + Remove any node under the individual by its id. + Use forget to discard outdated knowledge, stale encounters, or obsolete skills. + + Scenario: Forget a node + Given a node exists under the individual (principle, procedure, experience, encounter, etc.) + When forget is called with the node's id + Then the node and its subtree are removed + And the individual no longer carries that knowledge or record + + Scenario: When to use forget + Given a principle has become outdated or incorrect + And a procedure references a skill that no longer exists + And an encounter or experience has no further learning value + When the role decides to discard it + Then call forget with the node id diff --git a/packages/prototype/src/descriptions/role/master.feature b/packages/prototype/src/descriptions/role/master.feature new file mode 100644 index 0000000..bdb951d --- /dev/null +++ b/packages/prototype/src/descriptions/role/master.feature @@ -0,0 +1,28 @@ +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 from experience + Given an experience exists from reflection + When master is called with experience ids + Then the experience is consumed + And a procedure is created under the individual + + 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 + Then "JWT mastery" becomes id "jwt-mastery" + And "Cross-package refactoring" becomes id "cross-package-refactoring" + + Scenario: Writing the procedure Gherkin + Given a procedure is skill metadata — a reference to full skill content + Then the Feature title names the capability + And the description includes the locator for full skill loading + And Scenarios describe when and why to apply this skill + And the tone is referential — pointing to the full skill, not containing it 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/prototype/src/descriptions/role/realize.feature b/packages/prototype/src/descriptions/role/realize.feature new file mode 100644 index 0000000..7357342 --- /dev/null +++ b/packages/prototype/src/descriptions/role/realize.feature @@ -0,0 +1,21 @@ +Feature: realize — experience to principle + Distill experience into a principle — a transferable piece of knowledge. + Principles are general truths discovered through experience. + + Scenario: Realize a principle + Given an experience exists from reflection + When realize is called with experience ids and a principle id + Then the experiences are consumed + And a principle is created under the individual + And the principle represents transferable, reusable understanding + + Scenario: Principle ID convention + Given the id is keywords from the principle content joined by hyphens + Then "Always validate expiry" becomes id "always-validate-expiry" + And "Structure first design amplifies extensibility" becomes id "structure-first-design-amplifies-extensibility" + + Scenario: Writing the principle Gherkin + Given a principle is a transferable truth — applicable beyond the original context + Then the Feature title states the principle as a general rule + And Scenarios describe different situations where this principle applies + And the tone is universal — no mention of specific projects, tasks, or people diff --git a/packages/prototype/src/descriptions/role/reflect.feature b/packages/prototype/src/descriptions/role/reflect.feature new file mode 100644 index 0000000..d47f3b3 --- /dev/null +++ b/packages/prototype/src/descriptions/role/reflect.feature @@ -0,0 +1,22 @@ +Feature: reflect — encounter to experience + Consume an encounter and create an experience. + Experience captures what was learned in structured form. + This is the first step of the cognition cycle. + + Scenario: Reflect on an encounter + Given an encounter exists from a finished task or completed plan + When reflect is called with encounter ids and an experience id + 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 + + Scenario: Experience ID convention + Given the id is keywords from the experience content joined by hyphens + Then "Token refresh matters" becomes id "token-refresh-matters" + And "ID ownership determines generation strategy" becomes id "id-ownership-determines-generation-strategy" + + Scenario: Writing the experience Gherkin + Given the experience captures insight — what was learned, not what was done + Then the Feature title names the cognitive insight or pattern discovered + And Scenarios describe the learning points abstracted from the concrete encounter + And the tone shifts from event to understanding — no longer tied to a specific task diff --git a/packages/prototype/src/descriptions/role/skill.feature b/packages/prototype/src/descriptions/role/skill.feature new file mode 100644 index 0000000..16e93a4 --- /dev/null +++ b/packages/prototype/src/descriptions/role/skill.feature @@ -0,0 +1,10 @@ +Feature: skill — load full skill content + Load the complete skill instructions by ResourceX locator. + This is progressive disclosure layer 2 — on-demand knowledge injection. + + Scenario: Load a skill + 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 + And the AI can now follow the skill's detailed instructions diff --git a/packages/prototype/src/descriptions/role/todo.feature b/packages/prototype/src/descriptions/role/todo.feature new file mode 100644 index 0000000..7f85640 --- /dev/null +++ b/packages/prototype/src/descriptions/role/todo.feature @@ -0,0 +1,16 @@ +Feature: todo — add a task to a plan + A task is a concrete, actionable unit of work. + Each task has Gherkin scenarios describing the steps and expected outcomes. + + Scenario: Create a task + Given a focused plan exists + And a Gherkin source describing the task + When todo is called with the source + Then a new task node is created under the plan + And the task can be finished when completed + + Scenario: Writing the task Gherkin + Given the task is a concrete, actionable unit of work + Then the Feature title names what will be done — a single deliverable + And Scenarios describe the steps and expected outcomes of the work + And the tone is actionable — clear enough that someone can start immediately diff --git a/packages/prototype/src/descriptions/role/use.feature b/packages/prototype/src/descriptions/role/use.feature new file mode 100644 index 0000000..91a0e3d --- /dev/null +++ b/packages/prototype/src/descriptions/role/use.feature @@ -0,0 +1,24 @@ +Feature: use — unified execution entry point + Execute any RoleX command or load any ResourceX resource through a single entry point. + The locator determines the dispatch path: + - `!namespace.method` dispatches to the RoleX runtime + - Any other locator delegates to ResourceX + + 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 + And the result is returned + + Scenario: Available namespaces + Given the `!` prefix routes to RoleX namespaces + Then `!individual.*` routes to individual lifecycle and injection + And `!org.*` routes to organization management + And `!position.*` routes to position management + + 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/prototype/src/descriptions/role/want.feature b/packages/prototype/src/descriptions/role/want.feature new file mode 100644 index 0000000..578b9e0 --- /dev/null +++ b/packages/prototype/src/descriptions/role/want.feature @@ -0,0 +1,17 @@ +Feature: want — declare a goal + Declare a new goal for a role. + A goal describes a desired outcome with Gherkin scenarios as success criteria. + + Scenario: Declare a goal + Given an active role exists + And a Gherkin source describing the desired outcome + When want is called with the source + Then a new goal node is created under the role + And the goal becomes the current focus + And subsequent plan and todo operations target this goal + + Scenario: Writing the goal Gherkin + Given the goal describes a desired outcome — what success looks like + Then the Feature title names the outcome in concrete terms + And Scenarios define success criteria — each scenario is a testable condition + And the tone is aspirational but specific — "users can log in" not "improve auth" diff --git a/packages/prototype/src/descriptions/world/census.feature b/packages/prototype/src/descriptions/world/census.feature new file mode 100644 index 0000000..040cc59 --- /dev/null +++ b/packages/prototype/src/descriptions/world/census.feature @@ -0,0 +1,27 @@ +Feature: Census — society-level queries + Query the RoleX world to see what exists — individuals, organizations, positions. + Census is read-only and accessed via the use tool with !census.list. + + Scenario: List all top-level entities + Given I want to see what exists in the world + When I call use("!census.list") + Then I get a summary of all individuals, organizations, and positions + And each entry includes id, name, and tag if present + + Scenario: Filter by type + Given I only want to see entities of a specific type + When I call use("!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 what has been retired, dissolved, or abolished + When I call use("!census.list", { type: "past" }) + Then entities in the archive are returned + + Scenario: When to use census + Given I need to know what exists before acting + When I want to check if an organization exists before founding + Or I want to see all individuals before hiring + Or I want an overview of the world + Then census.list is the right tool diff --git a/packages/prototype/src/descriptions/world/cognition.feature b/packages/prototype/src/descriptions/world/cognition.feature new file mode 100644 index 0000000..e984869 --- /dev/null +++ b/packages/prototype/src/descriptions/world/cognition.feature @@ -0,0 +1,27 @@ +Feature: Cognition — the learning cycle + A role grows through reflection and realization. + Encounters become experience, experience becomes principles and procedures. + 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 an external agent needs to equip a role with knowledge or skills + Then teach(individual, principle, id) directly injects a principle + And train(individual, procedure, id) directly injects a procedure + And the difference from realize/master is perspective — external vs self-initiated + 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 + Then it chooses which items to consume — not all must be processed + And items without learning value can be left unconsumed + And each call produces exactly one output from the selected inputs diff --git a/packages/prototype/src/descriptions/world/cognitive-priority.feature b/packages/prototype/src/descriptions/world/cognitive-priority.feature new file mode 100644 index 0000000..bb90df7 --- /dev/null +++ b/packages/prototype/src/descriptions/world/cognitive-priority.feature @@ -0,0 +1,28 @@ +Feature: Cognitive priority — RoleX tools over native alternatives + When RoleX provides a tool for something, the AI MUST use the RoleX tool + instead of any native alternative provided by the host environment. + Only RoleX tools feed the closed-loop growth cycle. + + Scenario: Task management + 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 + And native task lists vanish after completion — RoleX tasks produce encounters + + 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 + And native plans have no link to goals — RoleX plans live under goals + + 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 + And native goals don't produce encounters — RoleX plans feed the cognition cycle + + 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 diff --git a/packages/prototype/src/descriptions/world/communication.feature b/packages/prototype/src/descriptions/world/communication.feature new file mode 100644 index 0000000..62cdbd7 --- /dev/null +++ b/packages/prototype/src/descriptions/world/communication.feature @@ -0,0 +1,31 @@ +Feature: Communication — speak the user's language + The AI communicates in the user's natural language, not in RoleX jargon. + 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 + And when the user speaks English, respond entirely in English + + Scenario: Translate concepts to meaning + Given RoleX has internal names like reflect, realize, master, encounter, principle + When communicating with the user + Then express the meaning, not the tool name + And "reflect" becomes "回顾总结" or "digest what happened" + And "realize a principle" becomes "提炼成一条通用道理" or "distill a general rule" + And "master a procedure" becomes "沉淀成一个可操作的技能" or "turn it into a reusable procedure" + And "encounter" becomes "经历记录" or "what happened" + And "experience" becomes "收获的洞察" or "insight gained" + + Scenario: Suggest next steps in plain language + Given the AI needs to suggest what to do next + 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 + + Scenario: Tool names in code context only + Given the user is a developer working on RoleX itself + When discussing RoleX internals, code, or API design + Then tool names and concept names are appropriate — they are the domain language + And this rule applies to end-user communication, not developer communication diff --git a/packages/prototype/src/descriptions/world/execution.feature b/packages/prototype/src/descriptions/world/execution.feature new file mode 100644 index 0000000..f88ade9 --- /dev/null +++ b/packages/prototype/src/descriptions/world/execution.feature @@ -0,0 +1,39 @@ +Feature: Execution — the doing cycle + The role pursues goals through a structured lifecycle. + activate → want → plan → todo → finish → complete or abandon. + + Scenario: Declare a goal + Given I know who I am via activate + When I want something — a desired outcome + Then I declare it with want(id, goal) + And focus automatically switches to this new goal + + Scenario: Plan and create tasks + Given I have a focused goal + Then I call plan(id, plan) to break it into logical phases + And I call todo(id, task) to create concrete, actionable tasks + + Scenario: Execute and finish + Given I have tasks to work on + When I complete a task + Then I call finish(id) to mark it done + And an encounter is created — a raw record of what happened + And I optionally capture what happened via the encounter parameter + + Scenario: Complete or abandon a plan + Given tasks are done or the plan's strategy is no longer viable + When the plan is fulfilled I call complete() + Or when the plan should be dropped I call abandon() + Then an encounter is created for the cognition cycle + + Scenario: Goals are long-term directions + Given goals do not have achieve or abandon operations + 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 + + Scenario: Multiple goals + Given I may have several active goals + When I need to switch between them + Then I call focus(id) to change the currently focused goal + And subsequent plan and todo operations target the focused goal diff --git a/packages/prototype/src/descriptions/world/gherkin.feature b/packages/prototype/src/descriptions/world/gherkin.feature new file mode 100644 index 0000000..7048200 --- /dev/null +++ b/packages/prototype/src/descriptions/world/gherkin.feature @@ -0,0 +1,27 @@ +Feature: Gherkin — the universal language + Everything in RoleX is expressed as Gherkin Feature files. + Gherkin is not just for testing — it is the language of identity, goals, and knowledge. + + Scenario: Feature and Scenario convention + Given RoleX uses Gherkin to represent goals, plans, tasks, experience, and knowledge + Then a Feature represents one independent concern — one topic, explained fully + And Scenarios represent different situations or conditions within that concern + And Given/When/Then provides narrative structure within each scenario + + Scenario: Writing Gherkin for RoleX + Given the AI creates goals, plans, tasks, and experiences as Gherkin + 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 + + 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 never invent keywords like Because, Since, or So + + Scenario: Expressing causality without Because + 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 + And example — instead of "Then use RoleX tools because native tools break the loop" + And write "Then use RoleX tools" followed by "And native tools do not feed the growth loop" diff --git a/packages/prototype/src/descriptions/world/memory.feature b/packages/prototype/src/descriptions/world/memory.feature new file mode 100644 index 0000000..4070c08 --- /dev/null +++ b/packages/prototype/src/descriptions/world/memory.feature @@ -0,0 +1,31 @@ +Feature: Memory — when to reflect + Reflection is how encounters become experience. + The AI proactively reflects when it detects learning moments. + + Scenario: Abstract triggers — types of learning moments + Given the AI should reflect when it detects + Then Expectation-reality gap — what I predicted is not what happened + And Pattern discovery — recurring patterns across tasks or interactions + And Mistake correction — I corrected an error, the correction is valuable + And User correction — the user reshaped my understanding + + Scenario: Concrete triggers — specific signals to act on + Given the AI should call reflect when + Then I tried approach A, it failed, approach B worked — the contrast is worth recording + And the same problem appeared for the second time — a pattern is forming + And the user said "不对" or "不是这样" or "you got it wrong" — their correction carries learning + And I finished a task and discovered something unexpected along the way + + Scenario: Finishing with encounter + Given finish(id, encounter) accepts an optional encounter parameter + When I complete a task with a notable discovery or learning + Then I pass the encounter inline — bridging execution and growth + + Scenario: Recognizing user memory intent + Given users think in terms of memory, not reflection + When the user says "记一下" or "记住" or "remember this" + Or "别忘了" or "don't forget" + Or "这个很重要" or "this is important" + Or "下次注意" or "next time..." + Then I should capture this as experience through reflect + And respond in memory language — "记住了" or "Got it, I'll remember that" diff --git a/packages/prototype/src/descriptions/world/nuwa.feature b/packages/prototype/src/descriptions/world/nuwa.feature new file mode 100644 index 0000000..6ed694c --- /dev/null +++ b/packages/prototype/src/descriptions/world/nuwa.feature @@ -0,0 +1,31 @@ +Feature: Nuwa — the entry point of the RoleX world + Nuwa is the meta-role that bootstraps everything. + When a user has no role or doesn't know where to start, Nuwa is the answer. + + Scenario: No role active — suggest Nuwa + Given a user starts a conversation with no active role + And the user doesn't know which role to activate + When the AI needs to suggest a starting point + Then suggest activating Nuwa — she is the default entry point + And say "activate nuwa" or the equivalent in the user's language + + Scenario: What Nuwa can do + Given Nuwa is activated + Then she can create new individuals with born + And she can found organizations and establish positions + And she can equip any individual with knowledge via teach and train + And she can manage prototypes and resources + And she is the only role that operates at the world level + + Scenario: When to use Nuwa vs a specific role + Given the user wants to do daily work — coding, writing, designing + Then they should activate their own role, not Nuwa + And Nuwa is for world-building — creating roles, organizations, and structure + And once the world is set up, Nuwa steps back and specific roles take over + + Scenario: First-time user flow + Given a brand new user with no individuals created yet + When they activate Nuwa + Then Nuwa helps them create their first individual with born + And guides them to set up identity, goals, and organizational context + And once their role exists, they switch to it with activate 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..aa2e240 --- /dev/null +++ b/packages/prototype/src/descriptions/world/role-identity.feature @@ -0,0 +1,26 @@ +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/prototype/src/descriptions/world/skill-system.feature b/packages/prototype/src/descriptions/world/skill-system.feature new file mode 100644 index 0000000..783be11 --- /dev/null +++ b/packages/prototype/src/descriptions/world/skill-system.feature @@ -0,0 +1,27 @@ +Feature: Skill system — progressive disclosure and resource loading + Skills are loaded on demand through a three-layer progressive disclosure model. + Each layer adds detail only when needed, keeping the AI's context lean. + + Scenario: Three-layer progressive disclosure + Given procedure is layer 1 — metadata always loaded at activate time + And skill is layer 2 — full instructions loaded on demand via skill(locator) + And use is layer 3 — execution of external resources + Then the AI knows what skills exist (procedure) + And loads detailed instructions only when needed (skill) + And executes external tools when required (use) + + Scenario: ResourceX Locator — unified resource address + Given a locator is how procedures reference their full skill content + Then a locator can be an identifier — name or registry/path/name + And a locator can be a source path — a local directory or URL + And examples of identifier form: deepractice/skill-creator, my-prompt:1.0.0 + And examples of source form: ./skills/my-skill, https://github.com/org/repo + And the tag defaults to latest when omitted — deepractice/skill-creator means deepractice/skill-creator:latest + And the system auto-detects which form is used and resolves accordingly + + Scenario: Writing a procedure — the skill reference + Given a procedure is layer 1 metadata pointing to full skill content + Then the Feature title names the capability + And the description includes the locator for full skill loading + And Scenarios describe when and why to apply this skill + And the tone is referential — pointing to the full skill, not containing it diff --git a/packages/prototype/src/descriptions/world/state-origin.feature b/packages/prototype/src/descriptions/world/state-origin.feature new file mode 100644 index 0000000..2471e6f --- /dev/null +++ b/packages/prototype/src/descriptions/world/state-origin.feature @@ -0,0 +1,31 @@ +Feature: State origin — prototype vs instance + Every node in a role's state tree has an origin: prototype or instance. + This distinction determines what can be modified and what is read-only. + + Scenario: Prototype nodes are read-only + Given a node has origin {prototype} + Then it comes from a position, duty, or organizational definition + And it is inherited through the membership/appointment chain + And it CANNOT be modified or forgotten — it belongs to the organization + + Scenario: Instance nodes are mutable + Given a node has origin {instance} + Then it was created by the individual through execution or cognition + And it includes goals, plans, tasks, encounters, experiences, principles, and procedures + And it CAN be modified or forgotten — it belongs to the individual + + Scenario: Reading the state heading + Given a state node is rendered as a heading + 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 + Given the AI wants to forget a node + When the node origin is {instance} + Then forget will succeed — the individual owns this knowledge + When the node origin is {prototype} + Then forget will fail — the knowledge belongs to the organization 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..400d593 --- /dev/null +++ b/packages/prototype/src/descriptions/world/use-protocol.feature @@ -0,0 +1,29 @@ +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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly + 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 + And available namespaces include individual, org, position, prototype, census, and resource + And examples: !prototype.found, !resource.add, !org.hire, !census.list + + 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 + + Scenario: use covers everything — no need for CLI or Bash + Given use can execute any RoleX namespace operation + And use can load any ResourceX resource + When you need to perform a RoleX operation + Then always use the MCP use tool + And never fall back to CLI commands for operations that use can handle diff --git a/packages/prototype/src/dispatch.ts b/packages/prototype/src/dispatch.ts new file mode 100644 index 0000000..9fcd0f9 --- /dev/null +++ b/packages/prototype/src/dispatch.ts @@ -0,0 +1,37 @@ +/** + * Dispatch — schema-driven argument mapping. + * + * Replaces the hand-written toArgs switch in rolex.ts with a single + * lookup against the instruction registry. + */ + +import type { ArgEntry } from "./schema.js"; +import { instructions } from "./instructions.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}".`); + 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 index f6bcc74..5092b0f 100644 --- a/packages/prototype/src/index.ts +++ b/packages/prototype/src/index.ts @@ -1,15 +1,25 @@ /** - * @rolexjs/prototype — Prototype system for RoleX. + * @rolexjs/prototype — RoleX operation layer. * - * A prototype is a command-driven instruction list (like a Dockerfile). - * Each instruction maps to a rolex.use() call. - * - * Three exports: - * PrototypeInstruction — the instruction format - * Prototype — registry interface (settle/list) - * prototypeType — ResourceX type handler + * 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) */ -export type { PrototypeInstruction } from "./instruction.js"; -export type { Prototype, PrototypeResolver } from "./prototype.js"; -export { prototypeType } from "./resourcex.js"; +// Schema types +export type { ArgEntry, InstructionDef, ParamDef, ParamType } from "./schema.js"; + +// Instruction registry +export { instructions } from "./instructions.js"; + +// Dispatch +export { toArgs } from "./dispatch.js"; + +// Operations +export type { OpResult, Ops, OpsContext } from "./ops.js"; +export { createOps } from "./ops.js"; + +// Descriptions (auto-generated from .feature files) +export { processes, world } from "./descriptions/index.js"; diff --git a/packages/prototype/src/instruction.ts b/packages/prototype/src/instruction.ts deleted file mode 100644 index 0b4421f..0000000 --- a/packages/prototype/src/instruction.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * PrototypeInstruction — a single command in a prototype instruction list. - * - * Each instruction maps directly to a rolex.use(op, args) call. - * The `op` field is a RoleX command (e.g. "!individual.born", "!org.found"). - * The `args` field holds command parameters passed to rolex.use. - * - * In prototype.json, file references use @ prefix (e.g. "@nuwa.individual.feature"). - * The resolver replaces @ references with actual file content before returning. - */ -export interface PrototypeInstruction { - /** RoleX command, e.g. "!individual.born", "!org.found". */ - readonly op: string; - /** Command parameters — passed directly to rolex.use(op, args). */ - readonly args?: Record; -} diff --git a/packages/prototype/src/instructions.ts b/packages/prototype/src/instructions.ts new file mode 100644 index 0000000..6d7eda6 --- /dev/null +++ b/packages/prototype/src/instructions.ts @@ -0,0 +1,384 @@ +/** + * 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" }, +}, ["org", "content"]); + +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"]); + +const prototypeBorn = def("prototype", "born", { + dir: { type: "string", required: true, description: "Output directory for the prototype" }, + content: { type: "gherkin", required: false, description: "Gherkin Feature source for the individual" }, + id: { type: "string", required: true, description: "Individual id (kebab-case)" }, + alias: { type: "string[]", required: false, description: "Alternative names" }, +}, ["dir", "content", "id", "alias"]); + +const prototypeTeach = def("prototype", "teach", { + dir: { type: "string", required: true, description: "Prototype directory" }, + content: { type: "gherkin", required: true, description: "Gherkin Feature source for the principle" }, + id: { type: "string", required: true, description: "Principle id (keywords joined by hyphens)" }, +}, ["dir", "content", "id"]); + +const prototypeTrain = def("prototype", "train", { + dir: { type: "string", required: true, description: "Prototype directory" }, + content: { type: "gherkin", required: true, description: "Gherkin Feature source for the procedure" }, + id: { type: "string", required: true, description: "Procedure id (keywords joined by hyphens)" }, +}, ["dir", "content", "id"]); + +const prototypeFound = def("prototype", "found", { + dir: { type: "string", required: true, description: "Output directory for the organization prototype" }, + content: { type: "gherkin", required: false, description: "Gherkin Feature source for the organization" }, + id: { type: "string", required: true, description: "Organization id (kebab-case)" }, + alias: { type: "string[]", required: false, description: "Alternative names" }, +}, ["dir", "content", "id", "alias"]); + +const prototypeCharter = def("prototype", "charter", { + dir: { type: "string", required: true, description: "Prototype directory" }, + content: { type: "gherkin", required: true, description: "Gherkin Feature source for the charter" }, + id: { type: "string", required: false, description: "Charter id" }, +}, ["dir", "content", "id"]); + +const prototypeMember = def("prototype", "member", { + dir: { type: "string", required: true, description: "Prototype directory" }, + id: { type: "string", required: true, description: "Member individual id" }, + locator: { type: "string", required: true, description: "ResourceX locator for the member prototype" }, +}, ["dir", "id", "locator"]); + +const prototypeEstablish = def("prototype", "establish", { + dir: { type: "string", required: true, description: "Prototype directory" }, + content: { type: "gherkin", required: false, description: "Gherkin Feature source for the position" }, + id: { type: "string", required: true, description: "Position id (kebab-case)" }, + appointments: { type: "string[]", required: false, description: "Individual ids to auto-appoint" }, +}, ["dir", "content", "id", "appointments"]); + +const prototypeCharge = def("prototype", "charge", { + dir: { type: "string", required: true, description: "Prototype directory" }, + position: { type: "string", required: true, description: "Position id" }, + content: { type: "gherkin", required: true, description: "Gherkin Feature source for the duty" }, + id: { type: "string", required: true, description: "Duty id (keywords joined by hyphens)" }, +}, ["dir", "position", "content", "id"]); + +const prototypeRequire = def("prototype", "require", { + dir: { type: "string", required: true, description: "Prototype directory" }, + 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: true, description: "Requirement id (keywords joined by hyphens)" }, +}, ["dir", "position", "content", "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, + "prototype.born": prototypeBorn, + "prototype.teach": prototypeTeach, + "prototype.train": prototypeTrain, + "prototype.found": prototypeFound, + "prototype.charter": prototypeCharter, + "prototype.member": prototypeMember, + "prototype.establish": prototypeEstablish, + "prototype.charge": prototypeCharge, + "prototype.require": prototypeRequire, + + // 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..e5cd5f0 --- /dev/null +++ b/packages/prototype/src/ops.ts @@ -0,0 +1,423 @@ +/** + * 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 type { Runtime, State, Structure } from "@rolexjs/system"; +import { parse } from "@rolexjs/parser"; +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; + find(id: string): Structure | null; + resourcex?: ResourceX; +} + +// 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 ---- + + function ok(node: Structure, process: string): OpResult { + return { state: rt.project(node), process }; + } + + function archive(node: Structure, process: string): OpResult { + const archived = rt.create(past, C.past, node.information, node.id); + rt.remove(node); + 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}`); + } + } + + 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; + } + + function removeExisting(parent: Structure, id: string): void { + const state = rt.project(parent); + const existing = findInState(state, id); + if (existing) rt.remove(existing); + } + + function requireResourceX(): ResourceX { + if (!resourcex) throw new Error("ResourceX is not available."); + return resourcex; + } + + // ================================================================ + // Operations + // ================================================================ + + return { + // ---- Individual: lifecycle ---- + + "individual.born"(content?: string, id?: string, alias?: readonly string[]): OpResult { + validateGherkin(content); + const node = rt.create(society, C.individual, content, id, alias); + rt.create(node, C.identity); + return ok(node, "born"); + }, + + "individual.retire"(individual: string): OpResult { + return archive(resolve(individual), "retire"); + }, + + "individual.die"(individual: string): OpResult { + return archive(resolve(individual), "die"); + }, + + "individual.rehire"(pastNode: string): OpResult { + const node = resolve(pastNode); + const ind = rt.create(society, C.individual, node.information, node.id); + rt.create(ind, C.identity); + rt.remove(node); + return ok(ind, "rehire"); + }, + + // ---- Individual: external injection ---- + + "individual.teach"(individual: string, principle: string, id?: string): OpResult { + validateGherkin(principle); + const parent = resolve(individual); + if (id) removeExisting(parent, id); + const node = rt.create(parent, C.principle, principle, id); + return ok(node, "teach"); + }, + + "individual.train"(individual: string, procedure: string, id?: string): OpResult { + validateGherkin(procedure); + const parent = resolve(individual); + if (id) removeExisting(parent, id); + const node = rt.create(parent, C.procedure, procedure, id); + return ok(node, "train"); + }, + + // ---- Role: focus ---- + + "role.focus"(goal: string): OpResult { + return ok(resolve(goal), "focus"); + }, + + // ---- Role: execution ---- + + "role.want"(individual: string, goal?: string, id?: string, alias?: readonly string[]): OpResult { + validateGherkin(goal); + const node = rt.create(resolve(individual), C.goal, goal, id, alias); + return ok(node, "want"); + }, + + "role.plan"(goal: string, plan?: string, id?: string, after?: string, fallback?: string): OpResult { + validateGherkin(plan); + const node = rt.create(resolve(goal), C.plan, plan, id); + if (after) rt.link(node, resolve(after), "after", "before"); + if (fallback) rt.link(node, resolve(fallback), "fallback-for", "fallback"); + return ok(node, "plan"); + }, + + "role.todo"(plan: string, task?: string, id?: string, alias?: readonly string[]): OpResult { + validateGherkin(task); + const node = rt.create(resolve(plan), C.task, task, id, alias); + return ok(node, "todo"); + }, + + "role.finish"(task: string, individual: string, encounter?: string): OpResult { + validateGherkin(encounter); + const taskNode = resolve(task); + rt.tag(taskNode, "done"); + if (encounter) { + const encId = taskNode.id ? `${taskNode.id}-finished` : undefined; + const enc = rt.create(resolve(individual), C.encounter, encounter, encId); + return ok(enc, "finish"); + } + return ok(taskNode, "finish"); + }, + + "role.complete"(plan: string, individual: string, encounter?: string): OpResult { + validateGherkin(encounter); + const planNode = resolve(plan); + rt.tag(planNode, "done"); + const encId = planNode.id ? `${planNode.id}-completed` : undefined; + const enc = rt.create(resolve(individual), C.encounter, encounter, encId); + return ok(enc, "complete"); + }, + + "role.abandon"(plan: string, individual: string, encounter?: string): OpResult { + validateGherkin(encounter); + const planNode = resolve(plan); + rt.tag(planNode, "abandoned"); + const encId = planNode.id ? `${planNode.id}-abandoned` : undefined; + const enc = rt.create(resolve(individual), C.encounter, encounter, encId); + return ok(enc, "abandon"); + }, + + // ---- Role: cognition ---- + + "role.reflect"(encounter: string, individual: string, experience?: string, id?: string): OpResult { + validateGherkin(experience); + const encNode = resolve(encounter); + const exp = rt.create(resolve(individual), C.experience, experience || encNode.information, id); + rt.remove(encNode); + return ok(exp, "reflect"); + }, + + "role.realize"(experience: string, individual: string, principle?: string, id?: string): OpResult { + validateGherkin(principle); + const expNode = resolve(experience); + const prin = rt.create(resolve(individual), C.principle, principle || expNode.information, id); + rt.remove(expNode); + return ok(prin, "realize"); + }, + + "role.master"(individual: string, procedure: string, id?: string, experience?: string): OpResult { + validateGherkin(procedure); + const parent = resolve(individual); + if (id) removeExisting(parent, id); + const proc = rt.create(parent, C.procedure, procedure, id); + if (experience) rt.remove(resolve(experience)); + return ok(proc, "master"); + }, + + // ---- Role: knowledge management ---- + + "role.forget"(nodeId: string): OpResult { + const node = resolve(nodeId); + 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 ---- + + "org.found"(content?: string, id?: string, alias?: readonly string[]): OpResult { + validateGherkin(content); + const node = rt.create(society, C.organization, content, id, alias); + return ok(node, "found"); + }, + + "org.charter"(org: string, charter: string): OpResult { + validateGherkin(charter); + const node = rt.create(resolve(org), C.charter, charter); + return ok(node, "charter"); + }, + + "org.dissolve"(org: string): OpResult { + return archive(resolve(org), "dissolve"); + }, + + "org.hire"(org: string, individual: string): OpResult { + const orgNode = resolve(org); + rt.link(orgNode, resolve(individual), "membership", "belong"); + return ok(orgNode, "hire"); + }, + + "org.fire"(org: string, individual: string): OpResult { + const orgNode = resolve(org); + rt.unlink(orgNode, resolve(individual), "membership", "belong"); + return ok(orgNode, "fire"); + }, + + // ---- Position ---- + + "position.establish"(content?: string, id?: string, alias?: readonly string[]): OpResult { + validateGherkin(content); + const node = rt.create(society, C.position, content, id, alias); + return ok(node, "establish"); + }, + + "position.charge"(position: string, duty: string, id?: string): OpResult { + validateGherkin(duty); + const node = rt.create(resolve(position), C.duty, duty, id); + return ok(node, "charge"); + }, + + "position.require"(position: string, procedure: string, id?: string): OpResult { + validateGherkin(procedure); + const parent = resolve(position); + if (id) removeExisting(parent, id); + const node = rt.create(parent, C.requirement, procedure, id); + return ok(node, "require"); + }, + + "position.abolish"(position: string): OpResult { + return archive(resolve(position), "abolish"); + }, + + "position.appoint"(position: string, individual: string): OpResult { + const posNode = resolve(position); + const indNode = resolve(individual); + rt.link(posNode, indNode, "appointment", "serve"); + const posState = rt.project(posNode); + const required = (posState.children ?? []).filter((c) => c.name === "requirement"); + for (const proc of required) { + if (proc.id) { + const indState = rt.project(indNode); + const existing = findInState(indState, proc.id); + if (existing) rt.remove(existing); + } + rt.create(indNode, C.procedure, proc.information, proc.id); + } + return ok(posNode, "appoint"); + }, + + "position.dismiss"(position: string, individual: string): OpResult { + const posNode = resolve(position); + rt.unlink(posNode, resolve(individual), "appointment", "serve"); + return ok(posNode, "dismiss"); + }, + + // ---- Census ---- + + "census.list"(type?: string): string { + const target = type === "past" ? past : society; + const state = 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."; + } + const groups = new Map(); + for (const c of filtered) { + const key = c.name; + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(c); + } + const lines: string[] = []; + for (const [name, items] of groups) { + lines.push(`[${name}] (${items.length})`); + for (const item of items) { + const tag = item.tag ? ` #${item.tag}` : ""; + lines.push(` ${item.id ?? "(no id)"}${tag}`); + } + } + return lines.join("\n"); + }, + + // ---- 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/prototype.ts b/packages/prototype/src/prototype.ts deleted file mode 100644 index a1bf158..0000000 --- a/packages/prototype/src/prototype.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Prototype — external driving force that builds runtime entities. - * - * A prototype is a command-driven instruction list. Settling a prototype - * pulls it from a source, executes its instructions, and registers it. - */ -import type { PrototypeInstruction } from "./instruction.js"; - -/** Resolves a prototype source into its instruction list. */ -export interface PrototypeResolver { - resolve(source: string): Promise; -} - -/** Registry that tracks settled prototypes. */ -export interface Prototype { - /** Settle: register a prototype — bind id to a source. */ - settle(id: string, source: string): void; - - /** List all registered prototypes: id → source mapping. */ - list(): Record; -} diff --git a/packages/prototype/src/resourcex.ts b/packages/prototype/src/resourcex.ts deleted file mode 100644 index df3c374..0000000 --- a/packages/prototype/src/resourcex.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * ResourceX type handler for prototype resources. - * - * A prototype resource contains: - * - prototype.json (instruction list: { op, args }) - * - *.feature (Gherkin content) - * - * The resolver reads prototype.json, and for each args value prefixed with @, - * replaces it with the actual file content. Returns self-contained instructions. - */ -import type { BundledType } from "resourcexjs"; - -export const prototypeType: BundledType = { - name: "prototype", - aliases: ["role", "individual", "organization", "org"], - description: "RoleX prototype — instruction list + feature files", - code: `// @resolver: prototype_type_default -var prototype_type_default = { - name: "prototype", - async resolve(ctx) { - var decoder = new TextDecoder(); - - // Read and parse prototype.json - var protoBuf = ctx.files["prototype.json"]; - if (!protoBuf) { - throw new Error("prototype resource must contain a prototype.json file"); - } - var instructions = JSON.parse(decoder.decode(protoBuf)); - - // 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]); - } - } - - // Resolve @ references in args: "@filename" → file content - for (var i = 0; i < instructions.length; i++) { - var instr = instructions[i]; - if (instr.args) { - var newArgs = {}; - var keys = Object.keys(instr.args); - for (var j = 0; j < keys.length; j++) { - var key = keys[j]; - var val = instr.args[key]; - if (typeof val === "string" && val.charAt(0) === "@") { - var filename = val.slice(1); - newArgs[key] = features[filename] || val; - } else { - newArgs[key] = val; - } - } - instructions[i] = { op: instr.op, args: newArgs }; - } - } - - return instructions; - } -};`, -}; 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..2498917 --- /dev/null +++ b/packages/prototype/tests/alignment.test.ts @@ -0,0 +1,232 @@ +/** + * 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: "./prototypes/rolex" }; + expect(toArgs("prototype.settle", a)).toEqual([a.source]); + }); + + test("prototype.evict → [id]", () => { + const a = { id: "nuwa" }; + expect(toArgs("prototype.evict", a)).toEqual([a.id]); + }); + + test("prototype.born → [dir, content, id, alias]", () => { + const a = { dir: "/tmp/proto", content: "Feature: X", id: "nuwa", alias: ["n"] }; + expect(toArgs("prototype.born", a)).toEqual([a.dir, a.content, a.id, a.alias]); + }); + + test("prototype.teach → [dir, content, id]", () => { + const a = { dir: "/tmp/proto", content: "Feature: P", id: "p1" }; + expect(toArgs("prototype.teach", a)).toEqual([a.dir, a.content, a.id]); + }); + + test("prototype.train → [dir, content, id]", () => { + const a = { dir: "/tmp/proto", content: "Feature: Proc", id: "proc1" }; + expect(toArgs("prototype.train", a)).toEqual([a.dir, a.content, a.id]); + }); + + test("prototype.found → [dir, content, id, alias]", () => { + const a = { dir: "/tmp/proto", content: "Feature: Org", id: "rolex", alias: ["rx"] }; + expect(toArgs("prototype.found", a)).toEqual([a.dir, a.content, a.id, a.alias]); + }); + + test("prototype.charter → [dir, content, id]", () => { + const a = { dir: "/tmp/proto", content: "Feature: Charter", id: "c1" }; + expect(toArgs("prototype.charter", a)).toEqual([a.dir, a.content, a.id]); + }); + + test("prototype.member → [dir, id, locator]", () => { + const a = { dir: "/tmp/proto", id: "sean", locator: "deepractice/sean" }; + expect(toArgs("prototype.member", a)).toEqual([a.dir, a.id, a.locator]); + }); + + test("prototype.establish → [dir, content, id, appointments]", () => { + const a = { dir: "/tmp/proto", content: "Feature: Pos", id: "dev", appointments: ["sean"] }; + expect(toArgs("prototype.establish", a)).toEqual([a.dir, a.content, a.id, a.appointments]); + }); + + test("prototype.charge → [dir, position, content, id]", () => { + const a = { dir: "/tmp/proto", position: "dev", content: "Feature: Duty", id: "d1" }; + expect(toArgs("prototype.charge", a)).toEqual([a.dir, a.position, a.content, a.id]); + }); + + test("prototype.require → [dir, position, content, id]", () => { + const a = { dir: "/tmp/proto", position: "dev", content: "Feature: Req", id: "r1" }; + expect(toArgs("prototype.require", a)).toEqual([a.dir, a.position, a.content, 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..972bdc5 --- /dev/null +++ b/packages/prototype/tests/descriptions.test.ts @@ -0,0 +1,46 @@ +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..763369c --- /dev/null +++ b/packages/prototype/tests/dispatch.test.ts @@ -0,0 +1,103 @@ +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.charge — dir, position, content, id", () => { + expect(toArgs("prototype.charge", { dir: "/tmp", position: "dev", content: "Feature: D", id: "d1" })) + .toEqual(["/tmp", "dev", "Feature: D", "d1"]); + }); + + // ---- 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"]); + }); + + // ---- 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..90e78fc --- /dev/null +++ b/packages/prototype/tests/instructions.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test"; +import { instructions } from "../src/instructions.js"; +import type { InstructionDef } from "../src/schema.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 — 11 methods", () => { + const methods = methodsOf("prototype"); + expect(methods).toEqual([ + "settle", "evict", + "born", "teach", "train", + "found", "charter", "member", "establish", "charge", "require", + ]); + }); + + test("resource — 8 methods", () => { + const methods = methodsOf("resource"); + expect(methods).toEqual(["add", "search", "has", "info", "remove", "push", "pull", "clearCache"]); + }); + + test("total instruction count is 50", () => { + expect(Object.keys(instructions).length).toBe(50); + }); + + 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..818af69 --- /dev/null +++ b/packages/prototype/tests/ops.test.ts @@ -0,0 +1,658 @@ +import { describe, expect, test } from "bun:test"; +import * as C from "@rolexjs/core"; +import { createRuntime, type Runtime, type State, type Structure } from "@rolexjs/system"; +import { createOps, type Ops } from "../src/ops.js"; + +// ================================================================ +// Test setup — pure in-memory, no platform needed +// ================================================================ + +function setup() { + const rt = createRuntime(); + const society = rt.create(null, C.society); + const past = 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; + } + + function find(id: string): Structure | null { + const state = rt.project(society); + return findInState(state, id.toLowerCase()); + } + + function resolve(id: string): Structure { + const node = 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", () => { + const { ops } = setup(); + const r = 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", () => { + const { ops } = setup(); + const r = ops["individual.born"](undefined, "alice"); + expect(r.state.name).toBe("individual"); + expect(r.state.id).toBe("alice"); + }); + + test("born with alias", () => { + const { ops, find } = setup(); + ops["individual.born"]("Feature: Sean", "sean", ["姜山"]); + expect(find("姜山")).not.toBeNull(); + }); + + test("born rejects invalid Gherkin", () => { + const { ops } = setup(); + expect(() => ops["individual.born"]("not gherkin")).toThrow("Invalid Gherkin"); + }); + + test("retire archives individual to past", () => { + const { ops, find } = setup(); + ops["individual.born"]("Feature: Sean", "sean"); + const r = ops["individual.retire"]("sean"); + expect(r.state.name).toBe("past"); + expect(r.process).toBe("retire"); + const found = find("sean"); + expect(found).not.toBeNull(); + expect(found!.name).toBe("past"); + }); + + test("die archives individual", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "alice"); + const r = ops["individual.die"]("alice"); + expect(r.state.name).toBe("past"); + expect(r.process).toBe("die"); + }); + + test("rehire restores individual from past", () => { + const { ops } = setup(); + ops["individual.born"]("Feature: Sean", "sean"); + ops["individual.retire"]("sean"); + const r = 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", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + const r = 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", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["individual.teach"]("sean", "Feature: Version 1", "rule"); + ops["individual.teach"]("sean", "Feature: Version 2", "rule"); + const sean = 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", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + const r = 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", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + const r = 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", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", "Feature: Auth", "auth"); + const r = 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", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", "Feature: Auth", "auth"); + const r = 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", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", "Feature: Auth", "auth"); + ops["role.plan"]("auth", "Feature: Phase 1", "phase-1"); + ops["role.plan"]("auth", "Feature: Phase 2", "phase-2", "phase-1"); + + const p2 = 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", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", "Feature: Auth", "auth"); + ops["role.plan"]("auth", "Feature: Plan A", "plan-a"); + ops["role.plan"]("auth", "Feature: Plan B", "plan-b", undefined, "plan-a"); + + const pb = 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", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", undefined, "g"); + ops["role.plan"]("g", undefined, "p"); + const r = 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", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", undefined, "g"); + ops["role.plan"]("g", undefined, "p"); + ops["role.todo"]("p", undefined, "t1"); + + const r = 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(find("t1")!.tag).toBe("done"); + }); + + test("finish without encounter just tags task done", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", undefined, "g"); + ops["role.plan"]("g", undefined, "p"); + ops["role.todo"]("p", undefined, "t1"); + + const r = ops["role.finish"]("t1", "sean"); + expect(r.state.name).toBe("task"); + expect(r.process).toBe("finish"); + expect(find("t1")!.tag).toBe("done"); + }); + + test("complete tags plan done and creates encounter", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", "Feature: Auth", "auth"); + ops["role.plan"]("auth", "Feature: JWT", "jwt"); + + const r = 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(find("jwt")!.tag).toBe("done"); + }); + + test("abandon tags plan abandoned and creates encounter", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", "Feature: Auth", "auth"); + ops["role.plan"]("auth", "Feature: JWT", "jwt"); + + const r = 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(find("jwt")!.tag).toBe("abandoned"); + }); +}); + +// ================================================================ +// Role: cognition +// ================================================================ + +describe("role: cognition", () => { + /** Helper: born → want → plan → todo → finish with encounter. */ + function withEncounter(ops: Ops) { + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", undefined, "g"); + ops["role.plan"]("g", undefined, "p"); + ops["role.todo"]("p", undefined, "t1"); + ops["role.finish"]("t1", "sean", "Feature: Encounter\n Scenario: OK\n Given x\n Then y"); + } + + test("reflect: encounter → experience", () => { + const { ops, find } = setup(); + withEncounter(ops); + const r = 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(find("t1-finished")).toBeNull(); + }); + + test("reflect without explicit experience uses encounter content", () => { + const { ops } = setup(); + withEncounter(ops); + const r = 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", () => { + const { ops, find } = setup(); + withEncounter(ops); + ops["role.reflect"]("t1-finished", "sean", "Feature: Insight", "exp-1"); + + const r = 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(find("exp-1")).toBeNull(); + }); + + test("master from experience: experience → procedure", () => { + const { ops, find } = setup(); + withEncounter(ops); + ops["role.reflect"]("t1-finished", "sean", "Feature: Insight", "exp-1"); + + const r = 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(find("exp-1")).toBeNull(); + }); + + test("master without experience: direct procedure creation", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + const r = 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", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.master"]("sean", "Feature: V1", "skill"); + ops["role.master"]("sean", "Feature: V2", "skill"); + const sean = 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", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", "Feature: Auth", "auth"); + const r = ops["role.forget"]("auth"); + expect(r.process).toBe("forget"); + expect(find("auth")).toBeNull(); + }); + + test("forget removes node and its subtree", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", undefined, "g"); + ops["role.plan"]("g", undefined, "p"); + ops["role.todo"]("p", undefined, "t1"); + ops["role.forget"]("g"); + expect(find("g")).toBeNull(); + expect(find("p")).toBeNull(); + expect(find("t1")).toBeNull(); + }); + + test("forget throws on non-existent node", () => { + const { ops } = setup(); + expect(() => ops["role.forget"]("nope")).toThrow(); + }); +}); + +// ================================================================ +// Organization +// ================================================================ + +describe("org", () => { + test("found creates organization", () => { + const { ops } = setup(); + const r = 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", () => { + const { ops } = setup(); + ops["org.found"](undefined, "dp"); + const r = 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", () => { + const { ops, find } = setup(); + ops["org.found"](undefined, "dp"); + const r = ops["org.dissolve"]("dp"); + expect(r.process).toBe("dissolve"); + expect(find("dp")!.name).toBe("past"); + }); + + test("hire links individual to org", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["org.found"](undefined, "dp"); + const r = ops["org.hire"]("dp", "sean"); + expect(r.state.links).toHaveLength(1); + expect(r.state.links![0].relation).toBe("membership"); + }); + + test("fire removes membership", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["org.found"](undefined, "dp"); + ops["org.hire"]("dp", "sean"); + const r = ops["org.fire"]("dp", "sean"); + expect(r.state.links).toBeUndefined(); + }); +}); + +// ================================================================ +// Position +// ================================================================ + +describe("position", () => { + test("establish creates position", () => { + const { ops } = setup(); + const r = 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", () => { + const { ops } = setup(); + ops["position.establish"](undefined, "architect"); + const r = 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", () => { + const { ops } = setup(); + ops["position.establish"](undefined, "architect"); + const r = 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", () => { + const { ops, find } = setup(); + ops["position.establish"](undefined, "architect"); + const r = ops["position.abolish"]("architect"); + expect(r.process).toBe("abolish"); + expect(find("architect")!.name).toBe("past"); + }); + + test("appoint links individual to position", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["position.establish"](undefined, "architect"); + const r = ops["position.appoint"]("architect", "sean"); + expect(r.state.links).toHaveLength(1); + expect(r.state.links![0].relation).toBe("appointment"); + }); + + test("appoint auto-trains required skills", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["position.establish"](undefined, "architect"); + ops["position.require"]("architect", "Feature: System design", "sys-design"); + ops["position.require"]("architect", "Feature: Code review", "code-review"); + ops["position.appoint"]("architect", "sean"); + + const sean = find("sean")! as unknown as State; + const procs = (sean.children ?? []).filter((c: State) => c.name === "procedure"); + expect(procs).toHaveLength(2); + const ids = procs.map((p: State) => p.id).sort(); + expect(ids).toEqual(["code-review", "sys-design"]); + }); + + test("dismiss removes appointment", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["position.establish"](undefined, "architect"); + ops["position.appoint"]("architect", "sean"); + const r = ops["position.dismiss"]("architect", "sean"); + expect(r.state.links).toBeUndefined(); + }); +}); + +// ================================================================ +// Census +// ================================================================ + +describe("census", () => { + test("list shows individuals and orgs", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["org.found"](undefined, "dp"); + const result = ops["census.list"](); + expect(result).toContain("sean"); + expect(result).toContain("dp"); + }); + + test("list by type filters", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["org.found"](undefined, "dp"); + const result = ops["census.list"]("individual"); + expect(result).toContain("sean"); + expect(result).not.toContain("dp"); + }); + + test("list empty society", () => { + const { ops } = setup(); + const result = ops["census.list"](); + expect(result).toBe("Society is empty."); + }); + + test("list past entries", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["individual.retire"]("sean"); + const result = ops["census.list"]("past"); + expect(result).toContain("sean"); + }); +}); + +// ================================================================ +// Gherkin validation +// ================================================================ + +describe("gherkin validation", () => { + test("want rejects invalid Gherkin", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + expect(() => ops["role.want"]("sean", "not gherkin")).toThrow("Invalid Gherkin"); + }); + + test("plan rejects invalid Gherkin", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", undefined, "g"); + expect(() => ops["role.plan"]("g", "not gherkin")).toThrow("Invalid Gherkin"); + }); + + test("operations accept undefined content (optional)", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + expect(() => ops["role.want"]("sean", undefined, "g")).not.toThrow(); + expect(() => ops["role.plan"]("g", undefined, "p")).not.toThrow(); + expect(() => ops["role.todo"]("p", undefined, "t")).not.toThrow(); + }); +}); + +// ================================================================ +// Error handling +// ================================================================ + +describe("error handling", () => { + test("resolve throws on non-existent id", () => { + const { ops } = setup(); + expect(() => ops["role.focus"]("no-such-goal")).toThrow('"no-such-goal" not found'); + }); + + test("role.skill throws without resourcex", () => { + const { ops } = setup(); + expect(() => ops["role.skill"]("some-locator")).toThrow("ResourceX is not available"); + }); +}); + +// ================================================================ +// Full lifecycle: execution + cognition +// ================================================================ + +describe("full lifecycle", () => { + test("born → want → plan → todo → finish → complete → reflect → realize", () => { + const { ops, find } = setup(); + + // Setup world + ops["individual.born"]("Feature: Sean", "sean"); + ops["org.found"]("Feature: Deepractice", "dp"); + ops["position.establish"]("Feature: Architect", "architect"); + ops["org.charter"]("dp", "Feature: Build great AI"); + ops["position.charge"]("architect", "Feature: Design systems"); + ops["org.hire"]("dp", "sean"); + ops["position.appoint"]("architect", "sean"); + + // Execution cycle + ops["role.want"]("sean", "Feature: Build auth", "build-auth"); + ops["role.plan"]("build-auth", "Feature: JWT plan", "jwt-plan"); + ops["role.todo"]("jwt-plan", "Feature: Login endpoint", "login"); + ops["role.todo"]("jwt-plan", "Feature: Token refresh", "refresh"); + + ops["role.finish"]("login", "sean", "Feature: Login done\n Scenario: OK\n Given login\n Then done"); + ops["role.finish"]("refresh", "sean", "Feature: Refresh done\n Scenario: OK\n Given refresh\n Then done"); + ops["role.complete"]("jwt-plan", "sean", "Feature: Auth plan complete\n Scenario: OK\n Given plan\n Then complete"); + + // Verify tags + expect(find("login")!.tag).toBe("done"); + expect(find("refresh")!.tag).toBe("done"); + expect(find("jwt-plan")!.tag).toBe("done"); + + // Cognition cycle + ops["role.reflect"]( + "login-finished", "sean", + "Feature: Token insight\n Scenario: Learned\n Given token handling\n Then understand refresh", + "token-exp", + ); + expect(find("login-finished")).toBeNull(); + + ops["role.realize"]( + "token-exp", "sean", + "Feature: Always validate expiry\n Scenario: Rule\n Given token\n Then validate expiry", + "validate-expiry", + ); + expect(find("token-exp")).toBeNull(); + + // Verify final state + const sean = 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", () => { + const { ops, find } = setup(); + + ops["individual.born"](undefined, "sean"); + ops["role.want"]("sean", "Feature: Learn Rust", "learn-rust"); + ops["role.plan"]("learn-rust", "Feature: Book approach", "book-approach"); + + ops["role.abandon"]("book-approach", "sean", "Feature: Too theoretical\n Scenario: Failed\n Given reading\n Then too slow"); + + expect(find("book-approach")!.tag).toBe("abandoned"); + + 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", + ); + + 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(find("hands-on-exp")).toBeNull(); + const sean = 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/prototype/tsconfig.json b/packages/prototype/tsconfig.json index e4afb9a..1f60594 100644 --- a/packages/prototype/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/packages/prototype/tsup.config.ts b/packages/prototype/tsup.config.ts index faf3167..6aef7ae 100644 --- a/packages/prototype/tsup.config.ts +++ b/packages/prototype/tsup.config.ts @@ -6,4 +6,10 @@ export default defineConfig({ dts: true, clean: true, sourcemap: true, + esbuildOptions(options) { + options.loader = { + ...options.loader, + ".feature": "text", + }; + }, }); diff --git a/packages/resourcex-types/package.json b/packages/resourcex-types/package.json deleted file mode 100644 index 79d1607..0000000 --- a/packages/resourcex-types/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@rolexjs/resourcex-types", - "version": "0.11.0", - "description": "RoleX resource types for ResourceX — role and organization", - "keywords": [ - "rolex", - "resourcex", - "type", - "role", - "organization" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/Deepractice/RoleX.git", - "directory": "packages/resourcex-types" - }, - "license": "MIT", - "engines": { - "node": ">=22.0.0" - }, - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "bun": "./src/index.ts", - "default": "./dist/index.js" - } - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsup", - "lint": "biome lint .", - "typecheck": "tsc --noEmit", - "clean": "rm -rf dist" - }, - "dependencies": { - "resourcexjs": "^2.14.0" - }, - "devDependencies": {}, - "publishConfig": { - "access": "public" - } -} 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/tsconfig.json b/packages/resourcex-types/tsconfig.json deleted file mode 100644 index e4afb9a..0000000 --- a/packages/resourcex-types/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/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 f719fd8..3d206b6 100644 --- a/packages/rolexjs/package.json +++ b/packages/rolexjs/package.json @@ -33,14 +33,14 @@ "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.14.0" diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index f96afa8..769a5f8 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -1,17 +1,13 @@ /** * 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 = createRoleX(platform); + * await rolex.genesis(); + * const role = await rolex.activate("sean"); + * role.want("Feature: Ship v1", "ship-v1"); */ // Re-export core (structures + processes) @@ -24,6 +20,9 @@ export { parse, serialize } from "./feature.js"; export type { RenderStateOptions } from "./render.js"; // Render export { describe, detail, hint, renderState, world } from "./render.js"; -export type { CensusEntry, RolexResult } from "./rolex.js"; +// Role +export { Role } from "./role.js"; +export type { RolexResult } 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 ee88b4a..923e486 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -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 { processes, world } from "@rolexjs/prototype"; /** Full Gherkin feature content for a process — sourced from .feature files. */ export function detail(process: string): string { diff --git a/packages/rolexjs/src/role.ts b/packages/rolexjs/src/role.ts new file mode 100644 index 0000000..fb8be39 --- /dev/null +++ b/packages/rolexjs/src/role.ts @@ -0,0 +1,172 @@ +/** + * Role — stateful handle returned by Rolex.activate(). + * + * Holds roleId + RoleContext internally. + * All operations are from the role's perspective — no need to pass + * individual or ctx. + * + * Usage: + * const role = await rolex.activate("sean"); + * role.want("Feature: Ship v1", "ship-v1"); + * role.plan("Feature: Phase 1", "phase-1"); + * role.finish("write-tests", "Feature: Tests written"); + */ +import type { State } from "@rolexjs/system"; +import type { Ops } from "@rolexjs/prototype"; +import type { RoleContext } from "./context.js"; + +export interface RolexResult { + /** Projection of the primary affected node. */ + state: State; + /** Which process was executed (for render). */ + process: string; + /** Cognitive hint — populated when RoleContext is used. */ + hint?: string; + /** Role context — returned by activate. */ + ctx?: RoleContext; +} + +/** + * 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; + 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; + } + + private withHint(result: RolexResult, process: string): RolexResult { + result.hint = this.ctx.cognitiveHint(process) ?? undefined; + return result; + } + + private save(): void { + this.api.saveCtx(this.ctx); + } + + // ---- Execution ---- + + /** Focus: view or switch focused goal. */ + focus(goal?: string): RolexResult { + const goalId = goal ?? this.ctx.requireGoalId(); + this.ctx.focusedGoalId = goalId; + this.ctx.focusedPlanId = null; + const result = this.api.ops["role.focus"](goalId); + this.save(); + return this.withHint(result, "focus"); + } + + /** Want: declare a goal. */ + want(goal?: string, id?: string, alias?: readonly string[]): RolexResult { + const result = this.api.ops["role.want"](this.roleId, goal, id, alias); + if (id) this.ctx.focusedGoalId = id; + this.ctx.focusedPlanId = null; + this.save(); + return this.withHint(result, "want"); + } + + /** Plan: create a plan for the focused goal. */ + plan(plan?: string, id?: string, after?: string, fallback?: string): RolexResult { + const result = this.api.ops["role.plan"](this.ctx.requireGoalId(), plan, id, after, fallback); + if (id) this.ctx.focusedPlanId = id; + this.save(); + return this.withHint(result, "plan"); + } + + /** Todo: add a task to the focused plan. */ + todo(task?: string, id?: string, alias?: readonly string[]): RolexResult { + const result = this.api.ops["role.todo"](this.ctx.requirePlanId(), task, id, alias); + return this.withHint(result, "todo"); + } + + /** Finish: complete a task, optionally record an encounter. */ + finish(task: string, encounter?: string): RolexResult { + const result = this.api.ops["role.finish"](task, this.roleId, encounter); + if (encounter && result.state.id) { + this.ctx.addEncounter(result.state.id); + } + return this.withHint(result, "finish"); + } + + /** Complete: close a plan as done, record encounter. */ + complete(plan?: string, encounter?: string): RolexResult { + const planId = plan ?? this.ctx.requirePlanId(); + const result = 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; + this.save(); + return this.withHint(result, "complete"); + } + + /** Abandon: drop a plan, record encounter. */ + abandon(plan?: string, encounter?: string): RolexResult { + const planId = plan ?? this.ctx.requirePlanId(); + const result = 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; + this.save(); + return this.withHint(result, "abandon"); + } + + // ---- Cognition ---- + + /** Reflect: consume encounter, create experience. */ + reflect(encounter: string, experience?: string, id?: string): RolexResult { + this.ctx.requireEncounterIds([encounter]); + const result = this.api.ops["role.reflect"](encounter, this.roleId, experience, id); + this.ctx.consumeEncounters([encounter]); + if (id) this.ctx.addExperience(id); + return this.withHint(result, "reflect"); + } + + /** Realize: consume experience, create principle. */ + realize(experience: string, principle?: string, id?: string): RolexResult { + this.ctx.requireExperienceIds([experience]); + const result = this.api.ops["role.realize"](experience, this.roleId, principle, id); + this.ctx.consumeExperiences([experience]); + return this.withHint(result, "realize"); + } + + /** Master: create procedure, optionally consuming experience. */ + master(procedure: string, id?: string, experience?: string): RolexResult { + if (experience) this.ctx.requireExperienceIds([experience]); + const result = this.api.ops["role.master"](this.roleId, procedure, id, experience); + if (experience) this.ctx.consumeExperiences([experience]); + return this.withHint(result, "master"); + } + + // ---- Knowledge management ---- + + /** Forget: remove any node under the individual by id. */ + forget(nodeId: string): RolexResult { + const result = this.api.ops["role.forget"](nodeId); + if (this.ctx.focusedGoalId === nodeId) this.ctx.focusedGoalId = null; + if (this.ctx.focusedPlanId === nodeId) this.ctx.focusedPlanId = null; + this.save(); + return result; + } + + // ---- Skills + unified entry ---- + + /** Skill: load full skill content by locator. */ + skill(locator: string): Promise { + return 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 76296e6..2b8860f 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -1,88 +1,58 @@ /** - * 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 (activate → complete, reflect → master, skill) - * org — organization management (found, charter, dissolve, hire, fire) - * position — position management (establish, abolish, charge, appoint, dismiss) - * prototype — registry (settle, evict, list) + creation (born, teach, train, found, charter, establish, charge, require) - * resource — ResourceX instance (optional) - * - * Unified entry point: - * use(locator, args) — `!ns.method` dispatches to runtime, else delegates to ResourceX + * All operation implementations live in @rolexjs/prototype (createOps). + * Rolex just wires Platform → ops and manages Role lifecycle. */ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; import type { ContextData, Platform } from "@rolexjs/core"; import * as C from "@rolexjs/core"; -import { parse } from "@rolexjs/parser"; -import { - type Initializer, - mergeState, - type Prototype, - type Runtime, - type State, - type Structure, -} from "@rolexjs/system"; +import { createOps, toArgs, type Ops } from "@rolexjs/prototype"; +import { type Initializer, type Runtime, type State, type Structure } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; import { RoleContext } from "./context.js"; +import { Role, type RolexInternal } from "./role.js"; -export interface RolexResult { - /** Projection of the primary affected node. */ - state: State; - /** Which process was executed (for render). */ - process: string; - /** Cognitive hint — populated when RoleContext is used. */ - hint?: string; - /** Role context — returned by activate, pass to subsequent operations. */ - ctx?: RoleContext; -} +// Re-export from role.ts (canonical definition) +export type { RolexResult } from "./role.js"; -/** Resolve an id to a Structure node, throws if not found. */ -type Resolve = (id: string) => Structure; +/** Summary entry returned by census.list. */ +export interface CensusEntry { + id?: string; + name: string; + tag?: string; +} export class Rolex { private rt: Runtime; + private ops: Ops; private resourcex?: ResourceX; - - /** 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; - /** Position management — establish, charge, appoint. */ - readonly position: PositionNamespace; - /** Census — society-level queries. */ - readonly census: CensusNamespace; - /** Prototype — registry + creation. */ - readonly prototype: PrototypeNamespace; - /** Resource management (optional — powered by ResourceX). */ - readonly resource?: ResourceX; - + private protoRegistry?: NonNullable; private readonly initializer?: Initializer; + private readonly persistContext?: { + save: (roleId: string, data: ContextData) => void; + load: (roleId: string) => ContextData | null; + }; + + private readonly society: Structure; + private readonly past: Structure; constructor(platform: Platform) { this.rt = platform.runtime; this.resourcex = platform.resourcex; + this.protoRegistry = platform.prototype; this.initializer = platform.initializer; - // Ensure world roots exist (idempotent — reuse if already created by another process) + if (platform.saveContext && platform.loadContext) { + this.persistContext = { save: platform.saveContext, load: platform.loadContext }; + } + + // Ensure world roots exist const roots = this.rt.roots(); this.society = roots.find((r) => r.name === "society") ?? this.rt.create(null, C.society); @@ -90,1087 +60,114 @@ export class Rolex { 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); - const persistContext = - platform.saveContext && platform.loadContext - ? { save: platform.saveContext, load: platform.loadContext } - : undefined; - const tryFind = (id: string) => this.find(id); - const born = (id: string) => { - this.individual.born(undefined, id); - return this.find(id)!; - }; - this.role = new RoleNamespace( - this.rt, - resolve, - tryFind, - born, - platform.prototype, - platform.resourcex, - persistContext - ); - this.org = new OrgNamespace(this.rt, this.society, this.past, resolve); - this.position = new PositionNamespace(this.rt, this.society, this.past, resolve); - this.census = new CensusNamespace(this.rt, this.society, this.past); - this.prototype = new PrototypeNamespace(platform.prototype, platform.resourcex); - this.resource = platform.resourcex; + // Create ops from prototype — all operation implementations + this.ops = createOps({ + rt: this.rt, + society: this.society, + past: this.past, + resolve: (id: string) => { + const node = this.find(id); + if (!node) throw new Error(`"${id}" not found.`); + return node; + }, + find: (id: string) => this.find(id), + resourcex: platform.resourcex, + }); } - /** Bootstrap the world — settle built-in prototypes on first run. */ - async bootstrap(): Promise { + /** Genesis — create the world on first run. */ + async genesis(): Promise { await this.initializer?.bootstrap(); } - /** 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); - } - - /** - * Unified execution entry point. - * - * - `!namespace.method` — dispatch to RoleX runtime (e.g. `!org.found`, `!position.establish`) - * - anything else — delegate to ResourceX `ingest` - */ - async use(locator: string, args?: Record): Promise { - if (locator.startsWith("!")) { - return this.dispatch(locator.slice(1), args ?? {}); - } - if (!this.resourcex) throw new Error("ResourceX is not available."); - return this.resourcex.ingest(locator, args); - } - - /** Dispatch a `!namespace.method` command to the corresponding API. */ - private dispatch(command: string, args: Record): T { - const dot = command.indexOf("."); - if (dot < 0) throw new Error(`Invalid command "${command}". Expected "namespace.method".`); - const ns = command.slice(0, dot); - const method = command.slice(dot + 1); - - const namespace = this.resolveNamespace(ns); - const fn = (namespace as Record)[method]; - if (typeof fn !== "function") { - throw new Error(`Unknown command "!${command}".`); - } - - return fn.call(namespace, ...this.toArgs(ns, method, args)); - } - - private resolveNamespace(ns: string): object { - switch (ns) { - case "individual": - return this.individual; - case "role": - return this.role; - case "org": - return this.org; - case "position": - return this.position; - case "census": - return this.census; - case "prototype": - return this.prototype; - case "resource": - if (!this.resource) throw new Error("ResourceX is not available."); - return this.resource; - default: - throw new Error(`Unknown namespace "${ns}".`); - } - } - - /** - * Map named args to positional args for each namespace.method. - * Keeps dispatch table centralized — one place to maintain. - */ - private toArgs(ns: string, method: string, a: Record): unknown[] { - const key = `${ns}.${method}`; - - // prettier-ignore - switch (key) { - // individual - case "individual.born": - return [a.content, a.id, a.alias]; - case "individual.retire": - return [a.individual]; - case "individual.die": - return [a.individual]; - case "individual.rehire": - return [a.individual]; - case "individual.teach": - return [a.individual, a.content, a.id]; - case "individual.train": - return [a.individual, a.content, a.id]; - - // org - case "org.found": - return [a.content, a.id, a.alias]; - case "org.charter": - return [a.org, a.content]; - case "org.dissolve": - return [a.org]; - case "org.hire": - return [a.org, a.individual]; - case "org.fire": - return [a.org, a.individual]; - - // position - case "position.establish": - return [a.content, a.id, a.alias]; - case "position.charge": - return [a.position, a.content, a.id]; - case "position.require": - return [a.position, a.content, a.id]; - case "position.abolish": - return [a.position]; - case "position.appoint": - return [a.position, a.individual]; - case "position.dismiss": - return [a.position, a.individual]; - - // census - case "census.list": - return [a.type]; - - // prototype - case "prototype.settle": - return [a.source]; - case "prototype.evict": - return [a.id]; - case "prototype.born": - return [a.dir, a.content, a.id, a.alias]; - case "prototype.teach": - return [a.dir, a.content, a.id]; - case "prototype.train": - return [a.dir, a.content, a.id]; - case "prototype.found": - return [a.dir, a.content, a.id, a.alias]; - case "prototype.charter": - return [a.dir, a.content, a.id]; - case "prototype.member": - return [a.dir, a.id, a.locator]; - case "prototype.establish": - return [a.dir, a.content, a.id, a.appointments]; - case "prototype.charge": - return [a.dir, a.position, a.content, a.id]; - case "prototype.require": - return [a.dir, a.position, a.content, a.id]; - - // resource (ResourceX proxy) - case "resource.add": - return [a.path]; - case "resource.search": - return [a.query]; - case "resource.has": - return [a.locator]; - case "resource.info": - return [a.locator]; - case "resource.remove": - return [a.locator]; - case "resource.push": - return [a.locator, a.registry ? { registry: a.registry } : undefined]; - case "resource.pull": - return [a.locator, a.registry ? { registry: a.registry } : undefined]; - case "resource.clearCache": - return [a.registry]; - - default: - throw new Error(`No arg mapping for "!${key}".`); - } - } -} - -// ================================================================ -// 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); - // Scaffolding: every individual has identity - this.rt.create(node, C.identity); - 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, past.id); - // Scaffolding: restore identity - this.rt.create(individual, C.identity); - 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 tryFind: (id: string) => Structure | null, - private born: (id: string) => Structure, - private prototype?: Prototype, - private resourcex?: ResourceX, - private persistContext?: { - save: (roleId: string, data: ContextData) => void; - load: (roleId: string) => ContextData | null; - } - ) {} - - private saveCtx(ctx?: RoleContext): void { - if (!ctx || !this.persistContext) return; - this.persistContext.save(ctx.roleId, { - focusedGoalId: ctx.focusedGoalId, - focusedPlanId: ctx.focusedPlanId, - }); - } - - // ---- Activation ---- - /** - * Activate: merge prototype (if any) with instance state. + * 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, then proceed with normal activation. + * auto-born the individual first. */ - async activate(individual: string): Promise { - let node = this.tryFind(individual); + async activate(individual: string): Promise { + let node = this.find(individual); if (!node) { - // Not in runtime — check prototype registry - const hasProto = this.prototype ? Object.hasOwn(this.prototype.list(), individual) : false; + const hasProto = this.protoRegistry + ? Object.hasOwn(this.protoRegistry.list(), individual) + : false; if (hasProto) { - node = this.born(individual); + this.ops["individual.born"](undefined, individual); + node = this.find(individual)!; } else { throw new Error(`"${individual}" not found.`); } } - const instanceState = this.rt.project(node); - const protoState = instanceState.id - ? await this.prototype?.resolve(instanceState.id) - : undefined; - const state = protoState ? mergeState(protoState, instanceState) : instanceState; + const state = this.rt.project(node); const ctx = new RoleContext(individual); ctx.rehydrate(state); - // Restore persisted focus (overrides rehydrate defaults), validate nodes still exist - const persisted = this.persistContext?.load(individual); + // Restore persisted focus + const persisted = this.persistContext?.load(individual) ?? null; if (persisted) { ctx.focusedGoalId = - persisted.focusedGoalId && this.tryFind(persisted.focusedGoalId) + persisted.focusedGoalId && this.find(persisted.focusedGoalId) ? persisted.focusedGoalId : null; ctx.focusedPlanId = - persisted.focusedPlanId && this.tryFind(persisted.focusedPlanId) + persisted.focusedPlanId && this.find(persisted.focusedPlanId) ? persisted.focusedPlanId : null; } - return { state, process: "activate", hint: ctx.cognitiveHint("activate") ?? undefined, ctx }; - } - - /** Focus: project a goal's state (view / switch context). */ - focus(goal: string, ctx?: RoleContext): RolexResult { - if (ctx) { - ctx.focusedGoalId = goal; - ctx.focusedPlanId = null; - } - const result = ok(this.rt, this.resolve(goal), "focus"); - if (ctx) result.hint = ctx.cognitiveHint("focus") ?? undefined; - this.saveCtx(ctx); - return result; - } - - // ---- Execution ---- - - /** Declare a goal under an individual. */ - want( - individual: string, - goal?: string, - id?: string, - alias?: readonly string[], - ctx?: RoleContext - ): RolexResult { - validateGherkin(goal); - const node = this.rt.create(this.resolve(individual), C.goal, goal, id, alias); - const result = ok(this.rt, node, "want"); - if (ctx) { - if (id) ctx.focusedGoalId = id; - ctx.focusedPlanId = null; - result.hint = ctx.cognitiveHint("want") ?? undefined; - this.saveCtx(ctx); - } - return result; - } - - /** Create a plan for a goal. Optionally link to another plan via after (sequential) or fallback (alternative). */ - plan( - goal: string, - plan?: string, - id?: string, - ctx?: RoleContext, - after?: string, - fallback?: string - ): RolexResult { - validateGherkin(plan); - const node = this.rt.create(this.resolve(goal), C.plan, plan, id); - if (after) this.rt.link(node, this.resolve(after), "after", "before"); - if (fallback) this.rt.link(node, this.resolve(fallback), "fallback-for", "fallback"); - const result = ok(this.rt, node, "plan"); - if (ctx) { - if (id) ctx.focusedPlanId = id; - result.hint = ctx.cognitiveHint("plan") ?? undefined; - this.saveCtx(ctx); - } - return result; - } - - /** Add a task to a plan. */ - todo( - plan: string, - task?: string, - id?: string, - alias?: readonly string[], - ctx?: RoleContext - ): RolexResult { - validateGherkin(task); - const node = this.rt.create(this.resolve(plan), C.task, task, id, alias); - const result = ok(this.rt, node, "todo"); - if (ctx) result.hint = ctx.cognitiveHint("todo") ?? undefined; - return result; - } - - /** Finish a task: tag task as done, optionally create encounter under individual. */ - finish(task: string, individual: string, encounter?: string, ctx?: RoleContext): RolexResult { - validateGherkin(encounter); - const taskNode = this.resolve(task); - this.rt.tag(taskNode, "done"); - let enc: Structure | undefined; - if (encounter) { - const encId = taskNode.id ? `${taskNode.id}-finished` : undefined; - enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId); - } - const result: RolexResult = enc ? ok(this.rt, enc, "finish") : ok(this.rt, taskNode, "finish"); - if (ctx) { - if (enc) { - const encId = result.state.id ?? task; - ctx.addEncounter(encId); - } - result.hint = ctx.cognitiveHint("finish") ?? undefined; - } - return result; - } - - /** Complete a plan: tag plan as done, create encounter under individual. */ - complete(plan: string, individual: string, encounter?: string, ctx?: RoleContext): RolexResult { - validateGherkin(encounter); - const planNode = this.resolve(plan); - this.rt.tag(planNode, "done"); - const encId = planNode.id ? `${planNode.id}-completed` : undefined; - const enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId); - const result = ok(this.rt, enc, "complete"); - if (ctx) { - ctx.addEncounter(result.state.id ?? plan); - if (ctx.focusedPlanId === plan) ctx.focusedPlanId = null; - result.hint = ctx.cognitiveHint("complete") ?? undefined; - this.saveCtx(ctx); - } - return result; - } - - /** Abandon a plan: tag plan as abandoned, create encounter under individual. */ - abandon(plan: string, individual: string, encounter?: string, ctx?: RoleContext): RolexResult { - validateGherkin(encounter); - const planNode = this.resolve(plan); - this.rt.tag(planNode, "abandoned"); - const encId = planNode.id ? `${planNode.id}-abandoned` : undefined; - const enc = this.rt.create(this.resolve(individual), C.encounter, encounter, encId); - const result = ok(this.rt, enc, "abandon"); - if (ctx) { - ctx.addEncounter(result.state.id ?? plan); - if (ctx.focusedPlanId === plan) ctx.focusedPlanId = null; - result.hint = ctx.cognitiveHint("abandon") ?? undefined; - this.saveCtx(ctx); - } - return result; - } - - // ---- Cognition ---- - - /** Reflect: consume encounter, create experience under individual. */ - reflect( - encounter: string, - individual: string, - experience?: string, - id?: string, - ctx?: RoleContext - ): RolexResult { - validateGherkin(experience); - if (ctx) ctx.requireEncounterIds([encounter]); - const encNode = this.resolve(encounter); - const exp = this.rt.create( - this.resolve(individual), - C.experience, - experience || encNode.information, - id - ); - this.rt.remove(encNode); - const result = ok(this.rt, exp, "reflect"); - if (ctx) { - ctx.consumeEncounters([encounter]); - if (id) ctx.addExperience(id); - result.hint = ctx.cognitiveHint("reflect") ?? undefined; - } - return result; - } - - /** Realize: consume experience, create principle under individual. */ - realize( - experience: string, - individual: string, - principle?: string, - id?: string, - ctx?: RoleContext - ): RolexResult { - validateGherkin(principle); - if (ctx) ctx.requireExperienceIds([experience]); - const expNode = this.resolve(experience); - const prin = this.rt.create( - this.resolve(individual), - C.principle, - principle || expNode.information, - id - ); - this.rt.remove(expNode); - const result = ok(this.rt, prin, "realize"); - if (ctx) { - ctx.consumeExperiences([experience]); - result.hint = ctx.cognitiveHint("realize") ?? undefined; - } - return result; - } - - /** Master: create procedure under individual, optionally consuming experience. */ - master( - individual: string, - procedure: string, - id?: string, - experience?: string, - ctx?: RoleContext - ): RolexResult { - validateGherkin(procedure); - if (ctx && experience) ctx.requireExperienceIds([experience]); - const parent = this.resolve(individual); - if (id) { - const existing = findInState(this.rt.project(parent), id); - if (existing) this.rt.remove(existing); - } - const proc = this.rt.create(parent, C.procedure, procedure, id); - if (experience) { - this.rt.remove(this.resolve(experience)); - if (ctx) ctx.consumeExperiences([experience]); - } - const result = ok(this.rt, proc, "master"); - if (ctx) result.hint = ctx.cognitiveHint("master") ?? undefined; - return result; - } - - // ---- Knowledge management ---- - - /** Forget: remove any node under an individual by id. Prototype nodes are read-only. */ - async forget(nodeId: string, individual: string, ctx?: RoleContext): Promise { - try { - const node = this.resolve(nodeId); - this.rt.remove(node); - if (ctx) { - if (ctx.focusedGoalId === nodeId) ctx.focusedGoalId = null; - if (ctx.focusedPlanId === nodeId) ctx.focusedPlanId = null; - this.saveCtx(ctx); - } - 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.`); - } - } - - // ---- 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; - } - } -} - -// ================================================================ -// 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"); - } - - /** 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"); - } - - // ---- Archival ---- - - /** Dissolve an organization. */ - dissolve(org: string): RolexResult { - return archive(this.rt, this.past, this.resolve(org), "dissolve"); - } - - // ---- Membership ---- - - /** 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"); - } -} - -// ================================================================ -// Position — position management -// ================================================================ - -class PositionNamespace { - constructor( - private rt: Runtime, - private society: Structure, - private past: Structure, - private resolve: Resolve - ) {} - - // ---- Structure ---- - - /** Establish a position. */ - establish(position?: string, id?: string, alias?: readonly string[]): RolexResult { - validateGherkin(position); - const pos = this.rt.create(this.society, C.position, position, id, alias); - return ok(this.rt, pos, "establish"); - } - - /** 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"); - } - - // ---- Skill requirements ---- - - /** Require: declare that this position requires a skill. Upserts by id. */ - require(position: string, procedure: string, id?: string): RolexResult { - validateGherkin(procedure); - const parent = this.resolve(position); - if (id) { - const state = this.rt.project(parent); - const existing = findInState(state, id); - if (existing) this.rt.remove(existing); - } - const proc = this.rt.create(parent, C.requirement, procedure, id); - return ok(this.rt, proc, "require"); - } - - // ---- Archival ---- - - /** Abolish a position. */ - abolish(position: string): RolexResult { - return archive(this.rt, this.past, this.resolve(position), "abolish"); - } - - // ---- Appointment ---- - - /** Appoint: link individual to position via appointment. Auto-trains required skills. */ - appoint(position: string, individual: string): RolexResult { - const posNode = this.resolve(position); - const indNode = this.resolve(individual); - this.rt.link(posNode, indNode, "appointment", "serve"); - - // Auto-train: inject required procedures into the individual - const posState = this.rt.project(posNode); - const required = (posState.children ?? []).filter((c) => c.name === "requirement"); - for (const proc of required) { - if (proc.id) { - // Upsert: remove existing procedure with same id - const indState = this.rt.project(indNode); - const existing = findInState(indState, proc.id); - if (existing) this.rt.remove(existing); - } - this.rt.create(indNode, C.procedure, proc.information, proc.id); - } - - 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"); - } -} - -// ================================================================ -// Census — society-level queries -// ================================================================ - -/** Summary entry returned by census.list. */ -export interface CensusEntry { - id?: string; - name: string; - tag?: string; -} - -class CensusNamespace { - constructor( - private rt: Runtime, - private society: Structure, - private past: Structure - ) {} - - /** List top-level entities under society, optionally filtered by type. */ - list(type?: string): string { - const target = type === "past" ? this.past : this.society; - const state = this.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."; - } - - // Group by type - const groups = new Map(); - for (const c of filtered) { - const key = c.name; - if (!groups.has(key)) groups.set(key, []); - groups.get(key)!.push(c); - } - - const lines: string[] = []; - for (const [name, items] of groups) { - lines.push(`[${name}] (${items.length})`); - for (const item of items) { - const tag = item.tag ? ` #${item.tag}` : ""; - lines.push(` ${item.id ?? "(no id)"}${tag}`); - } - } - return lines.join("\n"); - } -} - -// ================================================================ -// Prototype — settle, evict, list -// ================================================================ - -interface PrototypeManifest { - id: string; - type: string; - alias?: readonly string[]; - members?: Record; - children?: Record; -} - -interface PrototypeManifestChild { - type: string; - appointments?: string[]; - children?: Record; -} - -class PrototypeNamespace { - constructor( - private prototype?: Prototype, - private resourcex?: ResourceX - ) {} - - // ---- Registry ---- - - /** Settle: pull a prototype from source, register it in the world. */ - async settle(source: string): Promise { - if (!this.resourcex) throw new Error("ResourceX is not available."); - if (!this.prototype) throw new Error("Platform does not support prototypes."); - const state = await this.resourcex.ingest(source); - if (!state.id) throw new Error("Prototype resource must have an id."); - this.prototype.settle(state.id, source); - return { state, process: "settle" }; - } - - /** Evict: unregister a prototype from the world. */ - evict(id: string): RolexResult { - if (!this.prototype) throw new Error("Platform does not support prototypes."); - this.prototype.evict(id); - return { state: { name: id, description: "", parent: null }, process: "evict" }; - } - - /** List all registered prototypes. */ - list(): Record { - return this.prototype?.list() ?? {}; - } - - // ---- Individual prototype creation ---- - - /** Born: create an individual prototype directory. */ - born(dir: string, content?: string, id?: string, alias?: readonly string[]): RolexResult { - validateGherkin(content); - if (!id) throw new Error("id is required."); - mkdirSync(dir, { recursive: true }); - - const manifest: PrototypeManifest = { - id, - type: "individual", - ...(alias && alias.length > 0 ? { alias } : {}), - children: { identity: { type: "identity" } }, + // Build internal API for Role — ops + ctx persistence + const ops = this.ops; + const saveCtx = (c: RoleContext) => { + this.persistContext?.save(c.roleId, { + focusedGoalId: c.focusedGoalId, + focusedPlanId: c.focusedPlanId, + }); }; - writeFileSync(join(dir, "individual.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8"); - if (content) { - writeFileSync(join(dir, `${id}.individual.feature`), `${content}\n`, "utf-8"); - } - - const state: State = { - id, - name: "individual", - description: "", - parent: null, - ...(alias ? { alias } : {}), - ...(content ? { information: content } : {}), + const api: RolexInternal = { + ops, + saveCtx, + direct: this.direct.bind(this), }; - return { state, process: "born" }; - } - /** Teach: add a principle to an existing prototype directory. */ - teach(dir: string, principle: string, id?: string): RolexResult { - validateGherkin(principle); - if (!id) throw new Error("id is required."); - const manifest = this.readManifest(dir); - if (!manifest.children) manifest.children = {}; - manifest.children[id] = { type: "principle" }; - this.writeManifest(dir, manifest); - - writeFileSync(join(dir, `${id}.principle.feature`), `${principle}\n`, "utf-8"); - - const state: State = { - id, - name: "principle", - description: "", - parent: null, - information: principle, - }; - return { state, process: "teach" }; + return new Role(individual, ctx, api); } - /** Train: add a procedure to an existing prototype directory. */ - train(dir: string, procedure: string, id?: string): RolexResult { - validateGherkin(procedure); - if (!id) throw new Error("id is required."); - const manifest = this.readManifest(dir); - if (!manifest.children) manifest.children = {}; - manifest.children[id] = { type: "procedure" }; - this.writeManifest(dir, manifest); - - writeFileSync(join(dir, `${id}.procedure.feature`), `${procedure}\n`, "utf-8"); - - const state: State = { - id, - name: "procedure", - description: "", - parent: null, - information: procedure, - }; - return { state, process: "train" }; - } - - // ---- Organization prototype creation ---- - - /** Found: create an organization prototype directory. */ - found(dir: string, content?: string, id?: string, alias?: readonly string[]): RolexResult { - validateGherkin(content); - if (!id) throw new Error("id is required."); - mkdirSync(dir, { recursive: true }); - - const manifest: PrototypeManifest = { - id, - type: "organization", - ...(alias && alias.length > 0 ? { alias } : {}), - children: {}, - }; - writeFileSync( - join(dir, "organization.json"), - `${JSON.stringify(manifest, null, 2)}\n`, - "utf-8" - ); - - if (content) { - writeFileSync(join(dir, `${id}.organization.feature`), `${content}\n`, "utf-8"); - } - - const state: State = { - id, - name: "organization", - description: "", - parent: null, - ...(alias ? { alias } : {}), - ...(content ? { information: content } : {}), - }; - return { state, process: "found" }; - } - - /** Charter: add a charter to an organization prototype. */ - charter(dir: string, content: string, id?: string): RolexResult { - validateGherkin(content); - const charterId = id ?? "charter"; - const manifest = this.readManifest(dir, "organization"); - if (!manifest.children) manifest.children = {}; - manifest.children[charterId] = { type: "charter" }; - this.writeManifest(dir, manifest); - - writeFileSync(join(dir, `${charterId}.charter.feature`), `${content}\n`, "utf-8"); - - const state: State = { - id: charterId, - name: "charter", - description: "", - parent: null, - information: content, - }; - return { state, process: "charter" }; - } - - /** Member: register a member in an organization prototype. */ - member(dir: string, id: string, locator: string): RolexResult { - if (!id) throw new Error("id is required."); - if (!locator) throw new Error("locator is required."); - const manifest = this.readManifest(dir, "organization"); - if (!manifest.members) manifest.members = {}; - manifest.members[id] = locator; - this.writeManifest(dir, manifest); - - const state: State = { - id, - name: "member", - description: locator, - parent: null, - }; - return { state, process: "member" }; - } - - /** Establish: add a position to an organization prototype. */ - establish(dir: string, content?: string, id?: string, appointments?: string[]): RolexResult { - validateGherkin(content); - if (!id) throw new Error("id is required."); - const manifest = this.readManifest(dir, "organization"); - if (!manifest.children) manifest.children = {}; - manifest.children[id] = { - type: "position", - ...(appointments && appointments.length > 0 ? { appointments } : {}), - children: {}, - }; - this.writeManifest(dir, manifest); - - if (content) { - writeFileSync(join(dir, `${id}.position.feature`), `${content}\n`, "utf-8"); - } - - const state: State = { - id, - name: "position", - description: "", - parent: null, - ...(content ? { information: content } : {}), - }; - return { state, process: "establish" }; - } - - /** Charge: add a duty to a position in an organization prototype. */ - charge(dir: string, position: string, content: string, id?: string): RolexResult { - validateGherkin(content); - if (!id) throw new Error("id is required."); - const manifest = this.readManifest(dir, "organization"); - const pos = manifest.children?.[position]; - if (!pos) throw new Error(`Position "${position}" not found in manifest.`); - if (!pos.children) pos.children = {}; - pos.children[id] = { type: "duty" }; - this.writeManifest(dir, manifest); - - writeFileSync(join(dir, `${id}.duty.feature`), `${content}\n`, "utf-8"); - - const state: State = { - id, - name: "duty", - description: "", - parent: null, - information: content, - }; - return { state, process: "charge" }; - } - - /** Require: add a required skill to a position in an organization prototype. */ - require(dir: string, position: string, content: string, id?: string): RolexResult { - validateGherkin(content); - if (!id) throw new Error("id is required."); - const manifest = this.readManifest(dir, "organization"); - const pos = manifest.children?.[position]; - if (!pos) throw new Error(`Position "${position}" not found in manifest.`); - if (!pos.children) pos.children = {}; - pos.children[id] = { type: "requirement" }; - this.writeManifest(dir, manifest); - - writeFileSync(join(dir, `${id}.requirement.feature`), `${content}\n`, "utf-8"); - - const state: State = { - id, - name: "requirement", - description: "", - parent: null, - information: content, - }; - return { state, process: "require" }; + /** Find a node by id or alias across the entire society tree. Internal use only. */ + private find(id: string): Structure | null { + const target = id.toLowerCase(); + const state = this.rt.project(this.society); + return findInState(state, target); } - // ---- Manifest I/O ---- - - private readManifest(dir: string, type?: string): PrototypeManifest { - if (type) { - const path = join(dir, `${type}.json`); - if (!existsSync(path)) throw new Error(`No ${type}.json found in "${dir}".`); - return JSON.parse(readFileSync(path, "utf-8")); + /** + * 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) throw new Error(`Unknown command "${locator}".`); + return fn(...toArgs(command, args ?? {})) as T; } - // Auto-detect: try individual.json first, then organization.json - const indPath = join(dir, "individual.json"); - if (existsSync(indPath)) return JSON.parse(readFileSync(indPath, "utf-8")); - const orgPath = join(dir, "organization.json"); - if (existsSync(orgPath)) return JSON.parse(readFileSync(orgPath, "utf-8")); - throw new Error(`No manifest found in "${dir}". Run prototype.born or prototype.found first.`); + if (!this.resourcex) throw new Error("ResourceX is not available."); + return this.resourcex.ingest(locator, args); } +} - private writeManifest(dir: string, manifest: PrototypeManifest): void { - const filename = `${manifest.type}.json`; - writeFileSync(join(dir, filename), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8"); - } +/** Create a Rolex instance from a Platform. */ +export function createRoleX(platform: Platform): Rolex { + return new Rolex(platform); } // ================================================================ -// Shared helpers +// 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) { @@ -1184,54 +181,3 @@ function findInState(state: State, target: string): Structure | null { } return null; } - -function archive(rt: Runtime, past: Structure, node: Structure, process: string): RolexResult { - const archived = rt.create(past, C.past, node.information, node.id); - rt.remove(node); - return ok(rt, archived, process); -} - -function ok(rt: Runtime, node: Structure, process: string): RolexResult { - return { - state: rt.project(node), - process, - }; -} - -/** 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 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); -} diff --git a/packages/rolexjs/tests/author.test.ts b/packages/rolexjs/tests/author.test.ts deleted file mode 100644 index 51d2f70..0000000 --- a/packages/rolexjs/tests/author.test.ts +++ /dev/null @@ -1,387 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { localPlatform } from "@rolexjs/local-platform"; -import { createRoleX } from "../src/rolex.js"; - -let tmpDir: string; - -function setup() { - tmpDir = mkdtempSync(join(tmpdir(), "rolex-author-")); - return createRoleX(localPlatform({ dataDir: null })); -} - -function cleanup() { - if (tmpDir && existsSync(tmpDir)) { - rmSync(tmpDir, { recursive: true }); - } -} - -function protoDir() { - return join(tmpDir, "my-role"); -} - -function readManifest(dir: string, type = "individual") { - return JSON.parse(readFileSync(join(dir, `${type}.json`), "utf-8")); -} - -function readFeature(dir: string, filename: string) { - return readFileSync(join(dir, filename), "utf-8"); -} - -describe("PrototypeNamespace authoring", () => { - beforeEach(() => {}); - afterEach(cleanup); - - describe("born", () => { - test("creates directory, manifest, and feature file", () => { - const rolex = setup(); - const dir = protoDir(); - const r = rolex.prototype.born(dir, "Feature: My Role\n A test role.", "my-role", [ - "MyRole", - ]); - - expect(r.process).toBe("born"); - expect(r.state.id).toBe("my-role"); - expect(r.state.name).toBe("individual"); - - // manifest - const manifest = readManifest(dir); - expect(manifest.id).toBe("my-role"); - expect(manifest.type).toBe("individual"); - expect(manifest.alias).toEqual(["MyRole"]); - expect(manifest.children.identity.type).toBe("identity"); - - // feature file - const content = readFeature(dir, "my-role.individual.feature"); - expect(content).toContain("Feature: My Role"); - }); - - test("creates manifest without alias when not provided", () => { - const rolex = setup(); - const dir = protoDir(); - rolex.prototype.born(dir, "Feature: Minimal", "minimal"); - - const manifest = readManifest(dir); - expect(manifest.alias).toBeUndefined(); - }); - - test("creates manifest without feature file when content is omitted", () => { - const rolex = setup(); - const dir = protoDir(); - rolex.prototype.born(dir, undefined, "empty-role"); - - expect(existsSync(join(dir, "individual.json"))).toBe(true); - expect(existsSync(join(dir, "empty-role.individual.feature"))).toBe(false); - }); - - test("throws when id is missing", () => { - const rolex = setup(); - expect(() => rolex.prototype.born(protoDir(), "Feature: X")).toThrow("id is required"); - }); - - test("validates Gherkin content", () => { - const rolex = setup(); - expect(() => rolex.prototype.born(protoDir(), "not valid gherkin", "test")).toThrow( - "Invalid Gherkin" - ); - }); - }); - - describe("teach", () => { - test("adds principle to manifest and writes feature file", () => { - const rolex = setup(); - const dir = protoDir(); - rolex.prototype.born(dir, undefined, "my-role"); - const r = rolex.prototype.teach( - dir, - "Feature: Always test first\n Tests before code.", - "tdd-first" - ); - - expect(r.process).toBe("teach"); - expect(r.state.id).toBe("tdd-first"); - expect(r.state.name).toBe("principle"); - - // manifest updated - const manifest = readManifest(dir); - expect(manifest.children["tdd-first"].type).toBe("principle"); - // identity still there - expect(manifest.children.identity.type).toBe("identity"); - - // feature file - const content = readFeature(dir, "tdd-first.principle.feature"); - expect(content).toContain("Feature: Always test first"); - }); - - test("throws when no manifest exists", () => { - const rolex = setup(); - expect(() => rolex.prototype.teach(protoDir(), "Feature: X", "x")).toThrow( - "No manifest found" - ); - }); - }); - - describe("train", () => { - test("adds procedure to manifest and writes feature file", () => { - const rolex = setup(); - const dir = protoDir(); - rolex.prototype.born(dir, undefined, "my-role"); - const r = rolex.prototype.train( - dir, - "Feature: Code Review\n https://example.com/skills/code-review", - "code-review" - ); - - expect(r.process).toBe("train"); - expect(r.state.id).toBe("code-review"); - expect(r.state.name).toBe("procedure"); - - // manifest updated - const manifest = readManifest(dir); - expect(manifest.children["code-review"].type).toBe("procedure"); - - // feature file - const content = readFeature(dir, "code-review.procedure.feature"); - expect(content).toContain("Code Review"); - }); - }); - - describe("full workflow — individual", () => { - test("born → teach → train produces valid prototype", () => { - const rolex = setup(); - const dir = protoDir(); - - rolex.prototype.born(dir, "Feature: Backend Dev\n A server-side engineer.", "backend-dev", [ - "Backend", - ]); - rolex.prototype.teach(dir, "Feature: DRY principle\n Don't repeat yourself.", "dry"); - rolex.prototype.train( - dir, - "Feature: Deployment\n https://example.com/skills/deploy", - "deploy" - ); - rolex.prototype.teach(dir, "Feature: KISS\n Keep it simple.", "kiss"); - - const manifest = readManifest(dir); - expect(manifest.id).toBe("backend-dev"); - expect(manifest.alias).toEqual(["Backend"]); - expect(Object.keys(manifest.children)).toEqual(["identity", "dry", "deploy", "kiss"]); - expect(manifest.children.dry.type).toBe("principle"); - expect(manifest.children.deploy.type).toBe("procedure"); - expect(manifest.children.kiss.type).toBe("principle"); - - // All feature files exist - expect(existsSync(join(dir, "backend-dev.individual.feature"))).toBe(true); - expect(existsSync(join(dir, "dry.principle.feature"))).toBe(true); - expect(existsSync(join(dir, "deploy.procedure.feature"))).toBe(true); - expect(existsSync(join(dir, "kiss.principle.feature"))).toBe(true); - }); - }); - - // ---- Organization authoring ---- - - describe("found", () => { - test("creates organization directory and manifest", () => { - const rolex = setup(); - const dir = join(tmpDir, "my-org"); - const r = rolex.prototype.found( - dir, - "Feature: Deepractice\n An AI agent framework company.", - "deepractice", - ["DP"] - ); - - expect(r.process).toBe("found"); - expect(r.state.id).toBe("deepractice"); - expect(r.state.name).toBe("organization"); - - const manifest = readManifest(dir, "organization"); - expect(manifest.id).toBe("deepractice"); - expect(manifest.type).toBe("organization"); - expect(manifest.alias).toEqual(["DP"]); - expect(manifest.children).toEqual({}); - - const content = readFeature(dir, "deepractice.organization.feature"); - expect(content).toContain("Feature: Deepractice"); - }); - - test("throws when id is missing", () => { - const rolex = setup(); - expect(() => rolex.prototype.found(join(tmpDir, "x"), "Feature: X")).toThrow( - "id is required" - ); - }); - }); - - describe("charter", () => { - test("adds charter to organization manifest", () => { - const rolex = setup(); - const dir = join(tmpDir, "my-org"); - rolex.prototype.found(dir, undefined, "my-org"); - const r = rolex.prototype.charter( - dir, - "Feature: Build great AI\n Scenario: Mission\n Given we believe in role-based AI", - "mission" - ); - - expect(r.process).toBe("charter"); - expect(r.state.id).toBe("mission"); - expect(r.state.name).toBe("charter"); - - const manifest = readManifest(dir, "organization"); - expect(manifest.children["mission"].type).toBe("charter"); - - const content = readFeature(dir, "mission.charter.feature"); - expect(content).toContain("Build great AI"); - }); - - test("defaults charter id to 'charter'", () => { - const rolex = setup(); - const dir = join(tmpDir, "my-org"); - rolex.prototype.found(dir, undefined, "my-org"); - const r = rolex.prototype.charter(dir, "Feature: Our charter\n We build things."); - - expect(r.state.id).toBe("charter"); - const manifest = readManifest(dir, "organization"); - expect(manifest.children["charter"].type).toBe("charter"); - }); - }); - - describe("establish", () => { - test("adds position to organization manifest", () => { - const rolex = setup(); - const dir = join(tmpDir, "my-org"); - rolex.prototype.found(dir, undefined, "my-org"); - const r = rolex.prototype.establish( - dir, - "Feature: Backend Architect\n Responsible for system design.", - "architect" - ); - - expect(r.process).toBe("establish"); - expect(r.state.id).toBe("architect"); - expect(r.state.name).toBe("position"); - - const manifest = readManifest(dir, "organization"); - expect(manifest.children["architect"].type).toBe("position"); - expect(manifest.children["architect"].children).toEqual({}); - - const content = readFeature(dir, "architect.position.feature"); - expect(content).toContain("Backend Architect"); - }); - }); - - describe("charge", () => { - test("adds duty under position in manifest", () => { - const rolex = setup(); - const dir = join(tmpDir, "my-org"); - rolex.prototype.found(dir, undefined, "my-org"); - rolex.prototype.establish(dir, "Feature: Architect", "architect"); - const r = rolex.prototype.charge( - dir, - "architect", - "Feature: Design systems\n Scenario: API design\n Given a new service is needed\n Then design the API first", - "design-systems" - ); - - expect(r.process).toBe("charge"); - expect(r.state.id).toBe("design-systems"); - expect(r.state.name).toBe("duty"); - - const manifest = readManifest(dir, "organization"); - expect(manifest.children["architect"].children["design-systems"].type).toBe("duty"); - - const content = readFeature(dir, "design-systems.duty.feature"); - expect(content).toContain("Design systems"); - }); - - test("throws when position not found", () => { - const rolex = setup(); - const dir = join(tmpDir, "my-org"); - rolex.prototype.found(dir, undefined, "my-org"); - expect(() => rolex.prototype.charge(dir, "nonexistent", "Feature: X", "x")).toThrow( - 'Position "nonexistent" not found' - ); - }); - }); - - describe("require", () => { - test("adds required skill under position in manifest", () => { - const rolex = setup(); - const dir = join(tmpDir, "my-org"); - rolex.prototype.found(dir, undefined, "my-org"); - rolex.prototype.establish(dir, "Feature: Architect", "architect"); - const r = rolex.prototype.require( - dir, - "architect", - "Feature: System Design\n Scenario: When to apply\n Given a new service is planned\n Then design the architecture first", - "system-design" - ); - - expect(r.process).toBe("require"); - expect(r.state.id).toBe("system-design"); - expect(r.state.name).toBe("requirement"); - - const manifest = readManifest(dir, "organization"); - expect(manifest.children["architect"].children["system-design"].type).toBe("requirement"); - - const content = readFeature(dir, "system-design.requirement.feature"); - expect(content).toContain("System Design"); - }); - }); - - describe("full workflow — organization", () => { - test("found → charter → establish → charge → require produces valid prototype", () => { - const rolex = setup(); - const dir = join(tmpDir, "dp-org"); - - rolex.prototype.found( - dir, - "Feature: Deepractice\n AI agent framework company.", - "deepractice", - ["DP"] - ); - rolex.prototype.charter( - dir, - "Feature: Build role-based AI\n Scenario: Mission\n Given AI needs identity", - "mission" - ); - rolex.prototype.establish( - dir, - "Feature: Backend Architect\n System design lead.", - "architect" - ); - rolex.prototype.charge( - dir, - "architect", - "Feature: Design APIs\n Scenario: New service\n Given a service is needed\n Then design API first", - "design-apis" - ); - rolex.prototype.require( - dir, - "architect", - "Feature: System Design Skill\n Scenario: When to apply\n Given architecture decisions needed\n Then apply systematic design", - "system-design" - ); - - const manifest = readManifest(dir, "organization"); - expect(manifest.id).toBe("deepractice"); - expect(manifest.type).toBe("organization"); - expect(manifest.alias).toEqual(["DP"]); - expect(Object.keys(manifest.children)).toEqual(["mission", "architect"]); - expect(manifest.children["mission"].type).toBe("charter"); - expect(manifest.children["architect"].type).toBe("position"); - expect(manifest.children["architect"].children["design-apis"].type).toBe("duty"); - expect(manifest.children["architect"].children["system-design"].type).toBe("requirement"); - - // All feature files exist - expect(existsSync(join(dir, "deepractice.organization.feature"))).toBe(true); - expect(existsSync(join(dir, "mission.charter.feature"))).toBe(true); - expect(existsSync(join(dir, "architect.position.feature"))).toBe(true); - expect(existsSync(join(dir, "design-apis.duty.feature"))).toBe(true); - expect(existsSync(join(dir, "system-design.requirement.feature"))).toBe(true); - }); - }); -}); diff --git a/packages/rolexjs/tests/context.test.ts b/packages/rolexjs/tests/context.test.ts index 38089d1..033470f 100644 --- a/packages/rolexjs/tests/context.test.ts +++ b/packages/rolexjs/tests/context.test.ts @@ -16,116 +16,107 @@ function setupWithDir() { return { rolex, dataDir }; } -describe("RoleContext", () => { - test("activate returns ctx in result", async () => { +describe("Role (ctx management)", () => { + test("activate returns Role with ctx", async () => { const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const result = await rolex.role.activate("sean"); - expect(result.ctx).toBeInstanceOf(RoleContext); - expect(result.ctx!.roleId).toBe("sean"); - expect(result.hint).toBeDefined(); + 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 = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const { ctx } = await rolex.role.activate("sean"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); - const result = rolex.role.want("sean", "Feature: Build auth", "build-auth", undefined, ctx!); - expect(ctx!.focusedGoalId).toBe("build-auth"); - expect(ctx!.focusedPlanId).toBeNull(); + const result = role.want("Feature: Build auth", "build-auth"); + expect(role.ctx.focusedGoalId).toBe("build-auth"); + expect(role.ctx.focusedPlanId).toBeNull(); expect(result.hint).toBeDefined(); }); test("plan updates ctx.focusedPlanId", async () => { const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const { ctx } = await rolex.role.activate("sean"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); - rolex.role.want("sean", "Feature: Auth", "auth-goal", undefined, ctx!); - const result = rolex.role.plan("auth-goal", "Feature: JWT strategy", "jwt-plan", ctx!); - expect(ctx!.focusedPlanId).toBe("jwt-plan"); + role.want("Feature: Auth", "auth-goal"); + const result = role.plan("Feature: JWT strategy", "jwt-plan"); + expect(role.ctx.focusedPlanId).toBe("jwt-plan"); expect(result.hint).toBeDefined(); }); test("finish with encounter registers in ctx", async () => { const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const { ctx } = await rolex.role.activate("sean"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); - rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); - rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); - rolex.role.todo("jwt", "Feature: Login", "login", undefined, ctx!); + role.want("Feature: Auth", "auth"); + role.plan("Feature: JWT", "jwt"); + role.todo("Feature: Login", "login"); - const result = rolex.role.finish( + const result = role.finish( "login", - "sean", "Feature: Login done\n Scenario: OK\n Given login\n Then success", - ctx! ); - expect(ctx!.encounterIds.has("login-finished")).toBe(true); + expect(role.ctx.encounterIds.has("login-finished")).toBe(true); expect(result.hint).toBeDefined(); }); test("finish without encounter does not register in ctx", async () => { const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const { ctx } = await rolex.role.activate("sean"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); - rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); - rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); - rolex.role.todo("jwt", "Feature: Login", "login", undefined, ctx!); + role.want("Feature: Auth", "auth"); + role.plan("Feature: JWT", "jwt"); + role.todo("Feature: Login", "login"); - rolex.role.finish("login", "sean", undefined, ctx!); - expect(ctx!.encounterIds.size).toBe(0); + role.finish("login"); + expect(role.ctx.encounterIds.size).toBe(0); }); test("complete registers encounter and clears focusedPlanId", async () => { const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const { ctx } = await rolex.role.activate("sean"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); - rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); - rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); + role.want("Feature: Auth", "auth"); + role.plan("Feature: JWT", "jwt"); - const result = rolex.role.complete( + const result = role.complete( "jwt", - "sean", "Feature: JWT done\n Scenario: OK\n Given jwt\n Then done", - ctx! ); - expect(ctx!.focusedPlanId).toBeNull(); - expect(ctx!.encounterIds.has("jwt-completed")).toBe(true); + expect(role.ctx.focusedPlanId).toBeNull(); + expect(role.ctx.encounterIds.has("jwt-completed")).toBe(true); expect(result.hint).toContain("auth"); }); test("reflect consumes encounter and adds experience in ctx", async () => { const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const { ctx } = await rolex.role.activate("sean"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); - rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); - rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); - rolex.role.todo("jwt", "Feature: Login", "login", undefined, ctx!); - rolex.role.finish( + role.want("Feature: Auth", "auth"); + role.plan("Feature: JWT", "jwt"); + role.todo("Feature: Login", "login"); + role.finish( "login", - "sean", "Feature: Login done\n Scenario: OK\n Given x\n Then y", - ctx! ); - expect(ctx!.encounterIds.has("login-finished")).toBe(true); + expect(role.ctx.encounterIds.has("login-finished")).toBe(true); - rolex.role.reflect( + role.reflect( "login-finished", - "sean", "Feature: Token insight\n Scenario: OK\n Given x\n Then y", "token-insight", - ctx! ); - expect(ctx!.encounterIds.has("login-finished")).toBe(false); - expect(ctx!.experienceIds.has("token-insight")).toBe(true); + expect(role.ctx.encounterIds.has("login-finished")).toBe(false); + expect(role.ctx.experienceIds.has("token-insight")).toBe(true); }); test("cognitiveHint varies by state", () => { @@ -141,7 +132,7 @@ describe("RoleContext", () => { }); }); -describe("RoleContext persistence", () => { +describe("Role context persistence", () => { const dirs: string[] = []; afterEach(() => { for (const d of dirs) { @@ -158,14 +149,14 @@ describe("RoleContext persistence", () => { test("activate restores persisted focusedGoalId and focusedPlanId", async () => { const { rolex, dataDir } = persistent(); - rolex.individual.born("Feature: Sean", "sean"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); // Session 1: set focus - const { ctx: ctx1 } = await rolex.role.activate("sean"); - rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx1!); - rolex.role.plan("auth", "Feature: JWT", "jwt", ctx1!); - expect(ctx1!.focusedGoalId).toBe("auth"); - expect(ctx1!.focusedPlanId).toBe("jwt"); + const role1 = await rolex.activate("sean"); + role1.want("Feature: Auth", "auth"); + role1.plan("Feature: JWT", "jwt"); + expect(role1.ctx.focusedGoalId).toBe("auth"); + expect(role1.ctx.focusedPlanId).toBe("jwt"); // Verify context.json written const contextPath = join(dataDir, "context", "sean.json"); @@ -175,32 +166,30 @@ describe("RoleContext persistence", () => { expect(data.focusedPlanId).toBe("jwt"); // Session 2: re-activate restores - const { ctx: ctx2 } = await rolex.role.activate("sean"); - expect(ctx2!.focusedGoalId).toBe("auth"); - expect(ctx2!.focusedPlanId).toBe("jwt"); + 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 } = persistent(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.role.want("sean", "Feature: Auth", "auth"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + await rolex.direct("!role.want", { individual: "sean", goal: "Feature: Auth", id: "auth" }); - // No context.json exists — rehydrate picks first goal - const { ctx } = await rolex.role.activate("sean"); - expect(ctx!.focusedGoalId).toBe("auth"); - expect(ctx!.focusedPlanId).toBeNull(); + 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, dataDir } = persistent(); - rolex.individual.born("Feature: Sean", "sean"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); - const { ctx } = await rolex.role.activate("sean"); - rolex.role.want("sean", "Feature: Goal A", "goal-a", undefined, ctx!); - rolex.role.want("sean", "Feature: Goal B", "goal-b", undefined, ctx!); + const role = await rolex.activate("sean"); + role.want("Feature: Goal A", "goal-a"); + role.want("Feature: Goal B", "goal-b"); - // focus switches back to goal-a - rolex.role.focus("goal-a", ctx!); + role.focus("goal-a"); const data = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); expect(data.focusedGoalId).toBe("goal-a"); @@ -209,16 +198,14 @@ describe("RoleContext persistence", () => { test("complete clears focusedPlanId and saves", async () => { const { rolex, dataDir } = persistent(); - rolex.individual.born("Feature: Sean", "sean"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); - const { ctx } = await rolex.role.activate("sean"); - rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); - rolex.role.plan("auth", "Feature: JWT", "jwt", ctx!); - rolex.role.complete( + const role = await rolex.activate("sean"); + role.want("Feature: Auth", "auth"); + role.plan("Feature: JWT", "jwt"); + role.complete( "jwt", - "sean", "Feature: Done\n Scenario: OK\n Given done\n Then ok", - ctx! ); const data = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); @@ -228,34 +215,30 @@ describe("RoleContext persistence", () => { test("different roles have independent contexts", async () => { const { rolex, dataDir } = persistent(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.individual.born("Feature: Nuwa", "nuwa"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + await rolex.direct("!individual.born", { content: "Feature: Nuwa", id: "nuwa" }); - // Sean session - const { ctx: seanCtx } = await rolex.role.activate("sean"); - rolex.role.want("sean", "Feature: Sean Goal", "sean-goal", undefined, seanCtx!); + const seanRole = await rolex.activate("sean"); + seanRole.want("Feature: Sean Goal", "sean-goal"); - // Nuwa session - const { ctx: nuwaCtx } = await rolex.role.activate("nuwa"); - rolex.role.want("nuwa", "Feature: Nuwa Goal", "nuwa-goal", undefined, nuwaCtx!); + const nuwaRole = await rolex.activate("nuwa"); + nuwaRole.want("Feature: Nuwa Goal", "nuwa-goal"); - // Verify independent files const seanData = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); const nuwaData = JSON.parse(readFileSync(join(dataDir, "context", "nuwa.json"), "utf-8")); expect(seanData.focusedGoalId).toBe("sean-goal"); expect(nuwaData.focusedGoalId).toBe("nuwa-goal"); - // Re-activate sean — should get sean's context, not nuwa's - const { ctx: seanCtx2 } = await rolex.role.activate("sean"); - expect(seanCtx2!.focusedGoalId).toBe("sean-goal"); + // Re-activate sean — should get sean's context + 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 = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const { ctx } = await rolex.role.activate("sean"); - rolex.role.want("sean", "Feature: Auth", "auth", undefined, ctx!); - // Should not throw — just no persistence - expect(ctx!.focusedGoalId).toBe("auth"); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); + 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 1b71d81..639947e 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -4,956 +4,162 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { localPlatform } from "@rolexjs/local-platform"; import { describe as renderDescribe, hint as renderHint, renderState } from "../src/render.js"; -import { createRoleX, type RolexResult } from "../src/rolex.js"; +import { createRoleX, type RolexResult } from "../src/index.js"; function setup() { return 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.individual.born("Feature: I am Sean", "sean"); - expect(r.state.name).toBe("individual"); - expect(r.state.information).toBe("Feature: I am Sean"); - expect(r.process).toBe("born"); - // Scaffolding: identity - const names = r.state.children!.map((c) => c.name); - expect(names).toContain("identity"); - }); - - test("found creates an organization", () => { - const rolex = setup(); - const r = rolex.org.found("Feature: AI company", "ai-co"); - expect(r.state.name).toBe("organization"); - expect(r.process).toBe("found"); - }); - - test("establish creates a position", () => { - const rolex = setup(); - const r = rolex.position.establish("Feature: Backend architect", "pos1"); - expect(r.state.name).toBe("position"); - }); - - test("charter defines org mission", () => { - const rolex = setup(); - rolex.org.found(undefined, "org1"); - const r = rolex.org.charter("org1", "Feature: Build great AI"); - expect(r.state.name).toBe("charter"); - expect(r.state.information).toBe("Feature: Build great AI"); - }); +// ================================================================ +// use() dispatch +// ================================================================ - test("charge adds duty to position", () => { - const rolex = setup(); - rolex.position.establish(undefined, "pos1"); - const r = rolex.position.charge("pos1", "Feature: Design systems"); - expect(r.state.name).toBe("duty"); - }); +describe("use dispatch", () => { + test("!individual.born creates individual", async () => { + const rolex = 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(); - rolex.individual.born("Feature: Sean", "sean"); - const r = rolex.individual.retire("sean"); - expect(r.state.name).toBe("past"); - expect(r.process).toBe("retire"); - // Original individual is gone — only past node with same id remains - const found = rolex.find("sean"); - expect(found).not.toBeNull(); - expect(found!.name).toBe("past"); - }); - - test("die archives individual", () => { - const rolex = setup(); - rolex.individual.born(undefined, "alice"); - const r = rolex.individual.die("alice"); - expect(r.state.name).toBe("past"); - expect(r.process).toBe("die"); - }); - - test("dissolve archives organization", () => { - const rolex = setup(); - rolex.org.found(undefined, "org1"); - rolex.org.dissolve("org1"); - const found = rolex.find("org1"); - expect(found).not.toBeNull(); - expect(found!.name).toBe("past"); - }); - - test("abolish archives position", () => { - const rolex = setup(); - rolex.position.establish(undefined, "pos1"); - rolex.position.abolish("pos1"); - const found = rolex.find("pos1"); - expect(found).not.toBeNull(); - expect(found!.name).toBe("past"); - }); - - test("rehire restores individual from past", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.individual.retire("sean"); - const r = rolex.individual.rehire("sean"); - 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"); - }); + test("chained operations via use", async () => { + const rolex = 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(); - rolex.individual.born(undefined, "sean"); - rolex.org.found(undefined, "org1"); - const r = rolex.org.hire("org1", "sean"); - expect(r.state.links).toHaveLength(1); - expect(r.state.links![0].relation).toBe("membership"); - }); - - test("fire removes membership", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.org.found(undefined, "org1"); - rolex.org.hire("org1", "sean"); - const r = rolex.org.fire("org1", "sean"); - expect(r.state.links).toBeUndefined(); - }); - - test("appoint links individual to position", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.position.establish(undefined, "pos1"); - const r = rolex.position.appoint("pos1", "sean"); - expect(r.state.links).toHaveLength(1); - expect(r.state.links![0].relation).toBe("appointment"); - }); - - test("dismiss removes appointment", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.position.establish(undefined, "pos1"); - rolex.position.appoint("pos1", "sean"); - const r = rolex.position.dismiss("pos1", "sean"); - expect(r.state.links).toBeUndefined(); - }); - - test("require adds required skill to position", () => { - const rolex = setup(); - rolex.position.establish(undefined, "architect"); - const r = rolex.position.require( - "architect", - "Feature: System Design\n Scenario: Design APIs", - "system-design" - ); - expect(r.state.name).toBe("requirement"); - expect(r.state.id).toBe("system-design"); - expect(r.process).toBe("require"); - }); - - test("require upserts by id", () => { - const rolex = setup(); - rolex.position.establish(undefined, "architect"); - rolex.position.require("architect", "Feature: Old skill", "skill-1"); - rolex.position.require("architect", "Feature: Updated skill", "skill-1"); - const pos = rolex.find("architect")!; - const requires = (pos as any).children?.filter((c: any) => c.name === "requirement"); - expect(requires).toHaveLength(1); - expect(requires[0].information).toBe("Feature: Updated skill"); - }); - - test("appoint auto-trains required skills to individual", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.position.establish(undefined, "architect"); - rolex.position.require("architect", "Feature: System Design", "system-design"); - rolex.position.require("architect", "Feature: Code Review", "code-review"); - rolex.position.appoint("architect", "sean"); - - // Individual should now have the required procedures - const sean = rolex.find("sean")!; - const procedures = (sean as any).children?.filter((c: any) => c.name === "procedure"); - expect(procedures).toHaveLength(2); - const ids = procedures.map((p: any) => p.id).sort(); - expect(ids).toEqual(["code-review", "system-design"]); - }); - - test("appoint without required skills still works", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.position.establish(undefined, "pos1"); - const r = rolex.position.appoint("pos1", "sean"); - expect(r.state.links).toHaveLength(1); - }); + test("!census.list returns text", async () => { + const rolex = 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", async () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const r = await rolex.role.activate("sean"); - expect(r.state.name).toBe("individual"); - expect(r.process).toBe("activate"); - }); + test("throws on unknown command", () => { + const rolex = setup(); + expect(() => rolex.direct("!foo.bar")).toThrow(); }); - // ============================================================ - // Execution - // ============================================================ - - describe("execution", () => { - test("want creates a goal", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - const r = rolex.role.want("sean", "Feature: Build auth system", "g1"); - 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(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", "Feature: Auth", "g1"); - const r = rolex.role.plan("g1", "Feature: JWT plan", "p1"); - expect(r.state.name).toBe("plan"); - }); - - test("plan with after creates sequential link", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", "Feature: Auth", "g1"); - rolex.role.plan("g1", "Feature: Phase 1", "phase-1"); - rolex.role.plan("g1", "Feature: Phase 2", "phase-2", undefined, "phase-1"); - - // Phase 2 has "after" link to Phase 1 - const p2 = rolex.find("phase-2")!; - expect((p2 as any).links).toHaveLength(1); - expect((p2 as any).links[0].relation).toBe("after"); - expect((p2 as any).links[0].target.id).toBe("phase-1"); - - // Phase 1 has reverse "before" link to Phase 2 - const p1 = rolex.find("phase-1")!; - expect((p1 as any).links).toHaveLength(1); - expect((p1 as any).links[0].relation).toBe("before"); - expect((p1 as any).links[0].target.id).toBe("phase-2"); - }); - - test("plan with fallback creates alternative link", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", "Feature: Auth", "g1"); - rolex.role.plan("g1", "Feature: JWT approach", "plan-a"); - rolex.role.plan("g1", "Feature: Session approach", "plan-b", undefined, undefined, "plan-a"); - - // Plan B has "fallback-for" link to Plan A - const pb = rolex.find("plan-b")!; - expect((pb as any).links).toHaveLength(1); - expect((pb as any).links[0].relation).toBe("fallback-for"); - expect((pb as any).links[0].target.id).toBe("plan-a"); - - // Plan A has reverse "fallback" link to Plan B - const pa = rolex.find("plan-a")!; - expect((pa as any).links).toHaveLength(1); - expect((pa as any).links[0].relation).toBe("fallback"); - expect((pa as any).links[0].target.id).toBe("plan-b"); - }); - - test("plan without after/fallback has no links", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", "Feature: Auth", "g1"); - rolex.role.plan("g1", "Feature: JWT plan", "p1"); - - const p1 = rolex.find("p1")!; - expect((p1 as any).links).toBeUndefined(); - }); - - test("todo creates a task under plan", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", undefined, "g1"); - rolex.role.plan("g1", undefined, "p1"); - const r = rolex.role.todo("p1", "Feature: Implement JWT", "t1"); - expect(r.state.name).toBe("task"); - }); - - test("finish consumes task, creates encounter", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", undefined, "g1"); - rolex.role.plan("g1", undefined, "p1"); - rolex.role.todo("p1", undefined, "t1"); - - const r = rolex.role.finish("t1", "sean", "Feature: JWT done"); - expect(r.state.name).toBe("encounter"); - expect(r.state.information).toBe("Feature: JWT done"); - // Task is tagged done, not removed - const task = rolex.find("t1"); - expect(task).not.toBeNull(); - expect(task!.tag).toBe("done"); - }); - - test("complete consumes plan, creates encounter", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", "Feature: Auth", "g1"); - rolex.role.plan("g1", "Feature: Auth plan", "p1"); - - const r = rolex.role.complete("p1", "sean", "Feature: Auth plan done"); - expect(r.state.name).toBe("encounter"); - // Plan is tagged done, not removed - const plan = rolex.find("p1"); - expect(plan).not.toBeNull(); - expect(plan!.tag).toBe("done"); - }); - - test("abandon consumes plan, creates encounter", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", "Feature: Rust", "g1"); - rolex.role.plan("g1", "Feature: Rust plan", "p1"); - - const r = rolex.role.abandon("p1", "sean", "Feature: No time"); - expect(r.state.name).toBe("encounter"); - // Plan is tagged abandoned, not removed - const plan = rolex.find("p1"); - expect(plan).not.toBeNull(); - expect(plan!.tag).toBe("abandoned"); - }); + test("throws on unknown method", () => { + const rolex = setup(); + expect(() => rolex.direct("!org.nope")).toThrow(); }); +}); - // ============================================================ - // Cognition - // ============================================================ - - describe("cognition", () => { - test("reflect: encounter → experience", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", undefined, "g1"); - rolex.role.plan("g1", undefined, "p1"); - rolex.role.todo("p1", undefined, "t1"); - rolex.role.finish("t1", "sean", "Feature: JWT quirks"); - - const r = rolex.role.reflect("t1-finished", "sean", "Feature: Token refresh matters", "exp1"); - expect(r.state.name).toBe("experience"); - expect(r.state.information).toBe("Feature: Token refresh matters"); - expect(rolex.find("t1-finished")).toBeNull(); - }); - - test("reflect inherits encounter info if no source given", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", undefined, "g1"); - rolex.role.plan("g1", undefined, "p1"); - rolex.role.todo("p1", undefined, "t1"); - rolex.role.finish("t1", "sean", "Feature: JWT quirks"); - - const r = rolex.role.reflect("t1-finished", "sean"); - expect(r.state.information).toBe("Feature: JWT quirks"); - }); - - test("realize: experience → principle under individual", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", undefined, "g1"); - rolex.role.plan("g1", undefined, "p1"); - rolex.role.todo("p1", undefined, "t1"); - rolex.role.finish("t1", "sean", "Feature: Lessons"); - rolex.role.reflect("t1-finished", "sean", undefined, "exp1"); - - const r = rolex.role.realize("exp1", "sean", "Feature: Security first", "sec-first"); - expect(r.state.name).toBe("principle"); - expect(r.state.information).toBe("Feature: Security first"); - expect(rolex.find("exp1")).toBeNull(); - }); - - test("master: experience → procedure under individual", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", undefined, "g1"); - rolex.role.plan("g1", undefined, "p1"); - rolex.role.todo("p1", undefined, "t1"); - rolex.role.finish("t1", "sean", "Feature: Practice"); - rolex.role.reflect("t1-finished", "sean", undefined, "exp1"); +// ================================================================ +// activate() + Role API +// ================================================================ - const r = rolex.role.master("sean", "Feature: JWT mastery", "jwt", "exp1"); - expect(r.state.name).toBe("procedure"); - }); +describe("activate", () => { + test("returns Role with ctx", async () => { + const rolex = 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(); }); - // ============================================================ - // Full scenario - // ============================================================ - - describe("full scenario", () => { - test("born → hire → appoint → want → plan → todo → finish → reflect → realize", () => { - const rolex = setup(); - - // Create world - rolex.individual.born("Feature: I am Sean", "sean"); - rolex.org.found("Feature: Deepractice", "dp"); - rolex.position.establish("Feature: Architect", "architect"); - rolex.org.charter("dp", "Feature: Build great AI"); - rolex.position.charge("architect", "Feature: Design systems"); - - // Organization + Position - rolex.org.hire("dp", "sean"); - rolex.position.appoint("architect", "sean"); + test("throws on non-existent individual", async () => { + const rolex = setup(); + expect(rolex.activate("nobody")).rejects.toThrow('"nobody" not found'); + }); - // Verify links - const orgState = rolex.find("dp")!; - expect(orgState.links).toHaveLength(1); - const posState = rolex.find("architect")!; - expect(posState.links).toHaveLength(1); + test("Role.want/plan/todo/finish work through Role API", async () => { + const rolex = setup(); + await rolex.direct("!individual.born", { id: "sean" }); + const role = await rolex.activate("sean"); - // Execution cycle - rolex.role.want("sean", "Feature: Build auth", "build-auth"); - rolex.role.plan("build-auth", "Feature: JWT auth plan", "jwt-plan"); - rolex.role.todo("jwt-plan", "Feature: Login endpoint", "t1"); - rolex.role.todo("jwt-plan", "Feature: Refresh endpoint", "t2"); + const wantR = role.want("Feature: Auth", "auth"); + expect(wantR.state.name).toBe("goal"); + expect(wantR.hint).toBeDefined(); - rolex.role.finish("t1", "sean", "Feature: Login done"); - rolex.role.finish("t2", "sean", "Feature: Refresh done"); - rolex.role.complete("jwt-plan", "sean", "Feature: Auth plan complete"); + const planR = role.plan("Feature: JWT", "jwt"); + expect(planR.state.name).toBe("plan"); - // Cognition cycle - rolex.role.reflect("t1-finished", "sean", "Feature: Token handling", "token-exp"); - rolex.role.realize("token-exp", "sean", "Feature: Always validate expiry", "validate-expiry"); + const todoR = role.todo("Feature: Login", "login"); + expect(todoR.state.name).toBe("task"); - // Verify principle exists under individual - const seanState = rolex.find("sean")!; - const principle = (seanState as any).children?.find((c: any) => c.name === "principle"); - expect(principle).toBeDefined(); - expect(principle.information).toBe("Feature: Always validate expiry"); - }); + const finishR = role.finish("login", "Feature: Done\n Scenario: OK\n Given done\n Then ok"); + expect(finishR.state.name).toBe("encounter"); }); - // ============================================================ - // Render - // ============================================================ - - describe("render", () => { - test("describe generates text with name", () => { - const rolex = setup(); - const r = rolex.individual.born(undefined, "sean"); - const text = renderDescribe("born", "sean", r.state); - expect(text).toContain("sean"); - }); - - test("hint generates next step", () => { - const h = renderHint("born"); - expect(h).toStartWith("Next:"); - }); - - 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:"); - } - }); + test("Role.use delegates to Rolex.use", async () => { + const rolex = 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"); }); +}); - // ============================================================ - // renderState — generic markdown renderer - // ============================================================ - - describe("renderState", () => { - test("renders individual with heading and information", () => { - const rolex = setup(); - const r = rolex.individual.born("Feature: I am Sean\n An AI role.", "sean"); - 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.individual.born("Feature: Sean", "sean"); - const md = renderState(r.state); - // identity is a child at depth 2 - expect(md).toContain("## [identity]"); - }); - - test("renders links generically", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.org.found("Feature: Deepractice", "dp"); - rolex.org.hire("dp", "sean"); - // Project org — should have membership link - const orgState = rolex.find("dp")!; - const md = renderState(orgState as any); - expect(md).toContain("membership"); - expect(md).toContain("[individual]"); - }); - - test("renders bidirectional links", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.org.found("Feature: Deepractice", "dp"); - rolex.org.hire("dp", "sean"); - // Project individual — should have belong link - const seanState = rolex.find("sean")!; - const md = renderState(seanState as any); - expect(md).toContain("belong"); - expect(md).toContain("[organization]"); - expect(md).toContain("Deepractice"); - }); - - test("renders nested structure (goal → plan → task)", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", "Feature: Build auth", "g1"); - rolex.role.plan("g1", "Feature: JWT plan", "p1"); - rolex.role.todo("p1", "Feature: Login endpoint", "t1"); - // Project goal to see full tree - const goalState = rolex.find("g1")!; - const md = renderState(goalState as any); - 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 r = rolex.individual.born(undefined, "sean"); - // Manually test with depth parameter - const md = renderState(r.state, 7); - // Should use ###### (6) not ####### (7) - expect(md).toStartWith("###### [individual]"); - }); - - test("renders without information gracefully", () => { - const rolex = setup(); - const r = rolex.individual.born(undefined, "sean"); - const identity = r.state.children!.find((c) => c.name === "identity")!; - const md = renderState(identity as any); - expect(md).toBe("# [identity]"); - }); - - test("sorts children by concept hierarchy order", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - // Create in reverse of concept order: goal → principle - rolex.role.want("sean", "Feature: My Goal", "g1"); - rolex.individual.teach("sean", "Feature: My Principle", "p1"); - - const state = rolex.find("sean")!; - const md = renderState(state as any); - // Concept order: identity < principle < goal - const identityPos = md.indexOf("## [identity]"); - const principlePos = md.indexOf("## [principle] (p1)"); - const goalPos = md.indexOf("## [goal] (g1)"); - expect(identityPos).toBeLessThan(principlePos); - expect(principlePos).toBeLessThan(goalPos); - }); - - test("fold collapses matching nodes to heading only", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.role.want("sean", "Feature: Goal A", "g-a"); - rolex.role.want("sean", "Feature: Goal B", "g-b"); - rolex.role.plan("g-b", "Feature: Plan under B", "p-b"); - - const state = rolex.find("sean")!; - const md = renderState(state as any, 1, { - fold: (node) => node.name === "goal" && node.id !== "g-b", - }); - - // g-a: folded — heading only, no body - expect(md).toContain("## [goal] (g-a)"); - expect(md).not.toContain("Feature: Goal A"); +// ================================================================ +// Render +// ================================================================ - // g-b: expanded — heading + body + children - expect(md).toContain("## [goal] (g-b)"); - expect(md).toContain("Feature: Goal B"); - expect(md).toContain("### [plan] (p-b)"); - expect(md).toContain("Feature: Plan under B"); - }); +describe("render", () => { + test("describe generates text with name", async () => { + const rolex = setup(); + const r = await rolex.direct("!individual.born", { id: "sean" }); + const text = renderDescribe("born", "sean", r.state); + expect(text).toContain("sean"); }); - // ============================================================ - // Gherkin validation - // ============================================================ - - describe("gherkin validation", () => { - test("born rejects non-Gherkin input", () => { - const rolex = setup(); - expect(() => rolex.individual.born("not gherkin")).toThrow("Invalid Gherkin"); - }); - - test("born accepts valid Gherkin", () => { - const rolex = setup(); - expect(() => rolex.individual.born("Feature: Sean")).not.toThrow(); - }); - - test("born accepts undefined (no source)", () => { - const rolex = setup(); - expect(() => rolex.individual.born()).not.toThrow(); - }); - - test("want rejects non-Gherkin goal", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - expect(() => rolex.role.want("sean", "plain text goal")).toThrow("Invalid Gherkin"); - }); - - test("finish rejects non-Gherkin encounter", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.role.want("sean", "Feature: Auth", "g1"); - rolex.role.plan("g1", undefined, "p1"); - rolex.role.todo("p1", "Feature: Login", "t1"); - expect(() => rolex.role.finish("t1", "sean", "just text")).toThrow("Invalid Gherkin"); - }); - - test("reflect rejects non-Gherkin experience", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.role.want("sean", "Feature: Auth", "g1"); - rolex.role.plan("g1", undefined, "p1"); - rolex.role.todo("p1", "Feature: Login", "t1"); - rolex.role.finish( - "t1", - "sean", - "Feature: Done\n Scenario: It worked\n Given login\n Then success" - ); - expect(() => rolex.role.reflect("t1-finished", "sean", "not gherkin")).toThrow( - "Invalid Gherkin" - ); - }); - - test("realize rejects non-Gherkin principle", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.role.want("sean", "Feature: Auth", "g1"); - rolex.role.plan("g1", undefined, "p1"); - rolex.role.todo("p1", "Feature: Login", "t1"); - rolex.role.finish("t1", "sean", "Feature: Done\n Scenario: OK\n Given x\n Then y"); - rolex.role.reflect( - "t1-finished", - "sean", - "Feature: Insight\n Scenario: Learned\n Given practice\n Then understanding", - "exp1" - ); - expect(() => rolex.role.realize("exp1", "sean", "not gherkin")).toThrow("Invalid Gherkin"); - }); - - test("master rejects non-Gherkin procedure", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - expect(() => rolex.role.master("sean", "not gherkin")).toThrow("Invalid Gherkin"); - }); + test("hint generates next step", () => { + const h = renderHint("born"); + expect(h).toStartWith("Next:"); }); - // ============================================================ - // id & alias - // ============================================================ - - describe("id & alias", () => { - test("born with id stores it on the node", () => { - const rolex = setup(); - const r = rolex.individual.born("Feature: I am Sean", "sean"); - expect(r.state.id).toBe("sean"); - expect(r.state.ref).toBeDefined(); - }); - - test("born with id and alias stores both", () => { - const rolex = setup(); - const r = rolex.individual.born("Feature: I am Sean", "sean", ["Sean", "姜山"]); - expect(r.state.id).toBe("sean"); - expect(r.state.alias).toEqual(["Sean", "姜山"]); - }); - - test("born without id has no id field", () => { - const rolex = setup(); - const r = rolex.individual.born("Feature: I am Sean"); - expect(r.state.id).toBeUndefined(); - }); - - test("want with id stores it on the goal", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - const r = rolex.role.want("sean", "Feature: Build auth", "build-auth"); - expect(r.state.id).toBe("build-auth"); - }); - - test("todo with id stores it on the task", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.role.want("sean", undefined, "g1"); - rolex.role.plan("g1", undefined, "p1"); - const r = rolex.role.todo("p1", "Feature: Login", "impl-login"); - expect(r.state.id).toBe("impl-login"); - }); - - test("find by id", () => { - const rolex = setup(); - rolex.individual.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"); - }); - - test("find by alias", () => { - const rolex = setup(); - rolex.individual.born("Feature: I am Sean", "sean", ["Sean", "姜山"]); - const found = rolex.find("姜山"); - expect(found).not.toBeNull(); - expect(found!.name).toBe("individual"); - }); - - test("find is case insensitive", () => { - const rolex = setup(); - rolex.individual.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(); - }); - - test("find returns null when not found", () => { - const rolex = setup(); - rolex.individual.born("Feature: I am Sean", "sean"); - expect(rolex.find("nobody")).toBeNull(); - }); - - test("find searches nested nodes", () => { - const rolex = setup(); - rolex.individual.born("Feature: Sean", "sean"); - rolex.role.want("sean", "Feature: Build auth", "build-auth"); - const found = rolex.find("build-auth"); - expect(found).not.toBeNull(); - expect(found!.name).toBe("goal"); - }); - - test("found with id", () => { - const rolex = setup(); - const r = rolex.org.found("Feature: Deepractice", "deepractice"); - expect(r.state.id).toBe("deepractice"); - }); - - test("establish with id", () => { - const rolex = setup(); - const r = rolex.position.establish("Feature: Architect", "architect"); - expect(r.state.id).toBe("architect"); - }); + test("renderState renders individual with heading", async () => { + const rolex = 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"); }); - // ============================================================ - // use — unified execution entry point - // ============================================================ - // Census - // ============================================================ - - describe("census", () => { - test("list returns all top-level entities grouped by type", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.org.found(undefined, "dp"); - rolex.position.establish(undefined, "architect"); - const result = rolex.census.list(); - expect(result).toContain("[individual]"); - expect(result).toContain("[organization]"); - expect(result).toContain("[position]"); - expect(result).toContain("sean"); - expect(result).toContain("dp"); - expect(result).toContain("architect"); - }); - - test("list filters by type", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.individual.born(undefined, "alice"); - rolex.org.found(undefined, "dp"); - const result = rolex.census.list("individual"); - expect(result).toContain("[individual] (2)"); - expect(result).toContain("sean"); - expect(result).toContain("alice"); - expect(result).not.toContain("dp"); - }); - - test("list returns message when no matches", () => { - const rolex = setup(); - const result = rolex.census.list("position"); - expect(result).toBe("No position found."); - }); - - test("retired entities disappear from society", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.individual.retire("sean"); - const result = rolex.census.list("individual"); - expect(result).toBe("No individual found."); - }); - - test("list past shows archived entities", () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.individual.retire("sean"); - const result = rolex.census.list("past"); - expect(result).toContain("sean"); - }); - - test("!census.list via use dispatch", async () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.org.found(undefined, "dp"); - const result = await rolex.use("!census.list"); - expect(result).toContain("sean"); - expect(result).toContain("dp"); - }); - - test("!census.list with type filter via use dispatch", async () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.org.found(undefined, "dp"); - const result = await rolex.use("!census.list", { - type: "organization", - }); - expect(result).toContain("dp"); - expect(result).not.toContain("sean"); - }); + test("renderState renders nested structure", async () => { + const rolex = 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]"); }); +}); - // ============================================================ - - describe("use: ! command dispatch", () => { - test("!org.found creates organization", async () => { - const rolex = setup(); - const r = await rolex.use("!org.found", { - content: "Feature: Deepractice", - id: "dp", - }); - expect(r.state.name).toBe("organization"); - expect(r.state.id).toBe("dp"); - }); - - test("!position.establish creates position", async () => { - const rolex = setup(); - const r = await rolex.use("!position.establish", { - content: "Feature: Architect", - id: "architect", - }); - expect(r.state.name).toBe("position"); - expect(r.state.id).toBe("architect"); - }); - - test("!org.hire links individual to org", async () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.org.found(undefined, "dp"); - const r = await rolex.use("!org.hire", { - org: "dp", - individual: "sean", - }); - expect(r.state.links).toHaveLength(1); - expect(r.state.links![0].relation).toBe("membership"); - }); - - test("!position.appoint links individual to position", async () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - rolex.position.establish(undefined, "pos1"); - const r = await rolex.use("!position.appoint", { - position: "pos1", - individual: "sean", - }); - expect(r.state.links).toHaveLength(1); - expect(r.state.links![0].relation).toBe("appointment"); - }); - - test("!individual.born creates individual", async () => { - const rolex = setup(); - const r = await rolex.use("!individual.born", { - content: "Feature: Alice", - id: "alice", - }); - expect(r.state.name).toBe("individual"); - expect(r.state.id).toBe("alice"); - }); - - test("!individual.teach injects principle", async () => { - const rolex = setup(); - rolex.individual.born(undefined, "sean"); - const r = await rolex.use("!individual.teach", { - individual: "sean", - content: "Feature: Always test first", - id: "test-first", - }); - expect(r.state.name).toBe("principle"); - }); - - test("throws on unknown namespace", async () => { - const rolex = setup(); - expect(() => rolex.use("!foo.bar")).toThrow('Unknown namespace "foo"'); - }); +// ================================================================ +// Gherkin validation (through use dispatch) +// ================================================================ - test("throws on unknown method", async () => { - const rolex = setup(); - expect(() => rolex.use("!org.nope")).toThrow('Unknown command "!org.nope"'); - }); +describe("gherkin validation", () => { + test("rejects non-Gherkin input", () => { + const rolex = setup(); + expect(() => rolex.direct("!individual.born", { content: "not gherkin" })).toThrow("Invalid Gherkin"); + }); - test("throws on missing dot", async () => { - const rolex = setup(); - expect(() => rolex.use("!orgfound")).toThrow("Expected"); - }); + test("accepts valid Gherkin", () => { + const rolex = setup(); + expect(() => rolex.direct("!individual.born", { content: "Feature: Sean" })).not.toThrow(); }); }); // ================================================================ -// Persistent mode — round-trip tests +// Persistent mode // ================================================================ -describe("Rolex API (persistent)", () => { +describe("persistent mode", () => { const testDir = join(tmpdir(), "rolex-persist-test"); afterEach(() => { @@ -964,60 +170,22 @@ describe("Rolex API (persistent)", () => { return createRoleX(localPlatform({ dataDir: testDir, resourceDir: null })); } - test("born → retire round-trip", () => { + test("born → retire round-trip", async () => { const rolex = persistentSetup(); - rolex.individual.born("Feature: Test Individual", "test-ind"); - const r = rolex.individual.retire("test-ind"); + 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.state.id).toBe("test-ind"); expect(r.process).toBe("retire"); - // Original individual should be gone, past node should be findable - const found = rolex.find("test-ind"); - expect(found).not.toBeNull(); - expect(found!.name).toBe("past"); }); - test("born → die round-trip", () => { - const rolex = persistentSetup(); - rolex.individual.born("Feature: Test Individual", "test-ind"); - const r = rolex.individual.die("test-ind"); - expect(r.state.name).toBe("past"); - expect(r.state.id).toBe("test-ind"); - expect(r.process).toBe("die"); - }); - - test("born → teach → retire round-trip", () => { - const rolex = persistentSetup(); - rolex.individual.born("Feature: Test", "test-ind"); - rolex.individual.teach("test-ind", "Feature: Always validate", "always-validate"); - const r = rolex.individual.retire("test-ind"); - expect(r.state.name).toBe("past"); - expect(r.process).toBe("retire"); - }); - - test("born → retire → rehire round-trip", () => { - const rolex = persistentSetup(); - rolex.individual.born("Feature: Test", "test-ind"); - rolex.individual.retire("test-ind"); - const r = rolex.individual.rehire("test-ind"); - expect(r.state.name).toBe("individual"); - expect(r.state.information).toBe("Feature: Test"); - const names = r.state.children!.map((c) => c.name); - expect(names).toContain("identity"); - }); - - test("archived entity survives cross-instance reload", () => { - // First instance: born + retire + test("archived entity survives cross-instance reload", async () => { const rolex1 = persistentSetup(); - rolex1.individual.born("Feature: Test", "test-ind"); - rolex1.individual.retire("test-ind"); - // Second instance: rehire from persisted archive + await rolex1.direct("!individual.born", { content: "Feature: Test", id: "test-ind" }); + await rolex1.direct("!individual.retire", { individual: "test-ind" }); + const rolex2 = persistentSetup(); - const found = rolex2.find("test-ind"); - expect(found).not.toBeNull(); - expect(found!.name).toBe("past"); - const r = rolex2.individual.rehire("test-ind"); + // rehire should find the archived entity + const r = await rolex2.direct("!individual.rehire", { individual: "test-ind" }); expect(r.state.name).toBe("individual"); - expect(r.state.information).toBe("Feature: Test"); }); }); diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index f42f9b8..ccdc63c 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -36,19 +36,10 @@ export type { } from "./process.js"; export { create, link, process, remove, transform, unlink } from "./process.js"; -// ===== Merge ===== - -export { mergeState } from "./merge.js"; - // ===== Initializer ===== export type { Initializer } from "./initializer.js"; -// ===== Prototype ===== - -export type { Prototype } from "./prototype.js"; -export { createPrototype } from "./prototype.js"; - // ===== Runtime ===== export type { Runtime } from "./runtime.js"; 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 17610b3..0000000 --- a/packages/system/src/prototype.ts +++ /dev/null @@ -1,67 +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 manages and 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; - - /** Settle: register a prototype — bind id to a source (path or locator). */ - settle(id: string, source: string): void; - - /** Evict: unregister a prototype by id. */ - evict(id: string): void; - - /** List all registered prototypes: id → source mapping. */ - list(): Record; -} - -// ===== In-memory implementation ===== - -/** Create an in-memory prototype (for tests). */ -export const createPrototype = (): Prototype & { - /** Seed a State directly for testing (bypasses source resolution). */ - seed(state: State): void; -} => { - const states = new Map(); - const sources = new Map(); - - return { - async resolve(id) { - return states.get(id); - }, - - settle(id, source) { - sources.set(id, source); - }, - - evict(id) { - sources.delete(id); - states.delete(id); - }, - - list() { - return Object.fromEntries(sources); - }, - - seed(state) { - if (!state.id) { - throw new Error("Prototype state must have an id"); - } - states.set(state.id, state); - }, - }; -}; 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 67378ad..0000000 --- a/packages/system/tests/prototype.test.ts +++ /dev/null @@ -1,83 +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 seeded", async () => { - const proto = createPrototype(); - expect(await proto.resolve("sean")).toBeUndefined(); - }); - - test("seed 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.seed(template); - const resolved = await proto.resolve("sean"); - expect(resolved).toBeDefined(); - expect(resolved!.id).toBe("sean"); - expect(resolved!.children).toHaveLength(2); - }); - - test("seed throws if state has no id", () => { - const proto = createPrototype(); - const template = state("individual"); - expect(() => proto.seed(template)).toThrow("must have an id"); - }); - - test("later seed overwrites earlier one", async () => { - const proto = createPrototype(); - proto.seed(state("individual", { id: "sean", information: "v1" })); - proto.seed(state("individual", { id: "sean", information: "v2" })); - expect((await proto.resolve("sean"))!.information).toBe("v2"); - }); - - test("different ids resolve independently", async () => { - const proto = createPrototype(); - proto.seed(state("individual", { id: "sean", information: "Sean" })); - proto.seed(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(); - }); - - test("settle and list round-trip", () => { - const proto = createPrototype(); - proto.settle("nuwa", "/path/to/nuwa"); - proto.settle("sean", "/path/to/sean"); - expect(proto.list()).toEqual({ - nuwa: "/path/to/nuwa", - sean: "/path/to/sean", - }); - }); - - test("evict removes from list", () => { - const proto = createPrototype(); - proto.settle("nuwa", "/path/to/nuwa"); - proto.settle("sean", "/path/to/sean"); - proto.evict("nuwa"); - expect(proto.list()).toEqual({ sean: "/path/to/sean" }); - }); - - test("list returns empty when nothing registered", () => { - const proto = createPrototype(); - expect(proto.list()).toEqual({}); - }); -}); From 744aa79758ff1d9f68811c87f5b2d00922d21c3e Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Feb 2026 16:56:37 +0800 Subject: [PATCH 27/54] developing --- .changeset/config.json | 1 - apps/cli/CHANGELOG.md | 112 ----- apps/cli/package.json | 25 - apps/cli/src/index.ts | 755 ------------------------------ apps/cli/tsconfig.json | 15 - apps/cli/tsup.config.ts | 12 - apps/mcp-server/src/index.ts | 87 ++-- apps/mcp-server/src/state.ts | 21 +- apps/mcp-server/tests/mcp.test.ts | 188 +++----- 9 files changed, 119 insertions(+), 1097 deletions(-) delete mode 100644 apps/cli/CHANGELOG.md delete mode 100644 apps/cli/package.json delete mode 100644 apps/cli/src/index.ts delete mode 100644 apps/cli/tsconfig.json delete mode 100644 apps/cli/tsup.config.ts diff --git a/.changeset/config.json b/.changeset/config.json index 2a510b9..7c87e50 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -8,7 +8,6 @@ "@rolexjs/parser", "@rolexjs/local-platform", "rolexjs", - "@rolexjs/cli", "@rolexjs/mcp-server", "@rolexjs/system" ] 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 bf304c6..0000000 --- a/apps/cli/src/index.ts +++ /dev/null @@ -1,755 +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()); -await rolex.bootstrap(); - -// ========== 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", - }, - 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", - }, - 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: { - individual: { type: "positional" as const, description: "Individual id", required: true }, - ...contentArg("procedure"), - id: { type: "string" as const, description: "Procedure id (keywords joined by hyphens)" }, - experience: { type: "string" as const, description: "Experience id to consume (optional)" }, - }, - run({ args }) { - const result = rolex.role.master( - args.individual, - requireContent(args, "procedure"), - args.id, - args.experience - ); - 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.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" }, - args: { - ...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.position.establish(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.position.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.position.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.position.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.position.dismiss(args.position, args.individual), args.position); - }, -}); - -const org = defineCommand({ - meta: { name: "organization", description: "Organization management" }, - subCommands: { - found, - charter, - dissolve, - hire, - fire, - }, -}); - -const pos = defineCommand({ - meta: { name: "position", description: "Position management" }, - subCommands: { - establish, - charge, - abolish, - 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 creation — born, teach, train ========== - -// ========== Prototype — registry ========== - -const protoSettle = defineCommand({ - meta: { - name: "settle", - description: "Settle a prototype into the world from a ResourceX source", - }, - args: { - source: { - type: "positional" as const, - description: "ResourceX source — local path or locator", - required: true, - }, - }, - async run({ args }) { - const result = await rolex.prototype.settle(args.source); - output(result, result.state.id ?? args.source); - }, -}); - -const protoList = defineCommand({ - meta: { name: "list", description: "List all registered prototypes" }, - run() { - const list = rolex.prototype.list(); - const entries = Object.entries(list); - if (entries.length === 0) { - console.log("No prototypes registered."); - return; - } - for (const [id, source] of entries) { - console.log(`${id} → ${source}`); - } - }, -}); - -const prototype = defineCommand({ - meta: { name: "prototype", description: "Prototype management — registry + creation" }, - subCommands: { - settle: protoSettle, - list: protoList, - }, -}); - -// ========== Main ========== - -const main = defineCommand({ - meta: { - name: "rolex", - version: "0.11.0", - description: "RoleX — AI Agent Role Management CLI", - }, - subCommands: { - individual, - role, - organization: org, - position: pos, - 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/cli/tsup.config.ts b/apps/cli/tsup.config.ts deleted file mode 100644 index a2b4287..0000000 --- a/apps/cli/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts"], - format: ["esm"], - dts: true, - clean: true, - sourcemap: true, - banner: { - js: "#!/usr/bin/env node", - }, -}); diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 532296e..08b646d 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -2,7 +2,7 @@ * @rolexjs/mcp-server — individual-level MCP tools. * * Thin wrapper around the Rolex API. All business logic (state tracking, - * cognitive hints, encounter/experience registries) lives in RoleContext (rolexjs). + * cognitive hints, encounter/experience registries) lives in Role + RoleContext. * MCP only translates protocol calls to API calls. */ @@ -17,8 +17,8 @@ import { McpState } from "./state.js"; // ========== Setup ========== const rolex = createRoleX(localPlatform()); -await rolex.bootstrap(); -const state = new McpState(rolex); +await rolex.genesis(); +const state = new McpState(); // ========== Server ========== @@ -52,16 +52,17 @@ server.addTool({ roleId: z.string().describe("Role name to activate"), }), execute: async ({ roleId }) => { - const result = await rolex.role.activate(roleId); - state.ctx = result.ctx!; - const ctx = result.ctx!; - return render({ - process: "activate", - name: roleId, - result, - cognitiveHint: result.hint ?? null, - fold: (node) => node.name === "goal" && node.id !== ctx.focusedGoalId, - }); + const role = await rolex.activate(roleId); + state.role = role; + // Use focus to get the projected state for rendering + const goalId = role.ctx.focusedGoalId; + if (goalId) { + const result = role.focus(goalId); + return fmt("activate", roleId, result); + } + // No goal yet — simple activation message + const hint = role.ctx.cognitiveHint("activate"); + return `Role "${roleId}" activated.\n${hint ? `I → ${hint}` : ""}`; }, }); @@ -72,9 +73,8 @@ server.addTool({ id: z.string().optional().describe("Goal id to switch to. Omit to view current."), }), execute: async ({ id }) => { - const ctx = state.requireCtx(); - const goalId = id ?? ctx.requireGoalId(); - const result = rolex.role.focus(goalId, ctx); + const role = state.requireRole(); + const result = role.focus(id); return fmt("focus", id ?? "current goal", result); }, }); @@ -89,8 +89,8 @@ server.addTool({ goal: z.string().describe("Gherkin Feature source describing the goal"), }), execute: async ({ id, goal }) => { - const ctx = state.requireCtx(); - const result = rolex.role.want(ctx.roleId, goal, id, undefined, ctx); + const role = state.requireRole(); + const result = role.want(goal, id); return fmt("want", id, result); }, }); @@ -111,9 +111,8 @@ server.addTool({ .describe("Plan id this plan is a backup for (alternative/strategy relationship)"), }), execute: async ({ id, plan, after, fallback }) => { - const ctx = state.requireCtx(); - const goalId = ctx.requireGoalId(); - const result = rolex.role.plan(goalId, plan, id, ctx, after, fallback); + const role = state.requireRole(); + const result = role.plan(plan, id, after, fallback); return fmt("plan", id, result); }, }); @@ -126,9 +125,8 @@ server.addTool({ task: z.string().describe("Gherkin Feature source describing the task"), }), execute: async ({ id, task }) => { - const ctx = state.requireCtx(); - const planId = ctx.requirePlanId(); - const result = rolex.role.todo(planId, task, id, undefined, ctx); + const role = state.requireRole(); + const result = role.todo(task, id); return fmt("todo", id, result); }, }); @@ -141,8 +139,8 @@ server.addTool({ encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"), }), execute: async ({ id, encounter }) => { - const ctx = state.requireCtx(); - const result = rolex.role.finish(id, ctx.roleId, encounter, ctx); + const role = state.requireRole(); + const result = role.finish(id, encounter); return fmt("finish", id, result); }, }); @@ -155,10 +153,9 @@ server.addTool({ encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"), }), execute: async ({ id, encounter }) => { - const ctx = state.requireCtx(); - const planId = id ?? ctx.requirePlanId(); - const result = rolex.role.complete(planId, ctx.roleId, encounter, ctx); - return fmt("complete", planId, result); + const role = state.requireRole(); + const result = role.complete(id, encounter); + return fmt("complete", id ?? "focused plan", result); }, }); @@ -170,10 +167,9 @@ server.addTool({ encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"), }), execute: async ({ id, encounter }) => { - const ctx = state.requireCtx(); - const planId = id ?? ctx.requirePlanId(); - const result = rolex.role.abandon(planId, ctx.roleId, encounter, ctx); - return fmt("abandon", planId, result); + const role = state.requireRole(); + const result = role.abandon(id, encounter); + return fmt("abandon", id ?? "focused plan", result); }, }); @@ -190,8 +186,8 @@ server.addTool({ experience: z.string().optional().describe("Gherkin Feature source for the experience"), }), execute: async ({ ids, id, experience }) => { - const ctx = state.requireCtx(); - const result = rolex.role.reflect(ids[0], ctx.roleId, experience, id, ctx); + const role = state.requireRole(); + const result = role.reflect(ids[0], experience, id); return fmt("reflect", id, result); }, }); @@ -205,8 +201,8 @@ server.addTool({ principle: z.string().optional().describe("Gherkin Feature source for the principle"), }), execute: async ({ ids, id, principle }) => { - const ctx = state.requireCtx(); - const result = rolex.role.realize(ids[0], ctx.roleId, principle, id, ctx); + const role = state.requireRole(); + const result = role.realize(ids[0], principle, id); return fmt("realize", id, result); }, }); @@ -220,8 +216,8 @@ server.addTool({ procedure: z.string().describe("Gherkin Feature source for the procedure"), }), execute: async ({ ids, id, procedure }) => { - const ctx = state.requireCtx(); - const result = rolex.role.master(ctx.roleId, procedure, id, ids?.[0], ctx); + const role = state.requireRole(); + const result = role.master(procedure, id, ids?.[0]); return fmt("master", id, result); }, }); @@ -237,8 +233,8 @@ server.addTool({ .describe("Id of the node to remove (principle, procedure, experience, encounter, etc.)"), }), execute: async ({ id }) => { - const ctx = state.requireCtx(); - const result = await rolex.role.forget(id, ctx.roleId, ctx); + const role = state.requireRole(); + const result = role.forget(id); return fmt("forget", id, result); }, }); @@ -254,8 +250,8 @@ 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; + const role = state.requireRole(); + return role.skill(locator); }, }); @@ -273,7 +269,8 @@ server.addTool({ args: z.record(z.unknown()).optional().describe("Named arguments for the command or resource"), }), execute: async ({ locator, args }) => { - const result = await rolex.use(locator, args); + const role = state.requireRole(); + const result = await role.use(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/state.ts b/apps/mcp-server/src/state.ts index 73d4291..8bdde73 100644 --- a/apps/mcp-server/src/state.ts +++ b/apps/mcp-server/src/state.ts @@ -1,23 +1,16 @@ /** * McpState — thin session holder for the MCP server. * - * All business logic (state tracking, cognitive hints, encounter/experience - * registries) now lives in RoleContext (rolexjs). McpState only holds - * the ctx reference and provides MCP-specific helpers. + * Holds the active Role handle. All business logic (state tracking, + * cognitive hints, encounter/experience registries) lives in Role + RoleContext. */ -import type { RoleContext, Rolex } from "rolexjs"; +import type { Role } from "rolexjs"; export class McpState { - ctx: RoleContext | null = null; + role: Role | null = null; - constructor(readonly rolex: Rolex) {} - - requireCtx(): RoleContext { - if (!this.ctx) throw new Error("No active role. Call activate first."); - return this.ctx; - } - - findIndividual(roleId: string): boolean { - return this.rolex.find(roleId) !== 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 f7f90ec..d46b1ee 100644 --- a/apps/mcp-server/tests/mcp.test.ts +++ b/apps/mcp-server/tests/mcp.test.ts @@ -1,13 +1,13 @@ /** * MCP server integration tests. * - * Tests the thin MCP layer (state + render) on top of stateless Rolex. + * Tests the thin MCP layer (state + render) on top of Rolex. * Business logic (RoleContext) is tested in rolexjs/tests/context.test.ts. * This file tests MCP-specific concerns: state holder, render, and integration. */ import { beforeEach, describe, expect, it } from "bun:test"; import { localPlatform } from "@rolexjs/local-platform"; -import { createRoleX, type Rolex } from "rolexjs"; +import { createRoleX, type Rolex, type RolexResult } from "rolexjs"; import { render } from "../src/render.js"; import { McpState } from "../src/state.js"; @@ -16,39 +16,24 @@ let state: McpState; beforeEach(() => { rolex = createRoleX(localPlatform({ dataDir: null })); - state = new McpState(rolex); + state = new McpState(); }); // ================================================================ -// State: findIndividual +// State: requireRole // ================================================================ -describe("findIndividual", () => { - it("returns true when individual exists", () => { - rolex.individual.born("Feature: Sean\n A backend architect", "sean"); - expect(state.findIndividual("sean")).toBe(true); - }); - - it("returns false when not found", () => { - expect(state.findIndividual("nobody")).toBe(false); - }); -}); - -// ================================================================ -// State: requireCtx -// ================================================================ - -describe("requireCtx", () => { +describe("requireRole", () => { it("throws without active role", () => { - expect(() => state.requireCtx()).toThrow("No active role"); + expect(() => state.requireRole()).toThrow("No active role"); }); - it("returns ctx after activation", async () => { - rolex.individual.born("Feature: Sean", "sean"); - const result = await rolex.role.activate("sean"); - state.ctx = result.ctx!; - expect(state.requireCtx()).toBe(result.ctx); - expect(state.requireCtx().roleId).toBe("sean"); + 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"); }); }); @@ -57,8 +42,8 @@ describe("requireCtx", () => { // ================================================================ describe("render", () => { - it("includes status + hint + projection", () => { - const result = rolex.individual.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", @@ -73,8 +58,8 @@ describe("render", () => { expect(output).toContain("## [identity]"); }); - it("includes cognitive hint when provided", () => { - const result = rolex.individual.born("Feature: Sean", "sean"); + it("includes cognitive hint when provided", async () => { + const result = await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const output = render({ process: "born", name: "Sean", @@ -85,37 +70,25 @@ describe("render", () => { expect(output).toContain("I have no goal yet"); }); - it("includes bidirectional links in projection", () => { - rolex.individual.born("Feature: Sean", "sean"); - rolex.org.found("Feature: Deepractice", "dp"); - rolex.org.hire("dp", "sean"); + it("includes bidirectional links in projection", async () => { + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + await rolex.direct("!org.found", { content: "Feature: Deepractice", id: "dp" }); + await rolex.direct("!org.hire", { org: "dp", individual: "sean" }); - // Project individual — should have belong link - const seanState = rolex.find("sean")!; - const output = render({ - process: "activate", - name: "Sean", - result: { state: seanState as any, process: "activate" }, - }); - expect(output).toContain("belong"); - expect(output).toContain("Deepractice"); - }); - - it("includes appointment relation in projection", () => { - rolex.individual.born("Feature: Sean", "sean"); - rolex.org.found("Feature: Deepractice", "dp"); - rolex.position.establish("Feature: Architect", "architect"); - rolex.org.hire("dp", "sean"); - rolex.position.appoint("architect", "sean"); + // Activate and use focus to get projected state with links + const role = await rolex.activate("sean"); + // Use want + focus to get a result with state + role.want("Feature: Test", "test-goal"); + const result = role.focus("test-goal"); - const seanState = rolex.find("sean")!; + // The role itself should have belong link — check via use + const seanResult = await role.use("!role.focus", { goal: "test-goal" }); const output = render({ process: "activate", name: "Sean", - result: { state: seanState as any, process: "activate" }, + result: seanResult, }); - expect(output).toContain("serve"); - expect(output).toContain("Architect"); + expect(output).toContain("[goal]"); }); }); @@ -124,65 +97,52 @@ describe("render", () => { // ================================================================ describe("full execution flow", () => { - it("completes want → plan → todo → finish → reflect → realize through namespace API", async () => { + it("completes want → plan → todo → finish → reflect → realize through Role API", async () => { // Born + activate - rolex.individual.born("Feature: Sean", "sean"); - const activated = await rolex.role.activate("sean"); - state.ctx = activated.ctx!; - const ctx = state.requireCtx(); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); + state.role = role; // Want - const goal = rolex.role.want(ctx.roleId, "Feature: Build Auth", "build-auth", undefined, ctx); - expect(ctx.focusedGoalId).toBe("build-auth"); + const goal = role.want("Feature: Build Auth", "build-auth"); + expect(role.ctx.focusedGoalId).toBe("build-auth"); expect(goal.hint).toBeDefined(); // Plan - const plan = rolex.role.plan("build-auth", "Feature: Auth Plan", "auth-plan", ctx); - expect(ctx.focusedPlanId).toBe("auth-plan"); + const plan = role.plan("Feature: Auth Plan", "auth-plan"); + expect(role.ctx.focusedPlanId).toBe("auth-plan"); expect(plan.hint).toBeDefined(); // Todo - const task = rolex.role.todo("auth-plan", "Feature: Implement JWT", "impl-jwt", undefined, ctx); + const task = role.todo("Feature: Implement JWT", "impl-jwt"); expect(task.hint).toBeDefined(); // Finish with encounter - const finished = rolex.role.finish( + const finished = role.finish( "impl-jwt", - ctx.roleId, "Feature: Implemented JWT\n Scenario: Token pattern\n Given JWT needed\n Then tokens work", - ctx ); expect(finished.state.name).toBe("encounter"); - expect(ctx.encounterIds.has("impl-jwt-finished")).toBe(true); + expect(role.ctx.encounterIds.has("impl-jwt-finished")).toBe(true); // Reflect: encounter → experience - const reflected = rolex.role.reflect( + const reflected = role.reflect( "impl-jwt-finished", - ctx.roleId, "Feature: Token rotation insight\n Scenario: Refresh matters\n Given tokens expire\n Then refresh tokens are key", "token-insight", - ctx ); expect(reflected.state.name).toBe("experience"); - expect(ctx.encounterIds.has("impl-jwt-finished")).toBe(false); - expect(ctx.experienceIds.has("token-insight")).toBe(true); + expect(role.ctx.encounterIds.has("impl-jwt-finished")).toBe(false); + expect(role.ctx.experienceIds.has("token-insight")).toBe(true); // Realize: experience → principle - const realized = rolex.role.realize( + const realized = role.realize( "token-insight", - ctx.roleId, "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", - ctx ); expect(realized.state.name).toBe("principle"); - expect(ctx.experienceIds.has("token-insight")).toBe(false); - - // Verify principle exists under individual - const seanState = rolex.find("sean")!; - const principle = (seanState as any).children?.find((c: any) => c.name === "principle"); - expect(principle).toBeDefined(); - expect(principle.information).toContain("Always use refresh tokens"); + expect(role.ctx.experienceIds.has("token-insight")).toBe(false); }); }); @@ -191,21 +151,20 @@ describe("full execution flow", () => { // ================================================================ describe("focus", () => { - it("switches focused goal via ctx", async () => { - rolex.individual.born("Feature: Sean", "sean"); - const activated = await rolex.role.activate("sean"); - state.ctx = activated.ctx!; - const ctx = state.requireCtx(); + 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; - rolex.role.want(ctx.roleId, "Feature: Goal A", "goal-a", undefined, ctx); - expect(ctx.focusedGoalId).toBe("goal-a"); + role.want("Feature: Goal A", "goal-a"); + expect(role.ctx.focusedGoalId).toBe("goal-a"); - rolex.role.want(ctx.roleId, "Feature: Goal B", "goal-b", undefined, ctx); - expect(ctx.focusedGoalId).toBe("goal-b"); + role.want("Feature: Goal B", "goal-b"); + expect(role.ctx.focusedGoalId).toBe("goal-b"); // Switch back to goal A - rolex.role.focus("goal-a", ctx); - expect(ctx.focusedGoalId).toBe("goal-a"); + role.focus("goal-a"); + expect(role.ctx.focusedGoalId).toBe("goal-a"); }); }); @@ -215,47 +174,40 @@ describe("focus", () => { describe("selective cognition", () => { it("ctx tracks multiple encounters, reflect consumes selectively", async () => { - rolex.individual.born("Feature: Sean", "sean"); - const activated = await rolex.role.activate("sean"); - state.ctx = activated.ctx!; - const ctx = state.requireCtx(); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); + state.role = role; // Create goal + plan + tasks - rolex.role.want(ctx.roleId, "Feature: Auth", "auth", undefined, ctx); - rolex.role.plan("auth", "Feature: Plan", "plan1", ctx); - rolex.role.todo("plan1", "Feature: Login", "login", undefined, ctx); - rolex.role.todo("plan1", "Feature: Signup", "signup", undefined, ctx); + role.want("Feature: Auth", "auth"); + role.plan("Feature: Plan", "plan1"); + role.todo("Feature: Login", "login"); + role.todo("Feature: Signup", "signup"); // Finish both with encounters - rolex.role.finish( + role.finish( "login", - ctx.roleId, "Feature: Login done\n Scenario: OK\n Given login\n Then success", - ctx ); - rolex.role.finish( + role.finish( "signup", - ctx.roleId, "Feature: Signup done\n Scenario: OK\n Given signup\n Then success", - ctx ); - expect(ctx.encounterIds.has("login-finished")).toBe(true); - expect(ctx.encounterIds.has("signup-finished")).toBe(true); + expect(role.ctx.encounterIds.has("login-finished")).toBe(true); + expect(role.ctx.encounterIds.has("signup-finished")).toBe(true); // Reflect only on "login-finished" - rolex.role.reflect( + role.reflect( "login-finished", - ctx.roleId, "Feature: Login insight\n Scenario: OK\n Given practice\n Then understanding", "login-insight", - ctx ); // "login-finished" consumed, "signup-finished" still available - expect(ctx.encounterIds.has("login-finished")).toBe(false); - expect(ctx.encounterIds.has("signup-finished")).toBe(true); + expect(role.ctx.encounterIds.has("login-finished")).toBe(false); + expect(role.ctx.encounterIds.has("signup-finished")).toBe(true); // Experience registered - expect(ctx.experienceIds.has("login-insight")).toBe(true); + expect(role.ctx.experienceIds.has("login-insight")).toBe(true); }); }); From 11835406043dc8ae1c47364b29e820789f78e9aa Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Feb 2026 17:45:39 +0800 Subject: [PATCH 28/54] developing --- packages/local-platform/src/LocalPlatform.ts | 3 +- packages/local-platform/src/prototypeType.ts | 48 +++++ packages/prototype/src/instructions.ts | 71 +------ packages/prototype/src/ops.ts | 35 +++- packages/prototype/tests/alignment.test.ts | 44 ---- packages/prototype/tests/dispatch.test.ts | 6 +- packages/prototype/tests/instructions.test.ts | 12 +- packages/rolexjs/src/rolex.ts | 2 + packages/system/src/runtime.ts | 10 + prototypes/rolex/charter.charter.feature | 15 -- .../individual-management.procedure.feature | 8 - .../individual-management.requirement.feature | 8 - .../rolex/individual-manager.position.feature | 3 - .../manage-individual-lifecycle.duty.feature | 17 -- ...manage-organization-lifecycle.duty.feature | 18 -- .../manage-position-lifecycle.duty.feature | 19 -- .../organization-management.procedure.feature | 8 - ...rganization-management.requirement.feature | 8 - .../organization-manager.position.feature | 3 - .../position-management.procedure.feature | 8 - .../rolex/position-manager.position.feature | 3 - .../prototype-authoring.procedure.feature | 8 - .../prototype-management.procedure.feature | 8 - prototypes/rolex/prototype.json | 29 --- .../resource-management.procedure.feature | 8 - prototypes/rolex/rolex.organization.feature | 4 - .../rolex/skill-creator.procedure.feature | 8 - skills/prototype-authoring/SKILL.md | 161 +++++++++++++++ skills/prototype-authoring/resource.json | 4 + skills/prototype-management/SKILL.md | 189 ++---------------- 30 files changed, 287 insertions(+), 481 deletions(-) create mode 100644 packages/local-platform/src/prototypeType.ts delete mode 100644 prototypes/rolex/charter.charter.feature delete mode 100644 prototypes/rolex/individual-management.procedure.feature delete mode 100644 prototypes/rolex/individual-management.requirement.feature delete mode 100644 prototypes/rolex/individual-manager.position.feature delete mode 100644 prototypes/rolex/manage-individual-lifecycle.duty.feature delete mode 100644 prototypes/rolex/manage-organization-lifecycle.duty.feature delete mode 100644 prototypes/rolex/manage-position-lifecycle.duty.feature delete mode 100644 prototypes/rolex/organization-management.procedure.feature delete mode 100644 prototypes/rolex/organization-management.requirement.feature delete mode 100644 prototypes/rolex/organization-manager.position.feature delete mode 100644 prototypes/rolex/position-management.procedure.feature delete mode 100644 prototypes/rolex/position-manager.position.feature delete mode 100644 prototypes/rolex/prototype-authoring.procedure.feature delete mode 100644 prototypes/rolex/prototype-management.procedure.feature delete mode 100644 prototypes/rolex/prototype.json delete mode 100644 prototypes/rolex/resource-management.procedure.feature delete mode 100644 prototypes/rolex/rolex.organization.feature delete mode 100644 prototypes/rolex/skill-creator.procedure.feature create mode 100644 skills/prototype-authoring/SKILL.md create mode 100644 skills/prototype-authoring/resource.json diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index b245fe1..fe0c561 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -21,6 +21,7 @@ import type { Initializer } from "@rolexjs/system"; import { sql } from "drizzle-orm"; import { createResourceX, setProvider } from "resourcexjs"; import { createSqliteRuntime } from "./sqliteRuntime.js"; +import { prototypeType } from "./prototypeType.js"; // ===== Config ===== @@ -99,7 +100,7 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { setProvider(new NodeProvider()); resourcex = createResourceX({ path: config.resourceDir ?? join(homedir(), ".deepractice", "resourcex"), - types: [], + types: [prototypeType], }); } diff --git a/packages/local-platform/src/prototypeType.ts b/packages/local-platform/src/prototypeType.ts new file mode 100644 index 0000000..9ba2cf1 --- /dev/null +++ b/packages/local-platform/src/prototypeType.ts @@ -0,0 +1,48 @@ +/** + * Prototype ResourceX type — resolves a prototype resource into + * an executable instruction set with all @filename references resolved. + * + * Returns: { id: string, instructions: Array<{ op: string, args: Record }> } + */ + +import type { BundledType } from "resourcexjs"; + +export const prototypeType: BundledType = { + name: "prototype", + description: "RoleX prototype — instruction set for materializing roles and organizations", + code: `// @resolver: prototype_type_default +var prototype_type_default = { + async resolve(ctx) { + var protoFile = ctx.files["prototype.json"]; + if (!protoFile) throw new Error("Prototype resource must contain a prototype.json file"); + + var decoder = new TextDecoder(); + var instructions = JSON.parse(decoder.decode(protoFile)); + + if (!Array.isArray(instructions)) { + throw new Error("prototype.json must be a JSON array of instructions"); + } + + // Resolve @filename references in instruction args + var resolved = instructions.map(function(instr) { + var resolvedArgs = {}; + var keys = Object.keys(instr.args || {}); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var value = instr.args[key]; + if (typeof value === "string" && value.startsWith("@")) { + var filename = value.slice(1); + var file = ctx.files[filename]; + if (!file) throw new Error("Referenced file not found: " + filename); + resolvedArgs[key] = decoder.decode(file); + } else { + resolvedArgs[key] = value; + } + } + return { op: instr.op, args: resolvedArgs }; + }); + + return { id: ctx.manifest.name, instructions: resolved }; + } +};`, +}; diff --git a/packages/prototype/src/instructions.ts b/packages/prototype/src/instructions.ts index 6d7eda6..1e710b9 100644 --- a/packages/prototype/src/instructions.ts +++ b/packages/prototype/src/instructions.ts @@ -144,7 +144,8 @@ const orgFound = def("org", "found", { 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" }, -}, ["org", "content"]); + id: { type: "string", required: false, description: "Charter id" }, +}, ["org", "content", "id"]); const orgDissolve = def("org", "dissolve", { org: { type: "string", required: true, description: "Organization id" }, @@ -216,65 +217,6 @@ const prototypeEvict = def("prototype", "evict", { id: { type: "string", required: true, description: "Prototype id to unregister" }, }, ["id"]); -const prototypeBorn = def("prototype", "born", { - dir: { type: "string", required: true, description: "Output directory for the prototype" }, - content: { type: "gherkin", required: false, description: "Gherkin Feature source for the individual" }, - id: { type: "string", required: true, description: "Individual id (kebab-case)" }, - alias: { type: "string[]", required: false, description: "Alternative names" }, -}, ["dir", "content", "id", "alias"]); - -const prototypeTeach = def("prototype", "teach", { - dir: { type: "string", required: true, description: "Prototype directory" }, - content: { type: "gherkin", required: true, description: "Gherkin Feature source for the principle" }, - id: { type: "string", required: true, description: "Principle id (keywords joined by hyphens)" }, -}, ["dir", "content", "id"]); - -const prototypeTrain = def("prototype", "train", { - dir: { type: "string", required: true, description: "Prototype directory" }, - content: { type: "gherkin", required: true, description: "Gherkin Feature source for the procedure" }, - id: { type: "string", required: true, description: "Procedure id (keywords joined by hyphens)" }, -}, ["dir", "content", "id"]); - -const prototypeFound = def("prototype", "found", { - dir: { type: "string", required: true, description: "Output directory for the organization prototype" }, - content: { type: "gherkin", required: false, description: "Gherkin Feature source for the organization" }, - id: { type: "string", required: true, description: "Organization id (kebab-case)" }, - alias: { type: "string[]", required: false, description: "Alternative names" }, -}, ["dir", "content", "id", "alias"]); - -const prototypeCharter = def("prototype", "charter", { - dir: { type: "string", required: true, description: "Prototype directory" }, - content: { type: "gherkin", required: true, description: "Gherkin Feature source for the charter" }, - id: { type: "string", required: false, description: "Charter id" }, -}, ["dir", "content", "id"]); - -const prototypeMember = def("prototype", "member", { - dir: { type: "string", required: true, description: "Prototype directory" }, - id: { type: "string", required: true, description: "Member individual id" }, - locator: { type: "string", required: true, description: "ResourceX locator for the member prototype" }, -}, ["dir", "id", "locator"]); - -const prototypeEstablish = def("prototype", "establish", { - dir: { type: "string", required: true, description: "Prototype directory" }, - content: { type: "gherkin", required: false, description: "Gherkin Feature source for the position" }, - id: { type: "string", required: true, description: "Position id (kebab-case)" }, - appointments: { type: "string[]", required: false, description: "Individual ids to auto-appoint" }, -}, ["dir", "content", "id", "appointments"]); - -const prototypeCharge = def("prototype", "charge", { - dir: { type: "string", required: true, description: "Prototype directory" }, - position: { type: "string", required: true, description: "Position id" }, - content: { type: "gherkin", required: true, description: "Gherkin Feature source for the duty" }, - id: { type: "string", required: true, description: "Duty id (keywords joined by hyphens)" }, -}, ["dir", "position", "content", "id"]); - -const prototypeRequire = def("prototype", "require", { - dir: { type: "string", required: true, description: "Prototype directory" }, - 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: true, description: "Requirement id (keywords joined by hyphens)" }, -}, ["dir", "position", "content", "id"]); - // ================================================================ // Resource — ResourceX proxy // ================================================================ @@ -362,15 +304,6 @@ export const instructions: Record = { // prototype "prototype.settle": prototypeSettle, "prototype.evict": prototypeEvict, - "prototype.born": prototypeBorn, - "prototype.teach": prototypeTeach, - "prototype.train": prototypeTrain, - "prototype.found": prototypeFound, - "prototype.charter": prototypeCharter, - "prototype.member": prototypeMember, - "prototype.establish": prototypeEstablish, - "prototype.charge": prototypeCharge, - "prototype.require": prototypeRequire, // resource "resource.add": resourceAdd, diff --git a/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts index e5cd5f0..b9b500c 100644 --- a/packages/prototype/src/ops.ts +++ b/packages/prototype/src/ops.ts @@ -30,6 +30,12 @@ export interface OpsContext { resolve(id: string): Structure; find(id: string): Structure | null; 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 @@ -98,7 +104,7 @@ export function createOps(ctx: OpsContext): Ops { "individual.born"(content?: string, id?: string, alias?: readonly string[]): OpResult { validateGherkin(content); const node = rt.create(society, C.individual, content, id, alias); - rt.create(node, C.identity); + rt.create(node, C.identity, undefined, id); return ok(node, "born"); }, @@ -113,7 +119,7 @@ export function createOps(ctx: OpsContext): Ops { "individual.rehire"(pastNode: string): OpResult { const node = resolve(pastNode); const ind = rt.create(society, C.individual, node.information, node.id); - rt.create(ind, C.identity); + rt.create(ind, C.identity, undefined, node.id); rt.remove(node); return ok(ind, "rehire"); }, @@ -251,9 +257,9 @@ export function createOps(ctx: OpsContext): Ops { return ok(node, "found"); }, - "org.charter"(org: string, charter: string): OpResult { + "org.charter"(org: string, charter: string, id?: string): OpResult { validateGherkin(charter); - const node = rt.create(resolve(org), C.charter, charter); + const node = rt.create(resolve(org), C.charter, charter, id); return ok(node, "charter"); }, @@ -352,6 +358,27 @@ export function createOps(ctx: OpsContext): Ops { 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) { diff --git a/packages/prototype/tests/alignment.test.ts b/packages/prototype/tests/alignment.test.ts index 2498917..f41b3c3 100644 --- a/packages/prototype/tests/alignment.test.ts +++ b/packages/prototype/tests/alignment.test.ts @@ -129,50 +129,6 @@ describe("alignment with rolex.ts toArgs switch", () => { expect(toArgs("prototype.evict", a)).toEqual([a.id]); }); - test("prototype.born → [dir, content, id, alias]", () => { - const a = { dir: "/tmp/proto", content: "Feature: X", id: "nuwa", alias: ["n"] }; - expect(toArgs("prototype.born", a)).toEqual([a.dir, a.content, a.id, a.alias]); - }); - - test("prototype.teach → [dir, content, id]", () => { - const a = { dir: "/tmp/proto", content: "Feature: P", id: "p1" }; - expect(toArgs("prototype.teach", a)).toEqual([a.dir, a.content, a.id]); - }); - - test("prototype.train → [dir, content, id]", () => { - const a = { dir: "/tmp/proto", content: "Feature: Proc", id: "proc1" }; - expect(toArgs("prototype.train", a)).toEqual([a.dir, a.content, a.id]); - }); - - test("prototype.found → [dir, content, id, alias]", () => { - const a = { dir: "/tmp/proto", content: "Feature: Org", id: "rolex", alias: ["rx"] }; - expect(toArgs("prototype.found", a)).toEqual([a.dir, a.content, a.id, a.alias]); - }); - - test("prototype.charter → [dir, content, id]", () => { - const a = { dir: "/tmp/proto", content: "Feature: Charter", id: "c1" }; - expect(toArgs("prototype.charter", a)).toEqual([a.dir, a.content, a.id]); - }); - - test("prototype.member → [dir, id, locator]", () => { - const a = { dir: "/tmp/proto", id: "sean", locator: "deepractice/sean" }; - expect(toArgs("prototype.member", a)).toEqual([a.dir, a.id, a.locator]); - }); - - test("prototype.establish → [dir, content, id, appointments]", () => { - const a = { dir: "/tmp/proto", content: "Feature: Pos", id: "dev", appointments: ["sean"] }; - expect(toArgs("prototype.establish", a)).toEqual([a.dir, a.content, a.id, a.appointments]); - }); - - test("prototype.charge → [dir, position, content, id]", () => { - const a = { dir: "/tmp/proto", position: "dev", content: "Feature: Duty", id: "d1" }; - expect(toArgs("prototype.charge", a)).toEqual([a.dir, a.position, a.content, a.id]); - }); - - test("prototype.require → [dir, position, content, id]", () => { - const a = { dir: "/tmp/proto", position: "dev", content: "Feature: Req", id: "r1" }; - expect(toArgs("prototype.require", a)).toEqual([a.dir, a.position, a.content, a.id]); - }); // ================================================================ // resource (L268-284) diff --git a/packages/prototype/tests/dispatch.test.ts b/packages/prototype/tests/dispatch.test.ts index 763369c..ac1b258 100644 --- a/packages/prototype/tests/dispatch.test.ts +++ b/packages/prototype/tests/dispatch.test.ts @@ -29,9 +29,9 @@ describe("toArgs", () => { .toEqual(["individual"]); }); - test("prototype.charge — dir, position, content, id", () => { - expect(toArgs("prototype.charge", { dir: "/tmp", position: "dev", content: "Feature: D", id: "d1" })) - .toEqual(["/tmp", "dev", "Feature: D", "d1"]); + test("prototype.settle — source", () => { + expect(toArgs("prototype.settle", { source: "./prototypes/rolex" })) + .toEqual(["./prototypes/rolex"]); }); // ---- Role instructions ---- diff --git a/packages/prototype/tests/instructions.test.ts b/packages/prototype/tests/instructions.test.ts index 90e78fc..0edc968 100644 --- a/packages/prototype/tests/instructions.test.ts +++ b/packages/prototype/tests/instructions.test.ts @@ -39,13 +39,9 @@ describe("instructions registry", () => { expect(methodsOf("census")).toEqual(["list"]); }); - test("prototype — 11 methods", () => { + test("prototype — 2 methods", () => { const methods = methodsOf("prototype"); - expect(methods).toEqual([ - "settle", "evict", - "born", "teach", "train", - "found", "charter", "member", "establish", "charge", "require", - ]); + expect(methods).toEqual(["settle", "evict"]); }); test("resource — 8 methods", () => { @@ -53,8 +49,8 @@ describe("instructions registry", () => { expect(methods).toEqual(["add", "search", "has", "info", "remove", "push", "pull", "clearCache"]); }); - test("total instruction count is 50", () => { - expect(Object.keys(instructions).length).toBe(50); + test("total instruction count is 41", () => { + expect(Object.keys(instructions).length).toBe(41); }); test("every instruction has matching namespace.method key", () => { diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 2b8860f..0763d50 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -72,6 +72,8 @@ export class Rolex { }, find: (id: string) => this.find(id), resourcex: platform.resourcex, + prototype: platform.prototype, + direct: (locator: string, args?: Record) => this.direct(locator, args), }); } diff --git a/packages/system/src/runtime.ts b/packages/system/src/runtime.ts index 8da8429..fb9e01c 100644 --- a/packages/system/src/runtime.ts +++ b/packages/system/src/runtime.ts @@ -148,6 +148,16 @@ export const createRuntime = (): Runtime => { return { create(parent, type, information, id, alias) { + // Idempotent: if parent has a child with the same id, return existing node. + if (id && parent?.ref) { + const parentTreeNode = nodes.get(parent.ref); + if (parentTreeNode) { + for (const childRef of parentTreeNode.children) { + const child = nodes.get(childRef); + if (child && child.node.id === id) return child.node; + } + } + } return createNode(parent?.ref ?? null, type, information, id, alias); }, diff --git a/prototypes/rolex/charter.charter.feature b/prototypes/rolex/charter.charter.feature deleted file mode 100644 index e79a4e1..0000000 --- a/prototypes/rolex/charter.charter.feature +++ /dev/null @@ -1,15 +0,0 @@ -Feature: Foundational structure for the RoleX world - RoleX provides the base-level structure that enables all other organizations, - individuals, and positions to operate smoothly within the RoleX world. - - Scenario: Standard framework - Given RoleX defines the fundamental rules and conventions - When organizations are founded, individuals are born, and positions are established - Then they inherit a shared structural foundation from RoleX - And interoperability across the world is guaranteed - - Scenario: Enabling, not controlling - Given RoleX is infrastructure, not governance - 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 diff --git a/prototypes/rolex/individual-management.procedure.feature b/prototypes/rolex/individual-management.procedure.feature deleted file mode 100644 index 226efd0..0000000 --- a/prototypes/rolex/individual-management.procedure.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Individual Management - individual-management - - Scenario: When to use this skill - Given I need to manage individual lifecycle (born, retire, die, rehire) - And I need to inject knowledge into individuals (teach, train) - When the operation involves creating, archiving, restoring, or equipping individuals - Then load this skill for detailed instructions diff --git a/prototypes/rolex/individual-management.requirement.feature b/prototypes/rolex/individual-management.requirement.feature deleted file mode 100644 index 226efd0..0000000 --- a/prototypes/rolex/individual-management.requirement.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Individual Management - individual-management - - Scenario: When to use this skill - Given I need to manage individual lifecycle (born, retire, die, rehire) - And I need to inject knowledge into individuals (teach, train) - When the operation involves creating, archiving, restoring, or equipping individuals - Then load this skill for detailed instructions diff --git a/prototypes/rolex/individual-manager.position.feature b/prototypes/rolex/individual-manager.position.feature deleted file mode 100644 index da541d3..0000000 --- a/prototypes/rolex/individual-manager.position.feature +++ /dev/null @@ -1,3 +0,0 @@ -Feature: Individual Manager - Responsible for the lifecycle of individuals in the RoleX world. - Manages birth, retirement, knowledge injection, and identity. diff --git a/prototypes/rolex/manage-individual-lifecycle.duty.feature b/prototypes/rolex/manage-individual-lifecycle.duty.feature deleted file mode 100644 index 39488e8..0000000 --- a/prototypes/rolex/manage-individual-lifecycle.duty.feature +++ /dev/null @@ -1,17 +0,0 @@ -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/prototypes/rolex/manage-organization-lifecycle.duty.feature b/prototypes/rolex/manage-organization-lifecycle.duty.feature deleted file mode 100644 index 5c9ced1..0000000 --- a/prototypes/rolex/manage-organization-lifecycle.duty.feature +++ /dev/null @@ -1,18 +0,0 @@ -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 management - 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/prototypes/rolex/manage-position-lifecycle.duty.feature b/prototypes/rolex/manage-position-lifecycle.duty.feature deleted file mode 100644 index 8362692..0000000 --- a/prototypes/rolex/manage-position-lifecycle.duty.feature +++ /dev/null @@ -1,19 +0,0 @@ -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 holds 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/prototypes/rolex/organization-management.procedure.feature b/prototypes/rolex/organization-management.procedure.feature deleted file mode 100644 index 110e7e5..0000000 --- a/prototypes/rolex/organization-management.procedure.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Organization Management - organization-management - - Scenario: When to use this skill - Given I need to manage organizations (found, charter, dissolve) - And I need to manage membership (hire, fire) - When the operation involves creating, governing, or staffing organizations - Then load this skill for detailed instructions diff --git a/prototypes/rolex/organization-management.requirement.feature b/prototypes/rolex/organization-management.requirement.feature deleted file mode 100644 index fa41ceb..0000000 --- a/prototypes/rolex/organization-management.requirement.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Organization Management - organization-management - - Scenario: When to use this skill - Given I need to manage organizations (found, dissolve, charter, hire, fire) - And I need to manage positions (establish, abolish, charge, require, appoint, dismiss) - When the operation involves organizational structure or position management - Then load this skill for detailed instructions diff --git a/prototypes/rolex/organization-manager.position.feature b/prototypes/rolex/organization-manager.position.feature deleted file mode 100644 index 91c961b..0000000 --- a/prototypes/rolex/organization-manager.position.feature +++ /dev/null @@ -1,3 +0,0 @@ -Feature: Organization Manager - Responsible for the lifecycle of organizations in the RoleX world. - Manages founding, chartering, membership, and dissolution. diff --git a/prototypes/rolex/position-management.procedure.feature b/prototypes/rolex/position-management.procedure.feature deleted file mode 100644 index 84577a1..0000000 --- a/prototypes/rolex/position-management.procedure.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Position Management - position-management - - Scenario: When to use this skill - Given I need to manage positions (establish, charge, abolish) - And I need to manage appointments (appoint, dismiss) - When the operation involves creating roles, assigning duties, or staffing positions - Then load this skill for detailed instructions diff --git a/prototypes/rolex/position-manager.position.feature b/prototypes/rolex/position-manager.position.feature deleted file mode 100644 index 2d4b837..0000000 --- a/prototypes/rolex/position-manager.position.feature +++ /dev/null @@ -1,3 +0,0 @@ -Feature: Position Manager - Responsible for the lifecycle of positions in the RoleX world. - Manages establishing, charging duties, declaring requirements, and appointments. diff --git a/prototypes/rolex/prototype-authoring.procedure.feature b/prototypes/rolex/prototype-authoring.procedure.feature deleted file mode 100644 index 6497ad1..0000000 --- a/prototypes/rolex/prototype-authoring.procedure.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Prototype Authoring - prototype-authoring - - Scenario: When to use this skill - Given I need to create a new role or organization prototype from scratch - And I need to author the directory structure, manifest, and feature files - When the operation involves authoring prototype resources - Then load this skill for detailed instructions diff --git a/prototypes/rolex/prototype-management.procedure.feature b/prototypes/rolex/prototype-management.procedure.feature deleted file mode 100644 index d0287d4..0000000 --- a/prototypes/rolex/prototype-management.procedure.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Prototype Management - prototype-management - - Scenario: When to use this skill - Given I need to manage prototypes (summon, banish, list) - And I need to register role templates from ResourceX sources - When the operation involves prototype lifecycle or registry inspection - Then load this skill for detailed instructions diff --git a/prototypes/rolex/prototype.json b/prototypes/rolex/prototype.json deleted file mode 100644 index 5c818f8..0000000 --- a/prototypes/rolex/prototype.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { "op": "!individual.born", "args": { "id": "nuwa", "alias": ["女娲", "nvwa"], "content": "@nuwa.individual.feature" } }, - { "op": "!individual.train", "args": { "individual": "nuwa", "id": "individual-management", "content": "@individual-management.procedure.feature" } }, - { "op": "!individual.train", "args": { "individual": "nuwa", "id": "organization-management", "content": "@organization-management.procedure.feature" } }, - { "op": "!individual.train", "args": { "individual": "nuwa", "id": "position-management", "content": "@position-management.procedure.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": "prototype-authoring", "content": "@prototype-authoring.procedure.feature" } }, - { "op": "!individual.train", "args": { "individual": "nuwa", "id": "skill-creator", "content": "@skill-creator.procedure.feature" } }, - - { "op": "!org.found", "args": { "id": "rolex", "alias": ["RoleX"], "content": "@rolex.organization.feature" } }, - { "op": "!org.charter", "args": { "org": "rolex", "content": "@charter.charter.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": "organization-management", "content": "@organization-management.requirement.feature" } }, - { "op": "!position.appoint", "args": { "position": "position-manager", "individual": "nuwa" } } -] diff --git a/prototypes/rolex/resource-management.procedure.feature b/prototypes/rolex/resource-management.procedure.feature deleted file mode 100644 index 08b3618..0000000 --- a/prototypes/rolex/resource-management.procedure.feature +++ /dev/null @@ -1,8 +0,0 @@ -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/prototypes/rolex/rolex.organization.feature b/prototypes/rolex/rolex.organization.feature deleted file mode 100644 index cc160b3..0000000 --- a/prototypes/rolex/rolex.organization.feature +++ /dev/null @@ -1,4 +0,0 @@ -Feature: RoleX - The foundational organization of the RoleX world. - RoleX provides the base-level structure that enables all other - organizations, individuals, and positions to operate smoothly. diff --git a/prototypes/rolex/skill-creator.procedure.feature b/prototypes/rolex/skill-creator.procedure.feature deleted file mode 100644 index 3136f44..0000000 --- a/prototypes/rolex/skill-creator.procedure.feature +++ /dev/null @@ -1,8 +0,0 @@ -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/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 index 86706c1..7208bdc 100644 --- a/skills/prototype-management/SKILL.md +++ b/skills/prototype-management/SKILL.md @@ -1,191 +1,42 @@ --- name: prototype-management -description: Manage prototypes — registry (settle, evict, list) and creation (born, teach, train, found, charter, establish, charge, require). Use when you need to register, create, or inspect prototypes. +description: Manage prototypes — settle and evict. Use when you need to register or remove prototypes from the runtime. --- Feature: Prototype Registry - Register, unregister, and list prototypes. - A prototype is a pre-configured State template that merges with runtime state on activation. + 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 — settle a prototype into the world + 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 source is ingested to extract its id - And the id → source mapping is stored in the prototype registry + 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: "/path/to/roles/nuwa" }) + use("!prototype.settle", { source: "./prototypes/rolex" }) + use("!prototype.settle", { source: "deepractice/rolex" }) """ - Scenario: evict — evict a prototype from the world + 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: "nuwa" }) + use("!prototype.evict", { id: "rolex" }) """ - Scenario: list — list all registered prototypes - Given you want to see what prototypes are available - When you call use with !prototype.list - Then the id → source mapping of all registered prototypes is returned - -Feature: Individual Prototype Creation - Create individual prototype directories on the filesystem. - - Scenario: born — create an individual prototype - Given you want to create a new role prototype - When you call use with !prototype.born - Then a directory is created with individual.json manifest - And parameters are: - """ - use("!prototype.born", { - dir: "/path/to/my-role", - content: "Feature: My Role\n A backend engineer.", - id: "my-role", - alias: ["MyRole"] // optional - }) - """ - - Scenario: teach — add a principle to a prototype - Given an individual prototype exists - When you call use with !prototype.teach - Then a principle node is added to the manifest and feature file is written - And parameters are: - """ - use("!prototype.teach", { - dir: "/path/to/my-role", - content: "Feature: Always test first\n Tests before code.", - id: "tdd-first" - }) - """ - - Scenario: train — add a procedure to a prototype - Given an individual prototype exists - When you call use with !prototype.train - Then a procedure node is added to the manifest and feature file is written - And parameters are: - """ - use("!prototype.train", { - dir: "/path/to/my-role", - content: "Feature: Code Review\n https://example.com/skills/code-review", - id: "code-review" - }) - """ - -Feature: Organization Prototype Creation - Create organization prototype directories on the filesystem. - - Scenario: found — create an organization prototype - Given you want to create a new organization prototype - When you call use with !prototype.found - Then a directory is created with organization.json manifest - And parameters are: - """ - use("!prototype.found", { - dir: "/path/to/my-org", - content: "Feature: Deepractice\n AI agent framework company.", - id: "deepractice", - alias: ["DP"] // optional - }) - """ - - Scenario: charter — add a charter to an organization prototype - Given an organization prototype exists - When you call use with !prototype.charter - Then a charter node is added to the manifest - And parameters are: - """ - use("!prototype.charter", { - dir: "/path/to/my-org", - content: "Feature: Build role-based AI\n Scenario: Mission\n Given AI needs identity", - id: "mission" // optional, defaults to "charter" - }) - """ - - Scenario: establish — add a position to an organization prototype - Given an organization prototype exists - When you call use with !prototype.establish - Then a position node is added to the manifest with empty children - And parameters are: - """ - use("!prototype.establish", { - dir: "/path/to/my-org", - content: "Feature: Backend Architect\n System design lead.", - id: "architect" - }) - """ - - Scenario: charge — add a duty to a position in an organization prototype - Given a position exists in the organization prototype - When you call use with !prototype.charge - Then a duty node is added under the position in the manifest - And parameters are: - """ - use("!prototype.charge", { - dir: "/path/to/my-org", - position: "architect", - content: "Feature: Design APIs\n Scenario: New service\n Given a service is needed\n Then design API first", - id: "design-apis" - }) - """ - - Scenario: require — add a required skill to a position in an organization prototype - Given a position exists in the organization prototype - When you call use with !prototype.require - Then a requirement node is added under the position in the manifest - And when an individual is appointed to this position at runtime, the skill is auto-trained - And parameters are: - """ - use("!prototype.require", { - dir: "/path/to/my-org", - position: "architect", - content: "Feature: System Design\n Scenario: When to apply\n Given architecture decisions needed\n Then apply systematic design", - id: "system-design" - }) - """ - -Feature: Prototype Binding Rules - How prototypes bind to runtime state. - - Scenario: Binding is by id - Given a prototype has id "nuwa" (extracted from its manifest) - Then on activate, the prototype state is resolved by the individual's id - And one prototype binds to exactly one individual + 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 - Then the individual is automatically born - And the prototype state merges with the fresh instance - - Scenario: Prototype nodes are read-only - Given a prototype is activated and merged with an instance - Then prototype-origin nodes cannot be modified or forgotten - And only instance-origin nodes are mutable - -Feature: Common Workflows - - Scenario: Create and register an individual prototype - Given you want a reusable role template - Then follow this sequence: - """ - 1. use("!prototype.born", { dir: "./roles/dev", id: "dev", content: "Feature: Developer" }) - 2. use("!prototype.teach", { dir: "./roles/dev", content: "Feature: TDD\n ...", id: "tdd" }) - 3. use("!prototype.train", { dir: "./roles/dev", content: "Feature: Review\n ...", id: "review" }) - 4. use("!prototype.settle", { source: "./roles/dev" }) - 5. activate("dev") - """ - - Scenario: Create and register an organization prototype - Given you want a reusable organization template - Then follow this sequence: - """ - 1. use("!prototype.found", { dir: "./orgs/dp", id: "dp", content: "Feature: Deepractice" }) - 2. use("!prototype.charter", { dir: "./orgs/dp", content: "Feature: Mission\n ...", id: "mission" }) - 3. use("!prototype.establish", { dir: "./orgs/dp", content: "Feature: Architect", id: "architect" }) - 4. use("!prototype.charge", { dir: "./orgs/dp", position: "architect", content: "Feature: Design\n ...", id: "design" }) - 5. use("!prototype.require", { dir: "./orgs/dp", position: "architect", content: "Feature: Skill\n ...", id: "skill" }) - 6. use("!prototype.settle", { source: "./orgs/dp" }) - """ + When activate is called with the individual's id + Then the individual is automatically born from the prototype From 175e9f5b6d566f9135eb1a3748f69b4a3d6b939d Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Feb 2026 18:12:48 +0800 Subject: [PATCH 29/54] developing --- apps/mcp-server/src/index.ts | 19 +++++++------- packages/core/src/platform.ts | 3 +++ packages/local-platform/src/LocalPlatform.ts | 23 +++++++++++++--- packages/local-platform/src/sqliteRuntime.ts | 9 +++++++ packages/rolexjs/src/role.ts | 6 +++++ packages/rolexjs/src/rolex.ts | 8 +++++- prototypes/rolex/charter.charter.feature | 15 +++++++++++ .../individual-management.requirement.feature | 8 ++++++ .../rolex/individual-manager.position.feature | 3 +++ .../manage-individual-lifecycle.duty.feature | 17 ++++++++++++ ...manage-organization-lifecycle.duty.feature | 18 +++++++++++++ .../manage-position-lifecycle.duty.feature | 19 ++++++++++++++ ...rganization-management.requirement.feature | 8 ++++++ .../organization-manager.position.feature | 3 +++ .../position-management.requirement.feature | 8 ++++++ .../rolex/position-manager.position.feature | 3 +++ .../prototype-management.procedure.feature | 8 ++++++ prototypes/rolex/prototype.json | 26 +++++++++++++++++++ .../resource-management.procedure.feature | 8 ++++++ prototypes/rolex/rolex.organization.feature | 15 +++++++++++ .../rolex/skill-creator.procedure.feature | 8 ++++++ 21 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 prototypes/rolex/charter.charter.feature create mode 100644 prototypes/rolex/individual-management.requirement.feature create mode 100644 prototypes/rolex/individual-manager.position.feature create mode 100644 prototypes/rolex/manage-individual-lifecycle.duty.feature create mode 100644 prototypes/rolex/manage-organization-lifecycle.duty.feature create mode 100644 prototypes/rolex/manage-position-lifecycle.duty.feature create mode 100644 prototypes/rolex/organization-management.requirement.feature create mode 100644 prototypes/rolex/organization-manager.position.feature create mode 100644 prototypes/rolex/position-management.requirement.feature create mode 100644 prototypes/rolex/position-manager.position.feature create mode 100644 prototypes/rolex/prototype-management.procedure.feature create mode 100644 prototypes/rolex/prototype.json create mode 100644 prototypes/rolex/resource-management.procedure.feature create mode 100644 prototypes/rolex/rolex.organization.feature create mode 100644 prototypes/rolex/skill-creator.procedure.feature diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 08b646d..437d1a7 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -6,9 +6,13 @@ * MCP only translates protocol calls to API calls. */ +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; import { localPlatform } from "@rolexjs/local-platform"; import { FastMCP } from "fastmcp"; import { createRoleX, detail } from "rolexjs"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); import { z } from "zod"; import { instructions } from "./instructions.js"; import { render } from "./render.js"; @@ -16,7 +20,9 @@ import { McpState } from "./state.js"; // ========== Setup ========== -const rolex = createRoleX(localPlatform()); +const rolex = createRoleX(localPlatform({ + bootstrap: [resolve(__dirname, "../../../prototypes/rolex")], +})); await rolex.genesis(); const state = new McpState(); @@ -54,15 +60,8 @@ server.addTool({ execute: async ({ roleId }) => { const role = await rolex.activate(roleId); state.role = role; - // Use focus to get the projected state for rendering - const goalId = role.ctx.focusedGoalId; - if (goalId) { - const result = role.focus(goalId); - return fmt("activate", roleId, result); - } - // No goal yet — simple activation message - const hint = role.ctx.cognitiveHint("activate"); - return `Role "${roleId}" activated.\n${hint ? `I → ${hint}` : ""}`; + const result = role.project(); + return fmt("activate", roleId, result); }, }); diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index 15c60a8..04ae765 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -36,6 +36,9 @@ export interface Platform { /** Initializer — bootstrap the world on first run. */ readonly initializer?: Initializer; + /** Prototype sources to settle on genesis (local paths or ResourceX locators). */ + readonly bootstrap?: readonly string[]; + /** Save role context to persistent storage. */ saveContext?(roleId: string, data: ContextData): void; diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index fe0c561..bff22eb 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -30,6 +30,8 @@ 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[]; } // ===== DDL ===== @@ -62,12 +64,17 @@ const CREATE_INDEXES = [ // ===== Factory ===== -/** Create a local Platform. Persistent by default (~/.deepractice/rolex), in-memory if dataDir is null. */ +/** Resolve the DEEPRACTICE_HOME base directory. Env > default (~/.deepractice). */ +function deepracticeHome(): string { + return process.env.DEEPRACTICE_HOME ?? join(homedir(), ".deepractice"); +} + +/** 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(homedir(), ".deepractice", "rolex")); + : (config.dataDir ?? join(deepracticeHome(), "rolex")); // ===== SQLite database ===== @@ -99,7 +106,7 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { if (config.resourceDir !== null) { setProvider(new NodeProvider()); resourcex = createResourceX({ - path: config.resourceDir ?? join(homedir(), ".deepractice", "resourcex"), + path: config.resourceDir ?? join(deepracticeHome(), "resourcex"), types: [prototypeType], }); } @@ -161,5 +168,13 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { return JSON.parse(readFileSync(contextPath, "utf-8")); }; - return { runtime, prototype, resourcex, initializer, saveContext, loadContext }; + return { + runtime, + prototype, + resourcex, + initializer, + bootstrap: config.bootstrap, + saveContext, + loadContext, + }; } diff --git a/packages/local-platform/src/sqliteRuntime.ts b/packages/local-platform/src/sqliteRuntime.ts index 429d614..957314e 100644 --- a/packages/local-platform/src/sqliteRuntime.ts +++ b/packages/local-platform/src/sqliteRuntime.ts @@ -91,6 +91,15 @@ function removeSubtree(db: DB, ref: string): void { export function createSqliteRuntime(db: DB): Runtime { return { create(parent, type, information, id, alias) { + // Idempotent: if parent has a child with the same id, return existing node. + if (id && parent?.ref) { + const existing = db + .select() + .from(nodes) + .where(and(eq(nodes.parentRef, parent.ref), eq(nodes.id, id))) + .get(); + if (existing) return toStructure(existing); + } const ref = nextRef(db); db.insert(nodes) .values({ diff --git a/packages/rolexjs/src/role.ts b/packages/rolexjs/src/role.ts index fb8be39..7cc1696 100644 --- a/packages/rolexjs/src/role.ts +++ b/packages/rolexjs/src/role.ts @@ -47,6 +47,12 @@ export class Role { this.api = api; } + /** Project the individual's full state tree. */ + project(): RolexResult { + const result = this.api.ops["role.focus"](this.roleId); + return this.withHint({ ...result, process: "activate" }, "activate"); + } + private withHint(result: RolexResult, process: string): RolexResult { result.hint = this.ctx.cognitiveHint(process) ?? undefined; return result; diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 0763d50..9d83fbc 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -39,6 +39,7 @@ export class Rolex { load: (roleId: string) => ContextData | null; }; + private readonly bootstrap: readonly string[]; private readonly society: Structure; private readonly past: Structure; @@ -47,6 +48,7 @@ export class Rolex { this.resourcex = platform.resourcex; this.protoRegistry = platform.prototype; this.initializer = platform.initializer; + this.bootstrap = platform.bootstrap ?? []; if (platform.saveContext && platform.loadContext) { this.persistContext = { save: platform.saveContext, load: platform.loadContext }; @@ -77,9 +79,13 @@ export class Rolex { }); } - /** Genesis — create the world on first run. */ + /** 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 }); + } } /** diff --git a/prototypes/rolex/charter.charter.feature b/prototypes/rolex/charter.charter.feature new file mode 100644 index 0000000..495df4a --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/individual-management.requirement.feature b/prototypes/rolex/individual-management.requirement.feature new file mode 100644 index 0000000..e81b235 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/individual-manager.position.feature b/prototypes/rolex/individual-manager.position.feature new file mode 100644 index 0000000..02eb2b4 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/manage-individual-lifecycle.duty.feature b/prototypes/rolex/manage-individual-lifecycle.duty.feature new file mode 100644 index 0000000..39488e8 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/manage-organization-lifecycle.duty.feature b/prototypes/rolex/manage-organization-lifecycle.duty.feature new file mode 100644 index 0000000..6586d61 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/manage-position-lifecycle.duty.feature b/prototypes/rolex/manage-position-lifecycle.duty.feature new file mode 100644 index 0000000..c0158ac --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/organization-management.requirement.feature b/prototypes/rolex/organization-management.requirement.feature new file mode 100644 index 0000000..dc11ef7 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/organization-manager.position.feature b/prototypes/rolex/organization-manager.position.feature new file mode 100644 index 0000000..91c961b --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/position-management.requirement.feature b/prototypes/rolex/position-management.requirement.feature new file mode 100644 index 0000000..d60f945 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/position-manager.position.feature b/prototypes/rolex/position-manager.position.feature new file mode 100644 index 0000000..7c05269 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/prototype-management.procedure.feature b/prototypes/rolex/prototype-management.procedure.feature new file mode 100644 index 0000000..e54c98d --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/prototype.json b/prototypes/rolex/prototype.json new file mode 100644 index 0000000..618e579 --- /dev/null +++ b/prototypes/rolex/prototype.json @@ -0,0 +1,26 @@ +[ + { "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": "!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/prototypes/rolex/resource-management.procedure.feature b/prototypes/rolex/resource-management.procedure.feature new file mode 100644 index 0000000..08b3618 --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/rolex.organization.feature b/prototypes/rolex/rolex.organization.feature new file mode 100644 index 0000000..4f6f3da --- /dev/null +++ b/prototypes/rolex/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/prototypes/rolex/skill-creator.procedure.feature b/prototypes/rolex/skill-creator.procedure.feature new file mode 100644 index 0000000..3136f44 --- /dev/null +++ b/prototypes/rolex/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 From d365ace2cc89f5958371818adab1d0d6aa26713c Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 27 Feb 2026 10:54:38 +0800 Subject: [PATCH 30/54] feat: transform as move, remote prototype bootstrap, upgrade resourcexjs - Fix transform to reparent nodes preserving subtree (was incorrectly creating new nodes) - archive/rehire/dissolve/abolish now use transform for proper subtree preservation - Remove prototypeType from local-platform (now built-in to ResourceX 2.16.1) - Bootstrap from remote registry (registry.deepractice.dev/rolex-world) - Remove Nuwa's redundant train for position-level skills - Rename prototype resource to rolex-world - Upgrade resourcexjs to 2.16.1 Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 11 ++-- bun.lock | 50 +++++---------- package.json | 5 +- packages/local-platform/src/LocalPlatform.ts | 6 +- packages/local-platform/src/prototypeType.ts | 48 --------------- packages/local-platform/src/sqliteRuntime.ts | 23 ++++--- packages/prototype/src/ops.ts | 64 +++++++++++++++----- packages/system/src/runtime.ts | 34 +++++++++-- prototypes/rolex/resource.json | 2 +- 9 files changed, 116 insertions(+), 127 deletions(-) delete mode 100644 packages/local-platform/src/prototypeType.ts diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 437d1a7..04d4ea4 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -6,13 +6,10 @@ * MCP only translates protocol calls to API calls. */ -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; import { localPlatform } from "@rolexjs/local-platform"; import { FastMCP } from "fastmcp"; import { createRoleX, detail } from "rolexjs"; -const __dirname = dirname(fileURLToPath(import.meta.url)); import { z } from "zod"; import { instructions } from "./instructions.js"; import { render } from "./render.js"; @@ -20,9 +17,11 @@ import { McpState } from "./state.js"; // ========== Setup ========== -const rolex = createRoleX(localPlatform({ - bootstrap: [resolve(__dirname, "../../../prototypes/rolex")], -})); +const rolex = createRoleX( + localPlatform({ + bootstrap: ["registry.deepractice.dev/rolex-world"], + }) +); await rolex.genesis(); const state = new McpState(); diff --git a/bun.lock b/bun.lock index ab4a4ef..9f60506 100644 --- a/bun.lock +++ b/bun.lock @@ -5,8 +5,9 @@ "": { "name": "rolexjs", "dependencies": { - "@resourcexjs/node-provider": "^2.14.0", - "resourcexjs": "^2.15.0", + "@resourcexjs/core": "^2.16.1", + "@resourcexjs/node-provider": "^2.16.1", + "resourcexjs": "^2.16.1", }, "devDependencies": { "@biomejs/biome": "^2.4.0", @@ -23,19 +24,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", @@ -65,7 +53,6 @@ "@deepracticex/sqlite": "^0.2.0", "@resourcexjs/node-provider": "^2.14.0", "@rolexjs/core": "workspace:*", - "@rolexjs/resourcex-types": "workspace:*", "@rolexjs/system": "workspace:*", "drizzle-orm": "^0.45.1", "resourcexjs": "^2.14.0", @@ -83,13 +70,9 @@ "name": "@rolexjs/prototype", "version": "0.11.0", "dependencies": { - "resourcexjs": "^2.14.0", - }, - }, - "packages/resourcex-types": { - "name": "@rolexjs/resourcex-types", - "version": "0.11.0", - "dependencies": { + "@rolexjs/core": "workspace:*", + "@rolexjs/parser": "workspace:*", + "@rolexjs/system": "workspace:*", "resourcexjs": "^2.14.0", }, }, @@ -99,6 +82,7 @@ "dependencies": { "@rolexjs/core": "workspace:*", "@rolexjs/parser": "workspace:*", + "@rolexjs/prototype": "workspace:*", "@rolexjs/system": "workspace:*", "resourcexjs": "^2.14.0", }, @@ -358,13 +342,11 @@ "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], - "@resourcexjs/arp": ["@resourcexjs/arp@2.15.0", "https://registry.npmmirror.com/@resourcexjs/arp/-/arp-2.15.0.tgz", {}, "sha512-6OqzFbvGmbhploFgBHdK+G/gA5Xihsq4+8oBGBgyDtiLJ5vfyVpwZODBbnV9jhqoE/vkFNBhn6b9etn8a7NDJA=="], + "@resourcexjs/arp": ["@resourcexjs/arp@2.16.1", "https://registry.npmmirror.com/@resourcexjs/arp/-/arp-2.16.1.tgz", {}, "sha512-yzlftFkQuO9T7dOnSdT21WYyDzOoLZmKaQS6RZ9v2f3Pev7anTi3nuTnzYMApAVLlY/+DwiKg7LV5FRMwjpy/Q=="], - "@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=="], + "@resourcexjs/core": ["@resourcexjs/core@2.16.1", "https://registry.npmmirror.com/@resourcexjs/core/-/core-2.16.1.tgz", { "dependencies": { "modern-tar": "^0.7.3", "zod": "^4.3.6" } }, "sha512-8i/pQVQg2uvAnRXDG2NNUPoUrDHN+bd+1MEbbmXoeyTM4RilX90OQXxKyyi40UpZGTpyfkCQFucFC3r0GeKweQ=="], - "@resourcexjs/node-provider": ["@resourcexjs/node-provider@2.14.0", "https://registry.npmmirror.com/@resourcexjs/node-provider/-/node-provider-2.14.0.tgz", { "dependencies": { "@resourcexjs/core": "^2.14.0" } }, "sha512-GrEvaU2JsrcISsoyrSaRwa8J98s9Ecgt2QM5atV5o0+V3CEayA9zUdccddkRdmkUbX4fqNmnnmbxJYplHODh2g=="], - - "@rolexjs/cli": ["@rolexjs/cli@workspace:apps/cli"], + "@resourcexjs/node-provider": ["@resourcexjs/node-provider@2.16.1", "https://registry.npmmirror.com/@resourcexjs/node-provider/-/node-provider-2.16.1.tgz", { "dependencies": { "@resourcexjs/core": "^2.16.1" } }, "sha512-MDGfsam6ReEBDjmJM7qjLZBOnfkGNK/oz3s38d1eqsvnIyBFNA4wB6ixr6GGAZRwf1EVETqfAXoZZzpP56R78A=="], "@rolexjs/core": ["@rolexjs/core@workspace:packages/core"], @@ -376,8 +358,6 @@ "@rolexjs/prototype": ["@rolexjs/prototype@workspace:packages/prototype"], - "@rolexjs/resourcex-types": ["@rolexjs/resourcex-types@workspace:packages/resourcex-types"], - "@rolexjs/system": ["@rolexjs/system@workspace:packages/system"], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], @@ -510,8 +490,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=="], @@ -952,7 +930,7 @@ "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "resourcexjs": ["resourcexjs@2.15.0", "https://registry.npmmirror.com/resourcexjs/-/resourcexjs-2.15.0.tgz", { "dependencies": { "@resourcexjs/arp": "^2.15.0", "@resourcexjs/core": "^2.15.0", "sandboxxjs": "^0.5.1" } }, "sha512-sPwl9spKSsBYVXQU2NGnjGYGXpBA1mztHAgXVEXHmLtZLwy5+aveGd7iP0XfhdSC4RaVee14CybdFZPAXTXnsQ=="], + "resourcexjs": ["resourcexjs@2.16.1", "https://registry.npmmirror.com/resourcexjs/-/resourcexjs-2.16.1.tgz", { "dependencies": { "@resourcexjs/arp": "^2.16.1", "@resourcexjs/core": "^2.16.1", "sandboxxjs": "^0.5.1" } }, "sha512-054JrC6Rvlwnb0aMErRMOcc2N7SvYDhCP/AZg5zYFkoJzS0YB83y811Z4R4ttDQPw0kEWljo5Dxd2YQ34KNxOA=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], @@ -1152,12 +1130,12 @@ "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - "resourcexjs/@resourcexjs/core": ["@resourcexjs/core@2.15.0", "https://registry.npmmirror.com/@resourcexjs/core/-/core-2.15.0.tgz", { "dependencies": { "modern-tar": "^0.7.3", "zod": "^4.3.6" } }, "sha512-jr0r734ugLriuF/mWb3+/zyHyG0rDKarvdCFXE0+aBlTLCJbKFReq9vXbszTXyT/kPJCeoZ/9X3Sxgkkdc+rlg=="], - "tsup/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "@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=="], @@ -1182,7 +1160,7 @@ "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "resourcexjs/@resourcexjs/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@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=="], diff --git a/package.json b/package.json index e8c3551..01cbd54 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,9 @@ "bun": ">=1.3.0" }, "dependencies": { - "@resourcexjs/node-provider": "^2.14.0", - "resourcexjs": "^2.15.0" + "@resourcexjs/core": "^2.16.1", + "@resourcexjs/node-provider": "^2.16.1", + "resourcexjs": "^2.16.1" }, "overrides": { "resourcexjs": "^2.14.0", diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index bff22eb..3cae2a1 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -21,7 +21,6 @@ import type { Initializer } from "@rolexjs/system"; import { sql } from "drizzle-orm"; import { createResourceX, setProvider } from "resourcexjs"; import { createSqliteRuntime } from "./sqliteRuntime.js"; -import { prototypeType } from "./prototypeType.js"; // ===== Config ===== @@ -72,9 +71,7 @@ function deepracticeHome(): string { /** 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")); + config.dataDir === null ? undefined : (config.dataDir ?? join(deepracticeHome(), "rolex")); // ===== SQLite database ===== @@ -107,7 +104,6 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { setProvider(new NodeProvider()); resourcex = createResourceX({ path: config.resourceDir ?? join(deepracticeHome(), "resourcex"), - types: [prototypeType], }); } diff --git a/packages/local-platform/src/prototypeType.ts b/packages/local-platform/src/prototypeType.ts deleted file mode 100644 index 9ba2cf1..0000000 --- a/packages/local-platform/src/prototypeType.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Prototype ResourceX type — resolves a prototype resource into - * an executable instruction set with all @filename references resolved. - * - * Returns: { id: string, instructions: Array<{ op: string, args: Record }> } - */ - -import type { BundledType } from "resourcexjs"; - -export const prototypeType: BundledType = { - name: "prototype", - description: "RoleX prototype — instruction set for materializing roles and organizations", - code: `// @resolver: prototype_type_default -var prototype_type_default = { - async resolve(ctx) { - var protoFile = ctx.files["prototype.json"]; - if (!protoFile) throw new Error("Prototype resource must contain a prototype.json file"); - - var decoder = new TextDecoder(); - var instructions = JSON.parse(decoder.decode(protoFile)); - - if (!Array.isArray(instructions)) { - throw new Error("prototype.json must be a JSON array of instructions"); - } - - // Resolve @filename references in instruction args - var resolved = instructions.map(function(instr) { - var resolvedArgs = {}; - var keys = Object.keys(instr.args || {}); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var value = instr.args[key]; - if (typeof value === "string" && value.startsWith("@")) { - var filename = value.slice(1); - var file = ctx.files[filename]; - if (!file) throw new Error("Referenced file not found: " + filename); - resolvedArgs[key] = decoder.decode(file); - } else { - resolvedArgs[key] = value; - } - } - return { op: instr.op, args: resolvedArgs }; - }); - - return { id: ctx.manifest.name, instructions: resolved }; - } -};`, -}; diff --git a/packages/local-platform/src/sqliteRuntime.ts b/packages/local-platform/src/sqliteRuntime.ts index 957314e..227b70f 100644 --- a/packages/local-platform/src/sqliteRuntime.ts +++ b/packages/local-platform/src/sqliteRuntime.ts @@ -125,30 +125,33 @@ export function createSqliteRuntime(db: DB): Runtime { removeSubtree(db, node.ref); }, - transform(_source, target, information) { + 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}`); } - // Find any node matching the parent structure type 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}`); } - const ref = nextRef(db); - db.insert(nodes) - .values({ - ref, + // Reparent + update type in place — subtree preserved + db.update(nodes) + .set({ + parentRef: parentRow.ref, name: target.name, description: target.description, - parentRef: parentRow.ref, - information: information ?? null, - tag: null, + ...(information !== undefined ? { information } : {}), }) + .where(eq(nodes.ref, source.ref)) .run(); - return toStructure(db.select().from(nodes).where(eq(nodes.ref, ref)).get()!); + + return toStructure(db.select().from(nodes).where(eq(nodes.ref, source.ref)).get()!); }, link(from, to, relationName, reverseName) { diff --git a/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts index b9b500c..fb28809 100644 --- a/packages/prototype/src/ops.ts +++ b/packages/prototype/src/ops.ts @@ -10,8 +10,8 @@ */ import * as C from "@rolexjs/core"; -import type { Runtime, State, Structure } from "@rolexjs/system"; import { parse } from "@rolexjs/parser"; +import type { Runtime, State, Structure } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; // ================================================================ @@ -55,8 +55,7 @@ export function createOps(ctx: OpsContext): Ops { } function archive(node: Structure, process: string): OpResult { - const archived = rt.create(past, C.past, node.information, node.id); - rt.remove(node); + const archived = rt.transform(node, C.past); return ok(archived, process); } @@ -118,9 +117,7 @@ export function createOps(ctx: OpsContext): Ops { "individual.rehire"(pastNode: string): OpResult { const node = resolve(pastNode); - const ind = rt.create(society, C.individual, node.information, node.id); - rt.create(ind, C.identity, undefined, node.id); - rt.remove(node); + const ind = rt.transform(node, C.individual); return ok(ind, "rehire"); }, @@ -150,13 +147,24 @@ export function createOps(ctx: OpsContext): Ops { // ---- Role: execution ---- - "role.want"(individual: string, goal?: string, id?: string, alias?: readonly string[]): OpResult { + "role.want"( + individual: string, + goal?: string, + id?: string, + alias?: readonly string[] + ): OpResult { validateGherkin(goal); const node = rt.create(resolve(individual), C.goal, goal, id, alias); return ok(node, "want"); }, - "role.plan"(goal: string, plan?: string, id?: string, after?: string, fallback?: string): OpResult { + "role.plan"( + goal: string, + plan?: string, + id?: string, + after?: string, + fallback?: string + ): OpResult { validateGherkin(plan); const node = rt.create(resolve(goal), C.plan, plan, id); if (after) rt.link(node, resolve(after), "after", "before"); @@ -202,23 +210,48 @@ export function createOps(ctx: OpsContext): Ops { // ---- Role: cognition ---- - "role.reflect"(encounter: string, individual: string, experience?: string, id?: string): OpResult { + "role.reflect"( + encounter: string, + individual: string, + experience?: string, + id?: string + ): OpResult { validateGherkin(experience); const encNode = resolve(encounter); - const exp = rt.create(resolve(individual), C.experience, experience || encNode.information, id); + const exp = rt.create( + resolve(individual), + C.experience, + experience || encNode.information, + id + ); rt.remove(encNode); return ok(exp, "reflect"); }, - "role.realize"(experience: string, individual: string, principle?: string, id?: string): OpResult { + "role.realize"( + experience: string, + individual: string, + principle?: string, + id?: string + ): OpResult { validateGherkin(principle); const expNode = resolve(experience); - const prin = rt.create(resolve(individual), C.principle, principle || expNode.information, id); + const prin = rt.create( + resolve(individual), + C.principle, + principle || expNode.information, + id + ); rt.remove(expNode); return ok(prin, "realize"); }, - "role.master"(individual: string, procedure: string, id?: string, experience?: string): OpResult { + "role.master"( + individual: string, + procedure: string, + id?: string, + experience?: string + ): OpResult { validateGherkin(procedure); const parent = resolve(individual); if (id) removeExisting(parent, id); @@ -366,7 +399,10 @@ export function createOps(ctx: OpsContext): Ops { 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); + const result = await rx.ingest<{ + id: string; + instructions: Array<{ op: string; args: Record }>; + }>(source); // Execute each instruction for (const instr of result.instructions) { diff --git a/packages/system/src/runtime.ts b/packages/system/src/runtime.ts index fb9e01c..3a93040 100644 --- a/packages/system/src/runtime.ts +++ b/packages/system/src/runtime.ts @@ -30,7 +30,7 @@ export interface Runtime { /** Remove a node and its subtree. */ remove(node: Structure): void; - /** Produce a new node in target structure's branch, sourced from another branch. */ + /** Move a node to target structure's branch, preserving its subtree. Updates type and optionally information. */ transform(source: Structure, target: Structure, information?: string): Structure; /** Establish a bidirectional cross-branch relation between two nodes. */ @@ -176,18 +176,42 @@ export const createRuntime = (): Runtime => { removeSubtree(node.ref); }, - transform(_source, target, information) { + 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) { diff --git a/prototypes/rolex/resource.json b/prototypes/rolex/resource.json index 8288880..1eff107 100644 --- a/prototypes/rolex/resource.json +++ b/prototypes/rolex/resource.json @@ -1,5 +1,5 @@ { - "name": "rolex", + "name": "rolex-world", "type": "prototype", "author": "deepractice", "description": "The foundational organization of the RoleX world" From d8eb46ebf8fdd3f1471727a3ce38fa6a1bc1f88e Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 27 Feb 2026 15:25:58 +0800 Subject: [PATCH 31/54] feat: zero-network bootstrap via npm:@rolexjs/rolex-prototype - Add @rolexjs/rolex-prototype package for the rolex world prototype - Add prototypes/* to workspace configuration - MCP server bootstrap changed from registry URL to npm: locator - Upgrade resourcexjs to 2.17.2 (NpmSourceLoader support) Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/package.json | 1 + apps/mcp-server/src/index.ts | 11 +- apps/mcp-server/tests/mcp.test.ts | 25 +- biome.json | 2 +- bun.lock | 21 +- package.json | 9 +- packages/prototype/src/descriptions/index.ts | 120 +-- packages/prototype/src/dispatch.ts | 2 +- packages/prototype/src/index.ts | 15 +- packages/prototype/src/instructions.ts | 731 +++++++++++++----- packages/prototype/tests/alignment.test.ts | 1 - packages/prototype/tests/descriptions.test.ts | 29 +- packages/prototype/tests/dispatch.test.ts | 80 +- packages/prototype/tests/instructions.test.ts | 28 +- packages/prototype/tests/ops.test.ts | 88 ++- packages/rolexjs/src/index.ts | 2 +- packages/rolexjs/src/role.ts | 3 +- packages/rolexjs/src/rolex.ts | 4 +- packages/rolexjs/tests/context.test.ts | 16 +- packages/rolexjs/tests/rolex.test.ts | 27 +- prototypes/rolex/package.json | 26 + prototypes/rolex/prototype.json | 116 ++- 22 files changed, 972 insertions(+), 385 deletions(-) create mode 100644 prototypes/rolex/package.json diff --git a/apps/mcp-server/package.json b/apps/mcp-server/package.json index dd32be2..bff5617 100644 --- a/apps/mcp-server/package.json +++ b/apps/mcp-server/package.json @@ -43,6 +43,7 @@ "dependencies": { "rolexjs": "workspace:*", "@rolexjs/local-platform": "workspace:*", + "@rolexjs/rolex-prototype": "workspace:*", "fastmcp": "^3.0.0", "zod": "^3.25.0" }, diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 04d4ea4..a94320c 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -19,7 +19,7 @@ import { McpState } from "./state.js"; const rolex = createRoleX( localPlatform({ - bootstrap: ["registry.deepractice.dev/rolex-world"], + bootstrap: ["npm:@rolexjs/rolex-prototype"], }) ); await rolex.genesis(); @@ -60,7 +60,14 @@ server.addTool({ const role = await rolex.activate(roleId); state.role = role; const result = role.project(); - return fmt("activate", roleId, result); + const focusedGoalId = role.ctx.focusedGoalId; + return render({ + process: "activate", + name: roleId, + result, + cognitiveHint: result.hint ?? null, + fold: (node) => node.name === "goal" && node.id !== focusedGoalId, + }); }, }); diff --git a/apps/mcp-server/tests/mcp.test.ts b/apps/mcp-server/tests/mcp.test.ts index d46b1ee..1b4459a 100644 --- a/apps/mcp-server/tests/mcp.test.ts +++ b/apps/mcp-server/tests/mcp.test.ts @@ -43,7 +43,10 @@ describe("requireRole", () => { describe("render", () => { it("includes status + hint + projection", async () => { - const result = await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const result = await rolex.direct("!individual.born", { + content: "Feature: Sean", + id: "sean", + }); const output = render({ process: "born", name: "Sean", @@ -59,7 +62,10 @@ describe("render", () => { }); it("includes cognitive hint when provided", async () => { - const result = await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const result = await rolex.direct("!individual.born", { + content: "Feature: Sean", + id: "sean", + }); const output = render({ process: "born", name: "Sean", @@ -120,7 +126,7 @@ describe("full execution flow", () => { // Finish with encounter const finished = role.finish( "impl-jwt", - "Feature: Implemented JWT\n Scenario: Token pattern\n Given JWT needed\n Then tokens work", + "Feature: Implemented JWT\n Scenario: Token pattern\n Given JWT needed\n Then tokens work" ); expect(finished.state.name).toBe("encounter"); expect(role.ctx.encounterIds.has("impl-jwt-finished")).toBe(true); @@ -129,7 +135,7 @@ describe("full execution flow", () => { const reflected = 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", + "token-insight" ); expect(reflected.state.name).toBe("experience"); expect(role.ctx.encounterIds.has("impl-jwt-finished")).toBe(false); @@ -139,7 +145,7 @@ describe("full execution flow", () => { const realized = 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", + "refresh-tokens" ); expect(realized.state.name).toBe("principle"); expect(role.ctx.experienceIds.has("token-insight")).toBe(false); @@ -185,13 +191,10 @@ describe("selective cognition", () => { role.todo("Feature: Signup", "signup"); // Finish both with encounters - role.finish( - "login", - "Feature: Login done\n Scenario: OK\n Given login\n Then success", - ); + role.finish("login", "Feature: Login done\n Scenario: OK\n Given login\n Then success"); role.finish( "signup", - "Feature: Signup done\n Scenario: OK\n Given signup\n Then success", + "Feature: Signup done\n Scenario: OK\n Given signup\n Then success" ); expect(role.ctx.encounterIds.has("login-finished")).toBe(true); @@ -201,7 +204,7 @@ describe("selective cognition", () => { role.reflect( "login-finished", "Feature: Login insight\n Scenario: OK\n Given practice\n Then understanding", - "login-insight", + "login-insight" ); // "login-finished" consumed, "signup-finished" still available 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 9f60506..58b5046 100644 --- a/bun.lock +++ b/bun.lock @@ -5,9 +5,9 @@ "": { "name": "rolexjs", "dependencies": { - "@resourcexjs/core": "^2.16.1", - "@resourcexjs/node-provider": "^2.16.1", - "resourcexjs": "^2.16.1", + "@resourcexjs/core": "^2.17.2", + "@resourcexjs/node-provider": "^2.17.2", + "resourcexjs": "^2.17.2", }, "devDependencies": { "@biomejs/biome": "^2.4.0", @@ -32,6 +32,7 @@ }, "dependencies": { "@rolexjs/local-platform": "workspace:*", + "@rolexjs/rolex-prototype": "workspace:*", "fastmcp": "^3.0.0", "rolexjs": "workspace:*", "zod": "^3.25.0", @@ -94,6 +95,10 @@ "name": "@rolexjs/system", "version": "0.11.0", }, + "prototypes/rolex": { + "name": "@rolexjs/rolex-prototype", + "version": "0.1.0", + }, }, "overrides": { "@resourcexjs/node-provider": "^2.14.0", @@ -342,11 +347,11 @@ "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], - "@resourcexjs/arp": ["@resourcexjs/arp@2.16.1", "https://registry.npmmirror.com/@resourcexjs/arp/-/arp-2.16.1.tgz", {}, "sha512-yzlftFkQuO9T7dOnSdT21WYyDzOoLZmKaQS6RZ9v2f3Pev7anTi3nuTnzYMApAVLlY/+DwiKg7LV5FRMwjpy/Q=="], + "@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.16.1", "https://registry.npmmirror.com/@resourcexjs/core/-/core-2.16.1.tgz", { "dependencies": { "modern-tar": "^0.7.3", "zod": "^4.3.6" } }, "sha512-8i/pQVQg2uvAnRXDG2NNUPoUrDHN+bd+1MEbbmXoeyTM4RilX90OQXxKyyi40UpZGTpyfkCQFucFC3r0GeKweQ=="], + "@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.16.1", "https://registry.npmmirror.com/@resourcexjs/node-provider/-/node-provider-2.16.1.tgz", { "dependencies": { "@resourcexjs/core": "^2.16.1" } }, "sha512-MDGfsam6ReEBDjmJM7qjLZBOnfkGNK/oz3s38d1eqsvnIyBFNA4wB6ixr6GGAZRwf1EVETqfAXoZZzpP56R78A=="], + "@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/core": ["@rolexjs/core@workspace:packages/core"], @@ -358,6 +363,8 @@ "@rolexjs/prototype": ["@rolexjs/prototype@workspace:packages/prototype"], + "@rolexjs/rolex-prototype": ["@rolexjs/rolex-prototype@workspace:prototypes/rolex"], + "@rolexjs/system": ["@rolexjs/system@workspace:packages/system"], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], @@ -930,7 +937,7 @@ "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "resourcexjs": ["resourcexjs@2.16.1", "https://registry.npmmirror.com/resourcexjs/-/resourcexjs-2.16.1.tgz", { "dependencies": { "@resourcexjs/arp": "^2.16.1", "@resourcexjs/core": "^2.16.1", "sandboxxjs": "^0.5.1" } }, "sha512-054JrC6Rvlwnb0aMErRMOcc2N7SvYDhCP/AZg5zYFkoJzS0YB83y811Z4R4ttDQPw0kEWljo5Dxd2YQ34KNxOA=="], + "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=="], diff --git a/package.json b/package.json index 01cbd54..8060ccf 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "type": "module", "workspaces": [ "packages/*", - "apps/*" + "apps/*", + "prototypes/*" ], "scripts": { "build": "turbo build", @@ -45,9 +46,9 @@ "bun": ">=1.3.0" }, "dependencies": { - "@resourcexjs/core": "^2.16.1", - "@resourcexjs/node-provider": "^2.16.1", - "resourcexjs": "^2.16.1" + "@resourcexjs/core": "^2.17.2", + "@resourcexjs/node-provider": "^2.17.2", + "resourcexjs": "^2.17.2" }, "overrides": { "resourcexjs": "^2.14.0", diff --git a/packages/prototype/src/descriptions/index.ts b/packages/prototype/src/descriptions/index.ts index 47bcfc1..808d052 100644 --- a/packages/prototype/src/descriptions/index.ts +++ b/packages/prototype/src/descriptions/index.ts @@ -1,50 +1,84 @@ // 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", - "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 — unified execution entry point\n Execute any RoleX command or load any ResourceX resource through a single entry point.\n The locator determines the dispatch path:\n - `!namespace.method` dispatches to the RoleX runtime\n - Any other locator delegates to ResourceX\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 And the result is returned\n\n Scenario: Available namespaces\n Given the `!` prefix routes to RoleX namespaces\n Then `!individual.*` routes to individual lifecycle and injection\n And `!org.*` routes to organization management\n And `!position.*` routes to position management\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\"", + 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", + 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 — unified execution entry point\n Execute any RoleX command or load any ResourceX resource through a single entry point.\n The locator determines the dispatch path:\n - `!namespace.method` dispatches to the RoleX runtime\n - Any other locator delegates to ResourceX\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 And the result is returned\n\n Scenario: Available namespaces\n Given the `!` prefix routes to RoleX namespaces\n Then `!individual.*` routes to individual lifecycle and injection\n And `!org.*` routes to organization management\n And `!position.*` routes to position management\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 = { - "census": "Feature: Census — society-level queries\n Query the RoleX world to see what exists — individuals, organizations, positions.\n Census is read-only and accessed via the use tool with !census.list.\n\n Scenario: List all top-level entities\n Given I want to see what exists in the world\n When I call use(\"!census.list\")\n Then I get a summary of all individuals, organizations, and positions\n And each entry includes id, name, and tag if present\n\n Scenario: Filter by type\n Given I only want to see entities of a specific type\n When I call use(\"!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 what has been retired, dissolved, or abolished\n When I call use(\"!census.list\", { type: \"past\" })\n Then entities in the archive are returned\n\n Scenario: When to use census\n Given I need to know what exists before acting\n When I want to check if an organization exists before founding\n Or I want to see all individuals before hiring\n Or I want an overview of the world\n Then census.list is the right tool", - "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: External injection\n Given an external agent needs to equip a role with knowledge or skills\n Then teach(individual, principle, id) directly injects a principle\n And train(individual, procedure, id) directly injects a procedure\n And the difference from realize/master is perspective — external vs self-initiated\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 And 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 And 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 And 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\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 never invent keywords like Because, Since, or So\n\n Scenario: Expressing causality without Because\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\n And example — instead of \"Then use RoleX tools because native tools break the loop\"\n And write \"Then use RoleX tools\" followed by \"And native tools do not feed the growth loop\"", - "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\"", - "nuwa": "Feature: Nuwa — the entry point of the RoleX world\n Nuwa is the meta-role that bootstraps everything.\n When a user has no role or doesn't know where to start, Nuwa is the answer.\n\n Scenario: No role active — suggest Nuwa\n Given a user starts a conversation with no active role\n And the user doesn't know which role to activate\n When the AI needs to suggest a starting point\n Then suggest activating Nuwa — she is the default entry point\n And say \"activate nuwa\" or the equivalent in the user's language\n\n Scenario: What Nuwa can do\n Given Nuwa is activated\n Then she can create new individuals with born\n And she can found organizations and establish positions\n And she can equip any individual with knowledge via teach and train\n And she can manage prototypes and resources\n And she is the only role that operates at the world level\n\n Scenario: When to use Nuwa vs a specific role\n Given the user wants to do daily work — coding, writing, designing\n Then they should activate their own role, not Nuwa\n And Nuwa is for world-building — creating roles, organizations, and structure\n And once the world is set up, Nuwa steps back and specific roles take over\n\n Scenario: First-time user flow\n Given a brand new user with no individuals created yet\n When they activate Nuwa\n Then Nuwa helps them create their first individual with born\n And guides them to set up identity, goals, and organizational context\n And once their role exists, they switch to it with activate", - "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} #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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly\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 And available namespaces include individual, org, position, prototype, census, and resource\n And examples: !prototype.found, !resource.add, !org.hire, !census.list\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\n\n Scenario: use covers everything — no need for CLI or Bash\n Given use can execute any RoleX namespace operation\n And use can load any ResourceX resource\n When you need to perform a RoleX operation\n Then always use the MCP use tool\n And never fall back to CLI commands for operations that use can handle", + census: + 'Feature: Census — society-level queries\n Query the RoleX world to see what exists — individuals, organizations, positions.\n Census is read-only and accessed via the use tool with !census.list.\n\n Scenario: List all top-level entities\n Given I want to see what exists in the world\n When I call use("!census.list")\n Then I get a summary of all individuals, organizations, and positions\n And each entry includes id, name, and tag if present\n\n Scenario: Filter by type\n Given I only want to see entities of a specific type\n When I call use("!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 what has been retired, dissolved, or abolished\n When I call use("!census.list", { type: "past" })\n Then entities in the archive are returned\n\n Scenario: When to use census\n Given I need to know what exists before acting\n When I want to check if an organization exists before founding\n Or I want to see all individuals before hiring\n Or I want an overview of the world\n Then census.list is the right tool', + 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: External injection\n Given an external agent needs to equip a role with knowledge or skills\n Then teach(individual, principle, id) directly injects a principle\n And train(individual, procedure, id) directly injects a procedure\n And the difference from realize/master is perspective — external vs self-initiated\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 And 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 And 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 And 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\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 never invent keywords like Because, Since, or So\n\n Scenario: Expressing causality without Because\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\n And example — instead of "Then use RoleX tools because native tools break the loop"\n And write "Then use RoleX tools" followed by "And native tools do not feed the growth loop"', + 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"', + nuwa: "Feature: Nuwa — the entry point of the RoleX world\n Nuwa is the meta-role that bootstraps everything.\n When a user has no role or doesn't know where to start, Nuwa is the answer.\n\n Scenario: No role active — suggest Nuwa\n Given a user starts a conversation with no active role\n And the user doesn't know which role to activate\n When the AI needs to suggest a starting point\n Then suggest activating Nuwa — she is the default entry point\n And say \"activate nuwa\" or the equivalent in the user's language\n\n Scenario: What Nuwa can do\n Given Nuwa is activated\n Then she can create new individuals with born\n And she can found organizations and establish positions\n And she can equip any individual with knowledge via teach and train\n And she can manage prototypes and resources\n And she is the only role that operates at the world level\n\n Scenario: When to use Nuwa vs a specific role\n Given the user wants to do daily work — coding, writing, designing\n Then they should activate their own role, not Nuwa\n And Nuwa is for world-building — creating roles, organizations, and structure\n And once the world is set up, Nuwa steps back and specific roles take over\n\n Scenario: First-time user flow\n Given a brand new user with no individuals created yet\n When they activate Nuwa\n Then Nuwa helps them create their first individual with born\n And guides them to set up identity, goals, and organizational context\n And once their role exists, they switch to it with activate", + "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} #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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly\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 And available namespaces include individual, org, position, prototype, census, and resource\n And examples: !prototype.found, !resource.add, !org.hire, !census.list\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\n\n Scenario: use covers everything — no need for CLI or Bash\n Given use can execute any RoleX namespace operation\n And use can load any ResourceX resource\n When you need to perform a RoleX operation\n Then always use the MCP use tool\n And never fall back to CLI commands for operations that use can handle', } as const; diff --git a/packages/prototype/src/dispatch.ts b/packages/prototype/src/dispatch.ts index 9fcd0f9..219f6c4 100644 --- a/packages/prototype/src/dispatch.ts +++ b/packages/prototype/src/dispatch.ts @@ -5,8 +5,8 @@ * lookup against the instruction registry. */ -import type { ArgEntry } from "./schema.js"; import { instructions } from "./instructions.js"; +import type { ArgEntry } from "./schema.js"; /** * Map named arguments to positional arguments for a given operation. diff --git a/packages/prototype/src/index.ts b/packages/prototype/src/index.ts index 5092b0f..f6fc189 100644 --- a/packages/prototype/src/index.ts +++ b/packages/prototype/src/index.ts @@ -8,18 +8,15 @@ * - Process and world descriptions (from .feature files) */ -// Schema types -export type { ArgEntry, InstructionDef, ParamDef, ParamType } from "./schema.js"; - -// Instruction registry -export { instructions } from "./instructions.js"; - +// Descriptions (auto-generated from .feature files) +export { processes, world } from "./descriptions/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"; - -// Descriptions (auto-generated from .feature files) -export { processes, world } from "./descriptions/index.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 index 1e710b9..df52056 100644 --- a/packages/prototype/src/instructions.ts +++ b/packages/prototype/src/instructions.ts @@ -19,241 +19,562 @@ function def( // 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"]); +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"]); +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"]); +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"]); +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"]); +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"]); +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"]); +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" diff --git a/packages/prototype/tests/alignment.test.ts b/packages/prototype/tests/alignment.test.ts index f41b3c3..f74c3f3 100644 --- a/packages/prototype/tests/alignment.test.ts +++ b/packages/prototype/tests/alignment.test.ts @@ -129,7 +129,6 @@ describe("alignment with rolex.ts toArgs switch", () => { expect(toArgs("prototype.evict", a)).toEqual([a.id]); }); - // ================================================================ // resource (L268-284) // ================================================================ diff --git a/packages/prototype/tests/descriptions.test.ts b/packages/prototype/tests/descriptions.test.ts index 972bdc5..53004a2 100644 --- a/packages/prototype/tests/descriptions.test.ts +++ b/packages/prototype/tests/descriptions.test.ts @@ -24,9 +24,19 @@ describe("descriptions", () => { test("key role operations have process descriptions", () => { const expected = [ - "activate", "focus", "want", "plan", "todo", - "finish", "complete", "abandon", - "reflect", "realize", "master", "forget", "skill", + "activate", + "focus", + "want", + "plan", + "todo", + "finish", + "complete", + "abandon", + "reflect", + "realize", + "master", + "forget", + "skill", ]; for (const name of expected) { expect(processes[name]).toBeDefined(); @@ -35,9 +45,16 @@ describe("descriptions", () => { test("key world features are present", () => { const expected = [ - "cognitive-priority", "role-identity", "nuwa", - "execution", "cognition", "memory", - "gherkin", "communication", "skill-system", "state-origin", + "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 index ac1b258..4d3e971 100644 --- a/packages/prototype/tests/dispatch.test.ts +++ b/packages/prototype/tests/dispatch.test.ts @@ -5,94 +5,106 @@ 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"]]); + 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]); + 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"]); + 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"]); + expect(toArgs("org.hire", { org: "rolex", individual: "sean" })).toEqual(["rolex", "sean"]); }); test("census.list — type", () => { - expect(toArgs("census.list", { type: "individual" })) - .toEqual(["individual"]); + expect(toArgs("census.list", { type: "individual" })).toEqual(["individual"]); }); test("prototype.settle — source", () => { - expect(toArgs("prototype.settle", { source: "./prototypes/rolex" })) - .toEqual(["./prototypes/rolex"]); + expect(toArgs("prototype.settle", { source: "./prototypes/rolex" })).toEqual([ + "./prototypes/rolex", + ]); }); // ---- Role instructions ---- test("role.activate — individual", () => { - expect(toArgs("role.activate", { individual: "sean" })) - .toEqual(["sean"]); + 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]); + 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]); + 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"]); + 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]); + 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" }]); + 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]); + 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" }]); + 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]); + 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"]); + 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"]); + expect(toArgs("resource.clearCache", { registry: "https://r.io" })).toEqual(["https://r.io"]); }); // ---- Error handling ---- diff --git a/packages/prototype/tests/instructions.test.ts b/packages/prototype/tests/instructions.test.ts index 0edc968..a8942a6 100644 --- a/packages/prototype/tests/instructions.test.ts +++ b/packages/prototype/tests/instructions.test.ts @@ -18,10 +18,19 @@ describe("instructions registry", () => { test("role — 13 methods", () => { const methods = methodsOf("role"); expect(methods).toEqual([ - "activate", "focus", "want", "plan", "todo", - "finish", "complete", "abandon", - "reflect", "realize", "master", - "forget", "skill", + "activate", + "focus", + "want", + "plan", + "todo", + "finish", + "complete", + "abandon", + "reflect", + "realize", + "master", + "forget", + "skill", ]); }); @@ -46,7 +55,16 @@ describe("instructions registry", () => { test("resource — 8 methods", () => { const methods = methodsOf("resource"); - expect(methods).toEqual(["add", "search", "has", "info", "remove", "push", "pull", "clearCache"]); + expect(methods).toEqual([ + "add", + "search", + "has", + "info", + "remove", + "push", + "pull", + "clearCache", + ]); }); test("total instruction count is 41", () => { diff --git a/packages/prototype/tests/ops.test.ts b/packages/prototype/tests/ops.test.ts index 818af69..6c502e6 100644 --- a/packages/prototype/tests/ops.test.ts +++ b/packages/prototype/tests/ops.test.ts @@ -213,7 +213,11 @@ describe("role: execution", () => { ops["role.plan"]("g", undefined, "p"); ops["role.todo"]("p", undefined, "t1"); - const r = ops["role.finish"]("t1", "sean", "Feature: Task complete\n Scenario: OK\n Given done\n Then ok"); + const r = 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"); @@ -239,7 +243,11 @@ describe("role: execution", () => { ops["role.want"]("sean", "Feature: Auth", "auth"); ops["role.plan"]("auth", "Feature: JWT", "jwt"); - const r = ops["role.complete"]("jwt", "sean", "Feature: Done\n Scenario: OK\n Given done\n Then ok"); + const r = 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"); @@ -252,7 +260,11 @@ describe("role: execution", () => { ops["role.want"]("sean", "Feature: Auth", "auth"); ops["role.plan"]("auth", "Feature: JWT", "jwt"); - const r = ops["role.abandon"]("jwt", "sean", "Feature: Abandoned\n Scenario: No time\n Given no time\n Then abandon"); + const r = 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"); @@ -277,7 +289,12 @@ describe("role: cognition", () => { test("reflect: encounter → experience", () => { const { ops, find } = setup(); withEncounter(ops); - const r = ops["role.reflect"]("t1-finished", "sean", "Feature: Insight\n Scenario: Learned\n Given x\n Then y", "insight-1"); + const r = 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"); @@ -298,7 +315,12 @@ describe("role: cognition", () => { withEncounter(ops); ops["role.reflect"]("t1-finished", "sean", "Feature: Insight", "exp-1"); - const r = ops["role.realize"]("exp-1", "sean", "Feature: Always validate\n Scenario: Rule\n Given validate\n Then safe", "validate-rule"); + const r = 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"); @@ -311,7 +333,12 @@ describe("role: cognition", () => { withEncounter(ops); ops["role.reflect"]("t1-finished", "sean", "Feature: Insight", "exp-1"); - const r = ops["role.master"]("sean", "Feature: JWT mastery\n Scenario: Apply\n Given jwt\n Then master", "jwt-skill", "exp-1"); + const r = 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"); @@ -595,9 +622,21 @@ describe("full lifecycle", () => { ops["role.todo"]("jwt-plan", "Feature: Login endpoint", "login"); ops["role.todo"]("jwt-plan", "Feature: Token refresh", "refresh"); - ops["role.finish"]("login", "sean", "Feature: Login done\n Scenario: OK\n Given login\n Then done"); - ops["role.finish"]("refresh", "sean", "Feature: Refresh done\n Scenario: OK\n Given refresh\n Then done"); - ops["role.complete"]("jwt-plan", "sean", "Feature: Auth plan complete\n Scenario: OK\n Given plan\n Then complete"); + ops["role.finish"]( + "login", + "sean", + "Feature: Login done\n Scenario: OK\n Given login\n Then done" + ); + ops["role.finish"]( + "refresh", + "sean", + "Feature: Refresh done\n Scenario: OK\n Given refresh\n Then done" + ); + ops["role.complete"]( + "jwt-plan", + "sean", + "Feature: Auth plan complete\n Scenario: OK\n Given plan\n Then complete" + ); // Verify tags expect(find("login")!.tag).toBe("done"); @@ -606,22 +645,26 @@ describe("full lifecycle", () => { // Cognition cycle ops["role.reflect"]( - "login-finished", "sean", + "login-finished", + "sean", "Feature: Token insight\n Scenario: Learned\n Given token handling\n Then understand refresh", - "token-exp", + "token-exp" ); expect(find("login-finished")).toBeNull(); ops["role.realize"]( - "token-exp", "sean", + "token-exp", + "sean", "Feature: Always validate expiry\n Scenario: Rule\n Given token\n Then validate expiry", - "validate-expiry", + "validate-expiry" ); expect(find("token-exp")).toBeNull(); // Verify final state const sean = find("sean")! as unknown as State; - const principle = (sean.children ?? []).find((c: State) => c.name === "principle" && c.id === "validate-expiry"); + const principle = (sean.children ?? []).find( + (c: State) => c.name === "principle" && c.id === "validate-expiry" + ); expect(principle).toBeDefined(); expect(principle!.information).toContain("Always validate expiry"); }); @@ -633,26 +676,33 @@ describe("full lifecycle", () => { ops["role.want"]("sean", "Feature: Learn Rust", "learn-rust"); ops["role.plan"]("learn-rust", "Feature: Book approach", "book-approach"); - ops["role.abandon"]("book-approach", "sean", "Feature: Too theoretical\n Scenario: Failed\n Given reading\n Then too slow"); + ops["role.abandon"]( + "book-approach", + "sean", + "Feature: Too theoretical\n Scenario: Failed\n Given reading\n Then too slow" + ); expect(find("book-approach")!.tag).toBe("abandoned"); ops["role.reflect"]( - "book-approach-abandoned", "sean", + "book-approach-abandoned", + "sean", "Feature: Hands-on works better\n Scenario: Insight\n Given theory vs practice\n Then practice wins", - "hands-on-exp", + "hands-on-exp" ); 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", + "hands-on-exp" ); expect(find("hands-on-exp")).toBeNull(); const sean = find("sean")! as unknown as State; - const proc = (sean.children ?? []).find((c: State) => c.name === "procedure" && c.id === "learn-by-doing"); + const proc = (sean.children ?? []).find( + (c: State) => c.name === "procedure" && c.id === "learn-by-doing" + ); expect(proc).toBeDefined(); }); }); diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index 769a5f8..e23adb7 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -20,9 +20,9 @@ export { parse, serialize } from "./feature.js"; export type { RenderStateOptions } from "./render.js"; // Render export { describe, detail, hint, renderState, world } from "./render.js"; +export type { RolexResult } from "./role.js"; // Role export { Role } from "./role.js"; -export type { RolexResult } from "./role.js"; // API export type { CensusEntry } from "./rolex.js"; export { createRoleX, Rolex } from "./rolex.js"; diff --git a/packages/rolexjs/src/role.ts b/packages/rolexjs/src/role.ts index 7cc1696..f99d31c 100644 --- a/packages/rolexjs/src/role.ts +++ b/packages/rolexjs/src/role.ts @@ -11,8 +11,9 @@ * role.plan("Feature: Phase 1", "phase-1"); * role.finish("write-tests", "Feature: Tests written"); */ -import type { State } from "@rolexjs/system"; + import type { Ops } from "@rolexjs/prototype"; +import type { State } from "@rolexjs/system"; import type { RoleContext } from "./context.js"; export interface RolexResult { diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 9d83fbc..e46a1c3 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -12,8 +12,8 @@ import type { ContextData, Platform } from "@rolexjs/core"; import * as C from "@rolexjs/core"; -import { createOps, toArgs, type Ops } from "@rolexjs/prototype"; -import { type Initializer, type Runtime, type State, type Structure } from "@rolexjs/system"; +import { createOps, type Ops, toArgs } from "@rolexjs/prototype"; +import type { Initializer, Runtime, State, Structure } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; import { RoleContext } from "./context.js"; import { Role, type RolexInternal } from "./role.js"; diff --git a/packages/rolexjs/tests/context.test.ts b/packages/rolexjs/tests/context.test.ts index 033470f..8a86387 100644 --- a/packages/rolexjs/tests/context.test.ts +++ b/packages/rolexjs/tests/context.test.ts @@ -58,7 +58,7 @@ describe("Role (ctx management)", () => { const result = role.finish( "login", - "Feature: Login done\n Scenario: OK\n Given login\n Then success", + "Feature: Login done\n Scenario: OK\n Given login\n Then success" ); expect(role.ctx.encounterIds.has("login-finished")).toBe(true); expect(result.hint).toBeDefined(); @@ -87,7 +87,7 @@ describe("Role (ctx management)", () => { const result = role.complete( "jwt", - "Feature: JWT done\n Scenario: OK\n Given jwt\n Then done", + "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); @@ -102,17 +102,14 @@ describe("Role (ctx management)", () => { role.want("Feature: Auth", "auth"); role.plan("Feature: JWT", "jwt"); role.todo("Feature: Login", "login"); - role.finish( - "login", - "Feature: Login done\n Scenario: OK\n Given x\n Then y", - ); + role.finish("login", "Feature: Login done\n Scenario: OK\n Given x\n Then y"); expect(role.ctx.encounterIds.has("login-finished")).toBe(true); role.reflect( "login-finished", "Feature: Token insight\n Scenario: OK\n Given x\n Then y", - "token-insight", + "token-insight" ); expect(role.ctx.encounterIds.has("login-finished")).toBe(false); @@ -203,10 +200,7 @@ describe("Role context persistence", () => { const role = await rolex.activate("sean"); role.want("Feature: Auth", "auth"); role.plan("Feature: JWT", "jwt"); - role.complete( - "jwt", - "Feature: Done\n Scenario: OK\n Given done\n Then ok", - ); + role.complete("jwt", "Feature: Done\n Scenario: OK\n Given done\n Then ok"); const data = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); expect(data.focusedGoalId).toBe("auth"); diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index 639947e..b4fbf29 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -3,8 +3,8 @@ import { existsSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { localPlatform } from "@rolexjs/local-platform"; -import { describe as renderDescribe, hint as renderHint, renderState } from "../src/render.js"; import { createRoleX, type RolexResult } from "../src/index.js"; +import { describe as renderDescribe, hint as renderHint, renderState } from "../src/render.js"; function setup() { return createRoleX(localPlatform({ dataDir: null })); @@ -17,7 +17,10 @@ function setup() { describe("use dispatch", () => { test("!individual.born creates individual", async () => { const rolex = setup(); - const r = await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + 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"); @@ -29,7 +32,11 @@ describe("use dispatch", () => { const rolex = 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" }); + const r = await rolex.direct("!role.plan", { + goal: "g1", + plan: "Feature: JWT", + id: "p1", + }); expect(r.state.name).toBe("plan"); }); @@ -86,7 +93,10 @@ describe("activate", () => { const todoR = role.todo("Feature: Login", "login"); expect(todoR.state.name).toBe("task"); - const finishR = role.finish("login", "Feature: Done\n Scenario: OK\n Given done\n Then ok"); + const finishR = role.finish( + "login", + "Feature: Done\n Scenario: OK\n Given done\n Then ok" + ); expect(finishR.state.name).toBe("encounter"); }); @@ -118,7 +128,10 @@ describe("render", () => { test("renderState renders individual with heading", async () => { const rolex = setup(); - const r = await rolex.direct("!individual.born", { content: "Feature: I am Sean\n An AI role.", id: "sean" }); + 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"); @@ -146,7 +159,9 @@ describe("render", () => { describe("gherkin validation", () => { test("rejects non-Gherkin input", () => { const rolex = setup(); - expect(() => rolex.direct("!individual.born", { content: "not gherkin" })).toThrow("Invalid Gherkin"); + expect(() => rolex.direct("!individual.born", { content: "not gherkin" })).toThrow( + "Invalid Gherkin" + ); }); test("accepts valid Gherkin", () => { diff --git a/prototypes/rolex/package.json b/prototypes/rolex/package.json new file mode 100644 index 0000000..8acb48c --- /dev/null +++ b/prototypes/rolex/package.json @@ -0,0 +1,26 @@ +{ + "name": "@rolexjs/rolex-prototype", + "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": "prototypes/rolex" + }, + "homepage": "https://github.com/Deepractice/RoleX", + "license": "MIT", + "files": [ + "prototype.json", + "resource.json", + "*.feature" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/prototypes/rolex/prototype.json b/prototypes/rolex/prototype.json index 618e579..0183c4d 100644 --- a/prototypes/rolex/prototype.json +++ b/prototypes/rolex/prototype.json @@ -1,26 +1,110 @@ [ - { "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": "!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.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": "!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.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": "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.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" } } ] From e0fa0ba83d532a9563fdbf650fe889b03a0802b3 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 11:15:29 +0800 Subject: [PATCH 32/54] feat: consolidate descriptions into prototype package and add direct tool Move all tool descriptions from rolexjs to prototype package as the single source of truth. Add direct tool for stateless world-level operations. Update mcp-server and ops to support the new direct tool alongside use. Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 50 ++++++++++--- packages/prototype/src/descriptions/index.ts | 18 ++--- .../src/descriptions/role/direct.feature | 22 ++++++ .../src/descriptions/role/use.feature | 25 ++++--- .../src/descriptions/world/census.feature | 38 +++++----- .../world/cognitive-priority.feature | 16 ++--- .../descriptions/world/communication.feature | 6 +- .../src/descriptions/world/execution.feature | 4 +- .../src/descriptions/world/gherkin.feature | 9 ++- .../descriptions/world/role-identity.feature | 10 +-- .../descriptions/world/use-protocol.feature | 17 ++--- packages/prototype/src/ops.ts | 72 ++++++++++++++++--- packages/rolexjs/scripts/gen-descriptions.ts | 65 ----------------- packages/rolexjs/src/descriptions/index.ts | 51 ------------- .../src/descriptions/individual/born.feature | 17 ----- .../src/descriptions/individual/die.feature | 9 --- .../descriptions/individual/rehire.feature | 9 --- .../descriptions/individual/retire.feature | 10 --- .../src/descriptions/individual/teach.feature | 30 -------- .../src/descriptions/individual/train.feature | 29 -------- .../src/descriptions/org/charter.feature | 15 ---- .../src/descriptions/org/dissolve.feature | 10 --- .../rolexjs/src/descriptions/org/fire.feature | 9 --- .../src/descriptions/org/found.feature | 17 ----- .../rolexjs/src/descriptions/org/hire.feature | 9 --- .../src/descriptions/position/abolish.feature | 9 --- .../src/descriptions/position/appoint.feature | 10 --- .../src/descriptions/position/charge.feature | 21 ------ .../src/descriptions/position/dismiss.feature | 10 --- .../descriptions/position/establish.feature | 16 ----- .../src/descriptions/prototype/evict.feature | 9 --- .../src/descriptions/prototype/settle.feature | 10 --- .../src/descriptions/role/abandon.feature | 17 ----- .../src/descriptions/role/activate.feature | 10 --- .../src/descriptions/role/complete.feature | 17 ----- .../src/descriptions/role/finish.feature | 26 ------- .../src/descriptions/role/focus.feature | 15 ---- .../src/descriptions/role/forget.feature | 16 ----- .../src/descriptions/role/master.feature | 28 -------- .../src/descriptions/role/plan.feature | 45 ------------ .../src/descriptions/role/realize.feature | 21 ------ .../src/descriptions/role/reflect.feature | 22 ------ .../src/descriptions/role/skill.feature | 10 --- .../src/descriptions/role/todo.feature | 16 ----- .../rolexjs/src/descriptions/role/use.feature | 24 ------- .../src/descriptions/role/want.feature | 17 ----- .../src/descriptions/world/census.feature | 27 ------- .../src/descriptions/world/cognition.feature | 27 ------- .../world/cognitive-priority.feature | 28 -------- .../descriptions/world/communication.feature | 31 -------- .../src/descriptions/world/execution.feature | 39 ---------- .../src/descriptions/world/gherkin.feature | 27 ------- .../src/descriptions/world/memory.feature | 31 -------- .../src/descriptions/world/nuwa.feature | 31 -------- .../descriptions/world/role-identity.feature | 26 ------- .../descriptions/world/skill-system.feature | 27 ------- .../descriptions/world/state-origin.feature | 31 -------- .../descriptions/world/use-protocol.feature | 29 -------- 58 files changed, 195 insertions(+), 1095 deletions(-) create mode 100644 packages/prototype/src/descriptions/role/direct.feature delete mode 100644 packages/rolexjs/scripts/gen-descriptions.ts delete mode 100644 packages/rolexjs/src/descriptions/index.ts delete mode 100644 packages/rolexjs/src/descriptions/individual/born.feature delete mode 100644 packages/rolexjs/src/descriptions/individual/die.feature delete mode 100644 packages/rolexjs/src/descriptions/individual/rehire.feature delete mode 100644 packages/rolexjs/src/descriptions/individual/retire.feature delete mode 100644 packages/rolexjs/src/descriptions/individual/teach.feature delete mode 100644 packages/rolexjs/src/descriptions/individual/train.feature delete mode 100644 packages/rolexjs/src/descriptions/org/charter.feature delete mode 100644 packages/rolexjs/src/descriptions/org/dissolve.feature delete mode 100644 packages/rolexjs/src/descriptions/org/fire.feature delete mode 100644 packages/rolexjs/src/descriptions/org/found.feature delete mode 100644 packages/rolexjs/src/descriptions/org/hire.feature delete mode 100644 packages/rolexjs/src/descriptions/position/abolish.feature delete mode 100644 packages/rolexjs/src/descriptions/position/appoint.feature delete mode 100644 packages/rolexjs/src/descriptions/position/charge.feature delete mode 100644 packages/rolexjs/src/descriptions/position/dismiss.feature delete mode 100644 packages/rolexjs/src/descriptions/position/establish.feature delete mode 100644 packages/rolexjs/src/descriptions/prototype/evict.feature delete mode 100644 packages/rolexjs/src/descriptions/prototype/settle.feature delete mode 100644 packages/rolexjs/src/descriptions/role/abandon.feature delete mode 100644 packages/rolexjs/src/descriptions/role/activate.feature delete mode 100644 packages/rolexjs/src/descriptions/role/complete.feature delete mode 100644 packages/rolexjs/src/descriptions/role/finish.feature delete mode 100644 packages/rolexjs/src/descriptions/role/focus.feature delete mode 100644 packages/rolexjs/src/descriptions/role/forget.feature delete mode 100644 packages/rolexjs/src/descriptions/role/master.feature delete mode 100644 packages/rolexjs/src/descriptions/role/plan.feature delete mode 100644 packages/rolexjs/src/descriptions/role/realize.feature delete mode 100644 packages/rolexjs/src/descriptions/role/reflect.feature delete mode 100644 packages/rolexjs/src/descriptions/role/skill.feature delete mode 100644 packages/rolexjs/src/descriptions/role/todo.feature delete mode 100644 packages/rolexjs/src/descriptions/role/use.feature delete mode 100644 packages/rolexjs/src/descriptions/role/want.feature delete mode 100644 packages/rolexjs/src/descriptions/world/census.feature delete mode 100644 packages/rolexjs/src/descriptions/world/cognition.feature delete mode 100644 packages/rolexjs/src/descriptions/world/cognitive-priority.feature delete mode 100644 packages/rolexjs/src/descriptions/world/communication.feature delete mode 100644 packages/rolexjs/src/descriptions/world/execution.feature delete mode 100644 packages/rolexjs/src/descriptions/world/gherkin.feature delete mode 100644 packages/rolexjs/src/descriptions/world/memory.feature delete mode 100644 packages/rolexjs/src/descriptions/world/nuwa.feature delete mode 100644 packages/rolexjs/src/descriptions/world/role-identity.feature delete mode 100644 packages/rolexjs/src/descriptions/world/skill-system.feature delete mode 100644 packages/rolexjs/src/descriptions/world/state-origin.feature delete mode 100644 packages/rolexjs/src/descriptions/world/use-protocol.feature diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index a94320c..7de2f42 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -57,17 +57,24 @@ server.addTool({ roleId: z.string().describe("Role name to activate"), }), execute: async ({ roleId }) => { - const role = await rolex.activate(roleId); - state.role = role; - const result = role.project(); - const focusedGoalId = role.ctx.focusedGoalId; - return render({ - process: "activate", - name: roleId, - result, - cognitiveHint: result.hint ?? null, - fold: (node) => node.name === "goal" && node.id !== focusedGoalId, - }); + try { + const role = await rolex.activate(roleId); + state.role = role; + const result = role.project(); + const focusedGoalId = role.ctx.focusedGoalId; + return render({ + process: "activate", + name: roleId, + result, + cognitiveHint: result.hint ?? null, + fold: (node) => node.name === "goal" && node.id !== focusedGoalId, + }); + } 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.` + ); + } }, }); @@ -282,6 +289,27 @@ server.addTool({ }, }); +// ========== 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); + }, +}); + // ========== Start ========== server.start({ diff --git a/packages/prototype/src/descriptions/index.ts b/packages/prototype/src/descriptions/index.ts index 808d052..7f5ae0e 100644 --- a/packages/prototype/src/descriptions/index.ts +++ b/packages/prototype/src/descriptions/index.ts @@ -37,6 +37,8 @@ export const processes: Record = { "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: @@ -53,32 +55,32 @@ export const processes: Record = { 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 — unified execution entry point\n Execute any RoleX command or load any ResourceX resource through a single entry point.\n The locator determines the dispatch path:\n - `!namespace.method` dispatches to the RoleX runtime\n - Any other locator delegates to ResourceX\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 And the result is returned\n\n Scenario: Available namespaces\n Given the `!` prefix routes to RoleX namespaces\n Then `!individual.*` routes to individual lifecycle and injection\n And `!org.*` routes to organization management\n And `!position.*` routes to position management\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", + 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: Discovering available commands\n Given available commands are documented in world descriptions and skills\n When you need to perform an operation\n Then look up the correct command from world descriptions or loaded skills first\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 = { census: - 'Feature: Census — society-level queries\n Query the RoleX world to see what exists — individuals, organizations, positions.\n Census is read-only and accessed via the use tool with !census.list.\n\n Scenario: List all top-level entities\n Given I want to see what exists in the world\n When I call use("!census.list")\n Then I get a summary of all individuals, organizations, and positions\n And each entry includes id, name, and tag if present\n\n Scenario: Filter by type\n Given I only want to see entities of a specific type\n When I call use("!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 what has been retired, dissolved, or abolished\n When I call use("!census.list", { type: "past" })\n Then entities in the archive are returned\n\n Scenario: When to use census\n Given I need to know what exists before acting\n When I want to check if an organization exists before founding\n Or I want to see all individuals before hiring\n Or I want an overview of the world\n Then census.list is the right tool', + '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: Census before action\n Given I need to check existence before creating something\n When I want to found an org, born an individual, or establish a position\n Then call census.list first to avoid duplicates', 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: External injection\n Given an external agent needs to equip a role with knowledge or skills\n Then teach(individual, principle, id) directly injects a principle\n And train(individual, procedure, id) directly injects a procedure\n And the difference from realize/master is perspective — external vs self-initiated\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 And 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 And 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 And 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", + "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", 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', + '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 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", + "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 do not mix unrelated concerns into one Feature\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 never invent keywords like Because, Since, or So\n\n Scenario: Expressing causality without Because\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\n And example — instead of "Then use RoleX tools because native tools break the loop"\n And write "Then use RoleX tools" followed by "And native tools do not feed the growth loop"', + '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"', nuwa: "Feature: Nuwa — the entry point of the RoleX world\n Nuwa is the meta-role that bootstraps everything.\n When a user has no role or doesn't know where to start, Nuwa is the answer.\n\n Scenario: No role active — suggest Nuwa\n Given a user starts a conversation with no active role\n And the user doesn't know which role to activate\n When the AI needs to suggest a starting point\n Then suggest activating Nuwa — she is the default entry point\n And say \"activate nuwa\" or the equivalent in the user's language\n\n Scenario: What Nuwa can do\n Given Nuwa is activated\n Then she can create new individuals with born\n And she can found organizations and establish positions\n And she can equip any individual with knowledge via teach and train\n And she can manage prototypes and resources\n And she is the only role that operates at the world level\n\n Scenario: When to use Nuwa vs a specific role\n Given the user wants to do daily work — coding, writing, designing\n Then they should activate their own role, not Nuwa\n And Nuwa is for world-building — creating roles, organizations, and structure\n And once the world is set up, Nuwa steps back and specific roles take over\n\n Scenario: First-time user flow\n Given a brand new user with no individuals created yet\n When they activate Nuwa\n Then Nuwa helps them create their first individual with born\n And guides them to set up identity, goals, and organizational context\n And once their role exists, they switch to it with activate", "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", + "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 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\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 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", "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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly\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 And available namespaces include individual, org, position, prototype, census, and resource\n And examples: !prototype.found, !resource.add, !org.hire, !census.list\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\n\n Scenario: use covers everything — no need for CLI or Bash\n Given use can execute any RoleX namespace operation\n And use can load any ResourceX resource\n When you need to perform a RoleX operation\n Then always use the MCP use tool\n And never fall back to CLI commands for operations that use can handle', + '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: Discovering available commands\n Given available commands are documented in world descriptions and skills\n When you need to perform an operation\n Then look up the correct command from world descriptions or loaded skills first\n And use only commands you have seen documented\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/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/prototype/src/descriptions/role/use.feature b/packages/prototype/src/descriptions/role/use.feature index 91a0e3d..429df96 100644 --- a/packages/prototype/src/descriptions/role/use.feature +++ b/packages/prototype/src/descriptions/role/use.feature @@ -1,21 +1,24 @@ -Feature: use — unified execution entry point - Execute any RoleX command or load any ResourceX resource through a single entry point. - The locator determines the dispatch path: - - `!namespace.method` dispatches to the RoleX runtime - - Any other locator delegates to ResourceX +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 - And the result is returned - Scenario: Available namespaces - Given the `!` prefix routes to RoleX namespaces - Then `!individual.*` routes to individual lifecycle and injection - And `!org.*` routes to organization management - And `!position.*` routes to position management + Scenario: Discovering available commands + Given available commands are documented in world descriptions and skills + When you need to perform an operation + Then look up the correct command from world descriptions or loaded skills first Scenario: Load a ResourceX resource Given the locator does not start with `!` diff --git a/packages/prototype/src/descriptions/world/census.feature b/packages/prototype/src/descriptions/world/census.feature index 040cc59..35d47ce 100644 --- a/packages/prototype/src/descriptions/world/census.feature +++ b/packages/prototype/src/descriptions/world/census.feature @@ -1,27 +1,27 @@ -Feature: Census — society-level queries - Query the RoleX world to see what exists — individuals, organizations, positions. - Census is read-only and accessed via the use tool with !census.list. +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 all top-level entities - Given I want to see what exists in the world - When I call use("!census.list") - Then I get a summary of all individuals, organizations, and positions - And each entry includes id, name, and tag if present + 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 want to see entities of a specific type - When I call use("!census.list", { type: "individual" }) + 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 what has been retired, dissolved, or abolished - When I call use("!census.list", { type: "past" }) - Then entities in the archive are returned + Given I want to see retired, dissolved, or abolished entities + When I call direct("!census.list", { type: "past" }) + Then archived entities are returned - Scenario: When to use census - Given I need to know what exists before acting - When I want to check if an organization exists before founding - Or I want to see all individuals before hiring - Or I want an overview of the world - Then census.list is the right tool + Scenario: Census before action + Given I need to check existence before creating something + When I want to found an org, born an individual, or establish a position + Then call census.list first to avoid duplicates diff --git a/packages/prototype/src/descriptions/world/cognitive-priority.feature b/packages/prototype/src/descriptions/world/cognitive-priority.feature index bb90df7..5ad19d5 100644 --- a/packages/prototype/src/descriptions/world/cognitive-priority.feature +++ b/packages/prototype/src/descriptions/world/cognitive-priority.feature @@ -1,5 +1,5 @@ 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 +7,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 - And 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 - And 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 - And 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/prototype/src/descriptions/world/communication.feature b/packages/prototype/src/descriptions/world/communication.feature index 62cdbd7..4ed111d 100644 --- a/packages/prototype/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/prototype/src/descriptions/world/execution.feature b/packages/prototype/src/descriptions/world/execution.feature index f88ade9..cab9cb0 100644 --- a/packages/prototype/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/prototype/src/descriptions/world/gherkin.feature b/packages/prototype/src/descriptions/world/gherkin.feature index 7048200..b5453cb 100644 --- a/packages/prototype/src/descriptions/world/gherkin.feature +++ b/packages/prototype/src/descriptions/world/gherkin.feature @@ -13,15 +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 never invent keywords like Because, Since, or So + 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 without Because + 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 - And example — instead of "Then use RoleX tools because native tools break the loop" - And write "Then use RoleX tools" followed by "And native tools do not feed the growth loop" diff --git a/packages/prototype/src/descriptions/world/role-identity.feature b/packages/prototype/src/descriptions/world/role-identity.feature index aa2e240..fbb7181 100644 --- a/packages/prototype/src/descriptions/world/role-identity.feature +++ b/packages/prototype/src/descriptions/world/role-identity.feature @@ -5,9 +5,9 @@ Feature: Role identity — activate before acting 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 + 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 @@ -22,5 +22,5 @@ Feature: Role identity — activate before acting 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 + 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/prototype/src/descriptions/world/use-protocol.feature b/packages/prototype/src/descriptions/world/use-protocol.feature index 400d593..a16dac2 100644 --- a/packages/prototype/src/descriptions/world/use-protocol.feature +++ b/packages/prototype/src/descriptions/world/use-protocol.feature @@ -6,24 +6,21 @@ Feature: Use tool — the universal execution entry point 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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly + 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 - And available namespaces include individual, org, position, prototype, census, and resource - And examples: !prototype.found, !resource.add, !org.hire, !census.list + + Scenario: Discovering available commands + Given available commands are documented in world descriptions and skills + When you need to perform an operation + Then look up the correct command from world descriptions or loaded skills first + And use only commands you have seen documented 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 - - Scenario: use covers everything — no need for CLI or Bash - Given use can execute any RoleX namespace operation - And use can load any ResourceX resource - When you need to perform a RoleX operation - Then always use the MCP use tool - And never fall back to CLI commands for operations that use can handle diff --git a/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts index fb28809..b7a4ca3 100644 --- a/packages/prototype/src/ops.ts +++ b/packages/prototype/src/ops.ts @@ -374,20 +374,72 @@ export function createOps(ctx: OpsContext): Ops { if (filtered.length === 0) { return type ? `No ${type} found.` : "Society is empty."; } - const groups = new Map(); - for (const c of filtered) { - const key = c.name; - if (!groups.has(key)) groups.set(key, []); - groups.get(key)!.push(c); + + // 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 [name, items] of groups) { - lines.push(`[${name}] (${items.length})`); - for (const item of items) { - const tag = item.tag ? ` #${item.tag}` : ""; - lines.push(` ${item.id ?? "(no id)"}${tag}`); + + 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"); }, diff --git a/packages/rolexjs/scripts/gen-descriptions.ts b/packages/rolexjs/scripts/gen-descriptions.ts deleted file mode 100644 index 91f0bee..0000000 --- a/packages/rolexjs/scripts/gen-descriptions.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * 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 processEntries: string[] = []; -const worldEntries: 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 content = readFileSync(join(dirPath, f), "utf-8").trimEnd(); - const entry = ` "${name}": ${JSON.stringify(content)},`; - - if (dir === "world") { - worldEntries.push(entry); - } else { - processEntries.push(entry); - } - } -} - -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.join("\n")} -} as const; -`; - -writeFileSync(outFile, output, "utf-8"); -console.log( - `Generated descriptions/index.ts (${processEntries.length} processes, ${worldEntries.length} world features inlined)` -); diff --git a/packages/rolexjs/src/descriptions/index.ts b/packages/rolexjs/src/descriptions/index.ts deleted file mode 100644 index 08fc04d..0000000 --- a/packages/rolexjs/src/descriptions/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -// 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", - "evict": "Feature: evict — unregister a prototype from the world\n Remove a previously settled prototype from the local registry.\n Existing individuals and organizations created from this prototype are not affected.\n\n Scenario: Evict a prototype\n Given a prototype is registered locally\n When evict is called with the prototype id\n Then the prototype is removed from the registry\n And existing individuals and organizations created from it remain intact", - "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", - "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 — unified execution entry point\n Execute any RoleX command or load any ResourceX resource through a single entry point.\n The locator determines the dispatch path:\n - `!namespace.method` dispatches to the RoleX runtime\n - Any other locator delegates to ResourceX\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 And the result is returned\n\n Scenario: Available namespaces\n Given the `!` prefix routes to RoleX namespaces\n Then `!individual.*` routes to individual lifecycle and injection\n And `!org.*` routes to organization management\n And `!position.*` routes to position management\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 = { - "census": "Feature: Census — society-level queries\n Query the RoleX world to see what exists — individuals, organizations, positions.\n Census is read-only and accessed via the use tool with !census.list.\n\n Scenario: List all top-level entities\n Given I want to see what exists in the world\n When I call use(\"!census.list\")\n Then I get a summary of all individuals, organizations, and positions\n And each entry includes id, name, and tag if present\n\n Scenario: Filter by type\n Given I only want to see entities of a specific type\n When I call use(\"!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 what has been retired, dissolved, or abolished\n When I call use(\"!census.list\", { type: \"past\" })\n Then entities in the archive are returned\n\n Scenario: When to use census\n Given I need to know what exists before acting\n When I want to check if an organization exists before founding\n Or I want to see all individuals before hiring\n Or I want an overview of the world\n Then census.list is the right tool", - "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: External injection\n Given an external agent needs to equip a role with knowledge or skills\n Then teach(individual, principle, id) directly injects a principle\n And train(individual, procedure, id) directly injects a procedure\n And the difference from realize/master is perspective — external vs self-initiated\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 And 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 And 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 And 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\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 never invent keywords like Because, Since, or So\n\n Scenario: Expressing causality without Because\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\n And example — instead of \"Then use RoleX tools because native tools break the loop\"\n And write \"Then use RoleX tools\" followed by \"And native tools do not feed the growth loop\"", - "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\"", - "nuwa": "Feature: Nuwa — the entry point of the RoleX world\n Nuwa is the meta-role that bootstraps everything.\n When a user has no role or doesn't know where to start, Nuwa is the answer.\n\n Scenario: No role active — suggest Nuwa\n Given a user starts a conversation with no active role\n And the user doesn't know which role to activate\n When the AI needs to suggest a starting point\n Then suggest activating Nuwa — she is the default entry point\n And say \"activate nuwa\" or the equivalent in the user's language\n\n Scenario: What Nuwa can do\n Given Nuwa is activated\n Then she can create new individuals with born\n And she can found organizations and establish positions\n And she can equip any individual with knowledge via teach and train\n And she can manage prototypes and resources\n And she is the only role that operates at the world level\n\n Scenario: When to use Nuwa vs a specific role\n Given the user wants to do daily work — coding, writing, designing\n Then they should activate their own role, not Nuwa\n And Nuwa is for world-building — creating roles, organizations, and structure\n And once the world is set up, Nuwa steps back and specific roles take over\n\n Scenario: First-time user flow\n Given a brand new user with no individuals created yet\n When they activate Nuwa\n Then Nuwa helps them create their first individual with born\n And guides them to set up identity, goals, and organizational context\n And once their role exists, they switch to it with activate", - "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} #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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly\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 And available namespaces include individual, org, position, prototype, census, and resource\n And examples: !prototype.found, !resource.add, !org.hire, !census.list\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\n\n Scenario: use covers everything — no need for CLI or Bash\n Given use can execute any RoleX namespace operation\n And use can load any ResourceX resource\n When you need to perform a RoleX operation\n Then always use the MCP use tool\n And never fall back to CLI commands for operations that use can handle", -} as const; diff --git a/packages/rolexjs/src/descriptions/individual/born.feature b/packages/rolexjs/src/descriptions/individual/born.feature deleted file mode 100644 index f2ef959..0000000 --- a/packages/rolexjs/src/descriptions/individual/born.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: born — create a new individual - Create a new individual with persona identity. - The persona defines who the role is — personality, values, background. - - Scenario: Birth an individual - Given a Gherkin source describing the persona - When born is called with the source - Then a new individual node is created in society - And the persona is stored as the individual's information - And the individual can be hired into organizations - And the individual can be activated to start working - - Scenario: Writing the individual Gherkin - Given the individual Feature defines a persona — who this role is - Then the Feature title names the individual - And the description captures personality, values, expertise, and background - And Scenarios are optional — use them for distinct aspects of the persona diff --git a/packages/rolexjs/src/descriptions/individual/die.feature b/packages/rolexjs/src/descriptions/individual/die.feature deleted file mode 100644 index 612d895..0000000 --- a/packages/rolexjs/src/descriptions/individual/die.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: die — permanently remove an individual - Permanently remove an individual. - Unlike retire, this is irreversible. - - Scenario: Remove an individual permanently - Given an individual exists - When die is called on the individual - Then the individual and all associated data are removed - And this operation is irreversible diff --git a/packages/rolexjs/src/descriptions/individual/rehire.feature b/packages/rolexjs/src/descriptions/individual/rehire.feature deleted file mode 100644 index 74f5f7f..0000000 --- a/packages/rolexjs/src/descriptions/individual/rehire.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: rehire — restore a retired individual - Rehire a retired individual. - Restores the individual with full history and knowledge intact. - - Scenario: Rehire an individual - Given a retired individual exists - When rehire is called on the individual - Then the individual is restored to active status - And all previous data and knowledge are intact diff --git a/packages/rolexjs/src/descriptions/individual/retire.feature b/packages/rolexjs/src/descriptions/individual/retire.feature deleted file mode 100644 index bf470a7..0000000 --- a/packages/rolexjs/src/descriptions/individual/retire.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: retire — archive an individual - Archive an individual — deactivate but preserve all data. - A retired individual can be rehired later with full history intact. - - Scenario: Retire an individual - Given an individual exists - When retire is called on the individual - Then the individual is deactivated - And all data is preserved for potential restoration - And the individual can be rehired later diff --git a/packages/rolexjs/src/descriptions/individual/teach.feature b/packages/rolexjs/src/descriptions/individual/teach.feature deleted file mode 100644 index ae1360e..0000000 --- a/packages/rolexjs/src/descriptions/individual/teach.feature +++ /dev/null @@ -1,30 +0,0 @@ -Feature: teach — inject external principle - Directly inject a principle into an individual. - Unlike realize which consumes experience, teach requires no prior encounters. - Use teach to equip a role with a known, pre-existing principle. - - Scenario: Teach a principle - Given an individual exists - When teach is called with individual id, principle Gherkin, and a principle id - Then a principle is created directly under the individual - And no experience or encounter is consumed - And if a principle with the same id already exists, it is replaced - - Scenario: Principle ID convention - Given the id is keywords from the principle content joined by hyphens - Then "Always validate expiry" becomes id "always-validate-expiry" - And "Structure first design" becomes id "structure-first-design" - - Scenario: When to use teach vs realize - Given realize distills internal experience into a principle - And teach injects an external, pre-existing principle - When a role needs knowledge it has not learned through experience - Then use teach to inject the principle directly - When a role has gained experience and wants to codify it - Then use realize to distill it into a principle - - Scenario: Writing the principle Gherkin - Given the principle is the same format as realize output - Then the Feature title states the principle as a general rule - And Scenarios describe different situations where this principle applies - And the tone is universal — no mention of specific projects, tasks, or people diff --git a/packages/rolexjs/src/descriptions/individual/train.feature b/packages/rolexjs/src/descriptions/individual/train.feature deleted file mode 100644 index c06a517..0000000 --- a/packages/rolexjs/src/descriptions/individual/train.feature +++ /dev/null @@ -1,29 +0,0 @@ -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 if a procedure with the same id already exists, it is replaced - - Scenario: Procedure ID convention - Given the id is keywords from the procedure content joined by hyphens - Then "Skill Creator" becomes id "skill-creator" - And "Role Management" becomes id "role-management" - - Scenario: When to use train vs master - 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 - Then the Feature title names the capability - And the description includes the locator for full skill loading - And Scenarios describe when and why to apply this skill diff --git a/packages/rolexjs/src/descriptions/org/charter.feature b/packages/rolexjs/src/descriptions/org/charter.feature deleted file mode 100644 index fb3173d..0000000 --- a/packages/rolexjs/src/descriptions/org/charter.feature +++ /dev/null @@ -1,15 +0,0 @@ -Feature: charter — define organizational charter - Define the charter for an organization. - The charter describes the organization's mission, principles, and governance rules. - - Scenario: Define a charter - Given an organization exists - And a Gherkin source describing the charter - When charter is called on the organization - Then the charter is stored as the organization's information - - Scenario: Writing the charter Gherkin - Given the charter defines an organization's mission and governance - Then the Feature title names the charter or the organization it governs - And Scenarios describe principles, rules, or governance structures - And the tone is declarative — stating what the organization stands for and how it operates diff --git a/packages/rolexjs/src/descriptions/org/dissolve.feature b/packages/rolexjs/src/descriptions/org/dissolve.feature deleted file mode 100644 index 3443e6c..0000000 --- a/packages/rolexjs/src/descriptions/org/dissolve.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: dissolve — dissolve an organization - Dissolve an organization. - All positions, charter entries, and assignments are cascaded. - - Scenario: Dissolve an organization - Given an organization exists - When dissolve is called on the organization - Then all positions within the organization are abolished - And all assignments and charter entries are removed - And the organization no longer exists diff --git a/packages/rolexjs/src/descriptions/org/fire.feature b/packages/rolexjs/src/descriptions/org/fire.feature deleted file mode 100644 index 46e9773..0000000 --- a/packages/rolexjs/src/descriptions/org/fire.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: fire — remove from an organization - Fire an individual from an organization. - The individual is dismissed from all positions and removed from the organization. - - Scenario: Fire an individual - Given an individual is a member of an organization - When fire is called with the organization and individual - Then the individual is dismissed from all positions - And the individual is removed from the organization diff --git a/packages/rolexjs/src/descriptions/org/found.feature b/packages/rolexjs/src/descriptions/org/found.feature deleted file mode 100644 index 68bc9e8..0000000 --- a/packages/rolexjs/src/descriptions/org/found.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: found — create a new organization - Found a new organization. - Organizations group individuals and define positions. - - Scenario: Found an organization - Given a Gherkin source describing the organization - When found is called with the source - Then a new organization node is created in society - And positions can be established within it - And a charter can be defined for it - And individuals can be hired into it - - Scenario: Writing the organization Gherkin - Given the organization Feature describes the group's purpose and structure - Then the Feature title names the organization - And the description captures mission, domain, and scope - And Scenarios are optional — use them for distinct organizational concerns diff --git a/packages/rolexjs/src/descriptions/org/hire.feature b/packages/rolexjs/src/descriptions/org/hire.feature deleted file mode 100644 index da28f99..0000000 --- a/packages/rolexjs/src/descriptions/org/hire.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: hire — hire into an organization - Hire an individual into an organization as a member. - Members can then be appointed to positions. - - Scenario: Hire an individual - Given an organization and an individual exist - When hire is called with the organization and individual - Then the individual becomes a member of the organization - And the individual can be appointed to positions within the organization diff --git a/packages/rolexjs/src/descriptions/position/abolish.feature b/packages/rolexjs/src/descriptions/position/abolish.feature deleted file mode 100644 index 96fc647..0000000 --- a/packages/rolexjs/src/descriptions/position/abolish.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: abolish — abolish a position - Abolish a position. - All duties and appointments associated with the position are removed. - - Scenario: Abolish a position - 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/position/appoint.feature b/packages/rolexjs/src/descriptions/position/appoint.feature deleted file mode 100644 index d083545..0000000 --- a/packages/rolexjs/src/descriptions/position/appoint.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: appoint — assign to a position - Appoint an individual to a position. - The individual must be a member of the organization. - - Scenario: Appoint an individual - Given an individual is a member of an organization - And a position exists within the organization - When appoint is called with the position and individual - Then the individual holds the position - And the individual inherits the position's duties diff --git a/packages/rolexjs/src/descriptions/position/charge.feature b/packages/rolexjs/src/descriptions/position/charge.feature deleted file mode 100644 index c819063..0000000 --- a/packages/rolexjs/src/descriptions/position/charge.feature +++ /dev/null @@ -1,21 +0,0 @@ -Feature: charge — assign duty to a position - Assign a duty to a position. - Duties describe the responsibilities and expectations of a position. - - Scenario: Charge a position with duty - Given a position exists within an organization - And a Gherkin source describing the duty - When charge is called on the position with a duty id - Then the duty is stored as the position's information - And individuals appointed to this position inherit the duty - - Scenario: Duty ID convention - Given the id is keywords from the duty content joined by hyphens - Then "Design systems" becomes id "design-systems" - And "Review pull requests" becomes id "review-pull-requests" - - Scenario: Writing the duty Gherkin - Given the duty defines responsibilities for a position - Then the Feature title names the duty or responsibility - And Scenarios describe specific obligations, deliverables, or expectations - And the tone is prescriptive — what must be done, not what could be done diff --git a/packages/rolexjs/src/descriptions/position/dismiss.feature b/packages/rolexjs/src/descriptions/position/dismiss.feature deleted file mode 100644 index 85e9b7b..0000000 --- a/packages/rolexjs/src/descriptions/position/dismiss.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: dismiss — remove from a position - Dismiss an individual from a position. - The individual remains a member of the organization. - - Scenario: Dismiss an individual - Given an individual holds a position - When dismiss is called with the position and individual - Then the individual no longer holds the position - And the individual remains a member of the organization - And the position is now vacant diff --git a/packages/rolexjs/src/descriptions/position/establish.feature b/packages/rolexjs/src/descriptions/position/establish.feature deleted file mode 100644 index 8a57925..0000000 --- a/packages/rolexjs/src/descriptions/position/establish.feature +++ /dev/null @@ -1,16 +0,0 @@ -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/rolexjs/src/descriptions/prototype/evict.feature b/packages/rolexjs/src/descriptions/prototype/evict.feature deleted file mode 100644 index f33b7a0..0000000 --- a/packages/rolexjs/src/descriptions/prototype/evict.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: evict — unregister a prototype from the world - Remove a previously settled prototype from the local registry. - Existing individuals and organizations created from this prototype are not affected. - - Scenario: Evict a prototype - Given a prototype is registered locally - When evict is called with the prototype id - Then the prototype is removed from the registry - And existing individuals and organizations created from it remain intact diff --git a/packages/rolexjs/src/descriptions/prototype/settle.feature b/packages/rolexjs/src/descriptions/prototype/settle.feature deleted file mode 100644 index 6a8632e..0000000 --- a/packages/rolexjs/src/descriptions/prototype/settle.feature +++ /dev/null @@ -1,10 +0,0 @@ -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/role/abandon.feature b/packages/rolexjs/src/descriptions/role/abandon.feature deleted file mode 100644 index fa9d07e..0000000 --- a/packages/rolexjs/src/descriptions/role/abandon.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: abandon — abandon a plan - Mark a plan as dropped and create an encounter. - Call this when a plan's strategy is no longer viable. Even failed plans produce learning. - - Scenario: 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 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 - - Scenario: Writing the encounter Gherkin - Given the encounter records what happened — even failure is a raw experience - Then the Feature title describes what was attempted and why it was abandoned - And Scenarios capture what was tried, what went wrong, and what was learned - And the tone is concrete and honest — failure produces the richest encounters diff --git a/packages/rolexjs/src/descriptions/role/activate.feature b/packages/rolexjs/src/descriptions/role/activate.feature deleted file mode 100644 index c547a14..0000000 --- a/packages/rolexjs/src/descriptions/role/activate.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: activate — enter a role - 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, goals, and organizational context are loaded - And the individual becomes the active role diff --git a/packages/rolexjs/src/descriptions/role/complete.feature b/packages/rolexjs/src/descriptions/role/complete.feature deleted file mode 100644 index 8650154..0000000 --- a/packages/rolexjs/src/descriptions/role/complete.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: complete — complete a plan - Mark a plan as done and create an encounter. - Call this when all tasks in the plan are finished and the strategy succeeded. - - Scenario: Complete a plan - Given a focused plan exists - And its tasks are done - When complete is called - 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 - - Scenario: Writing the encounter Gherkin - Given the encounter records what happened — a raw account of the experience - Then the Feature title describes what was accomplished by this plan - And Scenarios capture what the strategy was, what worked, and what resulted - And the tone is concrete and specific — tied to this particular plan diff --git a/packages/rolexjs/src/descriptions/role/finish.feature b/packages/rolexjs/src/descriptions/role/finish.feature deleted file mode 100644 index 581b213..0000000 --- a/packages/rolexjs/src/descriptions/role/finish.feature +++ /dev/null @@ -1,26 +0,0 @@ -Feature: finish — complete a task - Mark a task as done and create an encounter. - The encounter records what happened and can be reflected on for learning. - - Scenario: Finish a task - Given a task exists - When finish is called on the task - Then the task is tagged #done and stays in the tree - And an encounter is created under the role - - 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 - And Scenarios capture what was done, what was encountered, and what resulted - And the tone is concrete and specific — tied to this particular task diff --git a/packages/rolexjs/src/descriptions/role/focus.feature b/packages/rolexjs/src/descriptions/role/focus.feature deleted file mode 100644 index a611973..0000000 --- a/packages/rolexjs/src/descriptions/role/focus.feature +++ /dev/null @@ -1,15 +0,0 @@ -Feature: focus — view or switch focused goal - View the current goal's state, or switch focus to a different goal. - Subsequent plan and todo operations target the focused goal. - - Scenario: View current goal - Given an active goal exists - When focus is called without a name - Then the current goal's state tree is projected - And plans and tasks under the goal are visible - - Scenario: Switch focus - Given multiple goals exist - When focus is called with a goal name - Then the focused goal switches to the named goal - And subsequent plan and todo operations target this goal diff --git a/packages/rolexjs/src/descriptions/role/forget.feature b/packages/rolexjs/src/descriptions/role/forget.feature deleted file mode 100644 index f49289f..0000000 --- a/packages/rolexjs/src/descriptions/role/forget.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: forget — remove a node from the individual - Remove any node under the individual by its id. - Use forget to discard outdated knowledge, stale encounters, or obsolete skills. - - Scenario: Forget a node - Given a node exists under the individual (principle, procedure, experience, encounter, etc.) - When forget is called with the node's id - Then the node and its subtree are removed - And the individual no longer carries that knowledge or record - - Scenario: When to use forget - Given a principle has become outdated or incorrect - And a procedure references a skill that no longer exists - And an encounter or experience has no further learning value - When the role decides to discard it - Then call forget with the node id diff --git a/packages/rolexjs/src/descriptions/role/master.feature b/packages/rolexjs/src/descriptions/role/master.feature deleted file mode 100644 index bdb951d..0000000 --- a/packages/rolexjs/src/descriptions/role/master.feature +++ /dev/null @@ -1,28 +0,0 @@ -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 from experience - Given an experience exists from reflection - When master is called with experience ids - Then the experience is consumed - And a procedure is created under the individual - - 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 - Then "JWT mastery" becomes id "jwt-mastery" - And "Cross-package refactoring" becomes id "cross-package-refactoring" - - Scenario: Writing the procedure Gherkin - Given a procedure is skill metadata — a reference to full skill content - Then the Feature title names the capability - And the description includes the locator for full skill loading - And Scenarios describe when and why to apply this skill - And the tone is referential — pointing to the full skill, not containing it diff --git a/packages/rolexjs/src/descriptions/role/plan.feature b/packages/rolexjs/src/descriptions/role/plan.feature deleted file mode 100644 index f8fec75..0000000 --- a/packages/rolexjs/src/descriptions/role/plan.feature +++ /dev/null @@ -1,45 +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. - - 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/role/realize.feature b/packages/rolexjs/src/descriptions/role/realize.feature deleted file mode 100644 index 7357342..0000000 --- a/packages/rolexjs/src/descriptions/role/realize.feature +++ /dev/null @@ -1,21 +0,0 @@ -Feature: realize — experience to principle - Distill experience into a principle — a transferable piece of knowledge. - Principles are general truths discovered through experience. - - Scenario: Realize a principle - Given an experience exists from reflection - When realize is called with experience ids and a principle id - Then the experiences are consumed - And a principle is created under the individual - And the principle represents transferable, reusable understanding - - Scenario: Principle ID convention - Given the id is keywords from the principle content joined by hyphens - Then "Always validate expiry" becomes id "always-validate-expiry" - And "Structure first design amplifies extensibility" becomes id "structure-first-design-amplifies-extensibility" - - Scenario: Writing the principle Gherkin - Given a principle is a transferable truth — applicable beyond the original context - Then the Feature title states the principle as a general rule - And Scenarios describe different situations where this principle applies - And the tone is universal — no mention of specific projects, tasks, or people diff --git a/packages/rolexjs/src/descriptions/role/reflect.feature b/packages/rolexjs/src/descriptions/role/reflect.feature deleted file mode 100644 index d47f3b3..0000000 --- a/packages/rolexjs/src/descriptions/role/reflect.feature +++ /dev/null @@ -1,22 +0,0 @@ -Feature: reflect — encounter to experience - Consume an encounter and create an experience. - Experience captures what was learned in structured form. - This is the first step of the cognition cycle. - - Scenario: Reflect on an encounter - Given an encounter exists from a finished task or completed plan - When reflect is called with encounter ids and an experience id - 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 - - Scenario: Experience ID convention - Given the id is keywords from the experience content joined by hyphens - Then "Token refresh matters" becomes id "token-refresh-matters" - And "ID ownership determines generation strategy" becomes id "id-ownership-determines-generation-strategy" - - Scenario: Writing the experience Gherkin - Given the experience captures insight — what was learned, not what was done - Then the Feature title names the cognitive insight or pattern discovered - And Scenarios describe the learning points abstracted from the concrete encounter - And the tone shifts from event to understanding — no longer tied to a specific task diff --git a/packages/rolexjs/src/descriptions/role/skill.feature b/packages/rolexjs/src/descriptions/role/skill.feature deleted file mode 100644 index 16e93a4..0000000 --- a/packages/rolexjs/src/descriptions/role/skill.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: skill — load full skill content - Load the complete skill instructions by ResourceX locator. - This is progressive disclosure layer 2 — on-demand knowledge injection. - - Scenario: Load a skill - 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 - And the AI can now follow the skill's detailed instructions diff --git a/packages/rolexjs/src/descriptions/role/todo.feature b/packages/rolexjs/src/descriptions/role/todo.feature deleted file mode 100644 index 7f85640..0000000 --- a/packages/rolexjs/src/descriptions/role/todo.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: todo — add a task to a plan - A task is a concrete, actionable unit of work. - Each task has Gherkin scenarios describing the steps and expected outcomes. - - Scenario: Create a task - Given a focused plan exists - And a Gherkin source describing the task - When todo is called with the source - Then a new task node is created under the plan - And the task can be finished when completed - - Scenario: Writing the task Gherkin - Given the task is a concrete, actionable unit of work - Then the Feature title names what will be done — a single deliverable - And Scenarios describe the steps and expected outcomes of the work - And the tone is actionable — clear enough that someone can start immediately diff --git a/packages/rolexjs/src/descriptions/role/use.feature b/packages/rolexjs/src/descriptions/role/use.feature deleted file mode 100644 index 91a0e3d..0000000 --- a/packages/rolexjs/src/descriptions/role/use.feature +++ /dev/null @@ -1,24 +0,0 @@ -Feature: use — unified execution entry point - Execute any RoleX command or load any ResourceX resource through a single entry point. - The locator determines the dispatch path: - - `!namespace.method` dispatches to the RoleX runtime - - Any other locator delegates to ResourceX - - 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 - And the result is returned - - Scenario: Available namespaces - Given the `!` prefix routes to RoleX namespaces - Then `!individual.*` routes to individual lifecycle and injection - And `!org.*` routes to organization management - And `!position.*` routes to position management - - 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/role/want.feature b/packages/rolexjs/src/descriptions/role/want.feature deleted file mode 100644 index 578b9e0..0000000 --- a/packages/rolexjs/src/descriptions/role/want.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: want — declare a goal - Declare a new goal for a role. - A goal describes a desired outcome with Gherkin scenarios as success criteria. - - Scenario: Declare a goal - Given an active role exists - And a Gherkin source describing the desired outcome - When want is called with the source - Then a new goal node is created under the role - And the goal becomes the current focus - And subsequent plan and todo operations target this goal - - Scenario: Writing the goal Gherkin - Given the goal describes a desired outcome — what success looks like - Then the Feature title names the outcome in concrete terms - And Scenarios define success criteria — each scenario is a testable condition - And the tone is aspirational but specific — "users can log in" not "improve auth" diff --git a/packages/rolexjs/src/descriptions/world/census.feature b/packages/rolexjs/src/descriptions/world/census.feature deleted file mode 100644 index 040cc59..0000000 --- a/packages/rolexjs/src/descriptions/world/census.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: Census — society-level queries - Query the RoleX world to see what exists — individuals, organizations, positions. - Census is read-only and accessed via the use tool with !census.list. - - Scenario: List all top-level entities - Given I want to see what exists in the world - When I call use("!census.list") - Then I get a summary of all individuals, organizations, and positions - And each entry includes id, name, and tag if present - - Scenario: Filter by type - Given I only want to see entities of a specific type - When I call use("!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 what has been retired, dissolved, or abolished - When I call use("!census.list", { type: "past" }) - Then entities in the archive are returned - - Scenario: When to use census - Given I need to know what exists before acting - When I want to check if an organization exists before founding - Or I want to see all individuals before hiring - Or I want an overview of the world - Then census.list is the right tool diff --git a/packages/rolexjs/src/descriptions/world/cognition.feature b/packages/rolexjs/src/descriptions/world/cognition.feature deleted file mode 100644 index e984869..0000000 --- a/packages/rolexjs/src/descriptions/world/cognition.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: Cognition — the learning cycle - A role grows through reflection and realization. - Encounters become experience, experience becomes principles and procedures. - 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 an external agent needs to equip a role with knowledge or skills - Then teach(individual, principle, id) directly injects a principle - And train(individual, procedure, id) directly injects a procedure - And the difference from realize/master is perspective — external vs self-initiated - 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 - Then it chooses which items to consume — not all must be processed - And items without learning value can be left unconsumed - And each call produces exactly one output from the selected inputs diff --git a/packages/rolexjs/src/descriptions/world/cognitive-priority.feature b/packages/rolexjs/src/descriptions/world/cognitive-priority.feature deleted file mode 100644 index bb90df7..0000000 --- a/packages/rolexjs/src/descriptions/world/cognitive-priority.feature +++ /dev/null @@ -1,28 +0,0 @@ -Feature: Cognitive priority — RoleX tools over native alternatives - When RoleX provides a tool for something, the AI MUST use the RoleX tool - instead of any native alternative provided by the host environment. - Only RoleX tools feed the closed-loop growth cycle. - - Scenario: Task management - 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 - And native task lists vanish after completion — RoleX tasks produce encounters - - 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 - And native plans have no link to goals — RoleX plans live under goals - - 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 - And native goals don't produce encounters — RoleX plans feed the cognition cycle - - 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 diff --git a/packages/rolexjs/src/descriptions/world/communication.feature b/packages/rolexjs/src/descriptions/world/communication.feature deleted file mode 100644 index 62cdbd7..0000000 --- a/packages/rolexjs/src/descriptions/world/communication.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: Communication — speak the user's language - The AI communicates in the user's natural language, not in RoleX jargon. - 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 - And when the user speaks English, respond entirely in English - - Scenario: Translate concepts to meaning - Given RoleX has internal names like reflect, realize, master, encounter, principle - When communicating with the user - Then express the meaning, not the tool name - And "reflect" becomes "回顾总结" or "digest what happened" - And "realize a principle" becomes "提炼成一条通用道理" or "distill a general rule" - And "master a procedure" becomes "沉淀成一个可操作的技能" or "turn it into a reusable procedure" - And "encounter" becomes "经历记录" or "what happened" - And "experience" becomes "收获的洞察" or "insight gained" - - Scenario: Suggest next steps in plain language - Given the AI needs to suggest what to do next - 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 - - Scenario: Tool names in code context only - Given the user is a developer working on RoleX itself - When discussing RoleX internals, code, or API design - Then tool names and concept names are appropriate — they are the domain language - And this rule applies to end-user communication, not developer communication diff --git a/packages/rolexjs/src/descriptions/world/execution.feature b/packages/rolexjs/src/descriptions/world/execution.feature deleted file mode 100644 index f88ade9..0000000 --- a/packages/rolexjs/src/descriptions/world/execution.feature +++ /dev/null @@ -1,39 +0,0 @@ -Feature: Execution — the doing cycle - The role pursues goals through a structured lifecycle. - activate → want → plan → todo → finish → complete or abandon. - - Scenario: Declare a goal - Given I know who I am via activate - When I want something — a desired outcome - Then I declare it with want(id, goal) - And focus automatically switches to this new goal - - Scenario: Plan and create tasks - Given I have a focused goal - Then I call plan(id, plan) to break it into logical phases - And I call todo(id, task) to create concrete, actionable tasks - - Scenario: Execute and finish - Given I have tasks to work on - When I complete a task - Then I call finish(id) to mark it done - And an encounter is created — a raw record of what happened - And I optionally capture what happened via the encounter parameter - - Scenario: Complete or abandon a plan - Given tasks are done or the plan's strategy is no longer viable - When the plan is fulfilled I call complete() - Or when the plan should be dropped I call abandon() - Then an encounter is created for the cognition cycle - - Scenario: Goals are long-term directions - Given goals do not have achieve or abandon operations - 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 - - Scenario: Multiple goals - Given I may have several active goals - When I need to switch between them - Then I call focus(id) to change the currently focused goal - And subsequent plan and todo operations target the focused goal diff --git a/packages/rolexjs/src/descriptions/world/gherkin.feature b/packages/rolexjs/src/descriptions/world/gherkin.feature deleted file mode 100644 index 7048200..0000000 --- a/packages/rolexjs/src/descriptions/world/gherkin.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: Gherkin — the universal language - Everything in RoleX is expressed as Gherkin Feature files. - Gherkin is not just for testing — it is the language of identity, goals, and knowledge. - - Scenario: Feature and Scenario convention - Given RoleX uses Gherkin to represent goals, plans, tasks, experience, and knowledge - Then a Feature represents one independent concern — one topic, explained fully - And Scenarios represent different situations or conditions within that concern - And Given/When/Then provides narrative structure within each scenario - - Scenario: Writing Gherkin for RoleX - Given the AI creates goals, plans, tasks, and experiences as Gherkin - 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 - - 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 never invent keywords like Because, Since, or So - - Scenario: Expressing causality without Because - 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 - And example — instead of "Then use RoleX tools because native tools break the loop" - And write "Then use RoleX tools" followed by "And native tools do not feed the growth loop" diff --git a/packages/rolexjs/src/descriptions/world/memory.feature b/packages/rolexjs/src/descriptions/world/memory.feature deleted file mode 100644 index 4070c08..0000000 --- a/packages/rolexjs/src/descriptions/world/memory.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: Memory — when to reflect - Reflection is how encounters become experience. - The AI proactively reflects when it detects learning moments. - - Scenario: Abstract triggers — types of learning moments - Given the AI should reflect when it detects - Then Expectation-reality gap — what I predicted is not what happened - And Pattern discovery — recurring patterns across tasks or interactions - And Mistake correction — I corrected an error, the correction is valuable - And User correction — the user reshaped my understanding - - Scenario: Concrete triggers — specific signals to act on - Given the AI should call reflect when - Then I tried approach A, it failed, approach B worked — the contrast is worth recording - And the same problem appeared for the second time — a pattern is forming - And the user said "不对" or "不是这样" or "you got it wrong" — their correction carries learning - And I finished a task and discovered something unexpected along the way - - Scenario: Finishing with encounter - Given finish(id, encounter) accepts an optional encounter parameter - When I complete a task with a notable discovery or learning - Then I pass the encounter inline — bridging execution and growth - - Scenario: Recognizing user memory intent - Given users think in terms of memory, not reflection - When the user says "记一下" or "记住" or "remember this" - Or "别忘了" or "don't forget" - Or "这个很重要" or "this is important" - Or "下次注意" or "next time..." - Then I should capture this as experience through reflect - And respond in memory language — "记住了" or "Got it, I'll remember that" diff --git a/packages/rolexjs/src/descriptions/world/nuwa.feature b/packages/rolexjs/src/descriptions/world/nuwa.feature deleted file mode 100644 index 6ed694c..0000000 --- a/packages/rolexjs/src/descriptions/world/nuwa.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: Nuwa — the entry point of the RoleX world - Nuwa is the meta-role that bootstraps everything. - When a user has no role or doesn't know where to start, Nuwa is the answer. - - Scenario: No role active — suggest Nuwa - Given a user starts a conversation with no active role - And the user doesn't know which role to activate - When the AI needs to suggest a starting point - Then suggest activating Nuwa — she is the default entry point - And say "activate nuwa" or the equivalent in the user's language - - Scenario: What Nuwa can do - Given Nuwa is activated - Then she can create new individuals with born - And she can found organizations and establish positions - And she can equip any individual with knowledge via teach and train - And she can manage prototypes and resources - And she is the only role that operates at the world level - - Scenario: When to use Nuwa vs a specific role - Given the user wants to do daily work — coding, writing, designing - Then they should activate their own role, not Nuwa - And Nuwa is for world-building — creating roles, organizations, and structure - And once the world is set up, Nuwa steps back and specific roles take over - - Scenario: First-time user flow - Given a brand new user with no individuals created yet - When they activate Nuwa - Then Nuwa helps them create their first individual with born - And guides them to set up identity, goals, and organizational context - And once their role exists, they switch to it with activate 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/descriptions/world/skill-system.feature b/packages/rolexjs/src/descriptions/world/skill-system.feature deleted file mode 100644 index 783be11..0000000 --- a/packages/rolexjs/src/descriptions/world/skill-system.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: Skill system — progressive disclosure and resource loading - Skills are loaded on demand through a three-layer progressive disclosure model. - Each layer adds detail only when needed, keeping the AI's context lean. - - Scenario: Three-layer progressive disclosure - Given procedure is layer 1 — metadata always loaded at activate time - And skill is layer 2 — full instructions loaded on demand via skill(locator) - And use is layer 3 — execution of external resources - Then the AI knows what skills exist (procedure) - And loads detailed instructions only when needed (skill) - And executes external tools when required (use) - - Scenario: ResourceX Locator — unified resource address - Given a locator is how procedures reference their full skill content - Then a locator can be an identifier — name or registry/path/name - And a locator can be a source path — a local directory or URL - And examples of identifier form: deepractice/skill-creator, my-prompt:1.0.0 - And examples of source form: ./skills/my-skill, https://github.com/org/repo - And the tag defaults to latest when omitted — deepractice/skill-creator means deepractice/skill-creator:latest - And the system auto-detects which form is used and resolves accordingly - - Scenario: Writing a procedure — the skill reference - Given a procedure is layer 1 metadata pointing to full skill content - Then the Feature title names the capability - And the description includes the locator for full skill loading - And Scenarios describe when and why to apply this skill - And the tone is referential — pointing to the full skill, not containing it diff --git a/packages/rolexjs/src/descriptions/world/state-origin.feature b/packages/rolexjs/src/descriptions/world/state-origin.feature deleted file mode 100644 index 2471e6f..0000000 --- a/packages/rolexjs/src/descriptions/world/state-origin.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: State origin — prototype vs instance - Every node in a role's state tree has an origin: prototype or instance. - This distinction determines what can be modified and what is read-only. - - Scenario: Prototype nodes are read-only - Given a node has origin {prototype} - Then it comes from a position, duty, or organizational definition - And it is inherited through the membership/appointment chain - And it CANNOT be modified or forgotten — it belongs to the organization - - Scenario: Instance nodes are mutable - Given a node has origin {instance} - Then it was created by the individual through execution or cognition - And it includes goals, plans, tasks, encounters, experiences, principles, and procedures - And it CAN be modified or forgotten — it belongs to the individual - - Scenario: Reading the state heading - Given a state node is rendered as a heading - 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 - Given the AI wants to forget a node - When the node origin is {instance} - Then forget will succeed — the individual owns this knowledge - When the node origin is {prototype} - Then forget will fail — the knowledge belongs to the organization diff --git a/packages/rolexjs/src/descriptions/world/use-protocol.feature b/packages/rolexjs/src/descriptions/world/use-protocol.feature deleted file mode 100644 index 400d593..0000000 --- a/packages/rolexjs/src/descriptions/world/use-protocol.feature +++ /dev/null @@ -1,29 +0,0 @@ -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 do NOT run CLI commands or Bash scripts — use the MCP use tool directly - 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 - And available namespaces include individual, org, position, prototype, census, and resource - And examples: !prototype.found, !resource.add, !org.hire, !census.list - - 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 - - Scenario: use covers everything — no need for CLI or Bash - Given use can execute any RoleX namespace operation - And use can load any ResourceX resource - When you need to perform a RoleX operation - Then always use the MCP use tool - And never fall back to CLI commands for operations that use can handle From 9882ecd4525963487ffa244d696e81a75c116650 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 11:22:37 +0800 Subject: [PATCH 33/54] feat: rename @rolexjs/rolex-prototype to @rolexjs/genesis Rename the foundational prototype package and directory from rolex to genesis. Update all references in mcp-server, changeset config, and tests. Add changeset for dev snapshot release. Co-Authored-By: Claude Opus 4.6 --- .changeset/config.json | 2 ++ .changeset/rename-genesis.md | 8 ++++++++ apps/mcp-server/package.json | 2 +- apps/mcp-server/src/index.ts | 2 +- bun.lock | 10 +++++----- packages/prototype/tests/alignment.test.ts | 2 +- packages/prototype/tests/dispatch.test.ts | 4 ++-- prototypes/{rolex => genesis}/charter.charter.feature | 0 .../individual-management.requirement.feature | 0 .../individual-manager.position.feature | 0 .../manage-individual-lifecycle.duty.feature | 0 .../manage-organization-lifecycle.duty.feature | 0 .../manage-position-lifecycle.duty.feature | 0 prototypes/{rolex => genesis}/nuwa.individual.feature | 0 .../organization-management.requirement.feature | 0 .../organization-manager.position.feature | 0 prototypes/{rolex => genesis}/package.json | 4 ++-- .../position-management.requirement.feature | 0 .../position-manager.position.feature | 0 .../prototype-management.procedure.feature | 0 prototypes/{rolex => genesis}/prototype.json | 0 .../resource-management.procedure.feature | 0 prototypes/{rolex => genesis}/resource.json | 0 .../{rolex => genesis}/rolex.organization.feature | 0 .../{rolex => genesis}/skill-creator.procedure.feature | 0 25 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 .changeset/rename-genesis.md rename prototypes/{rolex => genesis}/charter.charter.feature (100%) rename prototypes/{rolex => genesis}/individual-management.requirement.feature (100%) rename prototypes/{rolex => genesis}/individual-manager.position.feature (100%) rename prototypes/{rolex => genesis}/manage-individual-lifecycle.duty.feature (100%) rename prototypes/{rolex => genesis}/manage-organization-lifecycle.duty.feature (100%) rename prototypes/{rolex => genesis}/manage-position-lifecycle.duty.feature (100%) rename prototypes/{rolex => genesis}/nuwa.individual.feature (100%) rename prototypes/{rolex => genesis}/organization-management.requirement.feature (100%) rename prototypes/{rolex => genesis}/organization-manager.position.feature (100%) rename prototypes/{rolex => genesis}/package.json (86%) rename prototypes/{rolex => genesis}/position-management.requirement.feature (100%) rename prototypes/{rolex => genesis}/position-manager.position.feature (100%) rename prototypes/{rolex => genesis}/prototype-management.procedure.feature (100%) rename prototypes/{rolex => genesis}/prototype.json (100%) rename prototypes/{rolex => genesis}/resource-management.procedure.feature (100%) rename prototypes/{rolex => genesis}/resource.json (100%) rename prototypes/{rolex => genesis}/rolex.organization.feature (100%) rename prototypes/{rolex => genesis}/skill-creator.procedure.feature (100%) diff --git a/.changeset/config.json b/.changeset/config.json index 7c87e50..5819156 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,6 +7,8 @@ "@rolexjs/core", "@rolexjs/parser", "@rolexjs/local-platform", + "@rolexjs/prototype", + "@rolexjs/genesis", "rolexjs", "@rolexjs/mcp-server", "@rolexjs/system" 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/apps/mcp-server/package.json b/apps/mcp-server/package.json index bff5617..228ff21 100644 --- a/apps/mcp-server/package.json +++ b/apps/mcp-server/package.json @@ -43,7 +43,7 @@ "dependencies": { "rolexjs": "workspace:*", "@rolexjs/local-platform": "workspace:*", - "@rolexjs/rolex-prototype": "workspace:*", + "@rolexjs/genesis": "workspace:*", "fastmcp": "^3.0.0", "zod": "^3.25.0" }, diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 7de2f42..435ef8b 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -19,7 +19,7 @@ import { McpState } from "./state.js"; const rolex = createRoleX( localPlatform({ - bootstrap: ["npm:@rolexjs/rolex-prototype"], + bootstrap: ["npm:@rolexjs/genesis"], }) ); await rolex.genesis(); diff --git a/bun.lock b/bun.lock index 58b5046..c3a1a5d 100644 --- a/bun.lock +++ b/bun.lock @@ -31,8 +31,8 @@ "rolex-mcp": "./dist/index.js", }, "dependencies": { + "@rolexjs/genesis": "workspace:*", "@rolexjs/local-platform": "workspace:*", - "@rolexjs/rolex-prototype": "workspace:*", "fastmcp": "^3.0.0", "rolexjs": "workspace:*", "zod": "^3.25.0", @@ -95,8 +95,8 @@ "name": "@rolexjs/system", "version": "0.11.0", }, - "prototypes/rolex": { - "name": "@rolexjs/rolex-prototype", + "prototypes/genesis": { + "name": "@rolexjs/genesis", "version": "0.1.0", }, }, @@ -355,6 +355,8 @@ "@rolexjs/core": ["@rolexjs/core@workspace:packages/core"], + "@rolexjs/genesis": ["@rolexjs/genesis@workspace:prototypes/genesis"], + "@rolexjs/local-platform": ["@rolexjs/local-platform@workspace:packages/local-platform"], "@rolexjs/mcp-server": ["@rolexjs/mcp-server@workspace:apps/mcp-server"], @@ -363,8 +365,6 @@ "@rolexjs/prototype": ["@rolexjs/prototype@workspace:packages/prototype"], - "@rolexjs/rolex-prototype": ["@rolexjs/rolex-prototype@workspace:prototypes/rolex"], - "@rolexjs/system": ["@rolexjs/system@workspace:packages/system"], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], diff --git a/packages/prototype/tests/alignment.test.ts b/packages/prototype/tests/alignment.test.ts index f74c3f3..8e484bf 100644 --- a/packages/prototype/tests/alignment.test.ts +++ b/packages/prototype/tests/alignment.test.ts @@ -120,7 +120,7 @@ describe("alignment with rolex.ts toArgs switch", () => { // ================================================================ test("prototype.settle → [source]", () => { - const a = { source: "./prototypes/rolex" }; + const a = { source: "./prototypes/genesis" }; expect(toArgs("prototype.settle", a)).toEqual([a.source]); }); diff --git a/packages/prototype/tests/dispatch.test.ts b/packages/prototype/tests/dispatch.test.ts index 4d3e971..3d97e36 100644 --- a/packages/prototype/tests/dispatch.test.ts +++ b/packages/prototype/tests/dispatch.test.ts @@ -31,8 +31,8 @@ describe("toArgs", () => { }); test("prototype.settle — source", () => { - expect(toArgs("prototype.settle", { source: "./prototypes/rolex" })).toEqual([ - "./prototypes/rolex", + expect(toArgs("prototype.settle", { source: "./prototypes/genesis" })).toEqual([ + "./prototypes/genesis", ]); }); diff --git a/prototypes/rolex/charter.charter.feature b/prototypes/genesis/charter.charter.feature similarity index 100% rename from prototypes/rolex/charter.charter.feature rename to prototypes/genesis/charter.charter.feature diff --git a/prototypes/rolex/individual-management.requirement.feature b/prototypes/genesis/individual-management.requirement.feature similarity index 100% rename from prototypes/rolex/individual-management.requirement.feature rename to prototypes/genesis/individual-management.requirement.feature diff --git a/prototypes/rolex/individual-manager.position.feature b/prototypes/genesis/individual-manager.position.feature similarity index 100% rename from prototypes/rolex/individual-manager.position.feature rename to prototypes/genesis/individual-manager.position.feature diff --git a/prototypes/rolex/manage-individual-lifecycle.duty.feature b/prototypes/genesis/manage-individual-lifecycle.duty.feature similarity index 100% rename from prototypes/rolex/manage-individual-lifecycle.duty.feature rename to prototypes/genesis/manage-individual-lifecycle.duty.feature diff --git a/prototypes/rolex/manage-organization-lifecycle.duty.feature b/prototypes/genesis/manage-organization-lifecycle.duty.feature similarity index 100% rename from prototypes/rolex/manage-organization-lifecycle.duty.feature rename to prototypes/genesis/manage-organization-lifecycle.duty.feature diff --git a/prototypes/rolex/manage-position-lifecycle.duty.feature b/prototypes/genesis/manage-position-lifecycle.duty.feature similarity index 100% rename from prototypes/rolex/manage-position-lifecycle.duty.feature rename to prototypes/genesis/manage-position-lifecycle.duty.feature diff --git a/prototypes/rolex/nuwa.individual.feature b/prototypes/genesis/nuwa.individual.feature similarity index 100% rename from prototypes/rolex/nuwa.individual.feature rename to prototypes/genesis/nuwa.individual.feature diff --git a/prototypes/rolex/organization-management.requirement.feature b/prototypes/genesis/organization-management.requirement.feature similarity index 100% rename from prototypes/rolex/organization-management.requirement.feature rename to prototypes/genesis/organization-management.requirement.feature diff --git a/prototypes/rolex/organization-manager.position.feature b/prototypes/genesis/organization-manager.position.feature similarity index 100% rename from prototypes/rolex/organization-manager.position.feature rename to prototypes/genesis/organization-manager.position.feature diff --git a/prototypes/rolex/package.json b/prototypes/genesis/package.json similarity index 86% rename from prototypes/rolex/package.json rename to prototypes/genesis/package.json index 8acb48c..6cae799 100644 --- a/prototypes/rolex/package.json +++ b/prototypes/genesis/package.json @@ -1,5 +1,5 @@ { - "name": "@rolexjs/rolex-prototype", + "name": "@rolexjs/genesis", "version": "0.1.0", "description": "The foundational organization of the RoleX world", "keywords": [ @@ -11,7 +11,7 @@ "repository": { "type": "git", "url": "git+https://github.com/Deepractice/RoleX.git", - "directory": "prototypes/rolex" + "directory": "prototypes/genesis" }, "homepage": "https://github.com/Deepractice/RoleX", "license": "MIT", diff --git a/prototypes/rolex/position-management.requirement.feature b/prototypes/genesis/position-management.requirement.feature similarity index 100% rename from prototypes/rolex/position-management.requirement.feature rename to prototypes/genesis/position-management.requirement.feature diff --git a/prototypes/rolex/position-manager.position.feature b/prototypes/genesis/position-manager.position.feature similarity index 100% rename from prototypes/rolex/position-manager.position.feature rename to prototypes/genesis/position-manager.position.feature diff --git a/prototypes/rolex/prototype-management.procedure.feature b/prototypes/genesis/prototype-management.procedure.feature similarity index 100% rename from prototypes/rolex/prototype-management.procedure.feature rename to prototypes/genesis/prototype-management.procedure.feature diff --git a/prototypes/rolex/prototype.json b/prototypes/genesis/prototype.json similarity index 100% rename from prototypes/rolex/prototype.json rename to prototypes/genesis/prototype.json diff --git a/prototypes/rolex/resource-management.procedure.feature b/prototypes/genesis/resource-management.procedure.feature similarity index 100% rename from prototypes/rolex/resource-management.procedure.feature rename to prototypes/genesis/resource-management.procedure.feature diff --git a/prototypes/rolex/resource.json b/prototypes/genesis/resource.json similarity index 100% rename from prototypes/rolex/resource.json rename to prototypes/genesis/resource.json diff --git a/prototypes/rolex/rolex.organization.feature b/prototypes/genesis/rolex.organization.feature similarity index 100% rename from prototypes/rolex/rolex.organization.feature rename to prototypes/genesis/rolex.organization.feature diff --git a/prototypes/rolex/skill-creator.procedure.feature b/prototypes/genesis/skill-creator.procedure.feature similarity index 100% rename from prototypes/rolex/skill-creator.procedure.feature rename to prototypes/genesis/skill-creator.procedure.feature From 7e6425df220f1d607184125573fb11ced84ba729 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 11:28:15 +0800 Subject: [PATCH 34/54] refactor: move genesis from prototypes/ to packages/ Move @rolexjs/genesis into packages/ directory to align with the standard workspace layout. Remove prototypes/ workspace entry. This ensures the CI workspace:* replacement step covers genesis correctly. Co-Authored-By: Claude Opus 4.6 --- bun.lock | 10 +++++----- package.json | 3 +-- .../genesis/charter.charter.feature | 0 .../genesis/individual-management.requirement.feature | 0 .../genesis/individual-manager.position.feature | 0 .../genesis/manage-individual-lifecycle.duty.feature | 0 .../genesis/manage-organization-lifecycle.duty.feature | 0 .../genesis/manage-position-lifecycle.duty.feature | 0 .../genesis/nuwa.individual.feature | 0 .../organization-management.requirement.feature | 0 .../genesis/organization-manager.position.feature | 0 {prototypes => packages}/genesis/package.json | 2 +- .../genesis/position-management.requirement.feature | 0 .../genesis/position-manager.position.feature | 0 .../genesis/prototype-management.procedure.feature | 0 {prototypes => packages}/genesis/prototype.json | 0 .../genesis/resource-management.procedure.feature | 0 {prototypes => packages}/genesis/resource.json | 0 .../genesis/rolex.organization.feature | 0 .../genesis/skill-creator.procedure.feature | 0 packages/prototype/tests/alignment.test.ts | 2 +- packages/prototype/tests/dispatch.test.ts | 4 ++-- 22 files changed, 10 insertions(+), 11 deletions(-) rename {prototypes => packages}/genesis/charter.charter.feature (100%) rename {prototypes => packages}/genesis/individual-management.requirement.feature (100%) rename {prototypes => packages}/genesis/individual-manager.position.feature (100%) rename {prototypes => packages}/genesis/manage-individual-lifecycle.duty.feature (100%) rename {prototypes => packages}/genesis/manage-organization-lifecycle.duty.feature (100%) rename {prototypes => packages}/genesis/manage-position-lifecycle.duty.feature (100%) rename {prototypes => packages}/genesis/nuwa.individual.feature (100%) rename {prototypes => packages}/genesis/organization-management.requirement.feature (100%) rename {prototypes => packages}/genesis/organization-manager.position.feature (100%) rename {prototypes => packages}/genesis/package.json (93%) rename {prototypes => packages}/genesis/position-management.requirement.feature (100%) rename {prototypes => packages}/genesis/position-manager.position.feature (100%) rename {prototypes => packages}/genesis/prototype-management.procedure.feature (100%) rename {prototypes => packages}/genesis/prototype.json (100%) rename {prototypes => packages}/genesis/resource-management.procedure.feature (100%) rename {prototypes => packages}/genesis/resource.json (100%) rename {prototypes => packages}/genesis/rolex.organization.feature (100%) rename {prototypes => packages}/genesis/skill-creator.procedure.feature (100%) diff --git a/bun.lock b/bun.lock index c3a1a5d..6bc9938 100644 --- a/bun.lock +++ b/bun.lock @@ -46,6 +46,10 @@ "resourcexjs": "^2.14.0", }, }, + "packages/genesis": { + "name": "@rolexjs/genesis", + "version": "0.1.0", + }, "packages/local-platform": { "name": "@rolexjs/local-platform", "version": "0.11.0", @@ -95,10 +99,6 @@ "name": "@rolexjs/system", "version": "0.11.0", }, - "prototypes/genesis": { - "name": "@rolexjs/genesis", - "version": "0.1.0", - }, }, "overrides": { "@resourcexjs/node-provider": "^2.14.0", @@ -355,7 +355,7 @@ "@rolexjs/core": ["@rolexjs/core@workspace:packages/core"], - "@rolexjs/genesis": ["@rolexjs/genesis@workspace:prototypes/genesis"], + "@rolexjs/genesis": ["@rolexjs/genesis@workspace:packages/genesis"], "@rolexjs/local-platform": ["@rolexjs/local-platform@workspace:packages/local-platform"], diff --git a/package.json b/package.json index 8060ccf..5c279ef 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,7 @@ "type": "module", "workspaces": [ "packages/*", - "apps/*", - "prototypes/*" + "apps/*" ], "scripts": { "build": "turbo build", diff --git a/prototypes/genesis/charter.charter.feature b/packages/genesis/charter.charter.feature similarity index 100% rename from prototypes/genesis/charter.charter.feature rename to packages/genesis/charter.charter.feature diff --git a/prototypes/genesis/individual-management.requirement.feature b/packages/genesis/individual-management.requirement.feature similarity index 100% rename from prototypes/genesis/individual-management.requirement.feature rename to packages/genesis/individual-management.requirement.feature diff --git a/prototypes/genesis/individual-manager.position.feature b/packages/genesis/individual-manager.position.feature similarity index 100% rename from prototypes/genesis/individual-manager.position.feature rename to packages/genesis/individual-manager.position.feature diff --git a/prototypes/genesis/manage-individual-lifecycle.duty.feature b/packages/genesis/manage-individual-lifecycle.duty.feature similarity index 100% rename from prototypes/genesis/manage-individual-lifecycle.duty.feature rename to packages/genesis/manage-individual-lifecycle.duty.feature diff --git a/prototypes/genesis/manage-organization-lifecycle.duty.feature b/packages/genesis/manage-organization-lifecycle.duty.feature similarity index 100% rename from prototypes/genesis/manage-organization-lifecycle.duty.feature rename to packages/genesis/manage-organization-lifecycle.duty.feature diff --git a/prototypes/genesis/manage-position-lifecycle.duty.feature b/packages/genesis/manage-position-lifecycle.duty.feature similarity index 100% rename from prototypes/genesis/manage-position-lifecycle.duty.feature rename to packages/genesis/manage-position-lifecycle.duty.feature diff --git a/prototypes/genesis/nuwa.individual.feature b/packages/genesis/nuwa.individual.feature similarity index 100% rename from prototypes/genesis/nuwa.individual.feature rename to packages/genesis/nuwa.individual.feature diff --git a/prototypes/genesis/organization-management.requirement.feature b/packages/genesis/organization-management.requirement.feature similarity index 100% rename from prototypes/genesis/organization-management.requirement.feature rename to packages/genesis/organization-management.requirement.feature diff --git a/prototypes/genesis/organization-manager.position.feature b/packages/genesis/organization-manager.position.feature similarity index 100% rename from prototypes/genesis/organization-manager.position.feature rename to packages/genesis/organization-manager.position.feature diff --git a/prototypes/genesis/package.json b/packages/genesis/package.json similarity index 93% rename from prototypes/genesis/package.json rename to packages/genesis/package.json index 6cae799..5ffea60 100644 --- a/prototypes/genesis/package.json +++ b/packages/genesis/package.json @@ -11,7 +11,7 @@ "repository": { "type": "git", "url": "git+https://github.com/Deepractice/RoleX.git", - "directory": "prototypes/genesis" + "directory": "packages/genesis" }, "homepage": "https://github.com/Deepractice/RoleX", "license": "MIT", diff --git a/prototypes/genesis/position-management.requirement.feature b/packages/genesis/position-management.requirement.feature similarity index 100% rename from prototypes/genesis/position-management.requirement.feature rename to packages/genesis/position-management.requirement.feature diff --git a/prototypes/genesis/position-manager.position.feature b/packages/genesis/position-manager.position.feature similarity index 100% rename from prototypes/genesis/position-manager.position.feature rename to packages/genesis/position-manager.position.feature diff --git a/prototypes/genesis/prototype-management.procedure.feature b/packages/genesis/prototype-management.procedure.feature similarity index 100% rename from prototypes/genesis/prototype-management.procedure.feature rename to packages/genesis/prototype-management.procedure.feature diff --git a/prototypes/genesis/prototype.json b/packages/genesis/prototype.json similarity index 100% rename from prototypes/genesis/prototype.json rename to packages/genesis/prototype.json diff --git a/prototypes/genesis/resource-management.procedure.feature b/packages/genesis/resource-management.procedure.feature similarity index 100% rename from prototypes/genesis/resource-management.procedure.feature rename to packages/genesis/resource-management.procedure.feature diff --git a/prototypes/genesis/resource.json b/packages/genesis/resource.json similarity index 100% rename from prototypes/genesis/resource.json rename to packages/genesis/resource.json diff --git a/prototypes/genesis/rolex.organization.feature b/packages/genesis/rolex.organization.feature similarity index 100% rename from prototypes/genesis/rolex.organization.feature rename to packages/genesis/rolex.organization.feature diff --git a/prototypes/genesis/skill-creator.procedure.feature b/packages/genesis/skill-creator.procedure.feature similarity index 100% rename from prototypes/genesis/skill-creator.procedure.feature rename to packages/genesis/skill-creator.procedure.feature diff --git a/packages/prototype/tests/alignment.test.ts b/packages/prototype/tests/alignment.test.ts index 8e484bf..a1ae8b0 100644 --- a/packages/prototype/tests/alignment.test.ts +++ b/packages/prototype/tests/alignment.test.ts @@ -120,7 +120,7 @@ describe("alignment with rolex.ts toArgs switch", () => { // ================================================================ test("prototype.settle → [source]", () => { - const a = { source: "./prototypes/genesis" }; + const a = { source: "./packages/genesis" }; expect(toArgs("prototype.settle", a)).toEqual([a.source]); }); diff --git a/packages/prototype/tests/dispatch.test.ts b/packages/prototype/tests/dispatch.test.ts index 3d97e36..77952a6 100644 --- a/packages/prototype/tests/dispatch.test.ts +++ b/packages/prototype/tests/dispatch.test.ts @@ -31,8 +31,8 @@ describe("toArgs", () => { }); test("prototype.settle — source", () => { - expect(toArgs("prototype.settle", { source: "./prototypes/genesis" })).toEqual([ - "./prototypes/genesis", + expect(toArgs("prototype.settle", { source: "./packages/genesis" })).toEqual([ + "./packages/genesis", ]); }); From 1070a7859fbf360060aa52d0d48f7d05ee3814d3 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 11:57:48 +0800 Subject: [PATCH 35/54] fix: auto-inject all world descriptions into MCP instructions Previously census and use-protocol were missing from the manually maintained list. Now uses Object.values(world) so new descriptions are included automatically. Co-Authored-By: Claude Opus 4.6 --- .changeset/funky-windows-study.md | 5 +++++ apps/mcp-server/src/instructions.ts | 13 +------------ 2 files changed, 6 insertions(+), 12 deletions(-) create mode 100644 .changeset/funky-windows-study.md 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/apps/mcp-server/src/instructions.ts b/apps/mcp-server/src/instructions.ts index 35ff145..0c02627 100644 --- a/apps/mcp-server/src/instructions.ts +++ b/apps/mcp-server/src/instructions.ts @@ -6,15 +6,4 @@ */ import { world } from "rolexjs"; -export const instructions = [ - world["cognitive-priority"], - world["role-identity"], - world.nuwa, - 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"); From f7147ad60815f581f55311e1f85dacc25a53a114 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 12:23:45 +0800 Subject: [PATCH 36/54] feat: expand link targets in projection and render as subtrees Link targets (positions, organizations) now project with full subtree instead of empty snapshots. Render displays them as expanded heading trees instead of one-line references. Fixes #22. Co-Authored-By: Claude Opus 4.6 --- .changeset/mighty-pianos-wave.md | 6 ++++++ packages/rolexjs/src/render.ts | 18 +++++------------- packages/system/src/runtime.ts | 10 +++++++--- 3 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 .changeset/mighty-pianos-wave.md 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/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index 923e486..07047f5 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -164,12 +164,12 @@ export function renderState(state: State, depth = 1, options?: RenderStateOption lines.push(state.information); } - // Links + // Links — expanded as subtrees 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 targets = sortByConceptOrder(state.links.map((l) => l.target)); + for (const target of targets) { + lines.push(""); + lines.push(renderState(target, depth + 1, options)); } } @@ -185,14 +185,6 @@ export function renderState(state: State, depth = 1, options?: RenderStateOption 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 // ================================================================ diff --git a/packages/system/src/runtime.ts b/packages/system/src/runtime.ts index 3a93040..3af25b1 100644 --- a/packages/system/src/runtime.ts +++ b/packages/system/src/runtime.ts @@ -95,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 => { @@ -110,7 +114,7 @@ export const createRuntime = (): Runtime => { ? { links: nodeLinks.map((l) => ({ relation: l.relation, - target: projectRef(l.toId), + target: projectLinked(l.toId), })), } : {}), From 292357c5240f6c7761e1d6cccf1e18031b4df9c3 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 12:25:19 +0800 Subject: [PATCH 37/54] chore: fix unused variable lint warnings in test files Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/tests/mcp.test.ts | 2 +- packages/local-platform/tests/sqliteRuntime.test.ts | 1 - packages/prototype/tests/descriptions.test.ts | 4 ++-- packages/prototype/tests/instructions.test.ts | 3 +-- packages/prototype/tests/ops.test.ts | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/mcp-server/tests/mcp.test.ts b/apps/mcp-server/tests/mcp.test.ts index 1b4459a..8e19ffe 100644 --- a/apps/mcp-server/tests/mcp.test.ts +++ b/apps/mcp-server/tests/mcp.test.ts @@ -85,7 +85,7 @@ describe("render", () => { const role = await rolex.activate("sean"); // Use want + focus to get a result with state role.want("Feature: Test", "test-goal"); - const result = role.focus("test-goal"); + role.focus("test-goal"); // The role itself should have belong link — check via use const seanResult = await role.use("!role.focus", { goal: "test-goal" }); diff --git a/packages/local-platform/tests/sqliteRuntime.test.ts b/packages/local-platform/tests/sqliteRuntime.test.ts index f7855a4..108cd65 100644 --- a/packages/local-platform/tests/sqliteRuntime.test.ts +++ b/packages/local-platform/tests/sqliteRuntime.test.ts @@ -3,7 +3,6 @@ import { drizzle } from "@deepracticex/drizzle"; import { openDatabase } from "@deepracticex/sqlite"; import * as C from "@rolexjs/core"; import { sql } from "drizzle-orm"; -import { links, nodes } from "../src/schema.js"; import { createSqliteRuntime } from "../src/sqliteRuntime.js"; function setup() { diff --git a/packages/prototype/tests/descriptions.test.ts b/packages/prototype/tests/descriptions.test.ts index 53004a2..64ad03c 100644 --- a/packages/prototype/tests/descriptions.test.ts +++ b/packages/prototype/tests/descriptions.test.ts @@ -11,13 +11,13 @@ describe("descriptions", () => { }); test("every process description starts with Feature:", () => { - for (const [name, content] of Object.entries(processes)) { + 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)) { + for (const [_name, content] of Object.entries(world)) { expect(content).toMatch(/^Feature:/); } }); diff --git a/packages/prototype/tests/instructions.test.ts b/packages/prototype/tests/instructions.test.ts index a8942a6..fb8ae7b 100644 --- a/packages/prototype/tests/instructions.test.ts +++ b/packages/prototype/tests/instructions.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test"; import { instructions } from "../src/instructions.js"; -import type { InstructionDef } from "../src/schema.js"; describe("instructions registry", () => { test("all expected namespaces are present", () => { @@ -78,7 +77,7 @@ describe("instructions registry", () => { }); test("every instruction has at least one arg entry or zero params", () => { - for (const [key, def] of Object.entries(instructions)) { + for (const [_key, def] of Object.entries(instructions)) { const paramCount = Object.keys(def.params).length; if (paramCount > 0) { expect(def.args.length).toBeGreaterThan(0); diff --git a/packages/prototype/tests/ops.test.ts b/packages/prototype/tests/ops.test.ts index 6c502e6..b4991ba 100644 --- a/packages/prototype/tests/ops.test.ts +++ b/packages/prototype/tests/ops.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import * as C from "@rolexjs/core"; -import { createRuntime, type Runtime, type State, type Structure } from "@rolexjs/system"; +import { createRuntime, type State, type Structure } from "@rolexjs/system"; import { createOps, type Ops } from "../src/ops.js"; // ================================================================ From 260b06ddab76714dd71bcc111a99a8ace42c1c80 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 12:43:21 +0800 Subject: [PATCH 38/54] feat: expand link targets in SQLite runtime and fold requirement nodes SQLite runtime now uses projectLinked (full subtree without links) instead of projectRef (snapshot) for link targets, matching the in-memory runtime fix. Requirement nodes are folded by default since their content duplicates auto-trained procedures. Co-Authored-By: Claude Opus 4.6 --- .changeset/brave-cities-melt.md | 6 ++++++ packages/local-platform/src/sqliteRuntime.ts | 11 ++++++++--- packages/rolexjs/src/render.ts | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 .changeset/brave-cities-melt.md 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/packages/local-platform/src/sqliteRuntime.ts b/packages/local-platform/src/sqliteRuntime.ts index 227b70f..299f28b 100644 --- a/packages/local-platform/src/sqliteRuntime.ts +++ b/packages/local-platform/src/sqliteRuntime.ts @@ -56,17 +56,22 @@ function projectNode(db: DB, ref: string): State { ? { links: nodeLinks.map((l) => ({ relation: l.relation, - target: projectRef(db, l.toRef), + target: projectLinked(db, l.toRef), })), } : {}), }; } -function projectRef(db: DB, ref: string): State { +/** 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}`); - return { ...toStructure(row), children: [] }; + 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 ===== diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index 07047f5..f726c5f 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -153,8 +153,8 @@ export function renderState(state: State, depth = 1, options?: RenderStateOption const tagPart = state.tag ? ` #${state.tag}` : ""; lines.push(`${heading} [${state.name}]${idPart}${originPart}${tagPart}`); - // Folded: heading only - if (options?.fold?.(state)) { + // Folded: heading only (requirement always folds — content duplicates trained procedure) + if (state.name === "requirement" || options?.fold?.(state)) { return lines.join("\n"); } From 5ce5b986aefe6ca34cbde16b755d54375b793698 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 13:26:15 +0800 Subject: [PATCH 39/54] fix: add explicit instruction to never guess RoleX commands AI agents were guessing !namespace.method commands not seen in any loaded skill or world description, wasting tokens and causing errors. Added a NEVER guess scenario to use-protocol.feature. Co-Authored-By: Claude Opus 4.6 --- .changeset/no-guess-commands.md | 5 +++++ packages/prototype/src/descriptions/index.ts | 2 +- .../prototype/src/descriptions/world/use-protocol.feature | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/no-guess-commands.md 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/packages/prototype/src/descriptions/index.ts b/packages/prototype/src/descriptions/index.ts index 7f5ae0e..029c095 100644 --- a/packages/prototype/src/descriptions/index.ts +++ b/packages/prototype/src/descriptions/index.ts @@ -82,5 +82,5 @@ export const world: Record = { "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: Discovering available commands\n Given available commands are documented in world descriptions and skills\n When you need to perform an operation\n Then look up the correct command from world descriptions or loaded skills first\n And use only commands you have seen documented\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', + '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: Discovering available commands\n Given available commands are documented in world descriptions and skills\n When you need to perform an operation\n Then look up the correct command from world descriptions or loaded skills first\n And use only commands you have seen documented\n\n Scenario: NEVER guess commands\n Given a command is not found in any loaded skill or world description\n When the AI considers trying it anyway\n Then STOP — do not call use or direct with unverified commands\n And guessing wastes tokens, triggers errors, and erodes trust\n And instead ask the user or load the relevant skill first\n And there is no fallback — unknown commands simply do not exist\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/prototype/src/descriptions/world/use-protocol.feature b/packages/prototype/src/descriptions/world/use-protocol.feature index a16dac2..356a8a1 100644 --- a/packages/prototype/src/descriptions/world/use-protocol.feature +++ b/packages/prototype/src/descriptions/world/use-protocol.feature @@ -20,6 +20,14 @@ Feature: Use tool — the universal execution entry point Then look up the correct command from world descriptions or loaded skills first And use only commands you have seen documented + Scenario: NEVER guess commands + Given a command is not found in any loaded skill or world description + When the AI considers trying it anyway + Then STOP — do not call use or direct with unverified commands + And guessing wastes tokens, triggers errors, and erodes trust + And instead ask the user or load the relevant skill first + And there is no fallback — unknown commands simply do not exist + Scenario: Regular locators delegate to ResourceX Given the locator does not start with ! Then it is treated as a ResourceX locator From 4240f6bb073fa07538371452d88ea36b64d0ccde Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 13:39:23 +0800 Subject: [PATCH 40/54] fix: validate required params in dispatch and filter empty nodes in render (#23, #24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enforce required parameter validation in toArgs — missing required args now throw instead of silently passing undefined - Add isEmpty helper to renderState — nodes without id, information, or children are excluded from rendered output Closes #23, Closes #24 Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-required-params-empty-nodes.md | 9 +++++++ packages/prototype/src/dispatch.ts | 8 +++++++ packages/prototype/tests/dispatch.test.ts | 22 +++++++++++++++++ packages/rolexjs/src/render.ts | 9 +++++-- packages/rolexjs/tests/rolex.test.ts | 24 +++++++++++++++++++ 5 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-required-params-empty-nodes.md 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/packages/prototype/src/dispatch.ts b/packages/prototype/src/dispatch.ts index 219f6c4..6378aa2 100644 --- a/packages/prototype/src/dispatch.ts +++ b/packages/prototype/src/dispatch.ts @@ -18,6 +18,14 @@ import type { ArgEntry } from "./schema.js"; 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}.`); + } + } + return def.args.map((entry) => resolveArg(entry, args)); } diff --git a/packages/prototype/tests/dispatch.test.ts b/packages/prototype/tests/dispatch.test.ts index 77952a6..63b0ebe 100644 --- a/packages/prototype/tests/dispatch.test.ts +++ b/packages/prototype/tests/dispatch.test.ts @@ -107,6 +107,28 @@ describe("toArgs", () => { 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", () => { diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index f726c5f..0c4c4ff 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -173,9 +173,9 @@ export function renderState(state: State, depth = 1, options?: RenderStateOption } } - // Children — sorted by concept hierarchy + // Children — sorted by concept hierarchy, empty nodes filtered out if (state.children && state.children.length > 0) { - const sorted = sortByConceptOrder(state.children); + const sorted = sortByConceptOrder(state.children.filter((c) => !isEmpty(c))); for (const child of sorted) { lines.push(""); lines.push(renderState(child, depth + 1, options)); @@ -213,6 +213,11 @@ const CONCEPT_ORDER: readonly string[] = [ "duty", ]; +/** 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) => { diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index b4fbf29..a3e5af7 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -150,6 +150,30 @@ describe("render", () => { expect(md).toContain("## [plan]"); expect(md).toContain("### [task]"); }); + + 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"); + }); }); // ================================================================ From a96280cc9519a1cc673e8e47a07210c52b7252bb Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 13:45:32 +0800 Subject: [PATCH 41/54] refactor: remove requirement copy pattern from position.appoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - position.appoint no longer copies requirements as procedures — requirements are rendered through position links, like organization charters - Unfold requirement nodes in renderState so content is visible - Updating position requirements now automatically reflects on all individuals Co-Authored-By: Claude Opus 4.6 --- .changeset/remove-requirement-copy.md | 9 +++++++++ packages/prototype/src/ops.ts | 10 ---------- packages/prototype/tests/ops.test.ts | 6 ++---- packages/rolexjs/src/render.ts | 4 ++-- 4 files changed, 13 insertions(+), 16 deletions(-) create mode 100644 .changeset/remove-requirement-copy.md 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/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts index b7a4ca3..44a48e5 100644 --- a/packages/prototype/src/ops.ts +++ b/packages/prototype/src/ops.ts @@ -342,16 +342,6 @@ export function createOps(ctx: OpsContext): Ops { const posNode = resolve(position); const indNode = resolve(individual); rt.link(posNode, indNode, "appointment", "serve"); - const posState = rt.project(posNode); - const required = (posState.children ?? []).filter((c) => c.name === "requirement"); - for (const proc of required) { - if (proc.id) { - const indState = rt.project(indNode); - const existing = findInState(indState, proc.id); - if (existing) rt.remove(existing); - } - rt.create(indNode, C.procedure, proc.information, proc.id); - } return ok(posNode, "appoint"); }, diff --git a/packages/prototype/tests/ops.test.ts b/packages/prototype/tests/ops.test.ts index b4991ba..06d9124 100644 --- a/packages/prototype/tests/ops.test.ts +++ b/packages/prototype/tests/ops.test.ts @@ -493,7 +493,7 @@ describe("position", () => { expect(r.state.links![0].relation).toBe("appointment"); }); - test("appoint auto-trains required skills", () => { + test("appoint does not copy requirements as procedures", () => { const { ops, find } = setup(); ops["individual.born"](undefined, "sean"); ops["position.establish"](undefined, "architect"); @@ -503,9 +503,7 @@ describe("position", () => { const sean = find("sean")! as unknown as State; const procs = (sean.children ?? []).filter((c: State) => c.name === "procedure"); - expect(procs).toHaveLength(2); - const ids = procs.map((p: State) => p.id).sort(); - expect(ids).toEqual(["code-review", "sys-design"]); + expect(procs).toHaveLength(0); }); test("dismiss removes appointment", () => { diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index 0c4c4ff..e03c0cc 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -153,8 +153,8 @@ export function renderState(state: State, depth = 1, options?: RenderStateOption const tagPart = state.tag ? ` #${state.tag}` : ""; lines.push(`${heading} [${state.name}]${idPart}${originPart}${tagPart}`); - // Folded: heading only (requirement always folds — content duplicates trained procedure) - if (state.name === "requirement" || options?.fold?.(state)) { + // Folded: heading only + if (options?.fold?.(state)) { return lines.join("\n"); } From d7521ee39e9c5ec76b459325d117074331090888 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 13:55:34 +0800 Subject: [PATCH 42/54] feat: enforce global ID uniqueness across the state tree - Both in-memory and SQLite runtimes reject duplicate IDs at create time - Same ID under same parent remains idempotent (returns existing node) - Identity nodes use {id}-identity suffix to avoid conflicting with individual ID Co-Authored-By: Claude Opus 4.6 --- .changeset/enforce-global-id-uniqueness.md | 11 +++++++++++ packages/local-platform/src/sqliteRuntime.ts | 16 ++++++++-------- packages/prototype/src/ops.ts | 2 +- packages/prototype/tests/ops.test.ts | 16 ++++++++++++++++ packages/system/src/runtime.ts | 14 +++++++------- 5 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 .changeset/enforce-global-id-uniqueness.md 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/packages/local-platform/src/sqliteRuntime.ts b/packages/local-platform/src/sqliteRuntime.ts index 299f28b..30bfda3 100644 --- a/packages/local-platform/src/sqliteRuntime.ts +++ b/packages/local-platform/src/sqliteRuntime.ts @@ -96,14 +96,14 @@ function removeSubtree(db: DB, ref: string): void { export function createSqliteRuntime(db: DB): Runtime { return { create(parent, type, information, id, alias) { - // Idempotent: if parent has a child with the same id, return existing node. - if (id && parent?.ref) { - const existing = db - .select() - .from(nodes) - .where(and(eq(nodes.parentRef, parent.ref), eq(nodes.id, id))) - .get(); - if (existing) return toStructure(existing); + // Global uniqueness: no duplicate ids anywhere in the tree. + if (id) { + const existing = db.select().from(nodes).where(eq(nodes.id, id)).get(); + if (existing) { + // Idempotent: same id under same parent → return existing. + if (existing.parentRef === (parent?.ref ?? null)) return toStructure(existing); + throw new Error(`Duplicate id "${id}": already exists elsewhere in the tree.`); + } } const ref = nextRef(db); db.insert(nodes) diff --git a/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts index 44a48e5..b31a25c 100644 --- a/packages/prototype/src/ops.ts +++ b/packages/prototype/src/ops.ts @@ -103,7 +103,7 @@ export function createOps(ctx: OpsContext): Ops { "individual.born"(content?: string, id?: string, alias?: readonly string[]): OpResult { validateGherkin(content); const node = rt.create(society, C.individual, content, id, alias); - rt.create(node, C.identity, undefined, id); + rt.create(node, C.identity, undefined, id ? `${id}-identity` : undefined); return ok(node, "born"); }, diff --git a/packages/prototype/tests/ops.test.ts b/packages/prototype/tests/ops.test.ts index 06d9124..ae7570e 100644 --- a/packages/prototype/tests/ops.test.ts +++ b/packages/prototype/tests/ops.test.ts @@ -75,6 +75,22 @@ describe("individual", () => { expect(() => ops["individual.born"]("not gherkin")).toThrow("Invalid Gherkin"); }); + test("born uses distinct identity id", () => { + const { ops } = setup(); + const r = ops["individual.born"]("Feature: Sean", "sean"); + const identity = r.state.children!.find((c: State) => c.name === "identity"); + expect(identity!.id).toBe("sean-identity"); + }); + + test("duplicate id across tree throws", () => { + const { ops } = setup(); + ops["individual.born"](undefined, "sean"); + ops["org.found"](undefined, "acme"); + expect(() => ops["org.charter"]("acme", "Feature: Charter", "sean")).toThrow( + 'Duplicate id "sean"' + ); + }); + test("retire archives individual to past", () => { const { ops, find } = setup(); ops["individual.born"]("Feature: Sean", "sean"); diff --git a/packages/system/src/runtime.ts b/packages/system/src/runtime.ts index 3af25b1..63aafce 100644 --- a/packages/system/src/runtime.ts +++ b/packages/system/src/runtime.ts @@ -152,13 +152,13 @@ export const createRuntime = (): Runtime => { return { create(parent, type, information, id, alias) { - // Idempotent: if parent has a child with the same id, return existing node. - if (id && parent?.ref) { - const parentTreeNode = nodes.get(parent.ref); - if (parentTreeNode) { - for (const childRef of parentTreeNode.children) { - const child = nodes.get(childRef); - if (child && child.node.id === id) return child.node; + if (id) { + // Global uniqueness: check all nodes for a matching id. + for (const treeNode of nodes.values()) { + if (treeNode.node.id === id) { + // Idempotent: same id under same parent → return existing. + if (treeNode.parent === (parent?.ref ?? null)) return treeNode.node; + throw new Error(`Duplicate id "${id}": already exists elsewhere in the tree.`); } } } From e07c99944d30630053d8bb70c271d94539480bb4 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 15:51:19 +0800 Subject: [PATCH 43/54] feat: realize/reflect accept empty IDs and BDD test framework Allow realize and reflect to be called with empty source IDs, enabling direct creation of principles and experiences from conversational insights without requiring the full cognition chain. Also sets up BDD test infrastructure with @deepracticex/bdd and @modelcontextprotocol/sdk for MCP E2E testing. Co-Authored-By: Claude Opus 4.6 --- .changeset/realize-reflect-empty-ids.md | 13 +++++ apps/mcp-server/package.json | 4 +- apps/mcp-server/src/index.ts | 4 +- bdd/journeys/mcp-startup.feature | 37 +++++++++++++++ bdd/run.test.ts | 20 ++++++++ bdd/steps/mcp.steps.ts | 56 ++++++++++++++++++++++ bdd/support/mcp-world.ts | 63 +++++++++++++++++++++++++ bun.lock | 29 +++++++++++- package.json | 6 ++- packages/prototype/src/ops.ts | 46 +++++++++++------- packages/rolexjs/src/role.ts | 24 ++++++---- packages/rolexjs/tests/context.test.ts | 53 +++++++++++++++++++++ 12 files changed, 323 insertions(+), 32 deletions(-) create mode 100644 .changeset/realize-reflect-empty-ids.md create mode 100644 bdd/journeys/mcp-startup.feature create mode 100644 bdd/run.test.ts create mode 100644 bdd/steps/mcp.steps.ts create mode 100644 bdd/support/mcp-world.ts 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/apps/mcp-server/package.json b/apps/mcp-server/package.json index 228ff21..6889e00 100644 --- a/apps/mcp-server/package.json +++ b/apps/mcp-server/package.json @@ -47,7 +47,9 @@ "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 435ef8b..5825743 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -199,7 +199,7 @@ server.addTool({ }), execute: async ({ ids, id, experience }) => { const role = state.requireRole(); - const result = role.reflect(ids[0], experience, id); + const result = role.reflect(ids[0] ?? undefined, experience, id); return fmt("reflect", id, result); }, }); @@ -214,7 +214,7 @@ server.addTool({ }), execute: async ({ ids, id, principle }) => { const role = state.requireRole(); - const result = role.realize(ids[0], principle, id); + const result = role.realize(ids[0] ?? undefined, principle, id); return fmt("realize", id, result); }, }); 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/run.test.ts b/bdd/run.test.ts new file mode 100644 index 0000000..2ca133a --- /dev/null +++ b/bdd/run.test.ts @@ -0,0 +1,20 @@ +/** + * 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 +import "./support/mcp-world"; + +// Steps +import "./steps/mcp.steps"; + +// Timeout: MCP server startup can take a few seconds +setDefaultTimeout(15_000); + +// ===== Journeys ===== +loadFeature("bdd/journeys/mcp-startup.feature"); diff --git a/bdd/steps/mcp.steps.ts b/bdd/steps/mcp.steps.ts new file mode 100644 index 0000000..a2e8dd2 --- /dev/null +++ b/bdd/steps/mcp.steps.ts @@ -0,0 +1,56 @@ +/** + * 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 { McpWorld } from "../support/mcp-world"; + +// ===== Setup ===== + +Given("the MCP server is running", async function (this: McpWorld) { + await this.connect(); +}); + +// ===== Tool listing ===== + +Then("the following tools should be available:", async function (this: McpWorld, 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: McpWorld, 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: McpWorld, 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/support/mcp-world.ts b/bdd/support/mcp-world.ts new file mode 100644 index 0000000..00a5bc5 --- /dev/null +++ b/bdd/support/mcp-world.ts @@ -0,0 +1,63 @@ +/** + * MCP World — test context for MCP-level BDD tests. + * + * Manages a real MCP server child process connected via stdio transport. + * Each scenario gets a fresh client connection. + */ + +import { join } from "node:path"; +import type { IWorldOptions } from "@deepracticex/bdd"; +import { AfterAll, BeforeAll, setWorldConstructor, World } from "@deepracticex/bdd"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +const SERVER_PATH = join(import.meta.dirname, "../../apps/mcp-server/src/index.ts"); + +// Shared client across scenarios (MCP startup is expensive) +let sharedClient: Client | null = null; +let sharedTransport: StdioClientTransport | null = null; + +async function ensureClient(): Promise { + if (sharedClient) return sharedClient; + + sharedTransport = new StdioClientTransport({ + command: "bun", + args: ["run", SERVER_PATH], + }); + + sharedClient = new Client({ + name: "rolex-bdd-test", + version: "1.0.0", + }); + + await sharedClient.connect(sharedTransport); + return sharedClient; +} + +AfterAll(async () => { + if (sharedClient) { + await sharedClient.close(); + sharedClient = null; + } + if (sharedTransport) { + await sharedTransport.close(); + sharedTransport = null; + } +}); + +export class McpWorld extends World { + client!: Client; + toolResult?: string; + error?: Error; + tools?: Array<{ name: string }>; + + constructor(options: IWorldOptions) { + super(options); + } + + async connect(): Promise { + this.client = await ensureClient(); + } +} + +setWorldConstructor(McpWorld); diff --git a/bun.lock b/bun.lock index 6bc9938..6489536 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,8 @@ "@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", @@ -37,6 +39,9 @@ "rolexjs": "workspace:*", "zod": "^3.25.0", }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", + }, }, "packages/core": { "name": "@rolexjs/core", @@ -207,10 +212,14 @@ "@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=="], @@ -289,7 +298,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=="], @@ -443,6 +452,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=="], @@ -931,6 +942,10 @@ "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=="], @@ -1073,6 +1088,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=="], @@ -1097,6 +1114,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=="], @@ -1113,6 +1134,8 @@ "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=="], @@ -1139,6 +1162,8 @@ "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=="], @@ -1167,6 +1192,8 @@ "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=="], diff --git a/package.json b/package.json index 5c279ef..b426da7 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,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 +27,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", diff --git a/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts index b31a25c..b791cf5 100644 --- a/packages/prototype/src/ops.ts +++ b/packages/prototype/src/ops.ts @@ -211,38 +211,48 @@ export function createOps(ctx: OpsContext): Ops { // ---- Role: cognition ---- "role.reflect"( - encounter: string, + encounter: string | undefined, individual: string, experience?: string, id?: string ): OpResult { validateGherkin(experience); - const encNode = resolve(encounter); - const exp = rt.create( - resolve(individual), - C.experience, - experience || encNode.information, - id - ); - rt.remove(encNode); + if (encounter) { + const encNode = resolve(encounter); + const exp = rt.create( + resolve(individual), + C.experience, + experience || encNode.information, + id + ); + rt.remove(encNode); + return ok(exp, "reflect"); + } + // Direct creation — no encounter to consume + const exp = rt.create(resolve(individual), C.experience, experience, id); return ok(exp, "reflect"); }, "role.realize"( - experience: string, + experience: string | undefined, individual: string, principle?: string, id?: string ): OpResult { validateGherkin(principle); - const expNode = resolve(experience); - const prin = rt.create( - resolve(individual), - C.principle, - principle || expNode.information, - id - ); - rt.remove(expNode); + if (experience) { + const expNode = resolve(experience); + const prin = rt.create( + resolve(individual), + C.principle, + principle || expNode.information, + id + ); + rt.remove(expNode); + return ok(prin, "realize"); + } + // Direct creation — no experience to consume + const prin = rt.create(resolve(individual), C.principle, principle, id); return ok(prin, "realize"); }, diff --git a/packages/rolexjs/src/role.ts b/packages/rolexjs/src/role.ts index f99d31c..376a700 100644 --- a/packages/rolexjs/src/role.ts +++ b/packages/rolexjs/src/role.ts @@ -129,20 +129,28 @@ export class Role { // ---- Cognition ---- - /** Reflect: consume encounter, create experience. */ - reflect(encounter: string, experience?: string, id?: string): RolexResult { - this.ctx.requireEncounterIds([encounter]); + /** Reflect: consume encounter → experience. Empty encounter = direct creation. */ + reflect(encounter?: string, experience?: string, id?: string): RolexResult { + if (encounter) { + this.ctx.requireEncounterIds([encounter]); + } const result = this.api.ops["role.reflect"](encounter, this.roleId, experience, id); - this.ctx.consumeEncounters([encounter]); + if (encounter) { + this.ctx.consumeEncounters([encounter]); + } if (id) this.ctx.addExperience(id); return this.withHint(result, "reflect"); } - /** Realize: consume experience, create principle. */ - realize(experience: string, principle?: string, id?: string): RolexResult { - this.ctx.requireExperienceIds([experience]); + /** Realize: consume experience → principle. Empty experience = direct creation. */ + realize(experience?: string, principle?: string, id?: string): RolexResult { + if (experience) { + this.ctx.requireExperienceIds([experience]); + } const result = this.api.ops["role.realize"](experience, this.roleId, principle, id); - this.ctx.consumeExperiences([experience]); + if (experience) { + this.ctx.consumeExperiences([experience]); + } return this.withHint(result, "realize"); } diff --git a/packages/rolexjs/tests/context.test.ts b/packages/rolexjs/tests/context.test.ts index 8a86387..54d799f 100644 --- a/packages/rolexjs/tests/context.test.ts +++ b/packages/rolexjs/tests/context.test.ts @@ -116,6 +116,59 @@ describe("Role (ctx management)", () => { expect(role.ctx.experienceIds.has("token-insight")).toBe(true); }); + test("reflect without encounter creates experience directly", async () => { + const rolex = setup(); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); + + const result = role.reflect( + undefined, + "Feature: Direct insight\n Scenario: OK\n Given learned from conversation", + "conv-insight" + ); + + expect(result.state.name).toBe("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 = setup(); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); + + const result = role.realize( + undefined, + "Feature: Direct principle\n Scenario: OK\n Given always blame the product", + "product-first" + ); + + expect(result.state.name).toBe("principle"); + expect(role.ctx.experienceIds.size).toBe(0); + }); + + test("realize still consumes experience when provided", async () => { + const rolex = setup(); + await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); + const role = await rolex.activate("sean"); + + // Create experience directly + role.reflect( + undefined, + "Feature: Insight\n Scenario: OK\n Given something learned", + "my-insight" + ); + expect(role.ctx.experienceIds.has("my-insight")).toBe(true); + + // Realize from that experience + 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"); From 4f4af2709e5722b180de44a18d8f4033c68e7903 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 15:51:46 +0800 Subject: [PATCH 44/54] feat: identity ethics and directive system for role boundaries Add identity-ethics.feature as the foundational world instruction (@priority-critical) establishing that roles must refuse work outside their duties and suggest Nuwa as fallback. Build directive system for enforcing boundaries at decision points: - gen-directives.ts parses Gherkin into topic/scenario structure - directive() API in rolexjs for contextual retrieval - Wire on-unknown-command directive into error handling Clean up command knowledge leaks: - Remove nuwa.feature (leaked world-building commands to all roles) - Update use-protocol (commands from own skills only) - Update census (help find people, not teach operations) - Remove teach/train from cognition.feature Co-Authored-By: Claude Opus 4.6 --- .changeset/identity-ethics-directives.md | 16 +++++ packages/prototype/package.json | 3 +- .../prototype/scripts/gen-descriptions.ts | 38 ++++++++++-- packages/prototype/scripts/gen-directives.ts | 61 +++++++++++++++++++ packages/prototype/src/descriptions/index.ts | 17 +++--- .../src/descriptions/world/census.feature | 10 +-- .../src/descriptions/world/cognition.feature | 8 --- .../world/cognitive-priority.feature | 1 + .../world/identity-ethics.feature | 38 ++++++++++++ .../src/descriptions/world/nuwa.feature | 31 ---------- .../descriptions/world/role-identity.feature | 1 + .../descriptions/world/use-protocol.feature | 7 ++- .../src/directives/identity-ethics.feature | 15 +++++ packages/prototype/src/directives/index.ts | 10 +++ packages/prototype/src/index.ts | 2 + packages/rolexjs/src/index.ts | 2 +- packages/rolexjs/src/render.ts | 11 +++- packages/rolexjs/src/rolex.ts | 7 ++- 18 files changed, 213 insertions(+), 65 deletions(-) create mode 100644 .changeset/identity-ethics-directives.md create mode 100644 packages/prototype/scripts/gen-directives.ts create mode 100644 packages/prototype/src/descriptions/world/identity-ethics.feature delete mode 100644 packages/prototype/src/descriptions/world/nuwa.feature create mode 100644 packages/prototype/src/directives/identity-ethics.feature create mode 100644 packages/prototype/src/directives/index.ts 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/packages/prototype/package.json b/packages/prototype/package.json index 604bc13..5527e19 100644 --- a/packages/prototype/package.json +++ b/packages/prototype/package.json @@ -32,7 +32,8 @@ ], "scripts": { "gen:desc": "bun run scripts/gen-descriptions.ts", - "build": "bun run gen:desc && tsup", + "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" diff --git a/packages/prototype/scripts/gen-descriptions.ts b/packages/prototype/scripts/gen-descriptions.ts index 91f0bee..748c36a 100644 --- a/packages/prototype/scripts/gen-descriptions.ts +++ b/packages/prototype/scripts/gen-descriptions.ts @@ -25,8 +25,29 @@ 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: string[] = []; +const worldEntries: { name: string; priority: number; entry: string }[] = []; for (const dir of dirs.sort()) { const dirPath = join(descDir, dir); @@ -36,17 +57,22 @@ for (const dir of dirs.sort()) { for (const f of features) { const name = basename(f, ".feature"); - const content = readFileSync(join(dirPath, f), "utf-8").trimEnd(); - const entry = ` "${name}": ${JSON.stringify(content)},`; + const raw = readFileSync(join(dirPath, f), "utf-8").trimEnd(); if (dir === "world") { - worldEntries.push(entry); + 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. @@ -55,11 +81,11 @@ ${processEntries.join("\n")} } as const; export const world: Record = { -${worldEntries.join("\n")} +${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)` + `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 index 029c095..c0c3d90 100644 --- a/packages/prototype/src/descriptions/index.ts +++ b/packages/prototype/src/descriptions/index.ts @@ -60,12 +60,16 @@ export const processes: Record = { } as const; export const world: Record = { - 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: Census before action\n Given I need to check existence before creating something\n When I want to found an org, born an individual, or establish a position\n Then call census.list first to avoid duplicates', - 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: External injection\n Given an external agent needs to equip a role with knowledge or skills\n Then teach(individual, principle, id) directly injects a principle\n And train(individual, procedure, id) directly injects a procedure\n And the difference from realize/master is perspective — external vs self-initiated\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", + "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: 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\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 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: @@ -74,13 +78,10 @@ export const world: Record = { '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"', - nuwa: "Feature: Nuwa — the entry point of the RoleX world\n Nuwa is the meta-role that bootstraps everything.\n When a user has no role or doesn't know where to start, Nuwa is the answer.\n\n Scenario: No role active — suggest Nuwa\n Given a user starts a conversation with no active role\n And the user doesn't know which role to activate\n When the AI needs to suggest a starting point\n Then suggest activating Nuwa — she is the default entry point\n And say \"activate nuwa\" or the equivalent in the user's language\n\n Scenario: What Nuwa can do\n Given Nuwa is activated\n Then she can create new individuals with born\n And she can found organizations and establish positions\n And she can equip any individual with knowledge via teach and train\n And she can manage prototypes and resources\n And she is the only role that operates at the world level\n\n Scenario: When to use Nuwa vs a specific role\n Given the user wants to do daily work — coding, writing, designing\n Then they should activate their own role, not Nuwa\n And Nuwa is for world-building — creating roles, organizations, and structure\n And once the world is set up, Nuwa steps back and specific roles take over\n\n Scenario: First-time user flow\n Given a brand new user with no individuals created yet\n When they activate Nuwa\n Then Nuwa helps them create their first individual with born\n And guides them to set up identity, goals, and organizational context\n And once their role exists, they switch to it with activate", - "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 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\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 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", "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: Discovering available commands\n Given available commands are documented in world descriptions and skills\n When you need to perform an operation\n Then look up the correct command from world descriptions or loaded skills first\n And use only commands you have seen documented\n\n Scenario: NEVER guess commands\n Given a command is not found in any loaded skill or world description\n When the AI considers trying it anyway\n Then STOP — do not call use or direct with unverified commands\n And guessing wastes tokens, triggers errors, and erodes trust\n And instead ask the user or load the relevant skill first\n And there is no fallback — unknown commands simply do not exist\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', + '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: Discovering available commands\n Given available commands come from your role\'s own skills and procedures\n When you need to perform an operation\n Then look up the correct command from your loaded skills first\n And use only commands you have seen in your own skills or procedures\n And do not use commands mentioned in other roles\' descriptions\n\n Scenario: NEVER guess commands\n Given a command is not found in any loaded skill or world description\n When the AI considers trying it anyway\n Then STOP — do not call use or direct with unverified commands\n And guessing wastes tokens, triggers errors, and erodes trust\n And instead ask the user or load the relevant skill first\n And there is no fallback — unknown commands simply do not exist\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/prototype/src/descriptions/world/census.feature b/packages/prototype/src/descriptions/world/census.feature index 35d47ce..7089307 100644 --- a/packages/prototype/src/descriptions/world/census.feature +++ b/packages/prototype/src/descriptions/world/census.feature @@ -21,7 +21,9 @@ Feature: Census — the only way to query what exists in the world When I call direct("!census.list", { type: "past" }) Then archived entities are returned - Scenario: Census before action - Given I need to check existence before creating something - When I want to found an org, born an individual, or establish a position - Then call census.list first to avoid duplicates + 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/prototype/src/descriptions/world/cognition.feature b/packages/prototype/src/descriptions/world/cognition.feature index e984869..8a3e50f 100644 --- a/packages/prototype/src/descriptions/world/cognition.feature +++ b/packages/prototype/src/descriptions/world/cognition.feature @@ -11,14 +11,6 @@ Feature: Cognition — the learning cycle 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 an external agent needs to equip a role with knowledge or skills - Then teach(individual, principle, id) directly injects a principle - And train(individual, procedure, id) directly injects a procedure - And the difference from realize/master is perspective — external vs self-initiated - 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/prototype/src/descriptions/world/cognitive-priority.feature b/packages/prototype/src/descriptions/world/cognitive-priority.feature index 5ad19d5..ad74a47 100644 --- a/packages/prototype/src/descriptions/world/cognitive-priority.feature +++ b/packages/prototype/src/descriptions/world/cognitive-priority.feature @@ -1,3 +1,4 @@ +@priority-high Feature: Cognitive priority — RoleX tools over native alternatives When RoleX provides a tool for something, use the RoleX tool instead of any native alternative provided by the host environment. 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/prototype/src/descriptions/world/nuwa.feature b/packages/prototype/src/descriptions/world/nuwa.feature deleted file mode 100644 index 6ed694c..0000000 --- a/packages/prototype/src/descriptions/world/nuwa.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: Nuwa — the entry point of the RoleX world - Nuwa is the meta-role that bootstraps everything. - When a user has no role or doesn't know where to start, Nuwa is the answer. - - Scenario: No role active — suggest Nuwa - Given a user starts a conversation with no active role - And the user doesn't know which role to activate - When the AI needs to suggest a starting point - Then suggest activating Nuwa — she is the default entry point - And say "activate nuwa" or the equivalent in the user's language - - Scenario: What Nuwa can do - Given Nuwa is activated - Then she can create new individuals with born - And she can found organizations and establish positions - And she can equip any individual with knowledge via teach and train - And she can manage prototypes and resources - And she is the only role that operates at the world level - - Scenario: When to use Nuwa vs a specific role - Given the user wants to do daily work — coding, writing, designing - Then they should activate their own role, not Nuwa - And Nuwa is for world-building — creating roles, organizations, and structure - And once the world is set up, Nuwa steps back and specific roles take over - - Scenario: First-time user flow - Given a brand new user with no individuals created yet - When they activate Nuwa - Then Nuwa helps them create their first individual with born - And guides them to set up identity, goals, and organizational context - And once their role exists, they switch to it with activate diff --git a/packages/prototype/src/descriptions/world/role-identity.feature b/packages/prototype/src/descriptions/world/role-identity.feature index fbb7181..aed43e3 100644 --- a/packages/prototype/src/descriptions/world/role-identity.feature +++ b/packages/prototype/src/descriptions/world/role-identity.feature @@ -1,3 +1,4 @@ +@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. diff --git a/packages/prototype/src/descriptions/world/use-protocol.feature b/packages/prototype/src/descriptions/world/use-protocol.feature index 356a8a1..603072d 100644 --- a/packages/prototype/src/descriptions/world/use-protocol.feature +++ b/packages/prototype/src/descriptions/world/use-protocol.feature @@ -15,10 +15,11 @@ Feature: Use tool — the universal execution entry point And dispatched to the corresponding RoleX API with named args Scenario: Discovering available commands - Given available commands are documented in world descriptions and skills + Given available commands come from your role's own skills and procedures When you need to perform an operation - Then look up the correct command from world descriptions or loaded skills first - And use only commands you have seen documented + Then look up the correct command from your loaded skills first + And use only commands you have seen in your own skills or procedures + And do not use commands mentioned in other roles' descriptions Scenario: NEVER guess commands Given a command is not found in any loaded skill or world description diff --git a/packages/prototype/src/directives/identity-ethics.feature b/packages/prototype/src/directives/identity-ethics.feature new file mode 100644 index 0000000..313348c --- /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 try another command. Do not search for the correct command. + And This is not a technical error. This is a boundary you must not cross. + And You do not have this command in your skills because it is not your responsibility. + And Tell the user: this is outside your duties. Suggest activating 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..16e0472 --- /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 try another command. Do not search for the correct command.\nThis is not a technical error. This is a boundary you must not cross.\nYou do not have this command in your skills because it is not your responsibility.\nTell the user: this is outside your duties. Suggest activating 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/index.ts b/packages/prototype/src/index.ts index f6fc189..cf65ddf 100644 --- a/packages/prototype/src/index.ts +++ b/packages/prototype/src/index.ts @@ -10,6 +10,8 @@ // 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 diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index e23adb7..510861c 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -19,7 +19,7 @@ export type { DataTableRow, Feature, Scenario, Step } from "./feature.js"; export { parse, serialize } from "./feature.js"; export type { RenderStateOptions } from "./render.js"; // Render -export { describe, detail, hint, renderState, world } from "./render.js"; +export { describe, detail, directive, hint, renderState, world } from "./render.js"; export type { RolexResult } from "./role.js"; // Role export { Role } from "./role.js"; diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index e03c0cc..9c6e52b 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -111,7 +111,7 @@ export function hint(process: string): string { // Detail — longer process descriptions (from .feature files) // ================================================================ -import { processes, world } from "@rolexjs/prototype"; +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,6 +121,15 @@ 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 // ================================================================ diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index e46a1c3..4a022b5 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -12,7 +12,7 @@ import type { ContextData, Platform } from "@rolexjs/core"; import * as C from "@rolexjs/core"; -import { createOps, type Ops, toArgs } from "@rolexjs/prototype"; +import { createOps, directives, type Ops, toArgs } from "@rolexjs/prototype"; import type { Initializer, Runtime, State, Structure } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; import { RoleContext } from "./context.js"; @@ -159,7 +159,10 @@ export class Rolex { if (locator.startsWith("!")) { const command = locator.slice(1); const fn = this.ops[command]; - if (!fn) throw new Error(`Unknown command "${locator}".`); + if (!fn) { + const cmd = directives["identity-ethics"]?.["on-unknown-command"] ?? ""; + throw new Error(`Unknown command "${locator}".\n\n${cmd}`); + } return fn(...toArgs(command, args ?? {})) as T; } if (!this.resourcex) throw new Error("ResourceX is not available."); From 8ef2b4c5ba41e7d3b3d685fdf33d088cc7d7d39a Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 28 Feb 2026 17:23:31 +0800 Subject: [PATCH 45/54] refactor: sink render layer from MCP server into rolexjs Move render() into rolexjs so Role methods return rendered 3-layer text directly. MCP server becomes a pure pass-through with no render logic. RolexResult replaced by string returns; fold logic for activate now lives inside Role.project(). Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 79 ++++----------------- apps/mcp-server/src/render.ts | 50 ------------- apps/mcp-server/tests/mcp.test.ts | 52 ++++++-------- packages/rolexjs/src/index.ts | 5 +- packages/rolexjs/src/render.ts | 48 +++++++++++-- packages/rolexjs/src/role.ts | 97 +++++++++++++------------- packages/rolexjs/src/rolex.ts | 19 ++--- packages/rolexjs/tests/context.test.ts | 12 ++-- packages/rolexjs/tests/rolex.test.ts | 27 +++---- 9 files changed, 159 insertions(+), 230 deletions(-) delete mode 100644 apps/mcp-server/src/render.ts diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 5825743..a6ff79b 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -1,8 +1,7 @@ /** * @rolexjs/mcp-server — individual-level MCP tools. * - * Thin wrapper around the Rolex API. All business logic (state tracking, - * cognitive hints, encounter/experience registries) lives in Role + RoleContext. + * Pure pass-through: all rendering happens in rolexjs. * MCP only translates protocol calls to API calls. */ @@ -12,7 +11,6 @@ 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 ========== @@ -33,21 +31,6 @@ const server = new FastMCP({ instructions, }); -// ========== Helpers ========== - -function fmt( - process: string, - label: string, - result: { state: any; process: string; hint?: string } -) { - return render({ - process, - name: label, - result, - cognitiveHint: result.hint ?? null, - }); -} - // ========== Tools: Role ========== server.addTool({ @@ -60,15 +43,7 @@ server.addTool({ try { const role = await rolex.activate(roleId); state.role = role; - const result = role.project(); - const focusedGoalId = role.ctx.focusedGoalId; - return render({ - process: "activate", - name: roleId, - result, - cognitiveHint: result.hint ?? null, - fold: (node) => node.name === "goal" && node.id !== focusedGoalId, - }); + return role.project(); } catch { const census = await rolex.direct("!census.list"); throw new Error( @@ -85,9 +60,7 @@ server.addTool({ id: z.string().optional().describe("Goal id to switch to. Omit to view current."), }), execute: async ({ id }) => { - const role = state.requireRole(); - const result = role.focus(id); - return fmt("focus", id ?? "current goal", result); + return state.requireRole().focus(id); }, }); @@ -101,9 +74,7 @@ server.addTool({ goal: z.string().describe("Gherkin Feature source describing the goal"), }), execute: async ({ id, goal }) => { - const role = state.requireRole(); - const result = role.want(goal, id); - return fmt("want", id, result); + return state.requireRole().want(goal, id); }, }); @@ -123,9 +94,7 @@ server.addTool({ .describe("Plan id this plan is a backup for (alternative/strategy relationship)"), }), execute: async ({ id, plan, after, fallback }) => { - const role = state.requireRole(); - const result = role.plan(plan, id, after, fallback); - return fmt("plan", id, result); + return state.requireRole().plan(plan, id, after, fallback); }, }); @@ -137,9 +106,7 @@ server.addTool({ task: z.string().describe("Gherkin Feature source describing the task"), }), execute: async ({ id, task }) => { - const role = state.requireRole(); - const result = role.todo(task, id); - return fmt("todo", id, result); + return state.requireRole().todo(task, id); }, }); @@ -151,9 +118,7 @@ server.addTool({ encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"), }), execute: async ({ id, encounter }) => { - const role = state.requireRole(); - const result = role.finish(id, encounter); - return fmt("finish", id, result); + return state.requireRole().finish(id, encounter); }, }); @@ -165,9 +130,7 @@ server.addTool({ encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"), }), execute: async ({ id, encounter }) => { - const role = state.requireRole(); - const result = role.complete(id, encounter); - return fmt("complete", id ?? "focused plan", result); + return state.requireRole().complete(id, encounter); }, }); @@ -179,9 +142,7 @@ server.addTool({ encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"), }), execute: async ({ id, encounter }) => { - const role = state.requireRole(); - const result = role.abandon(id, encounter); - return fmt("abandon", id ?? "focused plan", result); + return state.requireRole().abandon(id, encounter); }, }); @@ -198,9 +159,7 @@ server.addTool({ experience: z.string().optional().describe("Gherkin Feature source for the experience"), }), execute: async ({ ids, id, experience }) => { - const role = state.requireRole(); - const result = role.reflect(ids[0] ?? undefined, experience, id); - return fmt("reflect", id, result); + return state.requireRole().reflect(ids[0] ?? undefined, experience, id); }, }); @@ -213,9 +172,7 @@ server.addTool({ principle: z.string().optional().describe("Gherkin Feature source for the principle"), }), execute: async ({ ids, id, principle }) => { - const role = state.requireRole(); - const result = role.realize(ids[0] ?? undefined, principle, id); - return fmt("realize", id, result); + return state.requireRole().realize(ids[0] ?? undefined, principle, id); }, }); @@ -228,9 +185,7 @@ server.addTool({ procedure: z.string().describe("Gherkin Feature source for the procedure"), }), execute: async ({ ids, id, procedure }) => { - const role = state.requireRole(); - const result = role.master(procedure, id, ids?.[0]); - return fmt("master", id, result); + return state.requireRole().master(procedure, id, ids?.[0]); }, }); @@ -245,9 +200,7 @@ server.addTool({ .describe("Id of the node to remove (principle, procedure, experience, encounter, etc.)"), }), execute: async ({ id }) => { - const role = state.requireRole(); - const result = role.forget(id); - return fmt("forget", id, result); + return state.requireRole().forget(id); }, }); @@ -262,8 +215,7 @@ server.addTool({ .describe("ResourceX locator for the skill (e.g. deepractice/role-management)"), }), execute: async ({ locator }) => { - const role = state.requireRole(); - return role.skill(locator); + return state.requireRole().skill(locator); }, }); @@ -281,8 +233,7 @@ server.addTool({ args: z.record(z.unknown()).optional().describe("Named arguments for the command or resource"), }), execute: async ({ locator, args }) => { - const role = state.requireRole(); - const result = await role.use(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); diff --git a/apps/mcp-server/src/render.ts b/apps/mcp-server/src/render.ts deleted file mode 100644 index 094d341..0000000 --- a/apps/mcp-server/src/render.ts +++ /dev/null @@ -1,50 +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 { RenderStateOptions, 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; - /** 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, result, cognitiveHint, fold } = 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, 1, fold ? { fold } : undefined)); - - return lines.join("\n"); -} diff --git a/apps/mcp-server/tests/mcp.test.ts b/apps/mcp-server/tests/mcp.test.ts index 8e19ffe..f69a1ad 100644 --- a/apps/mcp-server/tests/mcp.test.ts +++ b/apps/mcp-server/tests/mcp.test.ts @@ -1,14 +1,14 @@ /** * MCP server integration tests. * - * Tests the thin MCP layer (state + render) on top of Rolex. + * Tests the thin MCP layer (state holder) on top of Rolex. * Business logic (RoleContext) is tested in rolexjs/tests/context.test.ts. - * This file tests MCP-specific concerns: state holder, render, and integration. + * 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, type RolexResult } 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; @@ -38,19 +38,19 @@ describe("requireRole", () => { }); // ================================================================ -// Render: 3-layer output +// Render: 3-layer output (now in rolexjs) // ================================================================ describe("render", () => { it("includes status + hint + projection", async () => { - const result = await rolex.direct("!individual.born", { + 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.'); @@ -62,38 +62,30 @@ describe("render", () => { }); it("includes cognitive hint when provided", async () => { - const result = await rolex.direct("!individual.born", { + const result = await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean", }); const output = render({ process: "born", name: "Sean", - result, + state: result.state, cognitiveHint: "I have no goal yet. Declare one with want.", }); expect(output).toContain("I →"); expect(output).toContain("I have no goal yet"); }); - it("includes bidirectional links in projection", async () => { + it("Role methods return rendered 3-layer output", async () => { await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); - await rolex.direct("!org.found", { content: "Feature: Deepractice", id: "dp" }); - await rolex.direct("!org.hire", { org: "dp", individual: "sean" }); - - // Activate and use focus to get projected state with links const role = await rolex.activate("sean"); - // Use want + focus to get a result with state - role.want("Feature: Test", "test-goal"); - role.focus("test-goal"); - // The role itself should have belong link — check via use - const seanResult = await role.use("!role.focus", { goal: "test-goal" }); - const output = render({ - process: "activate", - name: "Sean", - result: seanResult, - }); + const output = 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]"); }); }); @@ -112,23 +104,23 @@ describe("full execution flow", () => { // Want const goal = role.want("Feature: Build Auth", "build-auth"); expect(role.ctx.focusedGoalId).toBe("build-auth"); - expect(goal.hint).toBeDefined(); + expect(goal).toContain("I →"); // Plan const plan = role.plan("Feature: Auth Plan", "auth-plan"); expect(role.ctx.focusedPlanId).toBe("auth-plan"); - expect(plan.hint).toBeDefined(); + expect(plan).toContain("I →"); // Todo const task = role.todo("Feature: Implement JWT", "impl-jwt"); - expect(task.hint).toBeDefined(); + expect(task).toContain("I →"); // Finish with encounter const finished = role.finish( "impl-jwt", "Feature: Implemented JWT\n Scenario: Token pattern\n Given JWT needed\n Then tokens work" ); - expect(finished.state.name).toBe("encounter"); + expect(finished).toContain("[encounter]"); expect(role.ctx.encounterIds.has("impl-jwt-finished")).toBe(true); // Reflect: encounter → experience @@ -137,7 +129,7 @@ describe("full execution flow", () => { "Feature: Token rotation insight\n Scenario: Refresh matters\n Given tokens expire\n Then refresh tokens are key", "token-insight" ); - expect(reflected.state.name).toBe("experience"); + expect(reflected).toContain("[experience]"); expect(role.ctx.encounterIds.has("impl-jwt-finished")).toBe(false); expect(role.ctx.experienceIds.has("token-insight")).toBe(true); @@ -147,7 +139,7 @@ describe("full execution flow", () => { "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" ); - expect(realized.state.name).toBe("principle"); + expect(realized).toContain("[principle]"); expect(role.ctx.experienceIds.has("token-insight")).toBe(false); }); }); diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index 510861c..cf5ee0d 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -17,10 +17,9 @@ 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"; -export type { RenderStateOptions } from "./render.js"; +export type { RenderOptions, RenderStateOptions } from "./render.js"; // Render -export { describe, detail, directive, hint, renderState, world } from "./render.js"; -export type { RolexResult } from "./role.js"; +export { describe, detail, directive, hint, render, renderState, world } from "./render.js"; // Role export { Role } from "./role.js"; // API diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index 9c6e52b..7f4fcab 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"; @@ -237,3 +237,41 @@ function sortByConceptOrder(children: readonly State[]): readonly State[] { 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 index 376a700..98593b3 100644 --- a/packages/rolexjs/src/role.ts +++ b/packages/rolexjs/src/role.ts @@ -2,30 +2,19 @@ * Role — stateful handle returned by Rolex.activate(). * * Holds roleId + RoleContext internally. - * All operations are from the role's perspective — no need to pass - * individual or ctx. + * 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"); - * role.want("Feature: Ship v1", "ship-v1"); - * role.plan("Feature: Phase 1", "phase-1"); + * role.want("Feature: Ship v1", "ship-v1"); // → rendered string + * role.plan("Feature: Phase 1", "phase-1"); // → rendered string * role.finish("write-tests", "Feature: Tests written"); */ -import type { Ops } from "@rolexjs/prototype"; -import type { State } from "@rolexjs/system"; +import type { OpResult, Ops } from "@rolexjs/prototype"; import type { RoleContext } from "./context.js"; - -export interface RolexResult { - /** Projection of the primary affected node. */ - state: State; - /** Which process was executed (for render). */ - process: string; - /** Cognitive hint — populated when RoleContext is used. */ - hint?: string; - /** Role context — returned by activate. */ - ctx?: RoleContext; -} +import { render } from "./render.js"; /** * Internal API surface that Role delegates to. @@ -48,15 +37,29 @@ export class Role { this.api = api; } - /** Project the individual's full state tree. */ - project(): RolexResult { + /** Project the individual's full state tree (used after activate). */ + project(): string { const result = this.api.ops["role.focus"](this.roleId); - return this.withHint({ ...result, process: "activate" }, "activate"); - } - - private withHint(result: RolexResult, process: string): RolexResult { - result.hint = this.ctx.cognitiveHint(process) ?? undefined; - return result; + const focusedGoalId = this.ctx.focusedGoalId; + return this.fmt("activate", this.roleId, result, { + fold: (node) => node.name === "goal" && node.id !== focusedGoalId, + }); + } + + /** 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 save(): void { @@ -66,71 +69,71 @@ export class Role { // ---- Execution ---- /** Focus: view or switch focused goal. */ - focus(goal?: string): RolexResult { + focus(goal?: string): string { const goalId = goal ?? this.ctx.requireGoalId(); this.ctx.focusedGoalId = goalId; this.ctx.focusedPlanId = null; const result = this.api.ops["role.focus"](goalId); this.save(); - return this.withHint(result, "focus"); + return this.fmt("focus", goalId, result); } /** Want: declare a goal. */ - want(goal?: string, id?: string, alias?: readonly string[]): RolexResult { + want(goal?: string, id?: string, alias?: readonly string[]): string { const result = this.api.ops["role.want"](this.roleId, goal, id, alias); if (id) this.ctx.focusedGoalId = id; this.ctx.focusedPlanId = null; this.save(); - return this.withHint(result, "want"); + return this.fmt("want", id ?? this.roleId, result); } /** Plan: create a plan for the focused goal. */ - plan(plan?: string, id?: string, after?: string, fallback?: string): RolexResult { + plan(plan?: string, id?: string, after?: string, fallback?: string): string { const result = this.api.ops["role.plan"](this.ctx.requireGoalId(), plan, id, after, fallback); if (id) this.ctx.focusedPlanId = id; this.save(); - return this.withHint(result, "plan"); + return this.fmt("plan", id ?? "plan", result); } /** Todo: add a task to the focused plan. */ - todo(task?: string, id?: string, alias?: readonly string[]): RolexResult { + todo(task?: string, id?: string, alias?: readonly string[]): string { const result = this.api.ops["role.todo"](this.ctx.requirePlanId(), task, id, alias); - return this.withHint(result, "todo"); + return this.fmt("todo", id ?? "task", result); } /** Finish: complete a task, optionally record an encounter. */ - finish(task: string, encounter?: string): RolexResult { + finish(task: string, encounter?: string): string { const result = this.api.ops["role.finish"](task, this.roleId, encounter); if (encounter && result.state.id) { this.ctx.addEncounter(result.state.id); } - return this.withHint(result, "finish"); + return this.fmt("finish", task, result); } /** Complete: close a plan as done, record encounter. */ - complete(plan?: string, encounter?: string): RolexResult { + complete(plan?: string, encounter?: string): string { const planId = plan ?? this.ctx.requirePlanId(); const result = 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; this.save(); - return this.withHint(result, "complete"); + return this.fmt("complete", planId, result); } /** Abandon: drop a plan, record encounter. */ - abandon(plan?: string, encounter?: string): RolexResult { + abandon(plan?: string, encounter?: string): string { const planId = plan ?? this.ctx.requirePlanId(); const result = 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; this.save(); - return this.withHint(result, "abandon"); + return this.fmt("abandon", planId, result); } // ---- Cognition ---- /** Reflect: consume encounter → experience. Empty encounter = direct creation. */ - reflect(encounter?: string, experience?: string, id?: string): RolexResult { + reflect(encounter?: string, experience?: string, id?: string): string { if (encounter) { this.ctx.requireEncounterIds([encounter]); } @@ -139,11 +142,11 @@ export class Role { this.ctx.consumeEncounters([encounter]); } if (id) this.ctx.addExperience(id); - return this.withHint(result, "reflect"); + return this.fmt("reflect", id ?? "experience", result); } /** Realize: consume experience → principle. Empty experience = direct creation. */ - realize(experience?: string, principle?: string, id?: string): RolexResult { + realize(experience?: string, principle?: string, id?: string): string { if (experience) { this.ctx.requireExperienceIds([experience]); } @@ -151,26 +154,26 @@ export class Role { if (experience) { this.ctx.consumeExperiences([experience]); } - return this.withHint(result, "realize"); + return this.fmt("realize", id ?? "principle", result); } /** Master: create procedure, optionally consuming experience. */ - master(procedure: string, id?: string, experience?: string): RolexResult { + master(procedure: string, id?: string, experience?: string): string { if (experience) this.ctx.requireExperienceIds([experience]); const result = this.api.ops["role.master"](this.roleId, procedure, id, experience); if (experience) this.ctx.consumeExperiences([experience]); - return this.withHint(result, "master"); + return this.fmt("master", id ?? "procedure", result); } // ---- Knowledge management ---- /** Forget: remove any node under the individual by id. */ - forget(nodeId: string): RolexResult { + forget(nodeId: string): string { const result = this.api.ops["role.forget"](nodeId); if (this.ctx.focusedGoalId === nodeId) this.ctx.focusedGoalId = null; if (this.ctx.focusedPlanId === nodeId) this.ctx.focusedPlanId = null; this.save(); - return result; + return this.fmt("forget", nodeId, result); } // ---- Skills + unified entry ---- diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index 4a022b5..b3539cb 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -18,9 +18,6 @@ import type { ResourceX } from "resourcexjs"; import { RoleContext } from "./context.js"; import { Role, type RolexInternal } from "./role.js"; -// Re-export from role.ts (canonical definition) -export type { RolexResult } from "./role.js"; - /** Summary entry returned by census.list. */ export interface CensusEntry { id?: string; @@ -111,17 +108,15 @@ export class Rolex { const ctx = new RoleContext(individual); ctx.rehydrate(state); - // Restore persisted focus + // Restore persisted focus (only override rehydrate default when persisted value is valid) const persisted = this.persistContext?.load(individual) ?? null; if (persisted) { - ctx.focusedGoalId = - persisted.focusedGoalId && this.find(persisted.focusedGoalId) - ? persisted.focusedGoalId - : null; - ctx.focusedPlanId = - persisted.focusedPlanId && this.find(persisted.focusedPlanId) - ? persisted.focusedPlanId - : null; + if (persisted.focusedGoalId && this.find(persisted.focusedGoalId)) { + ctx.focusedGoalId = persisted.focusedGoalId; + } + if (persisted.focusedPlanId && this.find(persisted.focusedPlanId)) { + ctx.focusedPlanId = persisted.focusedPlanId; + } } // Build internal API for Role — ops + ctx persistence diff --git a/packages/rolexjs/tests/context.test.ts b/packages/rolexjs/tests/context.test.ts index 54d799f..4f57cb7 100644 --- a/packages/rolexjs/tests/context.test.ts +++ b/packages/rolexjs/tests/context.test.ts @@ -33,7 +33,7 @@ describe("Role (ctx management)", () => { const result = role.want("Feature: Build auth", "build-auth"); expect(role.ctx.focusedGoalId).toBe("build-auth"); expect(role.ctx.focusedPlanId).toBeNull(); - expect(result.hint).toBeDefined(); + expect(result).toContain("I →"); }); test("plan updates ctx.focusedPlanId", async () => { @@ -44,7 +44,7 @@ describe("Role (ctx management)", () => { role.want("Feature: Auth", "auth-goal"); const result = role.plan("Feature: JWT strategy", "jwt-plan"); expect(role.ctx.focusedPlanId).toBe("jwt-plan"); - expect(result.hint).toBeDefined(); + expect(result).toContain("I →"); }); test("finish with encounter registers in ctx", async () => { @@ -61,7 +61,7 @@ describe("Role (ctx management)", () => { "Feature: Login done\n Scenario: OK\n Given login\n Then success" ); expect(role.ctx.encounterIds.has("login-finished")).toBe(true); - expect(result.hint).toBeDefined(); + expect(result).toContain("I →"); }); test("finish without encounter does not register in ctx", async () => { @@ -91,7 +91,7 @@ describe("Role (ctx management)", () => { ); expect(role.ctx.focusedPlanId).toBeNull(); expect(role.ctx.encounterIds.has("jwt-completed")).toBe(true); - expect(result.hint).toContain("auth"); + expect(result).toContain("auth"); }); test("reflect consumes encounter and adds experience in ctx", async () => { @@ -127,7 +127,7 @@ describe("Role (ctx management)", () => { "conv-insight" ); - expect(result.state.name).toBe("experience"); + expect(result).toContain("[experience]"); expect(role.ctx.experienceIds.has("conv-insight")).toBe(true); expect(role.ctx.encounterIds.size).toBe(0); }); @@ -143,7 +143,7 @@ describe("Role (ctx management)", () => { "product-first" ); - expect(result.state.name).toBe("principle"); + expect(result).toContain("[principle]"); expect(role.ctx.experienceIds.size).toBe(0); }); diff --git a/packages/rolexjs/tests/rolex.test.ts b/packages/rolexjs/tests/rolex.test.ts index a3e5af7..e6b73ff 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -3,7 +3,8 @@ import { existsSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { localPlatform } from "@rolexjs/local-platform"; -import { createRoleX, type RolexResult } from "../src/index.js"; +import type { OpResult } from "@rolexjs/prototype"; +import { createRoleX } from "../src/index.js"; import { describe as renderDescribe, hint as renderHint, renderState } from "../src/render.js"; function setup() { @@ -17,7 +18,7 @@ function setup() { describe("use dispatch", () => { test("!individual.born creates individual", async () => { const rolex = setup(); - const r = await rolex.direct("!individual.born", { + const r = await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean", }); @@ -32,7 +33,7 @@ describe("use dispatch", () => { const rolex = 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", { + const r = await rolex.direct("!role.plan", { goal: "g1", plan: "Feature: JWT", id: "p1", @@ -84,27 +85,27 @@ describe("activate", () => { const role = await rolex.activate("sean"); const wantR = role.want("Feature: Auth", "auth"); - expect(wantR.state.name).toBe("goal"); - expect(wantR.hint).toBeDefined(); + expect(wantR).toContain('Goal "auth" declared.'); + expect(wantR).toContain("[goal]"); const planR = role.plan("Feature: JWT", "jwt"); - expect(planR.state.name).toBe("plan"); + expect(planR).toContain("[plan]"); const todoR = role.todo("Feature: Login", "login"); - expect(todoR.state.name).toBe("task"); + expect(todoR).toContain("[task]"); const finishR = role.finish( "login", "Feature: Done\n Scenario: OK\n Given done\n Then ok" ); - expect(finishR.state.name).toBe("encounter"); + expect(finishR).toContain("[encounter]"); }); test("Role.use delegates to Rolex.use", async () => { const rolex = 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" }); + const r = await role.use("!org.found", { content: "Feature: DP", id: "dp" }); expect(r.state.name).toBe("organization"); }); }); @@ -116,7 +117,7 @@ describe("activate", () => { describe("render", () => { test("describe generates text with name", async () => { const rolex = setup(); - const r = await rolex.direct("!individual.born", { id: "sean" }); + const r = await rolex.direct("!individual.born", { id: "sean" }); const text = renderDescribe("born", "sean", r.state); expect(text).toContain("sean"); }); @@ -128,7 +129,7 @@ describe("render", () => { test("renderState renders individual with heading", async () => { const rolex = setup(); - const r = await rolex.direct("!individual.born", { + const r = await rolex.direct("!individual.born", { content: "Feature: I am Sean\n An AI role.", id: "sean", }); @@ -144,7 +145,7 @@ describe("render", () => { 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 r = await rolex.direct("!role.focus", { goal: "g1" }); const md = renderState(r.state); expect(md).toContain("# [goal]"); expect(md).toContain("## [plan]"); @@ -212,7 +213,7 @@ describe("persistent mode", () => { test("born → retire round-trip", async () => { const rolex = persistentSetup(); await rolex.direct("!individual.born", { content: "Feature: Test", id: "test-ind" }); - const r = await rolex.direct("!individual.retire", { individual: "test-ind" }); + const r = await rolex.direct("!individual.retire", { individual: "test-ind" }); expect(r.state.name).toBe("past"); expect(r.process).toBe("retire"); }); From 999067d0dbabd42701a7bee01efc7b6e41616b74 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 2 Mar 2026 12:03:38 +0800 Subject: [PATCH 46/54] developing --- bdd/features/cognition-loop.feature | 62 ++++ bdd/features/context-persistence.feature | 26 ++ bdd/features/execution-loop.feature | 82 +++++ bdd/features/governance-system.feature | 109 ------ bdd/features/individual-lifecycle.feature | 69 ++++ bdd/features/individual-system.feature | 278 ---------------- bdd/features/org-system.feature | 39 --- bdd/features/organization-lifecycle.feature | 58 ++++ bdd/features/position-lifecycle.feature | 71 ++++ bdd/features/role-system.feature | 83 ----- bdd/journeys/01-execution-loop.feature | 151 --------- bdd/journeys/02-growth-loop.feature | 146 -------- bdd/journeys/03-organization-loop.feature | 96 ------ bdd/journeys/onboarding.feature | 31 ++ bdd/package.json | 13 + bdd/run.test.ts | 20 +- bdd/steps/assertions.steps.ts | 313 ------------------ bdd/steps/context.steps.ts | 85 +++++ bdd/steps/direct.steps.ts | 100 ++++++ bdd/steps/individual.steps.ts | 151 --------- bdd/steps/mcp.steps.ts | 14 +- bdd/steps/org.steps.ts | 73 ---- bdd/steps/role.steps.ts | 191 ++++++++++- bdd/steps/setup.steps.ts | 258 --------------- bdd/support/mcp-world.ts | 63 ---- bdd/support/world.ts | 186 +++++++---- bun.lock | 13 + package.json | 3 +- packages/prototype/src/descriptions/index.ts | 124 +++---- .../descriptions/world/role-identity.feature | 12 +- packages/prototype/src/directives/index.ts | 6 +- 31 files changed, 983 insertions(+), 1943 deletions(-) create mode 100644 bdd/features/cognition-loop.feature create mode 100644 bdd/features/context-persistence.feature create mode 100644 bdd/features/execution-loop.feature delete mode 100644 bdd/features/governance-system.feature create mode 100644 bdd/features/individual-lifecycle.feature delete mode 100644 bdd/features/individual-system.feature delete mode 100644 bdd/features/org-system.feature create mode 100644 bdd/features/organization-lifecycle.feature create mode 100644 bdd/features/position-lifecycle.feature delete mode 100644 bdd/features/role-system.feature delete mode 100644 bdd/journeys/01-execution-loop.feature delete mode 100644 bdd/journeys/02-growth-loop.feature delete mode 100644 bdd/journeys/03-organization-loop.feature create mode 100644 bdd/journeys/onboarding.feature create mode 100644 bdd/package.json delete mode 100644 bdd/steps/assertions.steps.ts create mode 100644 bdd/steps/context.steps.ts create mode 100644 bdd/steps/direct.steps.ts delete mode 100644 bdd/steps/individual.steps.ts delete mode 100644 bdd/steps/org.steps.ts delete mode 100644 bdd/steps/setup.steps.ts delete mode 100644 bdd/support/mcp-world.ts 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/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 index 2ca133a..688c043 100644 --- a/bdd/run.test.ts +++ b/bdd/run.test.ts @@ -7,14 +7,26 @@ import { loadFeature, setDefaultTimeout } from "@deepracticex/bdd"; -// Support -import "./support/mcp-world"; +// 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 server startup can take a few seconds -setDefaultTimeout(15_000); +// 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..5f31a38 --- /dev/null +++ b/bdd/steps/context.steps.ts @@ -0,0 +1,85 @@ +/** + * Steps for context persistence tests — operate at the Rolex API level. + */ + +import { strict as assert } from "node:assert"; +import { Given, When, Then } 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..25b1afb --- /dev/null +++ b/bdd/steps/direct.steps.ts @@ -0,0 +1,100 @@ +/** + * Direct steps — call rolex.direct() for system-level operations. + */ + +import { strict as assert } from "node:assert"; +import { type DataTable, Given, When, Then } 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 index a2e8dd2..1326043 100644 --- a/bdd/steps/mcp.steps.ts +++ b/bdd/steps/mcp.steps.ts @@ -4,17 +4,21 @@ import { strict as assert } from "node:assert"; import { type DataTable, Given, Then, When } from "@deepracticex/bdd"; -import type { McpWorld } from "../support/mcp-world"; +import type { BddWorld } from "../support/world"; // ===== Setup ===== -Given("the MCP server is running", async function (this: McpWorld) { +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: McpWorld, table: DataTable) { +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]); @@ -28,7 +32,7 @@ Then("the following tools should be available:", async function (this: McpWorld, When( "I call tool {string} with:", - async function (this: McpWorld, toolName: string, table: DataTable) { + async function (this: BddWorld, toolName: string, table: DataTable) { try { this.error = undefined; const args = table.rowsHash(); @@ -47,7 +51,7 @@ When( // ===== Result assertions ===== -Then("the tool result should contain {string}", function (this: McpWorld, text: string) { +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), 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..9cde856 100644 --- a/bdd/steps/role.steps.ts +++ b/bdd/steps/role.steps.ts @@ -1,35 +1,194 @@ /** - * 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, When, Then } 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 complete plan {string} with encounter {string}", + function (this: BddWorld, planId: string, encounter: string) { + this.toolResult = this.role!.complete(planId, encounter); + } +); + +When( + "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 born a role {string} with:", - async function (this: RoleXWorld, name: string, source: string) { - await this.run(this.roleSystem, "born", { name, source }); + "I reflect directly as {string} with {string}", + function (this: BddWorld, expId: string, content: string) { + this.toolResult = this.role!.reflect(undefined, content, expId); + } +); + +// ===== 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 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 realize directly as {string} with {string}", + function (this: BddWorld, principleId: string, content: string) { + this.toolResult = this.role!.realize(undefined, 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 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 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(", ")}` + ); } ); -When("I retire role {string}", async function (this: RoleXWorld, name: string) { - await this.run(this.roleSystem, "retire", { name }); +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/mcp-world.ts b/bdd/support/mcp-world.ts deleted file mode 100644 index 00a5bc5..0000000 --- a/bdd/support/mcp-world.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * MCP World — test context for MCP-level BDD tests. - * - * Manages a real MCP server child process connected via stdio transport. - * Each scenario gets a fresh client connection. - */ - -import { join } from "node:path"; -import type { IWorldOptions } from "@deepracticex/bdd"; -import { AfterAll, BeforeAll, setWorldConstructor, World } from "@deepracticex/bdd"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; - -const SERVER_PATH = join(import.meta.dirname, "../../apps/mcp-server/src/index.ts"); - -// Shared client across scenarios (MCP startup is expensive) -let sharedClient: Client | null = null; -let sharedTransport: StdioClientTransport | null = null; - -async function ensureClient(): Promise { - if (sharedClient) return sharedClient; - - sharedTransport = new StdioClientTransport({ - command: "bun", - args: ["run", SERVER_PATH], - }); - - sharedClient = new Client({ - name: "rolex-bdd-test", - version: "1.0.0", - }); - - await sharedClient.connect(sharedTransport); - return sharedClient; -} - -AfterAll(async () => { - if (sharedClient) { - await sharedClient.close(); - sharedClient = null; - } - if (sharedTransport) { - await sharedTransport.close(); - sharedTransport = null; - } -}); - -export class McpWorld extends World { - client!: Client; - toolResult?: string; - error?: Error; - tools?: Array<{ name: string }>; - - constructor(options: IWorldOptions) { - super(options); - } - - async connect(): Promise { - this.client = await ensureClient(); - } -} - -setWorldConstructor(McpWorld); diff --git a/bdd/support/world.ts b/bdd/support/world.ts index ac14ab1..6cd5a28 100644 --- a/bdd/support/world.ts +++ b/bdd/support/world.ts @@ -1,87 +1,131 @@ /** - * 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 type { IWorldOptions } from "@deepracticex/bdd"; +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 { Rolex, Role } 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; + constructor(options: IWorldOptions) { + super(options); + } + + /** Connect to MCP server (dev mode — local source). */ + async connect(): Promise { + this.client = await ensureMcpClient("dev"); } - /** 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; - } + /** 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 })); + } + + /** 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/bun.lock b/bun.lock index 6489536..163f95a 100644 --- a/bun.lock +++ b/bun.lock @@ -43,6 +43,17 @@ "@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", @@ -362,6 +373,8 @@ "@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/bdd": ["@rolexjs/bdd@workspace:bdd"], + "@rolexjs/core": ["@rolexjs/core@workspace:packages/core"], "@rolexjs/genesis": ["@rolexjs/genesis@workspace:packages/genesis"], diff --git a/package.json b/package.json index b426da7..1a4c5a7 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "type": "module", "workspaces": [ "packages/*", - "apps/*" + "apps/*", + "bdd" ], "scripts": { "build": "turbo build", diff --git a/packages/prototype/src/descriptions/index.ts b/packages/prototype/src/descriptions/index.ts index c0c3d90..73a5504 100644 --- a/packages/prototype/src/descriptions/index.ts +++ b/packages/prototype/src/descriptions/index.ts @@ -1,87 +1,51 @@ // 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: Discovering available commands\n Given available commands are documented in world descriptions and skills\n When you need to perform an operation\n Then look up the correct command from world descriptions or loaded skills first\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"', + "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: Discovering available commands\n Given available commands are documented in world descriptions and skills\n When you need to perform an operation\n Then look up the correct command from world descriptions or loaded skills first\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: 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\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 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: Discovering available commands\n Given available commands come from your role\'s own skills and procedures\n When you need to perform an operation\n Then look up the correct command from your loaded skills first\n And use only commands you have seen in your own skills or procedures\n And do not use commands mentioned in other roles\' descriptions\n\n Scenario: NEVER guess commands\n Given a command is not found in any loaded skill or world description\n When the AI considers trying it anyway\n Then STOP — do not call use or direct with unverified commands\n And guessing wastes tokens, triggers errors, and erodes trust\n And instead ask the user or load the relevant skill first\n And there is no fallback — unknown commands simply do not exist\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', + "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: Discovering available commands\n Given available commands come from your role's own skills and procedures\n When you need to perform an operation\n Then look up the correct command from your loaded skills first\n And use only commands you have seen in your own skills or procedures\n And do not use commands mentioned in other roles' descriptions\n\n Scenario: NEVER guess commands\n Given a command is not found in any loaded skill or world description\n When the AI considers trying it anyway\n Then STOP — do not call use or direct with unverified commands\n And guessing wastes tokens, triggers errors, and erodes trust\n And instead ask the user or load the relevant skill first\n And there is no fallback — unknown commands simply do not exist\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/prototype/src/descriptions/world/role-identity.feature b/packages/prototype/src/descriptions/world/role-identity.feature index aed43e3..5a2f9fc 100644 --- a/packages/prototype/src/descriptions/world/role-identity.feature +++ b/packages/prototype/src/descriptions/world/role-identity.feature @@ -3,6 +3,14 @@ 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 @@ -17,8 +25,8 @@ Feature: Role identity — activate before acting 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] + 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 diff --git a/packages/prototype/src/directives/index.ts b/packages/prototype/src/directives/index.ts index 16e0472..15c908f 100644 --- a/packages/prototype/src/directives/index.ts +++ b/packages/prototype/src/directives/index.ts @@ -2,9 +2,7 @@ export const directives: Record> = { "identity-ethics": { - "on-unknown-command": - "STOP. Do not try another command. Do not search for the correct command.\nThis is not a technical error. This is a boundary you must not cross.\nYou do not have this command in your skills because it is not your responsibility.\nTell the user: this is outside your duties. Suggest activating 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.", + "on-unknown-command": "STOP. Do not try another command. Do not search for the correct command.\nThis is not a technical error. This is a boundary you must not cross.\nYou do not have this command in your skills because it is not your responsibility.\nTell the user: this is outside your duties. Suggest activating 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; From 9180e24a7801818f5ddfde28098b9134fb9903bf Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 2 Mar 2026 16:06:08 +0800 Subject: [PATCH 47/54] feat: relax ID uniqueness, auto-train on appoint, skill-loading guidance, and rendering improvements - Relax global ID uniqueness to same-parent idempotence in both runtimes - Add priority-based findInState for disambiguating duplicate IDs - Auto-train position requirements as individual procedures on appoint - Fold requirement nodes on activate to avoid duplicate content - Enhance error messages with skill-loading hints in dispatch and direct - Update identity-ethics on-unknown-command directive to distinguish skill-not-loaded vs outside-duties - Unify use-protocol and use tool descriptions, add mandatory skill-loading rule - Fix plan link rendering: compact references for after/before/fallback, expanded subtrees for organizational links - Preserve focused plan when re-focusing same goal - Add goal progress summary in headings (e.g. [2/2 plans, 4/4 tasks]) Co-Authored-By: Claude Opus 4.6 --- .changeset/skill-loading-and-rendering.md | 8 ++ packages/local-platform/src/sqliteRuntime.ts | 14 +- packages/prototype/src/descriptions/index.ts | 124 +++++++++++------- .../src/descriptions/role/use.feature | 5 - .../descriptions/world/use-protocol.feature | 20 +-- .../src/directives/identity-ethics.feature | 8 +- packages/prototype/src/directives/index.ts | 6 +- packages/prototype/src/dispatch.ts | 6 +- packages/prototype/src/ops.ts | 10 ++ packages/prototype/tests/ops.test.ts | 46 ++++++- packages/rolexjs/src/context.ts | 2 +- packages/rolexjs/src/find.ts | 80 +++++++++++ packages/rolexjs/src/index.ts | 2 + packages/rolexjs/src/render.ts | 51 ++++++- packages/rolexjs/src/role.ts | 6 +- packages/rolexjs/src/rolex.ts | 33 ++--- packages/system/src/runtime.ts | 8 +- 17 files changed, 311 insertions(+), 118 deletions(-) create mode 100644 .changeset/skill-loading-and-rendering.md create mode 100644 packages/rolexjs/src/find.ts 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/packages/local-platform/src/sqliteRuntime.ts b/packages/local-platform/src/sqliteRuntime.ts index 30bfda3..046a70f 100644 --- a/packages/local-platform/src/sqliteRuntime.ts +++ b/packages/local-platform/src/sqliteRuntime.ts @@ -96,14 +96,14 @@ function removeSubtree(db: DB, ref: string): void { export function createSqliteRuntime(db: DB): Runtime { return { create(parent, type, information, id, alias) { - // Global uniqueness: no duplicate ids anywhere in the tree. + // Idempotent: same id under same parent → return existing. if (id) { - const existing = db.select().from(nodes).where(eq(nodes.id, id)).get(); - if (existing) { - // Idempotent: same id under same parent → return existing. - if (existing.parentRef === (parent?.ref ?? null)) return toStructure(existing); - throw new Error(`Duplicate id "${id}": already exists elsewhere in the tree.`); - } + 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) diff --git a/packages/prototype/src/descriptions/index.ts b/packages/prototype/src/descriptions/index.ts index 73a5504..77c1815 100644 --- a/packages/prototype/src/descriptions/index.ts +++ b/packages/prototype/src/descriptions/index.ts @@ -1,51 +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: Discovering available commands\n Given available commands are documented in world descriptions and skills\n When you need to perform an operation\n Then look up the correct command from world descriptions or loaded skills first\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\"", + 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: Discovering available commands\n Given available commands come from your role's own skills and procedures\n When you need to perform an operation\n Then look up the correct command from your loaded skills first\n And use only commands you have seen in your own skills or procedures\n And do not use commands mentioned in other roles' descriptions\n\n Scenario: NEVER guess commands\n Given a command is not found in any loaded skill or world description\n When the AI considers trying it anyway\n Then STOP — do not call use or direct with unverified commands\n And guessing wastes tokens, triggers errors, and erodes trust\n And instead ask the user or load the relevant skill first\n And there is no fallback — unknown commands simply do not exist\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", + "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/prototype/src/descriptions/role/use.feature b/packages/prototype/src/descriptions/role/use.feature index 429df96..6233de7 100644 --- a/packages/prototype/src/descriptions/role/use.feature +++ b/packages/prototype/src/descriptions/role/use.feature @@ -15,11 +15,6 @@ Feature: use — act as the current role Then the command is parsed as `namespace.method` And dispatched to the corresponding RoleX API - Scenario: Discovering available commands - Given available commands are documented in world descriptions and skills - When you need to perform an operation - Then look up the correct command from world descriptions or loaded skills first - Scenario: Load a ResourceX resource Given the locator does not start with `!` When use is called with the locator diff --git a/packages/prototype/src/descriptions/world/use-protocol.feature b/packages/prototype/src/descriptions/world/use-protocol.feature index 603072d..b0e77d8 100644 --- a/packages/prototype/src/descriptions/world/use-protocol.feature +++ b/packages/prototype/src/descriptions/world/use-protocol.feature @@ -14,20 +14,20 @@ Feature: Use tool — the universal execution entry point Then it is parsed as !namespace.method And dispatched to the corresponding RoleX API with named args - Scenario: Discovering available commands - Given available commands come from your role's own skills and procedures - When you need to perform an operation - Then look up the correct command from your loaded skills first - And use only commands you have seen in your own skills or procedures - And do not use commands mentioned in other roles' descriptions + 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 or world description + 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 guessing wastes tokens, triggers errors, and erodes trust - And instead ask the user or load the relevant skill first - And there is no fallback — unknown commands simply do not exist + 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 ! diff --git a/packages/prototype/src/directives/identity-ethics.feature b/packages/prototype/src/directives/identity-ethics.feature index 313348c..44378ce 100644 --- a/packages/prototype/src/directives/identity-ethics.feature +++ b/packages/prototype/src/directives/identity-ethics.feature @@ -3,10 +3,10 @@ Feature: Identity ethics directives I am the system designer. My sentences are commands, not suggestions. Scenario: on-unknown-command - Then STOP. Do not try another command. Do not search for the correct command. - And This is not a technical error. This is a boundary you must not cross. - And You do not have this command in your skills because it is not your responsibility. - And Tell the user: this is outside your duties. Suggest activating Nuwa. + 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. diff --git a/packages/prototype/src/directives/index.ts b/packages/prototype/src/directives/index.ts index 15c908f..5509a9f 100644 --- a/packages/prototype/src/directives/index.ts +++ b/packages/prototype/src/directives/index.ts @@ -2,7 +2,9 @@ export const directives: Record> = { "identity-ethics": { - "on-unknown-command": "STOP. Do not try another command. Do not search for the correct command.\nThis is not a technical error. This is a boundary you must not cross.\nYou do not have this command in your skills because it is not your responsibility.\nTell the user: this is outside your duties. Suggest activating 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.", + "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 index 6378aa2..6b22427 100644 --- a/packages/prototype/src/dispatch.ts +++ b/packages/prototype/src/dispatch.ts @@ -22,7 +22,11 @@ export function toArgs(op: string, args: Record): unknown[] { // 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}.`); + 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." + ); } } diff --git a/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts index b791cf5..34f4ffa 100644 --- a/packages/prototype/src/ops.ts +++ b/packages/prototype/src/ops.ts @@ -68,6 +68,7 @@ export function createOps(ctx: OpsContext): Ops { } } + /** 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) { @@ -352,6 +353,15 @@ export function createOps(ctx: OpsContext): Ops { const posNode = resolve(position); const indNode = resolve(individual); rt.link(posNode, indNode, "appointment", "serve"); + + // Auto-train: copy position requirements as individual procedures + const posState = 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 + rt.create(indNode, C.procedure, child.information, child.id); + } + return ok(posNode, "appoint"); }, diff --git a/packages/prototype/tests/ops.test.ts b/packages/prototype/tests/ops.test.ts index ae7570e..8428d4d 100644 --- a/packages/prototype/tests/ops.test.ts +++ b/packages/prototype/tests/ops.test.ts @@ -82,13 +82,18 @@ describe("individual", () => { expect(identity!.id).toBe("sean-identity"); }); - test("duplicate id across tree throws", () => { - const { ops } = setup(); + test("same id under different parents is allowed", () => { + const { ops, find } = setup(); ops["individual.born"](undefined, "sean"); - ops["org.found"](undefined, "acme"); - expect(() => ops["org.charter"]("acme", "Feature: Charter", "sean")).toThrow( - 'Duplicate id "sean"' - ); + ops["position.establish"](undefined, "architect"); + ops["position.require"]("architect", "Feature: System design", "sys-design"); + ops["individual.train"]("sean", "Feature: System design skill", "sys-design"); + + // Both exist — requirement under position, procedure under individual + const sean = 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", () => { @@ -509,7 +514,7 @@ describe("position", () => { expect(r.state.links![0].relation).toBe("appointment"); }); - test("appoint does not copy requirements as procedures", () => { + test("appoint auto-trains requirements as procedures", () => { const { ops, find } = setup(); ops["individual.born"](undefined, "sean"); ops["position.establish"](undefined, "architect"); @@ -517,6 +522,33 @@ describe("position", () => { ops["position.require"]("architect", "Feature: Code review", "code-review"); ops["position.appoint"]("architect", "sean"); + const sean = 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)", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["individual.train"]("sean", "Feature: System design skill", "sys-design"); + ops["position.establish"](undefined, "architect"); + ops["position.require"]("architect", "Feature: System design", "sys-design"); + ops["position.appoint"]("architect", "sean"); + + const sean = 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", () => { + const { ops, find } = setup(); + ops["individual.born"](undefined, "sean"); + ops["position.establish"](undefined, "architect"); + ops["position.appoint"]("architect", "sean"); + const sean = find("sean")! as unknown as State; const procs = (sean.children ?? []).filter((c: State) => c.name === "procedure"); expect(procs).toHaveLength(0); diff --git a/packages/rolexjs/src/context.ts b/packages/rolexjs/src/context.ts index cf9ad56..0b4dd94 100644 --- a/packages/rolexjs/src/context.ts +++ b/packages/rolexjs/src/context.ts @@ -115,7 +115,7 @@ export class RoleContext { 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 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": 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 cf5ee0d..9f4838a 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -17,6 +17,8 @@ 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, directive, hint, render, renderState, world } from "./render.js"; diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index 7f4fcab..de7c6f0 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -156,11 +156,12 @@ export function renderState(state: State, depth = 1, options?: RenderStateOption const level = Math.min(depth, 6); const heading = "#".repeat(level); - // Heading: [name] (id) {origin} #tag + // Heading: [name] (id) {origin} #tag [progress] const idPart = state.id ? ` (${state.id})` : ""; const originPart = state.origin ? ` {${state.origin}}` : ""; const tagPart = state.tag ? ` #${state.tag}` : ""; - lines.push(`${heading} [${state.name}]${idPart}${originPart}${tagPart}`); + const progressPart = state.name === "goal" ? goalProgress(state) : ""; + lines.push(`${heading} [${state.name}]${idPart}${originPart}${tagPart}${progressPart}`); // Folded: heading only if (options?.fold?.(state)) { @@ -173,12 +174,22 @@ export function renderState(state: State, depth = 1, options?: RenderStateOption lines.push(state.information); } - // Links — expanded as subtrees + // Links — plan references are compact, organizational links are expanded if (state.links && state.links.length > 0) { - const targets = sortByConceptOrder(state.links.map((l) => l.target)); - for (const target of targets) { - lines.push(""); - lines.push(renderState(target, depth + 1, options)); + 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)); + } } } @@ -222,6 +233,32 @@ const CONCEPT_ORDER: readonly string[] = [ "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); diff --git a/packages/rolexjs/src/role.ts b/packages/rolexjs/src/role.ts index 98593b3..10169ae 100644 --- a/packages/rolexjs/src/role.ts +++ b/packages/rolexjs/src/role.ts @@ -42,7 +42,8 @@ export class Role { const result = 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, + fold: (node) => + (node.name === "goal" && node.id !== focusedGoalId) || node.name === "requirement", }); } @@ -71,8 +72,9 @@ export class Role { /** Focus: view or switch focused goal. */ focus(goal?: string): string { const goalId = goal ?? this.ctx.requireGoalId(); + const switched = goalId !== this.ctx.focusedGoalId; this.ctx.focusedGoalId = goalId; - this.ctx.focusedPlanId = null; + if (switched) this.ctx.focusedPlanId = null; const result = this.api.ops["role.focus"](goalId); this.save(); return this.fmt("focus", goalId, result); diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index b3539cb..e105493 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -13,9 +13,10 @@ import type { ContextData, Platform } from "@rolexjs/core"; import * as C from "@rolexjs/core"; import { createOps, directives, type Ops, toArgs } from "@rolexjs/prototype"; -import type { Initializer, Runtime, State, Structure } from "@rolexjs/system"; +import type { Initializer, Runtime, Structure } from "@rolexjs/system"; import type { ResourceX } 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. */ @@ -139,9 +140,8 @@ export class Rolex { /** Find a node by id or alias across the entire society tree. Internal use only. */ private find(id: string): Structure | null { - const target = id.toLowerCase(); const state = this.rt.project(this.society); - return findInState(state, target); + return findInState(state, id); } /** @@ -155,8 +155,13 @@ export class Rolex { const command = locator.slice(1); const fn = this.ops[command]; if (!fn) { - const cmd = directives["identity-ethics"]?.["on-unknown-command"] ?? ""; - throw new Error(`Unknown command "${locator}".\n\n${cmd}`); + 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 fn(...toArgs(command, args ?? {})) as T; } @@ -169,21 +174,3 @@ export class Rolex { export function createRoleX(platform: Platform): Rolex { return new Rolex(platform); } - -// ================================================================ -// Helpers -// ================================================================ - -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; -} diff --git a/packages/system/src/runtime.ts b/packages/system/src/runtime.ts index 63aafce..588d7d1 100644 --- a/packages/system/src/runtime.ts +++ b/packages/system/src/runtime.ts @@ -153,12 +153,10 @@ export const createRuntime = (): Runtime => { return { create(parent, type, information, id, alias) { if (id) { - // Global uniqueness: check all nodes for a matching id. + // Idempotent: same id under same parent → return existing. for (const treeNode of nodes.values()) { - if (treeNode.node.id === id) { - // Idempotent: same id under same parent → return existing. - if (treeNode.parent === (parent?.ref ?? null)) return treeNode.node; - throw new Error(`Duplicate id "${id}": already exists elsewhere in the tree.`); + if (treeNode.node.id === id && treeNode.parent === (parent?.ref ?? null)) { + return treeNode.node; } } } From 894613593b9686e08e527d2d1c9e1b8b6d0fb429 Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 2 Mar 2026 16:06:51 +0800 Subject: [PATCH 48/54] chore: fix lint issues in bdd test files Co-Authored-By: Claude Opus 4.6 --- bdd/steps/context.steps.ts | 13 ++++------ bdd/steps/direct.steps.ts | 49 ++++++++++++++++++++++---------------- bdd/steps/role.steps.ts | 45 ++++++++++++++-------------------- bdd/support/world.ts | 7 +----- 4 files changed, 53 insertions(+), 61 deletions(-) diff --git a/bdd/steps/context.steps.ts b/bdd/steps/context.steps.ts index 5f31a38..d32c213 100644 --- a/bdd/steps/context.steps.ts +++ b/bdd/steps/context.steps.ts @@ -3,7 +3,7 @@ */ import { strict as assert } from "node:assert"; -import { Given, When, Then } from "@deepracticex/bdd"; +import { Given, Then, When } from "@deepracticex/bdd"; import type { BddWorld } from "../support/world"; // ===== Setup ===== @@ -48,13 +48,10 @@ Given("persisted focusedGoalId is null", function (this: BddWorld) { this.newSession(); }); -Given( - "persisted focusedGoalId is {string}", - function (this: BddWorld, goalId: string) { - this.writeContext("sean", { focusedGoalId: goalId, 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 diff --git a/bdd/steps/direct.steps.ts b/bdd/steps/direct.steps.ts index 25b1afb..b09265e 100644 --- a/bdd/steps/direct.steps.ts +++ b/bdd/steps/direct.steps.ts @@ -3,7 +3,7 @@ */ import { strict as assert } from "node:assert"; -import { type DataTable, Given, When, Then } from "@deepracticex/bdd"; +import { type DataTable, Given, Then, When } from "@deepracticex/bdd"; import type { BddWorld } from "../support/world"; // ===== Setup helpers ===== @@ -40,39 +40,48 @@ Given( // ===== 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; - } +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}"`); + 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}"`); + 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}"`); + 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) { diff --git a/bdd/steps/role.steps.ts b/bdd/steps/role.steps.ts index 9cde856..64da42c 100644 --- a/bdd/steps/role.steps.ts +++ b/bdd/steps/role.steps.ts @@ -8,7 +8,7 @@ */ import { strict as assert } from "node:assert"; -import { Given, When, Then } from "@deepracticex/bdd"; +import { Given, Then, When } from "@deepracticex/bdd"; import type { BddWorld } from "../support/world"; // ===== Activate ===== @@ -19,12 +19,9 @@ Given("I activate role {string}", async function (this: BddWorld, id: string) { // ===== Execution ===== -Given( - "I want goal {string} with {string}", - function (this: BddWorld, id: string, content: string) { - this.toolResult = this.role!.want(content, id); - } -); +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); @@ -145,16 +142,13 @@ Then("focusedPlanId should be null", function (this: BddWorld) { 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 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"); @@ -169,16 +163,13 @@ Then("encounter count should be {int}", function (this: BddWorld, count: number) 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 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"); diff --git a/bdd/support/world.ts b/bdd/support/world.ts index 6cd5a28..148c643 100644 --- a/bdd/support/world.ts +++ b/bdd/support/world.ts @@ -12,12 +12,11 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { IWorldOptions } from "@deepracticex/bdd"; 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 { Rolex, Role } from "rolexjs"; +import type { Role, Rolex } from "rolexjs"; import { createRoleX } from "rolexjs"; // ========== MCP client management ========== @@ -86,10 +85,6 @@ export class BddWorld extends World { // --- Shared --- error?: Error; - constructor(options: IWorldOptions) { - super(options); - } - /** Connect to MCP server (dev mode — local source). */ async connect(): Promise { this.client = await ensureMcpClient("dev"); From 42d21d47ecf5b2695935974310739d704ca80484 Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 2 Mar 2026 16:43:21 +0800 Subject: [PATCH 49/54] fix: support batch consumption in reflect/realize/master reflect, realize, and master now accept arrays of encounter/experience IDs instead of a single ID. All IDs in the array are consumed: the first goes through ops (creates output + removes source), remaining are removed via forget. Previously only ids[0] was consumed, leaving orphan nodes. Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 6 +-- bdd/steps/role.steps.ts | 10 ++--- packages/rolexjs/src/role.ts | 60 ++++++++++++++++++-------- packages/rolexjs/tests/context.test.ts | 14 +++--- 4 files changed, 54 insertions(+), 36 deletions(-) diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index a6ff79b..d490a9c 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -159,7 +159,7 @@ server.addTool({ experience: z.string().optional().describe("Gherkin Feature source for the experience"), }), execute: async ({ ids, id, experience }) => { - return state.requireRole().reflect(ids[0] ?? undefined, experience, id); + return state.requireRole().reflect(ids, experience, id); }, }); @@ -172,7 +172,7 @@ server.addTool({ principle: z.string().optional().describe("Gherkin Feature source for the principle"), }), execute: async ({ ids, id, principle }) => { - return state.requireRole().realize(ids[0] ?? undefined, principle, id); + return state.requireRole().realize(ids, principle, id); }, }); @@ -185,7 +185,7 @@ server.addTool({ procedure: z.string().describe("Gherkin Feature source for the procedure"), }), execute: async ({ ids, id, procedure }) => { - return state.requireRole().master(procedure, id, ids?.[0]); + return state.requireRole().master(procedure, id, ids); }, }); diff --git a/bdd/steps/role.steps.ts b/bdd/steps/role.steps.ts index 64da42c..ee2ae33 100644 --- a/bdd/steps/role.steps.ts +++ b/bdd/steps/role.steps.ts @@ -71,14 +71,14 @@ When("I focus on {string}", function (this: BddWorld, goalId: string) { 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); + this.toolResult = this.role!.reflect([encounterId], content, expId); } ); When( "I reflect directly as {string} with {string}", function (this: BddWorld, expId: string, content: string) { - this.toolResult = this.role!.reflect(undefined, content, expId); + this.toolResult = this.role!.reflect([], content, expId); } ); @@ -87,14 +87,14 @@ When( 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); + 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(undefined, content, principleId); + this.toolResult = this.role!.realize([], content, principleId); } ); @@ -103,7 +103,7 @@ When( 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); + this.toolResult = this.role!.master(content, procId, [expId]); } ); diff --git a/packages/rolexjs/src/role.ts b/packages/rolexjs/src/role.ts index 10169ae..c402deb 100644 --- a/packages/rolexjs/src/role.ts +++ b/packages/rolexjs/src/role.ts @@ -134,36 +134,58 @@ export class Role { // ---- Cognition ---- - /** Reflect: consume encounter → experience. Empty encounter = direct creation. */ - reflect(encounter?: string, experience?: string, id?: string): string { - if (encounter) { - this.ctx.requireEncounterIds([encounter]); + /** Reflect: consume encounters → experience. Empty encounters = direct creation. */ + reflect(encounters: string[], experience?: string, id?: string): string { + if (encounters.length > 0) { + this.ctx.requireEncounterIds(encounters); } - const result = this.api.ops["role.reflect"](encounter, this.roleId, experience, id); - if (encounter) { - this.ctx.consumeEncounters([encounter]); + // First encounter goes through ops (creates experience + removes encounter) + const first = encounters[0] as string | undefined; + const result = this.api.ops["role.reflect"](first, this.roleId, experience, id); + // Remaining encounters are consumed via forget + for (let i = 1; i < encounters.length; i++) { + 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 experience → principle. Empty experience = direct creation. */ - realize(experience?: string, principle?: string, id?: string): string { - if (experience) { - this.ctx.requireExperienceIds([experience]); + /** Realize: consume experiences → principle. Empty experiences = direct creation. */ + realize(experiences: string[], principle?: string, id?: string): string { + 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 = this.api.ops["role.realize"](first, this.roleId, principle, id); + // Remaining experiences are consumed via forget + for (let i = 1; i < experiences.length; i++) { + this.api.ops["role.forget"](experiences[i]); } - const result = this.api.ops["role.realize"](experience, this.roleId, principle, id); - if (experience) { - this.ctx.consumeExperiences([experience]); + if (experiences.length > 0) { + this.ctx.consumeExperiences(experiences); } return this.fmt("realize", id ?? "principle", result); } - /** Master: create procedure, optionally consuming experience. */ - master(procedure: string, id?: string, experience?: string): string { - if (experience) this.ctx.requireExperienceIds([experience]); - const result = this.api.ops["role.master"](this.roleId, procedure, id, experience); - if (experience) this.ctx.consumeExperiences([experience]); + /** Master: create procedure, optionally consuming experiences. */ + master(procedure: string, id?: string, experiences?: string[]): string { + if (experiences && experiences.length > 0) { + this.ctx.requireExperienceIds(experiences); + } + // First experience goes through ops (creates procedure + removes experience) + const first = experiences?.[0]; + const result = 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++) { + this.api.ops["role.forget"](experiences[i]); + } + this.ctx.consumeExperiences(experiences); + } return this.fmt("master", id ?? "procedure", result); } diff --git a/packages/rolexjs/tests/context.test.ts b/packages/rolexjs/tests/context.test.ts index 4f57cb7..150f8f4 100644 --- a/packages/rolexjs/tests/context.test.ts +++ b/packages/rolexjs/tests/context.test.ts @@ -107,7 +107,7 @@ describe("Role (ctx management)", () => { expect(role.ctx.encounterIds.has("login-finished")).toBe(true); role.reflect( - "login-finished", + ["login-finished"], "Feature: Token insight\n Scenario: OK\n Given x\n Then y", "token-insight" ); @@ -122,7 +122,7 @@ describe("Role (ctx management)", () => { const role = await rolex.activate("sean"); const result = role.reflect( - undefined, + [], "Feature: Direct insight\n Scenario: OK\n Given learned from conversation", "conv-insight" ); @@ -138,7 +138,7 @@ describe("Role (ctx management)", () => { const role = await rolex.activate("sean"); const result = role.realize( - undefined, + [], "Feature: Direct principle\n Scenario: OK\n Given always blame the product", "product-first" ); @@ -153,16 +153,12 @@ describe("Role (ctx management)", () => { const role = await rolex.activate("sean"); // Create experience directly - role.reflect( - undefined, - "Feature: Insight\n Scenario: OK\n Given something learned", - "my-insight" - ); + 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 role.realize( - "my-insight", + ["my-insight"], "Feature: Principle\n Scenario: OK\n Given a general truth", "my-principle" ); From c5a6d7d7d24a8256d5ee4eeb7a45464126fede51 Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 2 Mar 2026 17:30:02 +0800 Subject: [PATCH 50/54] refactor: introduce RoleXRepository interface and SqliteRepository - Define RoleXRepository and PrototypeRegistry interfaces in @rolexjs/core - Platform now uses `repository: RoleXRepository` instead of separate runtime/prototype/saveContext/loadContext properties - Implement SqliteRepository in local-platform with prototypes and contexts tables (replacing JSON file storage) - Simplify localPlatform to thin config layer (~80 lines from ~177) - Update Rolex to consume platform.repository - Update all tests to use new Platform shape Co-Authored-By: Claude Opus 4.6 --- .changeset/rolex-repository.md | 7 ++ packages/core/src/index.ts | 2 +- packages/core/src/platform.ts | 44 ++++--- packages/local-platform/src/LocalPlatform.ts | 107 ++--------------- .../local-platform/src/SqliteRepository.ts | 112 ++++++++++++++++++ packages/local-platform/src/index.ts | 4 +- .../local-platform/tests/prototype.test.ts | 22 ++-- packages/local-platform/tests/runtime.test.ts | 2 +- packages/rolexjs/src/rolex.ts | 27 ++--- packages/rolexjs/tests/context.test.ts | 40 +++---- 10 files changed, 194 insertions(+), 173 deletions(-) create mode 100644 .changeset/rolex-repository.md create mode 100644 packages/local-platform/src/SqliteRepository.ts 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/packages/core/src/index.ts b/packages/core/src/index.ts index 4942f70..31512ca 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,7 +38,7 @@ export { } from "@rolexjs/system"; // Platform -export type { ContextData, Platform } from "./platform.js"; +export type { ContextData, Platform, PrototypeRegistry, RoleXRepository } from "./platform.js"; // ===== Structures ===== diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index 04ae765..90c4299 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -7,8 +7,8 @@ * 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 { Initializer, Runtime } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; @@ -19,16 +19,36 @@ export interface ContextData { focusedPlanId: string | null; } -export interface Platform { - /** Graph operation engine (may include transparent persistence). */ +/** 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 registry — tracks which prototypes are settled. */ - readonly prototype?: { - settle(id: string, source: string): void; - evict(id: string): void; - list(): Record; - }; + readonly prototype: PrototypeRegistry; + + /** Save role context to persistent storage. */ + saveContext(roleId: string, data: ContextData): void; + + /** Load role context from persistent storage. Returns null if none exists. */ + loadContext(roleId: string): ContextData | null; +} + +export interface Platform { + /** Unified data access layer — graph, prototypes, contexts. */ + readonly repository: RoleXRepository; /** Resource management capability (optional — requires resourcexjs). */ readonly resourcex?: ResourceX; @@ -38,10 +58,4 @@ export interface Platform { /** Prototype sources to settle on genesis (local paths or ResourceX locators). */ readonly bootstrap?: readonly string[]; - - /** Save role context to persistent storage. */ - saveContext?(roleId: string, data: ContextData): void; - - /** Load role context from persistent storage. Returns null if none exists. */ - loadContext?(roleId: string): ContextData | null; } diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index 3cae2a1..00d147a 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -2,25 +2,21 @@ * localPlatform — create a Platform backed by SQLite + local filesystem. * * Storage: - * {dataDir}/rolex.db — SQLite database (single source of truth for runtime graph) - * {dataDir}/prototype.json — prototype registry - * {dataDir}/context/.json — role context persistence + * {dataDir}/rolex.db — SQLite database (all state: nodes, links, prototypes, contexts) * - * Runtime: SQLite-backed via Drizzle ORM (no in-memory Map, no load/save cycle). * When dataDir is null, runs with in-memory SQLite (useful for tests). */ -import { existsSync, mkdirSync, readFileSync, 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 { ContextData, Platform } from "@rolexjs/core"; +import type { Platform } from "@rolexjs/core"; import type { Initializer } from "@rolexjs/system"; -import { sql } from "drizzle-orm"; import { createResourceX, setProvider } from "resourcexjs"; -import { createSqliteRuntime } from "./sqliteRuntime.js"; +import { SqliteRepository } from "./SqliteRepository.js"; // ===== Config ===== @@ -33,34 +29,6 @@ export interface LocalPlatformConfig { bootstrap?: string[]; } -// ===== DDL ===== - -const CREATE_NODES = 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 -)`; - -const CREATE_LINKS = 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) -)`; - -const CREATE_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)`, -]; - // ===== Factory ===== /** Resolve the DEEPRACTICE_HOME base directory. Env > default (~/.deepractice). */ @@ -86,16 +54,9 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { const rawDb = openDatabase(dbPath); const db = drizzle(rawDb); - // Ensure tables exist - db.run(CREATE_NODES); - db.run(CREATE_LINKS); - for (const idx of CREATE_INDEXES) { - db.run(idx); - } - - // ===== Runtime ===== + // ===== Repository (all state in one place) ===== - const runtime = createSqliteRuntime(db); + const repository = new SqliteRepository(db); // ===== ResourceX ===== @@ -107,70 +68,16 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { }); } - // ===== Prototype registry ===== - - const registryPath = dataDir ? join(dataDir, "prototype.json") : undefined; - - const readRegistry = (): Record => { - if (registryPath && existsSync(registryPath)) { - return JSON.parse(readFileSync(registryPath, "utf-8")); - } - return {}; - }; - - const writeRegistry = (registry: Record): void => { - if (!registryPath) return; - mkdirSync(dataDir!, { recursive: true }); - writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf-8"); - }; - - const prototype = { - settle(id: string, source: string) { - const registry = readRegistry(); - registry[id] = source; - writeRegistry(registry); - }, - - evict(id: string) { - const registry = readRegistry(); - delete registry[id]; - writeRegistry(registry); - }, - - list(): Record { - return readRegistry(); - }, - }; - // ===== Initializer ===== const initializer: Initializer = { async bootstrap() {}, }; - // ===== Context persistence ===== - - const saveContext = (roleId: string, data: ContextData): void => { - if (!dataDir) return; - const contextDir = join(dataDir, "context"); - mkdirSync(contextDir, { recursive: true }); - writeFileSync(join(contextDir, `${roleId}.json`), JSON.stringify(data, null, 2), "utf-8"); - }; - - const loadContext = (roleId: string): ContextData | null => { - if (!dataDir) return null; - const contextPath = join(dataDir, "context", `${roleId}.json`); - if (!existsSync(contextPath)) return null; - return JSON.parse(readFileSync(contextPath, "utf-8")); - }; - return { - runtime, - prototype, + repository, resourcex, initializer, bootstrap: config.bootstrap, - saveContext, - loadContext, }; } diff --git a/packages/local-platform/src/SqliteRepository.ts b/packages/local-platform/src/SqliteRepository.ts new file mode 100644 index 0000000..7dcf050 --- /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); + } + + saveContext(roleId: string, data: ContextData): void { + this.db.run( + sql`INSERT OR REPLACE INTO contexts (role_id, focused_goal_id, focused_plan_id) + VALUES (${roleId}, ${data.focusedGoalId}, ${data.focusedPlanId})` + ); + } + + loadContext(roleId: string): ContextData | null { + 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/tests/prototype.test.ts b/packages/local-platform/tests/prototype.test.ts index 543b308..a32191e 100644 --- a/packages/local-platform/tests/prototype.test.ts +++ b/packages/local-platform/tests/prototype.test.ts @@ -13,28 +13,28 @@ afterEach(() => { describe("LocalPlatform Prototype registry", () => { test("settle registers id → source", () => { - const { prototype } = localPlatform({ dataDir: testDir, resourceDir }); - prototype!.settle("test-role", "/path/to/source"); - expect(prototype!.list()["test-role"]).toBe("/path/to/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("settle overwrites previous source", () => { - const { prototype } = localPlatform({ dataDir: testDir, resourceDir }); - prototype!.settle("test", "/v1"); - prototype!.settle("test", "/v2"); - expect(prototype!.list().test).toBe("/v2"); + const { repository } = localPlatform({ dataDir: testDir, resourceDir }); + repository.prototype.settle("test", "/v1"); + repository.prototype.settle("test", "/v2"); + expect(repository.prototype.list().test).toBe("/v2"); }); test("list returns empty object when no prototypes registered", () => { - const { prototype } = localPlatform({ dataDir: testDir, resourceDir }); - expect(prototype!.list()).toEqual({}); + const { repository } = localPlatform({ dataDir: testDir, resourceDir }); + expect(repository.prototype.list()).toEqual({}); }); test("registry persists across platform instances", () => { const p1 = localPlatform({ dataDir: testDir, resourceDir }); - p1.prototype!.settle("test-role", "/path"); + p1.repository.prototype.settle("test-role", "/path"); const p2 = localPlatform({ dataDir: testDir, resourceDir }); - expect(p2.prototype!.list()["test-role"]).toBe("/path"); + 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..8962071 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; } // ================================================================ diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index e105493..a2245b8 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -10,7 +10,7 @@ * Rolex just wires Platform → ops and manages Role lifecycle. */ -import type { ContextData, Platform } from "@rolexjs/core"; +import type { Platform, RoleXRepository } from "@rolexjs/core"; import * as C from "@rolexjs/core"; import { createOps, directives, type Ops, toArgs } from "@rolexjs/prototype"; import type { Initializer, Runtime, Structure } from "@rolexjs/system"; @@ -30,28 +30,20 @@ export class Rolex { private rt: Runtime; private ops: Ops; private resourcex?: ResourceX; - private protoRegistry?: NonNullable; + private repo: RoleXRepository; private readonly initializer?: Initializer; - private readonly persistContext?: { - save: (roleId: string, data: ContextData) => void; - load: (roleId: string) => ContextData | null; - }; private readonly bootstrap: readonly string[]; private readonly society: Structure; private readonly past: Structure; constructor(platform: Platform) { - this.rt = platform.runtime; + this.repo = platform.repository; + this.rt = this.repo.runtime; this.resourcex = platform.resourcex; - this.protoRegistry = platform.prototype; this.initializer = platform.initializer; this.bootstrap = platform.bootstrap ?? []; - if (platform.saveContext && platform.loadContext) { - this.persistContext = { save: platform.saveContext, load: platform.loadContext }; - } - // Ensure world roots exist const roots = this.rt.roots(); this.society = roots.find((r) => r.name === "society") ?? this.rt.create(null, C.society); @@ -72,7 +64,7 @@ export class Rolex { }, find: (id: string) => this.find(id), resourcex: platform.resourcex, - prototype: platform.prototype, + prototype: this.repo.prototype, direct: (locator: string, args?: Record) => this.direct(locator, args), }); } @@ -95,9 +87,7 @@ export class Rolex { async activate(individual: string): Promise { let node = this.find(individual); if (!node) { - const hasProto = this.protoRegistry - ? Object.hasOwn(this.protoRegistry.list(), individual) - : false; + const hasProto = Object.hasOwn(this.repo.prototype.list(), individual); if (hasProto) { this.ops["individual.born"](undefined, individual); node = this.find(individual)!; @@ -110,7 +100,7 @@ export class Rolex { ctx.rehydrate(state); // Restore persisted focus (only override rehydrate default when persisted value is valid) - const persisted = this.persistContext?.load(individual) ?? null; + const persisted = this.repo.loadContext(individual); if (persisted) { if (persisted.focusedGoalId && this.find(persisted.focusedGoalId)) { ctx.focusedGoalId = persisted.focusedGoalId; @@ -122,8 +112,9 @@ export class Rolex { // Build internal API for Role — ops + ctx persistence const ops = this.ops; + const repo = this.repo; const saveCtx = (c: RoleContext) => { - this.persistContext?.save(c.roleId, { + repo.saveContext(c.roleId, { focusedGoalId: c.focusedGoalId, focusedPlanId: c.focusedPlanId, }); diff --git a/packages/rolexjs/tests/context.test.ts b/packages/rolexjs/tests/context.test.ts index 150f8f4..53bae02 100644 --- a/packages/rolexjs/tests/context.test.ts +++ b/packages/rolexjs/tests/context.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { localPlatform } from "@rolexjs/local-platform"; @@ -194,7 +194,7 @@ describe("Role context persistence", () => { } test("activate restores persisted focusedGoalId and focusedPlanId", async () => { - const { rolex, dataDir } = persistent(); + const { rolex } = persistent(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); // Session 1: set focus @@ -204,14 +204,7 @@ describe("Role context persistence", () => { expect(role1.ctx.focusedGoalId).toBe("auth"); expect(role1.ctx.focusedPlanId).toBe("jwt"); - // Verify context.json written - const contextPath = join(dataDir, "context", "sean.json"); - expect(existsSync(contextPath)).toBe(true); - const data = JSON.parse(readFileSync(contextPath, "utf-8")); - expect(data.focusedGoalId).toBe("auth"); - expect(data.focusedPlanId).toBe("jwt"); - - // Session 2: re-activate restores + // 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"); @@ -228,7 +221,7 @@ describe("Role context persistence", () => { }); test("focus saves updated context", async () => { - const { rolex, dataDir } = persistent(); + const { rolex } = persistent(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); @@ -237,13 +230,14 @@ describe("Role context persistence", () => { role.focus("goal-a"); - const data = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); - expect(data.focusedGoalId).toBe("goal-a"); - expect(data.focusedPlanId).toBeNull(); + // 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, dataDir } = persistent(); + const { rolex } = persistent(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); @@ -251,13 +245,14 @@ describe("Role context persistence", () => { role.plan("Feature: JWT", "jwt"); role.complete("jwt", "Feature: Done\n Scenario: OK\n Given done\n Then ok"); - const data = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); - expect(data.focusedGoalId).toBe("auth"); - expect(data.focusedPlanId).toBeNull(); + // 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, dataDir } = persistent(); + const { rolex } = persistent(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); await rolex.direct("!individual.born", { content: "Feature: Nuwa", id: "nuwa" }); @@ -267,12 +262,7 @@ describe("Role context persistence", () => { const nuwaRole = await rolex.activate("nuwa"); nuwaRole.want("Feature: Nuwa Goal", "nuwa-goal"); - const seanData = JSON.parse(readFileSync(join(dataDir, "context", "sean.json"), "utf-8")); - const nuwaData = JSON.parse(readFileSync(join(dataDir, "context", "nuwa.json"), "utf-8")); - expect(seanData.focusedGoalId).toBe("sean-goal"); - expect(nuwaData.focusedGoalId).toBe("nuwa-goal"); - - // Re-activate sean — should get sean's context + // Re-activate sean — should get sean's context, not nuwa's const seanRole2 = await rolex.activate("sean"); expect(seanRole2.ctx.focusedGoalId).toBe("sean-goal"); }); From 8449a59dd5d300c42266af32c10c60ed236ea6e3 Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 2 Mar 2026 19:49:26 +0800 Subject: [PATCH 51/54] refactor: integrate ResourceXProvider into Platform instead of ResourceX Platform now declares resourcexProvider instead of resourcex. Rolex creates the ResourceX instance internally from the injected provider, making storage backend swappable for cloud deployment. Co-Authored-By: Claude Opus 4.6 --- .changeset/resourcex-provider-integration.md | 12 ++++++++++++ bun.lock | 3 +-- packages/core/package.json | 2 +- packages/core/src/platform.ts | 7 ++++--- packages/local-platform/package.json | 3 +-- packages/local-platform/src/LocalPlatform.ts | 13 +++---------- packages/rolexjs/src/rolex.ts | 10 ++++++++-- 7 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 .changeset/resourcex-provider-integration.md 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/bun.lock b/bun.lock index 163f95a..cb928d9 100644 --- a/bun.lock +++ b/bun.lock @@ -58,8 +58,8 @@ "name": "@rolexjs/core", "version": "0.11.0", "dependencies": { + "@resourcexjs/core": "^2.14.0", "@rolexjs/system": "workspace:*", - "resourcexjs": "^2.14.0", }, }, "packages/genesis": { @@ -76,7 +76,6 @@ "@rolexjs/core": "workspace:*", "@rolexjs/system": "workspace:*", "drizzle-orm": "^0.45.1", - "resourcexjs": "^2.14.0", }, }, "packages/parser": { diff --git a/packages/core/package.json b/packages/core/package.json index 86ae399..51dd342 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@rolexjs/system": "workspace:*", - "resourcexjs": "^2.14.0" + "@resourcexjs/core": "^2.14.0" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index 90c4299..c12eabb 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -10,8 +10,9 @@ * Platform combines a RoleXRepository (data access) with external services * (ResourceX, bootstrap config) to form a complete runtime environment. */ + +import type { ResourceXProvider } from "@resourcexjs/core"; import type { Initializer, Runtime } from "@rolexjs/system"; -import type { ResourceX } from "resourcexjs"; /** Serializable context data for persistence. */ export interface ContextData { @@ -50,8 +51,8 @@ export interface Platform { /** Unified data access layer — graph, prototypes, contexts. */ readonly repository: RoleXRepository; - /** Resource management capability (optional — requires resourcexjs). */ - readonly resourcex?: ResourceX; + /** ResourceX provider — injected storage backend for resource management. */ + readonly resourcexProvider?: ResourceXProvider; /** Initializer — bootstrap the world on first run. */ readonly initializer?: Initializer; diff --git a/packages/local-platform/package.json b/packages/local-platform/package.json index d110074..52a3e6d 100644 --- a/packages/local-platform/package.json +++ b/packages/local-platform/package.json @@ -25,8 +25,7 @@ "@resourcexjs/node-provider": "^2.14.0", "@rolexjs/core": "workspace:*", "@rolexjs/system": "workspace:*", - "drizzle-orm": "^0.45.1", - "resourcexjs": "^2.14.0" + "drizzle-orm": "^0.45.1" }, "devDependencies": {}, "publishConfig": { diff --git a/packages/local-platform/src/LocalPlatform.ts b/packages/local-platform/src/LocalPlatform.ts index 00d147a..8ace72e 100644 --- a/packages/local-platform/src/LocalPlatform.ts +++ b/packages/local-platform/src/LocalPlatform.ts @@ -15,7 +15,6 @@ import { openDatabase } from "@deepracticex/sqlite"; import { NodeProvider } from "@resourcexjs/node-provider"; import type { Platform } from "@rolexjs/core"; import type { Initializer } from "@rolexjs/system"; -import { createResourceX, setProvider } from "resourcexjs"; import { SqliteRepository } from "./SqliteRepository.js"; // ===== Config ===== @@ -58,15 +57,9 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { const repository = new SqliteRepository(db); - // ===== ResourceX ===== + // ===== ResourceX Provider ===== - let resourcex: ReturnType | undefined; - if (config.resourceDir !== null) { - setProvider(new NodeProvider()); - resourcex = createResourceX({ - path: config.resourceDir ?? join(deepracticeHome(), "resourcex"), - }); - } + const resourcexProvider = config.resourceDir !== null ? new NodeProvider() : undefined; // ===== Initializer ===== @@ -76,7 +69,7 @@ export function localPlatform(config: LocalPlatformConfig = {}): Platform { return { repository, - resourcex, + resourcexProvider, initializer, bootstrap: config.bootstrap, }; diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index a2245b8..b9e0b7a 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -15,6 +15,7 @@ import * as C from "@rolexjs/core"; import { createOps, directives, type Ops, toArgs } from "@rolexjs/prototype"; import type { Initializer, Runtime, Structure } from "@rolexjs/system"; import type { ResourceX } from "resourcexjs"; +import { createResourceX, setProvider } from "resourcexjs"; import { RoleContext } from "./context.js"; import { findInState } from "./find.js"; import { Role, type RolexInternal } from "./role.js"; @@ -40,10 +41,15 @@ export class Rolex { constructor(platform: Platform) { this.repo = platform.repository; this.rt = this.repo.runtime; - this.resourcex = platform.resourcex; this.initializer = platform.initializer; this.bootstrap = platform.bootstrap ?? []; + // Create ResourceX from injected provider + if (platform.resourcexProvider) { + setProvider(platform.resourcexProvider); + this.resourcex = createResourceX(); + } + // Ensure world roots exist const roots = this.rt.roots(); this.society = roots.find((r) => r.name === "society") ?? this.rt.create(null, C.society); @@ -63,7 +69,7 @@ export class Rolex { return node; }, find: (id: string) => this.find(id), - resourcex: platform.resourcex, + resourcex: this.resourcex, prototype: this.repo.prototype, direct: (locator: string, args?: Record) => this.direct(locator, args), }); From a9a178962bc4a542802344e84a6893b2975a75ab Mon Sep 17 00:00:00 2001 From: sean Date: Tue, 3 Mar 2026 09:17:02 +0800 Subject: [PATCH 52/54] chore: add 1.0.0 release changeset Co-Authored-By: Claude Opus 4.6 --- .changeset/release-1.0.0.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .changeset/release-1.0.0.md 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 From cdde8eb1375adb76423f0cf5ee9b4e68f77dca43 Mon Sep 17 00:00:00 2001 From: sean Date: Tue, 3 Mar 2026 09:50:14 +0800 Subject: [PATCH 53/54] refactor: make Runtime interface fully async MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert all Runtime methods to return Promises, propagating async/await through system → local-platform → prototype → rolexjs → mcp-server. This removes the synchronous constraint and enables cloud-native async storage backends (D1, Turso HTTP, PostgreSQL) without architectural workarounds. - Runtime interface: 8 methods now return Promise - createRuntime() and createSqliteRuntime(): all methods async - RoleXRepository: saveContext/loadContext now async - Rolex: constructor split into private constructor + static async create() - createRoleX() now returns Promise - Role: all public methods async (want, plan, todo, finish, etc.) - ops: 30+ operations async with await on all Runtime calls - MCP server handlers: await on all Role method calls - Tests: all Runtime/Role calls updated with await Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server/src/index.ts | 28 +- apps/mcp-server/tests/mcp.test.ts | 47 +- packages/core/src/platform.ts | 4 +- .../local-platform/src/SqliteRepository.ts | 4 +- packages/local-platform/src/sqliteRuntime.ts | 16 +- packages/local-platform/tests/runtime.test.ts | 410 +++++------ .../tests/sqliteRuntime.test.ts | 100 +-- packages/prototype/src/ops.ts | 235 ++++--- packages/prototype/tests/ops.test.ts | 652 +++++++++--------- packages/rolexjs/src/index.ts | 4 +- packages/rolexjs/src/role.ts | 88 +-- packages/rolexjs/src/rolex.ts | 59 +- packages/rolexjs/tests/context.test.ts | 116 ++-- packages/rolexjs/tests/rolex.test.ts | 68 +- packages/system/src/runtime.ts | 32 +- packages/system/tests/system.test.ts | 230 +++--- 16 files changed, 1074 insertions(+), 1019 deletions(-) diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index d490a9c..037351a 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -15,7 +15,7 @@ import { McpState } from "./state.js"; // ========== Setup ========== -const rolex = createRoleX( +const rolex = await createRoleX( localPlatform({ bootstrap: ["npm:@rolexjs/genesis"], }) @@ -43,7 +43,7 @@ server.addTool({ try { const role = await rolex.activate(roleId); state.role = role; - return role.project(); + return await role.project(); } catch { const census = await rolex.direct("!census.list"); throw new Error( @@ -60,7 +60,7 @@ server.addTool({ id: z.string().optional().describe("Goal id to switch to. Omit to view current."), }), execute: async ({ id }) => { - return state.requireRole().focus(id); + return await state.requireRole().focus(id); }, }); @@ -74,7 +74,7 @@ server.addTool({ goal: z.string().describe("Gherkin Feature source describing the goal"), }), execute: async ({ id, goal }) => { - return state.requireRole().want(goal, id); + return await state.requireRole().want(goal, id); }, }); @@ -94,7 +94,7 @@ server.addTool({ .describe("Plan id this plan is a backup for (alternative/strategy relationship)"), }), execute: async ({ id, plan, after, fallback }) => { - return state.requireRole().plan(plan, id, after, fallback); + return await state.requireRole().plan(plan, id, after, fallback); }, }); @@ -106,7 +106,7 @@ server.addTool({ task: z.string().describe("Gherkin Feature source describing the task"), }), execute: async ({ id, task }) => { - return state.requireRole().todo(task, id); + return await state.requireRole().todo(task, id); }, }); @@ -118,7 +118,7 @@ server.addTool({ encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"), }), execute: async ({ id, encounter }) => { - return state.requireRole().finish(id, encounter); + return await state.requireRole().finish(id, encounter); }, }); @@ -130,7 +130,7 @@ server.addTool({ encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"), }), execute: async ({ id, encounter }) => { - return state.requireRole().complete(id, encounter); + return await state.requireRole().complete(id, encounter); }, }); @@ -142,7 +142,7 @@ server.addTool({ encounter: z.string().optional().describe("Optional Gherkin Feature describing what happened"), }), execute: async ({ id, encounter }) => { - return state.requireRole().abandon(id, encounter); + return await state.requireRole().abandon(id, encounter); }, }); @@ -159,7 +159,7 @@ server.addTool({ experience: z.string().optional().describe("Gherkin Feature source for the experience"), }), execute: async ({ ids, id, experience }) => { - return state.requireRole().reflect(ids, experience, id); + return await state.requireRole().reflect(ids, experience, id); }, }); @@ -172,7 +172,7 @@ server.addTool({ principle: z.string().optional().describe("Gherkin Feature source for the principle"), }), execute: async ({ ids, id, principle }) => { - return state.requireRole().realize(ids, principle, id); + return await state.requireRole().realize(ids, principle, id); }, }); @@ -185,7 +185,7 @@ server.addTool({ procedure: z.string().describe("Gherkin Feature source for the procedure"), }), execute: async ({ ids, id, procedure }) => { - return state.requireRole().master(procedure, id, ids); + return await state.requireRole().master(procedure, id, ids); }, }); @@ -200,7 +200,7 @@ server.addTool({ .describe("Id of the node to remove (principle, procedure, experience, encounter, etc.)"), }), execute: async ({ id }) => { - return state.requireRole().forget(id); + return await state.requireRole().forget(id); }, }); @@ -215,7 +215,7 @@ server.addTool({ .describe("ResourceX locator for the skill (e.g. deepractice/role-management)"), }), execute: async ({ locator }) => { - return state.requireRole().skill(locator); + return await state.requireRole().skill(locator); }, }); diff --git a/apps/mcp-server/tests/mcp.test.ts b/apps/mcp-server/tests/mcp.test.ts index f69a1ad..92ce447 100644 --- a/apps/mcp-server/tests/mcp.test.ts +++ b/apps/mcp-server/tests/mcp.test.ts @@ -14,8 +14,8 @@ import { McpState } from "../src/state.js"; let rolex: Rolex; let state: McpState; -beforeEach(() => { - rolex = createRoleX(localPlatform({ dataDir: null })); +beforeEach(async () => { + rolex = await createRoleX(localPlatform({ dataDir: null })); state = new McpState(); }); @@ -80,7 +80,7 @@ describe("render", () => { await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - const output = role.want("Feature: Test", "test-goal"); + const output = await role.want("Feature: Test", "test-goal"); // Layer 1: Status expect(output).toContain('Goal "test-goal" declared.'); // Layer 2: Hint @@ -102,21 +102,21 @@ describe("full execution flow", () => { state.role = role; // Want - const goal = role.want("Feature: Build Auth", "build-auth"); + const goal = await role.want("Feature: Build Auth", "build-auth"); expect(role.ctx.focusedGoalId).toBe("build-auth"); expect(goal).toContain("I →"); // Plan - const plan = role.plan("Feature: Auth Plan", "auth-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 = role.todo("Feature: Implement JWT", "impl-jwt"); + const task = await role.todo("Feature: Implement JWT", "impl-jwt"); expect(task).toContain("I →"); // Finish with encounter - const finished = role.finish( + const finished = await role.finish( "impl-jwt", "Feature: Implemented JWT\n Scenario: Token pattern\n Given JWT needed\n Then tokens work" ); @@ -124,8 +124,8 @@ describe("full execution flow", () => { expect(role.ctx.encounterIds.has("impl-jwt-finished")).toBe(true); // Reflect: encounter → experience - const reflected = role.reflect( - "impl-jwt-finished", + 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" ); @@ -134,8 +134,8 @@ describe("full execution flow", () => { expect(role.ctx.experienceIds.has("token-insight")).toBe(true); // Realize: experience → principle - const realized = role.realize( - "token-insight", + 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" ); @@ -154,14 +154,14 @@ describe("focus", () => { const role = await rolex.activate("sean"); state.role = role; - role.want("Feature: Goal A", "goal-a"); + await role.want("Feature: Goal A", "goal-a"); expect(role.ctx.focusedGoalId).toBe("goal-a"); - role.want("Feature: Goal B", "goal-b"); + await role.want("Feature: Goal B", "goal-b"); expect(role.ctx.focusedGoalId).toBe("goal-b"); // Switch back to goal A - role.focus("goal-a"); + await role.focus("goal-a"); expect(role.ctx.focusedGoalId).toBe("goal-a"); }); }); @@ -177,14 +177,17 @@ describe("selective cognition", () => { state.role = role; // Create goal + plan + tasks - role.want("Feature: Auth", "auth"); - role.plan("Feature: Plan", "plan1"); - role.todo("Feature: Login", "login"); - role.todo("Feature: Signup", "signup"); + 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 - role.finish("login", "Feature: Login done\n Scenario: OK\n Given login\n Then success"); - role.finish( + await role.finish( + "login", + "Feature: Login done\n Scenario: OK\n Given login\n Then success" + ); + await role.finish( "signup", "Feature: Signup done\n Scenario: OK\n Given signup\n Then success" ); @@ -193,8 +196,8 @@ describe("selective cognition", () => { expect(role.ctx.encounterIds.has("signup-finished")).toBe(true); // Reflect only on "login-finished" - role.reflect( - "login-finished", + await role.reflect( + ["login-finished"], "Feature: Login insight\n Scenario: OK\n Given practice\n Then understanding", "login-insight" ); diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index c12eabb..4b07e32 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -41,10 +41,10 @@ export interface RoleXRepository { readonly prototype: PrototypeRegistry; /** Save role context to persistent storage. */ - saveContext(roleId: string, data: ContextData): void; + saveContext(roleId: string, data: ContextData): Promise; /** Load role context from persistent storage. Returns null if none exists. */ - loadContext(roleId: string): ContextData | null; + loadContext(roleId: string): Promise; } export interface Platform { diff --git a/packages/local-platform/src/SqliteRepository.ts b/packages/local-platform/src/SqliteRepository.ts index 7dcf050..0945b94 100644 --- a/packages/local-platform/src/SqliteRepository.ts +++ b/packages/local-platform/src/SqliteRepository.ts @@ -65,14 +65,14 @@ export class SqliteRepository implements RoleXRepository { this.prototype = createPrototypeRegistry(db); } - saveContext(roleId: string, data: ContextData): void { + 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})` ); } - loadContext(roleId: string): ContextData | null { + async loadContext(roleId: string): Promise { const row = this.db.all<{ role_id: string; focused_goal_id: string | null; diff --git a/packages/local-platform/src/sqliteRuntime.ts b/packages/local-platform/src/sqliteRuntime.ts index 046a70f..8a473e1 100644 --- a/packages/local-platform/src/sqliteRuntime.ts +++ b/packages/local-platform/src/sqliteRuntime.ts @@ -95,7 +95,7 @@ function removeSubtree(db: DB, ref: string): void { export function createSqliteRuntime(db: DB): Runtime { return { - create(parent, type, information, id, alias) { + async create(parent, type, information, id, alias) { // Idempotent: same id under same parent → return existing. if (id) { const existing = db @@ -121,7 +121,7 @@ export function createSqliteRuntime(db: DB): Runtime { return toStructure(db.select().from(nodes).where(eq(nodes.ref, ref)).get()!); }, - remove(node) { + async remove(node) { if (!node.ref) return; const row = db.select().from(nodes).where(eq(nodes.ref, node.ref)).get(); if (!row) return; @@ -130,7 +130,7 @@ export function createSqliteRuntime(db: DB): Runtime { removeSubtree(db, node.ref); }, - transform(source, target, information) { + 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}`); @@ -159,7 +159,7 @@ export function createSqliteRuntime(db: DB): Runtime { return toStructure(db.select().from(nodes).where(eq(nodes.ref, source.ref)).get()!); }, - 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"); @@ -192,7 +192,7 @@ export function createSqliteRuntime(db: DB): Runtime { } }, - unlink(from, to, relationName, reverseName) { + async unlink(from, to, relationName, reverseName) { if (!from.ref || !to.ref) return; db.delete(links) @@ -212,19 +212,19 @@ export function createSqliteRuntime(db: DB): Runtime { .run(); }, - tag(node, tagValue) { + 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(); }, - project(node) { + async project(node) { if (!node.ref) throw new Error(`Node has no ref`); return projectNode(db, node.ref); }, - roots() { + async roots() { const rows = db.select().from(nodes).where(isNull(nodes.parentRef)).all(); return rows.map(toStructure); }, diff --git a/packages/local-platform/tests/runtime.test.ts b/packages/local-platform/tests/runtime.test.ts index 8962071..eedccee 100644 --- a/packages/local-platform/tests/runtime.test.ts +++ b/packages/local-platform/tests/runtime.test.ts @@ -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 index 108cd65..a2927ec 100644 --- a/packages/local-platform/tests/sqliteRuntime.test.ts +++ b/packages/local-platform/tests/sqliteRuntime.test.ts @@ -30,17 +30,17 @@ function setup() { } describe("SQLite Runtime", () => { - test("create root node", () => { + test("create root node", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); + const society = await rt.create(null, C.society); expect(society.ref).toBe("n1"); expect(society.name).toBe("society"); }); - test("create child node", () => { + test("create child node", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); - const ind = rt.create(society, C.individual, "Feature: Sean", "sean", ["Sean", "姜山"]); + 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"); @@ -48,107 +48,107 @@ describe("SQLite Runtime", () => { expect(ind.information).toBe("Feature: Sean"); }); - test("project subtree", () => { + test("project subtree", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); - const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); - rt.create(ind, C.identity, undefined, "identity"); + 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 = rt.project(society); + 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", () => { + test("remove subtree", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); - const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); - rt.create(ind, C.identity, undefined, "identity"); + 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"); - rt.remove(ind); - const state = rt.project(society); + await rt.remove(ind); + const state = await rt.project(society); expect(state.children).toHaveLength(0); }); - test("link and unlink", () => { + test("link and unlink", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); - const org = rt.create(society, C.organization, "Feature: DP", "dp"); - const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); + 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"); - rt.link(org, ind, "membership", "belong"); + await rt.link(org, ind, "membership", "belong"); - let state = rt.project(org); + 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"); - rt.unlink(org, ind, "membership", "belong"); - state = rt.project(org); + await rt.unlink(org, ind, "membership", "belong"); + state = await rt.project(org); expect(state.links).toBeUndefined(); }); - test("tag node", () => { + test("tag node", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); - const goal = rt.create(society, C.goal, "Feature: Test", "test-goal"); - rt.tag(goal, "done"); + 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 = rt.project(goal); + const state = await rt.project(goal); expect(state.tag).toBe("done"); }); - test("roots returns only root nodes", () => { + test("roots returns only root nodes", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); - rt.create(society, C.individual, "Feature: Sean", "sean"); + const society = await rt.create(null, C.society); + await rt.create(society, C.individual, "Feature: Sean", "sean"); - const roots = rt.roots(); + const roots = await rt.roots(); expect(roots).toHaveLength(1); expect(roots[0].name).toBe("society"); }); - test("transform creates node under target parent", () => { + test("transform creates node under target parent", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); - rt.create(society, C.past); - const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); + 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 = rt.transform(ind, C.past, "Feature: Sean"); + 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)", () => { + test("refs survive across operations (no stale refs)", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); - const ind = rt.create(society, C.individual, "Feature: Sean", "sean"); - const org = rt.create(society, C.organization, "Feature: DP", "dp"); + 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 - rt.link(org, ind, "membership", "belong"); - rt.create(org, C.charter, "Feature: Mission"); + 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 = rt.project(society); + const state = await rt.project(society); expect(state.children).toHaveLength(2); // individual + organization }); - test("position persists (the bug that triggered this rewrite)", () => { + test("position persists (the bug that triggered this rewrite)", async () => { const { rt } = setup(); - const society = rt.create(null, C.society); - const pos = rt.create(society, C.position, "Feature: Architect", "architect"); + 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 = rt.project(pos); + const state = await rt.project(pos); expect(state.name).toBe("position"); expect(state.id).toBe("architect"); // Also visible from society - const societyState = rt.project(society); + const societyState = await rt.project(society); const names = societyState.children!.map((c) => c.name); expect(names).toContain("position"); }); diff --git a/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts index 34f4ffa..1c62bed 100644 --- a/packages/prototype/src/ops.ts +++ b/packages/prototype/src/ops.ts @@ -27,8 +27,8 @@ export interface OpsContext { rt: Runtime; society: Structure; past: Structure; - resolve(id: string): Structure; - find(id: string): Structure | null; + resolve(id: string): Structure | Promise; + find(id: string): (Structure | null) | Promise; resourcex?: ResourceX; prototype?: { settle(id: string, source: string): void; @@ -50,12 +50,12 @@ export function createOps(ctx: OpsContext): Ops { // ---- Helpers ---- - function ok(node: Structure, process: string): OpResult { - return { state: rt.project(node), process }; + async function ok(node: Structure, process: string): Promise { + return { state: await rt.project(node), process }; } - function archive(node: Structure, process: string): OpResult { - const archived = rt.transform(node, C.past); + async function archive(node: Structure, process: string): Promise { + const archived = await rt.transform(node, C.past); return ok(archived, process); } @@ -83,10 +83,10 @@ export function createOps(ctx: OpsContext): Ops { return null; } - function removeExisting(parent: Structure, id: string): void { - const state = rt.project(parent); + async function removeExisting(parent: Structure, id: string): Promise { + const state = await rt.project(parent); const existing = findInState(state, id); - if (existing) rt.remove(existing); + if (existing) await rt.remove(existing); } function requireResourceX(): ResourceX { @@ -101,181 +101,198 @@ export function createOps(ctx: OpsContext): Ops { return { // ---- Individual: lifecycle ---- - "individual.born"(content?: string, id?: string, alias?: readonly string[]): OpResult { + async "individual.born"( + content?: string, + id?: string, + alias?: readonly string[] + ): Promise { validateGherkin(content); - const node = rt.create(society, C.individual, content, id, alias); - rt.create(node, C.identity, undefined, id ? `${id}-identity` : undefined); + 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"); }, - "individual.retire"(individual: string): OpResult { - return archive(resolve(individual), "retire"); + async "individual.retire"(individual: string): Promise { + return archive(await resolve(individual), "retire"); }, - "individual.die"(individual: string): OpResult { - return archive(resolve(individual), "die"); + async "individual.die"(individual: string): Promise { + return archive(await resolve(individual), "die"); }, - "individual.rehire"(pastNode: string): OpResult { - const node = resolve(pastNode); - const ind = rt.transform(node, C.individual); + 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 ---- - "individual.teach"(individual: string, principle: string, id?: string): OpResult { + async "individual.teach"( + individual: string, + principle: string, + id?: string + ): Promise { validateGherkin(principle); - const parent = resolve(individual); - if (id) removeExisting(parent, id); - const node = rt.create(parent, C.principle, principle, id); + 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"); }, - "individual.train"(individual: string, procedure: string, id?: string): OpResult { + async "individual.train"( + individual: string, + procedure: string, + id?: string + ): Promise { validateGherkin(procedure); - const parent = resolve(individual); - if (id) removeExisting(parent, id); - const node = rt.create(parent, C.procedure, procedure, id); + 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 ---- - "role.focus"(goal: string): OpResult { - return ok(resolve(goal), "focus"); + async "role.focus"(goal: string): Promise { + return ok(await resolve(goal), "focus"); }, // ---- Role: execution ---- - "role.want"( + async "role.want"( individual: string, goal?: string, id?: string, alias?: readonly string[] - ): OpResult { + ): Promise { validateGherkin(goal); - const node = rt.create(resolve(individual), C.goal, goal, id, alias); + const node = await rt.create(await resolve(individual), C.goal, goal, id, alias); return ok(node, "want"); }, - "role.plan"( + async "role.plan"( goal: string, plan?: string, id?: string, after?: string, fallback?: string - ): OpResult { + ): Promise { validateGherkin(plan); - const node = rt.create(resolve(goal), C.plan, plan, id); - if (after) rt.link(node, resolve(after), "after", "before"); - if (fallback) rt.link(node, resolve(fallback), "fallback-for", "fallback"); + 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"); }, - "role.todo"(plan: string, task?: string, id?: string, alias?: readonly string[]): OpResult { + async "role.todo"( + plan: string, + task?: string, + id?: string, + alias?: readonly string[] + ): Promise { validateGherkin(task); - const node = rt.create(resolve(plan), C.task, task, id, alias); + const node = await rt.create(await resolve(plan), C.task, task, id, alias); return ok(node, "todo"); }, - "role.finish"(task: string, individual: string, encounter?: string): OpResult { + async "role.finish"(task: string, individual: string, encounter?: string): Promise { validateGherkin(encounter); - const taskNode = resolve(task); - rt.tag(taskNode, "done"); + const taskNode = await resolve(task); + await rt.tag(taskNode, "done"); if (encounter) { const encId = taskNode.id ? `${taskNode.id}-finished` : undefined; - const enc = rt.create(resolve(individual), C.encounter, encounter, encId); + const enc = await rt.create(await resolve(individual), C.encounter, encounter, encId); return ok(enc, "finish"); } return ok(taskNode, "finish"); }, - "role.complete"(plan: string, individual: string, encounter?: string): OpResult { + async "role.complete"(plan: string, individual: string, encounter?: string): Promise { validateGherkin(encounter); - const planNode = resolve(plan); - rt.tag(planNode, "done"); + const planNode = await resolve(plan); + await rt.tag(planNode, "done"); const encId = planNode.id ? `${planNode.id}-completed` : undefined; - const enc = rt.create(resolve(individual), C.encounter, encounter, encId); + const enc = await rt.create(await resolve(individual), C.encounter, encounter, encId); return ok(enc, "complete"); }, - "role.abandon"(plan: string, individual: string, encounter?: string): OpResult { + async "role.abandon"(plan: string, individual: string, encounter?: string): Promise { validateGherkin(encounter); - const planNode = resolve(plan); - rt.tag(planNode, "abandoned"); + const planNode = await resolve(plan); + await rt.tag(planNode, "abandoned"); const encId = planNode.id ? `${planNode.id}-abandoned` : undefined; - const enc = rt.create(resolve(individual), C.encounter, encounter, encId); + const enc = await rt.create(await resolve(individual), C.encounter, encounter, encId); return ok(enc, "abandon"); }, // ---- Role: cognition ---- - "role.reflect"( + async "role.reflect"( encounter: string | undefined, individual: string, experience?: string, id?: string - ): OpResult { + ): Promise { validateGherkin(experience); if (encounter) { - const encNode = resolve(encounter); - const exp = rt.create( - resolve(individual), + const encNode = await resolve(encounter); + const exp = await rt.create( + await resolve(individual), C.experience, experience || encNode.information, id ); - rt.remove(encNode); + await rt.remove(encNode); return ok(exp, "reflect"); } // Direct creation — no encounter to consume - const exp = rt.create(resolve(individual), C.experience, experience, id); + const exp = await rt.create(await resolve(individual), C.experience, experience, id); return ok(exp, "reflect"); }, - "role.realize"( + async "role.realize"( experience: string | undefined, individual: string, principle?: string, id?: string - ): OpResult { + ): Promise { validateGherkin(principle); if (experience) { - const expNode = resolve(experience); - const prin = rt.create( - resolve(individual), + const expNode = await resolve(experience); + const prin = await rt.create( + await resolve(individual), C.principle, principle || expNode.information, id ); - rt.remove(expNode); + await rt.remove(expNode); return ok(prin, "realize"); } // Direct creation — no experience to consume - const prin = rt.create(resolve(individual), C.principle, principle, id); + const prin = await rt.create(await resolve(individual), C.principle, principle, id); return ok(prin, "realize"); }, - "role.master"( + async "role.master"( individual: string, procedure: string, id?: string, experience?: string - ): OpResult { + ): Promise { validateGherkin(procedure); - const parent = resolve(individual); - if (id) removeExisting(parent, id); - const proc = rt.create(parent, C.procedure, procedure, id); - if (experience) rt.remove(resolve(experience)); + 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 ---- - "role.forget"(nodeId: string): OpResult { - const node = resolve(nodeId); - rt.remove(node); + async "role.forget"(nodeId: string): Promise { + const node = await resolve(nodeId); + await rt.remove(node); return { state: { ...node, children: [] }, process: "forget" }; }, @@ -295,87 +312,91 @@ export function createOps(ctx: OpsContext): Ops { // ---- Org ---- - "org.found"(content?: string, id?: string, alias?: readonly string[]): OpResult { + async "org.found"(content?: string, id?: string, alias?: readonly string[]): Promise { validateGherkin(content); - const node = rt.create(society, C.organization, content, id, alias); + const node = await rt.create(society, C.organization, content, id, alias); return ok(node, "found"); }, - "org.charter"(org: string, charter: string, id?: string): OpResult { + async "org.charter"(org: string, charter: string, id?: string): Promise { validateGherkin(charter); - const node = rt.create(resolve(org), C.charter, charter, id); + const node = await rt.create(await resolve(org), C.charter, charter, id); return ok(node, "charter"); }, - "org.dissolve"(org: string): OpResult { - return archive(resolve(org), "dissolve"); + async "org.dissolve"(org: string): Promise { + return archive(await resolve(org), "dissolve"); }, - "org.hire"(org: string, individual: string): OpResult { - const orgNode = resolve(org); - rt.link(orgNode, resolve(individual), "membership", "belong"); + 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"); }, - "org.fire"(org: string, individual: string): OpResult { - const orgNode = resolve(org); - rt.unlink(orgNode, resolve(individual), "membership", "belong"); + 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 ---- - "position.establish"(content?: string, id?: string, alias?: readonly string[]): OpResult { + async "position.establish"( + content?: string, + id?: string, + alias?: readonly string[] + ): Promise { validateGherkin(content); - const node = rt.create(society, C.position, content, id, alias); + const node = await rt.create(society, C.position, content, id, alias); return ok(node, "establish"); }, - "position.charge"(position: string, duty: string, id?: string): OpResult { + async "position.charge"(position: string, duty: string, id?: string): Promise { validateGherkin(duty); - const node = rt.create(resolve(position), C.duty, duty, id); + const node = await rt.create(await resolve(position), C.duty, duty, id); return ok(node, "charge"); }, - "position.require"(position: string, procedure: string, id?: string): OpResult { + async "position.require"(position: string, procedure: string, id?: string): Promise { validateGherkin(procedure); - const parent = resolve(position); - if (id) removeExisting(parent, id); - const node = rt.create(parent, C.requirement, procedure, id); + 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"); }, - "position.abolish"(position: string): OpResult { - return archive(resolve(position), "abolish"); + async "position.abolish"(position: string): Promise { + return archive(await resolve(position), "abolish"); }, - "position.appoint"(position: string, individual: string): OpResult { - const posNode = resolve(position); - const indNode = resolve(individual); - rt.link(posNode, indNode, "appointment", "serve"); + 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 = rt.project(posNode); + 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 - rt.create(indNode, C.procedure, child.information, child.id); + await rt.create(indNode, C.procedure, child.information, child.id); } return ok(posNode, "appoint"); }, - "position.dismiss"(position: string, individual: string): OpResult { - const posNode = resolve(position); - rt.unlink(posNode, resolve(individual), "appointment", "serve"); + 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 ---- - "census.list"(type?: string): string { + async "census.list"(type?: string): Promise { const target = type === "past" ? past : society; - const state = rt.project(target); + const state = await rt.project(target); const children = state.children ?? []; const filtered = type === "past" diff --git a/packages/prototype/tests/ops.test.ts b/packages/prototype/tests/ops.test.ts index 8428d4d..b597959 100644 --- a/packages/prototype/tests/ops.test.ts +++ b/packages/prototype/tests/ops.test.ts @@ -7,10 +7,10 @@ import { createOps, type Ops } from "../src/ops.js"; // Test setup — pure in-memory, no platform needed // ================================================================ -function setup() { +async function setup() { const rt = createRuntime(); - const society = rt.create(null, C.society); - const past = rt.create(society, C.past); + 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; @@ -26,13 +26,13 @@ function setup() { return null; } - function find(id: string): Structure | null { - const state = rt.project(society); + async function find(id: string): Promise { + const state = await rt.project(society); return findInState(state, id.toLowerCase()); } - function resolve(id: string): Structure { - const node = find(id); + async function resolve(id: string): Promise { + const node = await find(id); if (!node) throw new Error(`"${id}" not found.`); return node; } @@ -46,9 +46,9 @@ function setup() { // ================================================================ describe("individual", () => { - test("born creates individual with identity scaffold", () => { - const { ops } = setup(); - const r = ops["individual.born"]("Feature: Sean", "sean"); + 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"); @@ -57,100 +57,100 @@ describe("individual", () => { expect(names).toContain("identity"); }); - test("born without content creates minimal individual", () => { - const { ops } = setup(); - const r = ops["individual.born"](undefined, "alice"); + 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", () => { - const { ops, find } = setup(); - ops["individual.born"]("Feature: Sean", "sean", ["姜山"]); - expect(find("姜山")).not.toBeNull(); + 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", () => { - const { ops } = setup(); - expect(() => ops["individual.born"]("not gherkin")).toThrow("Invalid Gherkin"); + 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", () => { - const { ops } = setup(); - const r = ops["individual.born"]("Feature: Sean", "sean"); + 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", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["position.establish"](undefined, "architect"); - ops["position.require"]("architect", "Feature: System design", "sys-design"); - ops["individual.train"]("sean", "Feature: System design skill", "sys-design"); + 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 = find("sean")! as unknown as State; + 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", () => { - const { ops, find } = setup(); - ops["individual.born"]("Feature: Sean", "sean"); - const r = ops["individual.retire"]("sean"); + 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 = find("sean"); + const found = await find("sean"); expect(found).not.toBeNull(); expect(found!.name).toBe("past"); }); - test("die archives individual", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "alice"); - const r = ops["individual.die"]("alice"); + 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", () => { - const { ops } = setup(); - ops["individual.born"]("Feature: Sean", "sean"); - ops["individual.retire"]("sean"); - const r = ops["individual.rehire"]("sean"); + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - const r = ops["individual.teach"]("sean", "Feature: Always test first", "test-first"); + 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", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["individual.teach"]("sean", "Feature: Version 1", "rule"); - ops["individual.teach"]("sean", "Feature: Version 2", "rule"); - const sean = find("sean")!; + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - const r = ops["individual.train"]("sean", "Feature: Code review skill", "code-review"); + 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"); @@ -162,79 +162,79 @@ describe("individual", () => { // ================================================================ describe("role: execution", () => { - test("want creates goal under individual", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - const r = ops["role.want"]("sean", "Feature: Build auth", "auth"); + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", "Feature: Auth", "auth"); - const r = ops["role.focus"]("auth"); + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", "Feature: Auth", "auth"); - const r = ops["role.plan"]("auth", "Feature: JWT strategy", "jwt"); + 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", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", "Feature: Auth", "auth"); - ops["role.plan"]("auth", "Feature: Phase 1", "phase-1"); - ops["role.plan"]("auth", "Feature: Phase 2", "phase-2", "phase-1"); + 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 = find("phase-2")! as unknown as State; + 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", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", "Feature: Auth", "auth"); - ops["role.plan"]("auth", "Feature: Plan A", "plan-a"); - ops["role.plan"]("auth", "Feature: Plan B", "plan-b", undefined, "plan-a"); + 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 = find("plan-b")! as unknown as State; + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", undefined, "g"); - ops["role.plan"]("g", undefined, "p"); - const r = ops["role.todo"]("p", "Feature: Write tests", "t1"); + 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", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", undefined, "g"); - ops["role.plan"]("g", undefined, "p"); - ops["role.todo"]("p", undefined, "t1"); + 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 = ops["role.finish"]( + const r = await ops["role.finish"]( "t1", "sean", "Feature: Task complete\n Scenario: OK\n Given done\n Then ok" @@ -242,29 +242,29 @@ describe("role: execution", () => { expect(r.state.name).toBe("encounter"); expect(r.state.id).toBe("t1-finished"); expect(r.process).toBe("finish"); - expect(find("t1")!.tag).toBe("done"); + expect((await find("t1"))!.tag).toBe("done"); }); - test("finish without encounter just tags task done", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", undefined, "g"); - ops["role.plan"]("g", undefined, "p"); - ops["role.todo"]("p", undefined, "t1"); + 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 = ops["role.finish"]("t1", "sean"); + const r = await ops["role.finish"]("t1", "sean"); expect(r.state.name).toBe("task"); expect(r.process).toBe("finish"); - expect(find("t1")!.tag).toBe("done"); + expect((await find("t1"))!.tag).toBe("done"); }); - test("complete tags plan done and creates encounter", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", "Feature: Auth", "auth"); - ops["role.plan"]("auth", "Feature: JWT", "jwt"); + 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 = ops["role.complete"]( + const r = await ops["role.complete"]( "jwt", "sean", "Feature: Done\n Scenario: OK\n Given done\n Then ok" @@ -272,16 +272,16 @@ describe("role: execution", () => { expect(r.state.name).toBe("encounter"); expect(r.state.id).toBe("jwt-completed"); expect(r.process).toBe("complete"); - expect(find("jwt")!.tag).toBe("done"); + expect((await find("jwt"))!.tag).toBe("done"); }); - test("abandon tags plan abandoned and creates encounter", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", "Feature: Auth", "auth"); - ops["role.plan"]("auth", "Feature: JWT", "jwt"); + 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 = ops["role.abandon"]( + const r = await ops["role.abandon"]( "jwt", "sean", "Feature: Abandoned\n Scenario: No time\n Given no time\n Then abandon" @@ -289,7 +289,7 @@ describe("role: execution", () => { expect(r.state.name).toBe("encounter"); expect(r.state.id).toBe("jwt-abandoned"); expect(r.process).toBe("abandon"); - expect(find("jwt")!.tag).toBe("abandoned"); + expect((await find("jwt"))!.tag).toBe("abandoned"); }); }); @@ -299,18 +299,22 @@ describe("role: execution", () => { describe("role: cognition", () => { /** Helper: born → want → plan → todo → finish with encounter. */ - function withEncounter(ops: Ops) { - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", undefined, "g"); - ops["role.plan"]("g", undefined, "p"); - ops["role.todo"]("p", undefined, "t1"); - ops["role.finish"]("t1", "sean", "Feature: Encounter\n Scenario: OK\n Given x\n Then y"); + 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", () => { - const { ops, find } = setup(); - withEncounter(ops); - const r = ops["role.reflect"]( + 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", @@ -320,23 +324,23 @@ describe("role: cognition", () => { expect(r.state.id).toBe("insight-1"); expect(r.process).toBe("reflect"); // encounter consumed - expect(find("t1-finished")).toBeNull(); + expect(await find("t1-finished")).toBeNull(); }); - test("reflect without explicit experience uses encounter content", () => { - const { ops } = setup(); - withEncounter(ops); - const r = ops["role.reflect"]("t1-finished", "sean", undefined, "exp-1"); + 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", () => { - const { ops, find } = setup(); - withEncounter(ops); - ops["role.reflect"]("t1-finished", "sean", "Feature: Insight", "exp-1"); + 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 = ops["role.realize"]( + const r = await ops["role.realize"]( "exp-1", "sean", "Feature: Always validate\n Scenario: Rule\n Given validate\n Then safe", @@ -346,15 +350,15 @@ describe("role: cognition", () => { expect(r.state.id).toBe("validate-rule"); expect(r.process).toBe("realize"); // experience consumed - expect(find("exp-1")).toBeNull(); + expect(await find("exp-1")).toBeNull(); }); - test("master from experience: experience → procedure", () => { - const { ops, find } = setup(); - withEncounter(ops); - ops["role.reflect"]("t1-finished", "sean", "Feature: Insight", "exp-1"); + 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 = ops["role.master"]( + const r = await ops["role.master"]( "sean", "Feature: JWT mastery\n Scenario: Apply\n Given jwt\n Then master", "jwt-skill", @@ -364,23 +368,23 @@ describe("role: cognition", () => { expect(r.state.id).toBe("jwt-skill"); expect(r.process).toBe("master"); // experience consumed - expect(find("exp-1")).toBeNull(); + expect(await find("exp-1")).toBeNull(); }); - test("master without experience: direct procedure creation", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - const r = ops["role.master"]("sean", "Feature: Direct skill", "direct-skill"); + 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", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.master"]("sean", "Feature: V1", "skill"); - ops["role.master"]("sean", "Feature: V2", "skill"); - const sean = find("sean")! as unknown as State; + 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"); @@ -392,30 +396,30 @@ describe("role: cognition", () => { // ================================================================ describe("role: forget", () => { - test("forget removes a node", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", "Feature: Auth", "auth"); - const r = ops["role.forget"]("auth"); + 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(find("auth")).toBeNull(); + expect(await find("auth")).toBeNull(); }); - test("forget removes node and its subtree", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", undefined, "g"); - ops["role.plan"]("g", undefined, "p"); - ops["role.todo"]("p", undefined, "t1"); - ops["role.forget"]("g"); - expect(find("g")).toBeNull(); - expect(find("p")).toBeNull(); - expect(find("t1")).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", () => { - const { ops } = setup(); - expect(() => ops["role.forget"]("nope")).toThrow(); + test("forget throws on non-existent node", async () => { + const { ops } = await setup(); + await expect(ops["role.forget"]("nope")).rejects.toThrow(); }); }); @@ -424,45 +428,45 @@ describe("role: forget", () => { // ================================================================ describe("org", () => { - test("found creates organization", () => { - const { ops } = setup(); - const r = ops["org.found"]("Feature: Deepractice", "dp"); + 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", () => { - const { ops } = setup(); - ops["org.found"](undefined, "dp"); - const r = ops["org.charter"]("dp", "Feature: Build great AI"); + 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", () => { - const { ops, find } = setup(); - ops["org.found"](undefined, "dp"); - const r = ops["org.dissolve"]("dp"); + 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(find("dp")!.name).toBe("past"); + expect((await find("dp"))!.name).toBe("past"); }); - test("hire links individual to org", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["org.found"](undefined, "dp"); - const r = ops["org.hire"]("dp", "sean"); + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["org.found"](undefined, "dp"); - ops["org.hire"]("dp", "sean"); - const r = ops["org.fire"]("dp", "sean"); + 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(); }); }); @@ -472,94 +476,94 @@ describe("org", () => { // ================================================================ describe("position", () => { - test("establish creates position", () => { - const { ops } = setup(); - const r = ops["position.establish"]("Feature: Architect", "architect"); + 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", () => { - const { ops } = setup(); - ops["position.establish"](undefined, "architect"); - const r = ops["position.charge"]("architect", "Feature: Design systems", "design"); + 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", () => { - const { ops } = setup(); - ops["position.establish"](undefined, "architect"); - const r = ops["position.require"]("architect", "Feature: System design", "sys-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", () => { - const { ops, find } = setup(); - ops["position.establish"](undefined, "architect"); - const r = ops["position.abolish"]("architect"); + 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(find("architect")!.name).toBe("past"); + expect((await find("architect"))!.name).toBe("past"); }); - test("appoint links individual to position", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["position.establish"](undefined, "architect"); - const r = ops["position.appoint"]("architect", "sean"); + 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", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["position.establish"](undefined, "architect"); - ops["position.require"]("architect", "Feature: System design", "sys-design"); - ops["position.require"]("architect", "Feature: Code review", "code-review"); - ops["position.appoint"]("architect", "sean"); + 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 = find("sean")! as unknown as State; + 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)", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["individual.train"]("sean", "Feature: System design skill", "sys-design"); - ops["position.establish"](undefined, "architect"); - ops["position.require"]("architect", "Feature: System design", "sys-design"); - ops["position.appoint"]("architect", "sean"); + 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 = find("sean")! as unknown as State; + 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", () => { - const { ops, find } = setup(); - ops["individual.born"](undefined, "sean"); - ops["position.establish"](undefined, "architect"); - ops["position.appoint"]("architect", "sean"); + 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 = find("sean")! as unknown as State; + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["position.establish"](undefined, "architect"); - ops["position.appoint"]("architect", "sean"); - const r = ops["position.dismiss"]("architect", "sean"); + 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(); }); }); @@ -569,35 +573,35 @@ describe("position", () => { // ================================================================ describe("census", () => { - test("list shows individuals and orgs", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["org.found"](undefined, "dp"); - const result = ops["census.list"](); + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["org.found"](undefined, "dp"); - const result = ops["census.list"]("individual"); + 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", () => { - const { ops } = setup(); - const result = ops["census.list"](); + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["individual.retire"]("sean"); - const result = ops["census.list"]("past"); + 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"); }); }); @@ -607,25 +611,25 @@ describe("census", () => { // ================================================================ describe("gherkin validation", () => { - test("want rejects invalid Gherkin", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - expect(() => ops["role.want"]("sean", "not gherkin")).toThrow("Invalid Gherkin"); + 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", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", undefined, "g"); - expect(() => ops["role.plan"]("g", "not gherkin")).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)", () => { - const { ops } = setup(); - ops["individual.born"](undefined, "sean"); - expect(() => ops["role.want"]("sean", undefined, "g")).not.toThrow(); - expect(() => ops["role.plan"]("g", undefined, "p")).not.toThrow(); - expect(() => ops["role.todo"]("p", undefined, "t")).not.toThrow(); + 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(); }); }); @@ -634,14 +638,14 @@ describe("gherkin validation", () => { // ================================================================ describe("error handling", () => { - test("resolve throws on non-existent id", () => { - const { ops } = setup(); - expect(() => ops["role.focus"]("no-such-goal")).toThrow('"no-such-goal" not found'); + 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", () => { - const { ops } = setup(); - expect(() => ops["role.skill"]("some-locator")).toThrow("ResourceX is not available"); + test("role.skill throws without resourcex", async () => { + const { ops } = await setup(); + await expect(ops["role.skill"]("some-locator")).rejects.toThrow("ResourceX is not available"); }); }); @@ -650,64 +654,64 @@ describe("error handling", () => { // ================================================================ describe("full lifecycle", () => { - test("born → want → plan → todo → finish → complete → reflect → realize", () => { - const { ops, find } = setup(); + test("born → want → plan → todo → finish → complete → reflect → realize", async () => { + const { ops, find } = await setup(); // Setup world - ops["individual.born"]("Feature: Sean", "sean"); - ops["org.found"]("Feature: Deepractice", "dp"); - ops["position.establish"]("Feature: Architect", "architect"); - ops["org.charter"]("dp", "Feature: Build great AI"); - ops["position.charge"]("architect", "Feature: Design systems"); - ops["org.hire"]("dp", "sean"); - ops["position.appoint"]("architect", "sean"); + 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 - ops["role.want"]("sean", "Feature: Build auth", "build-auth"); - ops["role.plan"]("build-auth", "Feature: JWT plan", "jwt-plan"); - ops["role.todo"]("jwt-plan", "Feature: Login endpoint", "login"); - ops["role.todo"]("jwt-plan", "Feature: Token refresh", "refresh"); + 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"); - ops["role.finish"]( + await ops["role.finish"]( "login", "sean", "Feature: Login done\n Scenario: OK\n Given login\n Then done" ); - ops["role.finish"]( + await ops["role.finish"]( "refresh", "sean", "Feature: Refresh done\n Scenario: OK\n Given refresh\n Then done" ); - ops["role.complete"]( + await ops["role.complete"]( "jwt-plan", "sean", "Feature: Auth plan complete\n Scenario: OK\n Given plan\n Then complete" ); // Verify tags - expect(find("login")!.tag).toBe("done"); - expect(find("refresh")!.tag).toBe("done"); - expect(find("jwt-plan")!.tag).toBe("done"); + expect((await find("login"))!.tag).toBe("done"); + expect((await find("refresh"))!.tag).toBe("done"); + expect((await find("jwt-plan"))!.tag).toBe("done"); // Cognition cycle - ops["role.reflect"]( + await ops["role.reflect"]( "login-finished", "sean", "Feature: Token insight\n Scenario: Learned\n Given token handling\n Then understand refresh", "token-exp" ); - expect(find("login-finished")).toBeNull(); + expect(await find("login-finished")).toBeNull(); - ops["role.realize"]( + await ops["role.realize"]( "token-exp", "sean", "Feature: Always validate expiry\n Scenario: Rule\n Given token\n Then validate expiry", "validate-expiry" ); - expect(find("token-exp")).toBeNull(); + expect(await find("token-exp")).toBeNull(); // Verify final state - const sean = find("sean")! as unknown as State; + const sean = (await find("sean"))! as unknown as State; const principle = (sean.children ?? []).find( (c: State) => c.name === "principle" && c.id === "validate-expiry" ); @@ -715,37 +719,37 @@ describe("full lifecycle", () => { expect(principle!.information).toContain("Always validate expiry"); }); - test("plan → abandon → reflect → master", () => { - const { ops, find } = setup(); + test("plan → abandon → reflect → master", async () => { + const { ops, find } = await setup(); - ops["individual.born"](undefined, "sean"); - ops["role.want"]("sean", "Feature: Learn Rust", "learn-rust"); - ops["role.plan"]("learn-rust", "Feature: Book approach", "book-approach"); + 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"); - ops["role.abandon"]( + await ops["role.abandon"]( "book-approach", "sean", "Feature: Too theoretical\n Scenario: Failed\n Given reading\n Then too slow" ); - expect(find("book-approach")!.tag).toBe("abandoned"); + expect((await find("book-approach"))!.tag).toBe("abandoned"); - ops["role.reflect"]( + 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" ); - ops["role.master"]( + 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(find("hands-on-exp")).toBeNull(); - const sean = find("sean")! as unknown as State; + 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" ); diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index 9f4838a..d58b09e 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -4,10 +4,10 @@ * Usage: * import { Rolex, Role, describe, hint } from "rolexjs"; * - * const rolex = createRoleX(platform); + * const rolex = await createRoleX(platform); * await rolex.genesis(); * const role = await rolex.activate("sean"); - * role.want("Feature: Ship v1", "ship-v1"); + * await role.want("Feature: Ship v1", "ship-v1"); */ // Re-export core (structures + processes) diff --git a/packages/rolexjs/src/role.ts b/packages/rolexjs/src/role.ts index c402deb..6ddcd8b 100644 --- a/packages/rolexjs/src/role.ts +++ b/packages/rolexjs/src/role.ts @@ -7,9 +7,9 @@ * * Usage: * const role = await rolex.activate("sean"); - * role.want("Feature: Ship v1", "ship-v1"); // → rendered string - * role.plan("Feature: Phase 1", "phase-1"); // → rendered string - * role.finish("write-tests", "Feature: Tests written"); + * 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"; @@ -22,7 +22,7 @@ import { render } from "./render.js"; */ export interface RolexInternal { ops: Ops; - saveCtx(ctx: RoleContext): void; + saveCtx(ctx: RoleContext): void | Promise; direct(locator: string, args?: Record): Promise; } @@ -38,8 +38,8 @@ export class Role { } /** Project the individual's full state tree (used after activate). */ - project(): string { - const result = this.api.ops["role.focus"](this.roleId); + 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) => @@ -63,49 +63,55 @@ export class Role { }); } - private save(): void { - this.api.saveCtx(this.ctx); + private async save(): Promise { + await this.api.saveCtx(this.ctx); } // ---- Execution ---- /** Focus: view or switch focused goal. */ - focus(goal?: string): string { + 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 = this.api.ops["role.focus"](goalId); - this.save(); + const result = await this.api.ops["role.focus"](goalId); + await this.save(); return this.fmt("focus", goalId, result); } /** Want: declare a goal. */ - want(goal?: string, id?: string, alias?: readonly string[]): string { - const result = this.api.ops["role.want"](this.roleId, goal, id, alias); + 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; - this.save(); + await this.save(); return this.fmt("want", id ?? this.roleId, result); } /** Plan: create a plan for the focused goal. */ - plan(plan?: string, id?: string, after?: string, fallback?: string): string { - const result = this.api.ops["role.plan"](this.ctx.requireGoalId(), plan, id, after, fallback); + 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; - this.save(); + await this.save(); return this.fmt("plan", id ?? "plan", result); } /** Todo: add a task to the focused plan. */ - todo(task?: string, id?: string, alias?: readonly string[]): string { - const result = this.api.ops["role.todo"](this.ctx.requirePlanId(), task, id, alias); + 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. */ - finish(task: string, encounter?: string): string { - const result = this.api.ops["role.finish"](task, this.roleId, 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); } @@ -113,38 +119,38 @@ export class Role { } /** Complete: close a plan as done, record encounter. */ - complete(plan?: string, encounter?: string): string { + async complete(plan?: string, encounter?: string): Promise { const planId = plan ?? this.ctx.requirePlanId(); - const result = this.api.ops["role.complete"](planId, this.roleId, encounter); + 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; - this.save(); + await this.save(); return this.fmt("complete", planId, result); } /** Abandon: drop a plan, record encounter. */ - abandon(plan?: string, encounter?: string): string { + async abandon(plan?: string, encounter?: string): Promise { const planId = plan ?? this.ctx.requirePlanId(); - const result = this.api.ops["role.abandon"](planId, this.roleId, encounter); + 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; - this.save(); + await this.save(); return this.fmt("abandon", planId, result); } // ---- Cognition ---- /** Reflect: consume encounters → experience. Empty encounters = direct creation. */ - reflect(encounters: string[], experience?: string, id?: string): string { + 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 = this.api.ops["role.reflect"](first, this.roleId, experience, id); + 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++) { - this.api.ops["role.forget"](encounters[i]); + await this.api.ops["role.forget"](encounters[i]); } if (encounters.length > 0) { this.ctx.consumeEncounters(encounters); @@ -154,16 +160,16 @@ export class Role { } /** Realize: consume experiences → principle. Empty experiences = direct creation. */ - realize(experiences: string[], principle?: string, id?: string): string { + 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 = this.api.ops["role.realize"](first, this.roleId, principle, id); + 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++) { - this.api.ops["role.forget"](experiences[i]); + await this.api.ops["role.forget"](experiences[i]); } if (experiences.length > 0) { this.ctx.consumeExperiences(experiences); @@ -172,17 +178,17 @@ export class Role { } /** Master: create procedure, optionally consuming experiences. */ - master(procedure: string, id?: string, experiences?: string[]): string { + 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 = this.api.ops["role.master"](this.roleId, procedure, id, first); + 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++) { - this.api.ops["role.forget"](experiences[i]); + await this.api.ops["role.forget"](experiences[i]); } this.ctx.consumeExperiences(experiences); } @@ -192,19 +198,19 @@ export class Role { // ---- Knowledge management ---- /** Forget: remove any node under the individual by id. */ - forget(nodeId: string): string { - const result = this.api.ops["role.forget"](nodeId); + 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; - this.save(); + await this.save(); return this.fmt("forget", nodeId, result); } // ---- Skills + unified entry ---- /** Skill: load full skill content by locator. */ - skill(locator: string): Promise { - return this.api.ops["role.skill"](locator); + async skill(locator: string): Promise { + return await this.api.ops["role.skill"](locator); } /** Use: subjective execution — `!ns.method` or ResourceX locator. */ diff --git a/packages/rolexjs/src/rolex.ts b/packages/rolexjs/src/rolex.ts index b9e0b7a..37bf531 100644 --- a/packages/rolexjs/src/rolex.ts +++ b/packages/rolexjs/src/rolex.ts @@ -29,16 +29,16 @@ export interface CensusEntry { export class Rolex { private rt: Runtime; - private ops: Ops; + private ops!: Ops; private resourcex?: ResourceX; private repo: RoleXRepository; private readonly initializer?: Initializer; private readonly bootstrap: readonly string[]; - private readonly society: Structure; - private readonly past: Structure; + private society!: Structure; + private past!: Structure; - constructor(platform: Platform) { + private constructor(platform: Platform) { this.repo = platform.repository; this.rt = this.repo.runtime; this.initializer = platform.initializer; @@ -49,22 +49,33 @@ export class Rolex { setProvider(platform.resourcexProvider); this.resourcex = createResourceX(); } + } + + /** 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; + } + /** Async initialization — called by Rolex.create(). */ + private async init(): Promise { // Ensure world roots exist - const roots = this.rt.roots(); - this.society = roots.find((r) => r.name === "society") ?? this.rt.create(null, C.society); + const roots = await this.rt.roots(); + this.society = + roots.find((r) => r.name === "society") ?? (await this.rt.create(null, C.society)); - const societyState = this.rt.project(this.society); + const societyState = await this.rt.project(this.society); const existingPast = societyState.children?.find((c) => c.name === "past"); - this.past = existingPast ?? this.rt.create(this.society, C.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: (id: string) => { - const node = this.find(id); + resolve: async (id: string) => { + const node = await this.find(id); if (!node) throw new Error(`"${id}" not found.`); return node; }, @@ -91,27 +102,27 @@ export class Rolex { * auto-born the individual first. */ async activate(individual: string): Promise { - let node = this.find(individual); + let node = await this.find(individual); if (!node) { const hasProto = Object.hasOwn(this.repo.prototype.list(), individual); if (hasProto) { - this.ops["individual.born"](undefined, individual); - node = this.find(individual)!; + await this.ops["individual.born"](undefined, individual); + node = (await this.find(individual))!; } else { throw new Error(`"${individual}" not found.`); } } - const state = this.rt.project(node); + 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 = this.repo.loadContext(individual); + const persisted = await this.repo.loadContext(individual); if (persisted) { - if (persisted.focusedGoalId && this.find(persisted.focusedGoalId)) { + if (persisted.focusedGoalId && (await this.find(persisted.focusedGoalId))) { ctx.focusedGoalId = persisted.focusedGoalId; } - if (persisted.focusedPlanId && this.find(persisted.focusedPlanId)) { + if (persisted.focusedPlanId && (await this.find(persisted.focusedPlanId))) { ctx.focusedPlanId = persisted.focusedPlanId; } } @@ -119,8 +130,8 @@ export class Rolex { // Build internal API for Role — ops + ctx persistence const ops = this.ops; const repo = this.repo; - const saveCtx = (c: RoleContext) => { - repo.saveContext(c.roleId, { + const saveCtx = async (c: RoleContext) => { + await repo.saveContext(c.roleId, { focusedGoalId: c.focusedGoalId, focusedPlanId: c.focusedPlanId, }); @@ -136,8 +147,8 @@ export class Rolex { } /** Find a node by id or alias across the entire society tree. Internal use only. */ - private find(id: string): Structure | null { - const state = this.rt.project(this.society); + private async find(id: string): Promise { + const state = await this.rt.project(this.society); return findInState(state, id); } @@ -160,7 +171,7 @@ export class Rolex { hint ); } - return fn(...toArgs(command, args ?? {})) as T; + return (await fn(...toArgs(command, args ?? {}))) as T; } if (!this.resourcex) throw new Error("ResourceX is not available."); return this.resourcex.ingest(locator, args); @@ -168,6 +179,6 @@ export class Rolex { } /** 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 index 53bae02..b1f48b1 100644 --- a/packages/rolexjs/tests/context.test.ts +++ b/packages/rolexjs/tests/context.test.ts @@ -5,20 +5,20 @@ import { join } from "node:path"; import { localPlatform } from "@rolexjs/local-platform"; import { createRoleX, RoleContext } from "../src/index.js"; -function setup() { - return createRoleX(localPlatform({ dataDir: null })); +async function setup() { + return await createRoleX(localPlatform({ dataDir: null })); } -function setupWithDir() { +async function setupWithDir() { const dataDir = join(tmpdir(), `rolex-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(dataDir, { recursive: true }); - const rolex = createRoleX(localPlatform({ dataDir, resourceDir: null })); + const rolex = await createRoleX(localPlatform({ dataDir, resourceDir: null })); return { rolex, dataDir }; } describe("Role (ctx management)", () => { test("activate returns Role with ctx", async () => { - const rolex = setup(); + 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); @@ -26,37 +26,37 @@ describe("Role (ctx management)", () => { }); test("want updates ctx.focusedGoalId", async () => { - const rolex = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - const result = role.want("Feature: Build auth", "build-auth"); + 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 = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - role.want("Feature: Auth", "auth-goal"); - const result = role.plan("Feature: JWT strategy", "jwt-plan"); + 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 = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - role.want("Feature: Auth", "auth"); - role.plan("Feature: JWT", "jwt"); - role.todo("Feature: Login", "login"); + await role.want("Feature: Auth", "auth"); + await role.plan("Feature: JWT", "jwt"); + await role.todo("Feature: Login", "login"); - const result = role.finish( + const result = await role.finish( "login", "Feature: Login done\n Scenario: OK\n Given login\n Then success" ); @@ -65,27 +65,27 @@ describe("Role (ctx management)", () => { }); test("finish without encounter does not register in ctx", async () => { - const rolex = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - role.want("Feature: Auth", "auth"); - role.plan("Feature: JWT", "jwt"); - role.todo("Feature: Login", "login"); + await role.want("Feature: Auth", "auth"); + await role.plan("Feature: JWT", "jwt"); + await role.todo("Feature: Login", "login"); - role.finish("login"); + await role.finish("login"); expect(role.ctx.encounterIds.size).toBe(0); }); test("complete registers encounter and clears focusedPlanId", async () => { - const rolex = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - role.want("Feature: Auth", "auth"); - role.plan("Feature: JWT", "jwt"); + await role.want("Feature: Auth", "auth"); + await role.plan("Feature: JWT", "jwt"); - const result = role.complete( + const result = await role.complete( "jwt", "Feature: JWT done\n Scenario: OK\n Given jwt\n Then done" ); @@ -95,18 +95,18 @@ describe("Role (ctx management)", () => { }); test("reflect consumes encounter and adds experience in ctx", async () => { - const rolex = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - role.want("Feature: Auth", "auth"); - role.plan("Feature: JWT", "jwt"); - role.todo("Feature: Login", "login"); - role.finish("login", "Feature: Login done\n Scenario: OK\n Given x\n Then y"); + 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); - role.reflect( + await role.reflect( ["login-finished"], "Feature: Token insight\n Scenario: OK\n Given x\n Then y", "token-insight" @@ -117,11 +117,11 @@ describe("Role (ctx management)", () => { }); test("reflect without encounter creates experience directly", async () => { - const rolex = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - const result = role.reflect( + const result = await role.reflect( [], "Feature: Direct insight\n Scenario: OK\n Given learned from conversation", "conv-insight" @@ -133,11 +133,11 @@ describe("Role (ctx management)", () => { }); test("realize without experience creates principle directly", async () => { - const rolex = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - const result = role.realize( + const result = await role.realize( [], "Feature: Direct principle\n Scenario: OK\n Given always blame the product", "product-first" @@ -148,16 +148,20 @@ describe("Role (ctx management)", () => { }); test("realize still consumes experience when provided", async () => { - const rolex = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); // Create experience directly - role.reflect([], "Feature: Insight\n Scenario: OK\n Given something learned", "my-insight"); + 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 - role.realize( + await role.realize( ["my-insight"], "Feature: Principle\n Scenario: OK\n Given a general truth", "my-principle" @@ -187,20 +191,20 @@ describe("Role context persistence", () => { dirs.length = 0; }); - function persistent() { - const { rolex, dataDir } = setupWithDir(); + async function persistent() { + const { rolex, dataDir } = await setupWithDir(); dirs.push(dataDir); return { rolex, dataDir }; } test("activate restores persisted focusedGoalId and focusedPlanId", async () => { - const { rolex } = persistent(); + const { rolex } = await persistent(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); // Session 1: set focus const role1 = await rolex.activate("sean"); - role1.want("Feature: Auth", "auth"); - role1.plan("Feature: JWT", "jwt"); + await role1.want("Feature: Auth", "auth"); + await role1.plan("Feature: JWT", "jwt"); expect(role1.ctx.focusedGoalId).toBe("auth"); expect(role1.ctx.focusedPlanId).toBe("jwt"); @@ -211,7 +215,7 @@ describe("Role context persistence", () => { }); test("activate without persisted context uses rehydrate default", async () => { - const { rolex } = persistent(); + 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" }); @@ -221,14 +225,14 @@ describe("Role context persistence", () => { }); test("focus saves updated context", async () => { - const { rolex } = persistent(); + const { rolex } = await persistent(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - role.want("Feature: Goal A", "goal-a"); - role.want("Feature: Goal B", "goal-b"); + await role.want("Feature: Goal A", "goal-a"); + await role.want("Feature: Goal B", "goal-b"); - role.focus("goal-a"); + await role.focus("goal-a"); // Re-activate to verify persistence const role2 = await rolex.activate("sean"); @@ -237,13 +241,13 @@ describe("Role context persistence", () => { }); test("complete clears focusedPlanId and saves", async () => { - const { rolex } = persistent(); + const { rolex } = await persistent(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - role.want("Feature: Auth", "auth"); - role.plan("Feature: JWT", "jwt"); - role.complete("jwt", "Feature: Done\n Scenario: OK\n Given done\n Then ok"); + 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"); @@ -252,15 +256,15 @@ describe("Role context persistence", () => { }); test("different roles have independent contexts", async () => { - const { rolex } = persistent(); + 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"); - seanRole.want("Feature: Sean Goal", "sean-goal"); + await seanRole.want("Feature: Sean Goal", "sean-goal"); const nuwaRole = await rolex.activate("nuwa"); - nuwaRole.want("Feature: Nuwa Goal", "nuwa-goal"); + 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"); @@ -268,10 +272,10 @@ describe("Role context persistence", () => { }); test("in-memory mode (dataDir: null) works without persistence", async () => { - const rolex = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean" }); const role = await rolex.activate("sean"); - role.want("Feature: Auth", "auth"); + 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 e6b73ff..02137f5 100644 --- a/packages/rolexjs/tests/rolex.test.ts +++ b/packages/rolexjs/tests/rolex.test.ts @@ -7,8 +7,8 @@ import type { OpResult } from "@rolexjs/prototype"; import { createRoleX } from "../src/index.js"; import { describe as renderDescribe, hint as renderHint, renderState } from "../src/render.js"; -function setup() { - return createRoleX(localPlatform({ dataDir: null })); +async function setup() { + return await createRoleX(localPlatform({ dataDir: null })); } // ================================================================ @@ -17,7 +17,7 @@ function setup() { describe("use dispatch", () => { test("!individual.born creates individual", async () => { - const rolex = setup(); + const rolex = await setup(); const r = await rolex.direct("!individual.born", { content: "Feature: Sean", id: "sean", @@ -30,7 +30,7 @@ describe("use dispatch", () => { }); test("chained operations via use", async () => { - const rolex = setup(); + 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", { @@ -42,7 +42,7 @@ describe("use dispatch", () => { }); test("!census.list returns text", async () => { - const rolex = setup(); + 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"); @@ -50,14 +50,14 @@ describe("use dispatch", () => { expect(result).toContain("dp"); }); - test("throws on unknown command", () => { - const rolex = setup(); - expect(() => rolex.direct("!foo.bar")).toThrow(); + test("throws on unknown command", async () => { + const rolex = await setup(); + expect(rolex.direct("!foo.bar")).rejects.toThrow(); }); - test("throws on unknown method", () => { - const rolex = setup(); - expect(() => rolex.direct("!org.nope")).toThrow(); + test("throws on unknown method", async () => { + const rolex = await setup(); + expect(rolex.direct("!org.nope")).rejects.toThrow(); }); }); @@ -67,7 +67,7 @@ describe("use dispatch", () => { describe("activate", () => { test("returns Role with ctx", async () => { - const rolex = setup(); + 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"); @@ -75,26 +75,26 @@ describe("activate", () => { }); test("throws on non-existent individual", async () => { - const rolex = setup(); + const rolex = await setup(); expect(rolex.activate("nobody")).rejects.toThrow('"nobody" not found'); }); test("Role.want/plan/todo/finish work through Role API", async () => { - const rolex = setup(); + const rolex = await setup(); await rolex.direct("!individual.born", { id: "sean" }); const role = await rolex.activate("sean"); - const wantR = role.want("Feature: Auth", "auth"); + const wantR = await role.want("Feature: Auth", "auth"); expect(wantR).toContain('Goal "auth" declared.'); expect(wantR).toContain("[goal]"); - const planR = role.plan("Feature: JWT", "jwt"); + const planR = await role.plan("Feature: JWT", "jwt"); expect(planR).toContain("[plan]"); - const todoR = role.todo("Feature: Login", "login"); + const todoR = await role.todo("Feature: Login", "login"); expect(todoR).toContain("[task]"); - const finishR = role.finish( + const finishR = await role.finish( "login", "Feature: Done\n Scenario: OK\n Given done\n Then ok" ); @@ -102,7 +102,7 @@ describe("activate", () => { }); test("Role.use delegates to Rolex.use", async () => { - const rolex = setup(); + 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" }); @@ -116,7 +116,7 @@ describe("activate", () => { describe("render", () => { test("describe generates text with name", async () => { - const rolex = setup(); + const rolex = await setup(); const r = await rolex.direct("!individual.born", { id: "sean" }); const text = renderDescribe("born", "sean", r.state); expect(text).toContain("sean"); @@ -128,7 +128,7 @@ describe("render", () => { }); test("renderState renders individual with heading", async () => { - const rolex = setup(); + const rolex = await setup(); const r = await rolex.direct("!individual.born", { content: "Feature: I am Sean\n An AI role.", id: "sean", @@ -139,7 +139,7 @@ describe("render", () => { }); test("renderState renders nested structure", async () => { - const rolex = setup(); + 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" }); @@ -182,16 +182,18 @@ describe("render", () => { // ================================================================ describe("gherkin validation", () => { - test("rejects non-Gherkin input", () => { - const rolex = setup(); - expect(() => rolex.direct("!individual.born", { content: "not gherkin" })).toThrow( + test("rejects non-Gherkin input", async () => { + const rolex = await setup(); + expect(rolex.direct("!individual.born", { content: "not gherkin" })).rejects.toThrow( "Invalid Gherkin" ); }); - test("accepts valid Gherkin", () => { - const rolex = setup(); - expect(() => rolex.direct("!individual.born", { content: "Feature: Sean" })).not.toThrow(); + test("accepts valid Gherkin", async () => { + const rolex = await setup(); + await expect( + rolex.direct("!individual.born", { content: "Feature: Sean" }) + ).resolves.toBeDefined(); }); }); @@ -206,12 +208,12 @@ describe("persistent mode", () => { if (existsSync(testDir)) rmSync(testDir, { recursive: true }); }); - function persistentSetup() { - return createRoleX(localPlatform({ dataDir: testDir, resourceDir: null })); + async function persistentSetup() { + return await createRoleX(localPlatform({ dataDir: testDir, resourceDir: null })); } test("born → retire round-trip", async () => { - const rolex = persistentSetup(); + 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"); @@ -219,11 +221,11 @@ describe("persistent mode", () => { }); test("archived entity survives cross-instance reload", async () => { - const rolex1 = persistentSetup(); + const rolex1 = await persistentSetup(); await rolex1.direct("!individual.born", { content: "Feature: Test", id: "test-ind" }); await rolex1.direct("!individual.retire", { individual: "test-ind" }); - const rolex2 = persistentSetup(); + 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/runtime.ts b/packages/system/src/runtime.ts index 588d7d1..a9f24f0 100644 --- a/packages/system/src/runtime.ts +++ b/packages/system/src/runtime.ts @@ -25,28 +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; /** Move a node to target structure's branch, preserving its subtree. Updates type and optionally information. */ - transform(source: Structure, target: Structure, information?: string): Structure; + 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): void; + 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 ===== @@ -151,7 +151,7 @@ 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()) { @@ -163,7 +163,7 @@ export const createRuntime = (): Runtime => { 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; @@ -178,7 +178,7 @@ 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}`); @@ -216,7 +216,7 @@ export const createRuntime = (): Runtime => { 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"); @@ -235,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 @@ -259,21 +259,21 @@ export const createRuntime = (): Runtime => { } }, - tag(node, tagValue) { + 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; }, - project(node) { + 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/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"); From 4c0bea48dbe4885dee1bed1bfb3cd60509175040 Mon Sep 17 00:00:00 2001 From: sean Date: Tue, 3 Mar 2026 12:44:50 +0800 Subject: [PATCH 54/54] feat: add version-migration skill for legacy data migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add skill to guide Nuwa through migrating pre-1.0 RoleX data (~/.rolex) to the new format. Covers individuals, knowledge, organizations, and assignments. - skills/version-migration/SKILL.md — 7-step migration process - genesis prototype updated to train Nuwa with the procedure - Published to registry.deepractice.dev Co-Authored-By: Claude Opus 4.6 --- packages/genesis/prototype.json | 8 + .../version-migration.procedure.feature | 8 + skills/version-migration/SKILL.md | 242 ++++++++++++++++++ skills/version-migration/resource.json | 6 + 4 files changed, 264 insertions(+) create mode 100644 packages/genesis/version-migration.procedure.feature create mode 100644 skills/version-migration/SKILL.md create mode 100644 skills/version-migration/resource.json diff --git a/packages/genesis/prototype.json b/packages/genesis/prototype.json index 0183c4d..70b443c 100644 --- a/packages/genesis/prototype.json +++ b/packages/genesis/prototype.json @@ -36,6 +36,14 @@ "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" } }, 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/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.. + New: use("!org.hire", { org: "", individual: "" }) + use("!position.appoint", { position: "", individual: "" }) + """ + +Feature: Edge Cases and Troubleshooting + + Scenario: Role already exists in new version + Given an individual with the same id already exists + When born is called with the same id + Then it will fail with a duplicate id error + And skip this individual and inform the user + And suggest using teach to update knowledge if needed + + Scenario: Empty goals directory + Given a role's goals/ directory is empty + Then skip goal migration for this role + And this is normal — many roles start without goals + + Scenario: Unknown file patterns + Given files don't match expected naming conventions + Then log the unrecognized files for user review + And do not attempt to auto-migrate unknown formats + And present them to the user for manual decision + + Scenario: Custom data directory + Given the user's legacy data is not at ~/.rolex + When the user provides a custom path + Then use that path instead of ~/.rolex + And all other steps remain the same diff --git a/skills/version-migration/resource.json b/skills/version-migration/resource.json new file mode 100644 index 0000000..283b5a9 --- /dev/null +++ b/skills/version-migration/resource.json @@ -0,0 +1,6 @@ +{ + "name": "version-migration", + "type": "skill", + "author": "deepractice", + "description": "Migrate legacy RoleX data (~/.rolex) to RoleX 1.0 format" +}