0xray 4.0.2 → 4.0.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",
@@ -19,7 +19,7 @@ Before other work, Read `.xray/state/STATION.md`. Compaction and host change are
19
19
  `npm install 0xray` runs `install-bridges.cjs` and:
20
20
 
21
21
  - Copies this file → **`AGENTS.md`** in your project root
22
- - Syncs **45 mill skills** to platform skill directories, then overlays **your** `src/skills` (same name wins). Does **not** write root **`SKILLS.md`**
22
+ - Mill plant (`mill` skill) onto hangers, then overlays **your** `src/skills` (same name wins). Does **not** dump 45/42 costume unless `foundry.json` `"costume": true`. Does **not** write root **`SKILLS.md`**
23
23
  - Seeds **`.gitignore`** (from template, if absent)
24
24
  - Deploys **`.xray/`** (`codex.json`, `features.json`, `config.json`) then overlays **your** mill SSOT (`xray/codex.json`, `xray/features.json`, `src/skills`, `src/opencode/agents`; remap with `foundry.json`)
25
25
  - Writes **`.mcp.json`** with 7 MCPs servers (`npx -y 0xray mcp …`)
package/dist/CHANGELOG.md CHANGED
@@ -26,6 +26,13 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
26
26
  - Mill CI report: `mill-monitor.yml` (fresh workflow ID) listens for mill CI completion. Report only.
27
27
  - Published mill `hooks` uses the 0xray garment installer (`node_modules/0xray/scripts/hooks`), not `node_modules/@0xray/hooks`. Mill tree 0.1.4 (not a 0xray bump).
28
28
  - Nested mill in the 0xray tarball (`scripts/foundry/`). Wear no longer needs a second `npm i @0xray/foundry` for mint-on-install. 4.0.2.
29
+ - Empty garment: mill plant (`mill` skill) is the hanger. Not 45/42 costume unless `foundry.json` `"costume": true`. Mill 0.1.5.
30
+
31
+ ---
32
+
33
+ ## [4.0.3] - 2026-09-04
34
+
35
+ Empty garment. Their plant is the suit. Mill plant (`mill` skill) is the factory discipline on the hanger. Not 45/42 costume unless `foundry.json` `"costume": true`. Factory Repertoire still enable-when-resolves. PPE stays worn. Mill **0.1.5**. Not 5.0.0.
29
36
 
30
37
  ---
31
38
 
@@ -1,2 +1,3 @@
1
+ export declare function wantsCostume(targetDir: string): boolean;
1
2
  /** Re-apply mill overlay after CLI re-wear copies mill skills. */
2
3
  export declare function mintAfterWear(targetDir: string): void;
@@ -4,10 +4,13 @@ import { fileURLToPath } from "url";
4
4
  import { frameworkLogger } from "../../core/framework-logger.js";
5
5
  const require = createRequire(import.meta.url);
6
6
  const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
7
+ const millSuit = require(path.join(packageRoot, "scripts/foundry/mint-suit.cjs"));
8
+ export function wantsCostume(targetDir) {
9
+ return millSuit.wantsCostume(targetDir);
10
+ }
7
11
  /** Re-apply mill overlay after CLI re-wear copies mill skills. */
8
12
  export function mintAfterWear(targetDir) {
9
- const { mintConsumerSuit } = require(path.join(packageRoot, "scripts/foundry/mint-suit.cjs"));
10
- mintConsumerSuit(packageRoot, targetDir, (component, action, status, details) => {
13
+ millSuit.mintConsumerSuit(packageRoot, targetDir, (component, action, status, details) => {
11
14
  const level = status === "error" ? "error" : "info";
12
15
  frameworkLogger.log(component, action, level, details);
13
16
  });
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import { createRequire } from 'module';
4
4
  import { frameworkLogger } from '../../core/framework-logger.js';
5
5
  import { syncBuiltinSkills } from './skill-install.js';
6
- import { mintAfterWear } from './foundry-mint-wear.js';
6
+ import { mintAfterWear, wantsCostume } from './foundry-mint-wear.js';
7
7
  const __dirname = path.dirname(new URL(import.meta.url).pathname);
8
8
  const require = createRequire(import.meta.url);
9
9
  const wiring = require(path.join(__dirname, '..', '..', '..', 'scripts', 'node', 'bridge-mcp-wiring.cjs'));
@@ -105,8 +105,14 @@ async function installForOpencode(options = {}) {
105
105
  console.log('Use --force to reinstall.');
106
106
  }
107
107
  else {
108
- copyDir(opencodeSource, opencodeDest);
109
- frameworkLogger.log('opencode-integration', 'opencode-copied', 'info', { destination: opencodeDest });
108
+ if (wantsCostume(process.cwd())) {
109
+ copyDir(opencodeSource, opencodeDest);
110
+ frameworkLogger.log('opencode-integration', 'opencode-copied', 'info', { destination: opencodeDest });
111
+ }
112
+ else {
113
+ fs.mkdirSync(opencodeDest, { recursive: true });
114
+ frameworkLogger.log('opencode-integration', 'empty-garment-plugin', 'info', { destination: opencodeDest });
115
+ }
110
116
  const pluginSource = path.join(__dirname, '..', '..', '..', 'dist', 'plugin', 'xray-codex-injection.js');
111
117
  const pluginDest = path.join(opencodeDest, 'plugin', 'xray-codex-injection.js');
112
118
  if (fs.existsSync(pluginSource)) {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, cpSync, rmSync, statSync, unlinkSync, copyFileSync } from "fs";
2
2
  import { join, basename, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
+ import { createRequire } from "module";
4
5
  import { execSync } from "child_process";
5
6
  function getLocalRegistryPath() {
6
7
  return join(process.cwd(), ".xray", "skill-registry.json");
@@ -484,6 +485,9 @@ export async function skillRegistryCommand(action, args) {
484
485
  // Each platform's install command calls this to copy framework skills.
485
486
  export function syncBuiltinSkills(targetSkillsDir, packageRoot) {
486
487
  const root = packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
488
+ const millSuit = createRequire(import.meta.url)(join(root, "scripts/foundry/mint-suit.cjs"));
489
+ if (!millSuit.wantsCostume(process.cwd()))
490
+ return 0;
487
491
  const candidateDirs = [
488
492
  join(root, "dist", "skills"),
489
493
  join(root, "src", "skills"),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.5] - 2026-09-04
4
+
5
+ Empty garment: consumer hangers get mill plant (`plant/skills/mill`), not 45/42 costume. `foundry.json` `"costume": true` restores the dump. Plant overlay still mill-fill.
6
+
3
7
  ## [0.1.4] - 2026-09-04
4
8
 
5
9
  `npx @0xray/foundry hooks` on a published mill uses the worn garment
@@ -52,6 +52,8 @@ After the garment is on the hanger, overlay their plant (defaults; remap in `fou
52
52
 
53
53
  **Mill-fill is law:** their keys/files win; mill names they did not plant stay unless they overlay the same name. JSON facets always merge. Skill overlay is project hangers only. CLI re-wear calls mint **once** after mill copies.
54
54
 
55
+ **Empty garment (default):** mill plant (`plant/skills/mill`) is the hanger. Not 45 skills / 42 agents. `foundry.json` `"costume": true` copies that mill garment. Factory Repertoire still enable-when-resolves. PPE stays worn.
56
+
55
57
  ## Publish this mill
56
58
 
57
59
  From **this directory**, not the 0xray repo root:
@@ -72,22 +72,25 @@ function resolveInside(targetDir, rel) {
72
72
  return resolved;
73
73
  }
74
74
 
75
- function loadFoundryParams(targetDir) {
75
+ function loadFoundryExtra(targetDir) {
76
76
  const candidates = [
77
77
  path.join(targetDir, "foundry.json"),
78
78
  path.join(targetDir, ".xray", "foundry.json"),
79
79
  ];
80
- let extra = {};
81
80
  for (const candidate of candidates) {
82
81
  if (!fs.existsSync(candidate)) continue;
83
82
  try {
84
83
  const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
85
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) extra = parsed;
84
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
86
85
  } catch {
87
- extra = {};
86
+ return {};
88
87
  }
89
- break;
90
88
  }
89
+ return {};
90
+ }
91
+
92
+ function loadFoundryParams(targetDir) {
93
+ const extra = loadFoundryExtra(targetDir);
91
94
  const params = { ...DEFAULT_PARAMS };
92
95
  for (const key of ["codex", "features", "config", "skills", "agents", "agentsCard"]) {
93
96
  if (typeof extra[key] === "string" && extra[key].trim()) params[key] = extra[key].trim();
@@ -95,6 +98,80 @@ function loadFoundryParams(targetDir) {
95
98
  return params;
96
99
  }
97
100
 
101
+ /** 45/42 costume dump. Default false on consumers. Exo mill keeps the costume. */
102
+ function wantsCostume(targetDir) {
103
+ if (isExoRepo(targetDir)) return true;
104
+ return loadFoundryExtra(targetDir).costume === true;
105
+ }
106
+
107
+ function millPlantDir(millPackageRoot) {
108
+ const nested = path.join(millPackageRoot, "scripts", "foundry", "plant");
109
+ if (isDirectory(nested)) return nested;
110
+ const packed = path.join(millPackageRoot, "plant");
111
+ if (isDirectory(packed)) return packed;
112
+ return null;
113
+ }
114
+
115
+ function listSkillNamesAt(skillsSrc) {
116
+ if (!isDirectory(skillsSrc)) return [];
117
+ const names = [];
118
+ for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
119
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
120
+ const skillMd = path.join(skillsSrc, entry.name, "SKILL.md");
121
+ if (!fs.existsSync(skillMd)) continue;
122
+ names.push(entry.name);
123
+ }
124
+ return names;
125
+ }
126
+
127
+ function listAgentFilesAt(agentsSrc) {
128
+ if (!isDirectory(agentsSrc)) return [];
129
+ return fs
130
+ .readdirSync(agentsSrc, { withFileTypes: true })
131
+ .filter(
132
+ (entry) =>
133
+ entry.isFile() && (entry.name.endsWith(".yml") || entry.name.endsWith(".yaml")),
134
+ )
135
+ .map((entry) => entry.name);
136
+ }
137
+
138
+ /** Mill mill-fill mill plant: mill mill mill mill-fill mill names they did not plant stay. */
139
+ function millFillMillPlant(millPackageRoot, targetDir, log) {
140
+ const plant = millPlantDir(millPackageRoot);
141
+ if (!plant) return { skills: [], agents: [] };
142
+ const skillsSrc = path.join(plant, "skills");
143
+ const agentsSrc = path.join(plant, "agents");
144
+ const skills = listSkillNamesAt(skillsSrc);
145
+ const agents = listAgentFilesAt(agentsSrc);
146
+ const skillHangers = listSkillHangers(targetDir);
147
+ const agentsDest = path.join(targetDir, ".opencode", "agents");
148
+ for (const hanger of skillHangers) {
149
+ for (const name of skills) {
150
+ const src = path.join(skillsSrc, name, "SKILL.md");
151
+ const destMd = path.join(hanger, name, "SKILL.md");
152
+ if (path.resolve(src) === path.resolve(destMd)) continue;
153
+ if (fs.existsSync(destMd)) continue;
154
+ fs.mkdirSync(path.dirname(destMd), { recursive: true });
155
+ fs.copyFileSync(src, destMd);
156
+ }
157
+ }
158
+ for (const file of agents) {
159
+ const src = path.join(agentsSrc, file);
160
+ const dest = path.join(agentsDest, file);
161
+ if (path.resolve(src) === path.resolve(dest)) continue;
162
+ if (fs.existsSync(dest)) continue;
163
+ fs.mkdirSync(agentsDest, { recursive: true });
164
+ fs.copyFileSync(src, dest);
165
+ }
166
+ if (log && (skills.length > 0 || agents.length > 0)) {
167
+ log("foundry-mint", "Mill plant onto hangers", "info", {
168
+ skills: skills.length,
169
+ agents: agents.length,
170
+ });
171
+ }
172
+ return { skills, agents };
173
+ }
174
+
98
175
  function isDirectory(p) {
99
176
  try {
100
177
  return Boolean(p) && fs.existsSync(p) && fs.statSync(p).isDirectory();
@@ -138,28 +215,13 @@ function overlayJsonFacet(src, dest) {
138
215
  function listConsumerSkillNames(targetDir, skillsRel) {
139
216
  const rel = skillsRel || DEFAULT_PARAMS.skills;
140
217
  const skillsSrc = resolveInside(targetDir, rel);
141
- if (!isDirectory(skillsSrc)) return [];
142
- const names = [];
143
- for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
144
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
145
- const skillMd = path.join(skillsSrc, entry.name, "SKILL.md");
146
- if (!fs.existsSync(skillMd)) continue;
147
- names.push(entry.name);
148
- }
149
- return names;
218
+ return listSkillNamesAt(skillsSrc);
150
219
  }
151
220
 
152
221
  function listConsumerAgentFiles(targetDir, agentsRel) {
153
222
  const rel = agentsRel || DEFAULT_PARAMS.agents;
154
223
  const agentsSrc = resolveInside(targetDir, rel);
155
- if (!isDirectory(agentsSrc)) return [];
156
- return fs
157
- .readdirSync(agentsSrc, { withFileTypes: true })
158
- .filter(
159
- (entry) =>
160
- entry.isFile() && (entry.name.endsWith(".yml") || entry.name.endsWith(".yaml")),
161
- )
162
- .map((entry) => entry.name);
224
+ return listAgentFilesAt(agentsSrc);
163
225
  }
164
226
 
165
227
  const MANAGED_AGENTS_MARKER = "<!-- 0xray-managed -->";
@@ -233,13 +295,28 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
233
295
  features ||
234
296
  config ||
235
297
  agentsCard;
236
- const garment = overlayed ? "overlay" : "copied-onto-hanger";
298
+ const millPlanted =
299
+ (Array.isArray(tree?.millPlantSkills) && tree.millPlantSkills.length > 0) ||
300
+ (Array.isArray(tree?.millPlantAgents) && tree.millPlantAgents.length > 0);
301
+ const costume = wantsCostume(targetDir);
302
+ const garment = overlayed
303
+ ? "overlay"
304
+ : millPlanted
305
+ ? "mill-fill"
306
+ : costume
307
+ ? "copied-onto-hanger"
308
+ : "mill-fill";
237
309
  const inventory = {
238
310
  mill: { name: mill.name || "@0xray/foundry", version: mill.version },
239
311
  consumer: { name: consumer.name, version: consumer.version },
240
312
  garment,
241
313
  params: tree?.params || loadFoundryParams(targetDir),
242
314
  tree: { skills, agents },
315
+ millPlant: {
316
+ skills: Array.isArray(tree?.millPlantSkills) ? tree.millPlantSkills : [],
317
+ agents: Array.isArray(tree?.millPlantAgents) ? tree.millPlantAgents : [],
318
+ },
319
+ costume,
243
320
  facets: {
244
321
  constitution,
245
322
  features,
@@ -271,7 +348,10 @@ function mintConsumerSuit(millPackageRoot, targetDir, log) {
271
348
  return { garment: "dogfood", skipped: true };
272
349
  }
273
350
  const params = loadFoundryParams(targetDir);
351
+ const millPlant = millFillMillPlant(millPackageRoot, targetDir, log);
274
352
  const tree = overlayConsumerTree(targetDir, log, params);
353
+ tree.millPlantSkills = millPlant.skills;
354
+ tree.millPlantAgents = millPlant.agents;
275
355
  tree.codex = overlayJsonFacet(
276
356
  resolveInside(targetDir, params.codex),
277
357
  path.join(targetDir, ".xray", "codex.json"),
@@ -299,6 +379,10 @@ module.exports = {
299
379
  DEFAULT_PARAMS,
300
380
  deepMerge,
301
381
  loadFoundryParams,
382
+ loadFoundryExtra,
383
+ wantsCostume,
384
+ millPlantDir,
385
+ millFillMillPlant,
302
386
  resolveInside,
303
387
  overlayJsonFacet,
304
388
  overlayAgentsCard,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xray/foundry",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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.",
@@ -26,7 +26,8 @@
26
26
  "cli.js",
27
27
  "README.md",
28
28
  "LICENSE",
29
- "CHANGELOG.md"
29
+ "CHANGELOG.md",
30
+ "plant"
30
31
  ],
31
32
  "exports": {
32
33
  "./version-manager": "./version-manager.mjs",
@@ -0,0 +1,5 @@
1
+ name: mill
2
+ skill: mill
3
+ description: "Mill mill-fill mill plant. Their plant is the suit. Factory Repertoire."
4
+ version: "0.1.5"
5
+ mode: subagent
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: mill
3
+ description: Mill mill-fill mill plant. Their plant is the suit. Factory Repertoire. Not 45/42 costume.
4
+ ---
5
+
6
+ # Mill
7
+
8
+ The mill **fastens** their plant onto the hanger. It does not copy the 0xRay 45-skill / 42-agent costume unless they opt in.
9
+
10
+ **Plant (their SSOT):** `xray/codex.json`, `xray/features.json`, `xray/config.json`, `src/skills/<name>/SKILL.md`, `src/opencode/agents/*.yml`. Remap with `foundry.json`.
11
+
12
+ **Mill-fill:** their keys/files win. Mill mill mill-fill mill names they did not plant stay. JSON always merges.
13
+
14
+ **Factory:** Repertoire organ enable-when-resolves. Seven generated MCP. Constitution (PPE) stays worn. Not a fifth MCP. Not 5.0.
15
+
16
+ **Costume:** `foundry.json` `"costume": true` copies the 45/42 mill garment. Default is empty garment + this mill mill-fill mill plant.
17
+
18
+ ```bash
19
+ npx @0xray/foundry mint
20
+ ```
21
+
22
+ Nested mill: `node node_modules/0xray/scripts/foundry/cli.js mint`.
@@ -210,6 +210,7 @@ function main() {
210
210
  "scripts/node/postinstall.cjs",
211
211
  "scripts/foundry/mint-suit.cjs",
212
212
  "scripts/foundry/hooks.mjs",
213
+ "scripts/foundry/plant/skills/mill/SKILL.md",
213
214
  "package.json",
214
215
  "xray/features.json",
215
216
  "xray/features.schema.json",
@@ -223,6 +224,20 @@ function main() {
223
224
  console.log(` ✅ ${rel}`);
224
225
  }
225
226
 
227
+ const millSkillHanger = path.join(tmpRoot, ".opencode", "skills", "mill", "SKILL.md");
228
+ if (!fs.existsSync(millSkillHanger)) {
229
+ throw new Error("empty garment missing mill plant skill on hanger");
230
+ }
231
+ const hangerSkills = path.join(tmpRoot, ".opencode", "skills");
232
+ const hangerNames = fs.existsSync(hangerSkills)
233
+ ? fs.readdirSync(hangerSkills).filter((n) =>
234
+ fs.existsSync(path.join(hangerSkills, n, "SKILL.md")),
235
+ )
236
+ : [];
237
+ if (hangerNames.length > 3) {
238
+ throw new Error(`costume dump on empty garment: ${hangerNames.join(",")}`);
239
+ }
240
+
226
241
  const installedPkg = JSON.parse(fs.readFileSync(path.join(nmRoot, "package.json"), "utf-8"));
227
242
  if (installedPkg.version !== version) {
228
243
  throw new Error(`Installed version ${installedPkg.version} != packed ${version}`);
@@ -8,6 +8,7 @@ const fs = require("fs");
8
8
  const path = require("path");
9
9
  const os = require("os");
10
10
  const { execSync } = require("child_process");
11
+ const { wantsCostume } = require("../foundry/mint-suit.cjs");
11
12
  const {
12
13
  wireHermesBridge,
13
14
  wireOpencodeBridge,
@@ -92,6 +93,11 @@ function syncBuiltinSkills(targetSkillsDir, packageRoot) {
92
93
  return copied;
93
94
  }
94
95
 
96
+ function syncCostumeSkills(targetSkillsDir, packageRoot, targetDir) {
97
+ if (targetDir && !wantsCostume(targetDir)) return 0;
98
+ return syncBuiltinSkills(targetSkillsDir, packageRoot);
99
+ }
100
+
95
101
  function copyTree(src, dest, relPath = "") {
96
102
  if (!fs.existsSync(src)) return;
97
103
  if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
@@ -267,38 +273,46 @@ function writeGrokDiscoveredHooks(targetDir, hooksPath, log, label) {
267
273
  }
268
274
  }
269
275
 
276
+ function copyOpencodePlugin(packageRoot, opencodeDest, log) {
277
+ const pluginSource = path.join(packageRoot, "dist", "plugin", "xray-codex-injection.js");
278
+ const pluginDest = path.join(opencodeDest, "plugin", "xray-codex-injection.js");
279
+ if (!fs.existsSync(pluginSource)) return;
280
+ const pluginDestDir = path.dirname(pluginDest);
281
+ if (!fs.existsSync(pluginDestDir)) fs.mkdirSync(pluginDestDir, { recursive: true });
282
+ const shouldCopy =
283
+ !fs.existsSync(pluginDest) ||
284
+ fs.statSync(pluginSource).mtime > fs.statSync(pluginDest).mtime;
285
+ if (shouldCopy) {
286
+ fs.copyFileSync(pluginSource, pluginDest);
287
+ log("opencode-bridge", "plugin updated", "info");
288
+ }
289
+ }
290
+
270
291
  function installOpencodeBridge(targetDir, packageRoot, log) {
271
292
  const opencodeSource = path.join(packageRoot, ".opencode");
272
293
  const opencodeDest = path.join(targetDir, ".opencode");
273
- if (!fs.existsSync(opencodeSource)) {
274
- log("opencode-bridge", "skipped", "warn", { reason: ".opencode source missing" });
275
- return;
276
- }
294
+ const costume = wantsCostume(targetDir);
277
295
 
278
- if (!fs.existsSync(opencodeDest)) {
279
- copyTree(opencodeSource, opencodeDest);
280
- log("opencode-bridge", "copied .opencode tree", "info");
281
- } else {
282
- log("opencode-bridge", ".opencode exists — merging skills/plugin only", "info");
283
- }
284
-
285
- const pluginSource = path.join(packageRoot, "dist", "plugin", "xray-codex-injection.js");
286
- const pluginDest = path.join(opencodeDest, "plugin", "xray-codex-injection.js");
287
- if (fs.existsSync(pluginSource)) {
288
- const pluginDestDir = path.dirname(pluginDest);
289
- if (!fs.existsSync(pluginDestDir)) fs.mkdirSync(pluginDestDir, { recursive: true });
290
- const shouldCopy =
291
- !fs.existsSync(pluginDest) ||
292
- fs.statSync(pluginSource).mtime > fs.statSync(pluginDest).mtime;
293
- if (shouldCopy) {
294
- fs.copyFileSync(pluginSource, pluginDest);
295
- log("opencode-bridge", "plugin updated", "info");
296
+ if (costume) {
297
+ if (!fs.existsSync(opencodeSource)) {
298
+ log("opencode-bridge", "skipped", "warn", { reason: ".opencode source missing" });
299
+ return;
296
300
  }
301
+ if (!fs.existsSync(opencodeDest)) {
302
+ copyTree(opencodeSource, opencodeDest);
303
+ log("opencode-bridge", "copied .opencode tree", "info");
304
+ } else {
305
+ log("opencode-bridge", ".opencode exists — merging skills/plugin only", "info");
306
+ }
307
+ copyOpencodePlugin(packageRoot, opencodeDest, log);
308
+ const copied = syncCostumeSkills(path.join(opencodeDest, "skills"), packageRoot, targetDir);
309
+ if (copied > 0) log("opencode-bridge", `skills synced (${copied})`, "info", { path: ".opencode/skills/" });
310
+ } else {
311
+ log("opencode-bridge", "empty garment — plugin only, mill mill-fill mill plant", "info");
312
+ fs.mkdirSync(opencodeDest, { recursive: true });
313
+ copyOpencodePlugin(packageRoot, opencodeDest, log);
297
314
  }
298
315
 
299
- const copied = syncBuiltinSkills(path.join(opencodeDest, "skills"), packageRoot);
300
- if (copied > 0) log("opencode-bridge", `skills synced (${copied})`, "info", { path: ".opencode/skills/" });
301
-
302
316
  try {
303
317
  const count = wireOpencodeBridge(targetDir);
304
318
  log("opencode-bridge", `opencode.json mcp wired (${count} servers)`, "info");
@@ -376,14 +390,14 @@ function installGrokBridge(targetDir, packageRoot, log) {
376
390
  log("grok-bridge", "plugin copied", "info", { path: rel || dest });
377
391
  writePluginMcpJson(dest, targetDir, log, "grok-bridge");
378
392
  patchGrokHooks(dest, packageRoot, targetDir, log, "grok-bridge");
379
- const copied = syncBuiltinSkills(path.join(dest, "skills"), packageRoot);
393
+ const copied = syncCostumeSkills(path.join(dest, "skills"), packageRoot, targetDir);
380
394
  if (copied > 0) log("grok-bridge", `plugin skills synced (${copied})`, "info", { path: rel });
381
395
  }
382
396
  }
383
397
 
384
398
  if (!ephemeral) {
385
399
  const grokGlobalSkills = path.join(home, ".grok", "skills");
386
- const globalCopied = syncBuiltinSkills(grokGlobalSkills, packageRoot);
400
+ const globalCopied = syncCostumeSkills(grokGlobalSkills, packageRoot, targetDir);
387
401
  if (globalCopied > 0) {
388
402
  log("grok-bridge", `global skills synced (${globalCopied})`, "info", { path: "~/.grok/skills/" });
389
403
  }
@@ -421,7 +435,7 @@ function installHermesBridge(targetDir, packageRoot, log) {
421
435
 
422
436
  writePluginMcpJson(targetPluginDir, targetDir, log, "hermes-bridge");
423
437
 
424
- const copied = syncBuiltinSkills(path.join(targetPluginDir, "skills"), packageRoot);
438
+ const copied = syncCostumeSkills(path.join(targetPluginDir, "skills"), packageRoot, targetDir);
425
439
  if (copied > 0) log("hermes-bridge", `skills synced (${copied})`, "info");
426
440
 
427
441
  const rootMarker = path.join(targetPluginDir, "xray-consumer-root.txt");
@@ -473,7 +487,7 @@ function installOpenclawBridge(targetDir, packageRoot, log) {
473
487
  log("openclaw-bridge", "config created", "info", { path: ".xray/config/openclaw.json" });
474
488
  }
475
489
 
476
- const copied = syncBuiltinSkills(path.join(os.homedir(), ".openclaw", "skills"), packageRoot);
490
+ const copied = syncCostumeSkills(path.join(os.homedir(), ".openclaw", "skills"), packageRoot, targetDir);
477
491
  if (copied > 0) log("openclaw-bridge", `skills synced (${copied})`, "info", { path: "~/.openclaw/skills/" });
478
492
 
479
493
  try {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",