0xray 4.0.14 → 4.0.15

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 (68) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/orchestrator/SKILL.md +19 -1
  3. package/.xray/features.json +2 -2
  4. package/AGENTS-consumer.md +10 -5
  5. package/AGENTS.md +10 -7
  6. package/README.md +8 -7
  7. package/SKILLS.md +3 -2
  8. package/dist/AGENTS.md +10 -7
  9. package/dist/CHANGELOG.md +48 -0
  10. package/dist/README.md +8 -7
  11. package/dist/SKILLS.md +3 -2
  12. package/dist/cli/commands/hermes-install.js +2 -1
  13. package/dist/cli/commands/openclaw-install.js +2 -1
  14. package/dist/cli/commands/opencode-install.js +2 -1
  15. package/dist/cli/index.js +2 -1
  16. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +9 -0
  17. package/dist/integrations/grok/grok-cli.js +2 -1
  18. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  19. package/dist/reporting/log-parser.js +2 -2
  20. package/dist/skills/orchestrator/SKILL.md +19 -1
  21. package/llms.txt +8 -7
  22. package/package.json +2 -1
  23. package/scripts/foundry/CHANGELOG.md +33 -1
  24. package/scripts/foundry/README.md +12 -5
  25. package/scripts/foundry/assert-packed-dist-cli.mjs +6 -0
  26. package/scripts/foundry/blip-kapow.cjs +2 -0
  27. package/scripts/foundry/blip-looker.cjs +2 -0
  28. package/scripts/foundry/blip-render.cjs +2 -1237
  29. package/scripts/foundry/blip-rippel-organs.cjs +2 -0
  30. package/scripts/foundry/blip-rippel.cjs +2 -1963
  31. package/scripts/foundry/blip-scene.cjs +2 -0
  32. package/scripts/foundry/blip-stroke.cjs +2 -0
  33. package/scripts/foundry/blip-vibe.cjs +2 -0
  34. package/scripts/foundry/blip.mjs +23 -97
  35. package/scripts/foundry/cli.mjs +2 -2
  36. package/scripts/foundry/inspect.mjs +62 -19
  37. package/scripts/foundry/mill-plant.cjs +598 -0
  38. package/scripts/foundry/mint-suit.cjs +189 -99
  39. package/scripts/foundry/package.json +12 -1
  40. package/scripts/foundry/plant/agents/blip-inspect.yml +1 -1
  41. package/scripts/foundry/plant/agents/blip-looker.yml +5 -0
  42. package/scripts/foundry/plant/agents/blip-vibe.yml +5 -0
  43. package/scripts/foundry/plant/agents/blip.yml +1 -1
  44. package/scripts/foundry/plant/agents/inspect.yml +1 -1
  45. package/scripts/foundry/plant/agents/mill.yml +1 -1
  46. package/scripts/foundry/plant/agents/sound-inspect.yml +1 -1
  47. package/scripts/foundry/plant/agents/sound-mixer.yml +5 -0
  48. package/scripts/foundry/plant/agents/sound.yml +1 -1
  49. package/scripts/foundry/plant/motions/MOTION-REGISTRY.md +15 -9
  50. package/scripts/foundry/plant/motions/registry.json +5 -4
  51. package/scripts/foundry/plant/skills/blip/SKILL.md +4 -4
  52. package/scripts/foundry/plant/skills/blip-inspect/SKILL.md +2 -2
  53. package/scripts/foundry/plant/skills/blip-looker/SKILL.md +30 -0
  54. package/scripts/foundry/plant/skills/blip-vibe/SKILL.md +30 -0
  55. package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -2
  56. package/scripts/foundry/plant/skills/sound/SKILL.md +3 -1
  57. package/scripts/foundry/plant/skills/sound-inspect/SKILL.md +1 -1
  58. package/scripts/foundry/plant/skills/sound-mixer/SKILL.md +28 -0
  59. package/scripts/foundry/sound-bed.cjs +2 -454
  60. package/scripts/foundry/sound-mixer.cjs +2 -0
  61. package/scripts/foundry/sound-rippel.cjs +2 -1434
  62. package/scripts/foundry/sound-taste.cjs +2 -0
  63. package/scripts/foundry/sound-voice.cjs +2 -0
  64. package/scripts/foundry/sound.mjs +23 -81
  65. package/scripts/node/install-bridges.cjs +39 -4
  66. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  67. package/src/skills/orchestrator/SKILL.md +19 -1
  68. package/xray/features.json +2 -2
@@ -11,6 +11,7 @@ const os = require("os");
11
11
  const path = require("path");
12
12
 
13
13
  const { attachInventoryDna, inventoryDna } = require("./mill-dna.cjs");
14
+ const millPlantProtocol = require("./mill-plant.cjs");
14
15
 
15
16
  const DEFAULT_PARAMS = {
16
17
  codex: "xray/codex.json",
@@ -24,15 +25,13 @@ const DEFAULT_PARAMS = {
24
25
  /** Factory shop plant. First-class with mill plant. Not 45/42 costume. */
25
26
  const FACTORY_SHOP_SKILLS = ["shop-extract", "shop-witness", "shop-pin"];
26
27
 
27
- /** Factory plant kinds. Mill is default. Sound is a bed factory. Blip is a tiny-video factory. */
28
+ /** Builtin mill only. Package plants (blip, sound, @scope/pkg/prefix) use mill-plant.cjs. */
28
29
  const FACTORY_PLANT_CATALOG = {
29
30
  mill: { skills: ["mill", "inspect"], agents: ["mill.yml", "inspect.yml"] },
30
- sound: { skills: ["sound", "sound-inspect"], agents: ["sound.yml", "sound-inspect.yml"] },
31
- blip: { skills: ["blip", "blip-inspect"], agents: ["blip.yml", "blip-inspect.yml"] },
32
31
  };
33
32
 
34
33
  function isFactoryPlantKind(kind) {
35
- return Boolean(kind && FACTORY_PLANT_CATALOG[kind]);
34
+ return Boolean(millPlantProtocol.parsePlantToken(kind));
36
35
  }
37
36
 
38
37
  function deepMerge(src, dest) {
@@ -122,51 +121,51 @@ function wantsCostume(targetDir) {
122
121
  }
123
122
 
124
123
  function millPlantDir(millPackageRoot) {
125
- const nested = path.join(millPackageRoot, "scripts", "foundry", "plant");
126
- if (isDirectory(nested)) return nested;
127
- const packed = path.join(millPackageRoot, "plant");
128
- if (isDirectory(packed)) return packed;
129
- return null;
124
+ return millPlantProtocol.foundryPlantDir(millPackageRoot);
130
125
  }
131
126
 
132
127
  function emptyPlantFiles() {
133
128
  return { skills: [], agents: [] };
134
129
  }
135
130
 
136
- function catalogPlant(kind) {
137
- const spec = FACTORY_PLANT_CATALOG[kind];
138
- return spec ? { skills: [...spec.skills], agents: [...spec.agents] } : emptyPlantFiles();
131
+ function catalogPlant(kind, millPackageRoot) {
132
+ if (!kind || kind === "mill") {
133
+ const spec = FACTORY_PLANT_CATALOG.mill;
134
+ return spec ? { skills: [...spec.skills], agents: [...spec.agents] } : emptyPlantFiles();
135
+ }
136
+ const parsed = millPlantProtocol.parsePlantToken(kind);
137
+ if (!parsed || parsed.builtin) {
138
+ const spec = FACTORY_PLANT_CATALOG.mill;
139
+ return spec ? { skills: [...spec.skills], agents: [...spec.agents] } : emptyPlantFiles();
140
+ }
141
+ const pkgRoot = millPlantProtocol.resolvePackageRoot(parsed.packageName, {
142
+ targetDir: millPackageRoot,
143
+ millPackageRoot,
144
+ cwd: process.cwd(),
145
+ });
146
+ if (!pkgRoot) throw millPlantProtocol.unresolvedPackageError(parsed.packageName);
147
+ const plantId = parsed.plantId || millPlantProtocol.readProtocol(pkgRoot).default;
148
+ if (!plantId) {
149
+ throw millPlantProtocol.plantError(
150
+ `foundry-plant: mill package "${parsed.packageName}" has no default plant id`,
151
+ );
152
+ }
153
+ const files = millPlantProtocol.selectPlantFiles(pkgRoot, plantId);
154
+ return { skills: [...files.skills], agents: [...files.agents] };
139
155
  }
140
156
 
141
- /** foundry.json plant: "sound" | "blip" | "mill" | ["mill","sound","blip"]. millPlant:false turns mill off. */
142
- function loadFactoryPlantKinds(targetDir) {
157
+ /** foundry.json plant: mill | @scope/name | @scope/name/id | blip|sound aliases | array. millPlant:false strips mill. */
158
+ function loadFactoryPlantKinds(targetDir, millPackageRoot) {
143
159
  const extra = loadFoundryExtra(targetDir);
144
- const kinds = [];
145
- const raw = extra.plant;
146
- if (Array.isArray(raw)) {
147
- for (const item of raw) {
148
- if (isFactoryPlantKind(item)) kinds.push(item);
149
- }
150
- } else if (isFactoryPlantKind(raw)) {
151
- kinds.push(raw);
152
- } else if (extra.soundPlant === true || extra.blipPlant === true) {
153
- if (extra.millPlant !== false) kinds.push("mill");
154
- if (extra.soundPlant === true) kinds.push("sound");
155
- if (extra.blipPlant === true) kinds.push("blip");
156
- } else {
157
- kinds.push("mill");
158
- }
159
- if (extra.millPlant === false) {
160
- return [...new Set(kinds.filter((kind) => kind !== "mill"))];
161
- }
162
- return [...new Set(kinds)];
160
+ const seats = millPlantProtocol.resolvePlantSeats(targetDir, millPackageRoot, extra);
161
+ return millPlantProtocol.plantKindsFromSeats(seats);
163
162
  }
164
163
 
165
- function unionPlantFiles(kinds) {
164
+ function unionPlantFiles(kinds, millPackageRoot) {
166
165
  const skills = [];
167
166
  const agents = [];
168
167
  for (const kind of kinds) {
169
- const spec = catalogPlant(kind);
168
+ const spec = catalogPlant(kind, millPackageRoot);
170
169
  for (const name of spec.skills) {
171
170
  if (!skills.includes(name)) skills.push(name);
172
171
  }
@@ -190,27 +189,23 @@ function existingPlantFiles(plantDir, spec) {
190
189
  }
191
190
 
192
191
  function factoryPlantAllowlist(millPackageRoot, targetDir) {
193
- const kinds = loadFactoryPlantKinds(targetDir);
194
- return {
195
- kinds,
196
- ...existingPlantFiles(millPlantDir(millPackageRoot), unionPlantFiles(kinds)),
197
- };
192
+ const extra = loadFoundryExtra(targetDir);
193
+ const seats = millPlantProtocol.resolvePlantSeats(targetDir, millPackageRoot, extra);
194
+ const skills = [];
195
+ const agents = [];
196
+ for (const seat of seats) {
197
+ for (const name of seat.skills || []) {
198
+ if (!skills.includes(name)) skills.push(name);
199
+ }
200
+ for (const file of seat.agents || []) {
201
+ if (!agents.includes(file)) agents.push(file);
202
+ }
203
+ }
204
+ return { kinds: millPlantProtocol.plantKindsFromSeats(seats), skills, agents, seats };
198
205
  }
199
206
 
200
207
  function inventoryPlantKinds(inventory) {
201
- if (!inventory || typeof inventory !== "object") return ["mill"];
202
- if (Array.isArray(inventory.plant) && inventory.plant.length > 0) {
203
- return inventory.plant.filter((kind) => isFactoryPlantKind(kind));
204
- }
205
- if (isFactoryPlantKind(inventory.plant)) return [inventory.plant];
206
- const hasSound = Array.isArray(inventory.soundPlant?.skills) && inventory.soundPlant.skills.length > 0;
207
- const hasBlip = Array.isArray(inventory.blipPlant?.skills) && inventory.blipPlant.skills.length > 0;
208
- const hasMill = Array.isArray(inventory.millPlant?.skills) && inventory.millPlant.skills.length > 0;
209
- const kinds = [];
210
- if (hasMill) kinds.push("mill");
211
- if (hasSound) kinds.push("sound");
212
- if (hasBlip) kinds.push("blip");
213
- return kinds.length > 0 ? kinds : ["mill"];
208
+ return millPlantProtocol.plantKindsFromInventory(inventory);
214
209
  }
215
210
 
216
211
  function listSkillNamesAt(skillsSrc) {
@@ -237,64 +232,99 @@ function listAgentFilesAt(agentsSrc) {
237
232
  }
238
233
 
239
234
  function copyPlantFiles(plant, spec, targetDir) {
240
- const skillsSrc = path.join(plant, "skills");
241
- const agentsSrc = path.join(plant, "agents");
235
+ const skillsSrc = spec.skillsDir || (plant ? path.join(plant, "skills") : null);
236
+ const agentsSrc = spec.agentsDir || (plant ? path.join(plant, "agents") : null);
242
237
  const skills = spec.skills || [];
243
238
  const agents = spec.agents || [];
244
239
  const skillDirs = listProjectSkillDirs(targetDir);
245
240
  const agentsDest = path.join(targetDir, ".opencode", "agents");
246
- for (const dir of skillDirs) {
247
- for (const name of skills) {
248
- const src = path.join(skillsSrc, name, "SKILL.md");
249
- const destMd = path.join(dir, name, "SKILL.md");
250
- if (!fs.existsSync(src)) continue;
251
- if (path.resolve(src) === path.resolve(destMd)) continue;
252
- if (fs.existsSync(destMd)) continue;
253
- fs.mkdirSync(path.dirname(destMd), { recursive: true });
254
- fs.copyFileSync(src, destMd);
241
+ if (skillsSrc) {
242
+ for (const dir of skillDirs) {
243
+ for (const name of skills) {
244
+ const src = path.join(skillsSrc, name, "SKILL.md");
245
+ const destMd = path.join(dir, name, "SKILL.md");
246
+ if (!fs.existsSync(src)) continue;
247
+ if (path.resolve(src) === path.resolve(destMd)) continue;
248
+ if (fs.existsSync(destMd)) continue;
249
+ fs.mkdirSync(path.dirname(destMd), { recursive: true });
250
+ fs.copyFileSync(src, destMd);
251
+ }
255
252
  }
256
253
  }
257
- for (const file of agents) {
258
- const src = path.join(agentsSrc, file);
259
- const dest = path.join(agentsDest, file);
260
- if (!fs.existsSync(src)) continue;
261
- if (path.resolve(src) === path.resolve(dest)) continue;
262
- if (fs.existsSync(dest)) continue;
263
- fs.mkdirSync(agentsDest, { recursive: true });
264
- fs.copyFileSync(src, dest);
254
+ if (agentsSrc) {
255
+ for (const file of agents) {
256
+ const src = path.join(agentsSrc, file);
257
+ const dest = path.join(agentsDest, file);
258
+ if (!fs.existsSync(src)) continue;
259
+ if (path.resolve(src) === path.resolve(dest)) continue;
260
+ if (fs.existsSync(dest)) continue;
261
+ fs.mkdirSync(agentsDest, { recursive: true });
262
+ fs.copyFileSync(src, dest);
263
+ }
265
264
  }
266
265
  return { skills, agents };
267
266
  }
268
267
 
269
- /** Fasten requested factory plants. Default mill only. Sound/blip seats skip mill unless asked. */
268
+ /** Fasten requested plants. Mill files come from this foundry plant/. Mill-package organs come from that package. */
270
269
  function fastenMillPlant(millPackageRoot, targetDir, log) {
271
- const plant = millPlantDir(millPackageRoot);
272
- const kinds = loadFactoryPlantKinds(targetDir);
273
- if (!plant) {
274
- return { skills: [], agents: [], kinds, sound: emptyPlantFiles(), blip: emptyPlantFiles() };
275
- }
276
- const mill = kinds.includes("mill")
277
- ? existingPlantFiles(plant, catalogPlant("mill"))
270
+ const extra = loadFoundryExtra(targetDir);
271
+ const seats = millPlantProtocol.resolvePlantSeats(targetDir, millPackageRoot, extra);
272
+ const kinds = millPlantProtocol.plantKindsFromSeats(seats);
273
+ const millSeat = seats.find((seat) => seat.builtin && seat.kind === "mill");
274
+ const soundSeat = seats.find((seat) => seat.kind === "sound");
275
+ const blipSeat = seats.find((seat) => seat.kind === "blip");
276
+ const mill = millSeat
277
+ ? { skills: [...millSeat.skills], agents: [...millSeat.agents] }
278
278
  : emptyPlantFiles();
279
- const sound = kinds.includes("sound")
280
- ? existingPlantFiles(plant, catalogPlant("sound"))
279
+ const sound = soundSeat
280
+ ? { skills: [...soundSeat.skills], agents: [...soundSeat.agents] }
281
281
  : emptyPlantFiles();
282
- const blip = kinds.includes("blip")
283
- ? existingPlantFiles(plant, catalogPlant("blip"))
282
+ const blip = blipSeat
283
+ ? { skills: [...blipSeat.skills], agents: [...blipSeat.agents] }
284
284
  : emptyPlantFiles();
285
- const fastened = copyPlantFiles(
286
- plant,
287
- unionPlantFiles(kinds.filter((kind) => isFactoryPlantKind(kind))),
288
- targetDir,
289
- );
290
- if (log && (fastened.skills.length > 0 || fastened.agents.length > 0)) {
285
+ const plants = {};
286
+ const fastenedSkills = [];
287
+ const fastenedAgents = [];
288
+ for (const seat of seats) {
289
+ if (!seat.builtin) {
290
+ plants[seat.kind] = { skills: [...seat.skills], agents: [...seat.agents] };
291
+ }
292
+ const copied = copyPlantFiles(
293
+ seat.packageRoot,
294
+ {
295
+ skills: seat.skills,
296
+ agents: seat.agents,
297
+ skillsDir: seat.skillsDir,
298
+ agentsDir: seat.agentsDir,
299
+ },
300
+ targetDir,
301
+ );
302
+ for (const name of copied.skills) {
303
+ if (!fastenedSkills.includes(name)) fastenedSkills.push(name);
304
+ }
305
+ for (const file of copied.agents) {
306
+ if (!fastenedAgents.includes(file)) fastenedAgents.push(file);
307
+ }
308
+ }
309
+ const millPkgSeat = seats.find((seat) => seat.packageName);
310
+ if (log && (fastenedSkills.length > 0 || fastenedAgents.length > 0)) {
291
311
  log("foundry-mint", "Fastened factory plant", "info", {
292
312
  plant: kinds,
293
- skills: fastened.skills.length,
294
- agents: fastened.agents.length,
313
+ skills: fastenedSkills.length,
314
+ agents: fastenedAgents.length,
295
315
  });
296
316
  }
297
- return { skills: mill.skills, agents: mill.agents, kinds, sound, blip };
317
+ return {
318
+ skills: mill.skills,
319
+ agents: mill.agents,
320
+ kinds,
321
+ sound,
322
+ blip,
323
+ plants,
324
+ seats,
325
+ millPackage: millPkgSeat ? millPkgSeat.packageName : null,
326
+ millProtocol: millPkgSeat ? millPkgSeat.protocol || millPlantProtocol.PROTOCOL : null,
327
+ };
298
328
  }
299
329
 
300
330
  function isDirectory(p) {
@@ -500,6 +530,14 @@ function previousTreeAllowlist(targetDir) {
500
530
  ? normalizeNameList(blipFromInv)
501
531
  : normalizeNameList(blipFromInv?.skills);
502
532
  const blipAgents = normalizeNameList(blipFromInv?.agents);
533
+ const extraPlantSkills = [];
534
+ const extraPlantAgents = [];
535
+ if (inventory?.plants && typeof inventory.plants === "object") {
536
+ for (const spec of Object.values(inventory.plants)) {
537
+ extraPlantSkills.push(...normalizeNameList(spec?.skills));
538
+ extraPlantAgents.push(...normalizeNameList(spec?.agents));
539
+ }
540
+ }
503
541
  return {
504
542
  skills: [
505
543
  ...(Array.isArray(inventory?.tree?.skills) ? inventory.tree.skills : []),
@@ -507,12 +545,14 @@ function previousTreeAllowlist(targetDir) {
507
545
  ...shopSkills,
508
546
  ...soundSkills,
509
547
  ...blipSkills,
548
+ ...extraPlantSkills,
510
549
  ],
511
550
  agents: [
512
551
  ...(Array.isArray(inventory?.tree?.agents) ? inventory.tree.agents : []),
513
552
  ...(Array.isArray(inventory?.millPlant?.agents) ? inventory.millPlant.agents : []),
514
553
  ...soundAgents,
515
554
  ...blipAgents,
555
+ ...extraPlantAgents,
516
556
  ],
517
557
  };
518
558
  } catch {
@@ -520,26 +560,40 @@ function previousTreeAllowlist(targetDir) {
520
560
  }
521
561
  }
522
562
 
523
- function soundPlantFromTree(targetDir, tree) {
563
+ function millPackagePlantFromTree(targetDir, tree, plantId, millPackageRoot) {
564
+ const seats = Array.isArray(tree?.seats)
565
+ ? tree.seats
566
+ : millPlantProtocol.resolvePlantSeats(targetDir, millPackageRoot || tree?.millPackageRoot);
567
+ const seat = seats.find((item) => item.kind === plantId);
568
+ return seat ? { skills: [...seat.skills], agents: [...seat.agents] } : emptyPlantFiles();
569
+ }
570
+
571
+ function soundPlantFromTree(targetDir, tree, millPackageRoot) {
524
572
  if (Array.isArray(tree?.soundPlantSkills) || Array.isArray(tree?.sound?.skills)) {
525
573
  return {
526
574
  skills: normalizeNameList(tree.soundPlantSkills || tree.sound?.skills),
527
575
  agents: normalizeNameList(tree.soundPlantAgents || tree.sound?.agents),
528
576
  };
529
577
  }
530
- const kinds = Array.isArray(tree?.plantKinds) ? tree.plantKinds : loadFactoryPlantKinds(targetDir);
531
- return kinds.includes("sound") ? catalogPlant("sound") : emptyPlantFiles();
578
+ const kinds = Array.isArray(tree?.plantKinds)
579
+ ? tree.plantKinds
580
+ : loadFactoryPlantKinds(targetDir, millPackageRoot);
581
+ if (!kinds.includes("sound")) return emptyPlantFiles();
582
+ return millPackagePlantFromTree(targetDir, tree, "sound", millPackageRoot);
532
583
  }
533
584
 
534
- function blipPlantFromTree(targetDir, tree) {
585
+ function blipPlantFromTree(targetDir, tree, millPackageRoot) {
535
586
  if (Array.isArray(tree?.blipPlantSkills) || Array.isArray(tree?.blip?.skills)) {
536
587
  return {
537
588
  skills: normalizeNameList(tree.blipPlantSkills || tree.blip?.skills),
538
589
  agents: normalizeNameList(tree.blipPlantAgents || tree.blip?.agents),
539
590
  };
540
591
  }
541
- const kinds = Array.isArray(tree?.plantKinds) ? tree.plantKinds : loadFactoryPlantKinds(targetDir);
542
- return kinds.includes("blip") ? catalogPlant("blip") : emptyPlantFiles();
592
+ const kinds = Array.isArray(tree?.plantKinds)
593
+ ? tree.plantKinds
594
+ : loadFactoryPlantKinds(targetDir, millPackageRoot);
595
+ if (!kinds.includes("blip")) return emptyPlantFiles();
596
+ return millPackagePlantFromTree(targetDir, tree, "blip", millPackageRoot);
543
597
  }
544
598
 
545
599
  /** Extra worn names that are neither mill/sound/blip plant, their plant, shop plant, nor a prior overlay. */
@@ -548,10 +602,19 @@ function costumeDumpExtras(targetDir, millPlant, tree) {
548
602
  const shopPlant = loadShopPlant(targetDir);
549
603
  const soundPlant = soundPlantFromTree(targetDir, tree);
550
604
  const blipPlant = blipPlantFromTree(targetDir, tree);
605
+ const plantMapSkills = [];
606
+ const plantMapAgents = [];
607
+ if (millPlant?.plants && typeof millPlant.plants === "object") {
608
+ for (const spec of Object.values(millPlant.plants)) {
609
+ if (Array.isArray(spec?.skills)) plantMapSkills.push(...spec.skills);
610
+ if (Array.isArray(spec?.agents)) plantMapAgents.push(...spec.agents);
611
+ }
612
+ }
551
613
  const allowedSkills = new Set([
552
614
  ...(Array.isArray(millPlant?.skills) ? millPlant.skills : []),
553
615
  ...(Array.isArray(millPlant?.sound?.skills) ? millPlant.sound.skills : []),
554
616
  ...(Array.isArray(millPlant?.blip?.skills) ? millPlant.blip.skills : []),
617
+ ...plantMapSkills,
555
618
  ...soundPlant.skills,
556
619
  ...blipPlant.skills,
557
620
  ...(Array.isArray(tree?.skills) ? tree.skills : []),
@@ -562,6 +625,7 @@ function costumeDumpExtras(targetDir, millPlant, tree) {
562
625
  ...(Array.isArray(millPlant?.agents) ? millPlant.agents : []),
563
626
  ...(Array.isArray(millPlant?.sound?.agents) ? millPlant.sound.agents : []),
564
627
  ...(Array.isArray(millPlant?.blip?.agents) ? millPlant.blip.agents : []),
628
+ ...plantMapAgents,
565
629
  ...soundPlant.agents,
566
630
  ...blipPlant.agents,
567
631
  ...(Array.isArray(tree?.agents) ? tree.agents : []),
@@ -701,6 +765,13 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
701
765
  },
702
766
  mintedAt: new Date().toISOString(),
703
767
  };
768
+ if (tree?.millPackage) {
769
+ inventory.millPackage = tree.millPackage;
770
+ inventory.millProtocol = tree.millProtocol || millPlantProtocol.PROTOCOL;
771
+ }
772
+ if (tree?.plants && typeof tree.plants === "object" && Object.keys(tree.plants).length > 0) {
773
+ inventory.plants = tree.plants;
774
+ }
704
775
  const withDna = attachInventoryDna(inventory);
705
776
  const xrayDir = path.join(targetDir, ".xray");
706
777
  if (!fs.existsSync(xrayDir)) fs.mkdirSync(xrayDir, { recursive: true });
@@ -735,6 +806,11 @@ function mintConsumerSuit(millPackageRoot, targetDir, log) {
735
806
  tree.blipPlantSkills = millPlant.blip?.skills || [];
736
807
  tree.blipPlantAgents = millPlant.blip?.agents || [];
737
808
  tree.blip = millPlant.blip || emptyPlantFiles();
809
+ tree.plants = millPlant.plants || {};
810
+ tree.seats = millPlant.seats || [];
811
+ tree.millPackage = millPlant.millPackage || null;
812
+ tree.millProtocol = millPlant.millProtocol || null;
813
+ tree.millPackageRoot = millPackageRoot;
738
814
  tree.shopPlantSkills = loadShopPlant(targetDir);
739
815
  tree.codex = overlayJsonFacet(
740
816
  resolveInside(targetDir, params.codex),
@@ -764,6 +840,20 @@ module.exports = {
764
840
  DEFAULT_PARAMS,
765
841
  FACTORY_SHOP_SKILLS,
766
842
  FACTORY_PLANT_CATALOG,
843
+ FOUNDRY_PLANT_PROTOCOL: millPlantProtocol.PROTOCOL,
844
+ PROTOCOL: millPlantProtocol.PROTOCOL,
845
+ PACKAGE_ALIASES: millPlantProtocol.PACKAGE_ALIASES,
846
+ parsePlantToken: millPlantProtocol.parsePlantToken,
847
+ parsePlantRef: millPlantProtocol.parsePlantRef,
848
+ resolvePackageRoot: millPlantProtocol.resolvePackageRoot,
849
+ readProtocol: millPlantProtocol.readProtocol,
850
+ prefixSelect: millPlantProtocol.prefixSelect,
851
+ selectPlantFiles: millPlantProtocol.selectPlantFiles,
852
+ declaredPlantFiles: millPlantProtocol.declaredPlantFiles,
853
+ resolvePlantSeats: millPlantProtocol.resolvePlantSeats,
854
+ loadPlantRequests: millPlantProtocol.loadPlantRequests,
855
+ declaredReceiptSeats: millPlantProtocol.declaredReceiptSeats,
856
+ inventoryFilesForKind: millPlantProtocol.inventoryFilesForKind,
767
857
  isFactoryPlantKind,
768
858
  deepMerge,
769
859
  loadFoundryParams,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xray/foundry",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "0xRay mill: mint their suit params, stamp, gate, publish, CI report, git hooks. Not the worn exo.",
@@ -37,15 +37,26 @@
37
37
  "./release-gate": "./release-gate.mjs",
38
38
  "./mill-root": "./mill-root.mjs",
39
39
  "./mint-suit": "./mint-suit.cjs",
40
+ "./mill-plant": "./mill-plant.cjs",
40
41
  "./inspect": "./inspect.mjs",
41
42
  "./sound": "./sound.mjs",
42
43
  "./sound-bed": "./sound-bed.cjs",
44
+ "./sound-mixer": "./sound-mixer.cjs",
43
45
  "./blip": "./blip.mjs",
44
46
  "./blip-render": "./blip-render.cjs",
47
+ "./blip-vibe": "./blip-vibe.cjs",
48
+ "./blip-looker": "./blip-looker.cjs",
49
+ "./blip-scene": "./blip-scene.cjs",
50
+ "./sound-taste": "./sound-taste.cjs",
51
+ "./sound-voice": "./sound-voice.cjs",
52
+ "./blip-stroke": "./blip-stroke.cjs",
45
53
  "./ci-monitor": "./ci-monitor.mjs",
46
54
  "./hooks": "./hooks.mjs",
47
55
  "./docs-build": "./docs-build.mjs"
48
56
  },
57
+ "dependencies": {
58
+ "@0xray/blip": "^0.1.3"
59
+ },
49
60
  "engines": {
50
61
  "node": ">=18"
51
62
  },
@@ -1,5 +1,5 @@
1
1
  name: blip-inspect
2
2
  skill: blip-inspect
3
3
  description: "Inspect the last factory-blip: 4.44s duration, mode, file present. Fail-closed."
4
- version: "0.1.10"
4
+ version: "0.1.12"
5
5
  mode: subagent
@@ -0,0 +1,5 @@
1
+ name: blip-looker
2
+ skill: blip-looker
3
+ description: "Friend hitting replay. Scores hook → turn → tag as a Short. Appeal 0–10. Ship bar is 8. Not stamp density. Not last-blip inspect."
4
+ version: "0.1.12"
5
+ mode: subagent
@@ -0,0 +1,5 @@
1
+ name: blip-vibe
2
+ skill: blip-vibe
3
+ description: "Aesthetic / cool-factor organ. Scores still + Rippel five + kapow. Appeal 0–10. Ship bar is 8. Not last-blip inspect."
4
+ version: "0.1.12"
5
+ mode: subagent
@@ -1,5 +1,5 @@
1
1
  name: blip
2
2
  skill: blip
3
3
  description: "Factory-blip plant. Brief → seed → still|motion:<id> → 4.44s mp4 → inspect gate."
4
- version: "0.1.10"
4
+ version: "0.1.12"
5
5
  mode: subagent
@@ -1,5 +1,5 @@
1
1
  name: inspect
2
2
  skill: inspect
3
3
  description: "Inspect AI work: diffs, traces, mill receipts, CI. Fastened mill plant."
4
- version: "0.1.10"
4
+ version: "0.1.12"
5
5
  mode: subagent
@@ -1,5 +1,5 @@
1
1
  name: mill
2
2
  skill: mill
3
3
  description: "Fasten their plant as the suit. Factory Repertoire. Inspect is the mill discipline."
4
- version: "0.1.10"
4
+ version: "0.1.12"
5
5
  mode: subagent
@@ -1,5 +1,5 @@
1
1
  name: sound-inspect
2
2
  skill: sound-inspect
3
3
  description: "Inspect the last factory-sound bed: chop, levels, peak, hum. Fail-closed."
4
- version: "0.1.10"
4
+ version: "0.1.12"
5
5
  mode: subagent
@@ -0,0 +1,5 @@
1
+ name: sound-mixer
2
+ skill: sound-mixer
3
+ description: "Tuner for hats, plate, glue, and phrase levels. Levels every genre × tempo × motif. Not last-bed inspect."
4
+ version: "0.1.12"
5
+ mode: subagent
@@ -1,5 +1,5 @@
1
1
  name: sound
2
2
  skill: sound
3
3
  description: "Factory-sound plant. Brief → seed → genre → headless wav → metrics gate."
4
- version: "0.1.10"
4
+ version: "0.1.12"
5
5
  mode: subagent
@@ -1,6 +1,6 @@
1
1
  # Motion registry (factory-blip)
2
2
 
3
- Law: motions are a dynamic registry (`id → renderer`). Rippel names are **imports**, not a frozen enum. Unknown id FAIL. `kapow` is a growth stub (`renderer: null`) until a renderer ships.
3
+ Law: motions are a dynamic registry (`id → renderer`). Rippel names are **imports**, not a frozen enum. Unknown id FAIL. `kapow` is a design opt — two-tier stamp on hook → turn → tag.
4
4
 
5
5
  On-disk SSOT: `registry.json` next to this file.
6
6
 
@@ -12,24 +12,30 @@ On-disk SSOT: `registry.json` next to this file.
12
12
 
13
13
  Default motion path is the Rippel converter spine, not ffmpeg geometry. Look is **sharp** (opaque body + crisp rim + short glow) on all five. Motion is a **4.44s blip**: fast abstract travel, soft gold tints — no hard strobe. LFOs share the crystal-mill genre tempo + `CircleConfig.frequency` with the audio bed.
14
14
 
15
+ One mill suit on every body: Wu hairline, Power Plant satellites, short glow, hook → turn → tag on the shared motion grid. Picture and bed seed-pick the same genre. Rippel geo stays (petals / N-gons / synapse / cosmos / aurora) and wears that suit — not a second CAD look.
16
+
15
17
  - **SSOT** `htafolla/rippel-synapse-flow@e5014cd`
16
18
  - `animationIcons.ts` → `ANIMATION_TO_VISUALIZATION`
17
19
  - `types/index.ts` → `ChecksumResponse.visualConfig` (`CircleConfig[]`, `CanvasConfig`, `tlmCommand`)
18
20
  - `SimplifiedVisualConverter.tsx` → Animation → viz id, canvas **1280×720** @ ~30fps
19
21
  - `MiniAnimationViewer` / `FiveDimensionalVisualizer` → viz backends
20
22
 
21
- | id | viz id | backend |
22
- |----|--------|---------|
23
- | `orb` | `canvas` | Orb Glow — dual rings, beat tick, frequency flash |
24
- | `swirl` | `3d-sacred` | merkaba + hex plate, counter-spin, beat vertices |
25
- | `snap` | `neural` | dual-ring lattice, hub, skip-links, traveling pulses |
26
- | `waves` | `waveform` | Wave Flow — harmonic ribbons + beat envelope + gold needle |
27
- | `spark` | `particles` | Spark Drift — beat bursts, orbital + radial motes |
23
+ | id | viz id | mill body | rippel body |
24
+ |----|--------|-----------|-------------|
25
+ | `orb` | `canvas` | `focus` disc **or** `cage` Wu + nucleus | `mandala` |
26
+ | `swirl` | `3d-sacred` | platonic `mesh` | `sacred-flow` |
27
+ | `snap` | `neural` | `strike` (quantized) | `synapse` |
28
+ | `waves` | `waveform` | sharp `ribbons` | `liquid-waves` |
29
+ | `spark` | `particles` | `embers` | `cosmic-dance` |
28
30
 
29
- Every mint wears a **sparse seed mesh** (12 families · 5 gaits) plus a **field** (stars · grids `floor`/`meridian`/`ticks` · gradients · blinkers). Uniqueness is the field, not more wire in the middle. Receipt `visualConfig.mesh` + `visualConfig.field` are the NFT fingerprints.
31
+ Every mint wears a **sparse seed mesh** (12 families · 5 gaits · 6 cameras) plus a **field** (stars · grids `floor`/`none` · gradients · blinkers) **under** the drawing. No outer-edge tick ruler or meridian box. Phrase eases spin: hook crawl → turn whip → tag hold. Camera is a seed shot (`front` `three-quarter` `top` `low` `dutch` `side`) with a turn push-in — same organ, different silhouette. Turn ruptures cage (gold lantern) and snap (held gold slash). Focus wears one large eclipse moon the noun eats. Mill swirl is a cyan/gold jewel, snap is a ghost hull + 2–3 strikes, spark is coals on verts and midpoints. Receipt `visualConfig.mesh` + `visualConfig.field` + `organ` are the NFT fingerprints.
30
32
 
31
33
  Motions encode **≥720p**. The old 320×180 ffmpeg wireframe is **`--engine wireframe` only**.
32
34
 
33
35
  Every Blip muxes a **4.44s audio bed** (`--bed PATH` or auto sound mill). No audio stream = inspect FAIL.
34
36
 
35
37
  `still` is the **Power Plant ident** — titlecard / corridor / rain / endcard dissolve over 4.44s at ≥720p. Not a frozen poster. Palette `#08090B` `#F5F7FA` `#3DE0E8` `#F5C518` `#4A7FD4`.
38
+
39
+ ## Design opt — `kapow`
40
+
41
+ Not a Rippel import and not a `--look`. `--mode motion:kapow` paints a comic two-tier burst: 8–10 outer points, 6–8 inner, some spikes longer than others. Ink outline, sparse speed lines, readable `KAPOW!`. Hook winds the outer ring. The jewel cut slams the inner burst and the word. Tag holds. One noun sits under the word. `blip-vibe` scores still + Rippel five + kapow on the same organ — every type must hit **8** to ship. Same seed camera / genre grid / 4.44s bed as the five.
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "version": 1,
3
- "law": "Motions are a dynamic registry (id → renderer). Rippel names are imports. Kapow is a growth stub until a renderer ships.",
3
+ "law": "Motions are a dynamic registry (id → renderer). Rippel names are imports. Kapow is a design opt — two-tier stamp on the stanza.",
4
4
  "v0": ["still", "orb", "swirl", "snap", "waves", "spark"],
5
+ "opt": ["kapow"],
5
6
  "palette": {
6
7
  "void": "#08090B",
7
8
  "ink": "#F5F7FA",
@@ -73,9 +74,9 @@
73
74
  "id": "kapow",
74
75
  "kind": "motion",
75
76
  "source": "blips",
76
- "renderer": null,
77
- "status": "growth",
78
- "notes": "named growth slot — FAIL until a renderer ships"
77
+ "renderer": "kapow",
78
+ "status": "opt",
79
+ "notes": "Two-tier KAPOW on hook → turn → tag. Outer burst winds, inner burst + word stamp the jewel cut. Not a look. ≥720p + 4.44s bed."
79
80
  }
80
81
  ]
81
82
  }
@@ -13,7 +13,7 @@ This **factory plant** fastens a tiny-video factory. 4.44 seconds of picture **w
13
13
 
14
14
  `still` · `orb` · `swirl` · `snap` · `waves` · `spark`
15
15
 
16
- Rippel five are **imports**. Each mint is a **4.44s Short** — hook → turn → tag on one grid for picture and bed (continuous `hookEase`/`turnEase`/`tagEase`, not gear-shift flags). Seed-selected look (`focus` solid disc + satellites + field/mesh mass, or `cage` Wu hairline + field). Receipt `visualConfig.lookKind` + mesh + field are the fingerprints. `kapow` is a growth stub (FAIL until a renderer ships). Unknown id FAIL.
16
+ Rippel five are **imports**. Four seed + flag axes: `--look focus|cage`, `--body mill|rippel`, `--genre ambient|techno|phonk|jazz|rock|timeless`, `--camera front|three-quarter|top|low|dutch|side` (unknown camera FAIL). Suit is void + five hexes + Wu + hook → turn → tag — not a circle pack on every organ. Focus keeps the disc + one large eclipse moon the noun eats. Cage and snap rupture on the turn (gold lantern / held slash). Mill swirl is Wu mesh, snap is strike, spark is embers. Same organ, different shot — top flattens, low lifts, dutch slashes the floor. Turn dollies in. Cage recoils then whips. Receipt `lookKind` + `bodyKind` + `genre` + `organ` + `camera` + `stereoImage`. `kapow` is a design opt (`--mode motion:kapow`) — two-tier stamp, not a third look. Unknown id FAIL.
17
17
 
18
18
  `still` is the **Power Plant ident** — titlecard / corridor / rain / endcard dissolve over 4.44s at ≥720p, not a frozen poster: void `#08090B` · ink `#F5F7FA` · cyan `#3DE0E8` · gold `#F5C518` · agent blue `#4A7FD4`. Motions use that palette as the Rippel `VisualConfig.circles` theme. The mill is the renderer — no LLM in the pixel/audio loop.
19
19
 
@@ -21,8 +21,8 @@ Rippel five are **imports**. Each mint is a **4.44s Short** — hook → turn
21
21
 
22
22
  1. **Brief** → checksum seed → Rippel `VisualConfig` (`CircleConfig[]`).
23
23
  2. **Mode** → `still` or `motion:<id>` from the registry.
24
- 3. **Render** → 4.44s mp4. Motions are Rippel v2 viz at ≥720p (`--look focus|cage`, seed picks if omitted). `--engine wireframe` is emergency only.
25
- 4. **Bed** → `--bed PATH` or auto sound mill. Auto bed **syncopates** to the motion grid (same seed + tempo + phase; downbeat kick / AND hat). Every mp4 muxes **stereo AAC**. Missing stream or inaudible bed = inspect FAIL.
24
+ 3. **Render** → 4.44s mp4. Motions are Rippel v2 viz at ≥720p (`--look focus|cage --body mill|rippel`, seed picks if omitted). `--engine wireframe` is emergency only.
25
+ 4. **Bed** → `--bed PATH` or auto sound mill. Auto bed **syncopates** to the motion grid (same seed + tempo + phase; downbeat kick / AND hat). The crash + motif answer land on the jewel cut. Every mp4 muxes **stereo AAC**. Missing stream or inaudible bed = inspect FAIL.
26
26
  5. **Inspect gate** → file, duration 4.44s±tol, mode id, **audio stream**, motion ≥720p. Fail-closed.
27
27
  6. **Receipt** → `.xray/blip/receipt.json` (`PASS` / `FAIL` + pictureMode + motionId + visualConfig).
28
28
 
@@ -34,6 +34,6 @@ npx @0xray/foundry blip inspect
34
34
  npx @0xray/foundry inspect --skip-live
35
35
  ```
36
36
 
37
- Mint fastens `blip` + `blip-inspect`. Chat is not a receipt.
37
+ Mint fastens `blip` + `blip-inspect` + `blip-vibe`. Chat is not a receipt.
38
38
 
39
39
  A friend would hear: Blips should look like Rippel living motions with sound, not a silent wireframe box.