@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
@@ -67,7 +67,7 @@ export function lintDefinitions(designRoot, options = {}) {
67
67
  try {
68
68
  return (
69
69
  statSync(d).isDirectory() &&
70
- ["components", "atoms", "styles", "apps"].some((s) => existsSync(join(d, s)))
70
+ ["components", "atoms", "styles", "apps", "agents"].some((s) => existsSync(join(d, s)))
71
71
  );
72
72
  } catch {
73
73
  return false;
@@ -96,9 +96,7 @@ export function lintDefinitions(designRoot, options = {}) {
96
96
  const DS = (() => {
97
97
  const onDisk = join(DESIGN_ROOT, "marketplace");
98
98
  if (existsSync(onDisk)) return onDisk;
99
- const nodesHome = resolve(DESIGN_ROOT, "..", "nodes");
100
99
  for (const c of [
101
- join(nodesHome, "marketplace", "definitions"),
102
100
  join(resolve(DESIGN_ROOT, "..", "plugins"), "node_modules", "@unoverse-platform", "marketplace", "definitions"),
103
101
  ])
104
102
  if (existsSync(c)) return c;
@@ -190,6 +188,9 @@ const isAppPath = (f) => homeSegments(f).includes("apps");
190
188
  // carry no manifest and declare no state tree, the face being the filename (§9b rule 7).
191
189
  const isTemplatePath = (f) => homeSegments(f).includes("templates");
192
190
  const isTemplatePart = (f) => isTemplatePath(f) && homeSegments(f).includes("components");
191
+ // THE AGENT KIND (docs.unoverse.ai/design/tasks, 2026-09-06): one file under <org>/agents/,
192
+ // a workflow listed for the map. Not a UI tree, so the tree rules do not apply; its own check does.
193
+ const isAgentPath = (f) => homeSegments(f).includes("agents");
193
194
  // The DEFINITION ROOT folder for a file: a bare partial lives one level under it
194
195
  // (layouts/ states/ components/ blocks/); every `$include` resolves against it.
195
196
  const defRoot = (file) => {
@@ -597,7 +598,7 @@ const keyHomes = [
597
598
  [join(DS, "components"), true],
598
599
  ];
599
600
  for (const orgDir of orgDirs) {
600
- keyHomes.push([join(orgDir, "components"), true], [join(orgDir, "apps"), true]);
601
+ keyHomes.push([join(orgDir, "components"), true], [join(orgDir, "apps"), true], [join(orgDir, "agents"), false]);
601
602
  }
602
603
  for (const [dir, isDir] of keyHomes) {
603
604
  if (!existsSync(dir)) continue;
@@ -638,7 +639,9 @@ for (const [dir, isDir] of keyHomes) {
638
639
  // carries are ordinary components, so they were never exempt from anything.
639
640
  const homes = [{ dir: join(DS, "components") }, { dir: join(DS, "atoms") }, { dir: join(DS, "templates") }];
640
641
  for (const orgDir of orgDirs) {
641
- for (const sub of ["apps", "components", "templates"]) {
642
+ // `agents` (2026-09-06): a kind is linted; a home not enumerated is a home where every
643
+ // rule is off, silently.
644
+ for (const sub of ["apps", "components", "templates", "agents"]) {
642
645
  const d = join(orgDir, sub);
643
646
  if (existsSync(d) && statSync(d).isDirectory()) homes.push({ dir: d });
644
647
  }
@@ -701,64 +704,27 @@ for (const orgDir of orgDirs) {
701
704
  );
702
705
  }
703
706
 
704
- /**
705
- * ── the LATCH, on a state (docs/MCP_COMPLETE_GUIDE.md §The Component Latch) ──
706
- *
707
- * Declaring it makes whatever wears that face ADDRESSABLE: the pill goes up and the guest's
708
- * next sentences travel to that instance instead of opening a new answer. Every finding here
709
- * is something that produces a pill which looks fine and behaves wrongly, which is why they
710
- * are errors rather than warnings.
711
- *
712
- * Read from the ENVELOPE first and the manifest second, the same order the loader reads the
713
- * states tree in — a rule that judged only one home would pass whatever was authored in the
714
- * other.
715
- */
716
- const LATCH_KEYS = new Set(["title", "icon"]);
707
+ // ── `latch`, RETIRED (2026-09-07) ──
708
+ // The conversation is never "addressed at" a component: what is on screen is the server's
709
+ // fact and the model reads it. A leftover block is an intent nothing honours, so it errors.
717
710
  for (const orgDir of orgDirs) {
718
711
  const adir = join(orgDir, "apps");
719
712
  if (!(existsSync(adir) && statSync(adir).isDirectory())) continue;
720
713
  for (const e of readdirSync(adir)) {
721
714
  const folder = join(adir, e);
722
- let src, states;
723
715
  try {
724
716
  if (!statSync(folder).isDirectory()) continue;
725
- // The envelope (`<name>.yaml`) is the app tree's home; the manifest is the legacy one.
726
- const envelope = defPath(folder, e);
727
- const manifest = defPath(folder, "manifest");
728
- for (const p of [envelope, manifest]) {
717
+ for (const p of [defPath(folder, e), defPath(folder, "manifest")]) {
729
718
  if (!p || !existsSync(p)) continue;
730
- const s = readDef(p).states;
731
- if (s && typeof s === "object" && !Array.isArray(s)) {
732
- src = p;
733
- states = s;
734
- break;
735
- }
719
+ const states = readDef(p).states;
720
+ if (!states || typeof states !== "object" || Array.isArray(states)) continue;
721
+ for (const [sname, sentry] of Object.entries(states))
722
+ if (sentry && typeof sentry === "object" && !Array.isArray(sentry) && sentry.latch !== undefined)
723
+ report("error", p, `state "${sname}": "latch" is retired (2026-09-07). Delete the block — the model reads what is on screen; nothing addresses a component`);
736
724
  }
737
725
  } catch {
738
726
  continue; // a malformed envelope is reported elsewhere
739
727
  }
740
- if (!states) continue;
741
- for (const [sname, sentry] of Object.entries(states)) {
742
- const latch = sentry && typeof sentry === "object" && !Array.isArray(sentry) ? sentry.latch : undefined;
743
- if (latch === undefined) continue;
744
- if (typeof latch !== "object" || latch === null || Array.isArray(latch)) {
745
- report("error", src, `state "${sname}": "latch" must be a block ({ title, icon }), not ${Array.isArray(latch) ? "a list" : typeof latch}. Presence is what makes a state addressable, so a malformed one is an intent nothing can honour (docs/MCP_COMPLETE_GUIDE.md §The Component Latch)`);
746
- continue;
747
- }
748
- // The pill is the ONLY sign the guest has that their words are landing on this page
749
- // rather than the conversation. Unlabelled, it says nothing — and the compiler drops
750
- // a titleless block, so the state would silently stop being addressable.
751
- if (typeof latch.title !== "string" || !latch.title.trim())
752
- report("error", src, `state "${sname}": "latch" needs a "title" — it is what the pill reads, and the pill is the only thing telling the guest what they are addressing. Name the thing, not the verb ("Your Perfect Day", not "Edit the day")`);
753
- const extra = Object.keys(latch).filter((k) => !LATCH_KEYS.has(k));
754
- if (extra.length)
755
- report("error", src, `state "${sname}": "latch" has no key(s) ${extra.join(", ")}. The block is exactly ${[...LATCH_KEYS].join(", ")} — an invented key is silently ignored, so whatever it was meant to do never happens. The pill's colours belong to the design system's composer-bar, never to a state`);
756
- // A STATE NOTHING CAN ENTER CANNOT BE ADDRESSED. The base state is the app at rest —
757
- // the conversation itself — and `stateOrder` excludes it, so `resolveLatch` can never
758
- // rank a base-state instance and the pill would never appear.
759
- if (sname === Object.keys(states)[0])
760
- report("error", src, `state "${sname}": it is the BASE state — the app at rest, which the priority ladder excludes by construction (stateOrder is every state BUT the base). A latch here can never resolve, so no pill would ever draw. Declare it on a state something ARRIVES into`);
761
- }
762
728
  }
763
729
  }
764
730
 
@@ -778,8 +744,14 @@ const PLATFORM_HANDLERS = new Set(["getDetail"]); // named handlers needing no f
778
744
  // silently disable every rule after it.
779
745
  const credentialDefs = new Set();
780
746
  {
781
- const nodesHome = resolve(DESIGN_ROOT, "..", "nodes");
747
+ // Every tier's node packages: the marketplace's and each org's, all at <tier>/nodes/.
748
+ const nodesHomes = [];
782
749
  try {
750
+ for (const tier of readdirSync(DESIGN_ROOT)) nodesHomes.push(join(DESIGN_ROOT, tier, "nodes"));
751
+ } catch {
752
+ /* no design root: nothing declares a credential */
753
+ }
754
+ for (const nodesHome of nodesHomes) try {
783
755
  for (const pkg of readdirSync(nodesHome)) {
784
756
  try {
785
757
  for (const f of readdirSync(join(nodesHome, pkg, "credentials"))) if (isDefFile(f)) credentialDefs.add(defName(f));
@@ -859,24 +831,8 @@ for (const orgDir of [DS, ...orgDirs]) {
859
831
  if (f.endsWith(".js") && !declared.has(f.slice(0, -3)))
860
832
  report("error", join(folder, f), `${f} is a lifecycle handler no manifest opted into. Add "${f.slice(0, -3)}" to the manifest's lifecycle array, or delete the file — un-opted code never runs`);
861
833
 
862
- /**
863
- * LATCH, RETIRED FROM THE MANIFEST (2026-08-31, MCP guide §The Component Latch).
864
- *
865
- * It moved to the STATE, in the app's own tree, beside that state's `layout:` and
866
- * `preview:`. A component said one thing about itself for the whole of its life, but a
867
- * page is addressable when it IS the page and not when it is a summary in the flow, and
868
- * only the state knows which of those is on screen.
869
- *
870
- * An ERROR rather than a silent ignore: nothing reads this key any more, so a manifest
871
- * still carrying it declares an intent that never happens, and the author has no way to
872
- * tell that from a latch that simply does not work.
873
- */
874
834
  if (readDef(mf).latch !== undefined)
875
- report(
876
- "error",
877
- mf,
878
- `"latch" is no longer a manifest key — it moved to the STATE (2026-08-31). Declare it on the app state this component is addressable IN, beside that state's layout: \`states: { <state>: { layout: ..., latch: { title: "..." } } }\`. Nothing reads a manifest latch, so this one produces no pill (docs/MCP_COMPLETE_GUIDE.md §The Component Latch)`,
879
- );
835
+ report("error", mf, `"latch" is retired (2026-09-07). Delete the key — nothing reads it`);
880
836
  }
881
837
  }
882
838
 
@@ -910,9 +866,20 @@ function readAppSizes(dir) {
910
866
  try {
911
867
  const sizes = {};
912
868
  for (const [k, v] of Object.entries(readDef(p).appSize ?? {})) sizes[k] = typeof v === "string" ? v : v?.$value;
869
+ // `flex` IS THE SDK'S OWN WORD, NEVER A TOKEN (docs.unoverse.ai/design/apps): it means
870
+ // "fill the space remaining beside whatever core is open", on a slot and on a mounted
871
+ // template alike. A token named flex hard-codes ONE core's width and is wrong beside every
872
+ // other core: observed live 2026-09-10, an org's flex of 100% minus a 360px rail left a
873
+ // mounted grid overlapping a 480px voice core. The word is reserved; the file says so.
874
+ if (Object.prototype.hasOwnProperty.call(sizes, "flex"))
875
+ report("error", p, `app-sizes names "flex". flex is the SDK's own word (fill the space remaining beside whatever core is open), never a token: a token named flex hard-codes one core's width and is wrong beside every other core. Delete it; layouts keep writing appWidth: flex (docs.unoverse.ai/design/apps)`);
913
876
  return sizes;
914
- } catch {
915
- return null; // the file lints separately
877
+ } catch (e) {
878
+ // SAID, NOT SWALLOWED (2026-09-10): a sizes file that will not parse used to return null
879
+ // here, and the org silently inherited the foundation's set, so every width of its own
880
+ // "named no app size" and the real fault, a stray colon in the file, was never named.
881
+ report("error", p, `app-sizes is not valid ${p.endsWith(".json") ? "JSON" : "YAML"}: ${String(e?.message ?? e).split("\n")[0]}. Until it parses, none of this org's own sizes exist`);
882
+ return null;
916
883
  }
917
884
  }
918
885
  function appSizesForFile(file) {
@@ -975,13 +942,39 @@ function componentNamesForFile(file) {
975
942
  return names;
976
943
  }
977
944
 
945
+ // THE PLACES APPS DECLARE (UNOVERSE_SCREEN.md §2.2), across the whole tree. A component is shared
946
+ // by every app that shows it, so a state's `place:` is checked against every app's places, plus
947
+ // `chat`. Read once per run: every app layout under any `apps/` folder, every `Place` node in it.
948
+ let placeNamesCache;
949
+ function placeNames() {
950
+ if (placeNamesCache !== undefined) return placeNamesCache;
951
+ const names = new Set();
952
+ const walk = (n) => {
953
+ if (Array.isArray(n)) return n.forEach(walk);
954
+ if (!n || typeof n !== "object") return;
955
+ if (n.type === "Place" && typeof n.name === "string") names.add(n.name);
956
+ for (const v of Object.values(n)) walk(v);
957
+ };
958
+ const roots = [DS, ...readdirSync(DESIGN_ROOT).filter((e) => !e.startsWith(".")).map((e) => join(DESIGN_ROOT, e))];
959
+ for (const root of new Set(roots))
960
+ for (const f of jsonFiles(join(root, "apps"))) {
961
+ try {
962
+ walk(parseDef(readText(f), f));
963
+ } catch {
964
+ /* an unreadable file is reported by its own lint; it declares no place here */
965
+ }
966
+ }
967
+ placeNamesCache = names;
968
+ return names;
969
+ }
970
+
978
971
 
979
972
  // The run context: everything the extracted rule modules close over. Built here so it
980
973
  // cannot outlive the run, and passed once rather than threaded as ten parameters.
981
974
  const ctx = {
982
975
  DESIGN_ROOT, DS, orgDirs, report, spaceSteps, stepList, checkDimension, checkToken, checkCondition,
983
- appSizesForFile, componentNamesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists,
984
- isFixture, isHook, isPropsExtract, isManifest, isAppPath, isTemplatePath, isTemplatePart, defRoot, readText,
976
+ appSizesForFile, componentNamesForFile, placeNames, refResolves, canonicalRef, declaredProps, atomsDirExists,
977
+ isFixture, isHook, isPropsExtract, isManifest, isAppPath, isTemplatePath, isTemplatePart, isAgentPath, defRoot, readText,
985
978
  };
986
979
  const walkNode = makeWalkNode(ctx);
987
980
  const lintFile = makeLintFile({ ...ctx, walkNode });
@@ -13,7 +13,7 @@
13
13
  // ── ground truth (mirrors design/_schema/unoverse.schema.json + server guards) ──
14
14
  export const PRIMITIVES = new Set([
15
15
  "Box", "Stack", "Row", "Column", "Each", "Switch", "ComponentSlot", "Timeline",
16
- "Text", "Image", "Button", "Input", "Select", "Markdown", "Skeleton", "Icon", "Ref", "Orb", "Template",
16
+ "Text", "Image", "Button", "Input", "Select", "Markdown", "Skeleton", "Icon", "Ref", "Orb", "Template", "Place",
17
17
  ]);
18
18
  export const CONDITION_KEYS = new Set(["field", "eq", "ne", "in"]);
19
19
 
@@ -57,7 +57,7 @@ export const PROP_KEYS = [
57
57
  */
58
58
  export const INTERFACE_FIELDS = new Set([
59
59
  "universal_id", "title", "description", "object_type", "key_need", "source_url", "source_id",
60
- "tagline", "shortDescription", "introParagraph", "callToAction", "actionPrompt", "primaryImage", "images", "action",
60
+ "tagline", "shortDescription", "introParagraph", "callToAction", "primaryImage", "images", "action",
61
61
  "bodyCopy", "features", "mainCategory", "section", "needs",
62
62
  ]);
63
63
 
@@ -78,7 +78,7 @@ export const PRIMITIVE_PROPS = {
78
78
  Column: ["children", "autoScroll", "revealOnStick", "appWidth"],
79
79
  Text: ["value"],
80
80
  Image: ["src", "alt"],
81
- Markdown: ["value", "inheritLinkColor"],
81
+ Markdown: ["value", "inheritLinkColor", "citeAction"],
82
82
  Icon: ["icon", "name"],
83
83
  Skeleton: ["variant"],
84
84
  Orb: ["state", "size"],
@@ -94,6 +94,9 @@ export const PRIMITIVE_PROPS = {
94
94
  // template's machinery. `name` addresses the manifest's `templates:` entry;
95
95
  // `frame` is the dressing drawn around it when occupied.
96
96
  Template: ["name", "frame", "template", "director", "appWidth"],
97
+ // A PLACE (UNOVERSE_SCREEN.md §2.2): a named spot the screen shows interfaces into.
98
+ // `holds` is one or many; `frame` and `fallback` work as they do on a ComponentSlot.
99
+ Place: ["name", "holds", "frame", "fallback", "appWidth"],
97
100
  Timeline: ["user", "assistant", "userData", "assistantData", "autoScroll"],
98
101
  };
99
102
 
@@ -25,10 +25,12 @@ const SEARCH_KEYS = ["component", "type", "include", "pick", "items"];
25
25
  // The `object_type`s a place may admit. Apps and skills are absent by construction: the
26
26
  // page's own searches are SILENT (no app unlocked, no tool minted, no card lane, no
27
27
  // director woken), or every place would rain cards onto a page still assembling itself.
28
- const QUERY_TYPES = ["services", "images", "needs", "actions"];
28
+ // `products` since migration 035 (2026-09-05; `services` retired 2026-09-07, `actions` with it:
29
+ // no row is written as one). Each word maps to a declared spatial type (guarded by a test).
30
+ const QUERY_TYPES = ["products", "images", "needs"];
29
31
 
30
32
  export function makeWalkNode(ctx) {
31
- const { report, checkCondition, checkDimension, checkToken, appSizesForFile, componentNamesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists, stepList, spaceSteps, isAppPath, isTemplatePath, defRoot } = ctx;
33
+ const { report, checkCondition, checkDimension, checkToken, appSizesForFile, componentNamesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists, stepList, spaceSteps, isAppPath, isTemplatePath, isTemplatePart, defRoot } = ctx;
32
34
  // One director per LAYOUT FILE, tracked for this run only (each file is walked once).
33
35
  const directorsSeen = new Map();
34
36
 
@@ -36,6 +38,13 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
36
38
  if (Array.isArray(node)) return node.forEach((n) => walkNode(n, file, root, widthCap));
37
39
  if (!node || typeof node !== "object") return;
38
40
 
41
+ // THE ✕ BELONGS TO THE TEMPLATE'S LAYOUT, NEVER TO A PART (2026-09-07). `setValue` writes
42
+ // the interface's OWN slice (docs/design/state.md) and a part is its own interface, so a
43
+ // close inside a part retracts the PART into the chat column while the page stays open
44
+ // beside it (observed live: a page's hero, ✕ and all, sitting in the flow).
45
+ if (node.type === "Ref" && node.ref === "close-button" && isTemplatePart(file))
46
+ report("error", file, `a close-button inside a template PART writes the part's own slice, not the template's — the page stays open and the part falls into the chat. Move the ✕ to the template's layout (layouts/<state>.yaml), where its setValue writes the template's state`);
47
+
39
48
  // $include — resolves against the DEFINITION ROOT (layouts/ states/ components/…)
40
49
  if (typeof node.$include === "string") {
41
50
  const a = defPath(root, node.$include);
@@ -129,8 +138,9 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
129
138
  report("error", file, `a delivery band has no "${k}". It reads ${DELIVERY_KEYS.map((x) => `"${x}"`).join(", ")} (§9b; a place that names a component is a search place)`);
130
139
  if (d.pick !== undefined && (typeof d.pick !== "number" || d.pick < 1))
131
140
  report("error", file, `director.pick is a NUMBER — how many the director keeps, a cap the machine enforces (§9e rule 9)`);
132
- if (d.rules !== undefined && typeof d.rules !== "string")
133
- report("error", file, `director.rules is written like a brief: one string of English (§9b)`);
141
+ // NOTHING READS `rules`: the director's model was removed (UNOVERSE_SCREEN.md §2.7).
142
+ if (d.rules !== undefined)
143
+ report("warn", file, `director.rules is ignored: no model reads it any more. A band holds what arrives in search rank, up to pick. Remove it`);
134
144
  if (d.preview !== undefined && (!Array.isArray(d.preview) || d.preview.some((c) => typeof c !== "string")))
135
145
  report("error", file, `director.preview is an array of component names — Studio's stand-ins until a real run exists (§9b rule 6)`);
136
146
  else if (Array.isArray(d.preview)) {
@@ -1,15 +1,19 @@
1
1
  /**
2
- * The linters. TWO of them, because there are two things to validate and they share
3
- * almost nothing.
2
+ * The linters. FOUR siblings, one per kind of thing a developer authors, and every kind
3
+ * is checked the same way: `unoverse lint`, the publish gate, CI, all through
4
+ * items/publish lintForPublish.
4
5
  *
5
- * nodes/ node manifests: JSON Schema per part, plus the cross-file rules a schema
6
- * cannot express (events matching output connectors, calls reachable by name,
7
- * credentials declared in the package that needs them)
8
- * design/ definitions: a CLOSED primitive set, a closed style vocabulary, token-only
9
- * values, Switch discriminants, layouts reachable from a case
6
+ * nodes/ node manifests: JSON Schema per part, plus the cross-file rules a schema
7
+ * cannot express (events matching output connectors, calls reachable by name,
8
+ * credentials declared in the package that needs them)
9
+ * design/ definitions: a CLOSED primitive set, a closed style vocabulary, token-only
10
+ * values, Switch discriminants, layouts reachable from a case
11
+ * objects/ objects and pipelines: schema per file, plus the rules a schema cannot say
12
+ * prompts/ Agent skills and prompt blocks: frontmatter that parses, the fields the
13
+ * loaders need, and the discoverability contract on whenToUse
10
14
  *
11
- * They share `context.mjs` and nothing else. One reports on `api/run.yaml`, the other on
12
- * whether a colour is a raw hex. Merging them would produce a linter that is worse at both.
15
+ * They share `context.mjs` and nothing else. One reports on `api/run.yaml`, another on
16
+ * whether a colour is a raw hex. Merging them would produce a linter that is worse at all.
13
17
  *
14
18
  * WHY THEY LIVE HERE rather than in scripts/. The same rules are needed by four callers,
15
19
  * and only one of them is a terminal:
@@ -25,4 +29,6 @@
25
29
 
26
30
  export { lintNodes, hasErrors as nodesHaveErrors } from "./nodes/index.mjs";
27
31
  export { lintDefinitions, hasErrors as definitionsHaveErrors } from "./design/index.mjs";
32
+ export { lintObjects, hasErrors as objectsHaveErrors } from "./objects/index.mjs";
33
+ export { lintPrompts, hasErrors as promptsHaveErrors } from "./prompts/index.mjs";
28
34
  export { problems, report, rel, reset } from "./context.mjs";
@@ -23,7 +23,7 @@ export const HANDLEBARS_HELPERS = new Set([
23
23
  ]);
24
24
 
25
25
  /**
26
- * The prompt-block library: prompts/blocks/**\/*.md, keyed the way the resolver keys them
26
+ * The prompt-block library: design/marketplace/blocks/**\/*.md, keyed the way the resolver keys them
27
27
  * (engine/src/template/promptBlocks.ts) — the filename camelCased, so markdown-guidelines.md is
28
28
  * referenced as {{prompt.markdownGuidelines}}.
29
29
  *
@@ -41,7 +41,11 @@ export const HANDLEBARS_HELPERS = new Set([
41
41
  const CACHE = new Map();
42
42
 
43
43
  export function promptBlocks() {
44
- const home = resolve(state.nodesHome, "../prompts/blocks");
44
+ // The block library is the marketplace tier's. Beside the nodes when they ARE that
45
+ // tier (design/marketplace/{nodes,blocks}); one tier over for an org's own nodes
46
+ // (design/<org>/nodes → design/marketplace/blocks).
47
+ const home = [resolve(state.nodesHome, "../blocks"), resolve(state.nodesHome, "../../marketplace/blocks")]
48
+ .find((d) => existsSync(d)) ?? resolve(state.nodesHome, "../../marketplace/blocks");
45
49
  const cached = CACHE.get(home);
46
50
  if (cached) return cached;
47
51
 
@@ -13,7 +13,7 @@
13
13
  * without it is the gap LOCAL_STUDIO.md:246 names: "a malformed definition is one restart
14
14
  * from a broken server", and a published row does not even need the restart.
15
15
  *
16
- * FOUR TIERS (docs/architecture/DECLARATIVE_NODES.md §6). This is tiers 2 and 3.
16
+ * FOUR TIERS (docs/architecture/authoring/DECLARATIVE_NODES.md §6). This is tiers 2 and 3.
17
17
  * 1 editor $schema pointers + .vscode yaml.schemas, no tool
18
18
  * 2 structural every part validated against the node schemas (packages/docs/schemas/nodes)
19
19
  * 3 semantic the cross-file rules a schema cannot express
@@ -25,6 +25,7 @@ import { fileURLToPath } from "node:url";
25
25
  import { problems, derivedKinds, reset, report } from "../context.mjs";
26
26
  import { loadSchemas } from "./schema.mjs";
27
27
  import { lintPackage } from "./package.mjs";
28
+ import { lintAttachments } from "./node.mjs";
28
29
 
29
30
  /**
30
31
  * Lint every node package under `nodesHome`.
@@ -72,6 +73,10 @@ export function lintNodes(nodesHome) {
72
73
  nodeCount += lintPackage(dir);
73
74
  }
74
75
 
76
+ // AFTER every package: an attachment names no host, so whether it fits one cannot be known
77
+ // until every node's configSchema has been read. See lintAttachments.
78
+ lintAttachments();
79
+
75
80
  const rank = { error: 0, warn: 1, hint: 2 };
76
81
  const sorted = [...problems].sort((a, b) => rank[a.level] - rank[b.level] || a.file.localeCompare(b.file));
77
82
  return { problems: sorted, derivedKinds: new Map(derivedKinds), nodeCount, pkgCount, nodesHome: home };