0xray 4.0.3 → 4.0.5

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.3",
3
+ "version": "4.0.1",
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
- - 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`**
22
+ - Fastens mill plant (`mill` + `inspect`) then overlays **your** `src/skills` (same name wins). `npx @0xray/foundry inspect` runs the mill checks (chat is not a receipt). 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 …`)
@@ -58,6 +58,7 @@ Governance deliberation uses **code-review**, **security-audit** (via enforcer/s
58
58
  | `npx 0xray codex check` | Check codex rules |
59
59
  | `npx 0xray health` | Framework health check |
60
60
  | `npx 0xray mcp <server>` | Start an MCP server (stdio) |
61
+ | `npx @0xray/foundry inspect` | Mill inspect (nested mill). Chat is not a receipt. |
61
62
 
62
63
  ## Governance
63
64
 
package/dist/CHANGELOG.md CHANGED
@@ -27,6 +27,22 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
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
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
+ - Nested mill **0.1.7**: inspect organ, four floors, passwd HOME, mill CI receipt. 4.0.5.
32
+
33
+ ---
34
+
35
+ ## [4.0.5] - 2026-09-04
36
+
37
+ Nested mill **0.1.7**. Inspect is a mill command (`npx @0xray/foundry inspect`): costume dump fails mint, live PUT GETs the `.tgz`, four project floors, mill CI receipt, passwd HOME isolation. Mill plant stays `mill` + `inspect`. Not 45/42 costume. Not 5.0.0.
38
+
39
+ ---
40
+
41
+ ## [4.0.4] - 2026-09-04
42
+
43
+ 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.
44
+
45
+ Naming: inventory `suit` (`fastened` / `overlay` / `costume`). Not garment. Not hanger. Costume is the 45/42 dump flag only.
30
46
 
31
47
  ---
32
48
 
@@ -111,7 +111,7 @@ async function installForOpencode(options = {}) {
111
111
  }
112
112
  else {
113
113
  fs.mkdirSync(opencodeDest, { recursive: true });
114
- frameworkLogger.log('opencode-integration', 'empty-garment-plugin', 'info', { destination: opencodeDest });
114
+ frameworkLogger.log('opencode-integration', 'mill-plant-plugin', 'info', { destination: opencodeDest });
115
115
  }
116
116
  const pluginSource = path.join(__dirname, '..', '..', '..', 'dist', 'plugin', 'xray-codex-injection.js');
117
117
  const pluginDest = path.join(opencodeDest, 'plugin', 'xray-codex-injection.js');
@@ -50,8 +50,18 @@ function registerGrokMcpServers(targetDir) {
50
50
  }
51
51
  export async function installForGrokCLI(options = {}) {
52
52
  frameworkLogger.log('grok-integration', 'install-start', 'info', { options });
53
+ const millSuit = requireCjs(path.join(packageRoot, 'scripts/foundry/mint-suit.cjs'));
53
54
  const home = process.env.HOME || process.env.USERPROFILE || '';
55
+ const machine = millSuit.machineHome();
54
56
  const targetPluginDir = path.join(home, '.grok/plugins/0xray');
57
+ if (millSuit.wouldClobberMachineGrok(targetPluginDir, process.env, machine)) {
58
+ frameworkLogger.log('grok-integration', 'refuse-machine-grok-clobber', 'error', {
59
+ home,
60
+ machineHome: machine,
61
+ targetPluginDir,
62
+ });
63
+ throw new Error('foundry-inspect: isolated HOME must not clobber ~/.grok/plugins/0xray');
64
+ }
55
65
  // Try to find the plugin source from the installed package
56
66
  const possibleSources = [
57
67
  path.join(__dirname, '..', '..', '..', 'src/integrations/grok/plugin/0xray'), // dev
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.3",
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.3",
3
+ "version": "4.0.5",
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,12 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.7] - 2026-09-04
4
+
5
+ Inspect is mill flesh: `npx @0xray/foundry inspect` runs diff, plant vs worn (mint fails costume dump), mill receipt, CI, GET the `.tgz` (HTTP 200), isolated HOME vs passwd home. Four project skill floors created on fasten (OpenCode, Grok, Hermes, OpenClaw). CI report is `.xray/foundry-ci-report.json`. Not an 8th MCP. Not 5.0.
6
+
7
+ ## [0.1.6] - 2026-09-04
8
+
9
+ Mill plant fastens an **inspect** suit: inspect AI work (diffs, traces, mill receipts, CI). Not empty. `mill` + `inspect` skills. Costume dump still opt-in.
10
+
11
+ Inventory field is `suit` (`fastened` / `overlay` / `costume` / `dogfood`). Not `garment`. `foundry.json` `"costume": true` is the 45/42 dump.
12
+
3
13
  ## [0.1.5] - 2026-09-04
4
14
 
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.
15
+ Mill plant (`plant/skills/mill`) fastened by default, not 45/42 costume. `foundry.json` `"costume": true` restores the dump. Plant overlay still mill-fill.
6
16
 
7
17
  ## [0.1.4] - 2026-09-04
8
18
 
9
- `npx @0xray/foundry hooks` on a published mill uses the worn garment
19
+ `npx @0xray/foundry hooks` on a published mill uses the worn 0xray installer
10
20
  `node_modules/0xray/scripts/hooks/install-hooks.cjs`. Dogfood still uses
11
21
  `scripts/hooks` next to `scripts/foundry`. Missing installer fails honestly
12
22
  (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
 
@@ -23,6 +23,7 @@ npx @0xray/foundry release [patch|minor|major] --i-mean-it
23
23
  npx @0xray/foundry release --publish-only --dry-run
24
24
  npx @0xray/foundry release --publish-only --i-mean-it
25
25
  npx @0xray/foundry mint
26
+ npx @0xray/foundry inspect [--skip-live]
26
27
  npx @0xray/foundry ci [--commit SHA] [--report]
27
28
  npx @0xray/foundry hooks
28
29
  ```
@@ -31,28 +32,28 @@ npx @0xray/foundry hooks
31
32
 
32
33
  `FOUNDRY_ROOT` overrides cwd (the repo being milled).
33
34
 
34
- `gate` is build + test + docs-check. `docs-build` runs Docusaurus on the 0xray exo (`docs-site/`); stranger mills skip. `ci` reports GitHub Actions (no auto-push). `hooks` installs git pre/post hooks. GitHub `0xRay CI/CD` is the mill gate on `main`. `Deploy Docs` is the mill Pages put.
35
+ `gate` is build + test + docs-check. `docs-build` runs Docusaurus on the 0xray exo (`docs-site/`); stranger mills skip. `inspect` runs the six mill checks (diff, plant vs worn, receipt, CI, live tarball GET, isolated HOME). Mint fails on a costume dump. Isolated HOME skips machine `~/.grok`. `ci` reports GitHub Actions (no auto-push). `hooks` installs git pre/post hooks. GitHub `0xRay CI/CD` is the mill gate on `main`. `Deploy Docs` is the mill Pages put.
35
36
 
36
37
  `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
38
 
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`.
39
+ `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
40
 
40
41
  ## Suit params (`mint`)
41
42
 
42
- After the garment is on the hanger, overlay their plant (defaults; remap in `foundry.json` or `.xray/foundry.json`):
43
+ After mill plant is fastened, overlay their plant (defaults; remap in `foundry.json` or `.xray/foundry.json`):
43
44
 
44
- | Facet | Default plant | Hanger |
45
+ | Facet | Default plant | Worn |
45
46
  |---|---|---|
46
47
  | Constitution | `xray/codex.json` | `.xray/codex.json` |
47
48
  | Features / temperament | `xray/features.json` | `.xray/features.json` |
48
49
  | Config | `xray/config.json` | `.xray/config.json` |
49
- | Skills | `src/skills/<name>/SKILL.md` | `.opencode/skills/` and project `.grok/plugins/0xray/skills` if that dir exists |
50
- | Agents | `src/opencode/agents/*.yml` | `.opencode/agents/` (OpenCode hangar; other floors do not read mill YML agents) |
50
+ | Skills | `src/skills/<name>/SKILL.md` | project `.opencode/skills`, `.grok/plugins/0xray/skills`, `.hermes/plugins/xray-hermes/skills`, `.openclaw/skills` (created on fasten; never machine home) |
51
+ | Agents | `src/opencode/agents/*.yml` | `.opencode/agents/` (OpenCode wear dir; other floors do not read mill YML agents) |
51
52
  | Agents card | `xray/AGENTS.md` | `AGENTS.md` if mill-managed |
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
+ **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 on every TUI floor (OpenCode, Grok, Hermes, OpenClaw). YML agents stay OpenCode. CLI re-wear calls mint **once** after mill copies. Isolated HOME is passwd home, not `os.homedir()`. CI report is `.xray/foundry-ci-report.json` (`.opencode/logs` shim).
54
55
 
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
+ **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.
56
57
 
57
58
  ## Publish this mill
58
59
 
@@ -16,11 +16,15 @@ const commitIdx = args.indexOf("--commit");
16
16
  const shaArg = commitIdx >= 0 ? args[commitIdx + 1] : "";
17
17
  const sha = (shaArg && !shaArg.startsWith("-") ? shaArg : process.env.GITHUB_SHA || "").trim();
18
18
  const root = resolveMillRoot();
19
- const reportPath = path.join(root, ".opencode", "logs", "ci-cd-monitor-report.json");
19
+ const millReportPath = path.join(root, ".xray", "foundry-ci-report.json");
20
+ const shimReportPath = path.join(root, ".opencode", "logs", "ci-cd-monitor-report.json");
20
21
 
21
22
  function writeReport(report) {
22
- fs.mkdirSync(path.dirname(reportPath), { recursive: true });
23
- fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
23
+ const body = `${JSON.stringify(report, null, 2)}\n`;
24
+ for (const reportPath of [millReportPath, shimReportPath]) {
25
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
26
+ fs.writeFileSync(reportPath, body);
27
+ }
24
28
  }
25
29
 
26
30
  function emptyReport(status, reason) {
@@ -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|ci|hooks> [...args]
5
+ * npx @0xray/foundry <reconcile|stamp|gate|release|docs-check|docs-build|mint|inspect|ci|hooks> [...args]
6
6
  */
7
7
 
8
8
  import { spawnSync } from "node:child_process";
@@ -16,16 +16,18 @@ const COMMANDS = {
16
16
  "docs-check": { script: "validate-release-docs.mjs", preset: [] },
17
17
  "docs-build": { script: "docs-build.mjs", preset: [] },
18
18
  mint: { script: "mint.mjs", preset: [] },
19
+ inspect: { script: "inspect.mjs", preset: [] },
19
20
  ci: { script: "ci-monitor.mjs", preset: [] },
20
21
  hooks: { script: "hooks.mjs", preset: [] },
21
22
  };
22
23
 
23
24
  const HELP =
24
- "Usage: npx @0xray/foundry <reconcile|stamp|gate|release|docs-check|docs-build|mint|ci|hooks> [...args]\n" +
25
+ "Usage: npx @0xray/foundry <reconcile|stamp|gate|release|docs-check|docs-build|mint|inspect|ci|hooks> [...args]\n" +
25
26
  "\n" +
26
27
  "Mill, not exo. FOUNDRY_ROOT overrides cwd (the repo being milled).\n" +
27
28
  "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" +
29
+ "Mint fastens mill plant then overlays their plant (mill-fill JSON). 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" +
29
31
  "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
32
 
31
33
  const cmd = process.argv[2];
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mill inspect organ. Runs the six checks. Not an 8th MCP. Not PPE.
4
+ *
5
+ * npx @0xray/foundry inspect [--skip-live]
6
+ */
7
+
8
+ import { spawnSync } from "node:child_process";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { createRequire } from "node:module";
12
+ import { fileURLToPath } from "node:url";
13
+ import { millPackageDir, resolveMillRoot } from "./mill-root.mjs";
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const mint = require("./mint-suit.cjs");
17
+
18
+ export function npmTarballUrl(name, version) {
19
+ if (typeof name !== "string" || !name || typeof version !== "string" || !version) return null;
20
+ if (name.startsWith("@")) {
21
+ const slash = name.indexOf("/");
22
+ if (slash < 1) return null;
23
+ const pkg = name.slice(slash + 1);
24
+ return `https://registry.npmjs.org/${name}/-/${pkg}-${version}.tgz`;
25
+ }
26
+ return `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`;
27
+ }
28
+
29
+ export function isIsolatedHome(env = process.env, machine = mint.machineHome()) {
30
+ return mint.isIsolatedHome(env, machine);
31
+ }
32
+
33
+ export function machineGrokPluginDir(machine = mint.machineHome()) {
34
+ return mint.machineGrokPluginDir(machine);
35
+ }
36
+
37
+ export function wouldClobberMachineGrok(
38
+ dest,
39
+ env = process.env,
40
+ machine = mint.machineHome(),
41
+ ) {
42
+ return mint.wouldClobberMachineGrok(dest, env, machine);
43
+ }
44
+
45
+ function readJson(file) {
46
+ try {
47
+ return JSON.parse(fs.readFileSync(file, "utf8"));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function checkDiff(root) {
54
+ const status = spawnSync("git", ["status", "-sb"], { cwd: root, encoding: "utf8" });
55
+ if (status.error || status.status === 128) {
56
+ return {
57
+ id: "diff",
58
+ ok: true,
59
+ skipped: true,
60
+ detail: status.stderr?.trim() || "not a git repo",
61
+ };
62
+ }
63
+ const diff = spawnSync("git", ["diff", "--stat"], { cwd: root, encoding: "utf8" });
64
+ return {
65
+ id: "diff",
66
+ ok: true,
67
+ status: (status.stdout || "").trim(),
68
+ diffStat: (diff.stdout || "").trim(),
69
+ };
70
+ }
71
+
72
+ function checkPlantVsWorn(root, millRoot) {
73
+ const plantDir = mint.millPlantDir(millRoot);
74
+ const millPlant = {
75
+ skills: plantDir ? mint.listSkillNamesAt(path.join(plantDir, "skills")) : [],
76
+ agents: plantDir ? mint.listAgentFilesAt(path.join(plantDir, "agents")) : [],
77
+ };
78
+ const params = mint.loadFoundryParams(root);
79
+ const tree = {
80
+ skills: mint.listConsumerSkillNames(root, params.skills),
81
+ agents: mint.listConsumerAgentFiles(root, params.agents),
82
+ };
83
+ try {
84
+ mint.assertNoCostumeDump(root, millPlant, tree);
85
+ return {
86
+ id: "plant-vs-worn",
87
+ ok: true,
88
+ millPlant: millPlant.skills,
89
+ tree: tree.skills,
90
+ worn: mint.wornSkillNames(root),
91
+ };
92
+ } catch (err) {
93
+ return {
94
+ id: "plant-vs-worn",
95
+ ok: false,
96
+ detail: err instanceof Error ? err.message : String(err),
97
+ extraSkills: err.extraSkills || [],
98
+ extraAgents: err.extraAgents || [],
99
+ };
100
+ }
101
+ }
102
+
103
+ function checkReceipt(root) {
104
+ const file = path.join(root, ".xray", "foundry-inventory.json");
105
+ if (mint.isDogfood(millPackageDir(), root)) {
106
+ return { id: "receipt", ok: true, skipped: true, detail: "dogfood / exo — no consumer inventory" };
107
+ }
108
+ const inventory = readJson(file);
109
+ if (!inventory) {
110
+ return { id: "receipt", ok: false, detail: "missing .xray/foundry-inventory.json" };
111
+ }
112
+ const millPlantSkills = inventory.millPlant?.skills || [];
113
+ const hasMill = millPlantSkills.includes("mill");
114
+ const hasInspect = millPlantSkills.includes("inspect");
115
+ const ok = hasMill && hasInspect;
116
+ return {
117
+ id: "receipt",
118
+ ok,
119
+ suit: inventory.suit,
120
+ mill: inventory.mill,
121
+ millPlant: millPlantSkills,
122
+ detail: ok ? null : "millPlant.skills must include mill and inspect",
123
+ };
124
+ }
125
+
126
+ function checkCi(root) {
127
+ const millPath = path.join(root, ".xray", "foundry-ci-report.json");
128
+ const shimPath = path.join(root, ".opencode", "logs", "ci-cd-monitor-report.json");
129
+ const report = readJson(millPath) || readJson(shimPath);
130
+ if (!report) {
131
+ return {
132
+ id: "ci",
133
+ ok: true,
134
+ skipped: true,
135
+ detail: "no mill ci report — run npx @0xray/foundry ci --report",
136
+ };
137
+ }
138
+ const status = String(report.ci_status || "");
139
+ if (status === "failure" || status === "unhealthy") {
140
+ return { id: "ci", ok: false, status, reason: report.reason || null };
141
+ }
142
+ return {
143
+ id: "ci",
144
+ ok: true,
145
+ skipped: status === "unknown",
146
+ status,
147
+ reason: report.reason || null,
148
+ };
149
+ }
150
+
151
+ export async function checkLivePut(packages, fetchFn = fetch) {
152
+ const results = [];
153
+ for (const pkg of packages) {
154
+ const url = npmTarballUrl(pkg.name, pkg.version);
155
+ if (!url) {
156
+ results.push({ name: pkg.name, version: pkg.version, ok: false, detail: "missing name/version" });
157
+ continue;
158
+ }
159
+ let status = 0;
160
+ try {
161
+ const head = await fetchFn(url, { method: "HEAD" });
162
+ status = head.status;
163
+ if (status === 405 || status === 501) {
164
+ const get = await fetchFn(url, { method: "GET" });
165
+ status = get.status;
166
+ }
167
+ } catch (err) {
168
+ results.push({
169
+ name: pkg.name,
170
+ version: pkg.version,
171
+ url,
172
+ ok: false,
173
+ detail: err instanceof Error ? err.message : String(err),
174
+ });
175
+ continue;
176
+ }
177
+ results.push({ name: pkg.name, version: pkg.version, url, status, ok: status === 200 });
178
+ }
179
+ return results;
180
+ }
181
+
182
+ function packagesToProbe(root) {
183
+ const pkgs = [];
184
+ const inventory = readJson(path.join(root, ".xray", "foundry-inventory.json"));
185
+ if (inventory?.mill?.name && inventory?.mill?.version) {
186
+ pkgs.push({ name: inventory.mill.name, version: inventory.mill.version });
187
+ }
188
+ const worn = readJson(path.join(root, "node_modules", "0xray", "package.json"));
189
+ if (worn?.name && worn?.version) {
190
+ pkgs.push({ name: worn.name, version: worn.version });
191
+ }
192
+ const self = readJson(path.join(root, "package.json"));
193
+ if (self?.name === "0xray" && self?.version) {
194
+ pkgs.push({ name: "0xray", version: self.version });
195
+ }
196
+ const seen = new Set();
197
+ return pkgs.filter((p) => {
198
+ const key = `${p.name}@${p.version}`;
199
+ if (seen.has(key)) return false;
200
+ seen.add(key);
201
+ return true;
202
+ });
203
+ }
204
+
205
+ function checkIsolatedHome(env = process.env, machine = mint.machineHome()) {
206
+ const isolated = isIsolatedHome(env, machine);
207
+ const machinePlugin = machineGrokPluginDir(machine);
208
+ const home = env.HOME || env.USERPROFILE || "";
209
+ const dest = home ? path.join(home, ".grok", "plugins", "0xray") : "";
210
+ const clobber = wouldClobberMachineGrok(dest, env, machine);
211
+ if (clobber) {
212
+ return {
213
+ id: "isolated-home",
214
+ ok: false,
215
+ isolated,
216
+ detail: `isolated HOME must not write ${machinePlugin}`,
217
+ };
218
+ }
219
+ return {
220
+ id: "isolated-home",
221
+ ok: true,
222
+ isolated,
223
+ machinePlugin,
224
+ dest: dest || null,
225
+ };
226
+ }
227
+
228
+ export async function inspectSuit(root, opts = {}) {
229
+ const millRoot = opts.millRoot || millPackageDir();
230
+ const fetchFn = opts.fetch || fetch;
231
+ const skipLive = Boolean(opts.skipLive);
232
+ const env = opts.env || process.env;
233
+ const machineHome = opts.machineHome || mint.machineHome();
234
+
235
+ const checks = [];
236
+ checks.push(checkDiff(root));
237
+ checks.push(checkPlantVsWorn(root, millRoot));
238
+ checks.push(checkReceipt(root));
239
+ checks.push(checkCi(root));
240
+
241
+ if (skipLive) {
242
+ checks.push({ id: "live-put", ok: true, skipped: true, detail: "--skip-live" });
243
+ } else {
244
+ const pkgs = packagesToProbe(root);
245
+ if (pkgs.length === 0) {
246
+ checks.push({
247
+ id: "live-put",
248
+ ok: true,
249
+ skipped: true,
250
+ detail: "no mill/0xray package identity to GET",
251
+ });
252
+ } else {
253
+ const puts = await checkLivePut(pkgs, fetchFn);
254
+ checks.push({
255
+ id: "live-put",
256
+ ok: puts.every((p) => p.ok),
257
+ packages: puts,
258
+ });
259
+ }
260
+ }
261
+
262
+ checks.push(checkIsolatedHome(env, machineHome));
263
+
264
+ const failed = checks.filter((c) => c.ok === false);
265
+ return {
266
+ ok: failed.length === 0,
267
+ failed: failed.map((c) => c.id),
268
+ checks,
269
+ };
270
+ }
271
+
272
+ async function main() {
273
+ const skipLive = process.argv.includes("--skip-live");
274
+ const root = resolveMillRoot();
275
+ const report = await inspectSuit(root, { skipLive });
276
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
277
+ process.exit(report.ok ? 0 : 1);
278
+ }
279
+
280
+ const isMain =
281
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
282
+ if (isMain) {
283
+ main();
284
+ }
@@ -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,
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  const fs = require("fs");
10
+ const os = require("os");
10
11
  const path = require("path");
11
12
 
12
13
  const DEFAULT_PARAMS = {
@@ -135,20 +136,20 @@ function listAgentFilesAt(agentsSrc) {
135
136
  .map((entry) => entry.name);
136
137
  }
137
138
 
138
- /** Mill mill-fill mill plant: mill mill mill mill-fill mill names they did not plant stay. */
139
- function millFillMillPlant(millPackageRoot, targetDir, log) {
139
+ /** Fasten mill plant first. Mill names they did not plant stay; same-name overlay wins. */
140
+ function fastenMillPlant(millPackageRoot, targetDir, log) {
140
141
  const plant = millPlantDir(millPackageRoot);
141
142
  if (!plant) return { skills: [], agents: [] };
142
143
  const skillsSrc = path.join(plant, "skills");
143
144
  const agentsSrc = path.join(plant, "agents");
144
145
  const skills = listSkillNamesAt(skillsSrc);
145
146
  const agents = listAgentFilesAt(agentsSrc);
146
- const skillHangers = listSkillHangers(targetDir);
147
+ const skillDirs = listProjectSkillDirs(targetDir);
147
148
  const agentsDest = path.join(targetDir, ".opencode", "agents");
148
- for (const hanger of skillHangers) {
149
+ for (const dir of skillDirs) {
149
150
  for (const name of skills) {
150
151
  const src = path.join(skillsSrc, name, "SKILL.md");
151
- const destMd = path.join(hanger, name, "SKILL.md");
152
+ const destMd = path.join(dir, name, "SKILL.md");
152
153
  if (path.resolve(src) === path.resolve(destMd)) continue;
153
154
  if (fs.existsSync(destMd)) continue;
154
155
  fs.mkdirSync(path.dirname(destMd), { recursive: true });
@@ -164,7 +165,7 @@ function millFillMillPlant(millPackageRoot, targetDir, log) {
164
165
  fs.copyFileSync(src, dest);
165
166
  }
166
167
  if (log && (skills.length > 0 || agents.length > 0)) {
167
- log("foundry-mint", "Mill plant onto hangers", "info", {
168
+ log("foundry-mint", "Fastened mill plant", "info", {
168
169
  skills: skills.length,
169
170
  agents: agents.length,
170
171
  });
@@ -180,12 +181,57 @@ function isDirectory(p) {
180
181
  }
181
182
  }
182
183
 
183
- /** Project skill hangers only. */
184
- function listSkillHangers(targetDir) {
185
- const dests = [path.join(targetDir, ".opencode", "skills")];
186
- const projectGrok = path.join(targetDir, ".grok", "plugins", "0xray", "skills");
187
- if (isDirectory(projectGrok)) dests.push(projectGrok);
188
- return dests;
184
+ /** Passwd home. `os.homedir()` follows $HOME and cannot detect last-mile isolation. */
185
+ function machineHome() {
186
+ try {
187
+ const passwd = os.userInfo().homedir;
188
+ if (typeof passwd === "string" && passwd) return passwd;
189
+ } catch {
190
+ /* no passwd */
191
+ }
192
+ return os.homedir();
193
+ }
194
+
195
+ function processHome(env) {
196
+ const e = env || process.env;
197
+ return e.HOME || e.USERPROFILE || "";
198
+ }
199
+
200
+ function isIsolatedHome(env, machine) {
201
+ const home = processHome(env);
202
+ if (!home) return false;
203
+ const real = machine || machineHome();
204
+ try {
205
+ return path.resolve(home) !== path.resolve(real);
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+
211
+ function machineGrokPluginDir(machine) {
212
+ return path.join(machine || machineHome(), ".grok", "plugins", "0xray");
213
+ }
214
+
215
+ function wouldClobberMachineGrok(dest, env, machine) {
216
+ if (!dest) return false;
217
+ const realMachine = machine || machineHome();
218
+ if (!isIsolatedHome(env, realMachine)) return false;
219
+ const real = path.resolve(machineGrokPluginDir(realMachine));
220
+ const target = path.resolve(dest);
221
+ return target === real || target.startsWith(`${real}${path.sep}`);
222
+ }
223
+
224
+ /**
225
+ * Project skill dirs on every TUI floor. Create on fasten. Never machine home.
226
+ * YML agents stay OpenCode (`.opencode/agents`).
227
+ */
228
+ function listProjectSkillDirs(targetDir) {
229
+ return [
230
+ path.join(targetDir, ".opencode", "skills"),
231
+ path.join(targetDir, ".grok", "plugins", "0xray", "skills"),
232
+ path.join(targetDir, ".hermes", "plugins", "xray-hermes", "skills"),
233
+ path.join(targetDir, ".openclaw", "skills"),
234
+ ];
189
235
  }
190
236
 
191
237
  function overlayJsonFacet(src, dest) {
@@ -238,6 +284,69 @@ function overlayAgentsCard(targetDir, params) {
238
284
  return true;
239
285
  }
240
286
 
287
+ function wornSkillNames(targetDir) {
288
+ const names = new Set();
289
+ for (const dir of listProjectSkillDirs(targetDir)) {
290
+ for (const name of listSkillNamesAt(dir)) names.add(name);
291
+ }
292
+ return [...names];
293
+ }
294
+
295
+ function wornAgentFiles(targetDir) {
296
+ return listAgentFilesAt(path.join(targetDir, ".opencode", "agents"));
297
+ }
298
+
299
+ function previousTreeAllowlist(targetDir) {
300
+ const file = path.join(targetDir, ".xray", "foundry-inventory.json");
301
+ if (!fs.existsSync(file)) return { skills: [], agents: [] };
302
+ try {
303
+ const inventory = JSON.parse(fs.readFileSync(file, "utf8"));
304
+ return {
305
+ skills: Array.isArray(inventory?.tree?.skills) ? inventory.tree.skills : [],
306
+ agents: Array.isArray(inventory?.tree?.agents) ? inventory.tree.agents : [],
307
+ };
308
+ } catch {
309
+ return { skills: [], agents: [] };
310
+ }
311
+ }
312
+
313
+ /** Extra worn names that are neither mill plant, their plant, nor a prior overlay. */
314
+ function costumeDumpExtras(targetDir, millPlant, tree) {
315
+ const prior = previousTreeAllowlist(targetDir);
316
+ const allowedSkills = new Set([
317
+ ...(Array.isArray(millPlant?.skills) ? millPlant.skills : []),
318
+ ...(Array.isArray(tree?.skills) ? tree.skills : []),
319
+ ...prior.skills,
320
+ ]);
321
+ const allowedAgents = new Set([
322
+ ...(Array.isArray(millPlant?.agents) ? millPlant.agents : []),
323
+ ...(Array.isArray(tree?.agents) ? tree.agents : []),
324
+ ...prior.agents,
325
+ ]);
326
+ return {
327
+ extraSkills: wornSkillNames(targetDir).filter((name) => !allowedSkills.has(name)),
328
+ extraAgents: wornAgentFiles(targetDir).filter((name) => !allowedAgents.has(name)),
329
+ };
330
+ }
331
+
332
+ function assertNoCostumeDump(targetDir, millPlant, tree) {
333
+ if (wantsCostume(targetDir)) {
334
+ return { ok: true, extraSkills: [], extraAgents: [] };
335
+ }
336
+ const extras = costumeDumpExtras(targetDir, millPlant, tree);
337
+ if (extras.extraSkills.length > 0 || extras.extraAgents.length > 0) {
338
+ const listed = [...extras.extraSkills, ...extras.extraAgents].join(", ");
339
+ const err = new Error(
340
+ `foundry-inspect: costume dump without foundry.json "costume": true (${listed})`,
341
+ );
342
+ err.code = "FOUNDRY_COSTUME_DUMP";
343
+ err.extraSkills = extras.extraSkills;
344
+ err.extraAgents = extras.extraAgents;
345
+ throw err;
346
+ }
347
+ return { ok: true, extraSkills: [], extraAgents: [] };
348
+ }
349
+
241
350
  function overlayConsumerTree(targetDir, log, params) {
242
351
  const resolved = params || loadFoundryParams(targetDir);
243
352
  const skillsRel = resolved.skills;
@@ -246,13 +355,13 @@ function overlayConsumerTree(targetDir, log, params) {
246
355
  const agents = listConsumerAgentFiles(targetDir, agentsRel);
247
356
  const skillsSrc = resolveInside(targetDir, skillsRel);
248
357
  const agentsSrc = resolveInside(targetDir, agentsRel);
249
- const skillHangers = listSkillHangers(targetDir);
358
+ const skillDirs = listProjectSkillDirs(targetDir);
250
359
  const agentsDest = path.join(targetDir, ".opencode", "agents");
251
360
 
252
- for (const hanger of skillHangers) {
361
+ for (const dir of skillDirs) {
253
362
  for (const name of skills) {
254
363
  const src = path.join(skillsSrc, name, "SKILL.md");
255
- const destMd = path.join(hanger, name, "SKILL.md");
364
+ const destMd = path.join(dir, name, "SKILL.md");
256
365
  if (path.resolve(src) === path.resolve(destMd)) continue;
257
366
  fs.mkdirSync(path.dirname(destMd), { recursive: true });
258
367
  fs.copyFileSync(src, destMd);
@@ -267,10 +376,10 @@ function overlayConsumerTree(targetDir, log, params) {
267
376
  }
268
377
 
269
378
  if (log && (skills.length > 0 || agents.length > 0)) {
270
- log("foundry-mint", "Overlaid consumer skills/agents onto hangers", "info", {
379
+ log("foundry-mint", "Overlaid consumer skills/agents", "info", {
271
380
  skills: skills.length,
272
381
  agents: agents.length,
273
- skillHangers: skillHangers.length,
382
+ skillDirs: skillDirs.length,
274
383
  });
275
384
  }
276
385
  return { skills, agents, params: resolved };
@@ -278,7 +387,7 @@ function overlayConsumerTree(targetDir, log, params) {
278
387
 
279
388
  function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
280
389
  if (isDogfood(packageRoot, targetDir)) {
281
- return { garment: "dogfood", skipped: true, consumer: readPackageIdentity(path.join(targetDir, "package.json")) };
390
+ return { suit: "dogfood", skipped: true, consumer: readPackageIdentity(path.join(targetDir, "package.json")) };
282
391
  }
283
392
  const mill = readPackageIdentity(path.join(packageRoot, "package.json"));
284
393
  const consumer = readPackageIdentity(path.join(targetDir, "package.json"));
@@ -299,17 +408,17 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
299
408
  (Array.isArray(tree?.millPlantSkills) && tree.millPlantSkills.length > 0) ||
300
409
  (Array.isArray(tree?.millPlantAgents) && tree.millPlantAgents.length > 0);
301
410
  const costume = wantsCostume(targetDir);
302
- const garment = overlayed
411
+ const suit = overlayed
303
412
  ? "overlay"
304
413
  : millPlanted
305
- ? "mill-fill"
414
+ ? "fastened"
306
415
  : costume
307
- ? "copied-onto-hanger"
308
- : "mill-fill";
416
+ ? "costume"
417
+ : "fastened";
309
418
  const inventory = {
310
419
  mill: { name: mill.name || "@0xray/foundry", version: mill.version },
311
420
  consumer: { name: consumer.name, version: consumer.version },
312
- garment,
421
+ suit,
313
422
  params: tree?.params || loadFoundryParams(targetDir),
314
423
  tree: { skills, agents },
315
424
  millPlant: {
@@ -337,7 +446,7 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
337
446
  log("foundry-mint", "Minted foundry-inventory from consumer mill SSOT", "info", {
338
447
  consumer: consumer.name,
339
448
  version: consumer.version,
340
- garment,
449
+ suit,
341
450
  });
342
451
  }
343
452
  return inventory;
@@ -345,10 +454,10 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
345
454
 
346
455
  function mintConsumerSuit(millPackageRoot, targetDir, log) {
347
456
  if (isDogfood(millPackageRoot, targetDir)) {
348
- return { garment: "dogfood", skipped: true };
457
+ return { suit: "dogfood", skipped: true };
349
458
  }
350
459
  const params = loadFoundryParams(targetDir);
351
- const millPlant = millFillMillPlant(millPackageRoot, targetDir, log);
460
+ const millPlant = fastenMillPlant(millPackageRoot, targetDir, log);
352
461
  const tree = overlayConsumerTree(targetDir, log, params);
353
462
  tree.millPlantSkills = millPlant.skills;
354
463
  tree.millPlantAgents = millPlant.agents;
@@ -365,6 +474,7 @@ function mintConsumerSuit(millPackageRoot, targetDir, log) {
365
474
  path.join(targetDir, ".xray", "config.json"),
366
475
  );
367
476
  tree.agentsCard = overlayAgentsCard(targetDir, params);
477
+ assertNoCostumeDump(targetDir, millPlant, tree);
368
478
  if (log && (tree.codex || tree.features || tree.config)) {
369
479
  log("foundry-mint", "Overlaid consumer suit facets onto .xray", "info", {
370
480
  constitution: tree.codex,
@@ -382,16 +492,27 @@ module.exports = {
382
492
  loadFoundryExtra,
383
493
  wantsCostume,
384
494
  millPlantDir,
385
- millFillMillPlant,
495
+ listSkillNamesAt,
496
+ listAgentFilesAt,
497
+ fastenMillPlant,
386
498
  resolveInside,
387
499
  overlayJsonFacet,
388
500
  overlayAgentsCard,
389
501
  overlayConsumerTree,
390
502
  listConsumerSkillNames,
391
503
  listConsumerAgentFiles,
392
- listSkillHangers,
504
+ listProjectSkillDirs,
393
505
  mintConsumerFromSsot,
394
506
  mintConsumerSuit,
395
507
  isDogfood,
396
508
  readPackageIdentity,
509
+ wornSkillNames,
510
+ wornAgentFiles,
511
+ costumeDumpExtras,
512
+ assertNoCostumeDump,
513
+ machineHome,
514
+ processHome,
515
+ isIsolatedHome,
516
+ machineGrokPluginDir,
517
+ wouldClobberMachineGrok,
397
518
  };
@@ -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
 
@@ -15,11 +15,17 @@ const { mintConsumerSuit } = require("./mint-suit.cjs");
15
15
  function main() {
16
16
  const target = resolveMillRoot();
17
17
  const millRoot = millPackageDir();
18
- const inventory = mintConsumerSuit(millRoot, target, (component, action, status) => {
19
- process.stdout.write(`${component} ${action} ${status}\n`);
20
- });
18
+ let inventory;
19
+ try {
20
+ inventory = mintConsumerSuit(millRoot, target, (component, action, status) => {
21
+ process.stdout.write(`${component} ${action} ${status}\n`);
22
+ });
23
+ } catch (err) {
24
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
25
+ process.exit(1);
26
+ }
21
27
  if (inventory.skipped) {
22
- process.stderr.write("mint: skipped dogfood / 0xray exo plant (not a consumer hanger)\n");
28
+ process.stderr.write("mint: skipped dogfood / 0xray exo plant (not a consumer project)\n");
23
29
  process.exit(0);
24
30
  }
25
31
  process.stdout.write(`${JSON.stringify(inventory, null, 2)}\n`);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xray/foundry",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "0xRay mill: mint their suit params, stamp, gate, publish, CI report, git hooks. Not the worn exo.",
@@ -37,6 +37,7 @@
37
37
  "./release-gate": "./release-gate.mjs",
38
38
  "./mill-root": "./mill-root.mjs",
39
39
  "./mint-suit": "./mint-suit.cjs",
40
+ "./inspect": "./inspect.mjs",
40
41
  "./ci-monitor": "./ci-monitor.mjs",
41
42
  "./hooks": "./hooks.mjs",
42
43
  "./docs-build": "./docs-build.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.7"
5
+ mode: subagent
@@ -1,5 +1,5 @@
1
1
  name: mill
2
2
  skill: mill
3
- description: "Mill mill-fill mill plant. Their plant is the suit. Factory Repertoire."
4
- version: "0.1.5"
3
+ description: "Fasten their plant as the suit. Factory Repertoire. Inspect is the mill discipline."
4
+ version: "0.1.7"
5
5
  mode: subagent
@@ -0,0 +1,32 @@
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
+ Mill **does** these checks (`npx @0xray/foundry inspect`). Mint fails on a costume dump. Live PUT GETs the `.tgz`. Isolated HOME refuses `~/.grok/plugins/0xray`. Not an 8th MCP.
11
+
12
+ ## What mill inspects
13
+
14
+ 1. **Diff.** `git status` / `git diff --stat` of the milled cwd. Ignore the agent's summary if the diff disagrees.
15
+ 2. **Plant vs worn.** `src/skills`, `src/opencode/agents` vs `.opencode/skills`, `.opencode/agents`. Same name: plant wins. Costume dump (worn names they did not plant and mill did not fasten) fails mint unless `foundry.json` has `"costume": true`.
16
+ 3. **Mill receipt.** `.xray/foundry-inventory.json` — mill name/version, `suit` (`fastened` / `overlay` / `costume`), `millPlant.skills` must include `mill` and `inspect`.
17
+ 4. **CI.** GitHub mill CI report (`.opencode/logs/ci-cd-monitor-report.json`), not a green check in chat. No token is a skip, not a pass invented.
18
+ 5. **Live PUT.** HTTP 200 on the npm `.tgz`. Metadata without a tarball is not shipped.
19
+ 6. **Isolated HOME.** Last-mile must not clobber `~/.grok/plugins/0xray`. Mill compares `$HOME` to the passwd home (`os.userInfo().homedir`), not `os.homedir()` (which follows `$HOME`).
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 inspect
31
+ npx @0xray/foundry ci --report
32
+ ```
@@ -1,22 +1,20 @@
1
1
  ---
2
2
  name: mill
3
- description: Mill mill-fill mill plant. Their plant is the suit. Factory Repertoire. Not 45/42 costume.
3
+ description: Fasten their plant as the suit. Factory Repertoire. Inspect is the mill discipline. Not 45/42 costume.
4
4
  ---
5
5
 
6
6
  # Mill
7
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.
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
9
 
10
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
11
 
12
- **Mill-fill:** their keys/files win. Mill mill mill-fill mill names they did not plant stay. JSON always merges.
12
+ **Mill-fill:** their keys/files win. Mill plant names they did not overlay stay. JSON always merges.
13
13
 
14
- **Factory:** Repertoire organ enable-when-resolves. Seven generated MCP. Constitution (PPE) stays worn. Not a fifth MCP. Not 5.0.
14
+ **Factory:** Repertoire enable-when-resolves. Seven generated MCP. Constitution (PPE) stays worn. Not a fifth MCP. Not 5.0.
15
15
 
16
- **Costume:** `foundry.json` `"costume": true` copies the 45/42 mill garment. Default is empty garment + this mill mill-fill mill plant.
16
+ **Costume:** `foundry.json` `"costume": true` copies the 45/42 mill costume.
17
17
 
18
18
  ```bash
19
19
  npx @0xray/foundry mint
20
20
  ```
21
-
22
- Nested mill: `node node_modules/0xray/scripts/foundry/cli.js mint`.
@@ -222,6 +222,13 @@ function isEphemeralInstallRoot(targetDir) {
222
222
  return /\/T\/|\/tmp\/|\/var\/folders\/|\/Temp\//i.test(normalized);
223
223
  }
224
224
 
225
+ const millSuit = require("../foundry/mint-suit.cjs");
226
+
227
+ /** Last-mile isolated HOME vs passwd home. `os.homedir()` follows $HOME. */
228
+ function isIsolatedHome(env = process.env, machine = millSuit.machineHome()) {
229
+ return millSuit.isIsolatedHome(env, machine);
230
+ }
231
+
225
232
  function copyHermesHookRuntimes(packageRoot) {
226
233
  const hooksSrc = [
227
234
  path.join(packageRoot, "dist", "integrations", "hooks"),
@@ -560,6 +567,7 @@ module.exports = {
560
567
  isDefaultMemoryRoutingOff,
561
568
  enableMemoryRoutingIfResolves,
562
569
  isEphemeralInstallRoot,
570
+ isIsolatedHome,
563
571
  copyHermesHookRuntimes,
564
572
  resolveOpenClawPreToolHookSource,
565
573
  resolveOpenClawPluginDir,
@@ -226,16 +226,16 @@ function main() {
226
226
 
227
227
  const millSkillHanger = path.join(tmpRoot, ".opencode", "skills", "mill", "SKILL.md");
228
228
  if (!fs.existsSync(millSkillHanger)) {
229
- throw new Error("empty garment missing mill plant skill on hanger");
229
+ throw new Error("mill plant missing mill skill");
230
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")),
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
235
  )
236
236
  : [];
237
- if (hangerNames.length > 3) {
238
- throw new Error(`costume dump on empty garment: ${hangerNames.join(",")}`);
237
+ if (wornNames.length > 3) {
238
+ throw new Error(`costume dump: ${wornNames.join(",")}`);
239
239
  }
240
240
 
241
241
  const installedPkg = JSON.parse(fs.readFileSync(path.join(nmRoot, "package.json"), "utf-8"));
@@ -8,7 +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
+ const { wantsCostume, isIsolatedHome, machineHome } = require("../foundry/mint-suit.cjs");
12
12
  const {
13
13
  wireHermesBridge,
14
14
  wireOpencodeBridge,
@@ -308,7 +308,7 @@ function installOpencodeBridge(targetDir, packageRoot, log) {
308
308
  const copied = syncCostumeSkills(path.join(opencodeDest, "skills"), packageRoot, targetDir);
309
309
  if (copied > 0) log("opencode-bridge", `skills synced (${copied})`, "info", { path: ".opencode/skills/" });
310
310
  } else {
311
- log("opencode-bridge", "empty garment — plugin only, mill mill-fill mill plant", "info");
311
+ log("opencode-bridge", "mill plant — plugin only, not costume dump", "info");
312
312
  fs.mkdirSync(opencodeDest, { recursive: true });
313
313
  copyOpencodePlugin(packageRoot, opencodeDest, log);
314
314
  }
@@ -375,18 +375,25 @@ function installGrokBridge(targetDir, packageRoot, log) {
375
375
  return;
376
376
  }
377
377
 
378
- const home = os.homedir();
378
+ const machine = machineHome();
379
379
  const ephemeral = isEphemeralInstallRoot(targetDir);
380
+ const isolated = isIsolatedHome();
380
381
  const targets = [path.join(targetDir, ".grok", "plugins", "0xray")];
381
- if (!ephemeral) {
382
- targets.push(path.join(home, ".grok", "plugins", "0xray"));
382
+ if (!ephemeral && !isolated) {
383
+ targets.push(path.join(machine, ".grok", "plugins", "0xray"));
383
384
  } else {
384
- log("grok-bridge", "skip machine ~/.grok plugin — ephemeral consumer", "info");
385
+ log(
386
+ "grok-bridge",
387
+ isolated
388
+ ? "skip machine ~/.grok plugin — isolated HOME"
389
+ : "skip machine ~/.grok plugin — ephemeral consumer",
390
+ "info",
391
+ );
385
392
  }
386
393
 
387
394
  for (const dest of targets) {
388
395
  if (copyPluginDir(sourceDir, dest)) {
389
- const rel = dest.startsWith(home) ? dest.replace(home, "~") : path.relative(targetDir, dest);
396
+ const rel = dest.startsWith(machine) ? dest.replace(machine, "~") : path.relative(targetDir, dest);
390
397
  log("grok-bridge", "plugin copied", "info", { path: rel || dest });
391
398
  writePluginMcpJson(dest, targetDir, log, "grok-bridge");
392
399
  patchGrokHooks(dest, packageRoot, targetDir, log, "grok-bridge");
@@ -395,8 +402,8 @@ function installGrokBridge(targetDir, packageRoot, log) {
395
402
  }
396
403
  }
397
404
 
398
- if (!ephemeral) {
399
- const grokGlobalSkills = path.join(home, ".grok", "skills");
405
+ if (!ephemeral && !isolated) {
406
+ const grokGlobalSkills = path.join(machine, ".grok", "skills");
400
407
  const globalCopied = syncCostumeSkills(grokGlobalSkills, packageRoot, targetDir);
401
408
  if (globalCopied > 0) {
402
409
  log("grok-bridge", `global skills synced (${globalCopied})`, "info", { path: "~/.grok/skills/" });
@@ -706,5 +713,6 @@ module.exports = {
706
713
  grokHookShellCommand,
707
714
  writeGrokDiscoveredHooks,
708
715
  isEphemeralInstallRoot,
716
+ isIsolatedHome,
709
717
  installFrameworkDogfoodWear,
710
718
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.3",
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.3",
3
+ "version": "4.0.1",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",