@unoverse-platform/studio 0.1.25 → 0.1.27
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/package.json +1 -1
- package/screens/AtomsView.tsx +40 -2
- package/screens/ComponentsView.tsx +112 -21
- package/screens/NativeAppHost.tsx +8 -4
- package/screens/SourceView.tsx +1 -1
- package/screens/TemplatesView.tsx +52 -22
- package/screens/states.ts +27 -4
- package/src/PublishDialog.tsx +1 -1
- package/vendor/sdk/appEngine.tsx +44 -3
- package/vendor/sdk/lib/UnoverseComponent.tsx +1 -1
- package/vendor/sdk/lib/core/actions.ts +20 -6
- package/vendor/sdk/lib/core/analytics.ts +89 -0
- package/vendor/sdk/lib/core/client.ts +1 -1
- package/vendor/sdk/lib/core/index.ts +30 -0
- package/vendor/sdk/lib/core/store.ts +65 -38
- package/vendor/sdk/lib/core/templates.ts +252 -0
- package/vendor/sdk/lib/core/types.ts +40 -7
- package/vendor/sdk/lib/index.ts +9 -0
- package/vendor/sdk/lib/orb/ThinkingOrb.tsx +127 -0
- package/vendor/sdk/lib/orb/engine/braid.ts +50 -0
- package/vendor/sdk/lib/orb/engine/core.ts +157 -0
- package/vendor/sdk/lib/orb/engine/lattice.ts +205 -0
- package/vendor/sdk/lib/orb/engine/morph.ts +138 -0
- package/vendor/sdk/lib/orb/engine/orbits.ts +80 -0
- package/vendor/sdk/lib/orb/engine/profiles.ts +162 -0
- package/vendor/sdk/lib/orb/engine/registry.ts +24 -0
- package/vendor/sdk/lib/orb/engine/ribbon.ts +93 -0
- package/vendor/sdk/lib/orb/engine/types.ts +14 -0
- package/vendor/sdk/lib/orb/engine/web.ts +98 -0
- package/vendor/sdk/lib/orb/index.ts +20 -0
- package/vendor/sdk/lib/orb/presets.ts +104 -0
- package/vendor/sdk/lib/orb/theme.ts +83 -0
- package/vendor/sdk/lib/orb/types.ts +73 -0
- package/vendor/sdk/lib/primitives.tsx +114 -5
- package/vendor/sdk/lib/render.tsx +25 -5
- package/vendor/sdk/lib/style.ts +6 -2
- package/vendor/sdk/lib/template.tsx +77 -218
- package/vendor/sdk/main.tsx +7 -0
- package/vendor/skills/unoverse-create/SKILL.md +18 -18
- package/vendor/skills/unoverse-create/references/agent-skill.md +12 -12
- package/vendor/skills/unoverse-create/references/block.md +5 -5
- package/vendor/skills/unoverse-create/references/component.md +150 -75
- package/vendor/skills/unoverse-create/references/node.md +1 -1
- package/vendor/skills/unoverse-create/references/template.md +142 -82
- package/vendor/skills/unoverse-create/references/workflow.md +15 -15
|
@@ -1,115 +1,175 @@
|
|
|
1
|
-
# Playbook
|
|
1
|
+
# Playbook: Templates (MCP apps)
|
|
2
2
|
|
|
3
3
|
**Read first:** `docs/design/templates.md` (layouts + manifest-as-envelope) +
|
|
4
|
-
`docs/design/state.md` (the
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
`docs/design/state.md` (the state model). Deep law: `UNOVERSE_STATE_MODEL.md` §5
|
|
5
|
+
(the six rules), `UNOVERSE_LAYERS.md`. Everything in the component playbook (closed
|
|
6
|
+
vocabulary, tokens, scale steps) applies here too. **Study the exemplar before
|
|
7
|
+
writing**: `rx/sab/templates/sab-chat-layout` is the canonical State Model v2 template.
|
|
8
|
+
|
|
9
|
+
## THE model: the component drives; the template reacts by name
|
|
10
|
+
|
|
11
|
+
A component is a state tree whose public states are its whole interface; a template
|
|
12
|
+
declares its OWN state tree in the manifest and reacts to components by name:
|
|
13
|
+
|
|
14
|
+
> A component enters a public view → the template asks "do I have a state with that
|
|
15
|
+
> name?" Match: the template enters that state and its layout draws. No match: the
|
|
16
|
+
> component renders inline in the flow. Always.
|
|
17
|
+
|
|
18
|
+
Derived off the store, never stored, no wiring. The component never knows templates
|
|
19
|
+
exist, and post-spawn the template NEVER writes a component's state. Name-match is the
|
|
20
|
+
default convention (`reactsTo:` on a template state is the rare escape hatch for
|
|
21
|
+
vocabulary mismatches). **Rule of thumb: component causes it = a reaction state;
|
|
22
|
+
template knows it = a base substate or template state.**
|
|
23
|
+
|
|
24
|
+
## The manifest states tree: one declaration, everything follows
|
|
25
|
+
|
|
26
|
+
The canonical shape (`rx/sab/templates/sab-chat-layout/manifest.yaml`):
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
states:
|
|
30
|
+
main: # the base arrangement, FIRST
|
|
31
|
+
states:
|
|
32
|
+
welcome: {} # contained substate; first substate = the landing default
|
|
33
|
+
focus: {} # then the reaction ladder, in priority order
|
|
34
|
+
products: {}
|
|
35
|
+
detail: {}
|
|
36
|
+
```
|
|
18
37
|
|
|
19
|
-
|
|
38
|
+
- **Top-level order IS the priority ladder**, base first. The template can be in ONE
|
|
39
|
+
state at a time: it walks the list top-down and enters the first of its states that
|
|
40
|
+
ANY hosted component currently matches. The ladder is DERIVED from the tree; never
|
|
41
|
+
author `stateOrder` (that field survives only as the legacy form for templates
|
|
42
|
+
without a tree).
|
|
43
|
+
- **Nesting IS containment, enforced by compilation.** A declared base substate
|
|
44
|
+
(`welcome`) is stripped from every non-base arrangement; no hand-written guard ever
|
|
45
|
+
polices it. The substate's file keeps its own condition (`visibleWhen: isEmpty`).
|
|
46
|
+
- **The shell rule.** Base content drawn in every arrangement is NOT declared as a
|
|
47
|
+
substate. sab's conversation timeline is main's own shell, included by the shared
|
|
48
|
+
chat chrome and legitimately drawn beside the rail, the page and under focus;
|
|
49
|
+
declaring it would contain it to `main` and empty the chat column in every reaction
|
|
50
|
+
state. Only states the template must scope get declared.
|
|
51
|
+
- **Arrival honors the component's declared initial** when the template has a matching
|
|
52
|
+
state; otherwise the template scans its own states in declared order and takes the
|
|
53
|
+
first name on the component's public menu; no overlap and the component rides inline.
|
|
54
|
+
- **A higher state CANCELS the claims below it**: while active, hosted components in
|
|
55
|
+
lower-ranked states retract to inline (durable `lifetime: "conversation"` slices opt
|
|
56
|
+
out), so releasing it lands on the BASE arrangement, never a stale lower one.
|
|
57
|
+
- Ties (two components in the same view): most recent write wins (`limit: 1`).
|
|
58
|
+
- **No word is special.** `focus` outranks `products` by list position only; the engine
|
|
59
|
+
matches names and walks the list. Even priority is data.
|
|
60
|
+
- The template's own condition-guarded moods (a welcome hero on an empty conversation)
|
|
61
|
+
are private substates of the base, never ladder entries: the template does not
|
|
62
|
+
rearrange between them.
|
|
63
|
+
|
|
64
|
+
## The anatomy: manifest-only (NO `<name>.yaml` envelope)
|
|
20
65
|
|
|
21
66
|
```
|
|
22
|
-
apps/unoverse/rx
|
|
23
|
-
manifest.
|
|
24
|
-
layouts/ # the ARRANGEMENTS
|
|
25
|
-
|
|
26
|
-
products.
|
|
27
|
-
|
|
67
|
+
apps/unoverse/rx/<project>/templates/<name>/
|
|
68
|
+
manifest.yaml # THE ENVELOPE: the whole app contract, incl. the states tree
|
|
69
|
+
layouts/ # the ARRANGEMENTS: one per state that owns one (same-name default)
|
|
70
|
+
main.yaml # the base: the core alone
|
|
71
|
+
products.yaml # state "products" → core + cards rail
|
|
72
|
+
detail.yaml # state "detail" → core + full record page
|
|
28
73
|
components/ # template-local partials (core, header, composer-bar, turns…)
|
|
29
|
-
states/ #
|
|
30
|
-
#
|
|
74
|
+
states/ # base substates' drawings (welcome): included plainly by the
|
|
75
|
+
# chrome, own condition inside, containment compiled from the tree
|
|
31
76
|
```
|
|
32
77
|
|
|
33
|
-
Each layout = a complete arrangement:
|
|
34
|
-
surface. Shared chrome lives ONCE in `components/`; every layout includes it.
|
|
35
|
-
|
|
36
|
-
|
|
78
|
+
Each layout = a complete arrangement: `$include: components/core` + that state's
|
|
79
|
+
surface. Shared chrome lives ONCE in `components/`; every layout includes it. The tree
|
|
80
|
+
claims the instance, the layout's surface renders it: a reaction state's layout holds a
|
|
81
|
+
ComponentSlot selecting the matching view.
|
|
37
82
|
|
|
38
|
-
## manifest.
|
|
83
|
+
## manifest.yaml (the app contract)
|
|
39
84
|
|
|
40
|
-
Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped
|
|
41
|
-
USER's words; findIntent ranks on it
|
|
85
|
+
Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped: the
|
|
86
|
+
USER's words; findIntent ranks on it, the OPENING words carry the ranking; disqualify by
|
|
42
87
|
property, never by naming siblings; full contract: docs/design/05 §Naming) ·
|
|
43
|
-
`category` · `defaultState
|
|
44
|
-
|
|
45
|
-
`binding` (`workflow` + `trigger
|
|
46
|
-
is not done). Optional: `preview` (the per-
|
|
47
|
-
`{ "products": ["
|
|
48
|
-
`autoTrigger`, `expose`, `service` (`"voice"
|
|
49
|
-
service, which projects `callState` into scope).
|
|
88
|
+
`category` · `defaultState: "template"` (the app-level LOAD state, a protocol field,
|
|
89
|
+
nothing to do with component views) · `inputSchema` · `layout` (the BASE layout name) ·
|
|
90
|
+
`states` (the tree above) · `binding` (`workflow` + `trigger`: the app OWNS its
|
|
91
|
+
workflow; without a real binding it is not done). Optional: `preview` (the per-state
|
|
92
|
+
mock map for Studio: `{ "products": ["product-card", "product-card"] }`; repeated
|
|
93
|
+
name = several instances), `autoTrigger`, `expose`, `service` (`"voice"`: the channel
|
|
94
|
+
instantiates the native service, which projects `callState` into scope).
|
|
50
95
|
|
|
51
|
-
**The generalist trap:** a home/fallback surface must NOT enumerate its siblings' jobs
|
|
52
|
-
|
|
96
|
+
**The generalist trap:** a home/fallback surface must NOT enumerate its siblings' jobs.
|
|
97
|
+
It owns general help and cedes specific jobs by property, naming none.
|
|
53
98
|
|
|
54
|
-
## Sizing
|
|
99
|
+
## Sizing: each layout owns its widths
|
|
55
100
|
|
|
56
101
|
The app = the ACTIVE layout's total, nothing else ever. Inside a layout: the core panel
|
|
57
102
|
is always open (`appWidth` on the core box), the surface panel declares its width on the
|
|
58
103
|
ComponentSlot. Lint-enforced:
|
|
59
|
-
- `appWidth` = a NAMED size from the
|
|
60
|
-
(`chat` · `chat-slim` · `rail` · `panel`)
|
|
61
|
-
- ONE declaration per panel
|
|
104
|
+
- `appWidth` = a NAMED size from the project's `styles/semantic/app-sizes`
|
|
105
|
+
(`chat` · `chat-slim` · `rail` · `panel` · `flex`). Raw CSS is an ERROR.
|
|
106
|
+
- ONE declaration per panel; the frame never declares width/flex.
|
|
62
107
|
- NEVER on a layout root; NEVER `visibleWhen`-guarded (a conditional arrangement is a
|
|
63
|
-
|
|
64
|
-
- An OVERLAY surface (`inset: 0` over the core) declares NO appWidth and needs NO
|
|
65
|
-
of its own
|
|
66
|
-
- Nothing in the manifest sizes the app (`width`/`focusWidth` dead
|
|
108
|
+
STATE with its own layout).
|
|
109
|
+
- An OVERLAY surface (`inset: 0` over the core) declares NO appWidth and needs NO state
|
|
110
|
+
of its own. Reach for a state when the ARRANGEMENT changes.
|
|
111
|
+
- Nothing in the manifest sizes the app (`width`/`focusWidth` dead, a lint error).
|
|
67
112
|
- Layout root: `overflow: hidden`.
|
|
68
113
|
|
|
69
|
-
## Surfaces
|
|
70
|
-
|
|
71
|
-
One instance → one placeholder: the flow is the placeholder for `inline`; a
|
|
72
|
-
|
|
73
|
-
claimed instance out of the flow
|
|
74
|
-
|
|
75
|
-
- **Flow slot**: `{
|
|
76
|
-
- **Reaction surface**: select by the
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
114
|
+
## Surfaces: templates REACT, they never own
|
|
115
|
+
|
|
116
|
+
One instance → one placeholder: the flow is the placeholder for `inline`; a reaction
|
|
117
|
+
state's layout (via its inner surface) is the placeholder for a matched view. The SDK
|
|
118
|
+
keeps a claimed instance out of the flow; never hide a flow copy yourself.
|
|
119
|
+
|
|
120
|
+
- **Flow slot**: `{ type: ComponentSlot, select: {} }`, always present.
|
|
121
|
+
- **Reaction surface**: select by the public `view`, never by type/id:
|
|
122
|
+
```yaml
|
|
123
|
+
type: ComponentSlot
|
|
124
|
+
appWidth: panel
|
|
125
|
+
select:
|
|
126
|
+
from: all
|
|
127
|
+
where: { field: view, eq: detail }
|
|
128
|
+
limit: 1
|
|
129
|
+
frame: # chrome; an inner bare ComponentSlot marks the placement
|
|
81
130
|
```
|
|
82
|
-
❌ `select: { type: [...] }`, ❌ `where.field` ≠ `
|
|
83
|
-
writing a component's slice
|
|
131
|
+
❌ `select: { type: [...] }`, ❌ `where.field` ≠ `view`, ❌ template chrome
|
|
132
|
+
writing a component's slice, all lint-flagged. (`defaultState` as the select field
|
|
133
|
+
is the v1 legacy spelling.)
|
|
134
|
+
- **Input tools are template chrome** (unchanged rule): a composer, an edit form, a
|
|
135
|
+
picker is owned by the template, never a streamed component state; chrome never
|
|
136
|
+
"arrives". A card's Edit stages its data into the template's one input tool.
|
|
84
137
|
- **Template state** (`setTemplateValue`) is ONLY for the template's own chrome (a
|
|
85
|
-
disclosure, the composer `draft`). "Is something focused" is DERIVED
|
|
138
|
+
disclosure, the composer `draft`). "Is something focused" is DERIVED, never stored.
|
|
86
139
|
- **Chrome reacts via `surfacedView`** (SDK-projected active view name, `""` = all
|
|
87
|
-
inline): `visibleWhen {
|
|
88
|
-
- **A `limit: 1` surface's occupant FILLS it** automatically
|
|
89
|
-
`minHeight: full` through
|
|
140
|
+
inline): `visibleWhen { field: surfacedView, in: ["", "products"] }`.
|
|
141
|
+
- **A `limit: 1` surface's occupant FILLS it** automatically; never sprinkle
|
|
142
|
+
`minHeight: full` through component layouts. Rails (no limit) keep content-sized cards.
|
|
90
143
|
- A full layer caps its height and scrolls its BODY only (`flex: 1` + `minHeight: 0` +
|
|
91
144
|
`overflow: auto`; header/footer pinned).
|
|
145
|
+
- `pinned: true` on a reaction layout keeps late-arriving components from yanking the
|
|
146
|
+
page from under a reading guest; the guest's own tap still moves it.
|
|
92
147
|
|
|
93
148
|
## Voice templates
|
|
94
149
|
|
|
95
|
-
`"service": "voice"` in the manifest. Call phases (
|
|
96
|
-
|
|
97
|
-
slim core (`chat-slim`) in the card layouts. Cards during a call
|
|
98
|
-
exactly as in chat. Audio is never wired in a definition.
|
|
150
|
+
`"service": "voice"` in the manifest. Call phases (idle…user-speaking) branch off the
|
|
151
|
+
projected `callState` discriminant inside the base; typically a wide core in the base
|
|
152
|
+
layout and a slim core (`chat-slim`) in the card layouts. Cards during a call match
|
|
153
|
+
template states by name exactly as in chat. Audio is never wired in a definition.
|
|
99
154
|
|
|
100
|
-
## New
|
|
155
|
+
## New project?
|
|
101
156
|
|
|
102
|
-
`rx
|
|
103
|
-
unique across tiers) + a **complete self-contained** `styles/` (copy
|
|
104
|
-
`
|
|
105
|
-
`unoverse://apps/<
|
|
106
|
-
themes `unoverse://theme/<
|
|
157
|
+
`rx/<project>/` = `templates/` + optional `components/` (project-private microapps,
|
|
158
|
+
names unique across tiers) + a **complete self-contained** `styles/` (copy an existing
|
|
159
|
+
project's `styles/` and re-value the base scales). Addresses: apps
|
|
160
|
+
`unoverse://apps/<project>/<name>`, project components
|
|
161
|
+
`unoverse://components/<project>/<name>`, themes `unoverse://theme/<project>/<theme>`.
|
|
107
162
|
|
|
108
163
|
## Validate & ship
|
|
109
164
|
|
|
110
|
-
1.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
165
|
+
1. Manifest declares the states tree: base first, reaction states in priority order,
|
|
166
|
+
base-only moods nested inside the base, shell content NOT declared. No authored
|
|
167
|
+
`stateOrder`, no hand-written containment guards.
|
|
168
|
+
2. Publish from Studio; its lint must report 0 errors (the rules are v2-aware:
|
|
169
|
+
state/layout ownership, widths, reaction rules, tokens).
|
|
170
|
+
3. Restart unoverse; the node re-synthesizes.
|
|
171
|
+
4. **See it**: in Studio, the state switcher reads the tree (base first, ladder order),
|
|
172
|
+
base substates beneath, freely combinable; acting inside the preview transitions
|
|
173
|
+
like the runtime. Then run it live: the bound workflow streams components, a card
|
|
174
|
+
entering a public view walks the ladder and rearranges the template end-to-end,
|
|
175
|
+
and its ✕ hands the lower state back.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Playbook: Build a workflow (via the builder MCP)
|
|
2
2
|
|
|
3
|
-
Workflows are NOT files in the carve-out
|
|
3
|
+
Workflows are NOT files in the carve-out. They live in the platform and are built
|
|
4
4
|
through the **builder MCP** (`canvas`, auto-registered by this repo's
|
|
5
5
|
`.mcp.json`). You drive the platform's own authoring tools; the canvas updates live
|
|
6
6
|
in the developer's browser as you build.
|
|
@@ -8,15 +8,15 @@ in the developer's browser as you build.
|
|
|
8
8
|
## Prerequisites (check before starting)
|
|
9
9
|
|
|
10
10
|
1. **The platform is running** (`unoverse start`, or `npm run dev` in the platform
|
|
11
|
-
repo). The builder listens on `localhost:4106
|
|
11
|
+
repo). The builder listens on `localhost:4106`, local machine only, by design.
|
|
12
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
|
|
13
|
+
If it shows failed, the platform wasn't up when the session started. Reconnect
|
|
14
14
|
via `/mcp` after starting it.
|
|
15
15
|
3. **The developer gives you a pairing code.** Ask them to open a **new empty
|
|
16
16
|
workflow in Canvas** (http://localhost:3001), click **"Connect agent"** in the
|
|
17
17
|
toolbar, and paste the short code it shows (e.g. `FW9D-6QKA`). The code binds you
|
|
18
|
-
to that one workflow
|
|
19
|
-
way to list or discover workflows from here
|
|
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
20
|
Prefer a fresh workflow: saves are provenance-scoped, so wiring into nodes you
|
|
21
21
|
didn't create may be rejected.
|
|
22
22
|
|
|
@@ -25,19 +25,19 @@ in the developer's browser as you build.
|
|
|
25
25
|
1. **`bindWorkflow` first.** Every other tool refuses until the session is bound,
|
|
26
26
|
and every call after is pinned server-side to that one canvas.
|
|
27
27
|
2. **Read the in-band guides before building.** The deep how-to is served by the
|
|
28
|
-
platform itself via `readBuilderSkill
|
|
29
|
-
- `solution-design
|
|
30
|
-
- `workflow-building
|
|
31
|
-
- `workflow-testing
|
|
32
|
-
- `template-references
|
|
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
33
|
3. **Build ONE stage at a time.** The loop is: `saveWorkflow` one stage → `runTest`
|
|
34
34
|
→ read the trace → add the next stage based on that evidence. Never design or
|
|
35
|
-
pre-wire the whole graph up front
|
|
35
|
+
pre-wire the whole graph up front. You learn a node's real output fields by
|
|
36
36
|
running it, not by planning.
|
|
37
37
|
4. **Ground truth rules:** `getCanvas` is the real graph; a nodeType is valid only
|
|
38
38
|
if `getNodeCatalog` returned it (search it fresh per step, with a one-line
|
|
39
39
|
`task`); layout and node ids are not yours to set.
|
|
40
|
-
5. **The developer judges the result
|
|
40
|
+
5. **The developer judges the result. Never self-certify.** When the workflow runs
|
|
41
41
|
end-to-end with real, non-empty output, report what you built and what the test
|
|
42
42
|
showed. They watch the canvas live and accept, or ask for changes (which continue
|
|
43
43
|
on the same bound canvas).
|
|
@@ -47,6 +47,6 @@ in the developer's browser as you build.
|
|
|
47
47
|
| Symptom | Cause / fix |
|
|
48
48
|
|---|---|
|
|
49
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
|
|
51
|
-
| Tool calls error after working earlier | The platform restarted mid-session
|
|
52
|
-
| saveWorkflow rejects an edge into an existing node | Provenance guard: that node wasn't built this session
|
|
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 |
|