@unoverse-platform/studio 0.1.97 → 0.1.98

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.
Files changed (118) hide show
  1. package/bin/studio.mjs +2 -2
  2. package/local/catalog.ts +5 -1
  3. package/local/nodes.ts +28 -41
  4. package/local/plugin.ts +71 -13
  5. package/local/sample-identity/brand.yaml +24 -0
  6. package/local/sample-identity/organisation.yaml +14 -0
  7. package/local/sample-identity/purpose.yaml +14 -0
  8. package/local/sample-identity/story.yaml +18 -0
  9. package/local/scaffold.mjs +43 -23
  10. package/local/source.ts +3 -8
  11. package/package.json +1 -1
  12. package/screens/AgentsView.tsx +173 -0
  13. package/screens/ComponentsView.tsx +2 -2
  14. package/screens/EmptyLibrary.tsx +1 -1
  15. package/screens/IdentityView.tsx +3 -3
  16. package/screens/PromptBlocksView.tsx +1 -1
  17. package/screens/TemplatesLibraryView.tsx +91 -6
  18. package/screens/TemplatesView.tsx +24 -9
  19. package/screens/index.ts +2 -0
  20. package/src/App.tsx +8 -3
  21. package/src/host.ts +1 -14
  22. package/vendor/base/src/agent-mcp/discovery.ts +76 -18
  23. package/vendor/base/src/agent-mcp/handoff.ts +11 -31
  24. package/vendor/base/src/agent-mcp/index.ts +5 -5
  25. package/vendor/base/src/agent-mcp/invoke.ts +37 -19
  26. package/vendor/base/src/agent-mcp/lean.ts +19 -0
  27. package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
  28. package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
  29. package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
  30. package/vendor/base/src/agent-mcp/tools.ts +136 -7
  31. package/vendor/base/src/agent-mcp/types.ts +47 -10
  32. package/vendor/base/src/definitions/load/compile/component.ts +59 -4
  33. package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
  34. package/vendor/base/src/definitions/load/compile/template.ts +19 -15
  35. package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
  36. package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
  37. package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
  38. package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
  39. package/vendor/base/src/definitions/load/load.ts +37 -28
  40. package/vendor/base/src/definitions/model.ts +8 -12
  41. package/vendor/base/src/definitions/registry/agents.ts +111 -0
  42. package/vendor/base/src/definitions/registry/catalog.ts +28 -12
  43. package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
  44. package/vendor/base/src/definitions/registry/homes.ts +69 -5
  45. package/vendor/base/src/definitions/registry/objects.ts +413 -0
  46. package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
  47. package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
  48. package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
  49. package/vendor/base/src/definitions/theme.ts +1 -2
  50. package/vendor/base/src/items/collect.ts +29 -25
  51. package/vendor/base/src/items/publish.ts +30 -6
  52. package/vendor/base/src/lint/context.mjs +17 -0
  53. package/vendor/base/src/lint/design/file.mjs +199 -2
  54. package/vendor/base/src/lint/design/index.mjs +66 -73
  55. package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
  56. package/vendor/base/src/lint/design/walk.mjs +14 -4
  57. package/vendor/base/src/lint/index.mjs +15 -9
  58. package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
  59. package/vendor/base/src/lint/nodes/index.mjs +5 -0
  60. package/vendor/base/src/lint/nodes/node.mjs +157 -7
  61. package/vendor/base/src/lint/objects/index.d.mts +32 -0
  62. package/vendor/base/src/lint/objects/index.mjs +324 -0
  63. package/vendor/base/src/lint/objects/rules.d.mts +44 -0
  64. package/vendor/base/src/lint/objects/rules.mjs +446 -0
  65. package/vendor/base/src/lint/prompts/index.d.mts +34 -0
  66. package/vendor/base/src/lint/prompts/index.mjs +198 -0
  67. package/vendor/base/src/manifests/compose.ts +80 -9
  68. package/vendor/base/src/manifests/executor/callback.ts +15 -4
  69. package/vendor/base/src/manifests/executor/context.ts +9 -1
  70. package/vendor/base/src/manifests/executor/memory.ts +83 -3
  71. package/vendor/base/src/manifests/executor/session.ts +50 -0
  72. package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
  73. package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
  74. package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
  75. package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
  76. package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
  77. package/vendor/base/src/manifests/runtime/context.ts +18 -6
  78. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
  79. package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
  80. package/vendor/base/src/manifests/runtime/events.ts +39 -3
  81. package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
  82. package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
  83. package/vendor/base/src/manifests/runtime/index.ts +3 -2
  84. package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
  85. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
  86. package/vendor/base/src/manifests/runtime/usage.ts +10 -0
  87. package/vendor/base/src/paths.ts +16 -8
  88. package/vendor/base/src/platform/audioManager.ts +32 -0
  89. package/vendor/base/src/platform/executionContext.ts +14 -0
  90. package/vendor/base/src/platform/liveInput.ts +52 -0
  91. package/vendor/base/src/plugins/discovery.ts +1 -1
  92. package/vendor/base/src/template/SafeExpression.ts +6 -0
  93. package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
  94. package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
  95. package/vendor/sdk/appEngine.tsx +30 -11
  96. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
  97. package/vendor/sdk/lib/app.tsx +70 -95
  98. package/vendor/sdk/lib/connection.tsx +47 -57
  99. package/vendor/sdk/lib/core/actions.ts +6 -0
  100. package/vendor/sdk/lib/core/apps.ts +66 -81
  101. package/vendor/sdk/lib/core/client.ts +5 -14
  102. package/vendor/sdk/lib/core/connection.ts +30 -0
  103. package/vendor/sdk/lib/core/index.ts +2 -2
  104. package/vendor/sdk/lib/core/store.ts +90 -15
  105. package/vendor/sdk/lib/core/types.ts +9 -8
  106. package/vendor/sdk/lib/primitives.tsx +113 -4
  107. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
  108. package/vendor/sdk/lib/render.tsx +3 -3
  109. package/vendor/sdk/lib/voice.tsx +27 -13
  110. package/local/sample-identity/brand/brand.yaml +0 -88
  111. package/local/sample-identity/brand/manifest.yaml +0 -13
  112. package/local/sample-identity/organisation/manifest.yaml +0 -15
  113. package/local/sample-identity/organisation/organisation.yaml +0 -53
  114. package/local/sample-identity/purpose/manifest.yaml +0 -12
  115. package/local/sample-identity/purpose/purpose.yaml +0 -46
  116. package/local/sample-identity/story/manifest.yaml +0 -12
  117. package/local/sample-identity/story/story.yaml +0 -63
  118. package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
package/bin/studio.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * unoverse-studio — the authoring tool, run from a developer's own project.
4
4
  *
5
5
  * Studio is a TOOL, not a starter kit. A developer's repository holds only their assets
6
- * (rx/, prompts/, nodes/); this is installed alongside and reads them. That is why there
6
+ * (design/); this is installed alongside and reads them. That is why there
7
7
  * is no second repository to keep in step, and why their project contains no Docker, no
8
8
  * ansible and no platform source.
9
9
  *
@@ -143,7 +143,7 @@ async function scaffoldIfEmpty() {
143
143
  // publishes, so a flat rx/ could hold exactly one project and would read `components`
144
144
  // and `apps` as orgs.
145
145
  createFirstProject(home, name);
146
- console.log(`\n Created design/${name}/, prompts/ and nodes/ in ${cwd}\n`);
146
+ console.log(`\n Created design/${name}/ in ${cwd}\n`);
147
147
  }
148
148
 
149
149
  await scaffoldIfEmpty();
package/local/catalog.ts CHANGED
@@ -26,6 +26,8 @@ type Loaders = {
26
26
  listThemeNames: (org: string) => string[];
27
27
  /** The identity documents of one org or every org: `<org>/<doc>` items with manifest and briefed props. */
28
28
  listIdentity: (org?: string) => unknown[];
29
+ /** The Agents of one org or every org: `<org>/<name>` items with their meta and door. */
30
+ listAgents: (org?: string) => unknown[];
29
31
  resolveTheme: (name: string) => unknown;
30
32
  DEFAULT_ORG: string;
31
33
  /** A project's on-disk home. The writable set is built from these (see source.ts). */
@@ -46,7 +48,7 @@ export function findProjectRoot(from: string = process.cwd()): string {
46
48
  // A developer's project (starter kit): rx/ sits at the root.
47
49
  if (hasDesignHome(dir)) return dir;
48
50
  // This monorepo, where Studio's author works: rx/ lives under apps/unoverse. Same
49
- // shape underneath (rx/ + prompts/), just nested, so return that as the root.
51
+ // shape underneath (design/), just nested, so return that as the root.
50
52
  if (hasDesignHome(resolve(dir, "apps/unoverse"))) return resolve(dir, "apps/unoverse");
51
53
  const parent = resolve(dir, "..");
52
54
  if (parent === dir) break;
@@ -98,6 +100,7 @@ export async function loadCatalog(
98
100
  const inputs = await load(resolve(baseSrc, "definitions/inputs.ts"));
99
101
  const homes = await load(resolve(baseSrc, "definitions/registry/homes.ts"));
100
102
  const identity = await load(resolve(baseSrc, "definitions/registry/orgDocuments.ts"));
103
+ const agents = await load(resolve(baseSrc, "definitions/registry/agents.ts"));
101
104
 
102
105
  return {
103
106
  listDefinitions: defs.listDefinitions as Loaders["listDefinitions"],
@@ -120,6 +123,7 @@ export async function loadCatalog(
120
123
  }
121
124
  return out;
122
125
  }) as Loaders["listIdentity"],
126
+ listAgents: agents.listAgents as Loaders["listAgents"],
123
127
  resolveTheme: theme.resolveTheme as Loaders["resolveTheme"],
124
128
  DEFAULT_ORG: theme.DEFAULT_ORG as string,
125
129
  projectDir: defs.projectDir as Loaders["projectDir"],
package/local/nodes.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * The LOCAL node catalog and runner.
3
3
  *
4
- * Scope (decided 2026-07-25): the developer's OWN nodes only, the ones in their
5
- * project's `nodes/` folder. Local Studio never reads the installed plugins directory,
4
+ * Scope (decided 2026-07-25): the developer's OWN nodes only, the ones in each of their
5
+ * projects' `design/<project>/nodes/` folder (org-scoped since 2026-09-08, like skills;
6
+ * the workspace-root `nodes/` is gone). Local Studio never reads the installed plugins directory,
6
7
  * `installed_plugins`, or the marketplace. Acquiring a node is a universe's job; this
7
8
  * is developing the nodes you are writing.
8
9
  *
9
10
  * As with the definitions (catalog.ts), this reuses the server's REAL registration
10
- * path rather than scraping source: each package is loaded through `loadPlugin`, which
11
- * calls its `setup(api)` exactly as production does. So what Studio lists is what the
12
- * platform would register, including config schemas and credential types.
11
+ * path rather than scraping source: every package goes through `loadManifests` over a
12
+ * disk source, exactly as the server boots. So what Studio lists is what the platform
13
+ * would register, including config schemas and credential types.
13
14
  *
14
15
  * The bootstrap comment is explicit that this path touches neither Redis nor the
15
16
  * workflow service, which is what makes it usable with no backend.
@@ -29,57 +30,43 @@ export interface LocalNodes {
29
30
  /**
30
31
  * Load the developer's node packages into the in-memory registry.
31
32
  *
32
- * `projectRoot` is the folder holding `nodes/`. Returns null when there is no nodes/
33
- * folder at all, so Studio can simply omit the section rather than showing an error:
33
+ * `nodesDirs` is every project's `design/<project>/nodes/`. Returns null when none of
34
+ * them exists, so Studio can simply omit the section rather than showing an error:
34
35
  * plenty of developers only author design assets.
36
+ *
37
+ * YAML manifest nodes (package.yaml + nodes/<Node>/*.yaml) are the ONLY authoring
38
+ * format (DECLARATIVE_NODES.md §10, nothing stays code), so this is one pass over one
39
+ * disk source per project: local Studio reads the developer's folders, never a
40
+ * database, and loadManifests registers into the same registry getAllNodes() reads.
41
+ * The legacy code-package loader that ran ahead of this pass read a `NODES_PATH`
42
+ * variable paths.ts had stopped honouring, so it loaded nothing and is gone.
35
43
  */
36
44
  export async function loadLocalNodes(
37
45
  baseSrc: string,
38
46
  load: (id: string) => Promise<Record<string, unknown>>,
39
- projectRoot: string,
47
+ nodesDirs: string[],
40
48
  ): Promise<LocalNodes | null> {
41
- const nodesDir = resolve(projectRoot, "nodes");
42
- if (!existsSync(nodesDir)) return null;
43
-
44
- // Set BEFORE the modules load: paths.ts reads process.env at module load time.
45
- process.env.NODES_PATH = nodesDir;
49
+ const present = nodesDirs.filter((d) => existsSync(d));
50
+ if (!present.length) return null;
46
51
 
47
- const discovery = await load(resolve(baseSrc, "plugins/discovery.ts"));
48
- const loader = await load(resolve(baseSrc, "plugins/loader.ts"));
49
- const platform = await load(resolve(baseSrc, "platform/index.ts"));
50
52
  const registry = await load(resolve(baseSrc, "registry.ts"));
51
53
  const executor = await load(resolve(baseSrc, "executor.ts"));
54
+ const manifests = await load(resolve(baseSrc, "manifests/index.ts"));
55
+ const msource = await load(resolve(baseSrc, "manifests/source.ts"));
52
56
 
53
- const api = (platform.createNodeServiceAPI as () => unknown)();
54
- const packages = (discovery.discoverNodesHome as () => string[])();
55
-
56
- for (const name of packages) {
57
+ for (const dir of present) {
58
+ // One broken project must never stop the others loading. Same posture as the
59
+ // server's bootstrap, and the same reason: a bad asset cannot take the tool down.
57
60
  try {
58
- await (loader.loadPlugin as (n: string, a: unknown) => Promise<string[]>)(name, api);
61
+ const r = await (manifests.loadManifests as (s: unknown) => Promise<{ nodes: number; errors: string[] }>)(
62
+ (msource.diskSource as (d: string) => unknown)(dir),
63
+ );
64
+ for (const e of r.errors) console.warn(`[studio] manifest node: ${e}`);
59
65
  } catch (err) {
60
- // One broken package must never stop the others loading. Same posture as the
61
- // server's bootstrap, and the same reason: a bad asset cannot take the tool down.
62
- console.warn(`[studio] could not load node package ${name}: ${(err as Error).message}`);
66
+ console.warn(`[studio] nodes in ${dir} did not load: ${(err as Error).message}`);
63
67
  }
64
68
  }
65
69
 
66
- // YAML manifest nodes (package.yaml + nodes/<Node>/*.yaml) — the DEFAULT authoring
67
- // format, and what the scaffolded sample nodes are. The discovery above only finds
68
- // legacy CODE packages (a package.json with an entry point), so without this pass a
69
- // pure-YAML nodes/ folder — every new developer's — listed NOTHING at all. Explicit
70
- // disk source: local Studio reads the developer's folder, never a database, and
71
- // loadManifests registers into the same registry getAllNodes() reads.
72
- try {
73
- const manifests = await load(resolve(baseSrc, "manifests/index.ts"));
74
- const msource = await load(resolve(baseSrc, "manifests/source.ts"));
75
- const r = await (manifests.loadManifests as (s: unknown) => Promise<{ nodes: number; errors: string[] }>)(
76
- (msource.diskSource as (d: string) => unknown)(nodesDir),
77
- );
78
- for (const e of r.errors) console.warn(`[studio] manifest node: ${e}`);
79
- } catch (err) {
80
- console.warn(`[studio] manifest nodes did not load: ${(err as Error).message}`);
81
- }
82
-
83
70
  return {
84
71
  list: () => (registry.getAllNodes as () => unknown[])(),
85
72
  credentialTypes: () => (registry.getAllCredentialTypes as () => unknown[])(),
package/local/plugin.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Routes mirror the server's shapes exactly, because the screens parse those shapes:
10
10
  * GET /dev/orgs GET /dev/themes?org= GET /dev/components?org=
11
- * GET /dev/apps?org= GET /dev/atoms GET /dev/identity?org=
11
+ * GET /dev/apps?org= GET /dev/atoms GET /dev/identity?org= GET /dev/agents?org=
12
12
  * GET /local/theme?ref= GET /local/definition?uri=
13
13
  * GET /skills /skills/:name GET /prompt-blocks
14
14
  * GET|POST /registry/state (no database locally, see below)
@@ -92,7 +92,12 @@ function json(res: import("http").ServerResponse, body: unknown, status = 200):
92
92
  interface MarkdownEntry {
93
93
  /** Stable key the screens select on. Derived from the filename, like the server's. */
94
94
  id: string;
95
+ /** The LOOKUP name, org-qualified for an org's own skill or block. Round-trips verbatim. */
95
96
  name: string;
97
+ /** What a person reads. `name` carries the org, and a navigator printing it shows a path. */
98
+ title?: string;
99
+ /** The owning org. ABSENT means the universal tier, exactly as the server tags it. */
100
+ org?: string;
96
101
  description: string;
97
102
  category?: string;
98
103
  version?: string;
@@ -136,6 +141,7 @@ function readMarkdownLibrary(dir: string): MarkdownEntry[] {
136
141
  out.push({
137
142
  id,
138
143
  name: meta.name || id,
144
+ title: meta.title || meta.name || id,
139
145
  description: meta.description ?? "",
140
146
  category: meta.category,
141
147
  version: meta.version,
@@ -155,6 +161,37 @@ function readMarkdownLibrary(dir: string): MarkdownEntry[] {
155
161
  return out.sort((a, b) => a.name.localeCompare(b.name));
156
162
  }
157
163
 
164
+ /**
165
+ * A MARKDOWN LIBRARY HAS TWO TIERS, and this host only ever read one.
166
+ *
167
+ * A skill or a prompt block lives in one of two places, and the NAME's shape is what
168
+ * carries which:
169
+ *
170
+ * UNIVERSAL `design/marketplace/<folder>/`, named bare. Published to the marketplace.
171
+ * ORG `design/<org>/<folder>/`, named `<org>/<name>`. Private to that org.
172
+ *
173
+ * The universe server resolves both (the items loaders). This host read the universal
174
+ * home alone, so a developer's OWN skill was missing from local Studio while sitting
175
+ * correctly in their universe, and the two surfaces disagreed about the same folder.
176
+ *
177
+ * Scoping is the server's `?org=`: an org sees the universal tier plus its own and never
178
+ * another org's, the rule RX_ORG_MODEL.md settled for a project's assets.
179
+ */
180
+ function readLibrary(projectRoot: string, folder: "skills" | "blocks", orgs: string[], org?: string): MarkdownEntry[] {
181
+ const out = readMarkdownLibrary(resolve(designHome(projectRoot), "marketplace", folder));
182
+ for (const o of orgs) {
183
+ if (org && org !== "all" && o !== org) continue;
184
+ for (const e of readMarkdownLibrary(resolve(designHome(projectRoot), o, folder))) {
185
+ // A SKILL's lookup name carries its org, because that is the name every reader
186
+ // addresses it by and two orgs may hold one bare name. A BLOCK's does not: the
187
+ // server tags a block with its org and leaves the name alone, and the shape here
188
+ // has to be the server's or the screens read two different contracts.
189
+ out.push(folder === "skills" ? { ...e, name: `${o}/${e.id}`, org: o } : { ...e, org: o });
190
+ }
191
+ }
192
+ return out.sort((a, b) => a.name.localeCompare(b.name));
193
+ }
194
+
158
195
  /**
159
196
  * Files this server has just written, so the watcher can tell Studio's own save apart from
160
197
  * the developer's other editor. The window only has to outlast the filesystem event, which
@@ -205,7 +242,12 @@ export function localStudioHost(): Plugin {
205
242
  // Node packages load lazily: only when the Nodes section is first opened, so a
206
243
  // developer authoring only design assets never pays for it at startup.
207
244
  let nodesOnce: Promise<LocalNodes | null> | null = null;
208
- const nodes = () => (nodesOnce ??= loadLocalNodes(baseSrc, ssr, projectRoot));
245
+ // Every project's own nodes: design/<project>/nodes/, org-scoped like its skills.
246
+ const nodesDirs = async () => {
247
+ const c = await catalog;
248
+ return c.listOrgs().map((o) => resolve(c.projectDir(o), "nodes"));
249
+ };
250
+ const nodes = () => (nodesOnce ??= nodesDirs().then((dirs) => loadLocalNodes(baseSrc, ssr, dirs)));
209
251
 
210
252
  server.config.logger.info(`\n Studio reading: ${designHome(projectRoot)}\n`);
211
253
 
@@ -220,7 +262,7 @@ export function localStudioHost(): Plugin {
220
262
  *
221
263
  * Adding the paths puts them in the same watcher, so the existing hook fires.
222
264
  */
223
- for (const dir of [designHome(projectRoot), resolve(projectRoot, "prompts")]) {
265
+ for (const dir of [designHome(projectRoot)]) {
224
266
  const full = resolve(projectRoot, dir);
225
267
  if (existsSync(full)) server.watcher.add(full);
226
268
  }
@@ -255,7 +297,7 @@ export function localStudioHost(): Plugin {
255
297
  return json(res, {
256
298
  projectRoot,
257
299
  rx: designHome(projectRoot),
258
- nodes: resolve(projectRoot, "nodes"),
300
+ nodes: await nodesDirs(),
259
301
  envFiles: [resolve(projectRoot, "..", ".env"), resolve(projectRoot, ".env")],
260
302
  });
261
303
 
@@ -360,10 +402,14 @@ export function localStudioHost(): Plugin {
360
402
  case path === "/dev/apps":
361
403
  return json(res, { apps: c.listAppsForWorkbench(org) });
362
404
 
363
- // The org's identity documents (design/<org>/identity/<doc>/), one org or all.
405
+ // The org's identity documents (design/<org>/identity/<doc>.yaml), one org or all.
364
406
  case path === "/dev/identity":
365
407
  return json(res, { items: c.listIdentity(org) });
366
408
 
409
+ // Agents: the one file authored for the map, design/<org>/agents/<name>.yaml.
410
+ case path === "/dev/agents":
411
+ return json(res, { items: c.listAgents(org) });
412
+
367
413
  case path === "/dev/atoms":
368
414
  return json(res, { atoms: c.listDefinitions("atom") });
369
415
 
@@ -437,7 +483,14 @@ export function localStudioHost(): Plugin {
437
483
  if (!p || typeof b.text !== "string") return json(res, { error: "path and text are required" }, 400);
438
484
 
439
485
  type LintModule = { lintDefinitions(designRoot: string): { problems: { level?: string; file?: string }[] } };
486
+ type ObjectsLintModule = { lintObjects(designRoot: string): { problems: { level?: string; file?: string }[] } };
487
+ type PromptsLintModule = { lintPrompts(designRoot: string): { problems: { level?: string; file?: string }[] } };
440
488
  const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
489
+ // OBJECTS AND PIPELINES TOO (objects README 9a, 2026-09-10): the same third linter
490
+ // the publish gate runs, so a save here meets the sentence publish would print.
491
+ const objectsMod = (await ssr(`${baseSrc}/lint/objects/index.mjs`)) as unknown as ObjectsLintModule;
492
+ // SKILLS AND PROMPT BLOCKS TOO (2026-09-18): the fourth linter, same gate.
493
+ const promptsMod = (await ssr(`${baseSrc}/lint/prompts/index.mjs`)) as unknown as PromptsLintModule;
441
494
 
442
495
  // Marked BEFORE the write, so the filesystem event can never beat the mark.
443
496
  recentSelfWrites.set(resolve(p), Date.now());
@@ -446,7 +499,7 @@ export function localStudioHost(): Plugin {
446
499
  b.text as string,
447
500
  writableRoots(projectRoot, c),
448
501
  projectRoot,
449
- (designRoot) => lintMod.lintDefinitions(designRoot),
502
+ (designRoot) => ({ problems: [...lintMod.lintDefinitions(designRoot).problems, ...objectsMod.lintObjects(designRoot).problems, ...promptsMod.lintPrompts(designRoot).problems] }),
450
503
  );
451
504
  return json(res, result, result.saved ? 200 : result.error === "lint errors" ? 422 : 403);
452
505
  }
@@ -470,9 +523,14 @@ export function localStudioHost(): Plugin {
470
523
  options?: { overlay?: Record<string, string> },
471
524
  ): { problems: { level?: string; file?: string; line?: number; msg?: string }[] };
472
525
  };
526
+ type ObjectsLintModule = { lintObjects(designRoot: string, options?: { overlay?: Record<string, string> }): { problems: { level?: string; file?: string; line?: number; msg?: string }[] } };
527
+ type PromptsLintModule = { lintPrompts(designRoot: string, options?: { overlay?: Record<string, string> }): { problems: { level?: string; file?: string; line?: number; msg?: string }[] } };
473
528
  const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
529
+ const objectsMod = (await ssr(`${baseSrc}/lint/objects/index.mjs`)) as unknown as ObjectsLintModule;
530
+ const promptsMod = (await ssr(`${baseSrc}/lint/prompts/index.mjs`)) as unknown as PromptsLintModule;
474
531
  const abs = resolve(p);
475
- const { problems } = lintMod.lintDefinitions(designHome(projectRoot), { overlay: { [abs]: b.text as string } });
532
+ const overlay = { [abs]: b.text as string };
533
+ const problems = [...lintMod.lintDefinitions(designHome(projectRoot), { overlay }).problems, ...objectsMod.lintObjects(designHome(projectRoot), { overlay }).problems, ...promptsMod.lintPrompts(designHome(projectRoot), { overlay }).problems];
476
534
  return json(res, {
477
535
  findings: problems.filter((f) => f.file && resolve(f.file) === abs),
478
536
  elsewhere: problems.filter((f) => !f.file || resolve(f.file) !== abs).length,
@@ -511,17 +569,17 @@ export function localStudioHost(): Plugin {
511
569
  return json(res, selection ?? { empty: true });
512
570
 
513
571
  case path === "/skills":
514
- return json(res, { skills: readMarkdownLibrary(resolve(projectRoot, "prompts/skills")) });
572
+ return json(res, { skills: readLibrary(projectRoot, "skills", c.listOrgs(), org) });
515
573
 
516
574
  case path.startsWith("/skills/"): {
517
575
  const name = decodeURIComponent(path.slice("/skills/".length).replace(/\/file$/, ""));
518
- const found = readMarkdownLibrary(resolve(projectRoot, "prompts/skills")).find((s) => s.name === name || s.id === name);
576
+ const found = readLibrary(projectRoot, "skills", c.listOrgs()).find((s) => s.name === name || s.id === name);
519
577
  // SkillsView reads `d.skill`, matching the server's shape. Not the bare object.
520
578
  return found ? json(res, { skill: found }) : json(res, { error: "skill not found" }, 404);
521
579
  }
522
580
 
523
581
  case path === "/prompt-blocks":
524
- return json(res, { blocks: readMarkdownLibrary(resolve(projectRoot, "prompts/blocks")) });
582
+ return json(res, { blocks: readLibrary(projectRoot, "blocks", c.listOrgs(), org) });
525
583
 
526
584
  // Registry toggles are a DEPLOYED concept: they persist to installed_plugins
527
585
  // and decide what a running universe exposes. Local Studio has no database and
@@ -532,11 +590,11 @@ export function localStudioHost(): Plugin {
532
590
  if (req.method === "POST") return json(res, { ok: true });
533
591
  return json(res, { entries: [] });
534
592
 
535
- // ---- The developer's OWN nodes (their nodes/ folder). No marketplace. ----
593
+ // ---- The developer's OWN nodes (design/<project>/nodes/). No marketplace. ----
536
594
 
537
595
  case path === "/nodes": {
538
596
  const n = await nodes();
539
- if (!n) return json(res, { nodes: [], credentialTypes: [], reason: "no nodes/ folder in this project" });
597
+ if (!n) return json(res, { nodes: [], credentialTypes: [], reason: "no design/<project>/nodes/ folder in this workspace" });
540
598
  return json(res, { nodes: n.list(), credentialTypes: n.credentialTypes() });
541
599
  }
542
600
 
@@ -601,7 +659,7 @@ export function localStudioHost(): Plugin {
601
659
  * else, which means the developer's other editor, still reloads.
602
660
  */
603
661
  handleHotUpdate({ server, file }) {
604
- const watched = [designHome(projectRoot), resolve(projectRoot, "prompts")];
662
+ const watched = [designHome(projectRoot)];
605
663
  if (!watched.some((w) => file.startsWith(w))) return;
606
664
  const self = recentSelfWrites.get(resolve(file));
607
665
  if (self !== undefined && Date.now() - self < SELF_WRITE_WINDOW_MS) return [];
@@ -0,0 +1,24 @@
1
+ # The org's brand: its own words, and only those. The briefs, the limits and the shape are the
2
+ # brand object's (design/marketplace/objects/brand/); this file carries the answers as defaults.
3
+ unoverse: "1.0"
4
+ type: identity
5
+ extends: brand
6
+ props:
7
+ name:
8
+ default: ""
9
+ standsFor:
10
+ default: ""
11
+ promise:
12
+ default: ""
13
+ represents:
14
+ default: ""
15
+ positioning:
16
+ default: ""
17
+ differentiators:
18
+ default: []
19
+ brands:
20
+ default: []
21
+ rules:
22
+ default: []
23
+ examples:
24
+ default: []
@@ -0,0 +1,14 @@
1
+ # The org's organisation: its own words, and only those. The briefs, the limits and the shape are the
2
+ # organisation object's (design/marketplace/objects/organisation/); this file carries the answers as defaults.
3
+ unoverse: "1.0"
4
+ type: identity
5
+ extends: organisation
6
+ props:
7
+ name:
8
+ default: ""
9
+ what:
10
+ default: ""
11
+ units:
12
+ default: []
13
+ vocabulary:
14
+ default: ""
@@ -0,0 +1,14 @@
1
+ # The org's purpose: its own words, and only those. The briefs, the limits and the shape are the
2
+ # purpose object's (design/marketplace/objects/purpose/); this file carries the answers as defaults.
3
+ unoverse: "1.0"
4
+ type: identity
5
+ extends: purpose
6
+ props:
7
+ statement:
8
+ default: ""
9
+ values:
10
+ default: []
11
+ ambition:
12
+ default: ""
13
+ goals:
14
+ default: []
@@ -0,0 +1,18 @@
1
+ # The org's story: its own words, and only those. The briefs, the limits and the shape are the
2
+ # story object's (design/marketplace/objects/story/); this file carries the answers as defaults.
3
+ unoverse: "1.0"
4
+ type: identity
5
+ extends: story
6
+ props:
7
+ history:
8
+ default: ""
9
+ milestones:
10
+ default: []
11
+ present:
12
+ default: ""
13
+ vision:
14
+ default: ""
15
+ people:
16
+ default: ""
17
+ meaning:
18
+ default: ""
@@ -90,10 +90,10 @@ export function createProject(home, name) {
90
90
  cpSync(sampleStyles, join(root, "styles"), { recursive: true });
91
91
  }
92
92
 
93
- // THE IDENTITY (sample-identity: organisation, brand, purpose, story), four definitions
94
- // with their briefs written and their defaults empty, so the org's own words have a
95
- // place to land from day one. Mirrors @unoverse-platform/base identitySeed.ts file for
96
- // file (a guard test holds them equal). Same rule: copied only into an empty folder.
93
+ // THE IDENTITY (sample-identity: organisation, brand, purpose, story), four instances of
94
+ // the marketplace's identity objects with their defaults empty, so the org's own words
95
+ // have a place to land from day one. Generated by base's instanceText (a guard test holds
96
+ // them equal to what createOrgPack writes). Same rule: copied only into an empty folder.
97
97
  const sampleIdentity = join(dirname(fileURLToPath(import.meta.url)), "sample-identity");
98
98
  if (existsSync(sampleIdentity) && readdirSync(join(root, "identity")).length === 0) {
99
99
  cpSync(sampleIdentity, join(root, "identity"), { recursive: true });
@@ -124,7 +124,10 @@ export function createProject(home, name) {
124
124
  * makes that safe.
125
125
  */
126
126
  const SAMPLES = [
127
- { id: "nodes/samples", dir: "sample-nodes/samples", to: () => "nodes/samples" },
127
+ // ORG-SCOPED since 2026-09-08, like the skill and block below: a project's nodes live at
128
+ // design/<project>/nodes/, the same rule as every other kind it authors. The id keeps
129
+ // the old flat string for the same reason theirs do.
130
+ { id: "nodes/samples", dir: "sample-nodes/samples", to: (p) => `design/${p}/nodes/samples` },
128
131
  // ORG-SCOPED since 2026-08-24: a sample skill/block lands in the FIRST project's own
129
132
  // folder, because that is where authored skills and blocks now live. `to` is a
130
133
  // function of the project for exactly that reason — the id stays the old flat string
@@ -189,10 +192,10 @@ export function topUpSamples(home, project) {
189
192
  }
190
193
 
191
194
  /**
192
- * The FIRST project, plus the folders that live beside the design tree rather than inside it.
193
- *
194
- * Skills, prompt blocks and nodes are per-developer and not per-project, so they are made
195
- * once here and never by `createProject`.
195
+ * The FIRST project, plus what a workspace needs exactly once: editor settings and the
196
+ * worked-example samples. Nothing lives beside the design tree any more: skills, blocks
197
+ * and (since 2026-09-08) nodes are all per-project, made by `createProject` or landed by
198
+ * topUpSamples inside the first project.
196
199
  */
197
200
 
198
201
  /**
@@ -204,17 +207,34 @@ export function topUpSamples(home, project) {
204
207
  */
205
208
  const SCHEMAS = "https://docs.unoverse.ai/schemas/nodes";
206
209
  const NODE_SCHEMA_MAP = {
207
- [`${SCHEMAS}/package.schema.json`]: ["nodes/*/package.yaml"],
208
- [`${SCHEMAS}/credential.schema.json`]: ["nodes/*/credentials/*.yaml"],
209
- [`${SCHEMAS}/node.schema.json`]: ["nodes/*/nodes/*/node.yaml"],
210
- [`${SCHEMAS}/interface.schema.json`]: ["nodes/*/nodes/*/interface.yaml"],
211
- [`${SCHEMAS}/config.schema.json`]: ["nodes/*/nodes/*/config.yaml"],
212
- [`${SCHEMAS}/test.schema.json`]: ["nodes/*/nodes/*/test.yaml"],
213
- [`${SCHEMAS}/run.schema.json`]: ["nodes/*/nodes/*/api/run.yaml"],
214
- [`${SCHEMAS}/events.schema.json`]: ["nodes/*/nodes/*/api/events.yaml"],
215
- [`${SCHEMAS}/service.schema.json`]: ["nodes/*/nodes/*/api/service.yaml"],
216
- [`${SCHEMAS}/toolExchange.schema.json`]: ["nodes/*/nodes/*/api/toolExchange.yaml"],
217
- [`${SCHEMAS}/audio.schema.json`]: ["nodes/*/nodes/*/api/audio.yaml"],
210
+ [`${SCHEMAS}/package.schema.json`]: ["design/*/nodes/*/package.yaml"],
211
+ [`${SCHEMAS}/credential.schema.json`]: ["design/*/nodes/*/credentials/*.yaml"],
212
+ [`${SCHEMAS}/node.schema.json`]: ["design/*/nodes/*/nodes/*/node.yaml"],
213
+ [`${SCHEMAS}/interface.schema.json`]: ["design/*/nodes/*/nodes/*/interface.yaml"],
214
+ [`${SCHEMAS}/config.schema.json`]: ["design/*/nodes/*/nodes/*/config.yaml"],
215
+ [`${SCHEMAS}/test.schema.json`]: ["design/*/nodes/*/nodes/*/test.yaml"],
216
+ [`${SCHEMAS}/run.schema.json`]: ["design/*/nodes/*/nodes/*/api/run.yaml"],
217
+ [`${SCHEMAS}/events.schema.json`]: ["design/*/nodes/*/nodes/*/api/events.yaml"],
218
+ [`${SCHEMAS}/service.schema.json`]: ["design/*/nodes/*/nodes/*/api/service.yaml"],
219
+ [`${SCHEMAS}/toolExchange.schema.json`]: ["design/*/nodes/*/nodes/*/api/toolExchange.yaml"],
220
+ [`${SCHEMAS}/audio.schema.json`]: ["design/*/nodes/*/nodes/*/api/audio.yaml"],
221
+ // OBJECTS AND PIPELINES (objects README 9a, 2026-09-10): folders in the node grammar. The
222
+ // envelope is named for its folder, so its glob excludes the sibling files BY NAME, and
223
+ // api/service.yaml among them: in the YAML extension's matcher a `*` segment can cross
224
+ // `api/`, so without `service` in the exclusion the envelope schema claimed the functions
225
+ // file and red-underlined every key in it (seen live 2026-09-10, redhat.vscode-yaml 1.24.0).
226
+ [`${SCHEMAS.replace(/\/nodes$/, "/objects")}/object.schema.json`]: ["design/*/objects/*/!(props|test|service|make|dataset).yaml"],
227
+ // `dataset` joined the exclusion 2026-09-18: an object that owns a table declares its columns
228
+ // in dataset.yaml, and the envelope schema knows nothing of columns.
229
+ [`${SCHEMAS.replace(/\/nodes$/, "/objects")}/dataset.schema.json`]: ["design/*/objects/*/dataset.yaml"],
230
+ [`${SCHEMAS.replace(/\/nodes$/, "/objects")}/props.schema.json`]: ["design/*/objects/*/props.yaml"],
231
+ [`${SCHEMAS.replace(/\/nodes$/, "/objects")}/service.schema.json`]: ["design/*/objects/*/api/service.yaml"],
232
+ [`${SCHEMAS.replace(/\/nodes$/, "/objects")}/make.schema.json`]: ["design/*/objects/*/api/make.yaml"],
233
+ [`${SCHEMAS.replace(/\/nodes$/, "/objects")}/test.schema.json`]: ["design/*/objects/*/test.yaml"],
234
+ [`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/pipeline.schema.json`]: ["design/*/pipelines/*/pipeline.yaml"],
235
+ [`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/config.schema.json`]: ["design/*/pipelines/*/config.yaml"],
236
+ [`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/interface.schema.json`]: ["design/*/pipelines/*/interface.yaml"],
237
+ [`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/test.schema.json`]: ["design/*/pipelines/*/test.yaml"],
218
238
  };
219
239
  export function writeEditorSettings(home) {
220
240
  const dir = join(home, ".vscode");
@@ -227,9 +247,9 @@ export function writeEditorSettings(home) {
227
247
 
228
248
  export function createFirstProject(home, name) {
229
249
  const root = createProject(home, name);
230
- // Only nodes/ remains beside the design tree; skills/ and blocks/ moved INTO the org
231
- // (createProject makes them) so every authored kind sits in one place.
232
- for (const d of ["nodes"]) mkdirSync(join(home, d), { recursive: true });
250
+ // NOTHING beside the design tree. skills/ and blocks/ moved INTO the org on 2026-08-24
251
+ // and nodes/ followed on 2026-09-08 (the sample package lands at design/<org>/nodes/),
252
+ // so every authored kind sits in one place.
233
253
  writeEditorSettings(home);
234
254
 
235
255
  topUpSamples(home, name);
package/local/source.ts CHANGED
@@ -14,9 +14,8 @@
14
14
  * install, and §Design-system resolution is what keeps the token contract stable.
15
15
  *
16
16
  * So the writable set is exactly the developer's own work:
17
- * rx/<project>/** every project listOrgs() reports, via projectDir()
18
- * prompts/** skills and prompt blocks
19
- * nodes/** their own node packages
17
+ * design/<project>/** every project listOrgs() reports, via projectDir(); their own
18
+ * node packages live inside it (design/<project>/nodes/)
20
19
  * Everything else is readable and not writable, the marketplace and anything resolved out
21
20
  * of node_modules included.
22
21
  *
@@ -51,11 +50,7 @@ export function writableRoots(
51
50
  projectRoot: string,
52
51
  loaders: { listOrgs: () => string[]; projectDir: (name: string) => string },
53
52
  ): string[] {
54
- const roots = [
55
- ...loaders.listOrgs().map((org) => loaders.projectDir(org)),
56
- resolve(projectRoot, "prompts"),
57
- resolve(projectRoot, "nodes"),
58
- ];
53
+ const roots = loaders.listOrgs().map((org) => loaders.projectDir(org));
59
54
  return roots.map((r) => resolve(r)).filter((r) => existsSync(r));
60
55
  }
61
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.97",
3
+ "version": "0.1.98",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {