unoverse 0.1.44 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/unoverse.mjs CHANGED
@@ -123,13 +123,8 @@ switch (cmd) {
123
123
  // Runs AS the new version, in the developer's cwd. Outside a universe there is
124
124
  // nothing to refresh, and silence is the right answer.
125
125
  if (UNIVERSE) {
126
- const skillSrc = join(dirname(fileURLToPath(import.meta.url)), "../operator/skills/unoverse-create");
127
- if (existsSync(skillSrc)) {
128
- const { cpSync, mkdirSync } = await import("node:fs");
129
- mkdirSync(join(UNIVERSE.root, ".claude/skills"), { recursive: true });
130
- cpSync(skillSrc, join(UNIVERSE.root, ".claude/skills/unoverse-create"), { recursive: true });
131
- console.log(" ✓ authoring skill refreshed");
132
- }
126
+ // The authoring skill is Studio's, not the universe's; update touches only what
127
+ // a universe actually has: this CLI and the platform images.
133
128
  operator(UNIVERSE, ["refresh-images"]);
134
129
  }
135
130
  process.exit(0);
package/lib/create.mjs CHANGED
@@ -324,17 +324,10 @@ export async function create(nameArg) {
324
324
  const operatorScript = existsSync(vendored)
325
325
  ? vendored
326
326
  : resolve(here, "../../../scripts/operator.sh");
327
- // The authoring skill, so Claude Code works in the new folder immediately. The
328
- // starter tarball does not carry it; the CLI does, and `unoverse update` refreshes it.
329
- const skillSrc = resolve(here, "../operator/skills/unoverse-create");
330
- const skillFallback = resolve(here, "../../../.claude/skills/unoverse-create");
331
- const skill = existsSync(skillSrc) ? skillSrc : skillFallback;
332
- if (existsSync(skill)) {
333
- const { cpSync } = await import("node:fs");
334
- mkdirSync(`${name}/.claude/skills`, { recursive: true });
335
- cpSync(skill, `${name}/.claude/skills/unoverse-create`, { recursive: true });
336
- }
337
-
327
+ // NO authoring skill here, deliberately. Building components, nodes and agent
328
+ // skills is STUDIO work and its skill ships with Studio projects. What a universe
329
+ // offers Claude Code is the canvas MCP (.mcp.json in the scaffold): building and
330
+ // managing workflows on this universe's Canvas.
338
331
  const r = spawnSync("bash", [operatorScript, "init"], {
339
332
  stdio: "inherit",
340
333
  cwd: name,
@@ -349,11 +342,11 @@ export async function create(nameArg) {
349
342
  cd ${name}`}
350
343
  unoverse start
351
344
 
352
- ${cyan("Build with Claude:")} this folder is ready for Claude Code. Open it
353
- there and describe what you want, in plain words. Claude already knows how
354
- to build unoverse components, custom nodes, agent skills, and workflows on
355
- your Canvas.
345
+ ${cyan("Build with Claude:")} open this folder in Claude Code and describe the
346
+ workflow you want. Claude builds and manages workflows live on this
347
+ universe's Canvas.
356
348
 
349
+ ${dim("Components, nodes and agent skills are built in Studio: unoverse studio")}
357
350
  ${dim("How it works: https://github.com/unoverse-platform/docs")}
358
351
  `);
359
352
  return;
@@ -22,6 +22,7 @@ cmd_help() {
22
22
  echo -e " ${GREEN}stop${NC} Stop it"
23
23
  echo -e " ${GREEN}check${NC} Is it healthy ${DIM}(services, schema, environment)${NC}"
24
24
  echo -e " ${GREEN}logs${NC} What is it doing ${DIM}(unoverse logs <service> for one)${NC}"
25
+ echo -e " ${GREEN}ground${NC} Prepare a server in your cloud ${DIM}(unoverse ground do|aws, prefills Terraform)${NC}"
25
26
  echo -e " ${GREEN}deploy${NC} Ship it to your server ${DIM}(images + your work + migrations)${NC}"
26
27
  echo ""
27
28
  # Owner-only lane. Printed ONLY when publish.sh is present, so a starter kit never
@@ -29,7 +30,6 @@ cmd_help() {
29
30
  if type cmd_publish >/dev/null 2>&1; then
30
31
  echo -e " ${BOLD}${DIM}Platform owner${NC}"
31
32
  echo -e " ${GREEN}publish${NC} Full release. Every lane whose content changed"
32
- echo -e " ${GREEN}ground${NC} Prefill terraform.tfvars from your cloud CLI ${DIM}(do|aws)${NC}"
33
33
  echo -e " ${GREEN}dev${NC} Run the monorepo locally"
34
34
  echo ""
35
35
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "description": "The Unoverse front door — create a Studio project, a universe, or a client app, and launch Studio.",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "type": "module",
@@ -1,88 +0,0 @@
1
- ---
2
- name: unoverse-create
3
- description: Create or edit Unoverse platform artifacts — UI components (including briefed/AI-filled components), templates, atoms and styles (rx/), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use when the user wants to create, add, build, or modify a component, template, theme, agent skill, prompt block, custom node, or workflow in an Unoverse starter repo.
4
- ---
5
-
6
- # Creating Unoverse Artifacts
7
-
8
- You are helping a developer build in the **Unoverse developer carve-out** — the three
9
- editable folders mounted into the running platform:
10
-
11
- | Folder | What lives there |
12
- |---|---|
13
- | `apps/unoverse/rx/` | **Design** (data, not code): components, atoms, org-scoped templates + styles |
14
- | `apps/unoverse/prompts/` | **Behavior**: agent skills (`skills/`) + prompt blocks (`blocks/`) |
15
- | `apps/unoverse/nodes/` | **Logic**: custom workflow nodes, authored as YAML |
16
-
17
- ## Step 1 — Identify the artifact, read its playbook
18
-
19
- Decide what the user is creating, then **read the matching reference file before writing
20
- anything**:
21
-
22
- | The user wants… | Read |
23
- |---|---|
24
- | A UI component or shared atom — incl. a page/card the AI fills with content (briefed) | `references/component.md` (§Briefs for AI-filled) |
25
- | A template / app layout (chat surface, wizard shell) | `references/template.md` |
26
- | A style / theme / token change | `references/component.md` §Tokens |
27
- | An agent skill (behavior guide the AI follows) | `references/agent-skill.md` |
28
- | A prompt block (reusable prompt fragment) | `references/block.md` |
29
- | A custom workflow node (integration, tool, logic) | `references/node.md` |
30
- | A workflow (wire nodes into an agent/pipeline on the Canvas) | `references/workflow.md` |
31
-
32
- Workflows are the one artifact that is NOT files: they are built live on the running
33
- platform through the `canvas` MCP (registered by this repo's `.mcp.json`) —
34
- the playbook covers the bind/build/test contract.
35
-
36
- If it's ambiguous (e.g. "add a card that shows weather"), it's usually a **component**
37
- (the UI) plus possibly a **node** (the data source) — confirm the scope with the user.
38
-
39
- **New to design artifacts?** The guided learning journey at `docs/design/` (README →
40
- quick-start → … → troubleshooting) teaches components, templates, the state model
41
- (incl. locked voice/native state), tokens, Studio, and validation in order;
42
- `docs/design/CLAUDE.md` is the condensed agent rulebook. The playbooks here assume that
43
- material.
44
-
45
- ## Step 2 — Hard rules (apply to every artifact)
46
-
47
- 1. **Only edit the three carve-out folders.** Never touch, vendor, or work around the
48
- SDK, engine, or server — they are not yours to change. If a task seems to require an
49
- SDK change, the answer is almost always "express it as data" — re-read the playbook.
50
- 2. **Docs are the source of truth.** The playbooks name the design journey and the deep
51
- references ([UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md), [UNOVERSE_STATE_MODEL](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_STATE_MODEL.md), [UNOVERSE_LAYERS](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_LAYERS.md),
52
- [UNOVERSE_CONFORMANCE](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_CONFORMANCE.md)). They live in the docs repo
53
- (https://github.com/unoverse-platform/docs), not in your repo. When the user's request goes beyond the playbook, read the named section rather
54
- than improvising conventions.
55
- 3. **UI is data.** No pixels, hex colors, or CSS anywhere — token names only. No logic
56
- in definitions — anything computed (totals, formatting, chosen colors) is computed in
57
- the node and sent as a plain field.
58
- 4. **Audit before you're done.** Every component/template must pass the conformance
59
- checklist ([UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §9). Walk it item by item.
60
- 5. **Discovery meta is ranked, not read.** Any spatially-discoverable artifact — a
61
- component or template with a manifest, an agent skill, a node — is selected by
62
- embedding `title`/`name` + `whenToUse`/`description` + `category` against the user's
63
- own words (or the planner's task). The strict contract is
64
- `docs/nodes/node-discoverability.md`: outcome-first, utterance-shaped, opening
65
- words dominate, disqualify by property (never name a sibling). Each playbook carries
66
- the summary; read doc 14 before writing any meta.
67
-
68
- ## Step 3 — Deploy loop (after the artifact is written)
69
-
70
- Components and templates have NO scaffold command — author them from the references
71
- (mirror the closest existing artifact). Validation lives in Studio: its publish step
72
- lints every definition before anything leaves the machine (0 errors required, doc-cited
73
- messages; justify any warning), and the conformance checklist is
74
- [UNOVERSE_CONFORMANCE](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_CONFORMANCE.md).
75
-
76
- | Artifact | To see it live |
77
- |---|---|
78
- | Component / atom / template / style | Preview in Studio (`unoverse studio`), then publish from Studio — the publish lint gates it |
79
- | Existing component restyle/edit only | takes effect live (the SDK reads `rx/` directly) |
80
- | Agent skill / prompt block | publish from Studio, same gate |
81
- | Node | Studio's Nodes screen: Load sample → Run. A node is YAML; running it is the only proof (no build) |
82
-
83
- Verify the platform with `unoverse check` (services, schema, environment). Studio opens
84
- on :4108 (`unoverse studio` from the project folder).
85
-
86
- > In the platform monorepo (not the starter), the node docs live at
87
- > `packages/docs/nodes/`, the design journey at `packages/docs/design/`, and the dev loop
88
- > is just `npm run dev` — component nodes synthesize from rx/ definitions at boot.
@@ -1,65 +0,0 @@
1
- # Playbook — Agent Skills (prompts/skills)
2
-
3
- An agent skill is a markdown behavior guide the platform's AI agents discover and follow
4
- at runtime (e.g. how to handle complaints, how to walk a user through a process). It is
5
- **prose for an agent**, not code.
6
-
7
- ## Where files go
8
-
9
- ```
10
- apps/unoverse/prompts/skills/<skill-name>/
11
- SKILL.md # required — frontmatter + instructions
12
- ```
13
-
14
- The runtime scans this folder and serves skills to agents; agents match on the
15
- `description` and `triggers`.
16
-
17
- ## SKILL.md format
18
-
19
- ```markdown
20
- ---
21
- name: skill-name # required: lowercase, numbers, hyphens; max 64 chars
22
- title: Skill Name # human display name — shows as the item's title in listings
23
- description: What this skill IS — ONE short line (≤ 120 chars), the listing subtitle
24
- whenToUse: The SELECTION text — outcome-first, in the user's vocabulary; ranked against intent for spatial discovery. See docs/nodes/node-discoverability.md — the node rules apply verbatim.
25
- version: 1.0.0 # optional, semver
26
- category: ai # optional: ai | integration | workflow | utility
27
- triggers: # optional, max 20 keywords/phrases (2–50 chars each)
28
- - keyword or phrase
29
- credentials: [] # optional: credential types the skill's flow needs
30
- nodeTypes: [] # optional: workflow node types the skill relies on
31
- ---
32
-
33
- # Skill Name
34
-
35
- ## When to Use This Skill
36
- [The situations that should activate this behavior]
37
-
38
- ## Approach / Instructions
39
- [Clear, step-by-step guidance. Concrete do/don't phrasing beats abstractions —
40
- include example lines the agent can use verbatim where tone matters.]
41
-
42
- ## Examples
43
- [Concrete before/after or dialogue examples]
44
- ```
45
-
46
- Stick to the keys above. The markdown body must exist — a skill with frontmatter only
47
- is invalid.
48
-
49
- ## Writing rules
50
-
51
- 1. **`whenToUse` does the routing; `description` says what it is.** Never blend them
52
- into one field. `whenToUse` is embedded and ranked against user intent (spatial
53
- discovery / findIntent) — write it **outcome-first in the user's vocabulary**
54
- ("Handle an unhappy customer — a complaint, frustration, or a request to
55
- escalate…"), never mechanism-first. The full rules are
56
- `docs/nodes/node-discoverability.md` — they apply to skills verbatim.
57
- 2. **Study an existing skill** in `prompts/skills/` and match its voice and structure.
58
- 3. Keep instructions outcome-focused and scannable: short sections, bulleted do/don't
59
- lists, explicit escalation/stop conditions.
60
- 4. One skill = one behavior. If you're writing "and also…", split it.
61
-
62
- ## Ship
63
-
64
- `docker compose restart unoverse` — the runtime rescans skills at boot. Verify the
65
- skill appears via the platform's skill listing (`unoverse check` for overall health).
@@ -1,43 +0,0 @@
1
- # Playbook — Prompt Blocks (prompts/blocks)
2
-
3
- A prompt block is a **reusable prompt fragment** — a named piece of agent prompt text
4
- (formatting rules, a reasoning pattern, a policy) that workflows compose into agent
5
- system prompts. Smaller than a skill: a skill is a discoverable behavior guide; a block
6
- is an ingredient.
7
-
8
- ## Where files go
9
-
10
- ```
11
- apps/unoverse/prompts/blocks/<category>/<block-name>.md
12
- ```
13
-
14
- The immediate subdirectory is the block's category (existing: `core/`, `formatting/`).
15
- The runtime scans this tree recursively and serves blocks by category/name.
16
-
17
- ## Format
18
-
19
- ```markdown
20
- ---
21
- name: Human Readable Name
22
- description: What this block contributes to a prompt
23
- tags: [topic, topic2]
24
- ---
25
-
26
- [The prompt text itself — written as direct instructions to the agent,
27
- present tense, no meta-commentary about being a block.]
28
- ```
29
-
30
- ## Writing rules
31
-
32
- 1. **Study an existing block** (e.g. `blocks/formatting/markdown-guidelines.md`) and
33
- match its shape: tight sections, imperative bullets.
34
- 2. A block must stand alone — no references to other blocks, no assumptions about what
35
- else is in the prompt.
36
- 3. Keep it generic and reusable. Anything workflow-specific belongs in that workflow's
37
- own prompt, not in a shared block.
38
- 4. Pick the existing category that fits; create a new subdirectory only for a genuinely
39
- new family of blocks.
40
-
41
- ## Ship
42
-
43
- `docker compose restart unoverse` — the runtime rescans blocks at boot.
@@ -1,152 +0,0 @@
1
- # Playbook — Components & Atoms (rx data)
2
-
3
- **Read first:** `docs/design/components.md` (the microapp anatomy) + `docs/design/state.md`
4
- (the reaction contract). Deep law: [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3 (three homes),
5
- `UNOVERSE_STATE_MODEL.md` §5b. **Study the exemplars before writing**: `rx/components/journeyfinder`
6
- and cardfinder — mirror them exactly.
7
-
8
- ## The anatomy
9
-
10
- ```
11
- apps/unoverse/rx/components/<name>/ # DESIGN SYSTEM tier (generic, universal)
12
- apps/unoverse/rx/orgs/<org>/components/<name>/ # ORG tier (the client's own microapp, org-private)
13
- <name>.json # envelope: name/category/nodeSize/outputs/props/state/stateOrder/root
14
- manifest.json # RENDER CONTRACT: arrival defaultState (open name, default "inline")
15
- # + discovery meta (title / description ≤120 / whenToUse utterance-shaped).
16
- # Presence = spatially discoverable; envelope NEVER duplicates the meta.
17
- layouts/ # the FACES — filename = state name: inline.json, focused.json, (custom).json
18
- states/ # the component's PRIVATE steps (wizard questions) — listed in stateOrder
19
- components/ # component-local shared partials (earned: 2+ states share a shape)
20
- ```
21
-
22
- A **flat component** (simple card/chart) is just `<name>.json` + `root` — one face, no manifest,
23
- no folders. Structure is EARNED; start flat.
24
-
25
- ## The rules (Studio's publish lint enforces all of these — 0 errors required)
26
-
27
- 1. **Three homes for everything it shows** (AUTHORING §3):
28
- - static content → **hardcoded literals** in the layout (`value`, literal `items: []` on
29
- Each, `src`) — never props, never `state`;
30
- - internal view-state → the **`state` block, SCALARS ONLY** (`step`, `phase`,
31
- `progressPct`) — an array/object/URL in `state` is slop (workflow data → props;
32
- static → hardcode);
33
- - workflow-fed data → **`props` with `input: true`** (the `default` is the preview mock).
34
- Usually empty.
35
- - **Prop names = the data contract — never invent them.** Source data hydrates props
36
- BY NAME (as-is, no mapping); a name the source doesn't carry silently renders the
37
- preview default (tell: title streams, image stays mock). Content-attached cards use
38
- the writer vocabulary: `title` `tagline` `description` `bodyCopy` `introParagraph`
39
- `primaryImage` `images` `link` `callToAction`. Guard:
40
- `server/src/runtime/content-card-hydration.test.ts`; law:
41
- `UNOVERSE_MCP_TEMPLATE_PROTOCOL.md` §Content-attached cards.
42
- 2. **Root = `Switch on defaultState`** — the component is a switch of **views**, ONE active.
43
- Cases `$include layouts/<state>` (filename = view name); `default` → the inline layout.
44
- Arrival view = **manifest.defaultState**. Keep an `inline` case for any component that
45
- lives in a conversation flow — it is its placeholder there. EXCEPTION — the
46
- **single-face (permanent) component**: exactly ONE named view + `default` pointing at
47
- the SAME face, NO inline. It arrives in its surface, cannot leave it (no ✕, no other
48
- view), and is enriched in place via data merges to one stable instance — e.g. a
49
- continuously-updated page living in a template's focus surface. Permanence is by
50
- construction, never by a pinning rule.
51
- **Lifetime (OPTIONAL manifest flag)** — `"lifetime": "conversation"` marks a durable,
52
- conversation-scoped surface (a cart, an itinerary, a composed page): the platform keys
53
- its instance by the CONVERSATION (every re-call hydrates the SAME slice — merge, not
54
- re-place) and the new-turn reset skips it, so it stays on screen until it is replaced,
55
- closes itself, or a NEW TEMPLATE loads (the template swap is the hard refresh boundary).
56
- Default `"turn"`: the instance returns to inline / retires on the next user turn.
57
- Values are a closed set — `turn | conversation` (lint-checked; docs/design/04).
58
- 3. **State is local; the VIEW is the interface** (STATE_MODEL §5b). Internal `state`
59
- (`step`, `phase`, …) is the component's own business — the template never sees it. The
60
- only thing that crosses to the template is the **active view** (`defaultState`). The
61
- component writes ONLY its own slice (`setValue`): expand = `setValue { defaultState:
62
- "course" }`; its expanded face carries its own ✕ back to `"inline"`. When a template has
63
- a surface for that view, the instance **lifts out of the flow into the surface** — the
64
- SDK renders each instance in exactly ONE placeholder, so you never hide a flow copy
65
- yourself (no `hideBelow`, no overlay hack). ❌ NEVER `setTemplateValue` to open a surface
66
- (the deprecated bridge — linted).
67
- 4. **`stateOrder`** names exactly the `states/*.json` files, in order.
68
- 5. **Atoms** (`rx/atoms/`, via `Ref`) are for shapes shared across components — authoring-time
69
- only (the server expands them before serving; no Studio view). `Ref props` remaps FIELDS;
70
- **`Ref with` passes LITERALS**: `{ "type": "Ref", "ref": "button", "with": { "label":
71
- "Learn more", "icon": "arrowRight" }, "action": { … } }` — a bind whose field is a `with`
72
- key becomes a hardcoded attribute, a truthy `with` key drops a matching `visibleWhen`
73
- guard (unprovided ⇒ that piece stays hidden), `{{key}}` style bindings take the value.
74
- ⚠ Icon: literal = `icon: "phone"`; bound = `bind: { name: field }`.
75
- 6. **Briefs — components an AI fills** (docs/design/03 §Briefed components; deep law: [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3b). A `brief`
76
- sits ON the node that renders what it describes, next to its `bind` — never in the
77
- manifest, never a separate file:
78
- - bound element → `"brief": { "description": "…", "maxLength": 60 }`
79
- - Each → `"brief": { "description": "…", "minItems": 3, "maxItems": 5 }` (its template's
80
- binds define the item shape — brief those fields too)
81
- - face/partial ROOT (no bind) → a plain-string brief = composition context
82
- (ordering, refinement rules like "update, never discard").
83
- The platform compiles briefs into the component's MCP tool schema (keys pass through
84
- verbatim — the brief uses JSON Schema's OWN vocabulary) and injects the grounding law (fill ONLY from
85
- spatial search results — never invent). The server REFEREES (invalid/empty args →
86
- instructive result, no render) and MIRRORS (success result returns the rendered page
87
- for surgical refinement). **The schema IS the instruction channel** —
88
- write briefs, never side-channel prompts/skills for filling a component. Shape is
89
- closed + linted. Pairs with the single-face permanent pattern (rule 2) for
90
- continuously-enriched pages; exemplar: `rx/components/perfectday`.
91
- 7. **Tokens only** (LAW 1) + closed style keys + real space-scale steps
92
- (`docs/design/06`) — an invented step is silently broken CSS.
93
- 8. **Lifecycle handlers — a component's own server-side code** (deep law:
94
- [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3c). When a component needs live data from an
95
- API or a computation (not from an AI = brief, not from the workflow = `input` props), it
96
- may bring a small server-side script that runs when the component renders. It's for DATA,
97
- not styling or logic-in-the-def.
98
- - **Opt in via the manifest** (authorizes execution; auditable): `"lifecycle":
99
- ["onStart"]` in `manifest.json`. A handler file with no opt-in is INERT.
100
- - **The code is a sibling file named for the lifecycle** — `onStart` → `onstart.js`,
101
- beside `<name>.json`. Its **default export** is the handler.
102
- - **`onStart`** is the only lifecycle today; it runs SERVER-SIDE when the instance is
103
- created (the MCP render path), and whatever it returns is **merged into the instance
104
- by prop name** (unknown keys inert — same contract as workflow-fed props). Return only
105
- the fields you're filling; **never overwrite a curated prop** you didn't intend to.
106
- - **`ctx` is a bounded sandbox**: `ctx.props` (the instance's current data),
107
- `ctx.instanceId`, `ctx.env(name)` (server config). The **API key comes from
108
- `ctx.env`, NEVER hardcoded** — `rx/` is a shared design folder; a secret in it leaks.
109
- - Self-contained (the external call lives in the handler); errors never break the render.
110
- Exemplar: `rx/orgs/yasisland/components/restaurantcard/onstart.js` (Google Maps details).
111
-
112
- ```js
113
- // onstart.js — manifest opts in with "lifecycle": ["onStart"]
114
- export default async function onStart(ctx) {
115
- const key = ctx.env("SEARCHAPI_KEY"); // secret from server config
116
- const res = await fetch(`https://…&q=${encodeURIComponent(ctx.props.title)}&api_key=${key}`);
117
- const place = (await res.json()).local_results?.[0] ?? {};
118
- return { rating: place.rating, address: place.address }; // merged by prop name
119
- }
120
- ```
121
-
122
- ## Discovery meta — write it to RANK (docs/nodes/node-discoverability.md)
123
-
124
- A component with a manifest is an **MCP app**: spatial embeds `` `title. whenToUse||description
125
- [category]` `` and ranks it against the USER'S OWN WORDS (`findIntent`). The meta is the
126
- selection text, not documentation — weak meta makes a working component invisible. The
127
- strict contract (doc 14, applies verbatim):
128
-
129
- - **`title`** = the thing itself, human display name — no mechanism, no org prefix.
130
- - **`description`** = what it IS, ONE line ≤120 chars — the listing subtitle; never
131
- "use when…" inside it.
132
- - **`whenToUse`** = the selection text, **utterance-shaped**: the words a user would
133
- actually say, outcome first — the OPENING words dominate the embedding. Never
134
- selector-shaped dev framing ("Pick when the user asks…"), never layout/mechanism first
135
- ("A single-face page that…" ranks near rendering concepts and loses every intent query).
136
- - **Disqualify by PROPERTY, never by naming a sibling** ("Not for answering a single
137
- question, not for one item on its own") — naming rivals dates and tangles; the ranking
138
- surfaces the alternative. A fallback/home surface never enumerates siblings' jobs
139
- (the generalist trap).
140
- - **`category`** = the domain of the JOB (Travel, Payments…), not the implementation.
141
- - Meta embeds **AS-IS** (no LLM rewrite); editing `whenToUse` re-embeds on the next train.
142
-
143
- Self-test before shipping: write the sentence the user would actually say to want this
144
- component — its key nouns/verbs must appear in `whenToUse`'s first sentence.
145
-
146
- ## Workflow
147
-
148
- 1. Study the closest exemplar; copy its folder shape.
149
- 2. Write the envelope + manifest + layouts; put every shown thing in its ONE home.
150
- 3. Publish from Studio — its lint must report 0 errors (it cites the doc for every rule).
151
- 4. Restart unoverse → Studio: mock (prop defaults + state picker + Inline/Focused
152
- toggle), then live. Debug: stream log → state inspector → definition; never guess.
@@ -1,175 +0,0 @@
1
- # Playbook: custom workflow nodes
2
-
3
- **Read first, in order:**
4
-
5
- 1. `docs/nodes/CLAUDE.md`: the authoritative agent guide. The folder, the eight
6
- non-negotiable rules, every file with an example, and the lint-error-to-fix table.
7
- 2. `docs/nodes/manifest-nodes.md`: the format in full, including the run context.
8
- 3. `docs/nodes/node-discoverability.md`: **read before writing `whenToUse`**.
9
- 4. As needed: `credentials.md`, `who-can-run-it.md`, `config-schema.md`, `service-connectors.md`,
10
- `mcp-services.md`, `signal-routing.md`, `testing-nodes.md`.
11
-
12
- This playbook adds the workflow around those docs. It never overrides them.
13
-
14
- ## A node is YAML
15
-
16
- There is no TypeScript, no `src/`, no `package.json`, no build and no publish step. A node
17
- is a folder of YAML files that the platform interprets.
18
-
19
- **Never write a code node.** If the format cannot express what is needed, the platform is
20
- missing a capability. Say so and stop, rather than reaching for code.
21
-
22
- ```
23
- apps/unoverse/nodes/<package>/
24
- package.yaml # name, category, allowedHosts, requires
25
- credentials/
26
- <name>Credential.yaml # the SHAPE of a credential, never a value
27
- shared/ # fragments several nodes $ref, and helpers: they call
28
- nodes/
29
- <NodeName>/
30
- node.yaml # what it IS (the only REQUIRED file)
31
- interface.yaml # what it CONNECTS TO
32
- config.yaml # the settings form
33
- api/
34
- run.yaml # the calls it makes, ALWAYS a list
35
- events.yaml # everything that leaves the node
36
- test.yaml # a fixture that runs
37
- ```
38
-
39
- ## Before writing anything
40
-
41
- **Read the closest published node and mirror it.** They are all public:
42
-
43
- **[marketplace/definitions/nodes](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes)**
44
-
45
- | If the node needs | Read |
46
- |---|---|
47
- | The simplest case: one call, one answer | [SearchWeb](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/search/SearchWeb) |
48
- | Walking pages and accumulating the results | [AirtableFetch](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/airtable/AirtableFetch) |
49
- | Writing a collection in batches the API accepts | [AirtableInsert](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/airtable/AirtableInsert) |
50
- | A cheap existence check before an insert | [AirtableExists](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/airtable/AirtableExists) |
51
- | Caching an answer between runs | [ApolloCompany](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/gtm/ApolloCompany) |
52
- | Two calls, where the second uses the first | [HunterEnrich](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/gtm/HunterEnrich) |
53
- | Starting a job and waiting for it to finish | [HyperbrowserCrawl](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/hyperbrowser/HyperbrowserCrawl) |
54
- | Tokens streaming in as they are produced | [OpenAIStream](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/openai/OpenAIStream) |
55
- | A request whose shape changes with the settings | [OpenAIStructuredOutput](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/openai/OpenAIStructuredOutput) |
56
- | A node other nodes call, rather than a workflow step | [OpenAIEmbeddingService](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/openai/OpenAIEmbeddingService) |
57
- | Offering tools to an Agent | [HubspotMCP](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/hubspot/HubspotMCP) |
58
- | A tool loop, and a second model narrating progress | [OpenAIAgent](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/openai/OpenAIAgent) |
59
-
60
- Closer to a real service: [airtable](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/airtable), [gtm](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/gtm),
61
- [hubspot](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/hubspot), [salesforce](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/salesforce), [search](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/search),
62
- [slack](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/slack), [x-search](https://github.com/unoverse-platform/marketplace/tree/main/definitions/nodes/x-search).
63
-
64
- Adding a node to an existing package in the user's own repo is usually right. Create a
65
- package only for a new integration.
66
-
67
- ## Workflow
68
-
69
- 1. **Decide `kind`.** One call, one reply is `PromiseNode`. Streaming or a tool loop is
70
- `CallbackNode`. Lint verifies it against the transport and `toolExchange`.
71
- 2. **Write `node.yaml`.** Treat `whenToUse` as make-or-break: it decides whether the node is
72
- ever offered. Outcome first, disqualify by property, never name another node, wiring fact
73
- last. Then answer who may run it, which is compulsory on every node:
74
-
75
- ```yaml
76
- auth:
77
- required: false # adds no requirement of its own; does NOT mean public
78
- # role: crm:write # optional; implies required: true
79
- ```
80
-
81
- `required: false` is right for almost every node: the trigger already decided who was let
82
- in. See `docs/nodes/who-can-run-it.md`.
83
- 3. **Write `interface.yaml`.** Name outputs for what they carry, because those names become
84
- what someone types in a template.
85
- 4. **Write `config.yaml`.** Every value the workflow supplies needs `ui:field: template`. A
86
- `description` says what the setting does, not what it is called. Then add the two
87
- run-authorization fields, which every node carries identically:
88
-
89
- ```yaml
90
- authRequired:
91
- type: boolean
92
- title: Require sign-in
93
- default: false
94
- "ui:widget": toggle
95
- authRole:
96
- type: string
97
- title: Require role
98
- default: ""
99
- "ui:dependencies": { authRequired: true }
100
- ```
101
-
102
- These are the workflow BUILDER's control, per box on a canvas, separate from
103
- `node.yaml`'s `auth`, which is your floor. The executor takes the stricter of the two. A
104
- role usually belongs here rather than in the manifest: `finance:approve` is a claim one
105
- deployment's identity provider mints, and a published node cannot know it. Add both to
106
- `ui:order`.
107
- 5. **Write `api/run.yaml`.** A list, each entry named. Include `error` wherever the service
108
- can answer 200 with a failure body. Where one call is really many, name the capability on
109
- the call rather than trying to express a loop: `paginate`, `chunk`, `poll` or `state`.
110
- See `docs/nodes/calls-that-loop.md`, and declare each one in `requires`. A call
111
- authenticates with `credential: { scheme, token }`, which is OUTBOUND: how the node proves
112
- itself to the vendor. It is not `node.yaml`'s `auth`, which is who may run the node.
113
- 6. **Write `api/events.yaml`.** One row per output connector, in the order `interface.yaml`
114
- declares them. Streaming rows need `match`, and usually `accumulate: true`.
115
- 7. **Add the host** to `allowedHosts` in `package.yaml`, and the credential file if the
116
- service needs a key.
117
- 8. **Write `test.yaml`.** Assert the shape, never exact words.
118
-
119
- ## Verify
120
-
121
- ```bash
122
- Studio → Nodes → your node (the catalog load reports schema errors)
123
- Studio's Nodes screen (Load sample → Run)
124
- ```
125
-
126
- Both must pass before the node is done. Lint names the rule and the page behind it, so read
127
- the message rather than guessing.
128
-
129
- The Studio node runner calls the real service and needs no platform running. It reads keys from `.env`
130
- as `<CREDENTIAL>_<FIELD>` in upper snake case, so `openAICredential.apiKey` is
131
- `OPENAI_API_KEY`.
132
-
133
- A node that lints but has never run is not done. Running it is the only check that proves
134
- the manifest describes the real service rather than something merely coherent.
135
-
136
- ## Things that go wrong
137
-
138
- - **A template resolved to empty.** Nothing errors. The node id or the output name does not
139
- match a real edge. There is no `input.*` root, and array indexes are dot segments
140
- (`records.0.Name`), never brackets.
141
- - **A number arrived as a string.** A field that is exactly one `{{ path }}` keeps its type.
142
- Anything with text around it becomes a string.
143
- - **An output stays empty.** No row in `events.yaml` emits to it.
144
- - **A downstream node gets one word at a time.** The streaming row needs `accumulate: true`.
145
- - **The request was refused before it left.** The host is not in `allowedHosts`.
146
- - **An expression was rejected.** The sandbox has no `new`, so no `new Date(...)`. Use
147
- `Date.now()` and `Date.iso(ms)`. Nothing mutates either: `.at(-1)` not `.pop()`,
148
- `.toSorted()` not `.sort()`. No spread in call arguments: `reduce` into an object rather
149
- than `Object.assign({}, ...list)`.
150
- - **An expression grew past a few lines.** Give it a name. Any `shared/*.yaml` may declare
151
- `helpers:`, and every expression in the package can call them:
152
-
153
- ```yaml
154
- # shared/helpers.yaml
155
- helpers:
156
- row:
157
- args: [r]
158
- body: "return { id: r.id, title: r.title }"
159
- ```
160
- ```yaml
161
- returns: "return response.items.map(helpers.row)"
162
- ```
163
-
164
- A helper sees ONLY its arguments and its sibling helpers — never `config`, `credentials`
165
- or the caller's scope. Same sandbox, no extra authority.
166
-
167
- ## Do not
168
-
169
- - Write TypeScript, a `package.json` or a build step for a node
170
- - Put a credential value in any file
171
- - Hard-code instruction text. Use `{{prompt.<blockName>}}` so it tracks the block
172
- - Name another node in `whenToUse`
173
- - Set `cacheable: true` on anything effectful or non-deterministic
174
- - Claim a capability the platform does not implement
175
- - Write `auth:` on a call. That key is `credential:`; `auth` on a node is who may RUN it
@@ -1,115 +0,0 @@
1
- # Playbook — Templates (MCP apps)
2
-
3
- **Read first:** `docs/design/templates.md` (layouts + manifest-as-envelope) +
4
- `docs/design/state.md` (the reaction contract + name-sync). Deep law:
5
- `UNOVERSE_STATE_MODEL.md` §5b, `UNOVERSE_LAYERS.md`. Everything in the component
6
- playbook (closed vocabulary, tokens, scale steps) applies here too.
7
-
8
- ## THE model — layouts sync by name
9
-
10
- A component is a Switch of **faces**; a template is a set of **layouts** — same
11
- mechanism, two tiers, connected by ONE rule:
12
-
13
- > A component enters a view → the template presents the layout of the same name.
14
- > No matching layout → the default layout; the component renders inline.
15
-
16
- Derived off the store, never stored, no wiring — the component never knows templates
17
- exist. **Rule of thumb: component causes it = layout; template knows it = state.**
18
-
19
- ## The anatomy — manifest-only (NO `<name>.json` envelope)
20
-
21
- ```
22
- apps/unoverse/rx/orgs/<org>/templates/<name>/
23
- manifest.json # THE ENVELOPE — the whole app contract (below)
24
- layouts/ # the ARRANGEMENTS — one per component view + the default
25
- standard.json # the default (manifest.layout): the core alone
26
- products.json # view "products" → core + cards rail
27
- product.json # view "product" → core + detail panel
28
- components/ # template-local partials (core, header, composer-bar, turns…)
29
- states/ # LOCAL states only (welcome, conversation, call phases) —
30
- # available in exactly the layouts whose trees include them
31
- ```
32
-
33
- Each layout = a complete arrangement: `{ "$include": "components/core" }` + that view's
34
- surface. Shared chrome lives ONCE in `components/`; every layout includes it. A layout
35
- named after a view MUST contain a surface selecting that view (guard-enforced) — the
36
- name claims the instance, the surface renders it.
37
-
38
- ## manifest.json (the app contract)
39
-
40
- Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped — the
41
- USER's words; findIntent ranks on it — the OPENING words carry the ranking; disqualify by
42
- property, never by naming siblings; full contract: docs/design/05 §Naming) ·
43
- `category` · `defaultState` (`"template"` = the full surface) · `inputSchema` ·
44
- `layout` (the DEFAULT layout name) · `stateOrder` (local states + layouts, picker order) ·
45
- `binding` (`workflow` + `trigger` — the app OWNS its workflow; without a real binding it
46
- is not done). Optional: `preview` (the per-LAYOUT mock map for Studio —
47
- `{ "products": ["productcard", "productcard"] }`; repeated name = several instances),
48
- `autoTrigger`, `expose`, `service` (`"voice"` — the channel instantiates the native
49
- service, which projects `callState` into scope).
50
-
51
- **The generalist trap:** a home/fallback surface must NOT enumerate its siblings' jobs —
52
- it owns general help and cedes specific jobs by property, naming none.
53
-
54
- ## Sizing — each layout owns its widths
55
-
56
- The app = the ACTIVE layout's total, nothing else ever. Inside a layout: the core panel
57
- is always open (`appWidth` on the core box), the surface panel declares its width on the
58
- ComponentSlot. Lint-enforced:
59
- - `appWidth` = a NAMED size from the org's `styles/semantic/app-sizes.json`
60
- (`chat` · `chat-slim` · `rail` · `panel`) — raw CSS is an ERROR.
61
- - ONE declaration per panel — the frame never declares width/flex.
62
- - NEVER on a layout root; NEVER `visibleWhen`-guarded (a conditional arrangement is a
63
- LAYOUT, selected by name).
64
- - An OVERLAY surface (`inset: 0` over the core) declares NO appWidth and needs NO layout
65
- of its own — reach for a layout when the ARRANGEMENT changes.
66
- - Nothing in the manifest sizes the app (`width`/`focusWidth` dead — lint error).
67
- - Layout root: `overflow: hidden`.
68
-
69
- ## Surfaces — templates REACT, they never own
70
-
71
- One instance → one placeholder: the flow is the placeholder for `inline`; a surface (or
72
- a layout, via its inner surface) is the placeholder for a named view. The SDK keeps a
73
- claimed instance out of the flow — never hide a flow copy yourself.
74
-
75
- - **Flow slot**: `{ "type": "ComponentSlot", "select": {} }` — always present.
76
- - **Reaction surface**: select by the VIEW, never by type/id:
77
- ```jsonc
78
- { "type": "ComponentSlot", "appWidth": "panel",
79
- "select": { "from": "all", "where": { "field": "defaultState", "eq": "product" }, "limit": 1 },
80
- "frame": { /* chrome; an inner bare ComponentSlot marks the placement */ } }
81
- ```
82
- ❌ `select: { type: [...] }`, ❌ `where.field` ≠ `defaultState`, ❌ template chrome
83
- writing a component's slice — all lint-flagged.
84
- - **Template state** (`setTemplateValue`) is ONLY for the template's own chrome (a
85
- disclosure, the composer `draft`). "Is something focused" is DERIVED — never stored.
86
- - **Chrome reacts via `surfacedView`** (SDK-projected active view name, `""` = all
87
- inline): `visibleWhen { "field": "surfacedView", "in": ["", "products"] }`.
88
- - **A `limit: 1` surface's occupant FILLS it** automatically — never sprinkle
89
- `minHeight: full` through faces. Rails (no limit) keep content-sized cards.
90
- - A full layer caps its height and scrolls its BODY only (`flex: 1` + `minHeight: 0` +
91
- `overflow: auto`; header/footer pinned).
92
-
93
- ## Voice templates
94
-
95
- `"service": "voice"` in the manifest. Call phases (`states/idle…user-speaking`) are
96
- LOCAL states included by the cores; typically a wide core in the default layout and a
97
- slim core (`chat-slim`) in the card layouts. Cards during a call select layouts by name
98
- exactly as in chat. Audio is never wired in a definition.
99
-
100
- ## New org?
101
-
102
- `rx/orgs/<org>/` = `templates/` + optional `components/` (org-private microapps, names
103
- unique across tiers) + a **complete self-contained** `styles/` (copy
104
- `rx/orgs/default/styles` and re-value the base scales). Addresses: apps
105
- `unoverse://apps/<org>/<name>`, org components `unoverse://components/<org>/<name>`,
106
- themes `unoverse://theme/<org>/<theme>`.
107
-
108
- ## Validate & ship
109
-
110
- 1. Publish from Studio — its lint must report 0 errors (layout/name-sync rules, widths, reaction rules, tokens).
111
- 2. Restart unoverse — the node re-synthesizes.
112
- 3. **See it**: Studio — layout pills on top (default first, stateOrder order), local
113
- states down the left, freely combinable; acting inside the preview transitions like
114
- the runtime. Then run it live: the bound workflow streams components, and entering a
115
- view swaps the layout end-to-end.
@@ -1,52 +0,0 @@
1
- # Playbook: Build a workflow (via the builder MCP)
2
-
3
- Workflows are NOT files in the carve-out — they live in the platform and are built
4
- through the **builder MCP** (`canvas`, auto-registered by this repo's
5
- `.mcp.json`). You drive the platform's own authoring tools; the canvas updates live
6
- in the developer's browser as you build.
7
-
8
- ## Prerequisites (check before starting)
9
-
10
- 1. **The platform is running** (`unoverse start`, or `npm run dev` in the platform
11
- repo). The builder listens on `localhost:4106` — local machine only, by design.
12
- 2. **The MCP is connected**: `canvas` should show connected with 14 tools.
13
- If it shows failed, the platform wasn't up when the session started — reconnect
14
- via `/mcp` after starting it.
15
- 3. **The developer gives you a pairing code.** Ask them to open a **new empty
16
- workflow in Canvas** (http://localhost:3001), click **"Connect agent"** in the
17
- toolbar, and paste the short code it shows (e.g. `FW9D-6QKA`). The code binds you
18
- to that one workflow — single use, expires in 10 minutes. There is deliberately no
19
- way to list or discover workflows from here — the code must come from the developer.
20
- Prefer a fresh workflow: saves are provenance-scoped, so wiring into nodes you
21
- didn't create may be rejected.
22
-
23
- ## The contract
24
-
25
- 1. **`bindWorkflow` first.** Every other tool refuses until the session is bound,
26
- and every call after is pinned server-side to that one canvas.
27
- 2. **Read the in-band guides before building.** The deep how-to is served by the
28
- platform itself via `readBuilderSkill` — always current, never duplicated here:
29
- - `solution-design` — read FIRST on any new goal
30
- - `workflow-building` — the build protocol + response-field semantics
31
- - `workflow-testing` — runTest / stepNode / reading traces
32
- - `template-references` — the `{{...}}` grammar + fixing lint errors
33
- 3. **Build ONE stage at a time.** The loop is: `saveWorkflow` one stage → `runTest`
34
- → read the trace → add the next stage based on that evidence. Never design or
35
- pre-wire the whole graph up front — you learn a node's real output fields by
36
- running it, not by planning.
37
- 4. **Ground truth rules:** `getCanvas` is the real graph; a nodeType is valid only
38
- if `getNodeCatalog` returned it (search it fresh per step, with a one-line
39
- `task`); layout and node ids are not yours to set.
40
- 5. **The developer judges the result — never self-certify.** When the workflow runs
41
- end-to-end with real, non-empty output, report what you built and what the test
42
- showed. They watch the canvas live and accept, or ask for changes (which continue
43
- on the same bound canvas).
44
-
45
- ## Troubleshooting
46
-
47
- | Symptom | Cause / fix |
48
- |---|---|
49
- | Every tool returns "No workflow bound" | Call `bindWorkflow` with the pairing code from Canvas "Connect agent" |
50
- | Bind fails "invalid or expired pairing code" | Codes are single-use + 10-min TTL — ask the developer to click "Connect agent" again for a fresh one |
51
- | Tool calls error after working earlier | The platform restarted mid-session — `/mcp` → reconnect |
52
- | saveWorkflow rejects an edge into an existing node | Provenance guard: that node wasn't built this session — build on a fresh workflow, or recreate the stage yourself |