0xray 4.0.4 → 4.0.6

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.4",
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
- - 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`**
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
@@ -28,6 +28,20 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
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
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
+ - Nested mill **0.1.8**: isolated Hermes/OpenClaw, consumer tarball GET, CLI mint inspects. 4.0.6.
33
+
34
+ ---
35
+
36
+ ## [4.0.6] - 2026-09-09
37
+
38
+ Nested mill **0.1.8**. Isolated HOME skips machine Hermes/OpenClaw. Inspect GETs public consumer `.tgz`. CLI `mint` runs inspect (chat is not done). Postinstall does not inspect. Not 5.0.0.
39
+
40
+ ---
41
+
42
+ ## [4.0.5] - 2026-09-04
43
+
44
+ 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.
31
45
 
32
46
  ---
33
47
 
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { homedir } from 'os';
4
3
  import { createRequire } from 'module';
5
4
  import { frameworkLogger } from '../../core/framework-logger.js';
6
5
  import { syncBuiltinSkills } from './skill-install.js';
@@ -8,6 +7,7 @@ import { mintAfterWear } from './foundry-mint-wear.js';
8
7
  const __dirname = path.dirname(new URL(import.meta.url).pathname);
9
8
  const require = createRequire(import.meta.url);
10
9
  const packageRoot = path.join(__dirname, '..', '..', '..');
10
+ const millSuit = require(path.join(packageRoot, 'scripts', 'foundry', 'mint-suit.cjs'));
11
11
  const wiring = require(path.join(packageRoot, 'scripts', 'node', 'bridge-mcp-wiring.cjs'));
12
12
  export function registerHermesCommands(hermesCmd) {
13
13
  hermesCmd
@@ -21,8 +21,10 @@ export function registerHermesCommands(hermesCmd) {
21
21
  }
22
22
  async function installForHermes(options = {}) {
23
23
  frameworkLogger.log('hermes-integration', 'install-start', 'info', { options });
24
- const home = homedir();
25
- const targetPluginDir = path.join(home, '.hermes/plugins/xray-hermes');
24
+ const isolated = millSuit.isIsolatedHome();
25
+ const targetPluginDir = isolated
26
+ ? path.join(process.cwd(), '.hermes/plugins/xray-hermes')
27
+ : path.join(millSuit.machineHome(), '.hermes/plugins/xray-hermes');
26
28
  const possibleSources = [
27
29
  path.join(__dirname, '..', '..', '..', 'dist/integrations/hermes-agent'),
28
30
  path.join(__dirname, '..', '..', '..', 'src/integrations/hermes-agent'),
@@ -65,10 +67,10 @@ async function installForHermes(options = {}) {
65
67
  frameworkLogger.log('hermes-integration', 'skills-synced', 'info', { count: skillsCopied });
66
68
  }
67
69
  wiring.copyHermesFindProjectRootHelper(packageRoot, targetPluginDir);
68
- if (wiring.copyHermesHookRuntimes(packageRoot)) {
70
+ if (!isolated && wiring.copyHermesHookRuntimes(packageRoot)) {
69
71
  frameworkLogger.log('hermes-integration', 'hooks-copied', 'info', {});
70
72
  }
71
- const wired = wiring.wireHermesBridge(targetDir);
73
+ const wired = isolated ? { count: 0 } : wiring.wireHermesBridge(targetDir);
72
74
  mintAfterWear(targetDir);
73
75
  console.log(`\x1b[32m✓ Wired Hermes mcp_servers (${wired.count} servers)\x1b[0m`);
74
76
  console.log(`\x1b[32m✓ Consumer root → ${targetDir}\x1b[0m`);
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { homedir } from 'os';
4
3
  import { createRequire } from 'module';
5
4
  import { frameworkLogger } from '../../core/framework-logger.js';
6
5
  import { syncBuiltinSkills } from './skill-install.js';
@@ -9,6 +8,7 @@ import { OpenClawConfigLoader } from '../../integrations/openclaw/config.js';
9
8
  import { writeSuitSessionBoot } from '../../nucleus/suit-temperament.js';
10
9
  const __dirname = path.dirname(new URL(import.meta.url).pathname);
11
10
  const require = createRequire(import.meta.url);
11
+ const millSuit = require(path.join(__dirname, '..', '..', '..', 'scripts', 'foundry', 'mint-suit.cjs'));
12
12
  const wiring = require(path.join(__dirname, '..', '..', '..', 'scripts', 'node', 'bridge-mcp-wiring.cjs'));
13
13
  export function registerOpenClawCommands(openclawCmd) {
14
14
  openclawCmd
@@ -40,14 +40,19 @@ async function installForOpenClaw(options = {}) {
40
40
  const relative = path.relative(process.cwd(), configPath);
41
41
  console.log(`\x1b[32m✓ Created OpenClaw config at ${relative}\x1b[0m`);
42
42
  }
43
- const openclawSkillsDir = path.join(homedir(), '.openclaw', 'skills');
43
+ const isolated = millSuit.isIsolatedHome();
44
+ const openclawSkillsDir = isolated
45
+ ? path.join(targetDir, '.openclaw', 'skills')
46
+ : path.join(millSuit.machineHome(), '.openclaw', 'skills');
44
47
  const skillsCopied = syncBuiltinSkills(openclawSkillsDir);
45
48
  if (skillsCopied > 0) {
46
49
  console.log(`\x1b[32m✓ Synced ${skillsCopied} builtin skills to ~/.openclaw/skills/\x1b[0m`);
47
50
  }
48
51
  frameworkLogger.log('openclaw-integration', 'skills-synced', 'info', { count: skillsCopied });
49
52
  mintAfterWear(targetDir);
50
- const wired = wiring.wireOpenClawBridge(targetDir);
53
+ const wired = isolated
54
+ ? { count: 0, path: configPath, method: 'skipped-isolated-home' }
55
+ : wiring.wireOpenClawBridge(targetDir);
51
56
  console.log(`\x1b[32m✓ Wired OpenClaw MCP servers (${wired.count} via ${wired.method}) → ${wired.path}\x1b[0m`);
52
57
  console.log(`\x1b[32m✓ Consumer root → ${targetDir}\x1b[0m`);
53
58
  try {
@@ -57,8 +62,8 @@ async function installForOpenClaw(options = {}) {
57
62
  /* session-boot is best-effort */
58
63
  }
59
64
  const packageRoot = path.resolve(__dirname, '..', '..', '..');
60
- const hookInstalled = wiring.installOpenClawHostWear(packageRoot);
61
- const cliBackend = wiring.maybeWriteOpenClawCliBackend();
65
+ const hookInstalled = isolated ? null : wiring.installOpenClawHostWear(packageRoot);
66
+ const cliBackend = isolated ? false : wiring.maybeWriteOpenClawCliBackend();
62
67
  frameworkLogger.log('openclaw-integration', 'cli-backend', 'info', { written: cliBackend });
63
68
  if (hookInstalled) {
64
69
  console.log(`\x1b[32m✓ OpenClaw PreToolUse hook → ${hookInstalled}\x1b[0m`);
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.4",
3
+ "version": "4.0.6",
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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.8] - 2026-09-09
4
+
5
+ Isolated HOME skips machine `~/.hermes` and `~/.openclaw` (passwd home, same gate as Grok). Inspect GETs a public consumer tarball (`@0xray/review-suit`). CLI `mint` runs inspect after overlay; postinstall does not. Not an 8th MCP. Not 5.0.
6
+
7
+ ## [0.1.7] - 2026-09-04
8
+
9
+ 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.
10
+
3
11
  ## [0.1.6] - 2026-09-04
4
12
 
5
13
  Mill plant fastens an **inspect** suit: inspect AI work (diffs, traces, mill receipts, CI). Not empty. `mill` + `inspect` skills. Costume dump still opt-in.
@@ -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,7 +32,7 @@ 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
 
@@ -41,16 +42,16 @@ npx @0xray/foundry hooks
41
42
 
42
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 dirs only (`.opencode/skills`, project Grok plugin skills). 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
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
 
@@ -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 fastens mill plant then overlays their plant (mill-fill JSON). PPE stays worn.\n" +
29
+ "Mint fastens mill plant then overlays their plant (mill-fill JSON), then runs inspect. PPE stays worn.\n" +
30
+ "inspect runs the six mill checks (diff, plant vs worn, receipt, CI, live tarball GET, isolated HOME). Not an 8th MCP.\n" +
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,295 @@
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
+ export 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
+ if (
197
+ self &&
198
+ self.private !== true &&
199
+ inventory?.consumer?.name &&
200
+ inventory?.consumer?.version
201
+ ) {
202
+ pkgs.push({
203
+ name: inventory.consumer.name,
204
+ version: inventory.consumer.version,
205
+ });
206
+ }
207
+ const seen = new Set();
208
+ return pkgs.filter((p) => {
209
+ const key = `${p.name}@${p.version}`;
210
+ if (seen.has(key)) return false;
211
+ seen.add(key);
212
+ return true;
213
+ });
214
+ }
215
+
216
+ function checkIsolatedHome(env = process.env, machine = mint.machineHome()) {
217
+ const isolated = isIsolatedHome(env, machine);
218
+ const machinePlugin = machineGrokPluginDir(machine);
219
+ const home = env.HOME || env.USERPROFILE || "";
220
+ const dest = home ? path.join(home, ".grok", "plugins", "0xray") : "";
221
+ const clobber = wouldClobberMachineGrok(dest, env, machine);
222
+ if (clobber) {
223
+ return {
224
+ id: "isolated-home",
225
+ ok: false,
226
+ isolated,
227
+ detail: `isolated HOME must not write ${machinePlugin}`,
228
+ };
229
+ }
230
+ return {
231
+ id: "isolated-home",
232
+ ok: true,
233
+ isolated,
234
+ machinePlugin,
235
+ dest: dest || null,
236
+ };
237
+ }
238
+
239
+ export async function inspectSuit(root, opts = {}) {
240
+ const millRoot = opts.millRoot || millPackageDir();
241
+ const fetchFn = opts.fetch || fetch;
242
+ const skipLive = Boolean(opts.skipLive);
243
+ const env = opts.env || process.env;
244
+ const machineHome = opts.machineHome || mint.machineHome();
245
+
246
+ const checks = [];
247
+ checks.push(checkDiff(root));
248
+ checks.push(checkPlantVsWorn(root, millRoot));
249
+ checks.push(checkReceipt(root));
250
+ checks.push(checkCi(root));
251
+
252
+ if (skipLive) {
253
+ checks.push({ id: "live-put", ok: true, skipped: true, detail: "--skip-live" });
254
+ } else {
255
+ const pkgs = packagesToProbe(root);
256
+ if (pkgs.length === 0) {
257
+ checks.push({
258
+ id: "live-put",
259
+ ok: true,
260
+ skipped: true,
261
+ detail: "no mill/0xray package identity to GET",
262
+ });
263
+ } else {
264
+ const puts = await checkLivePut(pkgs, fetchFn);
265
+ checks.push({
266
+ id: "live-put",
267
+ ok: puts.every((p) => p.ok),
268
+ packages: puts,
269
+ });
270
+ }
271
+ }
272
+
273
+ checks.push(checkIsolatedHome(env, machineHome));
274
+
275
+ const failed = checks.filter((c) => c.ok === false);
276
+ return {
277
+ ok: failed.length === 0,
278
+ failed: failed.map((c) => c.id),
279
+ checks,
280
+ };
281
+ }
282
+
283
+ async function main() {
284
+ const skipLive = process.argv.includes("--skip-live");
285
+ const root = resolveMillRoot();
286
+ const report = await inspectSuit(root, { skipLive });
287
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
288
+ process.exit(report.ok ? 0 : 1);
289
+ }
290
+
291
+ const isMain =
292
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
293
+ if (isMain) {
294
+ main();
295
+ }
@@ -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,7 +136,7 @@ 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
+ /** Fasten mill plant first. Mill names they did not plant stay; same-name overlay wins. */
139
140
  function fastenMillPlant(millPackageRoot, targetDir, log) {
140
141
  const plant = millPlantDir(millPackageRoot);
141
142
  if (!plant) return { skills: [], agents: [] };
@@ -180,12 +181,57 @@ function isDirectory(p) {
180
181
  }
181
182
  }
182
183
 
183
- /** Project skill dirs only (.opencode/skills, project Grok plugin skills). */
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
+ */
184
228
  function listProjectSkillDirs(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;
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;
@@ -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,6 +492,8 @@ module.exports = {
382
492
  loadFoundryExtra,
383
493
  wantsCostume,
384
494
  millPlantDir,
495
+ listSkillNamesAt,
496
+ listAgentFilesAt,
385
497
  fastenMillPlant,
386
498
  resolveInside,
387
499
  overlayJsonFacet,
@@ -394,4 +506,13 @@ module.exports = {
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
  };
@@ -8,21 +8,32 @@ import { createRequire } from "node:module";
8
8
  import path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { millPackageDir, resolveMillRoot } from "./mill-root.mjs";
11
+ import { inspectSuit } from "./inspect.mjs";
11
12
 
12
13
  const require = createRequire(import.meta.url);
13
14
  const { mintConsumerSuit } = require("./mint-suit.cjs");
14
15
 
15
- function main() {
16
+ async function main() {
16
17
  const target = resolveMillRoot();
17
18
  const millRoot = millPackageDir();
18
- const inventory = mintConsumerSuit(millRoot, target, (component, action, status) => {
19
- process.stdout.write(`${component} ${action} ${status}\n`);
20
- });
19
+ const skipLive = process.argv.includes("--skip-live");
20
+ let inventory;
21
+ try {
22
+ inventory = mintConsumerSuit(millRoot, target, (component, action, status) => {
23
+ process.stdout.write(`${component} ${action} ${status}\n`);
24
+ });
25
+ } catch (err) {
26
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
27
+ process.exit(1);
28
+ }
21
29
  if (inventory.skipped) {
22
30
  process.stderr.write("mint: skipped dogfood / 0xray exo plant (not a consumer project)\n");
23
31
  process.exit(0);
24
32
  }
25
33
  process.stdout.write(`${JSON.stringify(inventory, null, 2)}\n`);
34
+ const report = await inspectSuit(target, { millRoot, skipLive });
35
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
36
+ if (!report.ok) process.exit(1);
26
37
  }
27
38
 
28
39
  const isMain =
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xray/foundry",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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"
@@ -1,5 +1,5 @@
1
1
  name: inspect
2
2
  skill: inspect
3
3
  description: "Inspect AI work: diffs, traces, mill receipts, CI. Fastened mill plant."
4
- version: "0.1.6"
4
+ version: "0.1.8"
5
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.6"
3
+ description: "Fasten their plant as the suit. Factory Repertoire. Inspect is the mill discipline."
4
+ version: "0.1.8"
5
5
  mode: subagent
@@ -7,16 +7,16 @@ description: Inspect AI work — diffs, tool traces, mill receipts, CI. Factory
7
7
 
8
8
  This mill **fastens** a suit for **inspecting AI work**. Not an empty suit. Not a copy of 0xRay's 45/42 costume.
9
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.
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
11
 
12
- ## What to inspect
12
+ ## What mill inspects
13
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`.
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
20
 
21
21
  ## How to work
22
22
 
@@ -27,5 +27,8 @@ Use this when an agent (or a mill) just wrote, minted, or shipped something. Rea
27
27
 
28
28
  ```bash
29
29
  npx @0xray/foundry mint
30
+ npx @0xray/foundry inspect
30
31
  npx @0xray/foundry ci --report
31
32
  ```
33
+
34
+ CLI `mint` runs inspect after overlay (exit 1 if inspect fails). Postinstall mint does not; `--skip-live` skips the tarball GET. Chat is not a receipt.
@@ -222,13 +222,21 @@ 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) {
233
+ if (millSuit.isIsolatedHome()) return false;
226
234
  const hooksSrc = [
227
235
  path.join(packageRoot, "dist", "integrations", "hooks"),
228
236
  path.join(packageRoot, "src", "integrations", "hooks"),
229
237
  ].find((p) => fs.existsSync(p));
230
238
  if (!hooksSrc) return false;
231
- const hooksDst = path.join(os.homedir(), ".hermes", "plugins", "hooks");
239
+ const hooksDst = path.join(millSuit.machineHome(), ".hermes", "plugins", "hooks");
232
240
  fs.mkdirSync(hooksDst, { recursive: true });
233
241
  fs.cpSync(hooksSrc, hooksDst, { recursive: true, force: true });
234
242
  return true;
@@ -421,10 +429,11 @@ function resolveOpenClawPluginDir(packageRoot) {
421
429
  }
422
430
 
423
431
  function installOpenClawHostWear(packageRoot) {
432
+ if (millSuit.isIsolatedHome()) return null;
424
433
  const source = resolveOpenClawPreToolHookSource(packageRoot);
425
434
  if (!source) return null;
426
435
 
427
- const hookDir = path.join(os.homedir(), ".openclaw", "hooks");
436
+ const hookDir = path.join(millSuit.machineHome(), ".openclaw", "hooks");
428
437
  fs.mkdirSync(hookDir, { recursive: true });
429
438
  const dest = path.join(hookDir, "xray-pre-tool.mjs");
430
439
  fs.copyFileSync(source, dest);
@@ -560,6 +569,7 @@ module.exports = {
560
569
  isDefaultMemoryRoutingOff,
561
570
  enableMemoryRoutingIfResolves,
562
571
  isEphemeralInstallRoot,
572
+ isIsolatedHome,
563
573
  copyHermesHookRuntimes,
564
574
  resolveOpenClawPreToolHookSource,
565
575
  resolveOpenClawPluginDir,
@@ -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,
@@ -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/" });
@@ -416,7 +423,16 @@ function installHermesBridge(targetDir, packageRoot, log) {
416
423
  return;
417
424
  }
418
425
 
419
- const targetPluginDir = path.join(os.homedir(), ".hermes", "plugins", "xray-hermes");
426
+ const ephemeral = isEphemeralInstallRoot(targetDir);
427
+ const isolated = isIsolatedHome();
428
+ const machine = machineHome();
429
+ const targetPluginDir =
430
+ ephemeral || isolated
431
+ ? path.join(targetDir, ".hermes", "plugins", "xray-hermes")
432
+ : path.join(machine, ".hermes", "plugins", "xray-hermes");
433
+ if (isolated) {
434
+ log("hermes-bridge", "skip machine ~/.hermes plugin — isolated HOME", "info");
435
+ }
420
436
  fs.mkdirSync(targetPluginDir, { recursive: true });
421
437
  for (const entry of fs.readdirSync(sourceDir)) {
422
438
  const src = path.join(sourceDir, entry);
@@ -428,10 +444,15 @@ function installHermesBridge(targetDir, packageRoot, log) {
428
444
  }
429
445
  }
430
446
  copyHermesFindProjectRootHelper(packageRoot, targetPluginDir);
431
- if (copyHermesHookRuntimes(packageRoot)) {
447
+ if (!ephemeral && !isolated && copyHermesHookRuntimes(packageRoot)) {
432
448
  log("hermes-bridge", "hook runtimes copied", "info", { path: "~/.hermes/plugins/hooks" });
433
449
  }
434
- log("hermes-bridge", "plugin copied", "info", { path: "~/.hermes/plugins/xray-hermes" });
450
+ log(
451
+ "hermes-bridge",
452
+ "plugin copied",
453
+ "info",
454
+ { path: ephemeral || isolated ? path.relative(targetDir, targetPluginDir) : "~/.hermes/plugins/xray-hermes" },
455
+ );
435
456
 
436
457
  writePluginMcpJson(targetPluginDir, targetDir, log, "hermes-bridge");
437
458
 
@@ -446,11 +467,13 @@ function installHermesBridge(targetDir, packageRoot, log) {
446
467
  log("hermes-bridge", "skip machine consumer marker — ephemeral consumer", "info");
447
468
  }
448
469
 
449
- try {
450
- const result = wireHermesBridge(targetDir);
451
- log("hermes-bridge", `mcp_servers wired (${result.count} servers)`, "info");
452
- } catch (e) {
453
- log("hermes-bridge", "mcp_servers wire failed", "warn", { error: e.message });
470
+ if (!ephemeral && !isolated) {
471
+ try {
472
+ const result = wireHermesBridge(targetDir);
473
+ log("hermes-bridge", `mcp_servers wired (${result.count} servers)`, "info");
474
+ } catch (e) {
475
+ log("hermes-bridge", "mcp_servers wire failed", "warn", { error: e.message });
476
+ }
454
477
  }
455
478
  }
456
479
 
@@ -487,26 +510,52 @@ function installOpenclawBridge(targetDir, packageRoot, log) {
487
510
  log("openclaw-bridge", "config created", "info", { path: ".xray/config/openclaw.json" });
488
511
  }
489
512
 
490
- const copied = syncCostumeSkills(path.join(os.homedir(), ".openclaw", "skills"), packageRoot, targetDir);
491
- if (copied > 0) log("openclaw-bridge", `skills synced (${copied})`, "info", { path: "~/.openclaw/skills/" });
513
+ const ephemeralOpenclaw = isEphemeralInstallRoot(targetDir);
514
+ const isolatedOpenclaw = isIsolatedHome();
515
+ if (!ephemeralOpenclaw && !isolatedOpenclaw) {
516
+ const copied = syncCostumeSkills(
517
+ path.join(machineHome(), ".openclaw", "skills"),
518
+ packageRoot,
519
+ targetDir,
520
+ );
521
+ if (copied > 0) {
522
+ log("openclaw-bridge", `skills synced (${copied})`, "info", { path: "~/.openclaw/skills/" });
523
+ }
524
+ } else {
525
+ log(
526
+ "openclaw-bridge",
527
+ isolatedOpenclaw
528
+ ? "skip machine ~/.openclaw skills — isolated HOME"
529
+ : "skip machine ~/.openclaw skills — ephemeral consumer",
530
+ "info",
531
+ );
532
+ }
492
533
 
493
- try {
494
- const result = wireOpenClawBridge(targetDir);
495
- log("openclaw-bridge", `openclaw.json mcp wired (${result.count} servers)`, "info", {
496
- method: result.method,
497
- });
498
- } catch (e) {
499
- log("openclaw-bridge", "openclaw.json mcp wire failed", "warn", { error: e.message });
534
+ if (!ephemeralOpenclaw && !isolatedOpenclaw) {
535
+ try {
536
+ const result = wireOpenClawBridge(targetDir);
537
+ log("openclaw-bridge", `openclaw.json mcp wired (${result.count} servers)`, "info", {
538
+ method: result.method,
539
+ });
540
+ } catch (e) {
541
+ log("openclaw-bridge", "openclaw.json mcp wire failed", "warn", { error: e.message });
542
+ }
500
543
  }
501
544
 
502
- if (!isEphemeralInstallRoot(targetDir)) {
545
+ if (!ephemeralOpenclaw && !isolatedOpenclaw) {
503
546
  const hook = installOpenClawHostWear(packageRoot);
504
547
  if (hook) log("openclaw-bridge", "PreToolUse hook installed", "info", { path: hook });
505
548
  if (maybeWriteOpenClawCliBackend()) {
506
549
  log("openclaw-bridge", "opencode-cli backend written", "info");
507
550
  }
508
551
  } else {
509
- log("openclaw-bridge", "skip machine PreToolUse wear — ephemeral consumer", "info");
552
+ log(
553
+ "openclaw-bridge",
554
+ isolatedOpenclaw
555
+ ? "skip machine PreToolUse wear — isolated HOME"
556
+ : "skip machine PreToolUse wear — ephemeral consumer",
557
+ "info",
558
+ );
510
559
  }
511
560
  }
512
561
 
@@ -706,5 +755,6 @@ module.exports = {
706
755
  grokHookShellCommand,
707
756
  writeGrokDiscoveredHooks,
708
757
  isEphemeralInstallRoot,
758
+ isIsolatedHome,
709
759
  installFrameworkDogfoodWear,
710
760
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.4",
3
+ "version": "4.0.1",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",