@unoverse-platform/studio 0.1.96 → 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 +3 -3
  2. package/local/authoring.ts +1 -1
  3. package/local/catalog.ts +22 -2
  4. package/local/keys.ts +1 -1
  5. package/local/nodes.ts +29 -42
  6. package/local/plugin.ts +75 -13
  7. package/local/sample-identity/brand.yaml +24 -0
  8. package/local/sample-identity/organisation.yaml +14 -0
  9. package/local/sample-identity/purpose.yaml +14 -0
  10. package/local/sample-identity/story.yaml +18 -0
  11. package/local/scaffold.mjs +51 -22
  12. package/local/source.ts +4 -9
  13. package/package.json +1 -1
  14. package/screens/AgentsView.tsx +173 -0
  15. package/screens/ComponentsView.tsx +2 -2
  16. package/screens/EmptyLibrary.tsx +1 -1
  17. package/screens/IdentityHeader.tsx +1 -1
  18. package/screens/IdentityView.tsx +206 -0
  19. package/screens/PromptBlocksView.tsx +1 -1
  20. package/screens/SourceView.tsx +1 -1
  21. package/screens/TemplatesLibraryView.tsx +91 -6
  22. package/screens/TemplatesView.tsx +24 -9
  23. package/screens/index.ts +5 -2
  24. package/src/App.tsx +14 -4
  25. package/src/NodeKeys.tsx +1 -1
  26. package/src/host.ts +3 -16
  27. package/vendor/base/src/agent-mcp/discovery.ts +76 -18
  28. package/vendor/base/src/agent-mcp/handoff.ts +11 -31
  29. package/vendor/base/src/agent-mcp/index.ts +5 -5
  30. package/vendor/base/src/agent-mcp/invoke.ts +37 -19
  31. package/vendor/base/src/agent-mcp/lean.ts +19 -0
  32. package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
  33. package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
  34. package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
  35. package/vendor/base/src/agent-mcp/tools.ts +136 -7
  36. package/vendor/base/src/agent-mcp/types.ts +47 -10
  37. package/vendor/base/src/definitions/load/compile/component.ts +59 -4
  38. package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
  39. package/vendor/base/src/definitions/load/compile/template.ts +19 -15
  40. package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
  41. package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
  42. package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
  43. package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
  44. package/vendor/base/src/definitions/load/load.ts +37 -28
  45. package/vendor/base/src/definitions/model.ts +8 -12
  46. package/vendor/base/src/definitions/registry/agents.ts +111 -0
  47. package/vendor/base/src/definitions/registry/catalog.ts +28 -12
  48. package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
  49. package/vendor/base/src/definitions/registry/homes.ts +69 -5
  50. package/vendor/base/src/definitions/registry/objects.ts +413 -0
  51. package/vendor/base/src/definitions/registry/orgDocuments.ts +107 -0
  52. package/vendor/base/src/definitions/registry/orgPack.ts +50 -0
  53. package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
  54. package/vendor/base/src/definitions/theme.ts +1 -2
  55. package/vendor/base/src/items/collect.ts +33 -25
  56. package/vendor/base/src/items/publish.ts +31 -7
  57. package/vendor/base/src/lint/context.mjs +17 -0
  58. package/vendor/base/src/lint/design/file.mjs +199 -2
  59. package/vendor/base/src/lint/design/index.mjs +66 -73
  60. package/vendor/base/src/lint/design/vocabulary.mjs +6 -3
  61. package/vendor/base/src/lint/design/walk.mjs +14 -4
  62. package/vendor/base/src/lint/index.mjs +15 -9
  63. package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
  64. package/vendor/base/src/lint/nodes/index.mjs +6 -1
  65. package/vendor/base/src/lint/nodes/node.mjs +157 -7
  66. package/vendor/base/src/lint/objects/index.d.mts +32 -0
  67. package/vendor/base/src/lint/objects/index.mjs +324 -0
  68. package/vendor/base/src/lint/objects/rules.d.mts +44 -0
  69. package/vendor/base/src/lint/objects/rules.mjs +446 -0
  70. package/vendor/base/src/lint/prompts/index.d.mts +34 -0
  71. package/vendor/base/src/lint/prompts/index.mjs +198 -0
  72. package/vendor/base/src/manifests/compose.ts +81 -10
  73. package/vendor/base/src/manifests/executor/callback.ts +15 -4
  74. package/vendor/base/src/manifests/executor/context.ts +12 -1
  75. package/vendor/base/src/manifests/executor/memory.ts +83 -3
  76. package/vendor/base/src/manifests/executor/session.ts +50 -0
  77. package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
  78. package/vendor/base/src/manifests/index.ts +1 -1
  79. package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
  80. package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
  81. package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
  82. package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
  83. package/vendor/base/src/manifests/runtime/context.ts +18 -6
  84. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
  85. package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
  86. package/vendor/base/src/manifests/runtime/events.ts +39 -3
  87. package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
  88. package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
  89. package/vendor/base/src/manifests/runtime/index.ts +3 -2
  90. package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
  91. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
  92. package/vendor/base/src/manifests/runtime/usage.ts +10 -0
  93. package/vendor/base/src/manifests/source.ts +1 -1
  94. package/vendor/base/src/paths.ts +16 -8
  95. package/vendor/base/src/platform/audioManager.ts +32 -0
  96. package/vendor/base/src/platform/executionContext.ts +14 -0
  97. package/vendor/base/src/platform/liveInput.ts +52 -0
  98. package/vendor/base/src/plugins/README.md +1 -1
  99. package/vendor/base/src/plugins/discovery.ts +1 -1
  100. package/vendor/base/src/template/SafeExpression.ts +6 -0
  101. package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
  102. package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
  103. package/vendor/sdk/appEngine.tsx +31 -12
  104. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
  105. package/vendor/sdk/lib/app.tsx +70 -95
  106. package/vendor/sdk/lib/connection.tsx +47 -57
  107. package/vendor/sdk/lib/core/actions.ts +6 -0
  108. package/vendor/sdk/lib/core/apps.ts +66 -81
  109. package/vendor/sdk/lib/core/client.ts +5 -14
  110. package/vendor/sdk/lib/core/connection.ts +30 -0
  111. package/vendor/sdk/lib/core/index.ts +2 -2
  112. package/vendor/sdk/lib/core/store.ts +90 -15
  113. package/vendor/sdk/lib/core/types.ts +9 -8
  114. package/vendor/sdk/lib/primitives.tsx +113 -4
  115. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
  116. package/vendor/sdk/lib/render.tsx +3 -3
  117. package/vendor/sdk/lib/voice.tsx +27 -13
  118. package/vite.config.ts +1 -1
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
  *
@@ -14,7 +14,7 @@
14
14
  * to avoid. So the tool starts the same dev server `npm run dev` does.
15
15
  *
16
16
  * No database, no Redis, no engine, no Docker, and no account: those belong to running a
17
- * universe, which is a different job. See docs/architecture/LOCAL_STUDIO.md.
17
+ * universe, which is a different job. See docs/architecture/authoring/LOCAL_STUDIO.md.
18
18
  */
19
19
  import { createServer } from "vite";
20
20
  import { dirname, resolve, join, basename } from "path";
@@ -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();
@@ -16,7 +16,7 @@
16
16
  * completion list is not a hint, it is the whole legal set, and it knows the developer's
17
17
  * real tokens in a way a JSON Schema in a text editor cannot.
18
18
  *
19
- * See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
19
+ * See docs/architecture/authoring/LOCAL_STUDIO.md §Authoring in Studio.
20
20
  */
21
21
  import { designHome } from "./design-home.mjs";
22
22
  import { resolve } from "path";
package/local/catalog.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * the exact problem this migration exists to fix). It imports the SAME modules and sets
9
9
  * RX_PATH to the developer's folder before they load.
10
10
  *
11
- * See docs/architecture/LOCAL_STUDIO.md.
11
+ * See docs/architecture/authoring/LOCAL_STUDIO.md.
12
12
  */
13
13
  import { hasDesignHome } from "./design-home.mjs";
14
14
  import { resolve } from "path";
@@ -24,6 +24,10 @@ type Loaders = {
24
24
  inputPropKeys: (props: unknown) => string[];
25
25
  themeOrgs: () => string[];
26
26
  listThemeNames: (org: string) => string[];
27
+ /** The identity documents of one org or every org: `<org>/<doc>` items with manifest and briefed props. */
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[];
27
31
  resolveTheme: (name: string) => unknown;
28
32
  DEFAULT_ORG: string;
29
33
  /** A project's on-disk home. The writable set is built from these (see source.ts). */
@@ -44,7 +48,7 @@ export function findProjectRoot(from: string = process.cwd()): string {
44
48
  // A developer's project (starter kit): rx/ sits at the root.
45
49
  if (hasDesignHome(dir)) return dir;
46
50
  // This monorepo, where Studio's author works: rx/ lives under apps/unoverse. Same
47
- // shape underneath (rx/ + prompts/), just nested, so return that as the root.
51
+ // shape underneath (design/), just nested, so return that as the root.
48
52
  if (hasDesignHome(resolve(dir, "apps/unoverse"))) return resolve(dir, "apps/unoverse");
49
53
  const parent = resolve(dir, "..");
50
54
  if (parent === dir) break;
@@ -94,6 +98,9 @@ export async function loadCatalog(
94
98
  const defs = await load(resolve(baseSrc, "definitions/definitions.ts"));
95
99
  const theme = await load(resolve(baseSrc, "definitions/theme.ts"));
96
100
  const inputs = await load(resolve(baseSrc, "definitions/inputs.ts"));
101
+ const homes = await load(resolve(baseSrc, "definitions/registry/homes.ts"));
102
+ const identity = await load(resolve(baseSrc, "definitions/registry/orgDocuments.ts"));
103
+ const agents = await load(resolve(baseSrc, "definitions/registry/agents.ts"));
97
104
 
98
105
  return {
99
106
  listDefinitions: defs.listDefinitions as Loaders["listDefinitions"],
@@ -104,6 +111,19 @@ export async function loadCatalog(
104
111
  inputPropKeys: inputs.inputPropKeys as Loaders["inputPropKeys"],
105
112
  themeOrgs: theme.themeOrgs as Loaders["themeOrgs"],
106
113
  listThemeNames: theme.listThemeNames as Loaders["listThemeNames"],
114
+ listIdentity: ((org?: string) => {
115
+ const orgs: string[] = org && org !== "all" ? [org] : (homes.listOrgs as () => string[])();
116
+ const out: unknown[] = [];
117
+ for (const o of orgs) {
118
+ const root = (homes.projectDir as (n: string) => string)(o);
119
+ for (const doc of identity.ORG_DOCUMENTS as readonly string[]) {
120
+ const read = (identity.readOrgDocument as (r: string, d: string) => { manifest: unknown; props: unknown; path: string } | null)(root, doc);
121
+ if (read) out.push({ name: `${o}/${doc}`, org: o, doc, manifest: read.manifest, props: read.props, path: read.path });
122
+ }
123
+ }
124
+ return out;
125
+ }) as Loaders["listIdentity"],
126
+ listAgents: agents.listAgents as Loaders["listAgents"],
107
127
  resolveTheme: theme.resolveTheme as Loaders["resolveTheme"],
108
128
  DEFAULT_ORG: theme.DEFAULT_ORG as string,
109
129
  projectDir: defs.projectDir as Loaders["projectDir"],
package/local/keys.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * snake-cased, so `airtableCredential.personalAccessToken` reads from
14
14
  * `AIRTABLE_PERSONAL_ACCESS_TOKEN`.
15
15
  *
16
- * See docs/architecture/LOCAL_STUDIO.md.
16
+ * See docs/architecture/authoring/LOCAL_STUDIO.md.
17
17
  */
18
18
  import { resolve } from "path";
19
19
  import { existsSync, readFileSync } from "fs";
package/local/nodes.ts CHANGED
@@ -1,20 +1,21 @@
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.
16
17
  *
17
- * See docs/architecture/LOCAL_STUDIO.md.
18
+ * See docs/architecture/authoring/LOCAL_STUDIO.md.
18
19
  */
19
20
  import { resolve } from "path";
20
21
  import { existsSync } from "fs";
@@ -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,12 +8,12 @@
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
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)
15
15
  *
16
- * See docs/architecture/LOCAL_STUDIO.md.
16
+ * See docs/architecture/authoring/LOCAL_STUDIO.md.
17
17
  */
18
18
  import { designHome } from "./design-home.mjs";
19
19
  import { createRequire } from "node:module";
@@ -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,6 +402,14 @@ export function localStudioHost(): Plugin {
360
402
  case path === "/dev/apps":
361
403
  return json(res, { apps: c.listAppsForWorkbench(org) });
362
404
 
405
+ // The org's identity documents (design/<org>/identity/<doc>.yaml), one org or all.
406
+ case path === "/dev/identity":
407
+ return json(res, { items: c.listIdentity(org) });
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
+
363
413
  case path === "/dev/atoms":
364
414
  return json(res, { atoms: c.listDefinitions("atom") });
365
415
 
@@ -433,7 +483,14 @@ export function localStudioHost(): Plugin {
433
483
  if (!p || typeof b.text !== "string") return json(res, { error: "path and text are required" }, 400);
434
484
 
435
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 }[] } };
436
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;
437
494
 
438
495
  // Marked BEFORE the write, so the filesystem event can never beat the mark.
439
496
  recentSelfWrites.set(resolve(p), Date.now());
@@ -442,7 +499,7 @@ export function localStudioHost(): Plugin {
442
499
  b.text as string,
443
500
  writableRoots(projectRoot, c),
444
501
  projectRoot,
445
- (designRoot) => lintMod.lintDefinitions(designRoot),
502
+ (designRoot) => ({ problems: [...lintMod.lintDefinitions(designRoot).problems, ...objectsMod.lintObjects(designRoot).problems, ...promptsMod.lintPrompts(designRoot).problems] }),
446
503
  );
447
504
  return json(res, result, result.saved ? 200 : result.error === "lint errors" ? 422 : 403);
448
505
  }
@@ -466,9 +523,14 @@ export function localStudioHost(): Plugin {
466
523
  options?: { overlay?: Record<string, string> },
467
524
  ): { problems: { level?: string; file?: string; line?: number; msg?: string }[] };
468
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 }[] } };
469
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;
470
531
  const abs = resolve(p);
471
- 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];
472
534
  return json(res, {
473
535
  findings: problems.filter((f) => f.file && resolve(f.file) === abs),
474
536
  elsewhere: problems.filter((f) => !f.file || resolve(f.file) !== abs).length,
@@ -507,17 +569,17 @@ export function localStudioHost(): Plugin {
507
569
  return json(res, selection ?? { empty: true });
508
570
 
509
571
  case path === "/skills":
510
- return json(res, { skills: readMarkdownLibrary(resolve(projectRoot, "prompts/skills")) });
572
+ return json(res, { skills: readLibrary(projectRoot, "skills", c.listOrgs(), org) });
511
573
 
512
574
  case path.startsWith("/skills/"): {
513
575
  const name = decodeURIComponent(path.slice("/skills/".length).replace(/\/file$/, ""));
514
- 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);
515
577
  // SkillsView reads `d.skill`, matching the server's shape. Not the bare object.
516
578
  return found ? json(res, { skill: found }) : json(res, { error: "skill not found" }, 404);
517
579
  }
518
580
 
519
581
  case path === "/prompt-blocks":
520
- return json(res, { blocks: readMarkdownLibrary(resolve(projectRoot, "prompts/blocks")) });
582
+ return json(res, { blocks: readLibrary(projectRoot, "blocks", c.listOrgs(), org) });
521
583
 
522
584
  // Registry toggles are a DEPLOYED concept: they persist to installed_plugins
523
585
  // and decide what a running universe exposes. Local Studio has no database and
@@ -528,11 +590,11 @@ export function localStudioHost(): Plugin {
528
590
  if (req.method === "POST") return json(res, { ok: true });
529
591
  return json(res, { entries: [] });
530
592
 
531
- // ---- The developer's OWN nodes (their nodes/ folder). No marketplace. ----
593
+ // ---- The developer's OWN nodes (design/<project>/nodes/). No marketplace. ----
532
594
 
533
595
  case path === "/nodes": {
534
596
  const n = await nodes();
535
- 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" });
536
598
  return json(res, { nodes: n.list(), credentialTypes: n.credentialTypes() });
537
599
  }
538
600
 
@@ -597,7 +659,7 @@ export function localStudioHost(): Plugin {
597
659
  * else, which means the developer's other editor, still reloads.
598
660
  */
599
661
  handleHotUpdate({ server, file }) {
600
- const watched = [designHome(projectRoot), resolve(projectRoot, "prompts")];
662
+ const watched = [designHome(projectRoot)];
601
663
  if (!watched.some((w) => file.startsWith(w))) return;
602
664
  const self = recentSelfWrites.get(resolve(file));
603
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: ""
@@ -11,7 +11,7 @@
11
11
  * alike and disagree about what a starting component contains. So this file is plain ESM
12
12
  * rather than TypeScript, because the CLI runs unbuilt and cannot transpile.
13
13
  *
14
- * See docs/architecture/LOCAL_STUDIO.md.
14
+ * See docs/architecture/authoring/LOCAL_STUDIO.md.
15
15
  */
16
16
  import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, readFileSync } from "node:fs";
17
17
  import { designHome } from "./design-home.mjs";
@@ -61,7 +61,7 @@ const WELCOME = [
61
61
  ].join("\n");
62
62
 
63
63
  /**
64
- * Create `design/<name>/` under `home`, with its three authoring folders and one component.
64
+ * Create `design/<name>/` under `home`, with its authoring folders, one component and the identity.
65
65
  *
66
66
  * Refuses an existing name rather than merging into it: "create" that silently adopts a
67
67
  * folder is how a developer ends up publishing someone else's work under their own project.
@@ -77,7 +77,7 @@ export function createProject(home, name) {
77
77
  // beside components/apps/styles so a skill is org-scoped exactly like a component —
78
78
  // two orgs may each ship an `hr-education-coach` without colliding on one flat name
79
79
  // (items/collect.ts qualifies a skill's row `<org>/<name>`).
80
- for (const d of ["components/welcome", "apps", "styles", "skills", "blocks"]) mkdirSync(join(root, d), { recursive: true });
80
+ for (const d of ["components/welcome", "apps", "styles", "skills", "blocks", "identity"]) mkdirSync(join(root, d), { recursive: true });
81
81
  writeFileSync(join(root, "components/welcome/welcome.yaml"), WELCOME);
82
82
 
83
83
  // Worked example styles (sample-styles: brand ingredients + the override rules), so
@@ -90,6 +90,15 @@ 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 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
+ const sampleIdentity = join(dirname(fileURLToPath(import.meta.url)), "sample-identity");
98
+ if (existsSync(sampleIdentity) && readdirSync(join(root, "identity")).length === 0) {
99
+ cpSync(sampleIdentity, join(root, "identity"), { recursive: true });
100
+ }
101
+
93
102
  // A worked example APP (a generic chat: focus overlay + the chat region),
94
103
  // so apps/ is never an empty folder and the first app is a rename away. Same
95
104
  // rule as the styles: copied only into an empty folder, then the developer's.
@@ -115,7 +124,10 @@ export function createProject(home, name) {
115
124
  * makes that safe.
116
125
  */
117
126
  const SAMPLES = [
118
- { 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` },
119
131
  // ORG-SCOPED since 2026-08-24: a sample skill/block lands in the FIRST project's own
120
132
  // folder, because that is where authored skills and blocks now live. `to` is a
121
133
  // function of the project for exactly that reason — the id stays the old flat string
@@ -180,10 +192,10 @@ export function topUpSamples(home, project) {
180
192
  }
181
193
 
182
194
  /**
183
- * The FIRST project, plus the folders that live beside the design tree rather than inside it.
184
- *
185
- * Skills, prompt blocks and nodes are per-developer and not per-project, so they are made
186
- * 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.
187
199
  */
188
200
 
189
201
  /**
@@ -195,17 +207,34 @@ export function topUpSamples(home, project) {
195
207
  */
196
208
  const SCHEMAS = "https://docs.unoverse.ai/schemas/nodes";
197
209
  const NODE_SCHEMA_MAP = {
198
- [`${SCHEMAS}/package.schema.json`]: ["nodes/*/package.yaml"],
199
- [`${SCHEMAS}/credential.schema.json`]: ["nodes/*/credentials/*.yaml"],
200
- [`${SCHEMAS}/node.schema.json`]: ["nodes/*/nodes/*/node.yaml"],
201
- [`${SCHEMAS}/interface.schema.json`]: ["nodes/*/nodes/*/interface.yaml"],
202
- [`${SCHEMAS}/config.schema.json`]: ["nodes/*/nodes/*/config.yaml"],
203
- [`${SCHEMAS}/test.schema.json`]: ["nodes/*/nodes/*/test.yaml"],
204
- [`${SCHEMAS}/run.schema.json`]: ["nodes/*/nodes/*/api/run.yaml"],
205
- [`${SCHEMAS}/events.schema.json`]: ["nodes/*/nodes/*/api/events.yaml"],
206
- [`${SCHEMAS}/service.schema.json`]: ["nodes/*/nodes/*/api/service.yaml"],
207
- [`${SCHEMAS}/toolExchange.schema.json`]: ["nodes/*/nodes/*/api/toolExchange.yaml"],
208
- [`${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"],
209
238
  };
210
239
  export function writeEditorSettings(home) {
211
240
  const dir = join(home, ".vscode");
@@ -218,9 +247,9 @@ export function writeEditorSettings(home) {
218
247
 
219
248
  export function createFirstProject(home, name) {
220
249
  const root = createProject(home, name);
221
- // Only nodes/ remains beside the design tree; skills/ and blocks/ moved INTO the org
222
- // (createProject makes them) so every authored kind sits in one place.
223
- 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.
224
253
  writeEditorSettings(home);
225
254
 
226
255
  topUpSamples(home, name);