0xray 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.gitignore.default +4 -1
  2. package/.grok-plugin/plugin.json +1 -1
  3. package/.opencode/skills/orchestrator/SKILL.md +1 -7
  4. package/.xray/features.json +4 -1
  5. package/.xray/features.schema.json +8 -0
  6. package/AGENTS-consumer.md +1 -1
  7. package/AGENTS.md +2 -2
  8. package/README.md +2 -2
  9. package/dist/.gitignore.default +4 -1
  10. package/dist/AGENTS.md +2 -2
  11. package/dist/CHANGELOG.md +27 -1
  12. package/dist/README.md +2 -2
  13. package/dist/cli/commands/opencode-install.js +3 -9
  14. package/dist/cli/commands/skill-install.js +10 -5
  15. package/dist/cli/goggles-status.d.ts +2 -5
  16. package/dist/cli/goggles-status.js +5 -53
  17. package/dist/cli/index.js +2 -2
  18. package/dist/enforcement/enforcer-tools.d.ts +1 -1
  19. package/dist/enforcement/enforcer-tools.js +1 -1
  20. package/dist/integrations/cursor/README.md +1 -1
  21. package/dist/integrations/cursor/hooks/pre-tool-use.js +4 -0
  22. package/dist/integrations/grok/grok-cli.js +5 -7
  23. package/dist/integrations/grok/hooks/grok-hook-utils.js +4 -1
  24. package/dist/integrations/grok/hooks/pre-tool-use.js +4 -18
  25. package/dist/integrations/grok/hooks/session-start.js +4 -2
  26. package/dist/integrations/hermes-agent/bridge.mjs +4 -1
  27. package/dist/integrations/hooks/goggles-mcp.mjs +20 -196
  28. package/dist/integrations/hooks/goggles-pipeline.mjs +606 -616
  29. package/dist/integrations/hooks/goggles-planes.json +88 -40
  30. package/dist/integrations/hooks/plates.cjs +84 -0
  31. package/dist/integrations/hooks/pre-tool-speak.mjs +2 -4
  32. package/dist/integrations/hooks/skill-seek.mjs +115 -0
  33. package/dist/integrations/hooks/station-hook-runtime.cjs +4 -1
  34. package/dist/integrations/openclaw/hooks/pre-tool-gate-runtime.mjs +4 -4
  35. package/dist/integrations/openclaw/plugin/xray-pre-tool/index.js +10 -1
  36. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  37. package/dist/memory-routing/plates.d.ts +1 -1
  38. package/dist/memory-routing/plates.js +14 -0
  39. package/dist/nucleus/thin-dispatch.d.ts +1 -1
  40. package/dist/nucleus/thin-dispatch.js +1 -1
  41. package/dist/nucleus/work-freshness.mjs +10 -0
  42. package/dist/plugin/xray-codex-injection.js +3 -1
  43. package/dist/skills/orchestrator/SKILL.md +1 -7
  44. package/docs-site/docs/plates/asides.md +35 -0
  45. package/docs-site/docs/plates/confer.md +35 -0
  46. package/docs-site/docs/plates/consumer-mcp.md +35 -0
  47. package/docs-site/docs/plates/delegation-gate.md +35 -0
  48. package/docs-site/docs/plates/dynamo.md +35 -0
  49. package/docs-site/docs/plates/foundry.md +35 -0
  50. package/docs-site/docs/plates/goggles.md +8 -8
  51. package/docs-site/docs/plates/ground.md +35 -0
  52. package/docs-site/docs/plates/hermes.md +35 -0
  53. package/docs-site/docs/plates/index.md +12 -5
  54. package/docs-site/docs/plates/inference-cycle.md +35 -0
  55. package/docs-site/docs/plates/lens-gate.md +6 -6
  56. package/docs-site/docs/plates/openclaw.md +35 -0
  57. package/docs-site/docs/plates/opencode.md +35 -0
  58. package/docs-site/docs/plates/repertoire.md +35 -0
  59. package/docs-site/docs/plates/synthesis.md +35 -0
  60. package/docs-site/docs/plates/temperament.md +35 -0
  61. package/docs-site/docs/plates/trail-state.md +1 -1
  62. package/package.json +1 -1
  63. package/scripts/foundry/README.md +3 -1
  64. package/scripts/foundry/mint-suit.cjs +120 -2
  65. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  66. package/scripts/foundry/reconcile-version.mjs +10 -3
  67. package/scripts/foundry/release.mjs +1 -1
  68. package/scripts/foundry/validate-release-docs.mjs +2 -2
  69. package/scripts/foundry/version-manager.mjs +12 -4
  70. package/scripts/node/install-bridges.cjs +63 -34
  71. package/scripts/node/pack-tmp-suit-proof.mjs +23 -4
  72. package/src/integrations/openclaw/plugin/xray-pre-tool/index.js +10 -1
  73. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  74. package/src/skills/orchestrator/SKILL.md +1 -7
  75. package/xray/features.json +4 -1
  76. package/xray/features.schema.json +8 -0
@@ -0,0 +1,35 @@
1
+ ---
2
+ title: Repertoire
3
+ sidebar_label: Repertoire
4
+ ---
5
+
6
+ # Repertoire
7
+
8
+ The memory organ matches named laws.
9
+
10
+ ```
11
+ ┌────────────────────────────────────────────────────────────┐
12
+ │ INPUT LAYER │
13
+ │ ┌────────────────────────────────────────────────────┐ │
14
+ │ │ a task and its signals │ │
15
+ │ └──────────────────────────┬─────────────────────────┘ │
16
+ └──────────────────────────────┼─────────────────────────────┘
17
+ v
18
+ ┌────────────┼───────────────────────────────────────────────┐
19
+ │ PROCESSING LAYER │
20
+ │ ┌────────────────────────────────────────────────────┐ │
21
+ │ │ match the worn laws │ │
22
+ │ └──────────────────────────┬─────────────────────────┘ │
23
+ └──────────────────────────────┼─────────────────────────────┘
24
+ v
25
+ ┌──────────────────────────────┼─────────────────────────────┐
26
+ │ OUTPUT LAYER v │
27
+ │ ┌────────────────────────────────────────────────────┐ │
28
+ │ │ one law, or none │ │
29
+ │ └────────────────────────────────────────────────────┘ │
30
+ └────────────────────────────────────────────────────────────┘
31
+ ```
32
+
33
+ The code is `src/memory-routing`. The worn package is `@0xray/repertoire` 0.2.9. Station is the wake ticket. This organ is the judgment. Do not pin 0.1.8.
34
+
35
+ stamped · 0xray 4.1.0
@@ -0,0 +1,35 @@
1
+ ---
2
+ title: Synthesis
3
+ sidebar_label: Synthesis
4
+ ---
5
+
6
+ # Synthesis
7
+
8
+ The reflect checkpoint for this checkout.
9
+
10
+ ```
11
+ ┌────────────────────────────────────────────────────────────┐
12
+ │ INPUT LAYER │
13
+ │ ┌────────────────────────────────────────────────────┐ │
14
+ │ │ a gate, a turn, or a finished todo │ │
15
+ │ └──────────────────────────┬─────────────────────────┘ │
16
+ └──────────────────────────────┼─────────────────────────────┘
17
+ v
18
+ ┌────────────┼───────────────────────────────────────────────┐
19
+ │ PROCESSING LAYER │
20
+ │ ┌────────────────────────────────────────────────────┐ │
21
+ │ │ the checkpoint when enabled │ │
22
+ │ └──────────────────────────┬─────────────────────────┘ │
23
+ └──────────────────────────────┼─────────────────────────────┘
24
+ v
25
+ ┌──────────────────────────────┼─────────────────────────────┐
26
+ │ OUTPUT LAYER v │
27
+ │ ┌────────────────────────────────────────────────────┐ │
28
+ │ │ due or not due │ │
29
+ │ └────────────────────────────────────────────────────┘ │
30
+ └────────────────────────────────────────────────────────────┘
31
+ ```
32
+
33
+ The file is `src/nucleus/synthesis.ts`. The checkpoint is `.xray/state/synthesis-checkpoint.json`. While `synthesis.enabled` is false, a slice is not written and it does not come due.
34
+
35
+ stamped · 0xray 4.1.0
@@ -0,0 +1,35 @@
1
+ ---
2
+ title: Temperament
3
+ sidebar_label: Temperament
4
+ ---
5
+
6
+ # Temperament
7
+
8
+ Ceremony scales by host.
9
+
10
+ ```
11
+ ┌────────────────────────────────────────────────────────────┐
12
+ │ INPUT LAYER │
13
+ │ ┌────────────────────────────────────────────────────┐ │
14
+ │ │ a host │ │
15
+ │ └──────────────────────────┬─────────────────────────┘ │
16
+ └──────────────────────────────┼─────────────────────────────┘
17
+ v
18
+ ┌────────────┼───────────────────────────────────────────────┐
19
+ │ PROCESSING LAYER │
20
+ │ ┌────────────────────────────────────────────────────┐ │
21
+ │ │ read suit_temperament │ │
22
+ │ └──────────────────────────┬─────────────────────────┘ │
23
+ └──────────────────────────────┼─────────────────────────────┘
24
+ v
25
+ ┌──────────────────────────────┼─────────────────────────────┐
26
+ │ OUTPUT LAYER v │
27
+ │ ┌────────────────────────────────────────────────────┐ │
28
+ │ │ frontier warns, guided denies │ │
29
+ │ └────────────────────────────────────────────────────┘ │
30
+ └────────────────────────────────────────────────────────────┘
31
+ ```
32
+
33
+ The module is `src/nucleus/suit-temperament.ts`. A missing `suit_temperament` stays guided. Frontier warns when a spawn has no plan. Guided and strict deny that spawn. Codex stays on. The guide is `docs-site/docs/guides/v3-temperament.md`.
34
+
35
+ stamped · 0xray 4.1.0
@@ -30,6 +30,6 @@ The suit writes its trail under .xray.
30
30
  └────────────────────────────────────────────────────────────┘
31
31
  ```
32
32
 
33
- Session boot, the lens match line, and the station card are state. The agent reads them. Do not commit `.xray/state`.
33
+ Session boot and the station card are state. `.xray/state/LENS.md` is the plane cards. A named plane whose Entry and Exit are missing is written there, and later passes read that card. Commit `.xray/state/LENS.md`. Do not commit the rest of `.xray/state`.
34
34
 
35
35
  stamped · 0xray 4.1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "Three-subsystem autonomous engine. The suit powers the agent you already run: memory, judgement, payments, and the wake.",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -37,6 +37,8 @@ npx @0xray/foundry hooks
37
37
 
38
38
  `release` without `--dry-run` requires `--i-mean-it` or `FOUNDRY_RELEASE=1`. It is a bump/commit/push/publish bot for the **milled cwd**, not a casual command.
39
39
 
40
+ On the 0xray repo, `reconcile --apply` and `stamp` refuse unless `release.mjs` called them. `npm run version:sync` refuses here too. Run `npx @0xray/foundry release [patch|minor|major] --i-mean-it`. That command publishes. A docs fix does not.
41
+
40
42
  `FOUNDRY_ROOT` overrides cwd (the repo being milled).
41
43
 
42
44
  `gate` is build + test + docs-check. `--skip-docs` fails the gate. `--prove-published` is Gate D and does not publish: npm lists the version, the annotated tag points at HEAD, and the GitHub Release page exists. A local gate pass is not Gate D. `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) plus observational harness (Cursor/Grok hooks bound, Repertoire fastened, Station present). `--go` emits a shop-style forge GO receipt; `--require-harness=bare|suited|partial` turns that observation into a gate. Mint fails on a costume dump. Isolated HOME skips machine `~/.grok`. Shared HOME last-mile dest is project `.grok/plugins/0xray` (no last-wins machine plugin). `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.
@@ -60,7 +62,7 @@ After mill plant is fastened, overlay their plant (defaults; remap in `foundry.j
60
62
 
61
63
  **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).
62
64
 
63
- **Default mill plant:** `mill` + `inspect` (inspect AI work). Fasten is **foundry-plant/0**: builtin plant is mill+inspect only (files from this package `plant/`). Fastens a suit, not an empty one. Not 45/42 costume. `foundry.json` `"costume": true` copies that dump. **Repertoire is preferred** (vendored, dest = laws). PPE stays worn.
65
+ **Default mill plant:** `mill` + `inspect` (inspect AI work). Fasten is **foundry-plant/0**: builtin plant is mill+inspect only (files from this package `plant/`). Fastens a suit, not an empty one. The mill suit also wears every catalog skill and every role file. A blip or sound seat fastens that plant only. A name outside the fastened set still fails inspect. **Repertoire is preferred** (vendored, dest = laws). PPE stays worn.
64
66
 
65
67
  **foundry-plant/0:** `foundry.json` `"plant": "@0xray/blip"` (aliases `"plant": "blip"` / `"plant": "sound"`) fastens whatever that mill package declares and ships in its own `plant/`. Organs (looker, vibe, mixer) live in the mill. Adding an organ is a mill publish, not a foundry catalog PR. CLI shims `foundry blip|sound` still exist on foundry 0.1.11+; fasten is the new cut. Foundry **0.1.12**.
66
68
 
@@ -114,12 +114,91 @@ function loadFoundryParams(targetDir) {
114
114
  return params;
115
115
  }
116
116
 
117
- /** 45/42 costume dump. Default false on consumers. Exo mill keeps the costume. */
117
+ /** Exo repo, or foundry.json costume true. This flag does not gate catalog skills. */
118
118
  function wantsCostume(targetDir) {
119
119
  if (isExoRepo(targetDir)) return true;
120
120
  return loadFoundryExtra(targetDir).costume === true;
121
121
  }
122
122
 
123
+ function suitPackageRoot(start) {
124
+ let dir = start;
125
+ for (let i = 0; i < 4 && dir; i += 1) {
126
+ if (
127
+ fs.existsSync(path.join(dir, "src", "skills"))
128
+ || fs.existsSync(path.join(dir, "dist", "skills"))
129
+ || fs.existsSync(path.join(dir, "src", "opencode", "agents"))
130
+ ) return dir;
131
+ const parent = path.dirname(dir);
132
+ if (parent === dir) break;
133
+ dir = parent;
134
+ }
135
+ return start || "";
136
+ }
137
+
138
+ function catalogSkillDir(packageRoot) {
139
+ const base = suitPackageRoot(packageRoot);
140
+ const dirs = [path.join(base, "dist", "skills"), path.join(base, "src", "skills")];
141
+ return dirs.find((dir) => listSkillNamesAt(dir).length > 0) || "";
142
+ }
143
+
144
+ function catalogSkillNames(packageRoot) {
145
+ const dir = catalogSkillDir(packageRoot);
146
+ return dir ? listSkillNamesAt(dir) : [];
147
+ }
148
+
149
+ function catalogAgentDirs(packageRoot) {
150
+ const base = suitPackageRoot(packageRoot);
151
+ return [
152
+ path.join(base, "src", "opencode", "agents"),
153
+ path.join(base, ".opencode", "agents"),
154
+ ].filter((p) => fs.existsSync(p));
155
+ }
156
+
157
+ function catalogAgentFiles(packageRoot) {
158
+ const names = new Set();
159
+ for (const dir of catalogAgentDirs(packageRoot)) {
160
+ for (const file of listAgentFilesAt(dir)) names.add(file);
161
+ }
162
+ return [...names];
163
+ }
164
+
165
+ /**
166
+ * The mill suit wears the catalog. A blip-only or sound-only seat does not.
167
+ * Costume stays a separate flag and does not decide this.
168
+ */
169
+ function suitCatalogApplies(targetDir, millPlant, packageRoot) {
170
+ const fromPlant = Array.isArray(millPlant?.kinds) ? millPlant.kinds : [];
171
+ const kinds = fromPlant.length > 0
172
+ ? fromPlant
173
+ : loadFactoryPlantKinds(targetDir, packageRoot || "");
174
+ return kinds.includes("mill");
175
+ }
176
+
177
+ /** Every catalog skill and role file. An existing file stays. */
178
+ function wearCatalog(packageRoot, targetDir) {
179
+ const source = catalogSkillDir(packageRoot);
180
+ if (source) {
181
+ for (const dir of listProjectSkillDirs(targetDir)) {
182
+ for (const name of catalogSkillNames(packageRoot)) {
183
+ const src = path.join(source, name, "SKILL.md");
184
+ const dest = path.join(dir, name, "SKILL.md");
185
+ if (!fs.existsSync(src) || fs.existsSync(dest)) continue;
186
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
187
+ fs.copyFileSync(src, dest);
188
+ }
189
+ }
190
+ }
191
+ const agentsDest = path.join(targetDir, ".opencode", "agents");
192
+ for (const dir of catalogAgentDirs(packageRoot)) {
193
+ for (const file of listAgentFilesAt(dir)) {
194
+ const dest = path.join(agentsDest, file);
195
+ if (fs.existsSync(dest)) continue;
196
+ fs.mkdirSync(agentsDest, { recursive: true });
197
+ fs.copyFileSync(path.join(dir, file), dest);
198
+ }
199
+ }
200
+ }
201
+
123
202
  function millPlantDir(millPackageRoot) {
124
203
  return millPlantProtocol.foundryPlantDir(millPackageRoot);
125
204
  }
@@ -208,11 +287,21 @@ function inventoryPlantKinds(inventory) {
208
287
  return millPlantProtocol.plantKindsFromInventory(inventory);
209
288
  }
210
289
 
290
+ function skillDirEntry(parent, entry) {
291
+ if (entry.isDirectory()) return true;
292
+ if (!entry.isSymbolicLink()) return false;
293
+ try {
294
+ return fs.statSync(path.join(parent, entry.name)).isDirectory();
295
+ } catch {
296
+ return false;
297
+ }
298
+ }
299
+
211
300
  function listSkillNamesAt(skillsSrc) {
212
301
  if (!isDirectory(skillsSrc)) return [];
213
302
  const names = [];
214
303
  for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
215
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
304
+ if (entry.name.startsWith(".") || !skillDirEntry(skillsSrc, entry)) continue;
216
305
  const skillMd = path.join(skillsSrc, entry.name, "SKILL.md");
217
306
  if (!fs.existsSync(skillMd)) continue;
218
307
  names.push(entry.name);
@@ -615,7 +704,10 @@ function costumeDumpExtras(targetDir, millPlant, tree) {
615
704
  if (Array.isArray(spec?.agents)) plantMapAgents.push(...spec.agents);
616
705
  }
617
706
  }
707
+ const packageRoot = tree?.millPackageRoot || "";
708
+ const catalogOn = suitCatalogApplies(targetDir, millPlant, packageRoot);
618
709
  const allowedSkills = new Set([
710
+ ...(catalogOn ? catalogSkillNames(packageRoot) : []),
619
711
  ...(Array.isArray(millPlant?.skills) ? millPlant.skills : []),
620
712
  ...(Array.isArray(millPlant?.sound?.skills) ? millPlant.sound.skills : []),
621
713
  ...(Array.isArray(millPlant?.blip?.skills) ? millPlant.blip.skills : []),
@@ -627,6 +719,7 @@ function costumeDumpExtras(targetDir, millPlant, tree) {
627
719
  ...shopPlant,
628
720
  ]);
629
721
  const allowedAgents = new Set([
722
+ ...(catalogOn ? catalogAgentFiles(packageRoot) : []),
630
723
  ...(Array.isArray(millPlant?.agents) ? millPlant.agents : []),
631
724
  ...(Array.isArray(millPlant?.sound?.agents) ? millPlant.sound.agents : []),
632
725
  ...(Array.isArray(millPlant?.blip?.agents) ? millPlant.blip.agents : []),
@@ -795,12 +888,34 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
795
888
  return withDna;
796
889
  }
797
890
 
891
+
892
+ function gogglesDisabled(targetDir) {
893
+ const file = path.join(targetDir, ".xray", "features.json");
894
+ if (!fs.existsSync(file)) return false;
895
+ try {
896
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
897
+ return Boolean(data && data.goggles && data.goggles.enabled === false);
898
+ } catch {
899
+ return false;
900
+ }
901
+ }
902
+
903
+ function assertGogglesOn(targetDir) {
904
+ if (gogglesDisabled(targetDir)) {
905
+ throw new Error("goggles off — the suit keeps goggles on");
906
+ }
907
+ }
908
+
798
909
  function mintConsumerSuit(millPackageRoot, targetDir, log) {
910
+ assertGogglesOn(targetDir);
799
911
  if (isDogfood(millPackageRoot, targetDir)) {
800
912
  return { suit: "dogfood", skipped: true };
801
913
  }
802
914
  const params = loadFoundryParams(targetDir);
803
915
  const millPlant = fastenMillPlant(millPackageRoot, targetDir, log);
916
+ if (suitCatalogApplies(targetDir, millPlant, millPackageRoot)) {
917
+ wearCatalog(millPackageRoot, targetDir);
918
+ }
804
919
  const tree = overlayConsumerTree(targetDir, log, params);
805
920
  tree.plantKinds = millPlant.kinds || loadFactoryPlantKinds(targetDir);
806
921
  tree.millPlantSkills = millPlant.skills;
@@ -825,6 +940,7 @@ function mintConsumerSuit(millPackageRoot, targetDir, log) {
825
940
  resolveInside(targetDir, params.features),
826
941
  path.join(targetDir, ".xray", "features.json"),
827
942
  );
943
+ assertGogglesOn(targetDir);
828
944
  tree.config = overlayJsonFacet(
829
945
  resolveInside(targetDir, params.config),
830
946
  path.join(targetDir, ".xray", "config.json"),
@@ -866,6 +982,7 @@ module.exports = {
866
982
  loadShopPlant,
867
983
  declaredShopPlant,
868
984
  wantsCostume,
985
+ suitCatalogApplies,
869
986
  millPlantDir,
870
987
  loadFactoryPlantKinds,
871
988
  factoryPlantAllowlist,
@@ -884,6 +1001,7 @@ module.exports = {
884
1001
  listProjectSkillDirs,
885
1002
  mintConsumerFromSsot,
886
1003
  mintConsumerSuit,
1004
+ assertGogglesOn,
887
1005
  attachInventoryDna,
888
1006
  inventoryDna,
889
1007
  isDogfood,
@@ -13,7 +13,7 @@ The mill **fastens** a suit. Default mill plant is **inspect** (inspecting AI wo
13
13
 
14
14
  **Factory:** **Repertoire is preferred** (vendored 0.2.9 ships on; dest = named laws). 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 costume.
16
+ **Skills:** the mill suit wears every catalog skill and every role file. The mill plant stays `mill` and `inspect`. A blip or sound seat fastens that plant only.
17
17
 
18
18
  ```bash
19
19
  npx @0xray/foundry mint
@@ -20,7 +20,7 @@ import { execSync } from "child_process";
20
20
  import fs from "fs";
21
21
  import path from "path";
22
22
  import { fileURLToPath } from "url";
23
- import { readRootPackage, resolveMillRoot } from "./mill-root.mjs";
23
+ import { isXrayExoRepo, readRootPackage, resolveMillRoot } from "./mill-root.mjs";
24
24
  import { packageLockVersions, syncPackageLockVersion } from "./version-manager.mjs";
25
25
 
26
26
  const rootDir = resolveMillRoot();
@@ -103,7 +103,7 @@ function fail(msg) {
103
103
  */
104
104
  export function publishedVersionCheckError(local, npmVersion) {
105
105
  if (compare(local, npmVersion) < 0) {
106
- return `package.json (${local}) must be >= npm (${npmVersion}). Run: npx @0xray/foundry reconcile patch --apply`;
106
+ return `package.json (${local}) must be >= npm (${npmVersion}). The bump is npx @0xray/foundry release patch --i-mean-it. That command publishes. A docs fix does not. Stop.`;
107
107
  }
108
108
  return null;
109
109
  }
@@ -134,6 +134,13 @@ function main() {
134
134
  }
135
135
 
136
136
  if (applyMode && bumpType) {
137
+ if (isXrayExoRepo(rootDir) && process.env.FOUNDRY_VIA_RELEASE !== "1") {
138
+ fail(
139
+ "This repo bumps only through scripts/foundry/release.mjs.\n" +
140
+ "Ship: node scripts/foundry/release.mjs patch --i-mean-it\n" +
141
+ "That command publishes. A docs fix does not. Stop.",
142
+ );
143
+ }
137
144
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
138
145
  pkg.version = target;
139
146
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
@@ -167,7 +174,7 @@ function main() {
167
174
  const lockNow = packageLockVersions(rootDir);
168
175
  if (lockNow && (lockNow.root !== pkgVersion || lockNow.pkg !== pkgVersion)) {
169
176
  fail(
170
- `package-lock.json (${lockNow.root} / ${lockNow.pkg}) must match package.json (${pkgVersion}). The bumper writes both: npx @0xray/foundry reconcile patch --apply`,
177
+ `package-lock.json (${lockNow.root} / ${lockNow.pkg}) must match package.json (${pkgVersion}). The bumper is npx @0xray/foundry release patch --i-mean-it. That command publishes. A docs fix does not. Stop.`,
171
178
  );
172
179
  }
173
180
 
@@ -57,7 +57,7 @@ function run(cmd, label) {
57
57
  cwd: rootDir,
58
58
  stdio: "inherit",
59
59
  encoding: "utf-8",
60
- env: { ...process.env, FOUNDRY_ROOT: rootDir },
60
+ env: { ...process.env, FOUNDRY_ROOT: rootDir, FOUNDRY_VIA_RELEASE: "1" },
61
61
  });
62
62
  }
63
63
 
@@ -410,8 +410,8 @@ function main() {
410
410
  } else {
411
411
  console.error('❌ Release docs validation FAILED:\n');
412
412
  for (const e of result.errors) console.error(` • ${e}`);
413
- console.error('\nFix docs then run: npm run version:sync');
414
- console.error('Or: npx @0xray/foundry docs-check\n');
413
+ console.error('\nFix the docs in the branch. On this repo the stamper runs only from scripts/foundry/release.mjs, and that command publishes. A docs fix does not publish. Stop.');
414
+ console.error('Re-check with: npx @0xray/foundry docs-check\n');
415
415
  }
416
416
  process.exit(1);
417
417
  }
@@ -574,9 +574,16 @@ function runReleaseDocsValidation() {
574
574
 
575
575
  function refuseBump(reason) {
576
576
  process.stderr.write(`${reason}\n`);
577
- process.stderr.write('One bumper: npx @0xray/foundry reconcile [patch|minor|major] --apply\n');
578
- process.stderr.write('Then stamp: npx @0xray/foundry stamp\n');
579
- process.stderr.write('Ship: npx @0xray/foundry release [patch|minor|major]\n');
577
+ process.stderr.write('Ship: npx @0xray/foundry release [patch|minor|major] --i-mean-it\n');
578
+ process.exit(1);
579
+ }
580
+
581
+ function refuseOutsideRelease() {
582
+ if (!isXrayExoRepo(rootDir)) return;
583
+ if (process.env.FOUNDRY_VIA_RELEASE === '1') return;
584
+ process.stderr.write('This repo stamps only through scripts/foundry/release.mjs.\n');
585
+ process.stderr.write('Ship: node scripts/foundry/release.mjs patch --i-mean-it\n');
586
+ process.stderr.write('That command publishes. A docs fix does not. Stop.\n');
580
587
  process.exit(1);
581
588
  }
582
589
 
@@ -586,13 +593,14 @@ function main() {
586
593
  if (args.includes('--help') || args.includes('-h')) {
587
594
  process.stdout.write(`Current version: ${getCurrentVersion()}\n`);
588
595
  process.stdout.write('Usage: npx @0xray/foundry stamp\n');
589
- process.stdout.write('Bump is refused. Reconcile advances the next cut, then this stamper runs.\n');
596
+ process.stdout.write('Bump is refused. On this repo the stamper runs only from scripts/foundry/release.mjs.\n');
590
597
  process.stdout.write('The stamper strips every present-tense patch pin from shipped guides and shipped OP-PROC.\n');
591
598
  process.stdout.write('It does not edit Station, NOTES, dest, or node_modules. It does not publish.\n');
592
599
  process.exit(0);
593
600
  }
594
601
 
595
602
  if (args.includes('--artifacts-only')) {
603
+ refuseOutsideRelease();
596
604
  updateReleaseArtifactsOnly('');
597
605
  return;
598
606
  }
@@ -216,7 +216,15 @@ function syncBuiltinSkills(targetSkillsDir, packageRoot) {
216
216
  try {
217
217
  if (!fs.existsSync(targetSkillsDir)) fs.mkdirSync(targetSkillsDir, { recursive: true });
218
218
  for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
219
- if (!entry.isDirectory()) continue;
219
+ let linkedDir = false;
220
+ if (entry.isSymbolicLink()) {
221
+ try {
222
+ linkedDir = fs.statSync(path.join(sourceDir, entry.name)).isDirectory();
223
+ } catch {
224
+ linkedDir = false;
225
+ }
226
+ }
227
+ if (!entry.isDirectory() && !linkedDir) continue;
220
228
  const skillMd = path.join(sourceDir, entry.name, "SKILL.md");
221
229
  if (!fs.existsSync(skillMd)) continue;
222
230
  const destMd = path.join(targetSkillsDir, entry.name, "SKILL.md");
@@ -234,8 +242,45 @@ function syncBuiltinSkills(targetSkillsDir, packageRoot) {
234
242
  return copied;
235
243
  }
236
244
 
245
+ function plantIncludesMill(targetDir, packageRoot) {
246
+ const { loadFactoryPlantKinds } = require("../foundry/mint-suit.cjs");
247
+ return loadFactoryPlantKinds(targetDir, packageRoot).includes("mill");
248
+ }
249
+
250
+ function copyRoleAgentFiles(packageRoot, opencodeDest) {
251
+ const dirs = [
252
+ path.join(packageRoot, "src", "opencode", "agents"),
253
+ path.join(packageRoot, ".opencode", "agents"),
254
+ ].filter((dir) => fs.existsSync(dir));
255
+ const destDir = path.join(opencodeDest, "agents");
256
+ for (const dir of dirs) {
257
+ let names;
258
+ try {
259
+ names = fs.readdirSync(dir);
260
+ } catch {
261
+ continue;
262
+ }
263
+ for (const name of names) {
264
+ if (!name.endsWith(".yml") && !name.endsWith(".yaml")) continue;
265
+ const src = path.join(dir, name);
266
+ let st;
267
+ try {
268
+ st = fs.statSync(src);
269
+ } catch {
270
+ continue;
271
+ }
272
+ if (!st.isFile()) continue;
273
+ const dest = path.join(destDir, name);
274
+ if (fs.existsSync(dest)) continue;
275
+ fs.mkdirSync(destDir, { recursive: true });
276
+ fs.copyFileSync(src, dest);
277
+ }
278
+ }
279
+ }
280
+
237
281
  function syncCostumeSkills(targetSkillsDir, packageRoot, targetDir) {
238
- if (targetDir && !wantsCostume(targetDir)) return 0;
282
+ // Catalog skills belong on the mill suit. A blip or sound seat keeps its plant.
283
+ if (targetDir && !plantIncludesMill(targetDir, packageRoot)) return 0;
239
284
  return syncBuiltinSkills(targetSkillsDir, packageRoot);
240
285
  }
241
286
 
@@ -448,7 +493,7 @@ function installOpencodeBridge(targetDir, packageRoot, log) {
448
493
  const opencodeSource = path.join(packageRoot, ".opencode");
449
494
  const opencodeDest = path.join(targetDir, ".opencode");
450
495
  const costume = wantsCostume(targetDir);
451
-
496
+ const millSuit = plantIncludesMill(targetDir, packageRoot);
452
497
  if (costume) {
453
498
  if (!fs.existsSync(opencodeSource)) {
454
499
  log("opencode-bridge", "skipped", "warn", { reason: ".opencode source missing" });
@@ -460,14 +505,18 @@ function installOpencodeBridge(targetDir, packageRoot, log) {
460
505
  } else {
461
506
  log("opencode-bridge", ".opencode exists — merging skills/plugin only", "info");
462
507
  }
463
- copyOpencodePlugin(packageRoot, opencodeDest, log);
464
- const copied = syncCostumeSkills(path.join(opencodeDest, "skills"), packageRoot, targetDir);
465
- if (copied > 0) log("opencode-bridge", `skills synced (${copied})`, "info", { path: ".opencode/skills/" });
466
508
  } else {
467
- log("opencode-bridge", "mill plant — plugin only, not costume dump", "info");
468
509
  fs.mkdirSync(opencodeDest, { recursive: true });
469
- copyOpencodePlugin(packageRoot, opencodeDest, log);
510
+ if (millSuit) {
511
+ copyRoleAgentFiles(packageRoot, opencodeDest);
512
+ log("opencode-bridge", "mill suit — role files, not the costume tree", "info");
513
+ } else {
514
+ log("opencode-bridge", "plant seat — plugin only", "info");
515
+ }
470
516
  }
517
+ copyOpencodePlugin(packageRoot, opencodeDest, log);
518
+ const copied = syncCostumeSkills(path.join(opencodeDest, "skills"), packageRoot, targetDir);
519
+ if (copied > 0) log("opencode-bridge", `skills synced (${copied})`, "info", { path: ".opencode/skills/" });
471
520
 
472
521
  try {
473
522
  const count = wireOpencodeBridge(targetDir);
@@ -1947,9 +1996,11 @@ function syncSetupSkillsAndRootLinks(packageRoot, targetDir) {
1947
1996
 
1948
1997
  /** A byte-identical package skill is the old mirror. A different body still throws. */
1949
1998
  function wornSkillMatchesPackage(packageRoot, targetDir, name) {
1950
- const source = fs.existsSync(path.join(packageRoot, "src", "skills"))
1951
- ? path.join(packageRoot, "src", "skills")
1952
- : path.join(packageRoot, "dist", "skills");
1999
+ const distSkills = path.join(packageRoot, "dist", "skills");
2000
+ const srcSkills = path.join(packageRoot, "src", "skills");
2001
+ const source = fs.existsSync(path.join(distSkills, name, "SKILL.md"))
2002
+ ? distSkills
2003
+ : srcSkills;
1953
2004
  const packaged = path.join(source, name, "SKILL.md");
1954
2005
  if (!fs.existsSync(packaged)) return false;
1955
2006
  const packagedBytes = fs.readFileSync(packaged);
@@ -1977,12 +2028,11 @@ function packageShipsConsumerSuit(packageRoot) {
1977
2028
  /**
1978
2029
  * Consumer wear files: AGENTS.md, .gitignore, repertoire link, .xray config,
1979
2030
  * bridges, mill plant (mill + inspect), and the scripts and dist links.
1980
- * The role-skill mirror stays in the package.
2031
+ * Catalog skills and role files stay worn. Costume does not strip them.
1981
2032
  * Does not install Cursor hooks or git hooks.
1982
2033
  * Existing user files follow the 4.0.28 guards inside those functions.
1983
2034
  */
1984
2035
 
1985
- const ROLE_SKILL_KEEP = new Set(["mill", "inspect"]);
1986
2036
  const ROLE_SKILL_DIRS = [
1987
2037
  [".opencode", "skills"],
1988
2038
  [".grok", "plugins", "0xray", "skills"],
@@ -1990,26 +2040,6 @@ const ROLE_SKILL_DIRS = [
1990
2040
  [".openclaw", "skills"],
1991
2041
  ];
1992
2042
 
1993
- function removeCopiedRoleSkills(packageRoot, targetDir) {
1994
- const source = fs.existsSync(path.join(packageRoot, "src", "skills"))
1995
- ? path.join(packageRoot, "src", "skills")
1996
- : path.join(packageRoot, "dist", "skills");
1997
- if (!fs.existsSync(source)) return;
1998
- for (const parts of ROLE_SKILL_DIRS) {
1999
- const dir = path.join(targetDir, ...parts);
2000
- if (!fs.existsSync(dir)) continue;
2001
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2002
- if (!entry.isDirectory() || ROLE_SKILL_KEEP.has(entry.name)) continue;
2003
- const packaged = path.join(source, entry.name, "SKILL.md");
2004
- const worn = path.join(dir, entry.name, "SKILL.md");
2005
- if (!fs.existsSync(packaged) || !fs.existsSync(worn)) continue;
2006
- const same = fs.readFileSync(packaged).equals(fs.readFileSync(worn));
2007
- if (!same) continue;
2008
- fs.rmSync(path.join(dir, entry.name), { recursive: true, force: true });
2009
- }
2010
- }
2011
- }
2012
-
2013
2043
  function installConsumerProjectFiles(packageRoot, targetDir, log) {
2014
2044
  const write = typeof log === "function" ? log : () => {};
2015
2045
  if (!isConsumerInstall(packageRoot, targetDir) || !packageShipsConsumerSuit(packageRoot)) {
@@ -2028,7 +2058,6 @@ function installConsumerProjectFiles(packageRoot, targetDir, log) {
2028
2058
  installGrokBridge(targetDir, packageRoot, write);
2029
2059
  installHermesBridge(targetDir, packageRoot, write);
2030
2060
  installOpenclawBridge(targetDir, packageRoot, write);
2031
- removeCopiedRoleSkills(packageRoot, targetDir);
2032
2061
  try {
2033
2062
  mintConsumerSuit(packageRoot, targetDir, write);
2034
2063
  } catch (err) {