0xray 4.0.42 → 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 (138) 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/.plugin/plugin.json +1 -1
  5. package/.xray/features.json +8 -5
  6. package/.xray/features.schema.json +8 -0
  7. package/AGENTS-consumer.md +9 -7
  8. package/AGENTS.md +19 -10
  9. package/LICENSE +1 -1
  10. package/README.md +15 -9
  11. package/SKILLS.md +3 -1
  12. package/dist/.gitignore.default +4 -1
  13. package/dist/AGENTS.md +19 -10
  14. package/dist/CHANGELOG.md +109 -11
  15. package/dist/LICENSE +1 -1
  16. package/dist/README.md +15 -9
  17. package/dist/SKILLS.md +3 -1
  18. package/dist/cli/commands/opencode-install.js +3 -9
  19. package/dist/cli/commands/plugin-commands.d.ts +4 -0
  20. package/dist/cli/commands/plugin-commands.js +84 -41
  21. package/dist/cli/commands/skill-install.js +10 -5
  22. package/dist/cli/goggles-status.d.ts +2 -5
  23. package/dist/cli/goggles-status.js +5 -53
  24. package/dist/cli/index.js +2 -2
  25. package/dist/core/config-loader.js +4 -4
  26. package/dist/core/features-config.d.ts +1 -1
  27. package/dist/core/features-config.js +3 -3
  28. package/dist/enforcement/enforcer-tools.d.ts +1 -1
  29. package/dist/enforcement/enforcer-tools.js +1 -1
  30. package/dist/integrations/cursor/README.md +1 -1
  31. package/dist/integrations/cursor/hooks/hooks.json +6 -3
  32. package/dist/integrations/cursor/hooks/pre-tool-use.js +104 -37
  33. package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +9 -1
  34. package/dist/integrations/grok/grok-cli.js +5 -7
  35. package/dist/integrations/grok/hooks/grok-hook-utils.js +4 -1
  36. package/dist/integrations/grok/hooks/pre-tool-use.js +6 -76
  37. package/dist/integrations/grok/hooks/session-start.js +4 -2
  38. package/dist/integrations/hermes-agent/bridge.mjs +17 -1
  39. package/dist/integrations/hooks/delegation-gate-runtime.mjs +3 -2
  40. package/dist/integrations/hooks/goggles-mcp.mjs +23 -181
  41. package/dist/integrations/hooks/goggles-pipeline.mjs +606 -571
  42. package/dist/integrations/hooks/goggles-planes.json +95 -46
  43. package/dist/integrations/hooks/plates.cjs +84 -0
  44. package/dist/integrations/hooks/pre-tool-speak.mjs +74 -0
  45. package/dist/integrations/hooks/skill-seek.mjs +115 -0
  46. package/dist/integrations/hooks/station-hook-runtime.cjs +4 -1
  47. package/dist/integrations/openclaw/hooks/pre-tool-gate-runtime.mjs +24 -7
  48. package/dist/integrations/openclaw/plugin/xray-pre-tool/index.js +26 -2
  49. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  50. package/dist/memory-routing/plates.d.ts +1 -1
  51. package/dist/memory-routing/plates.js +14 -0
  52. package/dist/monitoring/{nudge-watchdog.d.ts → nudge-monitor.d.ts} +14 -5
  53. package/dist/monitoring/{nudge-watchdog.js → nudge-monitor.js} +42 -17
  54. package/dist/nucleus/thin-dispatch.d.ts +1 -1
  55. package/dist/nucleus/thin-dispatch.js +1 -1
  56. package/dist/nucleus/work-freshness.mjs +10 -0
  57. package/dist/plugin/xray-codex-injection.js +20 -0
  58. package/dist/processors/implementations/nudge-processor.d.ts +1 -1
  59. package/dist/processors/implementations/nudge-processor.js +4 -4
  60. package/dist/skills/orchestrator/SKILL.md +1 -7
  61. package/docs-site/docs/plates/activity-log.md +1 -1
  62. package/docs-site/docs/plates/asides.md +35 -0
  63. package/docs-site/docs/plates/bifrost.md +48 -0
  64. package/docs-site/docs/plates/boot.md +2 -2
  65. package/docs-site/docs/plates/centrifuge.md +1 -1
  66. package/docs-site/docs/plates/confer.md +35 -0
  67. package/docs-site/docs/plates/consumer-mcp.md +35 -0
  68. package/docs-site/docs/plates/cursor-compact.md +1 -1
  69. package/docs-site/docs/plates/delegation-gate.md +35 -0
  70. package/docs-site/docs/plates/dynamo.md +35 -0
  71. package/docs-site/docs/plates/foundry.md +35 -0
  72. package/docs-site/docs/plates/glossary.md +7 -3
  73. package/docs-site/docs/plates/goggles.md +12 -1
  74. package/docs-site/docs/plates/governance.md +1 -1
  75. package/docs-site/docs/plates/grok-compact.md +1 -1
  76. package/docs-site/docs/plates/grokbot.md +3 -1
  77. package/docs-site/docs/plates/ground.md +35 -0
  78. package/docs-site/docs/plates/hermes.md +35 -0
  79. package/docs-site/docs/plates/host-pack.md +1 -1
  80. package/docs-site/docs/plates/house.md +1 -1
  81. package/docs-site/docs/plates/index.md +16 -7
  82. package/docs-site/docs/plates/inference-cycle.md +35 -0
  83. package/docs-site/docs/plates/inference-files.md +1 -1
  84. package/docs-site/docs/plates/kept-line.md +1 -1
  85. package/docs-site/docs/plates/kits.md +1 -1
  86. package/docs-site/docs/plates/lens-gate.md +8 -6
  87. package/docs-site/docs/plates/lens-page.md +1 -1
  88. package/docs-site/docs/plates/memory-recall.md +1 -1
  89. package/docs-site/docs/plates/notes-page.md +2 -2
  90. package/docs-site/docs/plates/openclaw.md +35 -0
  91. package/docs-site/docs/plates/opencode.md +35 -0
  92. package/docs-site/docs/plates/orchestration.md +1 -1
  93. package/docs-site/docs/plates/package-face.md +1 -1
  94. package/docs-site/docs/plates/payload-heat.md +1 -1
  95. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  96. package/docs-site/docs/plates/pre-tool.md +1 -1
  97. package/docs-site/docs/plates/processor.md +1 -1
  98. package/docs-site/docs/plates/record-map.md +1 -1
  99. package/docs-site/docs/plates/reflection-page.md +1 -1
  100. package/docs-site/docs/plates/repertoire.md +35 -0
  101. package/docs-site/docs/plates/reporting.md +1 -1
  102. package/docs-site/docs/plates/review.md +1 -1
  103. package/docs-site/docs/plates/routing.md +1 -1
  104. package/docs-site/docs/plates/session-capture.md +1 -1
  105. package/docs-site/docs/plates/site-manual.md +1 -1
  106. package/docs-site/docs/plates/stamp-plate.md +1 -1
  107. package/docs-site/docs/plates/station-card.md +1 -1
  108. package/docs-site/docs/plates/station-heat.md +1 -1
  109. package/docs-site/docs/plates/suit-organs.md +1 -1
  110. package/docs-site/docs/plates/suit-settings.md +1 -1
  111. package/docs-site/docs/plates/suit-wear.md +1 -1
  112. package/docs-site/docs/plates/suit.md +3 -3
  113. package/docs-site/docs/plates/synthesis.md +35 -0
  114. package/docs-site/docs/plates/temperament.md +35 -0
  115. package/docs-site/docs/plates/trail-state.md +2 -2
  116. package/docs-site/docs/plates/work-fresh.md +1 -1
  117. package/docs-site/docs/plates/write-home.md +1 -1
  118. package/llms.txt +14 -6
  119. package/package.json +3 -3
  120. package/scripts/foundry/README.md +5 -3
  121. package/scripts/foundry/mint-suit.cjs +120 -2
  122. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  123. package/scripts/foundry/reconcile-version.mjs +10 -3
  124. package/scripts/foundry/release-gate.mjs +111 -15
  125. package/scripts/foundry/release.mjs +9 -1
  126. package/scripts/foundry/validate-release-docs.mjs +2 -2
  127. package/scripts/foundry/version-manager.mjs +12 -4
  128. package/scripts/mjs/verify-pipeline-facets.mjs +1 -1
  129. package/scripts/node/install-bridges.cjs +91 -46
  130. package/scripts/node/pack-tmp-suit-proof.mjs +37 -10
  131. package/src/integrations/openclaw/plugin/xray-pre-tool/index.js +26 -2
  132. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  133. package/src/skills/orchestrator/SKILL.md +1 -7
  134. package/vendor/@0xray/repertoire/README.md +1 -1
  135. package/xray/agents_template.md +2 -2
  136. package/xray/features.json +8 -5
  137. package/xray/features.schema.json +8 -0
  138. package/docs-site/docs/plates/live-mesh.md +0 -41
@@ -3,10 +3,12 @@
3
3
  * release-gate.mjs — Single release checkpoint.
4
4
  *
5
5
  * Modes:
6
- * (default) build + test + consumer smoke (fresh + upgrade merge)
6
+ * (default) build + test + docs + consumer smoke (fresh + upgrade merge)
7
7
  * --verify-only git + reconcile + release docs + consumer smoke — after push, before publish
8
8
  * --skip-smoke skip consumer install smoke
9
- * --skip-docs skip release docs validation (not recommended for publish)
9
+ * --prove-published Gate D only: npm lists this version, the annotated tag points at HEAD,
10
+ * and the GitHub Release exists. This command does not publish.
11
+ * --skip-docs refused. Gate C runs on every gate.
10
12
  *
11
13
  * Usage:
12
14
  * npx @0xray/foundry gate
@@ -21,7 +23,7 @@ import { isXrayExoRepo, millScript, readRootPackage, resolveMillRoot } from "./m
21
23
  const rootDir = resolveMillRoot();
22
24
  const verifyOnly = process.argv.includes("--verify-only");
23
25
  const skipSmoke = process.argv.includes("--skip-smoke");
24
- const skipDocs = process.argv.includes("--skip-docs");
26
+ const provePublished = process.argv.includes("--prove-published");
25
27
 
26
28
  function step(label, cmd) {
27
29
  console.log(`\n${"─".repeat(60)}\n🔄 ${label}\n${"─".repeat(60)}\n`);
@@ -32,18 +34,114 @@ function docsCheckCmd() {
32
34
  return `${JSON.stringify(process.execPath)} ${JSON.stringify(millScript("validate-release-docs.mjs"))}`;
33
35
  }
34
36
 
37
+
38
+ /** Gate C is the docs check. A ship gate that skips it is not a pass. */
39
+ export function gateCRefusal(argv = process.argv) {
40
+ if (argv.includes("--skip-docs")) {
41
+ return "Gate C cannot be skipped. Remove --skip-docs. The docs check runs on every gate.";
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Gate D facts. listed, tagAnnotated, and tagMatchesHead are booleans.
48
+ * releaseUrl is the GitHub Release page, or empty when that page is missing.
49
+ */
50
+ export function provePublishedRelease(proof) {
51
+ const version = proof.version;
52
+ const errors = [];
53
+ if (proof.listed !== true) errors.push(`npm does not list ${version}`);
54
+ if (proof.tagAnnotated !== true) errors.push(`annotated tag v${version} is missing`);
55
+ if (proof.tagMatchesHead !== true) errors.push(`annotated tag v${version} does not point at HEAD`);
56
+ if (typeof proof.releaseUrl !== "string" || proof.releaseUrl.length === 0) {
57
+ errors.push(`GitHub Release v${version} is missing`);
58
+ }
59
+ return errors;
60
+ }
61
+
62
+ function gitOut(cmd) {
63
+ return execSync(cmd, { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
64
+ }
65
+
66
+ function originSlug() {
67
+ const url = gitOut("git remote get-url origin");
68
+ const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
69
+ if (!match) throw new Error(`origin is not a GitHub repo: ${url}`);
70
+ return { owner: match[1], repo: match[2] };
71
+ }
72
+
73
+ function collectPublishedProof() {
74
+ const pkg = readRootPackage(rootDir);
75
+ const version = pkg.version;
76
+ const name = pkg.name;
77
+ let listed = false;
78
+ try {
79
+ const view = gitOut(`npm view ${name}@${version} version`);
80
+ listed = view.split(/\s+/).includes(version);
81
+ } catch {
82
+ listed = false;
83
+ }
84
+ let tagAnnotated = false;
85
+ let tagMatchesHead = false;
86
+ try {
87
+ tagAnnotated = gitOut(`git cat-file -t v${version}`) === "tag";
88
+ const tagged = gitOut(`git rev-parse v${version}^{}`);
89
+ const head = gitOut("git rev-parse HEAD");
90
+ tagMatchesHead = tagAnnotated && tagged === head;
91
+ } catch {
92
+ tagAnnotated = false;
93
+ tagMatchesHead = false;
94
+ }
95
+ let releaseUrl = "";
96
+ try {
97
+ const { owner, repo } = originSlug();
98
+ const body = gitOut(
99
+ `curl -fsS -H "Accept: application/vnd.github+json" -H "User-Agent: 0xray-release-gate" https://api.github.com/repos/${owner}/${repo}/releases/tags/v${version}`,
100
+ );
101
+ const parsed = JSON.parse(body);
102
+ releaseUrl = typeof parsed.html_url === "string" ? parsed.html_url : "";
103
+ } catch {
104
+ releaseUrl = "";
105
+ }
106
+ return { version, name, listed, tagAnnotated, tagMatchesHead, releaseUrl };
107
+ }
108
+
109
+ function runProvePublished() {
110
+ const proof = collectPublishedProof();
111
+ const errors = provePublishedRelease(proof);
112
+ process.stdout.write(`\nGate D — ${proof.name}@${proof.version}\n`);
113
+ if (errors.length) {
114
+ for (const error of errors) process.stderr.write(`❌ ${error}\n`);
115
+ process.stderr.write("\n❌ GATE D FAILED — npm listing, annotated tag at HEAD, and GitHub Release are all required\n\n");
116
+ process.exit(1);
117
+ }
118
+ process.stdout.write(`✅ npm lists ${proof.version}\n`);
119
+ process.stdout.write(`✅ annotated tag v${proof.version} points at HEAD\n`);
120
+ process.stdout.write(`✅ GitHub Release ${proof.releaseUrl}\n`);
121
+ process.stdout.write("\n✅ GATE D PROVEN\n\n");
122
+ }
123
+
35
124
  function runLightGate() {
36
125
  const pkg = readRootPackage(rootDir);
37
126
  if (verifyOnly) {
38
- if (!skipDocs) step("1/1 Release docs (light)", docsCheckCmd());
127
+ step("1/1 Release docs (light)", docsCheckCmd());
39
128
  return;
40
129
  }
41
130
  if (pkg.scripts.build) step("1/3 Build", "npm run build");
42
131
  if (pkg.scripts.test) step("2/3 Tests", "npm test");
43
- if (!skipDocs) step("3/3 Release docs (light)", docsCheckCmd());
132
+ step("3/3 Release docs (light)", docsCheckCmd());
44
133
  }
45
134
 
46
135
  function main() {
136
+ const refused = gateCRefusal();
137
+ if (refused) {
138
+ process.stderr.write(`\n❌ ${refused}\n\n`);
139
+ process.exit(1);
140
+ }
141
+ if (provePublished) {
142
+ runProvePublished();
143
+ return;
144
+ }
47
145
  const mode = verifyOnly ? "verify" : "full";
48
146
  console.log("\n╔════════════════════════════════════════════════════════╗");
49
147
  console.log(`║ 🛡️ 0xRay Release Gate (${mode})`.padEnd(57) + "║");
@@ -54,9 +152,7 @@ function main() {
54
152
  runLightGate();
55
153
  } else if (verifyOnly) {
56
154
  step("1/6 Git + reconcile", "node scripts/node/pre-publish-guard.js --verify-only");
57
- if (!skipDocs) {
58
- step("2/6 Release docs", docsCheckCmd());
59
- }
155
+ step("2/6 Release docs", docsCheckCmd());
60
156
  step(
61
157
  "3/6 Plugin infrastructure",
62
158
  "node scripts/test/validate-plugins-e2e.cjs --structural-only",
@@ -75,9 +171,7 @@ function main() {
75
171
  `${JSON.stringify(process.execPath)} ${JSON.stringify(millScript("assert-packed-dist-cli.mjs"))}`,
76
172
  );
77
173
  step("3/8 Tests", "npm run test:comprehensive");
78
- if (!skipDocs) {
79
- step("4/8 Release docs", docsCheckCmd());
80
- }
174
+ step("4/8 Release docs", docsCheckCmd());
81
175
  step(
82
176
  "5/8 Consumer hook verifiers",
83
177
  "npm run verify:pre-commit-diff && npm run verify:pre-push-diff && node scripts/mjs/verify-delegation-gate-core.mjs --host=grok && node scripts/mjs/verify-delegation-gate-core.mjs --host=hermes && node scripts/mjs/verify-delegation-gate-core.mjs --host=opencode && node scripts/mjs/verify-hermes-session-start.mjs && node scripts/mjs/verify-confer-core.mjs && npm run verify:user-aside && npm run build && node scripts/mjs/verify-pipeline-facets.mjs --package-only",
@@ -91,9 +185,11 @@ function main() {
91
185
  }
92
186
  }
93
187
 
94
- console.log("\n╔════════════════════════════════════════════════════════╗");
95
- console.log("║ ✅ RELEASE GATE PASSED ║");
96
- console.log("╚════════════════════════════════════════════════════════╝\n");
188
+ process.stdout.write("\n╔════════════════════════════════════════════════════════╗\n");
189
+ process.stdout.write("║ ✅ GATE A–C LOCAL PASS ║\n");
190
+ process.stdout.write("║ Gate D is not proven by this command. ║\n");
191
+ process.stdout.write("╚════════════════════════════════════════════════════════╝\n");
192
+ process.stdout.write("Prove D after publish: npx @0xray/foundry gate --prove-published\n\n");
97
193
  } catch {
98
194
  console.error("\n❌ RELEASE GATE FAILED — do not tag or publish\n");
99
195
  process.exit(1);
@@ -105,4 +201,4 @@ const isMain =
105
201
 
106
202
  if (isMain) {
107
203
  main();
108
- }
204
+ }
@@ -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
 
@@ -191,6 +191,8 @@ async function main() {
191
191
  }
192
192
  }
193
193
  run(`git push origin v${version}`, "push tag");
194
+ say("Step 8: Gate D");
195
+ runMill("release-gate.mjs", ["--prove-published"], "gate D");
194
196
  return;
195
197
  }
196
198
 
@@ -281,6 +283,12 @@ async function main() {
281
283
  console.log(` would: git tag -a v${newVersion}`);
282
284
  }
283
285
  run(`git push origin v${newVersion}`, "push tag");
286
+ say("Step 8: Gate D");
287
+ runMill("release-gate.mjs", ["--prove-published"], "gate D");
288
+ if (dryRun) {
289
+ say("Dry run did not prove Gate D.");
290
+ return;
291
+ }
284
292
 
285
293
  console.log("\n╔════════════════════════════════════════════════════════╗");
286
294
  console.log("║ ✅ Release Complete! ║");
@@ -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
  }
@@ -110,7 +110,7 @@ if (packageOnly) {
110
110
  }
111
111
 
112
112
  // P1 — autonomous reporting
113
- if (features?.autonomous_reporting?.enabled === true) {
113
+ if (features?.autonomous_reporting?.enabled === false) {
114
114
  pass('autonomous_reporting.enabled');
115
115
  } else {
116
116
  fail('autonomous_reporting.enabled');
@@ -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);
@@ -1016,16 +1065,18 @@ const CURSOR_HOOK_EVENTS = [
1016
1065
  ["beforeReadFile", "before-read-file.sh"],
1017
1066
  ];
1018
1067
 
1068
+ const CURSOR_GATE_EVENTS = new Set(["preToolUse", "beforeShellExecution", "beforeReadFile"]);
1069
+
1019
1070
  const CURSOR_WEAR_BACKUP = "hooks.json.xray-before";
1020
1071
 
1021
1072
  const CLOUD_SAFE_CURSOR_HOOKS = {
1022
1073
  version: 1,
1023
1074
  hooks: {
1024
- preToolUse: [{ command: ".cursor/hooks/pre-tool-use.sh" }],
1075
+ preToolUse: [{ command: ".cursor/hooks/pre-tool-use.sh", failClosed: true }],
1025
1076
  preCompact: [{ command: ".cursor/hooks/pre-compact.sh" }],
1026
1077
  afterFileEdit: [{ command: ".cursor/hooks/after-file-edit.sh" }],
1027
- beforeShellExecution: [{ command: ".cursor/hooks/before-shell-execution.sh" }],
1028
- beforeReadFile: [{ command: ".cursor/hooks/before-read-file.sh" }],
1078
+ beforeShellExecution: [{ command: ".cursor/hooks/before-shell-execution.sh", failClosed: true }],
1079
+ beforeReadFile: [{ command: ".cursor/hooks/before-read-file.sh", failClosed: true }],
1029
1080
  },
1030
1081
  };
1031
1082
 
@@ -1071,6 +1122,9 @@ function mergeCloudSafeCursorHooks(existing) {
1071
1122
  !Array.isArray(current) || !current.some((h) => isRelativeCursorHookCommand(h && h.command));
1072
1123
  if (!current || leftover || missingRelative) {
1073
1124
  next.hooks[name] = entries;
1125
+ } else if (entries[0].failClosed) {
1126
+ // Gate events fail closed: our relative entry gets failClosed even on an existing hooks.json (re-wear upgrades it).
1127
+ next.hooks[name] = current.map((h) => (h && isRelativeCursorHookCommand(h.command) ? { ...h, failClosed: true } : h));
1074
1128
  }
1075
1129
  }
1076
1130
  return next;
@@ -1279,7 +1333,10 @@ function installedHookCommand(fromDir, packageRoot, scriptName) {
1279
1333
  function installedCursorHookEntries(fromDir, packageRoot) {
1280
1334
  const hooks = {};
1281
1335
  for (const [event, script] of CURSOR_HOOK_EVENTS) {
1282
- hooks[event] = [{ command: installedHookCommand(fromDir, packageRoot, script) }];
1336
+ const entry = { command: installedHookCommand(fromDir, packageRoot, script) };
1337
+ // Gate events fail closed: Cursor denies when the hook crashes or prints no JSON.
1338
+ if (CURSOR_GATE_EVENTS.has(event)) entry.failClosed = true;
1339
+ hooks[event] = [entry];
1283
1340
  }
1284
1341
  return hooks;
1285
1342
  }
@@ -1610,8 +1667,10 @@ function ownedDeletionEdits(tokens, elements) {
1610
1667
  return edits;
1611
1668
  }
1612
1669
 
1613
- function renderEventEntry(command) {
1614
- return `{ "command": ${JSON.stringify(command)} }`;
1670
+ function renderEventEntry(command, failClosed) {
1671
+ return failClosed
1672
+ ? `{ "command": ${JSON.stringify(command)}, "failClosed": true }`
1673
+ : `{ "command": ${JSON.stringify(command)} }`;
1615
1674
  }
1616
1675
 
1617
1676
  function editJsoncHooks(text, xrayByEvent, targetDir) {
@@ -1627,7 +1686,7 @@ function editJsoncHooks(text, xrayByEvent, targetDir) {
1627
1686
  const hooksPair = root.pairs.find((pair) => pair.key === "hooks");
1628
1687
  if (!hooksPair) {
1629
1688
  const inner = Object.entries(xrayByEvent)
1630
- .map(([event, entries]) => `"${event}": [${renderEventEntry(entries[0].command)}]`)
1689
+ .map(([event, entries]) => `"${event}": [${renderEventEntry(entries[0].command, entries[0].failClosed)}]`)
1631
1690
  .join(", ");
1632
1691
  const needsComma = root.pairs.length > 0;
1633
1692
  edits.push({
@@ -1642,9 +1701,10 @@ function editJsoncHooks(text, xrayByEvent, targetDir) {
1642
1701
  const missing = [];
1643
1702
  for (const [event, entries] of Object.entries(xrayByEvent)) {
1644
1703
  const command = entries[0].command;
1704
+ const failClosed = entries[0].failClosed === true;
1645
1705
  const pair = hooksObj.pairs.find((item) => item.key === event);
1646
1706
  if (!pair) {
1647
- missing.push([event, command]);
1707
+ missing.push([event, command, failClosed]);
1648
1708
  continue;
1649
1709
  }
1650
1710
  if (tokens[pair.valIdx].type !== "[") throw new Error(`${event} is not an array`);
@@ -1666,7 +1726,7 @@ function editJsoncHooks(text, xrayByEvent, targetDir) {
1666
1726
  edits.push({
1667
1727
  start: tokens[pair.valEnd].start,
1668
1728
  end: tokens[pair.valEnd].start,
1669
- insert: `${elements.length > 0 ? ", " : ""}${renderEventEntry(command)}`,
1729
+ insert: `${elements.length > 0 ? ", " : ""}${renderEventEntry(command, failClosed)}`,
1670
1730
  });
1671
1731
  continue;
1672
1732
  }
@@ -1678,13 +1738,17 @@ function editJsoncHooks(text, xrayByEvent, targetDir) {
1678
1738
  insert: JSON.stringify(command),
1679
1739
  });
1680
1740
  }
1741
+ if (failClosed && !objectPairs(tokens, first.el.startIdx).pairs.some((p) => p.key === "failClosed")) {
1742
+ // Re-wear upgrades an older entry: gate events fail closed.
1743
+ edits.push({ start: first.cmd.token.end, end: first.cmd.token.end, insert: ', "failClosed": true' });
1744
+ }
1681
1745
  if (owned.length > 1) {
1682
1746
  edits.push(...ownedDeletionEdits(tokens, owned.slice(1).map((item) => item.el)));
1683
1747
  }
1684
1748
  }
1685
1749
  if (missing.length > 0) {
1686
1750
  const rendered = missing
1687
- .map(([event, command]) => `"${event}": [${renderEventEntry(command)}]`)
1751
+ .map(([event, command, failClosed]) => `"${event}": [${renderEventEntry(command, failClosed)}]`)
1688
1752
  .join(", ");
1689
1753
  edits.push({
1690
1754
  start: tokens[hooksObj.closeIdx].start,
@@ -1778,20 +1842,21 @@ function mergeInstalledCursorHooks(existing, xrayByEvent) {
1778
1842
  continue;
1779
1843
  }
1780
1844
  const command = shipped[0].command;
1845
+ const extra = shipped[0].failClosed ? { failClosed: true } : {};
1781
1846
  let replaced = false;
1782
1847
  const next = [];
1783
1848
  for (const entry of current) {
1784
1849
  const cmd = entry && typeof entry === "object" ? entry.command : "";
1785
1850
  if (isXrayHookCommand(cmd)) {
1786
1851
  if (!replaced) {
1787
- next.push({ ...entry, command });
1852
+ next.push({ ...entry, command, ...extra });
1788
1853
  replaced = true;
1789
1854
  }
1790
1855
  continue;
1791
1856
  }
1792
1857
  next.push(entry);
1793
1858
  }
1794
- if (!replaced) next.push({ command });
1859
+ if (!replaced) next.push({ command, ...extra });
1795
1860
  hooks[name] = next;
1796
1861
  }
1797
1862
  const doc = {};
@@ -1931,9 +1996,11 @@ function syncSetupSkillsAndRootLinks(packageRoot, targetDir) {
1931
1996
 
1932
1997
  /** A byte-identical package skill is the old mirror. A different body still throws. */
1933
1998
  function wornSkillMatchesPackage(packageRoot, targetDir, name) {
1934
- const source = fs.existsSync(path.join(packageRoot, "src", "skills"))
1935
- ? path.join(packageRoot, "src", "skills")
1936
- : 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;
1937
2004
  const packaged = path.join(source, name, "SKILL.md");
1938
2005
  if (!fs.existsSync(packaged)) return false;
1939
2006
  const packagedBytes = fs.readFileSync(packaged);
@@ -1961,12 +2028,11 @@ function packageShipsConsumerSuit(packageRoot) {
1961
2028
  /**
1962
2029
  * Consumer wear files: AGENTS.md, .gitignore, repertoire link, .xray config,
1963
2030
  * bridges, mill plant (mill + inspect), and the scripts and dist links.
1964
- * The role-skill mirror stays in the package.
2031
+ * Catalog skills and role files stay worn. Costume does not strip them.
1965
2032
  * Does not install Cursor hooks or git hooks.
1966
2033
  * Existing user files follow the 4.0.28 guards inside those functions.
1967
2034
  */
1968
2035
 
1969
- const ROLE_SKILL_KEEP = new Set(["mill", "inspect"]);
1970
2036
  const ROLE_SKILL_DIRS = [
1971
2037
  [".opencode", "skills"],
1972
2038
  [".grok", "plugins", "0xray", "skills"],
@@ -1974,26 +2040,6 @@ const ROLE_SKILL_DIRS = [
1974
2040
  [".openclaw", "skills"],
1975
2041
  ];
1976
2042
 
1977
- function removeCopiedRoleSkills(packageRoot, targetDir) {
1978
- const source = fs.existsSync(path.join(packageRoot, "src", "skills"))
1979
- ? path.join(packageRoot, "src", "skills")
1980
- : path.join(packageRoot, "dist", "skills");
1981
- if (!fs.existsSync(source)) return;
1982
- for (const parts of ROLE_SKILL_DIRS) {
1983
- const dir = path.join(targetDir, ...parts);
1984
- if (!fs.existsSync(dir)) continue;
1985
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1986
- if (!entry.isDirectory() || ROLE_SKILL_KEEP.has(entry.name)) continue;
1987
- const packaged = path.join(source, entry.name, "SKILL.md");
1988
- const worn = path.join(dir, entry.name, "SKILL.md");
1989
- if (!fs.existsSync(packaged) || !fs.existsSync(worn)) continue;
1990
- const same = fs.readFileSync(packaged).equals(fs.readFileSync(worn));
1991
- if (!same) continue;
1992
- fs.rmSync(path.join(dir, entry.name), { recursive: true, force: true });
1993
- }
1994
- }
1995
- }
1996
-
1997
2043
  function installConsumerProjectFiles(packageRoot, targetDir, log) {
1998
2044
  const write = typeof log === "function" ? log : () => {};
1999
2045
  if (!isConsumerInstall(packageRoot, targetDir) || !packageShipsConsumerSuit(packageRoot)) {
@@ -2012,7 +2058,6 @@ function installConsumerProjectFiles(packageRoot, targetDir, log) {
2012
2058
  installGrokBridge(targetDir, packageRoot, write);
2013
2059
  installHermesBridge(targetDir, packageRoot, write);
2014
2060
  installOpenclawBridge(targetDir, packageRoot, write);
2015
- removeCopiedRoleSkills(packageRoot, targetDir);
2016
2061
  try {
2017
2062
  mintConsumerSuit(packageRoot, targetDir, write);
2018
2063
  } catch (err) {
@@ -39,17 +39,25 @@ export function foundryCliPath(millRoot) {
39
39
  return path.join(millRoot, "cli.js");
40
40
  }
41
41
 
42
- export function runFoundryCli(millRoot, consumerRoot, args) {
42
+ /** Clean-room foundry env. The machine home is empty so a plugin on the real machine cannot fail the seat inspect. */
43
+ export function proofFoundryEnv(consumerRoot, baseEnv = process.env) {
43
44
  const isolatedHome = path.join(consumerRoot, ".proof-home");
45
+ const machineHome = path.join(consumerRoot, ".proof-machine");
44
46
  fs.mkdirSync(isolatedHome, { recursive: true });
47
+ fs.mkdirSync(machineHome, { recursive: true });
48
+ return {
49
+ ...baseEnv,
50
+ FOUNDRY_ROOT: consumerRoot,
51
+ HOME: isolatedHome,
52
+ FOUNDRY_MACHINE_HOME: machineHome,
53
+ };
54
+ }
55
+
56
+ export function runFoundryCli(millRoot, consumerRoot, args) {
45
57
  return spawnSync(process.execPath, [foundryCliPath(millRoot), ...args], {
46
58
  cwd: consumerRoot,
47
59
  encoding: "utf8",
48
- env: {
49
- ...process.env,
50
- FOUNDRY_ROOT: consumerRoot,
51
- HOME: isolatedHome,
52
- },
60
+ env: proofFoundryEnv(consumerRoot),
53
61
  });
54
62
  }
55
63
 
@@ -92,7 +100,25 @@ export function wornSkillNames(consumerRoot) {
92
100
  );
93
101
  }
94
102
 
95
- export function assertFastenedMillPlant(consumerRoot) {
103
+ function catalogSkillNamesNear(millRoot) {
104
+ let dir = millRoot;
105
+ for (let i = 0; i < 4 && dir; i += 1) {
106
+ for (const rel of ["dist/skills", "src/skills"]) {
107
+ const skills = path.join(dir, rel);
108
+ if (!fs.existsSync(skills)) continue;
109
+ const names = fs.readdirSync(skills).filter((name) =>
110
+ fs.existsSync(path.join(skills, name, "SKILL.md")),
111
+ );
112
+ if (names.length > 0) return names;
113
+ }
114
+ const parent = path.dirname(dir);
115
+ if (parent === dir) break;
116
+ dir = parent;
117
+ }
118
+ return [];
119
+ }
120
+
121
+ export function assertFastenedMillPlant(consumerRoot, millRoot = consumerRoot) {
96
122
  const inventoryPath = path.join(consumerRoot, ".xray", "foundry-inventory.json");
97
123
  if (!fs.existsSync(inventoryPath)) {
98
124
  throw new Error("foundry-inventory.json missing — mill did not fasten");
@@ -118,8 +144,9 @@ export function assertFastenedMillPlant(consumerRoot) {
118
144
  }
119
145
  }
120
146
  const worn = wornSkillNames(consumerRoot);
147
+ const catalog = catalogSkillNamesNear(millRoot);
121
148
  const extras = worn.filter(
122
- (name) => !MILL_PLANT_SKILLS.includes(name) && !FACTORY_SHOP_SKILLS.includes(name),
149
+ (name) => !MILL_PLANT_SKILLS.includes(name) && !FACTORY_SHOP_SKILLS.includes(name) && !catalog.includes(name),
123
150
  );
124
151
  if (extras.length > 0) {
125
152
  throw new Error(`costume dump extras before hangar plant: ${extras.join(",")}`);
@@ -137,7 +164,7 @@ export function proveSuitAfterInstall(consumerRoot, millRoot) {
137
164
  if (mint.status !== 0) {
138
165
  throw new Error(`foundry mint --skip-live failed:\n${mint.stdout}\n${mint.stderr}`);
139
166
  }
140
- const inventory = assertFastenedMillPlant(consumerRoot);
167
+ const inventory = assertFastenedMillPlant(consumerRoot, millRoot);
141
168
  if (inventory.suit !== "fastened" && inventory.suit !== "overlay") {
142
169
  throw new Error(`expected fastened/overlay suit, got ${inventory.suit}`);
143
170
  }
@@ -172,7 +199,7 @@ export function proveSuitAfterInstall(consumerRoot, millRoot) {
172
199
  if (Array.isArray(plantVsWorn.extraSkills) && plantVsWorn.extraSkills.length > 0) {
173
200
  throw new Error(`inspect extraSkills after hangar plant: ${plantVsWorn.extraSkills.join(",")}`);
174
201
  }
175
- const after = assertFastenedMillPlant(consumerRoot);
202
+ const after = assertFastenedMillPlant(consumerRoot, millRoot);
176
203
  if (after.costume === true) {
177
204
  throw new Error("costume flipped true after hangar plant");
178
205
  }