0xray 4.0.2 → 4.0.4

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.4",
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
+ - Fastens mill plant (`mill` + `inspect`) then overlays **your** `src/skills` (same name wins). Inspect AI work is the mill discipline. 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,22 @@ 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
+ - Mill plant fastens **inspect** (inspect AI work) plus `mill`. Not empty. Mill 0.1.6.
31
+
32
+ ---
33
+
34
+ ## [4.0.4] - 2026-09-04
35
+
36
+ Mill **fastens** an inspect suit: mill plant is `mill` + `inspect` (inspect AI work — diffs, traces, mill receipts, CI). Not empty. Not 45/42 costume unless `foundry.json` `"costume": true`. Factory Repertoire + PPE stay. Mill **0.1.6**. Not 5.0.0.
37
+
38
+ Naming: inventory `suit` (`fastened` / `overlay` / `costume`). Not garment. Not hanger. Costume is the 45/42 dump flag only.
39
+
40
+ ---
41
+
42
+ ## [4.0.3] - 2026-09-04
43
+
44
+ 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
45
 
30
46
  ---
31
47
 
@@ -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', 'mill-plant-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.4",
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.4",
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,8 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.6] - 2026-09-04
4
+
5
+ Mill plant fastens an **inspect** suit: inspect AI work (diffs, traces, mill receipts, CI). Not empty. `mill` + `inspect` skills. Costume dump still opt-in.
6
+
7
+ Inventory field is `suit` (`fastened` / `overlay` / `costume` / `dogfood`). Not `garment`. `foundry.json` `"costume": true` is the 45/42 dump.
8
+
9
+ ## [0.1.5] - 2026-09-04
10
+
11
+ Mill plant (`plant/skills/mill`) fastened by default, not 45/42 costume. `foundry.json` `"costume": true` restores the dump. Plant overlay still mill-fill.
12
+
3
13
  ## [0.1.4] - 2026-09-04
4
14
 
5
- `npx @0xray/foundry hooks` on a published mill uses the worn garment
15
+ `npx @0xray/foundry hooks` on a published mill uses the worn 0xray installer
6
16
  `node_modules/0xray/scripts/hooks/install-hooks.cjs`. Dogfood still uses
7
17
  `scripts/hooks` next to `scripts/foundry`. Missing installer fails honestly
8
18
  (no spawn of `node_modules/@0xray/hooks/...`).
@@ -2,7 +2,7 @@
2
2
 
3
3
  The **mill**, not the worn exo. Reconcile, stamp JSON/CHANGELOG, verify docs, gate, publish, overlay their suit params.
4
4
 
5
- PPE (`evaluatePreToolGate`) stays worn. The mill plants **their** constitution, features, skills, and agents onto the hanger. It is not a fifth MCP. The suit is [`0xray`](https://www.npmjs.com/package/0xray).
5
+ PPE (`evaluatePreToolGate`) stays worn. The mill **fastens** their constitution, features, skills, and agents as the suit. It is not a fifth MCP. The suit is [`0xray`](https://www.npmjs.com/package/0xray).
6
6
 
7
7
  ## Install
8
8
 
@@ -35,11 +35,11 @@ npx @0xray/foundry hooks
35
35
 
36
36
  `docs-check` and `gate` run **full** 0xRay corpus only when the milled `package.json` name is `0xray` and `docs-site/` exists. Otherwise **light**: `package.json` + CHANGELOG.
37
37
 
38
- `hooks` installs git hooks via `scripts/hooks/install-hooks.cjs` (dogfood) or `node_modules/0xray/scripts/hooks/install-hooks.cjs` (published mill + worn garment). It does not look for `@0xray/hooks`.
38
+ `hooks` installs git hooks via `scripts/hooks/install-hooks.cjs` (dogfood) or `node_modules/0xray/scripts/hooks/install-hooks.cjs` (published mill + worn suit). It does not look for `@0xray/hooks`.
39
39
 
40
40
  ## Suit params (`mint`)
41
41
 
42
- After the garment is on the hanger, overlay their plant (defaults; remap in `foundry.json` or `.xray/foundry.json`):
42
+ After mill plant is fastened, overlay their plant (defaults; remap in `foundry.json` or `.xray/foundry.json`):
43
43
 
44
44
  | Facet | Default plant | Hanger |
45
45
  |---|---|---|
@@ -50,7 +50,9 @@ After the garment is on the hanger, overlay their plant (defaults; remap in `fou
50
50
  | Agents | `src/opencode/agents/*.yml` | `.opencode/agents/` (OpenCode hangar; other floors do not read mill YML agents) |
51
51
  | Agents card | `xray/AGENTS.md` | `AGENTS.md` if mill-managed |
52
52
 
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.
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 dirs only (`.opencode/skills`, project Grok plugin skills). CLI re-wear calls mint **once** after mill copies.
54
+
55
+ **Default mill plant:** `mill` + `inspect` (inspect AI work). Fastens a suit, not an empty one. Not 45/42 costume. `foundry.json` `"costume": true` copies that dump. Factory Repertoire still enable-when-resolves. PPE stays worn.
54
56
 
55
57
  ## Publish this mill
56
58
 
@@ -25,7 +25,7 @@ const HELP =
25
25
  "\n" +
26
26
  "Mill, not exo. FOUNDRY_ROOT overrides cwd (the repo being milled).\n" +
27
27
  "release bumps/commits/pushes/publishes the milled cwd — pass --dry-run or --i-mean-it (or FOUNDRY_RELEASE=1).\n" +
28
- "Mint overlays their plant onto project hangers (mill-fill). PPE stays worn.\n" +
28
+ "Mint fastens mill plant then overlays their plant (mill-fill JSON). PPE stays worn.\n" +
29
29
  "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";
30
30
 
31
31
  const cmd = process.argv[2];
@@ -24,7 +24,7 @@ export function resolveMillRoot() {
24
24
  /**
25
25
  * Git hook installer. Dogfood is `scripts/hooks` next to `scripts/foundry`.
26
26
  * Published mill lives in `node_modules/@0xray/foundry` — sibling `@0xray/hooks`
27
- * does not exist. Use the worn 0xray garment installer instead.
27
+ * does not exist. Use the worn 0xray installer instead.
28
28
  */
29
29
  export function resolveHookInstaller(rootDir = resolveMillRoot(), millPkg = millPackageDir()) {
30
30
  const candidates = [
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Mill overlay: their plant onto the hanger. Not PPE (evaluatePreToolGate).
2
+ * Mill overlay: fasten their plant as the suit. Not PPE (evaluatePreToolGate).
3
3
  *
4
4
  * Default SSOT (override with foundry.json or .xray/foundry.json):
5
5
  * xray/codex.json, xray/features.json, xray/config.json,
@@ -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 fastenMillPlant(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 skillDirs = listProjectSkillDirs(targetDir);
147
+ const agentsDest = path.join(targetDir, ".opencode", "agents");
148
+ for (const dir of skillDirs) {
149
+ for (const name of skills) {
150
+ const src = path.join(skillsSrc, name, "SKILL.md");
151
+ const destMd = path.join(dir, 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", "Fastened mill plant", "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();
@@ -103,8 +180,8 @@ function isDirectory(p) {
103
180
  }
104
181
  }
105
182
 
106
- /** Project skill hangers only. */
107
- function listSkillHangers(targetDir) {
183
+ /** Project skill dirs only (.opencode/skills, project Grok plugin skills). */
184
+ function listProjectSkillDirs(targetDir) {
108
185
  const dests = [path.join(targetDir, ".opencode", "skills")];
109
186
  const projectGrok = path.join(targetDir, ".grok", "plugins", "0xray", "skills");
110
187
  if (isDirectory(projectGrok)) dests.push(projectGrok);
@@ -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 -->";
@@ -184,13 +246,13 @@ function overlayConsumerTree(targetDir, log, params) {
184
246
  const agents = listConsumerAgentFiles(targetDir, agentsRel);
185
247
  const skillsSrc = resolveInside(targetDir, skillsRel);
186
248
  const agentsSrc = resolveInside(targetDir, agentsRel);
187
- const skillHangers = listSkillHangers(targetDir);
249
+ const skillDirs = listProjectSkillDirs(targetDir);
188
250
  const agentsDest = path.join(targetDir, ".opencode", "agents");
189
251
 
190
- for (const hanger of skillHangers) {
252
+ for (const dir of skillDirs) {
191
253
  for (const name of skills) {
192
254
  const src = path.join(skillsSrc, name, "SKILL.md");
193
- const destMd = path.join(hanger, name, "SKILL.md");
255
+ const destMd = path.join(dir, name, "SKILL.md");
194
256
  if (path.resolve(src) === path.resolve(destMd)) continue;
195
257
  fs.mkdirSync(path.dirname(destMd), { recursive: true });
196
258
  fs.copyFileSync(src, destMd);
@@ -205,10 +267,10 @@ function overlayConsumerTree(targetDir, log, params) {
205
267
  }
206
268
 
207
269
  if (log && (skills.length > 0 || agents.length > 0)) {
208
- log("foundry-mint", "Overlaid consumer skills/agents onto hangers", "info", {
270
+ log("foundry-mint", "Overlaid consumer skills/agents", "info", {
209
271
  skills: skills.length,
210
272
  agents: agents.length,
211
- skillHangers: skillHangers.length,
273
+ skillDirs: skillDirs.length,
212
274
  });
213
275
  }
214
276
  return { skills, agents, params: resolved };
@@ -216,7 +278,7 @@ function overlayConsumerTree(targetDir, log, params) {
216
278
 
217
279
  function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
218
280
  if (isDogfood(packageRoot, targetDir)) {
219
- return { garment: "dogfood", skipped: true, consumer: readPackageIdentity(path.join(targetDir, "package.json")) };
281
+ return { suit: "dogfood", skipped: true, consumer: readPackageIdentity(path.join(targetDir, "package.json")) };
220
282
  }
221
283
  const mill = readPackageIdentity(path.join(packageRoot, "package.json"));
222
284
  const consumer = readPackageIdentity(path.join(targetDir, "package.json"));
@@ -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 suit = overlayed
303
+ ? "overlay"
304
+ : millPlanted
305
+ ? "fastened"
306
+ : costume
307
+ ? "costume"
308
+ : "fastened";
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
- garment,
312
+ suit,
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,
@@ -260,7 +337,7 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
260
337
  log("foundry-mint", "Minted foundry-inventory from consumer mill SSOT", "info", {
261
338
  consumer: consumer.name,
262
339
  version: consumer.version,
263
- garment,
340
+ suit,
264
341
  });
265
342
  }
266
343
  return inventory;
@@ -268,10 +345,13 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
268
345
 
269
346
  function mintConsumerSuit(millPackageRoot, targetDir, log) {
270
347
  if (isDogfood(millPackageRoot, targetDir)) {
271
- return { garment: "dogfood", skipped: true };
348
+ return { suit: "dogfood", skipped: true };
272
349
  }
273
350
  const params = loadFoundryParams(targetDir);
351
+ const millPlant = fastenMillPlant(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,13 +379,17 @@ module.exports = {
299
379
  DEFAULT_PARAMS,
300
380
  deepMerge,
301
381
  loadFoundryParams,
382
+ loadFoundryExtra,
383
+ wantsCostume,
384
+ millPlantDir,
385
+ fastenMillPlant,
302
386
  resolveInside,
303
387
  overlayJsonFacet,
304
388
  overlayAgentsCard,
305
389
  overlayConsumerTree,
306
390
  listConsumerSkillNames,
307
391
  listConsumerAgentFiles,
308
- listSkillHangers,
392
+ listProjectSkillDirs,
309
393
  mintConsumerFromSsot,
310
394
  mintConsumerSuit,
311
395
  isDogfood,
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Overlay their mill SSOT onto the hanger (cwd / FOUNDRY_ROOT).
3
+ * Overlay their mill SSOT onto the project (cwd / FOUNDRY_ROOT).
4
4
  * Not a fifth 0xray CLI. Not PPE.
5
5
  */
6
6
 
@@ -19,7 +19,7 @@ function main() {
19
19
  process.stdout.write(`${component} ${action} ${status}\n`);
20
20
  });
21
21
  if (inventory.skipped) {
22
- process.stderr.write("mint: skipped dogfood / 0xray exo plant (not a consumer hanger)\n");
22
+ process.stderr.write("mint: skipped dogfood / 0xray exo plant (not a consumer project)\n");
23
23
  process.exit(0);
24
24
  }
25
25
  process.stdout.write(`${JSON.stringify(inventory, null, 2)}\n`);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xray/foundry",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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: inspect
2
+ skill: inspect
3
+ description: "Inspect AI work: diffs, traces, mill receipts, CI. Fastened mill plant."
4
+ version: "0.1.6"
5
+ mode: subagent
@@ -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.6"
5
+ mode: subagent
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: inspect
3
+ description: Inspect AI work — diffs, tool traces, mill receipts, CI. Factory mill plant. Not 45/42 costume.
4
+ ---
5
+
6
+ # Inspect
7
+
8
+ This mill **fastens** a suit for **inspecting AI work**. Not an empty suit. Not a copy of 0xRay's 45/42 costume.
9
+
10
+ Use this when an agent (or a mill) just wrote, minted, or shipped something. Read the work. Do not trust the receipt until you have opened the files.
11
+
12
+ ## What to inspect
13
+
14
+ 1. **Diff.** What actually changed. Ignore the agent's summary if the diff disagrees.
15
+ 2. **Plant vs worn.** `src/skills`, `src/opencode/agents`, `xray/*.json` vs `.opencode/skills`, `.opencode/agents`, `.xray/`. Same name: plant should win. Costume dump (45 skills / 42 agents they did not plant) is a fail unless `foundry.json` has `"costume": true`.
16
+ 3. **Mill receipt.** `.xray/foundry-inventory.json` — mill name/version, `suit` (`fastened` / `overlay` / `costume`), `millPlant.skills` should include `mill` and `inspect`.
17
+ 4. **CI.** GitHub mill CI and mill-monitor report, not just a green check in chat.
18
+ 5. **Live PUT.** `npm view <pkg> version` and HTTP 200 on the `.tgz`. Metadata without a tarball is not shipped.
19
+ 6. **Machine hangar.** Isolated HOME last-mile must not clobber `~/.grok/plugins/0xray`.
20
+
21
+ ## How to work
22
+
23
+ - Open the files. Quote paths. Do not invent a pass.
24
+ - Constitution stays on (Codex 11 / 69 / destructive). Inspecting is not a license to add an 8th MCP.
25
+ - Factory Repertoire is the organ, not the inspection.
26
+ - Fasten their plant on top; do not dump our costume.
27
+
28
+ ```bash
29
+ npx @0xray/foundry mint
30
+ npx @0xray/foundry ci --report
31
+ ```
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: mill
3
+ description: Fasten their plant as the suit. Factory Repertoire. Inspect is the mill discipline. Not 45/42 costume.
4
+ ---
5
+
6
+ # Mill
7
+
8
+ The mill **fastens** a suit. Default mill plant is **inspect** (inspecting AI work) plus this mill card. Not empty. Not 0xRay's 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 plant names they did not overlay stay. JSON always merges.
13
+
14
+ **Factory:** Repertoire 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 costume.
17
+
18
+ ```bash
19
+ npx @0xray/foundry mint
20
+ ```
@@ -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("mill plant missing mill skill");
230
+ }
231
+ const wornSkills = path.join(tmpRoot, ".opencode", "skills");
232
+ const wornNames = fs.existsSync(wornSkills)
233
+ ? fs.readdirSync(wornSkills).filter((n) =>
234
+ fs.existsSync(path.join(wornSkills, n, "SKILL.md")),
235
+ )
236
+ : [];
237
+ if (wornNames.length > 3) {
238
+ throw new Error(`costume dump: ${wornNames.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", "mill plant — plugin only, not costume dump", "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.4",
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.4",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",