0xray 4.0.12 → 4.0.14

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 (58) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +1 -1
  3. package/.xray/features.schema.json +1 -1
  4. package/AGENTS-consumer.md +4 -2
  5. package/AGENTS.md +8 -1
  6. package/README.md +1 -1
  7. package/SKILLS.md +1 -1
  8. package/dist/AGENTS.md +8 -1
  9. package/dist/CHANGELOG.md +25 -1
  10. package/dist/README.md +1 -1
  11. package/dist/SKILLS.md +1 -1
  12. package/dist/integrations/cursor/README.md +61 -0
  13. package/dist/integrations/cursor/hooks/after-file-edit.js +38 -0
  14. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +170 -0
  15. package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +298 -0
  16. package/dist/integrations/cursor/hooks/hooks.json +20 -0
  17. package/dist/integrations/cursor/hooks/pre-compact.js +88 -0
  18. package/dist/integrations/cursor/hooks/pre-tool-use.js +105 -0
  19. package/dist/integrations/hooks/station-hook-runtime.cjs +89 -1
  20. package/dist/integrations/hooks/station-hook-runtime.mjs +2 -0
  21. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  22. package/dist/nucleus/suit-temperament.d.ts +2 -2
  23. package/dist/nucleus/suit-temperament.js +3 -1
  24. package/llms.txt +6 -4
  25. package/package.json +4 -2
  26. package/scripts/foundry/CHANGELOG.md +37 -1
  27. package/scripts/foundry/README.md +13 -1
  28. package/scripts/foundry/assert-packed-dist-cli.mjs +97 -0
  29. package/scripts/foundry/blip-render.cjs +1237 -0
  30. package/scripts/foundry/blip-rippel.cjs +1963 -0
  31. package/scripts/foundry/blip.mjs +109 -0
  32. package/scripts/foundry/cli.mjs +8 -4
  33. package/scripts/foundry/inspect.mjs +117 -7
  34. package/scripts/foundry/mint-suit.cjs +215 -14
  35. package/scripts/foundry/package.json +7 -1
  36. package/scripts/foundry/plant/agents/blip-inspect.yml +5 -0
  37. package/scripts/foundry/plant/agents/blip.yml +5 -0
  38. package/scripts/foundry/plant/agents/inspect.yml +1 -1
  39. package/scripts/foundry/plant/agents/mill.yml +1 -1
  40. package/scripts/foundry/plant/agents/sound-inspect.yml +5 -0
  41. package/scripts/foundry/plant/agents/sound.yml +5 -0
  42. package/scripts/foundry/plant/motions/MOTION-REGISTRY.md +35 -0
  43. package/scripts/foundry/plant/motions/registry.json +81 -0
  44. package/scripts/foundry/plant/skills/blip/SKILL.md +39 -0
  45. package/scripts/foundry/plant/skills/blip-inspect/SKILL.md +32 -0
  46. package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -2
  47. package/scripts/foundry/plant/skills/sound/SKILL.md +29 -0
  48. package/scripts/foundry/plant/skills/sound-inspect/SKILL.md +28 -0
  49. package/scripts/foundry/release-gate.mjs +18 -10
  50. package/scripts/foundry/release.mjs +3 -0
  51. package/scripts/foundry/sound-bed.cjs +454 -0
  52. package/scripts/foundry/sound-rippel.cjs +1434 -0
  53. package/scripts/foundry/sound.mjs +93 -0
  54. package/scripts/node/assert-packed-dist-cli.mjs +4 -0
  55. package/scripts/node/consumer-install-smoke.mjs +20 -1
  56. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  57. package/xray/features.json +1 -1
  58. package/xray/features.schema.json +1 -1
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Factory-blip CLI — sibling plant to mill + sound, not a mill bolt-on.
4
+ * npx @0xray/foundry blip render --brief TEXT --mode still|motion:<id> [--look focus|cage] [--bed PATH] [--engine rippel|wireframe] [--out FILE]
5
+ * npx @0xray/foundry blip inspect
6
+ */
7
+
8
+ import { createRequire } from "node:module";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { resolveMillRoot } from "./mill-root.mjs";
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const blip = require("./blip-render.cjs");
15
+
16
+ const HELP =
17
+ "Usage: npx @0xray/foundry blip <render|inspect> [...args]\n" +
18
+ "\n" +
19
+ "Factory-blip plant, sibling to mill + sound. FOUNDRY_ROOT overrides cwd.\n" +
20
+ "render: brief → checksum seed → still|motion:<id> → 4.44s mp4 + audio bed → receipt.\n" +
21
+ "v0 ids: still, orb, swirl, snap, waves, spark (Rippel five are imports).\n" +
22
+ "Motions: Rippel VisualConfig.circles at ≥720p (default). --look focus|cage (seed picks if omitted).\n" +
23
+ "--engine wireframe is emergency only.\n" +
24
+ "Every mp4 muxes a 4.44s bed (--bed PATH or auto sound mill). Silent = inspect FAIL.\n" +
25
+ "kapow is a growth stub (FAIL until a renderer ships). Unknown id FAIL.\n" +
26
+ "still: Power Plant ident — plate dissolves over 4.44s at ≥720p, not a frozen poster.\n" +
27
+ "inspect: last .xray/blip/receipt.json PASS/FAIL (missing is FAIL). Lists live registry ids.\n";
28
+
29
+ function argValue(argv, name, fallback) {
30
+ const i = argv.indexOf(name);
31
+ if (i < 0 || i + 1 >= argv.length) return fallback;
32
+ return argv[i + 1];
33
+ }
34
+
35
+ function writeJson(value) {
36
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
37
+ }
38
+
39
+ function inspectBlip(root) {
40
+ const motions = blip.listMotionIds();
41
+ const receipt = blip.readReceipt(root);
42
+ if (!receipt) {
43
+ return {
44
+ ok: false,
45
+ status: "FAIL",
46
+ failClosed: true,
47
+ plant: "blip",
48
+ motions,
49
+ detail: "missing .xray/blip/receipt.json",
50
+ receipt: null,
51
+ };
52
+ }
53
+ const evaluated = blip.evaluateReceipt(root, receipt);
54
+ const status = evaluated.status === "PASS" ? "PASS" : "FAIL";
55
+ return {
56
+ ok: status === "PASS",
57
+ status,
58
+ failClosed: true,
59
+ plant: "blip",
60
+ motions,
61
+ mode: evaluated.mode || receipt.motionId || receipt.mode || null,
62
+ pictureMode: evaluated.pictureMode || receipt.pictureMode || null,
63
+ durationSec: evaluated.durationSec ?? receipt.durationSec ?? null,
64
+ receipt,
65
+ };
66
+ }
67
+
68
+ function main() {
69
+ const argv = process.argv.slice(2);
70
+ const cmd = argv[0];
71
+ const rest = argv.slice(1);
72
+ const root = resolveMillRoot();
73
+
74
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
75
+ process.stdout.write(HELP);
76
+ process.exit(cmd ? 0 : 1);
77
+ }
78
+
79
+ if (cmd === "inspect") {
80
+ const report = inspectBlip(root);
81
+ writeJson(report);
82
+ process.exit(report.ok ? 0 : 1);
83
+ }
84
+
85
+ if (cmd === "render") {
86
+ const result = blip.renderBlip({
87
+ root,
88
+ brief: argValue(rest, "--brief", "factory-blip"),
89
+ pictureMode: argValue(rest, "--picture-mode", argValue(rest, "--mode", "still")),
90
+ lookKind: argValue(rest, "--look", null),
91
+ bed: argValue(rest, "--bed", null),
92
+ engine: argValue(rest, "--engine", "rippel"),
93
+ out: argValue(rest, "--out", null),
94
+ });
95
+ writeJson(result.receipt);
96
+ process.exit(result.receipt.status === "PASS" ? 0 : 1);
97
+ }
98
+
99
+ process.stderr.write(HELP);
100
+ process.exit(1);
101
+ }
102
+
103
+ export { inspectBlip };
104
+
105
+ const isMain =
106
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
107
+ if (isMain) {
108
+ main();
109
+ }
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * @0xray/foundry CLI — mill, not exo. No fifth MCP.
4
4
  *
5
- * npx @0xray/foundry <reconcile|stamp|gate|release|docs-check|docs-build|mint|inspect|ci|hooks> [...args]
5
+ * npx @0xray/foundry <reconcile|stamp|gate|release|docs-check|docs-build|mint|inspect|sound|blip|ci|hooks> [...args]
6
6
  */
7
7
 
8
8
  import { spawnSync } from "node:child_process";
@@ -17,17 +17,21 @@ const COMMANDS = {
17
17
  "docs-build": { script: "docs-build.mjs", preset: [] },
18
18
  mint: { script: "mint.mjs", preset: [] },
19
19
  inspect: { script: "inspect.mjs", preset: [] },
20
+ sound: { script: "sound.mjs", preset: [] },
21
+ blip: { script: "blip.mjs", preset: [] },
20
22
  ci: { script: "ci-monitor.mjs", preset: [] },
21
23
  hooks: { script: "hooks.mjs", preset: [] },
22
24
  };
23
25
 
24
26
  const HELP =
25
- "Usage: npx @0xray/foundry <reconcile|stamp|gate|release|docs-check|docs-build|mint|inspect|ci|hooks> [...args]\n" +
27
+ "Usage: npx @0xray/foundry <reconcile|stamp|gate|release|docs-check|docs-build|mint|inspect|sound|blip|ci|hooks> [...args]\n" +
26
28
  "\n" +
27
29
  "Mill, not exo. FOUNDRY_ROOT overrides cwd (the repo being milled).\n" +
28
30
  "release bumps/commits/pushes/publishes the milled cwd — pass --dry-run or --i-mean-it (or FOUNDRY_RELEASE=1).\n" +
29
- "Mint fastens mill plant then overlays their plant (mill-fill JSON), then runs inspect. PPE stays worn.\n" +
30
- "inspect runs the six mill checks (diff, plant vs worn, receipt, CI, live tarball GET, isolated HOME). Not an 8th MCP.\n" +
31
+ "Mint fastens the requested factory plant (default mill; foundry.json plant: sound|blip turns mill off), then inspect.\n" +
32
+ "inspect runs mill checks plus, on a sound seat, last bed receipt PASS/FAIL; on a blip seat, last blip receipt.\n" +
33
+ "sound render|inspect is the factory-sound plant (brief → seed → genre → wav → metrics). Sibling to mill, not a mill copy.\n" +
34
+ "blip render|inspect is the factory-blip plant (brief → seed → still|motion:<id> → 4.44s mp4). Sibling to mill + sound.\n" +
31
35
  "gate is build+test. docs-build is Docusaurus on the 0xray exo. ci reports GitHub Actions (no auto-push). hooks installs git pre/post hooks.\n";
32
36
 
33
37
  const cmd = process.argv[2];
@@ -82,15 +82,25 @@ function checkDiff(root) {
82
82
  }
83
83
 
84
84
  function checkPlantVsWorn(root, millRoot) {
85
- const plantDir = mint.millPlantDir(millRoot);
85
+ const kinds = mint.loadFactoryPlantKinds(root);
86
+ const allow = mint.factoryPlantAllowlist(millRoot, root);
86
87
  const millPlant = {
87
- skills: plantDir ? mint.listSkillNamesAt(path.join(plantDir, "skills")) : [],
88
- agents: plantDir ? mint.listAgentFilesAt(path.join(plantDir, "agents")) : [],
88
+ skills: kinds.includes("mill") ? mint.catalogPlant("mill").skills : [],
89
+ agents: kinds.includes("mill") ? mint.catalogPlant("mill").agents : [],
90
+ sound: kinds.includes("sound") ? mint.catalogPlant("sound") : { skills: [], agents: [] },
91
+ blip: kinds.includes("blip") ? mint.catalogPlant("blip") : { skills: [], agents: [] },
89
92
  };
90
93
  const params = mint.loadFoundryParams(root);
91
94
  const tree = {
92
95
  skills: mint.listConsumerSkillNames(root, params.skills),
93
96
  agents: mint.listConsumerAgentFiles(root, params.agents),
97
+ plantKinds: kinds,
98
+ soundPlantSkills: millPlant.sound.skills,
99
+ soundPlantAgents: millPlant.sound.agents,
100
+ sound: millPlant.sound,
101
+ blipPlantSkills: millPlant.blip.skills,
102
+ blipPlantAgents: millPlant.blip.agents,
103
+ blip: millPlant.blip,
94
104
  };
95
105
  const shopPlant = mint.loadShopPlant(root);
96
106
  try {
@@ -98,10 +108,14 @@ function checkPlantVsWorn(root, millRoot) {
98
108
  return {
99
109
  id: "plant-vs-worn",
100
110
  ok: true,
111
+ plant: kinds,
101
112
  millPlant: millPlant.skills,
113
+ soundPlant: millPlant.sound.skills,
114
+ blipPlant: millPlant.blip.skills,
102
115
  shopPlant,
103
116
  tree: tree.skills,
104
117
  worn: mint.wornSkillNames(root),
118
+ allow: allow.skills,
105
119
  };
106
120
  } catch (err) {
107
121
  return {
@@ -110,6 +124,9 @@ function checkPlantVsWorn(root, millRoot) {
110
124
  detail: err instanceof Error ? err.message : String(err),
111
125
  extraSkills: err.extraSkills || [],
112
126
  extraAgents: err.extraAgents || [],
127
+ plant: kinds,
128
+ soundPlant: millPlant.sound.skills,
129
+ blipPlant: millPlant.blip.skills,
113
130
  shopPlant,
114
131
  };
115
132
  }
@@ -125,18 +142,107 @@ function checkReceipt(root) {
125
142
  return { id: "receipt", ok: false, detail: "missing .xray/foundry-inventory.json" };
126
143
  }
127
144
  const millPlantSkills = inventory.millPlant?.skills || [];
128
- const hasMill = millPlantSkills.includes("mill");
129
- const hasInspect = millPlantSkills.includes("inspect");
130
- const ok = hasMill && hasInspect;
145
+ const soundPlantSkills = inventory.soundPlant?.skills || [];
146
+ const blipPlantSkills = inventory.blipPlant?.skills || [];
147
+ const kinds = mint.inventoryPlantKinds(inventory);
148
+ const needMill = kinds.includes("mill");
149
+ const needSound = kinds.includes("sound");
150
+ const needBlip = kinds.includes("blip");
151
+ const millOk = !needMill || (millPlantSkills.includes("mill") && millPlantSkills.includes("inspect"));
152
+ const soundOk =
153
+ !needSound || (soundPlantSkills.includes("sound") && soundPlantSkills.includes("sound-inspect"));
154
+ const blipOk =
155
+ !needBlip || (blipPlantSkills.includes("blip") && blipPlantSkills.includes("blip-inspect"));
156
+ const ok = millOk && soundOk && blipOk && (needMill || needSound || needBlip);
157
+ let detail = null;
158
+ if (!ok && needMill && !millOk) detail = "millPlant.skills must include mill and inspect";
159
+ else if (!ok && needSound && !soundOk) detail = "soundPlant.skills must include sound and sound-inspect";
160
+ else if (!ok && needBlip && !blipOk) detail = "blipPlant.skills must include blip and blip-inspect";
161
+ else if (!ok) detail = "inventory plant is empty";
131
162
  return {
132
163
  id: "receipt",
133
164
  ok,
134
165
  suit: inventory.suit,
135
166
  mill: inventory.mill,
167
+ plant: kinds,
136
168
  millPlant: millPlantSkills,
169
+ soundPlant: soundPlantSkills,
170
+ blipPlant: blipPlantSkills,
137
171
  dna: typeof inventory.dna === "string" ? inventory.dna : null,
138
172
  pack: "0xray-suit",
139
- detail: ok ? null : "millPlant.skills must include mill and inspect",
173
+ detail,
174
+ };
175
+ }
176
+
177
+ function checkSoundBed(root) {
178
+ const inventory = readJson(path.join(root, ".xray", "foundry-inventory.json"));
179
+ const kinds = inventory ? mint.inventoryPlantKinds(inventory) : mint.loadFactoryPlantKinds(root);
180
+ if (!kinds.includes("sound")) return null;
181
+ const bed = require("./sound-bed.cjs");
182
+ const receipt = bed.readReceipt(root);
183
+ if (!receipt) {
184
+ return {
185
+ id: "sound-bed",
186
+ ok: true,
187
+ skipped: true,
188
+ status: "NONE",
189
+ plant: kinds,
190
+ detail: "no bed receipt yet — npx @0xray/foundry sound render",
191
+ };
192
+ }
193
+ const status = receipt.status === "PASS" ? "PASS" : "FAIL";
194
+ const unreadable = receipt.reason === "receipt-unreadable";
195
+ return {
196
+ id: "sound-bed",
197
+ ok: status === "PASS" && !unreadable,
198
+ status,
199
+ failClosed: true,
200
+ plant: kinds,
201
+ metrics: receipt.metrics || null,
202
+ gates: receipt.gates || null,
203
+ wav: receipt.wav || null,
204
+ seed: receipt.seed || null,
205
+ detail: status === "PASS" ? null : receipt.reason || "bed receipt FAIL",
206
+ };
207
+ }
208
+
209
+ function checkBlip(root) {
210
+ const inventory = readJson(path.join(root, ".xray", "foundry-inventory.json"));
211
+ const kinds = inventory ? mint.inventoryPlantKinds(inventory) : mint.loadFactoryPlantKinds(root);
212
+ if (!kinds.includes("blip")) return null;
213
+ const blip = require("./blip-render.cjs");
214
+ const receipt = blip.readReceipt(root);
215
+ const motions = blip.listMotionIds();
216
+ if (!receipt) {
217
+ return {
218
+ id: "blip",
219
+ ok: true,
220
+ skipped: true,
221
+ status: "NONE",
222
+ plant: kinds,
223
+ motions,
224
+ detail: "no blip receipt yet — npx @0xray/foundry blip render --brief \"...\" --mode still",
225
+ };
226
+ }
227
+ const evaluated = blip.evaluateReceipt(root, receipt);
228
+ const status = evaluated.status === "PASS" ? "PASS" : "FAIL";
229
+ const unreadable = receipt.reason === "receipt-unreadable";
230
+ return {
231
+ id: "blip",
232
+ ok: status === "PASS" && !unreadable,
233
+ status,
234
+ failClosed: true,
235
+ plant: kinds,
236
+ motions,
237
+ mode: evaluated.mode || receipt.motionId || receipt.mode || null,
238
+ pictureMode: evaluated.pictureMode || receipt.pictureMode || null,
239
+ palette: receipt.palette || null,
240
+ plate: receipt.plate || null,
241
+ durationSec: evaluated.durationSec ?? receipt.durationSec ?? null,
242
+ mp4: evaluated.mp4 || receipt.mp4 || null,
243
+ hasVideo: evaluated.hasVideo ?? receipt.hasVideo ?? null,
244
+ hasAudio: evaluated.hasAudio ?? receipt.hasAudio ?? null,
245
+ detail: status === "PASS" ? null : evaluated.reason || receipt.reason || "blip receipt FAIL",
140
246
  };
141
247
  }
142
248
 
@@ -268,6 +374,10 @@ export async function inspectSuit(root, opts = {}) {
268
374
  checks.push(checkDiff(root));
269
375
  checks.push(checkPlantVsWorn(root, millRoot));
270
376
  checks.push(checkReceipt(root));
377
+ const soundBed = checkSoundBed(root);
378
+ if (soundBed) checks.push(soundBed);
379
+ const blip = checkBlip(root);
380
+ if (blip) checks.push(blip);
271
381
  checks.push(checkCi(root));
272
382
 
273
383
  if (skipLive) {
@@ -24,6 +24,17 @@ const DEFAULT_PARAMS = {
24
24
  /** Factory shop plant. First-class with mill plant. Not 45/42 costume. */
25
25
  const FACTORY_SHOP_SKILLS = ["shop-extract", "shop-witness", "shop-pin"];
26
26
 
27
+ /** Factory plant kinds. Mill is default. Sound is a bed factory. Blip is a tiny-video factory. */
28
+ const FACTORY_PLANT_CATALOG = {
29
+ 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
+ };
33
+
34
+ function isFactoryPlantKind(kind) {
35
+ return Boolean(kind && FACTORY_PLANT_CATALOG[kind]);
36
+ }
37
+
27
38
  function deepMerge(src, dest) {
28
39
  if (typeof src !== "object" || src === null) return dest !== undefined ? dest : src;
29
40
  if (Array.isArray(src)) return Array.isArray(dest) ? dest : src;
@@ -118,6 +129,90 @@ function millPlantDir(millPackageRoot) {
118
129
  return null;
119
130
  }
120
131
 
132
+ function emptyPlantFiles() {
133
+ return { skills: [], agents: [] };
134
+ }
135
+
136
+ function catalogPlant(kind) {
137
+ const spec = FACTORY_PLANT_CATALOG[kind];
138
+ return spec ? { skills: [...spec.skills], agents: [...spec.agents] } : emptyPlantFiles();
139
+ }
140
+
141
+ /** foundry.json plant: "sound" | "blip" | "mill" | ["mill","sound","blip"]. millPlant:false turns mill off. */
142
+ function loadFactoryPlantKinds(targetDir) {
143
+ 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)];
163
+ }
164
+
165
+ function unionPlantFiles(kinds) {
166
+ const skills = [];
167
+ const agents = [];
168
+ for (const kind of kinds) {
169
+ const spec = catalogPlant(kind);
170
+ for (const name of spec.skills) {
171
+ if (!skills.includes(name)) skills.push(name);
172
+ }
173
+ for (const file of spec.agents) {
174
+ if (!agents.includes(file)) agents.push(file);
175
+ }
176
+ }
177
+ return { skills, agents };
178
+ }
179
+
180
+ function existingPlantFiles(plantDir, spec) {
181
+ if (!plantDir || !spec) return emptyPlantFiles();
182
+ const skillsSrc = path.join(plantDir, "skills");
183
+ const agentsSrc = path.join(plantDir, "agents");
184
+ const haveSkills = new Set(listSkillNamesAt(skillsSrc));
185
+ const haveAgents = new Set(listAgentFilesAt(agentsSrc));
186
+ return {
187
+ skills: spec.skills.filter((name) => haveSkills.has(name)),
188
+ agents: spec.agents.filter((file) => haveAgents.has(file)),
189
+ };
190
+ }
191
+
192
+ function factoryPlantAllowlist(millPackageRoot, targetDir) {
193
+ const kinds = loadFactoryPlantKinds(targetDir);
194
+ return {
195
+ kinds,
196
+ ...existingPlantFiles(millPlantDir(millPackageRoot), unionPlantFiles(kinds)),
197
+ };
198
+ }
199
+
200
+ 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"];
214
+ }
215
+
121
216
  function listSkillNamesAt(skillsSrc) {
122
217
  if (!isDirectory(skillsSrc)) return [];
123
218
  const names = [];
@@ -141,20 +236,18 @@ function listAgentFilesAt(agentsSrc) {
141
236
  .map((entry) => entry.name);
142
237
  }
143
238
 
144
- /** Fasten mill plant first. Mill names they did not plant stay; same-name overlay wins. */
145
- function fastenMillPlant(millPackageRoot, targetDir, log) {
146
- const plant = millPlantDir(millPackageRoot);
147
- if (!plant) return { skills: [], agents: [] };
239
+ function copyPlantFiles(plant, spec, targetDir) {
148
240
  const skillsSrc = path.join(plant, "skills");
149
241
  const agentsSrc = path.join(plant, "agents");
150
- const skills = listSkillNamesAt(skillsSrc);
151
- const agents = listAgentFilesAt(agentsSrc);
242
+ const skills = spec.skills || [];
243
+ const agents = spec.agents || [];
152
244
  const skillDirs = listProjectSkillDirs(targetDir);
153
245
  const agentsDest = path.join(targetDir, ".opencode", "agents");
154
246
  for (const dir of skillDirs) {
155
247
  for (const name of skills) {
156
248
  const src = path.join(skillsSrc, name, "SKILL.md");
157
249
  const destMd = path.join(dir, name, "SKILL.md");
250
+ if (!fs.existsSync(src)) continue;
158
251
  if (path.resolve(src) === path.resolve(destMd)) continue;
159
252
  if (fs.existsSync(destMd)) continue;
160
253
  fs.mkdirSync(path.dirname(destMd), { recursive: true });
@@ -164,18 +257,44 @@ function fastenMillPlant(millPackageRoot, targetDir, log) {
164
257
  for (const file of agents) {
165
258
  const src = path.join(agentsSrc, file);
166
259
  const dest = path.join(agentsDest, file);
260
+ if (!fs.existsSync(src)) continue;
167
261
  if (path.resolve(src) === path.resolve(dest)) continue;
168
262
  if (fs.existsSync(dest)) continue;
169
263
  fs.mkdirSync(agentsDest, { recursive: true });
170
264
  fs.copyFileSync(src, dest);
171
265
  }
172
- if (log && (skills.length > 0 || agents.length > 0)) {
173
- log("foundry-mint", "Fastened mill plant", "info", {
174
- skills: skills.length,
175
- agents: agents.length,
266
+ return { skills, agents };
267
+ }
268
+
269
+ /** Fasten requested factory plants. Default mill only. Sound/blip seats skip mill unless asked. */
270
+ 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"))
278
+ : emptyPlantFiles();
279
+ const sound = kinds.includes("sound")
280
+ ? existingPlantFiles(plant, catalogPlant("sound"))
281
+ : emptyPlantFiles();
282
+ const blip = kinds.includes("blip")
283
+ ? existingPlantFiles(plant, catalogPlant("blip"))
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)) {
291
+ log("foundry-mint", "Fastened factory plant", "info", {
292
+ plant: kinds,
293
+ skills: fastened.skills.length,
294
+ agents: fastened.agents.length,
176
295
  });
177
296
  }
178
- return { skills, agents };
297
+ return { skills: mill.skills, agents: mill.agents, kinds, sound, blip };
179
298
  }
180
299
 
181
300
  function isDirectory(p) {
@@ -371,30 +490,80 @@ function previousTreeAllowlist(targetDir) {
371
490
  const shopSkills = Array.isArray(shopFromInv)
372
491
  ? normalizeNameList(shopFromInv)
373
492
  : normalizeNameList(shopFromInv?.skills);
493
+ const soundFromInv = inventory?.soundPlant;
494
+ const soundSkills = Array.isArray(soundFromInv)
495
+ ? normalizeNameList(soundFromInv)
496
+ : normalizeNameList(soundFromInv?.skills);
497
+ const soundAgents = normalizeNameList(soundFromInv?.agents);
498
+ const blipFromInv = inventory?.blipPlant;
499
+ const blipSkills = Array.isArray(blipFromInv)
500
+ ? normalizeNameList(blipFromInv)
501
+ : normalizeNameList(blipFromInv?.skills);
502
+ const blipAgents = normalizeNameList(blipFromInv?.agents);
374
503
  return {
375
504
  skills: [
376
505
  ...(Array.isArray(inventory?.tree?.skills) ? inventory.tree.skills : []),
506
+ ...(Array.isArray(inventory?.millPlant?.skills) ? inventory.millPlant.skills : []),
377
507
  ...shopSkills,
508
+ ...soundSkills,
509
+ ...blipSkills,
510
+ ],
511
+ agents: [
512
+ ...(Array.isArray(inventory?.tree?.agents) ? inventory.tree.agents : []),
513
+ ...(Array.isArray(inventory?.millPlant?.agents) ? inventory.millPlant.agents : []),
514
+ ...soundAgents,
515
+ ...blipAgents,
378
516
  ],
379
- agents: Array.isArray(inventory?.tree?.agents) ? inventory.tree.agents : [],
380
517
  };
381
518
  } catch {
382
519
  return { skills: [], agents: [] };
383
520
  }
384
521
  }
385
522
 
386
- /** Extra worn names that are neither mill plant, their plant, shop plant, nor a prior overlay. */
523
+ function soundPlantFromTree(targetDir, tree) {
524
+ if (Array.isArray(tree?.soundPlantSkills) || Array.isArray(tree?.sound?.skills)) {
525
+ return {
526
+ skills: normalizeNameList(tree.soundPlantSkills || tree.sound?.skills),
527
+ agents: normalizeNameList(tree.soundPlantAgents || tree.sound?.agents),
528
+ };
529
+ }
530
+ const kinds = Array.isArray(tree?.plantKinds) ? tree.plantKinds : loadFactoryPlantKinds(targetDir);
531
+ return kinds.includes("sound") ? catalogPlant("sound") : emptyPlantFiles();
532
+ }
533
+
534
+ function blipPlantFromTree(targetDir, tree) {
535
+ if (Array.isArray(tree?.blipPlantSkills) || Array.isArray(tree?.blip?.skills)) {
536
+ return {
537
+ skills: normalizeNameList(tree.blipPlantSkills || tree.blip?.skills),
538
+ agents: normalizeNameList(tree.blipPlantAgents || tree.blip?.agents),
539
+ };
540
+ }
541
+ const kinds = Array.isArray(tree?.plantKinds) ? tree.plantKinds : loadFactoryPlantKinds(targetDir);
542
+ return kinds.includes("blip") ? catalogPlant("blip") : emptyPlantFiles();
543
+ }
544
+
545
+ /** Extra worn names that are neither mill/sound/blip plant, their plant, shop plant, nor a prior overlay. */
387
546
  function costumeDumpExtras(targetDir, millPlant, tree) {
388
547
  const prior = previousTreeAllowlist(targetDir);
389
548
  const shopPlant = loadShopPlant(targetDir);
549
+ const soundPlant = soundPlantFromTree(targetDir, tree);
550
+ const blipPlant = blipPlantFromTree(targetDir, tree);
390
551
  const allowedSkills = new Set([
391
552
  ...(Array.isArray(millPlant?.skills) ? millPlant.skills : []),
553
+ ...(Array.isArray(millPlant?.sound?.skills) ? millPlant.sound.skills : []),
554
+ ...(Array.isArray(millPlant?.blip?.skills) ? millPlant.blip.skills : []),
555
+ ...soundPlant.skills,
556
+ ...blipPlant.skills,
392
557
  ...(Array.isArray(tree?.skills) ? tree.skills : []),
393
558
  ...prior.skills,
394
559
  ...shopPlant,
395
560
  ]);
396
561
  const allowedAgents = new Set([
397
562
  ...(Array.isArray(millPlant?.agents) ? millPlant.agents : []),
563
+ ...(Array.isArray(millPlant?.sound?.agents) ? millPlant.sound.agents : []),
564
+ ...(Array.isArray(millPlant?.blip?.agents) ? millPlant.blip.agents : []),
565
+ ...soundPlant.agents,
566
+ ...blipPlant.agents,
398
567
  ...(Array.isArray(tree?.agents) ? tree.agents : []),
399
568
  ...prior.agents,
400
569
  ]);
@@ -482,10 +651,19 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
482
651
  const millPlanted =
483
652
  (Array.isArray(tree?.millPlantSkills) && tree.millPlantSkills.length > 0) ||
484
653
  (Array.isArray(tree?.millPlantAgents) && tree.millPlantAgents.length > 0);
654
+ const soundPlanted =
655
+ (Array.isArray(tree?.soundPlantSkills) && tree.soundPlantSkills.length > 0) ||
656
+ (Array.isArray(tree?.soundPlantAgents) && tree.soundPlantAgents.length > 0);
657
+ const blipPlanted =
658
+ (Array.isArray(tree?.blipPlantSkills) && tree.blipPlantSkills.length > 0) ||
659
+ (Array.isArray(tree?.blipPlantAgents) && tree.blipPlantAgents.length > 0);
660
+ const plantKinds = Array.isArray(tree?.plantKinds) && tree.plantKinds.length > 0
661
+ ? tree.plantKinds
662
+ : loadFactoryPlantKinds(targetDir);
485
663
  const costume = wantsCostume(targetDir);
486
664
  const suit = overlayed
487
665
  ? "overlay"
488
- : millPlanted
666
+ : millPlanted || soundPlanted || blipPlanted
489
667
  ? "fastened"
490
668
  : costume
491
669
  ? "costume"
@@ -494,12 +672,21 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
494
672
  mill: { name: mill.name || "@0xray/foundry", version: mill.version },
495
673
  consumer: { name: consumer.name, version: consumer.version },
496
674
  suit,
675
+ plant: plantKinds,
497
676
  params: tree?.params || loadFoundryParams(targetDir),
498
677
  tree: { skills, agents },
499
678
  millPlant: {
500
679
  skills: Array.isArray(tree?.millPlantSkills) ? tree.millPlantSkills : [],
501
680
  agents: Array.isArray(tree?.millPlantAgents) ? tree.millPlantAgents : [],
502
681
  },
682
+ soundPlant: {
683
+ skills: Array.isArray(tree?.soundPlantSkills) ? tree.soundPlantSkills : [],
684
+ agents: Array.isArray(tree?.soundPlantAgents) ? tree.soundPlantAgents : [],
685
+ },
686
+ blipPlant: {
687
+ skills: Array.isArray(tree?.blipPlantSkills) ? tree.blipPlantSkills : [],
688
+ agents: Array.isArray(tree?.blipPlantAgents) ? tree.blipPlantAgents : [],
689
+ },
503
690
  shopPlant: {
504
691
  skills: Array.isArray(tree?.shopPlantSkills) ? tree.shopPlantSkills : loadShopPlant(targetDir),
505
692
  },
@@ -539,8 +726,15 @@ function mintConsumerSuit(millPackageRoot, targetDir, log) {
539
726
  const params = loadFoundryParams(targetDir);
540
727
  const millPlant = fastenMillPlant(millPackageRoot, targetDir, log);
541
728
  const tree = overlayConsumerTree(targetDir, log, params);
729
+ tree.plantKinds = millPlant.kinds || loadFactoryPlantKinds(targetDir);
542
730
  tree.millPlantSkills = millPlant.skills;
543
731
  tree.millPlantAgents = millPlant.agents;
732
+ tree.soundPlantSkills = millPlant.sound?.skills || [];
733
+ tree.soundPlantAgents = millPlant.sound?.agents || [];
734
+ tree.sound = millPlant.sound || emptyPlantFiles();
735
+ tree.blipPlantSkills = millPlant.blip?.skills || [];
736
+ tree.blipPlantAgents = millPlant.blip?.agents || [];
737
+ tree.blip = millPlant.blip || emptyPlantFiles();
544
738
  tree.shopPlantSkills = loadShopPlant(targetDir);
545
739
  tree.codex = overlayJsonFacet(
546
740
  resolveInside(targetDir, params.codex),
@@ -569,6 +763,8 @@ function mintConsumerSuit(millPackageRoot, targetDir, log) {
569
763
  module.exports = {
570
764
  DEFAULT_PARAMS,
571
765
  FACTORY_SHOP_SKILLS,
766
+ FACTORY_PLANT_CATALOG,
767
+ isFactoryPlantKind,
572
768
  deepMerge,
573
769
  loadFoundryParams,
574
770
  loadFoundryExtra,
@@ -576,6 +772,11 @@ module.exports = {
576
772
  declaredShopPlant,
577
773
  wantsCostume,
578
774
  millPlantDir,
775
+ loadFactoryPlantKinds,
776
+ factoryPlantAllowlist,
777
+ inventoryPlantKinds,
778
+ catalogPlant,
779
+ unionPlantFiles,
579
780
  listSkillNamesAt,
580
781
  listAgentFilesAt,
581
782
  fastenMillPlant,