0xray 4.0.15 → 4.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.15",
3
+ "version": "4.0.17",
4
4
  "description": "0xRay suit for Grok, Hermes, OpenClaw, OpenCode: mill+inspect, Codex, 7 MCP servers. Costume 45/42 is opt-in foundry, not the default wear.",
5
5
  "author": {
6
6
  "name": "0xRay AI",
package/.opencode/init.sh CHANGED
@@ -35,8 +35,15 @@ XRAY_VERSION=$(node -e "console.log(require('$FRAMEWORK_ROOT/package.json').vers
35
35
  # Dedup guard — prevent duplicate runs during startup
36
36
  # Uses a TTL lockfile (10s window) since OpenCode may trigger config hook
37
37
  # from multiple plugin copies in quick succession
38
- # Key by PROJECT_ROOT (md5) so all invocations in the same project share one lock
39
- LOCK_KEY=$(echo -n "$PROJECT_ROOT" | md5 | cut -c1-16)
38
+ # Key by PROJECT_ROOT so all invocations in the same project share one lock.
39
+ # md5 is macOS; Linux has md5sum. Do not fail the boot on a leftover Mac tool.
40
+ if command -v md5 >/dev/null 2>&1; then
41
+ LOCK_KEY=$(echo -n "$PROJECT_ROOT" | md5 | cut -c1-16)
42
+ elif command -v md5sum >/dev/null 2>&1; then
43
+ LOCK_KEY=$(echo -n "$PROJECT_ROOT" | md5sum | awk '{print $1}' | cut -c1-16)
44
+ else
45
+ LOCK_KEY=$(echo -n "$PROJECT_ROOT" | cksum | awk '{print $1}')
46
+ fi
40
47
  LOCK_FILE="/tmp/xray-init-${LOCK_KEY}.lock"
41
48
  LOCK_TTL=10
42
49
  if [ -f "$LOCK_FILE" ]; then
@@ -123,8 +130,11 @@ else
123
130
  PLUGIN_STATUS="❌"
124
131
  fi
125
132
 
126
- # Framework config check
127
- if [ ! -f "$PROJECT_ROOT/.opencode/enforcer-config.json" ]; then
133
+ # Framework config check — consumer wear is .xray/codex.json, not leftover enforcer-config.json
134
+ if [ ! -f "$PROJECT_ROOT/.opencode/enforcer-config.json" ] \
135
+ && [ ! -f "$PROJECT_ROOT/.xray/codex.json" ] \
136
+ && [ ! -f "$PROJECT_ROOT/node_modules/0xray/.xray/codex.json" ] \
137
+ && [ ! -f "$PROJECT_ROOT/.mcp.json" ]; then
128
138
  echo -e "${PURPLE}// ❌ Framework configuration not found //${NC}"
129
139
  exit 1
130
140
  fi
@@ -67,12 +67,13 @@ The seven rules are the engine. The cadence is the time signature. Disk SSOT: `g
67
67
  - **Peer test:** another instance with only worn 0xRay continues the ticket. No command stream.
68
68
  - **Wear first:** Read `.xray/state/STATION.md`. Same cloud. Same critic (resume only). Dummy Reads Station.
69
69
  - **Dummy tests `SKILLS.md`:** a suited dummy proves the worn catalog. Same-sess bodies keep context and hold a Station card.
70
- - **Ship track:** always branch + worktree/aside + PR. Docs as you go. Builds. Rollback = close the PR.
71
- - **Publish:** spawn the OTP browser, watch success, poll `npm view` until the new version is live. Do not move on early. Railway same law.
72
- - **Live tick, not pacer:** `/loop` runs work now. Keep workstreams moving. Do not idle-wait 12 minutes.
70
+ - **Ship track:** always branch + worktree/aside + PR. Docs as you go. Builds. Rollback = close the PR. **Subject review. Fix n ship.** After critic PASS, review dest/domain, close leftovers, then mill-gate D. PASS is not ship. **Groover is not Repertoire.**
71
+ - **Publish:** stay logged in (do not `npm logout`). Every time run `npm publish --access public` in the live TTY. Paste the clickable `auth/cli` URL. No Enter, no VM browser, no subagent, no 6-digit code in chat. Poll `npm view` until live. Then prove **fresh** and **upgrade** registry installs. `npx 0xray validate` is the wear check (not leftover init.sh). `npm view` is not an install.
72
+ - **Peer boot:** four-line dispatch. They Read Station + this cadence and install without a command novel. Not a twin critic.
73
+ - **Live tick, not pacer:** `/loop` runs work now. Keep workstreams moving. Do not idle-wait 12 minutes. **Clean ticks every cycle** — unsubscribe then resubscribe so the prompt matches metal. Name-dedupe does not rewrite. A stale prompt (waiting URL after live, Hold after strip) is dirty: rewrite, do not act. **If the card and WAVEBOARD are idle, unsubscribe and do not resubscribe.**
73
74
  - **Board clock:** PR events + resume the same critic. Not two idle pacers. Merge after PASS+CI (gate A). Publish only after mill-gate D + `foundry gate`.
74
75
  - **Dispatch:** ticket + PR URLs + “Wear, then review.” The critic Reads `SEATS.md` Reviewer. Do not paste a command novel. When a law is spoken twice, encode it this wake.
75
- - **Close:** loop until Station is done. Compact: Station survives the cut. Repertoire keeps names.
76
+ - **Close:** loop until Station is done. Compact: Station survives the cut. Overlay dest names are OP-PROC — reload `repertoire-working.json` `opProcNames`, not Station.
76
77
  - **Board:** critic Strict comments; forge implements; lead stays main thread.
77
78
  - **Codex 69:** rewire this skill and existing ops. Do not add a new `SKILL.md` to hold the cadence.
78
79
  - **Ship matrix:** `LEAD-CADENCE.md` Decision matrix — A PR+CI · B pack · C `release:docs-check` · D `foundry release --i-mean-it`. Green CI is not ship. Spend / credentials stay Ask-first.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.15",
3
+ "version": "4.0.17",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",
package/dist/CHANGELOG.md CHANGED
@@ -17,14 +17,70 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
17
17
  - **Dispatch encode** — lead cards ticket + PR URLs + “Wear, then review.” Critic OP-PROC lives on `SEATS.md` Reviewer. When a law is spoken twice, write it to disk this wake. No new skill.
18
18
  - **Ship decision matrix** — lead-owned. A PR+CI · B pack (`assert-packed-dist-cli` + consumer smoke) · C `npm run release:docs-check` · D `npx @0xray/foundry release --i-mean-it`. Green CI is not ship. Spend / credentials stay Ask-first. Host Auto Review may still card npm/Railway; that is not the OS decision.
19
19
  - **Metamorphosis** — an LLM becomes OP-PROC through in-context worn seats (same critic, same dummy, four-line dispatch). Spawning more bots is the old skin.
20
+ - **Publish OTP latch** — stay logged in (do not `npm logout`). Every time run `npm publish --access public` in the live TTY (no `--auth-type=web`, no `--otp=` from chat, no `tee`). Paste the clickable `auth/cli` URL. Do not ask for a 6-digit authenticator code in chat. Do not press Enter. Do not hand publish to a browser subagent. Poll `npm view` until the version is live. Then prove **fresh** and **upgrade** registry installs. `npm view` is not an install.
21
+ - **`npx 0xray validate` cleansed** — no longer shells leftover `.opencode/init.sh` (macOS `md5` + `enforcer-config.json`). Wear check: `REQUIRED_PACK_PATHS`, Cursor hooks, mill+inspect, repertoire. `init.sh` still boots OpenCode but accepts Linux `md5sum` and consumer `.xray/codex.json`.
22
+ - **Swamp reflection (new arch)** — `docs/reflections/swamp-new-arch-reflection-2026-09-20.md`. Card, organ, hook. Lying metal. Live tarball lag.
23
+ - **Clean ticks every cycle** — `/loop` prompt is rewritten at the end of every live tick. `subscribe_timer` name-dedupe does not update the prompt. Unsubscribe then resubscribe. A tick that contradicts metal is dirty: rewrite, do not act. Not on Station.
24
+ - **Idle `/loop` stop** — if Station `Working` and the WAVEBOARD (open ship PR, live OTP, unfinished close) are idle, unsubscribe and do not resubscribe. A parked mill is not an active wave.
25
+ - **Subject review. Fix n ship.** After board PASS, review the worn dest / domain (not only CI), close leftovers, then mill-gate D. PASS is not ship. On `LEAD-CADENCE.md` Ship track + `SEATS.md` Reviewer. Not on Station.
26
+ - **OP-PROC reload after compact** — overlay dest names *are* OP-PROC. Heat writes them to `.xray/state/repertoire-working.json` `opProcNames`. Station stays a ticket (count + four working matches). Not a new skill.
27
+ - **Groover is not Repertoire.** Groover was a broken experiment of the organ. Do not grow dest from Groover field as if that is the KB. On `LEAD-CADENCE.md` Heads from tails. Not on Station.
20
28
 
21
29
  ### 🔄 Changes
30
+ - **Durable Hold npm** — heat + `cursorBootNeedsRefresh` + `npx 0xray validate` drop/fail `Hold npm` on Station. Identity stays. Mill-gate holds the ship latch. Not a new skill.
22
31
  - **Hangar catalog** — mill hangar docs use Clearing `GET /v1/catalog` as the directory (listed hangars: Groover DID + pin, plus solar + live shop), not a hardcoded extract/witness/pin route table. Groover MCP `list_hangars` reads that catalog. 0xray stays 4.0.15.
23
32
  - **foundry-plant/0** — fasten is a protocol any mill implements, not a 0xray-owned catalog of plant kinds. Builtin plant is mill+inspect only. `foundry.json` `"plant": "@0xray/blip"` (alias `"blip"`) fastens whatever that mill package declares. Organs (looker, vibe, mixer) live in the mill; adding one is a mill publish, not an xray catalog PR. Nested mill `@0xray/foundry@0.1.12`. CLI shims `foundry blip|sound` still exist on foundry 0.1.11+. 0xray stays 4.0.15.
24
33
  - **Blip/sound mills moved to `@0xray/blip`** — 4.44s drawers (render, kapow, vibe, wow-pass listen/cel, sound bed/mixer, looker) live in the blip product mill. `@0xray/foundry@0.1.11` shims `foundry blip|sound` including `blip look`. Railway plant consumes the product mill, not a hand-vendored copy. 0xray stays 4.0.15.
25
34
 
26
35
  ---
27
36
 
37
+ ## [4.0.17] - 2026-09-22
38
+
39
+ ### 🔄 Changes
40
+
41
+ ### ✨ Features
42
+ - feat: write session-*.json from Cursor heat when HEAD moves (363186a02)
43
+ - feat: persist overlay OP-PROC on repertoire-working after compact heat (8c55793e8)
44
+
45
+ ### 📚 Documentation
46
+ - docs: ASCII gather/store/retrieve trees for Station memory (d8f2f3b89)
47
+ - docs: Station is always the pickup memory after compact (bec431c61)
48
+ - docs: Groover is not Repertoire — do not grow dest from Groover field (203147d19)
49
+ - docs: Subject review. Fix n ship. — dest review then leftover then D (ad4ffa7f6)
50
+ - docs: stop /loop when the card and WAVEBOARD are idle (60f0fd53f)
51
+ - docs: clean ticks every cycle — rewrite /loop prompt, do not act on stale metal (e71a19559)
52
+
53
+ ### 🧪 Tests
54
+ - test: match Compact cut wording on Station-as-memory (7f9a4f091)
55
+
56
+ ### 🔎 Other Changes
57
+ - Merge main — include weekday-mirror #97 in 4.0.17 tarball (86fdec7ce)
58
+ - Merge pull request #97 from 0xRayAI/cursor/weekday-mirror-dist-voice-9b4d (d0853aad1)
59
+ - docs(grok-bot): weekday mirror for Dist reply voice and CoS wake hygiene (f91233b59)
60
+ - Merge branch 'cursor/clean-ticks-fbe0' — clean ticks every cycle (12dd06362)
61
+ - Merge branch 'cursor/leftover-validate-d-fbe0' — release 4.0.16 live, leftover validate gone (8fa037212)
62
+
63
+ ---
64
+
65
+ ## [4.0.16] - 2026-09-20
66
+
67
+ ### 🔄 Changes
68
+
69
+ ### 🐛 Bug Fixes
70
+ - fix: type validate pack paths — no untyped mjs import in src/cli (ff936b276)
71
+ - fix: cleanse npx 0xray validate — wear check, not leftover init.sh (741cca410)
72
+
73
+ ### 📚 Documentation
74
+ - docs: new-arch reflection after wading part of the swamp (12d1fff69)
75
+ - docs: persist peer-boot seat bc-49e17122 after four-line wear (5c8be98bb)
76
+ - docs: encode registry install as live-verify after npm view (bd32bd1c4)
77
+ - docs: encode npm CLI OTP latch — lead publishes, no subagent (d600d12a2)
78
+
79
+ ### 🔎 Other Changes
80
+ - Merge branch 'cursor/npm-cli-otp-fbe0' — OTP latch, two-path live-verify, cleanse leftover validate (568797952)
81
+
82
+ ---
83
+
28
84
  ## [4.0.15] - 2026-09-17
29
85
 
30
86
  ### ✨ Features
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Consumer wear check — not leftover OpenCode init.sh.
3
+ *
4
+ * `npx 0xray validate` used to shell `.opencode/init.sh` (macOS `md5`,
5
+ * `.opencode/enforcer-config.json`). That is factory boot theater, not wear.
6
+ */
7
+ /** Same paths as `REQUIRED_PACK_PATHS` in assert-packed-dist-cli.mjs — typed here so src/cli does not import untyped .mjs. */
8
+ export declare const VALIDATE_PACK_PATHS: readonly string[];
9
+ export declare const CONSUMER_WEAR_PATHS: readonly ["AGENTS.md", ".mcp.json", ".xray/codex.json", ".xray/features.json", ".cursor/hooks.json"];
10
+ export declare const MILL_PLANT_SKILLS: readonly ["mill", "inspect"];
11
+ export type ValidateCheck = {
12
+ id: string;
13
+ ok: boolean;
14
+ detail: string;
15
+ };
16
+ export type ValidateReport = {
17
+ ok: boolean;
18
+ consumer: boolean;
19
+ packageRoot: string;
20
+ checks: ValidateCheck[];
21
+ };
22
+ export declare function resolveInstalledPackageRoot(cwd: string): string | null;
23
+ export declare function isConsumerInstall(cwd: string, packageRoot: string): boolean;
24
+ export declare function collectValidateReport(cwd: string): ValidateReport;
25
+ export declare function validateCommand(cwd?: string): Promise<ValidateReport>;
26
+ export default validateCommand;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Consumer wear check — not leftover OpenCode init.sh.
3
+ *
4
+ * `npx 0xray validate` used to shell `.opencode/init.sh` (macOS `md5`,
5
+ * `.opencode/enforcer-config.json`). That is factory boot theater, not wear.
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { frameworkLogger } from "../../core/framework-logger.js";
10
+ /** Same paths as `REQUIRED_PACK_PATHS` in assert-packed-dist-cli.mjs — typed here so src/cli does not import untyped .mjs. */
11
+ export const VALIDATE_PACK_PATHS = [
12
+ "dist/cli/index.js",
13
+ "dist/mcps/orchestrator.server.js",
14
+ "dist/integrations/grok/hooks/session-start.js",
15
+ "dist/integrations/cursor/hooks/hooks.json",
16
+ "dist/integrations/cursor/hooks/pre-tool-use.js",
17
+ "dist/integrations/cursor/hooks/pre-compact.js",
18
+ "dist/integrations/cursor/hooks/after-file-edit.js",
19
+ "dist/integrations/cursor/hooks/cursor-hook-utils.js",
20
+ "dist/integrations/cursor/hooks/cursor-usage-receipt.js",
21
+ ];
22
+ export const CONSUMER_WEAR_PATHS = [
23
+ "AGENTS.md",
24
+ ".mcp.json",
25
+ ".xray/codex.json",
26
+ ".xray/features.json",
27
+ ".cursor/hooks.json",
28
+ ];
29
+ export const MILL_PLANT_SKILLS = ["mill", "inspect"];
30
+ export function resolveInstalledPackageRoot(cwd) {
31
+ const nested = join(cwd, "node_modules", "0xray");
32
+ if (existsSync(join(nested, "package.json"))) {
33
+ return nested;
34
+ }
35
+ const pkgPath = join(cwd, "package.json");
36
+ if (!existsSync(pkgPath)) {
37
+ return null;
38
+ }
39
+ try {
40
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
41
+ if (pkg.name === "0xray") {
42
+ return cwd;
43
+ }
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ return null;
49
+ }
50
+ export function isConsumerInstall(cwd, packageRoot) {
51
+ return packageRoot !== cwd;
52
+ }
53
+ function checkPath(root, rel) {
54
+ return existsSync(join(root, rel));
55
+ }
56
+ function stationDurableHoldsNpm(cwd) {
57
+ const dest = join(cwd, ".xray", "state", "STATION.md");
58
+ if (!existsSync(dest))
59
+ return false;
60
+ try {
61
+ return /\bhold\s+npm\b/i.test(readFileSync(dest, "utf8"));
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ export function collectValidateReport(cwd) {
68
+ const packageRoot = resolveInstalledPackageRoot(cwd);
69
+ const checks = [];
70
+ if (!packageRoot) {
71
+ return {
72
+ ok: false,
73
+ consumer: false,
74
+ packageRoot: cwd,
75
+ checks: [
76
+ {
77
+ id: "package",
78
+ ok: false,
79
+ detail: "0xray package not found in cwd or node_modules/0xray",
80
+ },
81
+ ],
82
+ };
83
+ }
84
+ const consumer = isConsumerInstall(cwd, packageRoot);
85
+ checks.push({
86
+ id: "package",
87
+ ok: true,
88
+ detail: consumer ? `consumer node_modules/0xray at ${packageRoot}` : `exo dogfood at ${packageRoot}`,
89
+ });
90
+ const missingPack = VALIDATE_PACK_PATHS.filter((p) => !checkPath(packageRoot, p));
91
+ checks.push({
92
+ id: "pack-paths",
93
+ ok: missingPack.length === 0,
94
+ detail: missingPack.length === 0
95
+ ? `REQUIRED_PACK_PATHS ${VALIDATE_PACK_PATHS.length}/${VALIDATE_PACK_PATHS.length}`
96
+ : `missing ${missingPack.join(", ")}`,
97
+ });
98
+ if (consumer) {
99
+ const missingWear = CONSUMER_WEAR_PATHS.filter((p) => !checkPath(cwd, p));
100
+ checks.push({
101
+ id: "consumer-wear",
102
+ ok: missingWear.length === 0,
103
+ detail: missingWear.length === 0
104
+ ? "AGENTS.md · .mcp.json · .xray · .cursor/hooks.json"
105
+ : `missing ${missingWear.join(", ")}`,
106
+ });
107
+ const millRoot = join(cwd, ".opencode", "skills");
108
+ const missingMill = MILL_PLANT_SKILLS.filter((name) => !existsSync(join(millRoot, name, "SKILL.md")));
109
+ checks.push({
110
+ id: "mill-plant",
111
+ ok: missingMill.length === 0,
112
+ detail: missingMill.length === 0
113
+ ? "mill + inspect fastened"
114
+ : `missing plant ${missingMill.join(", ")}`,
115
+ });
116
+ const repertoire = existsSync(join(cwd, "node_modules", "@0xray", "repertoire", "package.json")) ||
117
+ existsSync(join(packageRoot, "vendor", "@0xray", "repertoire", "package.json"));
118
+ checks.push({
119
+ id: "repertoire",
120
+ ok: repertoire,
121
+ detail: repertoire ? "repertoire organ on disk" : "missing @0xray/repertoire",
122
+ });
123
+ }
124
+ const stationPath = join(cwd, ".xray", "state", "STATION.md");
125
+ if (existsSync(stationPath)) {
126
+ const staleHold = stationDurableHoldsNpm(cwd);
127
+ checks.push({
128
+ id: "durable-hold",
129
+ ok: !staleHold,
130
+ detail: staleHold
131
+ ? "STATION Durable still says Hold npm — heat should have stripped it"
132
+ : "Durable has no Hold npm",
133
+ });
134
+ }
135
+ return {
136
+ ok: checks.every((c) => c.ok),
137
+ consumer,
138
+ packageRoot,
139
+ checks,
140
+ };
141
+ }
142
+ export async function validateCommand(cwd = process.cwd()) {
143
+ frameworkLogger.log("cli", "validate-start", "info", { cwd });
144
+ const report = collectValidateReport(cwd);
145
+ for (const check of report.checks) {
146
+ const mark = check.ok ? "✅" : "❌";
147
+ console.log(`${mark} ${check.id}: ${check.detail}`);
148
+ }
149
+ if (report.ok) {
150
+ console.log("");
151
+ console.log("🎉 Wear checks passed. This is not leftover init.sh.");
152
+ frameworkLogger.log("cli", "validate-success", "info", { consumer: report.consumer });
153
+ }
154
+ else {
155
+ console.log("");
156
+ console.log("❌ Validation failed. Pack/wear miss — not a Mac init.sh leftover.");
157
+ frameworkLogger.log("cli", "validate-fail", "error", {
158
+ consumer: report.consumer,
159
+ failed: report.checks.filter((c) => !c.ok).map((c) => c.id),
160
+ });
161
+ }
162
+ return report;
163
+ }
164
+ export default validateCommand;
package/dist/cli/index.js CHANGED
@@ -99,23 +99,15 @@ program
99
99
  });
100
100
  program
101
101
  .command("validate")
102
- .description("Validate xray framework installation")
102
+ .description("Validate consumer wear (pack paths, Cursor hooks, mill plant) — not leftover init.sh")
103
103
  .action(async () => {
104
104
  console.log("🔬 xray CLI: Validating installation...");
105
105
  try {
106
- // Run the init.sh script to validate
107
- const initScript = join(packageRoot, ".opencode", "init.sh");
108
- const fs = await import("fs");
109
- if (!fs.existsSync(initScript)) {
110
- console.error("❌ Validation failed: init script not found:", initScript);
106
+ const { validateCommand } = await import("./commands/validate.js");
107
+ const report = await validateCommand(process.cwd());
108
+ if (!report.ok) {
111
109
  process.exit(1);
112
110
  }
113
- // SECURITY: Validate script path before execution
114
- validateScriptPath(initScript, "init script");
115
- execSync(`bash "${initScript}"`, {
116
- stdio: "inherit",
117
- cwd: process.cwd(),
118
- });
119
111
  }
120
112
  catch (error) {
121
113
  console.error("❌ Validation failed:", error instanceof Error ? error.message : String(error));
@@ -4,6 +4,7 @@
4
4
  * sessionStart is unavailable on managed cloud; first preToolUse / afterFileEdit boots.
5
5
  */
6
6
 
7
+ import { execFileSync } from 'node:child_process';
7
8
  import fs from 'fs';
8
9
  import path from 'path';
9
10
  import {
@@ -25,6 +26,7 @@ import { appendHookActivity } from '../../grok/hooks/grok-hook-activity.js';
25
26
  import {
26
27
  buildRepertoireResume,
27
28
  readGitBrief,
29
+ stationDurableHoldsNpm,
28
30
  } from '../../hooks/station-hook-runtime.mjs';
29
31
 
30
32
  export const CURSOR_HOST = 'cursor';
@@ -147,15 +149,118 @@ export function cursorBootNeedsRefresh(existing, root) {
147
149
  if (liveGit && liveGit.head && bootHead !== liveGit.head) return true;
148
150
  const liveResume = buildRepertoireResume(root);
149
151
  if (liveResume && existing.repertoireResume && liveResume !== existing.repertoireResume) return true;
152
+ if (stationDurableHoldsNpm(root)) return true;
150
153
  return false;
151
154
  }
152
155
 
156
+ function sessionCaptureStampPath(root) {
157
+ return path.join(root, '.xray', 'state', 'session-capture-stamp.json');
158
+ }
159
+
160
+ function readSessionCaptureConfig(root) {
161
+ let cfg = {};
162
+ try {
163
+ const raw = JSON.parse(fs.readFileSync(path.join(root, '.xray', 'features.json'), 'utf8'));
164
+ cfg = raw.inference_session_capture || {};
165
+ } catch {
166
+ cfg = {};
167
+ }
168
+ return {
169
+ enabled: cfg.enabled === true,
170
+ minCommits: typeof cfg.min_commits === 'number' && cfg.min_commits > 0 ? cfg.min_commits : 3,
171
+ lookback: typeof cfg.lookback_commits === 'number' && cfg.lookback_commits > 0 ? cfg.lookback_commits : 20,
172
+ };
173
+ }
174
+
175
+ function readSessionCaptureStamp(root) {
176
+ try {
177
+ return JSON.parse(fs.readFileSync(sessionCaptureStampPath(root), 'utf8'));
178
+ } catch {
179
+ return null;
180
+ }
181
+ }
182
+
183
+ function gitCommitSubjects(root, args) {
184
+ try {
185
+ const out = execFileSync('git', ['log', '--format=%h||%s', '--no-merges', ...args], {
186
+ cwd: root,
187
+ encoding: 'utf8',
188
+ timeout: 5000,
189
+ stdio: ['ignore', 'pipe', 'pipe'],
190
+ });
191
+ return String(out)
192
+ .split('\n')
193
+ .map((line) => line.trim())
194
+ .filter(Boolean)
195
+ .map((line) => {
196
+ const idx = line.indexOf('||');
197
+ return {
198
+ hash: idx === -1 ? line : line.slice(0, idx),
199
+ message: idx === -1 ? '' : line.slice(idx + 2),
200
+ };
201
+ });
202
+ } catch {
203
+ return [];
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Cursor never runs the git post-commit hook. When HEAD moves, write the same
209
+ * session-*.json the kernel already knows how to ingest. Dedup per HEAD.
210
+ */
211
+ export function maybeCaptureSessionOnHeadMove(root) {
212
+ const cfg = readSessionCaptureConfig(root);
213
+ if (!cfg.enabled) return null;
214
+ const git = readGitBrief(root);
215
+ if (!git || !git.head) return null;
216
+ const stamp = readSessionCaptureStamp(root);
217
+ if (stamp && stamp.head === git.head) return null;
218
+
219
+ const commits = stamp && stamp.head
220
+ ? gitCommitSubjects(root, [`${stamp.head}..HEAD`])
221
+ : gitCommitSubjects(root, ['-n', String(cfg.lookback)]);
222
+ if (commits.length < cfg.minCommits) return null;
223
+
224
+ const sessionId = `session-${new Date().toISOString().slice(0, 10)}-${git.head}`;
225
+ const approaches = commits.map((c) => c.message).filter(Boolean);
226
+ const session = {
227
+ sessionId,
228
+ timestamp: new Date().toISOString(),
229
+ span: {
230
+ from: stamp && stamp.head ? stamp.head : `HEAD~${cfg.lookback}`,
231
+ to: git.head,
232
+ },
233
+ problems: [],
234
+ approaches,
235
+ wrongTurns: [],
236
+ solutions: [],
237
+ patterns: [],
238
+ matched_primitives: [],
239
+ metrics: { commits: commits.length },
240
+ };
241
+
242
+ const outDir = path.join(root, 'docs', 'inference');
243
+ fs.mkdirSync(outDir, { recursive: true });
244
+ const filePath = path.join(outDir, `session-${sessionId.replace(/^session-/, '')}.json`);
245
+ fs.writeFileSync(filePath, `${JSON.stringify(session, null, 2)}\n`);
246
+ fs.writeFileSync(path.join(outDir, 'latest-session.json'), `${JSON.stringify(session, null, 2)}\n`);
247
+ fs.mkdirSync(path.dirname(sessionCaptureStampPath(root)), { recursive: true });
248
+ fs.writeFileSync(
249
+ sessionCaptureStampPath(root),
250
+ `${JSON.stringify({ head: git.head, sessionId, path: filePath, updatedAt: session.timestamp }, null, 2)}\n`,
251
+ );
252
+ return filePath;
253
+ }
254
+
153
255
  export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-use-boot', extra = {}) {
154
256
  const bootPath = sessionBootPath(root);
155
257
  if (fs.existsSync(bootPath)) {
156
258
  try {
157
259
  const existing = JSON.parse(fs.readFileSync(bootPath, 'utf8'));
158
- if (!cursorBootNeedsRefresh(existing, root)) return bootPath;
260
+ if (!cursorBootNeedsRefresh(existing, root)) {
261
+ maybeCaptureSessionOnHeadMove(root);
262
+ return bootPath;
263
+ }
159
264
  } catch {
160
265
  /* rewrite corrupt or host-mismatched boot */
161
266
  }
@@ -164,7 +269,9 @@ export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-us
164
269
  host: CURSOR_HOST,
165
270
  ...extra,
166
271
  });
167
- return writeSessionBoot(root, payload) || bootPath;
272
+ const written = writeSessionBoot(root, payload) || bootPath;
273
+ maybeCaptureSessionOnHeadMove(root);
274
+ return written;
168
275
  }
169
276
 
170
277
  export function writeCursorPrecompactReceipt(root, fields) {
@@ -203,6 +203,25 @@ function stationSafeSignals(names) {
203
203
  return out;
204
204
  }
205
205
 
206
+ /** Overlay + factory names on dest. OP-PROC lives here — not on Station.md. */
207
+ function readOpProcNames(root) {
208
+ const dest = join(root, ".xray", "state", "repertoire", "curated_signals.json");
209
+ if (!existsSync(dest)) return [];
210
+ try {
211
+ const data = JSON.parse(readFileSync(dest, "utf8"));
212
+ if (!Array.isArray(data.signals)) return [];
213
+ const names = [];
214
+ for (const signal of data.signals) {
215
+ const name = String(signal && signal.name ? signal.name : "").trim();
216
+ if (!name || name.toLowerCase().startsWith("bedrock-")) continue;
217
+ names.push(name);
218
+ }
219
+ return names;
220
+ } catch {
221
+ return [];
222
+ }
223
+ }
224
+
206
225
  function persistRepertoireWorking(root, snapshot) {
207
226
  try {
208
227
  mkdirSync(join(root, ".xray", "state"), { recursive: true });
@@ -340,6 +359,8 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
340
359
  repertoireResume,
341
360
  };
342
361
  if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals;
362
+ const opProcNames = readOpProcNames(root);
363
+ if (opProcNames.length) workingSnapshot.opProcNames = opProcNames;
343
364
  const working = persistRepertoireWorking(root, workingSnapshot);
344
365
  if (isCompactHook(extra) && matchedSignals.length) {
345
366
  ingestCompactFeedbackSync(
@@ -383,6 +404,23 @@ function isDurableStationHeading(line) {
383
404
  return /^##\s+(durable|seed)\b/i.test(String(line || "").trim());
384
405
  }
385
406
 
407
+ /** Durable is identity. Hold/ship lives in mill-gate — not on the card. */
408
+ function isHoldNpmLine(line) {
409
+ return /\bhold\s+npm\b/i.test(String(line || "").trim());
410
+ }
411
+
412
+ function stripHoldNpm(line) {
413
+ return String(line || "")
414
+ .replace(/\s*hold\s+npm\.?/gi, "")
415
+ .replace(/[ \t]+$/g, "");
416
+ }
417
+
418
+ function stationDurableHoldsNpm(root) {
419
+ return readExistingStationMarkdown(root)
420
+ .split(/\r?\n/)
421
+ .some((line) => isHoldNpmLine(line));
422
+ }
423
+
386
424
  function isStockStationLine(line) {
387
425
  const trimmed = String(line || "").trim();
388
426
  if (!trimmed) return true;
@@ -411,12 +449,14 @@ function extractPreservedStationLines(existing) {
411
449
  if (otherHeading || isStockStationLine(line)) {
412
450
  inDurable = false;
413
451
  } else {
414
- preserved.push(line);
452
+ const durableLine = stripHoldNpm(line);
453
+ if (durableLine.trim()) preserved.push(durableLine);
415
454
  continue;
416
455
  }
417
456
  }
418
457
  if (isStockStationLine(line)) continue;
419
- preserved.push(line);
458
+ const kept = stripHoldNpm(line);
459
+ if (kept.trim()) preserved.push(kept);
420
460
  }
421
461
  while (preserved.length && !preserved[0].trim()) preserved.shift();
422
462
  while (preserved.length && !preserved[preserved.length - 1].trim()) preserved.pop();
@@ -500,10 +540,13 @@ module.exports = {
500
540
  isLeftoverMemoryRoutingOff,
501
541
  persistRepertoireWorking,
502
542
  readRepertoireWorking,
543
+ readOpProcNames,
503
544
  formatWorkingLine,
504
545
  applyStationHeat,
505
546
  extractPreservedStationLines,
506
547
  mergeStationMarkdown,
507
548
  formatStationMarkdown,
508
549
  writeStationMarkdown,
550
+ isHoldNpmLine,
551
+ stationDurableHoldsNpm,
509
552
  };
@@ -18,9 +18,12 @@ export const readPlanLine = impl.readPlanLine;
18
18
  export const buildRepertoireResume = impl.buildRepertoireResume;
19
19
  export const persistRepertoireWorking = impl.persistRepertoireWorking;
20
20
  export const readRepertoireWorking = impl.readRepertoireWorking;
21
+ export const readOpProcNames = impl.readOpProcNames;
21
22
  export const formatWorkingLine = impl.formatWorkingLine;
22
23
  export const applyStationHeat = impl.applyStationHeat;
23
24
  export const extractPreservedStationLines = impl.extractPreservedStationLines;
24
25
  export const mergeStationMarkdown = impl.mergeStationMarkdown;
25
26
  export const formatStationMarkdown = impl.formatStationMarkdown;
26
27
  export const writeStationMarkdown = impl.writeStationMarkdown;
28
+ export const isHoldNpmLine = impl.isHoldNpmLine;
29
+ export const stationDurableHoldsNpm = impl.stationDurableHoldsNpm;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.15",
3
+ "version": "4.0.17",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -67,12 +67,13 @@ The seven rules are the engine. The cadence is the time signature. Disk SSOT: `g
67
67
  - **Peer test:** another instance with only worn 0xRay continues the ticket. No command stream.
68
68
  - **Wear first:** Read `.xray/state/STATION.md`. Same cloud. Same critic (resume only). Dummy Reads Station.
69
69
  - **Dummy tests `SKILLS.md`:** a suited dummy proves the worn catalog. Same-sess bodies keep context and hold a Station card.
70
- - **Ship track:** always branch + worktree/aside + PR. Docs as you go. Builds. Rollback = close the PR.
71
- - **Publish:** spawn the OTP browser, watch success, poll `npm view` until the new version is live. Do not move on early. Railway same law.
72
- - **Live tick, not pacer:** `/loop` runs work now. Keep workstreams moving. Do not idle-wait 12 minutes.
70
+ - **Ship track:** always branch + worktree/aside + PR. Docs as you go. Builds. Rollback = close the PR. **Subject review. Fix n ship.** After critic PASS, review dest/domain, close leftovers, then mill-gate D. PASS is not ship. **Groover is not Repertoire.**
71
+ - **Publish:** stay logged in (do not `npm logout`). Every time run `npm publish --access public` in the live TTY. Paste the clickable `auth/cli` URL. No Enter, no VM browser, no subagent, no 6-digit code in chat. Poll `npm view` until live. Then prove **fresh** and **upgrade** registry installs. `npx 0xray validate` is the wear check (not leftover init.sh). `npm view` is not an install.
72
+ - **Peer boot:** four-line dispatch. They Read Station + this cadence and install without a command novel. Not a twin critic.
73
+ - **Live tick, not pacer:** `/loop` runs work now. Keep workstreams moving. Do not idle-wait 12 minutes. **Clean ticks every cycle** — unsubscribe then resubscribe so the prompt matches metal. Name-dedupe does not rewrite. A stale prompt (waiting URL after live, Hold after strip) is dirty: rewrite, do not act. **If the card and WAVEBOARD are idle, unsubscribe and do not resubscribe.**
73
74
  - **Board clock:** PR events + resume the same critic. Not two idle pacers. Merge after PASS+CI (gate A). Publish only after mill-gate D + `foundry gate`.
74
75
  - **Dispatch:** ticket + PR URLs + “Wear, then review.” The critic Reads `SEATS.md` Reviewer. Do not paste a command novel. When a law is spoken twice, encode it this wake.
75
- - **Close:** loop until Station is done. Compact: Station survives the cut. Repertoire keeps names.
76
+ - **Close:** loop until Station is done. Compact: Station survives the cut. Overlay dest names are OP-PROC — reload `repertoire-working.json` `opProcNames`, not Station.
76
77
  - **Board:** critic Strict comments; forge implements; lead stays main thread.
77
78
  - **Codex 69:** rewire this skill and existing ops. Do not add a new `SKILL.md` to hold the cadence.
78
79
  - **Ship matrix:** `LEAD-CADENCE.md` Decision matrix — A PR+CI · B pack · C `release:docs-check` · D `foundry release --i-mean-it`. Green CI is not ship. Spend / credentials stay Ask-first.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.15",
3
+ "version": "4.0.17",
4
4
  "description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.15",
3
+ "version": "4.0.17",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -67,12 +67,13 @@ The seven rules are the engine. The cadence is the time signature. Disk SSOT: `g
67
67
  - **Peer test:** another instance with only worn 0xRay continues the ticket. No command stream.
68
68
  - **Wear first:** Read `.xray/state/STATION.md`. Same cloud. Same critic (resume only). Dummy Reads Station.
69
69
  - **Dummy tests `SKILLS.md`:** a suited dummy proves the worn catalog. Same-sess bodies keep context and hold a Station card.
70
- - **Ship track:** always branch + worktree/aside + PR. Docs as you go. Builds. Rollback = close the PR.
71
- - **Publish:** spawn the OTP browser, watch success, poll `npm view` until the new version is live. Do not move on early. Railway same law.
72
- - **Live tick, not pacer:** `/loop` runs work now. Keep workstreams moving. Do not idle-wait 12 minutes.
70
+ - **Ship track:** always branch + worktree/aside + PR. Docs as you go. Builds. Rollback = close the PR. **Subject review. Fix n ship.** After critic PASS, review dest/domain, close leftovers, then mill-gate D. PASS is not ship. **Groover is not Repertoire.**
71
+ - **Publish:** stay logged in (do not `npm logout`). Every time run `npm publish --access public` in the live TTY. Paste the clickable `auth/cli` URL. No Enter, no VM browser, no subagent, no 6-digit code in chat. Poll `npm view` until live. Then prove **fresh** and **upgrade** registry installs. `npx 0xray validate` is the wear check (not leftover init.sh). `npm view` is not an install.
72
+ - **Peer boot:** four-line dispatch. They Read Station + this cadence and install without a command novel. Not a twin critic.
73
+ - **Live tick, not pacer:** `/loop` runs work now. Keep workstreams moving. Do not idle-wait 12 minutes. **Clean ticks every cycle** — unsubscribe then resubscribe so the prompt matches metal. Name-dedupe does not rewrite. A stale prompt (waiting URL after live, Hold after strip) is dirty: rewrite, do not act. **If the card and WAVEBOARD are idle, unsubscribe and do not resubscribe.**
73
74
  - **Board clock:** PR events + resume the same critic. Not two idle pacers. Merge after PASS+CI (gate A). Publish only after mill-gate D + `foundry gate`.
74
75
  - **Dispatch:** ticket + PR URLs + “Wear, then review.” The critic Reads `SEATS.md` Reviewer. Do not paste a command novel. When a law is spoken twice, encode it this wake.
75
- - **Close:** loop until Station is done. Compact: Station survives the cut. Repertoire keeps names.
76
+ - **Close:** loop until Station is done. Compact: Station survives the cut. Overlay dest names are OP-PROC — reload `repertoire-working.json` `opProcNames`, not Station.
76
77
  - **Board:** critic Strict comments; forge implements; lead stays main thread.
77
78
  - **Codex 69:** rewire this skill and existing ops. Do not add a new `SKILL.md` to hold the cadence.
78
79
  - **Ship matrix:** `LEAD-CADENCE.md` Decision matrix — A PR+CI · B pack · C `release:docs-check` · D `foundry release --i-mean-it`. Green CI is not ship. Spend / credentials stay Ask-first.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.15",
3
+ "version": "4.0.17",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",