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 +2 -7
- package/lib/create.mjs +8 -15
- package/operator/lib/help.sh +1 -1
- package/package.json +1 -1
- package/operator/skills/unoverse-create/SKILL.md +0 -88
- package/operator/skills/unoverse-create/references/agent-skill.md +0 -65
- package/operator/skills/unoverse-create/references/block.md +0 -43
- package/operator/skills/unoverse-create/references/component.md +0 -152
- package/operator/skills/unoverse-create/references/node.md +0 -175
- package/operator/skills/unoverse-create/references/template.md +0 -115
- package/operator/skills/unoverse-create/references/workflow.md +0 -52
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
|
-
|
|
127
|
-
|
|
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
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
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;
|
package/operator/lib/help.sh
CHANGED
|
@@ -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,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 |
|