0xray 3.4.5 → 3.4.7

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.
package/.xray/config.json CHANGED
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "$schema": "./config.schema.json",
3
- "version": "3.1.0",
4
3
  "description": "0xRay Framework - Token Management & Performance Configuration",
5
4
  "token_management": {
6
5
  "maxPromptTokens": 20000,
@@ -1,8 +1,10 @@
1
1
  # 0xRay AI Agents
2
2
 
3
+ <!-- 0xray-managed -->
4
+
3
5
  Quick reference for the 0xRay AI orchestration framework (shipped to your project on `npm install 0xray`).
4
6
 
5
- **v3.4.5** — 7 MCP servers · 45 skills · 68 codex terms · 4 platform bridges
7
+ **v3.4.7** — 7 MCP servers · 45 skills · 68 codex terms · 4 platform bridges
6
8
 
7
9
  ## Postinstall (automatic)
8
10
 
package/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # xray Agents
2
2
 
3
- Quick reference for the xray AI orchestration framework (**v3.4.5**).
3
+ Quick reference for the xray AI orchestration framework (**v3.4.7**).
4
4
 
5
5
  **42 agents** · **45 skills** · **7 consumer MCP servers** · **68 codex terms** · **3,226 tests**
6
6
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # xray — MCP-Centric AI Governance OS
2
2
 
3
- **v3.4.5** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
3
+ **v3.4.7** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
4
4
 
5
5
  xray is the pure v2 three-subsystem AI orchestration framework — **MCP-centric**, governed by Dynamo, and autonomous via thinDispatch. Consumer `npm install 0xray` auto-wires all four platform bridges and seven MCP servers with zero manual config.
6
6
 
package/SKILLS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 0xray Skills
2
2
 
3
- **v3.4.5** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
3
+ **v3.4.7** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
4
4
 
5
5
  xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
6
6
 
package/dist/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # xray Agents
2
2
 
3
- Quick reference for the xray AI orchestration framework (**v3.4.5**).
3
+ Quick reference for the xray AI orchestration framework (**v3.4.7**).
4
4
 
5
5
  **42 agents** · **45 skills** · **7 consumer MCP servers** · **68 codex terms** · **3,226 tests**
6
6
 
package/dist/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Conventional Commits](https://www.conventionalcommits.org/).
6
6
 
7
+ ## [3.4.7] - 2026-06-18
8
+
9
+ ### 🐛 Bug Fixes
10
+ - **Release gate upgrade smoke** — `consumer-install-smoke` simulates consumer opt-in merge on every publish
11
+ - **Release pipeline** — artifacts sync before gate; idempotent `npm publish`; tag after publish
12
+ - **Postinstall** — skip customized `AGENTS.md`; hard-fail on bridge wiring errors
13
+ - **pre-publish-guard** — allow `.xray/config.json` and `.xray/state/` runtime dirt between gate steps
14
+
15
+ ---
16
+
17
+ ## [3.4.6] - 2026-06-18
18
+
19
+ ### 🐛 Bug Fixes
20
+ - **Postinstall config merge** — `deployXrayConfig` deep-merges existing consumer `features.json`, `codex.json`, and `config.json` instead of overwriting on mtime; consumer opt-ins (e.g. `memory_routing`) are preserved and `features.version` bumps to the installed package
21
+
22
+ ---
23
+
7
24
  ## [3.4.5] - 2026-06-18
8
25
 
9
26
  ### ✨ Features
package/dist/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # xray — MCP-Centric AI Governance OS
2
2
 
3
- **v3.4.5** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
3
+ **v3.4.7** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
4
4
 
5
5
  xray is the pure v2 three-subsystem AI orchestration framework — **MCP-centric**, governed by Dynamo, and autonomous via thinDispatch. Consumer `npm install 0xray` auto-wires all four platform bridges and seven MCP servers with zero manual config.
6
6
 
package/dist/SKILLS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 0xray Skills
2
2
 
3
- **v3.4.5** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
3
+ **v3.4.7** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
4
4
 
5
5
  xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "3.4.5",
3
+ "version": "3.4.7",
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",
@@ -3,6 +3,7 @@
3
3
  * consumer-install-smoke.mjs — Clean-room pack + install verification.
4
4
  *
5
5
  * Simulates what consumers receive from npm without publishing.
6
+ * Runs fresh install checks + upgrade merge simulation.
6
7
  *
7
8
  * Usage:
8
9
  * node scripts/node/consumer-install-smoke.mjs
@@ -38,6 +39,85 @@ function run(cmd, cwd, { inherit = false } = {}) {
38
39
  return inherit ? "" : String(result).trim();
39
40
  }
40
41
 
42
+ function assertFreshInstallDefaults(tmpRoot, version) {
43
+ const consumerFeaturesPath = path.join(tmpRoot, ".xray", "features.json");
44
+ if (!fs.existsSync(consumerFeaturesPath)) {
45
+ throw new Error(".xray/features.json not deployed to consumer project by postinstall");
46
+ }
47
+ const features = JSON.parse(fs.readFileSync(consumerFeaturesPath, "utf-8"));
48
+ if (!features.memory_routing) {
49
+ throw new Error("Deployed features.json missing memory_routing block");
50
+ }
51
+ if (features.memory_routing.enabled !== false) {
52
+ throw new Error(
53
+ `Fresh install must default memory_routing.enabled=false (got ${features.memory_routing.enabled})`,
54
+ );
55
+ }
56
+ if (!features.inference_governance) {
57
+ throw new Error("Deployed features.json missing inference_governance block");
58
+ }
59
+ if (features.inference_governance.enabled !== false) {
60
+ throw new Error("Fresh install must default inference_governance.enabled=false");
61
+ }
62
+ console.log(" ✅ .xray/features.json fresh-install defaults (opt-in off)");
63
+ return { consumerFeaturesPath, features };
64
+ }
65
+
66
+ function runUpgradeMergeSmoke(tmpRoot, nmRoot, version) {
67
+ console.log("\n📋 Upgrade merge smoke (consumer opt-ins preserved)...");
68
+
69
+ const { deployXrayConfig } = require(path.join(nmRoot, "scripts/node/install-bridges.cjs"));
70
+ const consumerFeaturesPath = path.join(tmpRoot, ".xray", "features.json");
71
+
72
+ fs.writeFileSync(
73
+ consumerFeaturesPath,
74
+ `${JSON.stringify(
75
+ {
76
+ version: "3.4.0",
77
+ memory_routing: {
78
+ enabled: true,
79
+ provider: "repertoire",
80
+ module_path: "dist/provider/memory-routing-provider.js",
81
+ },
82
+ inference_governance: { enabled: true },
83
+ consumer_custom_block: { kept: true },
84
+ },
85
+ null,
86
+ 2,
87
+ )}\n`,
88
+ );
89
+
90
+ const shippedTemplate = JSON.parse(
91
+ fs.readFileSync(path.join(nmRoot, "xray/features.json"), "utf-8"),
92
+ );
93
+ const shippedMarker = shippedTemplate.token_optimization ? "token_optimization" : null;
94
+
95
+ deployXrayConfig(tmpRoot, nmRoot, () => {});
96
+
97
+ const merged = JSON.parse(fs.readFileSync(consumerFeaturesPath, "utf-8"));
98
+
99
+ if (merged.memory_routing?.enabled !== true) {
100
+ throw new Error(`Upgrade merge lost memory_routing.enabled (got ${merged.memory_routing?.enabled})`);
101
+ }
102
+ if (merged.memory_routing?.provider !== "repertoire") {
103
+ throw new Error(`Upgrade merge lost memory_routing.provider (got ${merged.memory_routing?.provider})`);
104
+ }
105
+ if (merged.inference_governance?.enabled !== true) {
106
+ throw new Error("Upgrade merge lost inference_governance.enabled");
107
+ }
108
+ if (merged.consumer_custom_block?.kept !== true) {
109
+ throw new Error("Upgrade merge dropped consumer-only keys");
110
+ }
111
+ if (merged.version !== version) {
112
+ throw new Error(`Upgrade merge version: expected ${version}, got ${merged.version}`);
113
+ }
114
+ if (shippedMarker && !merged[shippedMarker]) {
115
+ throw new Error(`Upgrade merge missing shipped template key: ${shippedMarker}`);
116
+ }
117
+
118
+ console.log(" ✅ upgrade merge preserves opt-ins + bumps features.version");
119
+ }
120
+
41
121
  function main() {
42
122
  console.log("\n=== Consumer Install Smoke ===\n");
43
123
 
@@ -94,7 +174,7 @@ function main() {
94
174
  }
95
175
 
96
176
  const bridges = require(path.join(nmRoot, "scripts/node/install-bridges.cjs"));
97
- for (const exp of ["installAllBridges", "resolveConsumerTargetDir", "isConsumerInstall"]) {
177
+ for (const exp of ["installAllBridges", "resolveConsumerTargetDir", "isConsumerInstall", "deployXrayConfig"]) {
98
178
  if (typeof bridges[exp] !== "function") {
99
179
  throw new Error(`install-bridges missing export: ${exp}`);
100
180
  }
@@ -106,7 +186,6 @@ function main() {
106
186
  }
107
187
  console.log(" ✅ consumer target resolution");
108
188
 
109
- // Postinstall should have run; verify .mcp.json merge
110
189
  const mcpPath = path.join(tmpRoot, ".mcp.json");
111
190
  if (!fs.existsSync(mcpPath)) {
112
191
  throw new Error(".mcp.json not created by postinstall");
@@ -124,26 +203,7 @@ function main() {
124
203
  }
125
204
  console.log(` ✅ .mcp.json has ${XRAY_MCP_NAMES.length} npx MCP servers`);
126
205
 
127
- const consumerFeaturesPath = path.join(tmpRoot, ".xray", "features.json");
128
- if (!fs.existsSync(consumerFeaturesPath)) {
129
- throw new Error(".xray/features.json not deployed to consumer project by postinstall");
130
- }
131
- const features = JSON.parse(fs.readFileSync(consumerFeaturesPath, "utf-8"));
132
- if (!features.memory_routing) {
133
- throw new Error("Deployed features.json missing memory_routing block");
134
- }
135
- if (features.memory_routing.enabled !== false) {
136
- throw new Error(
137
- `Shipped template must default memory_routing.enabled=false (got ${features.memory_routing.enabled})`,
138
- );
139
- }
140
- if (!features.inference_governance) {
141
- throw new Error("Deployed features.json missing inference_governance block");
142
- }
143
- if (features.inference_governance.enabled !== false) {
144
- throw new Error("Shipped template must default inference_governance.enabled=false");
145
- }
146
- console.log(" ✅ .xray/features.json deployed (memory_routing + inference_governance opt-in defaults)");
206
+ assertFreshInstallDefaults(tmpRoot, version);
147
207
 
148
208
  const schemaInPackage = path.join(nmRoot, "xray", "features.schema.json");
149
209
  if (!fs.existsSync(schemaInPackage)) {
@@ -151,6 +211,8 @@ function main() {
151
211
  }
152
212
  console.log(" ✅ xray/features.schema.json in package");
153
213
 
214
+ runUpgradeMergeSmoke(tmpRoot, nmRoot, version);
215
+
154
216
  console.log("\n✅ Consumer install smoke passed\n");
155
217
  } catch (err) {
156
218
  console.error(`\n❌ Consumer smoke failed: ${err.message}\n`);
@@ -403,6 +403,64 @@ function installOpenclawBridge(targetDir, packageRoot, log) {
403
403
 
404
404
  const XRAY_CONFIG_FILES = ["codex.json", "features.json", "features.schema.json", "config.json"];
405
405
 
406
+ /** Consumer JSON configs merge on upgrade — consumer values win; shipped fills new keys. */
407
+ const MERGE_CONFIG_FILES = new Set(["features.json", "config.json", "codex.json"]);
408
+
409
+ function readPackageVersion(packageRoot) {
410
+ try {
411
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
412
+ return pkg.version || null;
413
+ } catch {
414
+ return null;
415
+ }
416
+ }
417
+
418
+ function writeJsonFile(filePath, data) {
419
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
420
+ }
421
+
422
+ function deployXrayConfigFile(file, src, dst, packageRoot) {
423
+ if (!fs.existsSync(dst)) {
424
+ fs.copyFileSync(src, dst);
425
+ return true;
426
+ }
427
+
428
+ if (file === "features.schema.json") {
429
+ if (fs.statSync(src).mtime > fs.statSync(dst).mtime) {
430
+ fs.copyFileSync(src, dst);
431
+ return true;
432
+ }
433
+ return false;
434
+ }
435
+
436
+ if (MERGE_CONFIG_FILES.has(file)) {
437
+ try {
438
+ const shipped = JSON.parse(fs.readFileSync(src, "utf8"));
439
+ const consumer = JSON.parse(fs.readFileSync(dst, "utf8"));
440
+ const merged = deepMerge(shipped, consumer);
441
+ if (file === "features.json") {
442
+ const pkgVersion = readPackageVersion(packageRoot);
443
+ if (pkgVersion) merged.version = pkgVersion;
444
+ else if (shipped.version) merged.version = shipped.version;
445
+ }
446
+ writeJsonFile(dst, merged);
447
+ return true;
448
+ } catch {
449
+ if (fs.statSync(src).mtime > fs.statSync(dst).mtime) {
450
+ fs.copyFileSync(src, dst);
451
+ return true;
452
+ }
453
+ return false;
454
+ }
455
+ }
456
+
457
+ if (fs.statSync(src).mtime > fs.statSync(dst).mtime) {
458
+ fs.copyFileSync(src, dst);
459
+ return true;
460
+ }
461
+ return false;
462
+ }
463
+
406
464
  function resolveXrayConfigSource(packageRoot, file) {
407
465
  // Shipped SSOT: xray/ template wins over dev .xray/ runtime copy (P0.2)
408
466
  const xrayDir = path.join(packageRoot, "xray", file);
@@ -423,9 +481,7 @@ function deployXrayConfig(targetDir, packageRoot, log) {
423
481
  const src = resolveXrayConfigSource(packageRoot, file);
424
482
  const dst = path.join(xrayTargetDir, file);
425
483
  if (!src) continue;
426
- const shouldCopy = !fs.existsSync(dst) || fs.statSync(src).mtime > fs.statSync(dst).mtime;
427
- if (shouldCopy) {
428
- fs.copyFileSync(src, dst);
484
+ if (deployXrayConfigFile(file, src, dst, packageRoot)) {
429
485
  copied++;
430
486
  }
431
487
  }
@@ -479,6 +535,8 @@ module.exports = {
479
535
  syncBuiltinSkills,
480
536
  isConsumerInstall,
481
537
  deployXrayConfig,
538
+ deployXrayConfigFile,
482
539
  resolveXrayConfigSource,
483
540
  XRAY_CONFIG_FILES,
541
+ MERGE_CONFIG_FILES,
484
542
  };
@@ -19,11 +19,24 @@ const targetDir = resolveConsumerTargetDir(packageRoot, process.env.PWD || proce
19
19
  const resolvedPackage = path.resolve(packageRoot);
20
20
  const resolvedTarget = path.resolve(targetDir);
21
21
 
22
- // Copy AGENTS-consumer.md → AGENTS.md
22
+ const XRAY_MANAGED_AGENTS_MARKER = "<!-- 0xray-managed -->";
23
+
24
+ // Copy AGENTS-consumer.md → AGENTS.md (only if absent or still 0xray-managed)
23
25
  const agentsConsumer = path.join(packageRoot, "AGENTS-consumer.md");
24
26
  const agentsDest = path.join(targetDir, "AGENTS.md");
25
27
  if (fs.existsSync(agentsConsumer) && isConsumerInstall(resolvedPackage, resolvedTarget)) {
26
- fs.copyFileSync(agentsConsumer, agentsDest);
28
+ const shouldDeployAgents =
29
+ !fs.existsSync(agentsDest) ||
30
+ fs.readFileSync(agentsDest, "utf8").includes(XRAY_MANAGED_AGENTS_MARKER);
31
+ if (shouldDeployAgents) {
32
+ let content = fs.readFileSync(agentsConsumer, "utf8");
33
+ if (!content.includes(XRAY_MANAGED_AGENTS_MARKER)) {
34
+ content = `${content.trimEnd()}\n\n${XRAY_MANAGED_AGENTS_MARKER}\n`;
35
+ }
36
+ fs.writeFileSync(agentsDest, content);
37
+ } else {
38
+ structuredLog("postinstall", "Skipped AGENTS.md (consumer-customized)", "info");
39
+ }
27
40
  }
28
41
 
29
42
  // Copy .gitignore.default → .gitignore (never overwrite existing)
@@ -43,7 +56,9 @@ if (isConsumerInstall(resolvedPackage, resolvedTarget)) {
43
56
  log: structuredLog,
44
57
  });
45
58
  } catch (e) {
46
- structuredLog("postinstall", "Bridge install failed", "warn", { error: e.message });
59
+ structuredLog("postinstall", "Bridge install failed", "error", { error: e.message });
60
+ console.error("\n❌ 0xRay postinstall failed — bridge wiring did not complete.\n");
61
+ process.exit(1);
47
62
  }
48
63
 
49
64
  structuredLog(
@@ -67,6 +67,8 @@ const ARTIFACT_PREFIXES = [
67
67
  '.opencode/activity-report.json',
68
68
  '.opencode/init.sh',
69
69
  '.xray/inference/',
70
+ '.xray/config.json',
71
+ '.xray/state/',
70
72
  'logs/',
71
73
  'backups/',
72
74
  '.ci-reports/',
@@ -3,8 +3,8 @@
3
3
  * release-gate.mjs — Single release checkpoint.
4
4
  *
5
5
  * Modes:
6
- * (default) build + test + consumer smoke — run once before commit
7
- * --verify-only git + reconcile + release docs + consumer smoke — after push, before tag
6
+ * (default) build + test + consumer smoke (fresh + upgrade merge)
7
+ * --verify-only git + reconcile + release docs + consumer smoke — after push, before publish
8
8
  * --skip-smoke skip consumer install smoke
9
9
  * --skip-docs skip release docs validation (not recommended for publish)
10
10
  *
@@ -47,10 +47,7 @@ function main() {
47
47
  step("1/4 Build", "npm run build");
48
48
  step("2/4 Tests", "npm test");
49
49
  if (!skipSmoke) {
50
- step("3/4 Consumer install smoke", "node scripts/node/consumer-install-smoke.mjs");
51
- }
52
- if (!skipDocs && process.argv.includes("--with-docs")) {
53
- step("4/4 Release docs", "node scripts/node/validate-release-docs.mjs");
50
+ step("3/4 Consumer install smoke (fresh + upgrade)", "node scripts/node/consumer-install-smoke.mjs");
54
51
  }
55
52
  }
56
53
 
@@ -4,16 +4,15 @@
4
4
  * Canonical 0xRay release script.
5
5
  *
6
6
  * 1. Bump version from npm registry baseline (reconcile-version)
7
- * 2. Release gate once (build + test + consumer smoke)
8
- * 3. Update CHANGELOG / README / AGENTS / docs (version-manager --artifacts-only)
7
+ * 2. Release artifacts (CHANGELOG / README / AGENTS / docs) — before gate so docs tests pass
8
+ * 3. Release gate (build + test + consumer smoke)
9
9
  * 4. Commit release artifacts → push
10
10
  * 5. Verify gate (reconcile + git + release docs + smoke)
11
- * 6. Tag → push tag → npm publish
11
+ * 6. npm publish (idempotent — skip if version already on registry)
12
+ * 7. Tag → push tag (only after successful publish)
12
13
  *
13
14
  * Usage:
14
15
  * npm run release:patch
15
- * npm run release:minor
16
- * npm run release:major
17
16
  * node scripts/node/release.mjs patch --dry-run
18
17
  */
19
18
 
@@ -44,6 +43,32 @@ function currentBranch() {
44
43
  return execSync("git rev-parse --abbrev-ref HEAD", { cwd: rootDir, encoding: "utf-8" }).trim();
45
44
  }
46
45
 
46
+ function npmVersionPublished(version) {
47
+ try {
48
+ const out = execSync(`npm view 0xray@${version} version`, {
49
+ cwd: rootDir,
50
+ encoding: "utf-8",
51
+ stdio: ["pipe", "pipe", "pipe"],
52
+ }).trim();
53
+ return out === version;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ function publishIdempotent(version) {
60
+ if (dryRun) {
61
+ console.log(` would: npm publish --access public (check 0xray@${version} first)`);
62
+ return;
63
+ }
64
+ if (npmVersionPublished(version)) {
65
+ console.log(`ℹ️ 0xray@${version} already on npm — skipping publish`);
66
+ return;
67
+ }
68
+ execSync("npm publish --access public", { cwd: rootDir, stdio: "inherit", encoding: "utf-8" });
69
+ console.log(`✅ Published 0xray@${version}`);
70
+ }
71
+
47
72
  async function main() {
48
73
  if (!["major", "minor", "patch"].includes(releaseType)) {
49
74
  console.error("Usage: node scripts/node/release.mjs [patch|minor|major] [--dry-run]");
@@ -70,14 +95,14 @@ async function main() {
70
95
  : readVersion();
71
96
  console.log(`📌 Release version: ${newVersion}`);
72
97
 
73
- // 2. Single gate before commit
74
- console.log("\n📦 Step 2: Release gate (build + test + smoke)");
75
- run("node scripts/node/release-gate.mjs", "release gate");
76
-
77
- // 3. Changelog + doc artifacts
78
- console.log("\n📦 Step 3: Release artifacts (CHANGELOG, README, AGENTS, docs)");
98
+ // 2. Artifacts BEFORE gate (docs vitest matches package.json version)
99
+ console.log("\n📦 Step 2: Release artifacts (CHANGELOG, README, AGENTS, docs)");
79
100
  run("node scripts/node/version-manager.mjs --artifacts-only", "release artifacts");
80
101
 
102
+ // 3. Gate
103
+ console.log("\n📦 Step 3: Release gate (build + test + smoke)");
104
+ run("node scripts/node/release-gate.mjs", "release gate");
105
+
81
106
  // 4. Commit + push
82
107
  console.log("\n📦 Step 4: Commit & push");
83
108
  const releaseFiles = getReleaseArtifactPaths();
@@ -97,24 +122,33 @@ async function main() {
97
122
  const branch = currentBranch();
98
123
  run(`git push origin ${branch}`, "git push");
99
124
 
100
- // 5. Post-push verify before tag
101
- console.log("\n📦 Step 5: Verify gate (reconcile + git + smoke)");
125
+ // 5. Post-push verify before publish
126
+ console.log("\n📦 Step 5: Verify gate (reconcile + git + docs + smoke)");
102
127
  run("node scripts/node/release-gate.mjs --verify-only", "verify gate");
103
128
 
104
- // 6. Tag
105
- console.log("\n📦 Step 6: Tag");
129
+ // 6. Publish (idempotent)
130
+ console.log("\n📦 Step 6: npm publish");
131
+ publishIdempotent(newVersion);
132
+
133
+ // 7. Tag after publish
134
+ console.log("\n📦 Step 7: Tag");
106
135
  if (!dryRun) {
107
- execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: rootDir, stdio: "inherit" });
108
- console.log(`✅ Created tag v${newVersion}`);
136
+ try {
137
+ execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: rootDir, stdio: "inherit" });
138
+ console.log(`✅ Created tag v${newVersion}`);
139
+ } catch (e) {
140
+ const msg = String(e.stderr || e.message || "");
141
+ if (msg.includes("already exists")) {
142
+ console.log(`ℹ️ Tag v${newVersion} already exists`);
143
+ } else {
144
+ throw e;
145
+ }
146
+ }
109
147
  } else {
110
148
  console.log(` would: git tag -a v${newVersion}`);
111
149
  }
112
150
  run(`git push origin v${newVersion}`, "push tag");
113
151
 
114
- // 7. Publish
115
- console.log("\n📦 Step 7: npm publish");
116
- run("npm publish --access public", "npm publish");
117
-
118
152
  console.log("\n╔════════════════════════════════════════════════════════╗");
119
153
  console.log("║ ✅ Release Complete! ║");
120
154
  console.log("╚════════════════════════════════════════════════════════╝");
@@ -125,7 +159,7 @@ async function main() {
125
159
  fs.mkdirSync(path.join(rootDir, "tweets"), { recursive: true });
126
160
  fs.writeFileSync(
127
161
  tweetPath,
128
- `🎉 0xRay v${newVersion} is LIVE - {THEME}!\n...\n\`\`\`\nnpm install 0xray@latest\n\`\`\`\n`,
162
+ `🎉 0xRay v${newVersion} is LIVE - consumer-safe upgrades!\n...\n\`\`\`\nnpm install 0xray@latest\n\`\`\`\n`,
129
163
  );
130
164
  console.log(`📝 Tweet template: tweets/v${newVersion}.md`);
131
165
  }