0xray 4.0.19 → 4.0.21

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 (46) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/orchestrator/SKILL.md +1 -1
  3. package/.xray/features.json +1 -1
  4. package/AGENTS-consumer.md +4 -4
  5. package/AGENTS.md +4 -4
  6. package/README.md +14 -14
  7. package/SKILLS.md +3 -3
  8. package/dist/AGENTS.md +4 -4
  9. package/dist/CHANGELOG.md +27 -6
  10. package/dist/README.md +14 -14
  11. package/dist/SKILLS.md +3 -3
  12. package/dist/cli/commands/validate.d.ts +1 -1
  13. package/dist/cli/commands/validate.js +7 -1
  14. package/dist/integrations/cursor/README.md +6 -5
  15. package/dist/integrations/cursor/hooks/after-file-edit.js +6 -3
  16. package/dist/integrations/cursor/hooks/after-file-edit.sh +3 -0
  17. package/dist/integrations/cursor/hooks/before-read-file.sh +3 -0
  18. package/dist/integrations/cursor/hooks/before-shell-execution.sh +3 -0
  19. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +121 -20
  20. package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +3 -0
  21. package/dist/integrations/cursor/hooks/hooks.json +13 -3
  22. package/dist/integrations/cursor/hooks/pre-compact.js +39 -22
  23. package/dist/integrations/cursor/hooks/pre-compact.sh +3 -0
  24. package/dist/integrations/cursor/hooks/pre-tool-use.js +20 -15
  25. package/dist/integrations/cursor/hooks/pre-tool-use.sh +3 -0
  26. package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +63 -0
  27. package/dist/integrations/grok/hooks/grok-hook-utils.js +6 -3
  28. package/dist/integrations/hermes-agent/bridge.mjs +14 -0
  29. package/dist/integrations/hooks/station-hook-runtime.cjs +416 -37
  30. package/dist/integrations/hooks/station-hook-runtime.mjs +15 -0
  31. package/dist/integrations/hooks/station-memory-ingest.mjs +152 -4
  32. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  33. package/dist/nucleus/delegation-gate.js +10 -11
  34. package/dist/nucleus/suit-temperament.d.ts +0 -5
  35. package/dist/nucleus/suit-temperament.js +18 -2
  36. package/dist/skills/orchestrator/SKILL.md +1 -1
  37. package/llms.txt +1 -1
  38. package/package.json +2 -2
  39. package/scripts/foundry/README.md +2 -2
  40. package/scripts/foundry/assert-packed-dist-cli.mjs +4 -0
  41. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  42. package/scripts/foundry/validate-release-docs.mjs +44 -0
  43. package/scripts/node/install-bridges.cjs +256 -10
  44. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  45. package/src/skills/orchestrator/SKILL.md +1 -1
  46. package/xray/features.json +1 -1
@@ -23,13 +23,12 @@ import {
23
23
  } from '../../hooks/delegation-gate-runtime.mjs';
24
24
  import { appendHookActivity } from '../../grok/hooks/grok-hook-activity.js';
25
25
  import {
26
- buildRepertoireResume,
26
+ heatLiveMemory,
27
27
  maybeCaptureSessionOnHeadMove,
28
- readGitBrief,
29
- stationDurableHoldsNpm,
28
+ stationBootNeedsRefresh,
30
29
  } from '../../hooks/station-hook-runtime.mjs';
31
30
 
32
- export { maybeCaptureSessionOnHeadMove };
31
+ export { heatLiveMemory, maybeCaptureSessionOnHeadMove };
33
32
 
34
33
  export const CURSOR_HOST = 'cursor';
35
34
  export const EVENT_CLASS_HOST = 'cursor-host-precompact';
@@ -77,6 +76,112 @@ export function cursorWorkspaceRoot(event = {}) {
77
76
  );
78
77
  }
79
78
 
79
+ function addHeatRoot(out, seen, value) {
80
+ if (!value) return;
81
+ const resolved = path.resolve(String(value));
82
+ if (seen.has(resolved)) return;
83
+ seen.add(resolved);
84
+ out.push(resolved);
85
+ }
86
+
87
+ export function isCursorWorkspaceWrapper(root) {
88
+ if (!root) return false;
89
+ const resolved = path.resolve(String(root));
90
+ return (
91
+ fs.existsSync(path.join(resolved, 'repos', 'xray')) ||
92
+ fs.existsSync(path.join(resolved, 'repos', 'repertoire'))
93
+ );
94
+ }
95
+
96
+ export function heatRootHasCard(root) {
97
+ if (!root) return false;
98
+ const resolved = path.resolve(String(root));
99
+ return (
100
+ fs.existsSync(path.join(resolved, '.xray', 'state', 'STATION.md')) ||
101
+ fs.existsSync(path.join(resolved, '.xray', 'features.json'))
102
+ );
103
+ }
104
+
105
+ export function isHeatRootWritable(root) {
106
+ if (!root) return false;
107
+ const resolved = path.resolve(String(root));
108
+ try {
109
+ const probe = path.join(resolved, '.xray', 'state');
110
+ fs.mkdirSync(probe, { recursive: true });
111
+ fs.accessSync(probe, fs.constants.W_OK);
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ export function shouldHeatRoot(root) {
119
+ if (!root) return false;
120
+ const resolved = path.resolve(String(root));
121
+ if (isCursorWorkspaceWrapper(resolved)) return false;
122
+ if (heatRootHasCard(resolved)) return isHeatRootWritable(resolved);
123
+ return isHeatRootWritable(resolved);
124
+ }
125
+
126
+ export function millRootFromToolPath(filePath) {
127
+ if (!filePath) return null;
128
+ const resolved = path.resolve(String(filePath));
129
+ const marker = `${path.sep}repos${path.sep}`;
130
+ const idx = resolved.indexOf(marker);
131
+ if (idx === -1) return null;
132
+ const after = resolved.slice(idx + marker.length);
133
+ const repo = after.split(path.sep)[0];
134
+ if (!repo) return null;
135
+ return resolved.slice(0, idx + marker.length + repo.length);
136
+ }
137
+
138
+ function addMillCardsUnderWrapper(out, seen, wrapper) {
139
+ if (!wrapper || !isCursorWorkspaceWrapper(wrapper)) return;
140
+ const mill = path.join(path.resolve(String(wrapper)), 'repos', 'xray');
141
+ if (heatRootHasCard(mill) && isHeatRootWritable(mill)) addHeatRoot(out, seen, mill);
142
+ }
143
+
144
+ /** Every live context that already has a Station card — cwd plus mill when they differ. */
145
+ export function cursorHeatRoots(event = {}) {
146
+ const seen = new Set();
147
+ const out = [];
148
+ const candidates = [event.cwd, event.workspaceRoot];
149
+ const listed = event.workspace_roots || event.workspaceRoots;
150
+ if (Array.isArray(listed)) {
151
+ for (const row of listed) {
152
+ if (typeof row === 'string') candidates.push(row);
153
+ else if (row && typeof row === 'object' && row.path) candidates.push(row.path);
154
+ }
155
+ }
156
+ candidates.push(process.env.CURSOR_PROJECT_DIR);
157
+ for (const value of candidates) {
158
+ if (shouldHeatRoot(value)) addHeatRoot(out, seen, value);
159
+ addMillCardsUnderWrapper(out, seen, value);
160
+ }
161
+ const mill = process.env.XRAY_AI_PATH || process.env.XRAY_ROOT;
162
+ if (mill) {
163
+ const resolved = path.resolve(String(mill));
164
+ if (shouldHeatRoot(resolved)) addHeatRoot(out, seen, resolved);
165
+ }
166
+ const ctx = cursorToolContext(event);
167
+ for (const filePath of ctx.paths) {
168
+ const fromPath = millRootFromToolPath(filePath);
169
+ if (
170
+ fromPath &&
171
+ fs.existsSync(path.join(fromPath, '.xray', 'state', 'STATION.md')) &&
172
+ isHeatRootWritable(fromPath)
173
+ ) {
174
+ addHeatRoot(out, seen, fromPath);
175
+ }
176
+ }
177
+ if (out.length === 0) {
178
+ const fallback = cursorWorkspaceRoot(event);
179
+ if (shouldHeatRoot(fallback)) addHeatRoot(out, seen, fallback);
180
+ addMillCardsUnderWrapper(out, seen, fallback);
181
+ }
182
+ return out;
183
+ }
184
+
80
185
  export function cursorSessionId(event = {}) {
81
186
  return (
82
187
  event.conversation_id ||
@@ -88,6 +193,10 @@ export function cursorSessionId(event = {}) {
88
193
  );
89
194
  }
90
195
 
196
+ export function cursorGenerationId(event = {}) {
197
+ return event.generation_id || event.generationId || null;
198
+ }
199
+
91
200
  export function cursorToolContext(event = {}) {
92
201
  const toolName = event.tool_name || event.toolName || process.env.TOOL_NAME || 'unknown';
93
202
  const toolInput = event.tool_input ?? event.toolInput ?? {};
@@ -141,27 +250,21 @@ export function classifyPreCompactEvent(event = {}, argv = process.argv) {
141
250
  }
142
251
 
143
252
  export function cursorBootNeedsRefresh(existing, root) {
144
- if (!existing || typeof existing !== 'object') return true;
145
- if (existing.host !== CURSOR_HOST) return true;
146
- if (!existing.suit_profile) return true;
147
- if (existing.workspaceRoot && existing.workspaceRoot !== root) return true;
148
- if (!existing.stationLine) return true;
149
- const liveGit = readGitBrief(root);
150
- const bootHead = existing.git && existing.git.head ? String(existing.git.head) : '';
151
- if (liveGit && liveGit.head && bootHead !== liveGit.head) return true;
152
- const liveResume = buildRepertoireResume(root);
153
- if (liveResume && existing.repertoireResume && liveResume !== existing.repertoireResume) return true;
154
- if (stationDurableHoldsNpm(root)) return true;
155
- return false;
253
+ return stationBootNeedsRefresh(existing, root, CURSOR_HOST);
156
254
  }
157
255
 
158
256
  export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-use-boot', extra = {}) {
159
257
  const bootPath = sessionBootPath(root);
258
+ if (!shouldHeatRoot(root)) return bootPath;
160
259
  if (fs.existsSync(bootPath)) {
161
260
  try {
162
261
  const existing = JSON.parse(fs.readFileSync(bootPath, 'utf8'));
163
262
  if (!cursorBootNeedsRefresh(existing, root)) {
164
- maybeCaptureSessionOnHeadMove(root);
263
+ try {
264
+ heatLiveMemory(root);
265
+ } catch {
266
+ /* dest EACCES must not deny the host tool */
267
+ }
165
268
  return bootPath;
166
269
  }
167
270
  } catch {
@@ -172,9 +275,7 @@ export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-us
172
275
  host: CURSOR_HOST,
173
276
  ...extra,
174
277
  });
175
- const written = writeSessionBoot(root, payload) || bootPath;
176
- maybeCaptureSessionOnHeadMove(root);
177
- return written;
278
+ return writeSessionBoot(root, payload) || bootPath;
178
279
  }
179
280
 
180
281
  export function writeCursorPrecompactReceipt(root, fields) {
@@ -69,6 +69,9 @@ export function hostUsageFromPreCompactEvent(event = {}) {
69
69
  if (typeof event.windowCite === 'string' && event.windowCite) usage.windowCite = event.windowCite;
70
70
  if (typeof event.model === 'string' && event.model) usage.model = event.model;
71
71
  if (typeof event.bcId === 'string' && event.bcId) usage.bcId = event.bcId;
72
+ else if (typeof event.conversation_id === 'string' && event.conversation_id) {
73
+ usage.bcId = event.conversation_id;
74
+ }
72
75
  return usage;
73
76
  }
74
77
 
@@ -3,17 +3,27 @@
3
3
  "hooks": {
4
4
  "preToolUse": [
5
5
  {
6
- "command": "XRAY_AI_PATH=\"${XRAY_AI_PATH:-node_modules/0xray}\" node \"${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/cursor/hooks/pre-tool-use.js\""
6
+ "command": ".cursor/hooks/pre-tool-use.sh"
7
7
  }
8
8
  ],
9
9
  "preCompact": [
10
10
  {
11
- "command": "XRAY_AI_PATH=\"${XRAY_AI_PATH:-node_modules/0xray}\" node \"${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/cursor/hooks/pre-compact.js\""
11
+ "command": ".cursor/hooks/pre-compact.sh"
12
12
  }
13
13
  ],
14
14
  "afterFileEdit": [
15
15
  {
16
- "command": "XRAY_AI_PATH=\"${XRAY_AI_PATH:-node_modules/0xray}\" node \"${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/cursor/hooks/after-file-edit.js\""
16
+ "command": ".cursor/hooks/after-file-edit.sh"
17
+ }
18
+ ],
19
+ "beforeShellExecution": [
20
+ {
21
+ "command": ".cursor/hooks/before-shell-execution.sh"
22
+ }
23
+ ],
24
+ "beforeReadFile": [
25
+ {
26
+ "command": ".cursor/hooks/before-read-file.sh"
17
27
  }
18
28
  ]
19
29
  }
@@ -9,6 +9,8 @@ import {
9
9
  appendHookActivity,
10
10
  buildSessionBootPayload,
11
11
  classifyPreCompactEvent,
12
+ cursorGenerationId,
13
+ cursorHeatRoots,
12
14
  cursorSessionId,
13
15
  cursorWorkspaceRoot,
14
16
  readStdinJson,
@@ -39,34 +41,49 @@ async function main() {
39
41
  eventClass = classifyPreCompactEvent(event);
40
42
  const eventRoot = cursorWorkspaceRoot(event);
41
43
  const sessionId = cursorSessionId(event);
44
+ const generationId = cursorGenerationId(event);
42
45
  const intent = extractIntent(event);
43
- const payload = buildSessionBootPayload(eventRoot, '0xray/cursor-compact', {
44
- host: 'cursor',
45
- hookEvent: 'pre_compact',
46
- event_class: eventClass,
47
- sessionId,
48
- ...(intent ? { intent } : {}),
49
- });
50
- const bootPath = writeSessionBoot(eventRoot, payload);
51
- const receiptPath = writeCursorPrecompactReceipt(eventRoot, {
52
- event_class: eventClass,
53
- hookEvent: 'pre_compact',
54
- trigger: event.trigger || 'synthetic',
55
- sessionId,
56
- bootPath,
57
- timestamp: new Date().toISOString(),
58
- });
59
- const usagePath = writeCursorUsageReceipt(eventRoot, {
60
- eventClass,
61
- sessionId,
62
- usage: hostUsageFromPreCompactEvent(event),
63
- });
46
+ let bootPath = null;
47
+ let receiptPath = null;
48
+ let usagePath = null;
49
+ let stationLine = null;
50
+ for (const root of cursorHeatRoots(event)) {
51
+ const payload = buildSessionBootPayload(root, '0xray/cursor-compact', {
52
+ host: 'cursor',
53
+ hookEvent: 'pre_compact',
54
+ event_class: eventClass,
55
+ sessionId,
56
+ ...(intent ? { intent } : {}),
57
+ });
58
+ bootPath = writeSessionBoot(root, payload);
59
+ stationLine = payload.stationLine;
60
+ receiptPath = writeCursorPrecompactReceipt(root, {
61
+ event_class: eventClass,
62
+ hookEvent: 'pre_compact',
63
+ trigger: event.trigger || 'synthetic',
64
+ sessionId,
65
+ conversation_id: sessionId,
66
+ generation_id: generationId,
67
+ is_first_compaction:
68
+ typeof event.is_first_compaction === 'boolean' ? event.is_first_compaction : null,
69
+ message_count: typeof event.message_count === 'number' ? event.message_count : null,
70
+ messages_to_compact:
71
+ typeof event.messages_to_compact === 'number' ? event.messages_to_compact : null,
72
+ bootPath,
73
+ timestamp: new Date().toISOString(),
74
+ });
75
+ usagePath = writeCursorUsageReceipt(root, {
76
+ eventClass,
77
+ sessionId,
78
+ usage: hostUsageFromPreCompactEvent(event),
79
+ });
80
+ }
64
81
  appendHookActivity(eventRoot, 'cursor-pre-compact', 'station-written', 'success', {
65
82
  bootPath,
66
83
  receiptPath,
67
84
  usagePath,
68
85
  event_class: eventClass,
69
- stationLine: payload.stationLine,
86
+ stationLine,
70
87
  });
71
88
  console.log(
72
89
  JSON.stringify({
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ HERE="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
3
+ exec /bin/sh "${HERE}/xray-cloud-hook.sh" preCompact pre-compact.js
@@ -9,6 +9,7 @@ import {
9
9
  appendHookActivity,
10
10
  checkCodexPatterns,
11
11
  checkFullTestSuite,
12
+ cursorHeatRoots,
12
13
  cursorSessionId,
13
14
  cursorToolContext,
14
15
  cursorWorkspaceRoot,
@@ -47,57 +48,61 @@ async function main() {
47
48
  try {
48
49
  const event = await readStdinJson();
49
50
  const eventRoot = cursorWorkspaceRoot(event);
50
- ensureCursorSessionBoot(eventRoot, '0xray/cursor-pre-tool-use-boot', {
51
- sessionId: cursorSessionId(event),
52
- });
51
+ const sessionId = cursorSessionId(event);
52
+ const heatRoots = cursorHeatRoots(event);
53
+ for (const root of heatRoots) {
54
+ ensureCursorSessionBoot(root, '0xray/cursor-pre-tool-use-boot', { sessionId });
55
+ }
56
+ const gateRoot = heatRoots[0] || eventRoot;
53
57
 
54
- const features = loadFeatures(eventRoot);
55
- const gateFeatures = loadDelegationGateFeatures(eventRoot, 'cursor');
58
+ const features = loadFeatures(gateRoot);
59
+ const gateFeatures = loadDelegationGateFeatures(gateRoot, 'cursor');
56
60
  const ctx = cursorToolContext(event);
57
61
  toolName = ctx.toolName;
58
62
  const { content, cmd, toolInput } = ctx;
59
- const sessionId = cursorSessionId(event);
60
63
 
61
64
  const gateBlock = evaluatePreToolGate(toolName, toolInput, {
62
- projectRoot: eventRoot,
65
+ projectRoot: gateRoot,
63
66
  sessionId,
64
67
  features: gateFeatures,
65
68
  host: 'cursor',
66
69
  });
67
70
  if (!gateBlock.allow) {
68
- finish(eventRoot, 'deny', gateBlock.reason, gateBlock.hint, toolName, { gate: gateBlock.gate });
71
+ finish(gateRoot, 'deny', gateBlock.reason, gateBlock.hint, toolName, { gate: gateBlock.gate });
69
72
  }
70
73
 
71
74
  if (isWriteTool(toolName) && content) {
72
75
  const extraBlock = checkCodexPatterns(content, { terms: [11, 29] });
73
- if (extraBlock) finish(eventRoot, 'deny', extraBlock, null, toolName);
76
+ if (extraBlock) finish(gateRoot, 'deny', extraBlock, null, toolName);
74
77
  }
75
78
 
76
79
  if (isShellTool(toolName) && cmd) {
77
80
  const testHint = checkFullTestSuite(cmd, features);
78
- if (testHint) finish(eventRoot, 'allow', null, testHint, toolName);
81
+ if (testHint) finish(gateRoot, 'allow', null, testHint, toolName);
79
82
  }
80
83
 
81
84
  if (gateBlock.reason) {
82
- finish(eventRoot, 'allow', gateBlock.reason, gateBlock.hint, toolName, {
85
+ finish(gateRoot, 'allow', gateBlock.reason, gateBlock.hint, toolName, {
83
86
  gate: gateBlock.gate,
84
87
  warn: true,
85
88
  });
86
89
  }
87
90
 
88
- finish(eventRoot, 'allow', null, null, toolName);
91
+ finish(gateRoot, 'allow', null, null, toolName);
89
92
  } catch (err) {
90
93
  appendHookActivity(fallbackRoot, 'cursor-pre-tool-use', 'hook-error', 'error', {
91
94
  tool: toolName,
92
95
  error: err.message,
96
+ failOpen: true,
93
97
  });
98
+ // Dest EACCES on a Cloud workspace wrapper must not deny every tool.
94
99
  finish(
95
100
  fallbackRoot,
96
- 'deny',
97
- `preToolUse hook error — blocked for safety: ${err.message}`,
101
+ 'allow',
102
+ `preToolUse hook error — fail open: ${err.message}`,
98
103
  null,
99
104
  toolName,
100
- { gate: 'hook-error' },
105
+ { gate: 'hook-error-fail-open' },
101
106
  );
102
107
  }
103
108
  }
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ HERE="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
3
+ exec /bin/sh "${HERE}/xray-cloud-hook.sh" preToolUse pre-tool-use.js
@@ -0,0 +1,63 @@
1
+ #!/bin/sh
2
+ # Cloud-safe Cursor hook runner. Command in hooks.json must be a relative path.
3
+ # Do not put env-assignment in hooks.json — Cloud execs argv[0] without a shell.
4
+ set -eu
5
+
6
+ EVENT="${1:-preToolUse}"
7
+ JS="${2:-pre-tool-use.js}"
8
+ MILL=""
9
+ REL=""
10
+
11
+ pick_mill() {
12
+ mill="$1"
13
+ rel="$2"
14
+ if [ -f "${mill}/${rel}" ]; then
15
+ MILL="$mill"
16
+ REL="$rel"
17
+ return 0
18
+ fi
19
+ return 1
20
+ }
21
+
22
+ if [ -n "${XRAY_AI_PATH:-}" ]; then
23
+ pick_mill "${XRAY_AI_PATH}" "src/integrations/cursor/hooks/${JS}" || \
24
+ pick_mill "${XRAY_AI_PATH}" "dist/integrations/cursor/hooks/${JS}" || true
25
+ fi
26
+ if [ -z "$MILL" ]; then
27
+ pick_mill "$(pwd)" "src/integrations/cursor/hooks/${JS}" || \
28
+ pick_mill "$(pwd)/../xray" "src/integrations/cursor/hooks/${JS}" || \
29
+ pick_mill "$(pwd)/repos/xray" "src/integrations/cursor/hooks/${JS}" || \
30
+ pick_mill "${CURSOR_PROJECT_DIR:-}/repos/xray" "src/integrations/cursor/hooks/${JS}" || \
31
+ pick_mill "$(pwd)/node_modules/0xray" "dist/integrations/cursor/hooks/${JS}" || \
32
+ pick_mill "$(pwd)/node_modules/0xray" "src/integrations/cursor/hooks/${JS}" || true
33
+ fi
34
+
35
+ if [ -n "$MILL" ] && [ -d "$MILL" ]; then
36
+ MILL="$(cd "$MILL" && pwd)"
37
+ fi
38
+
39
+ NODE_BIN="$(command -v node 2>/dev/null || true)"
40
+
41
+ log_invoke() {
42
+ dest="$1"
43
+ mkdir -p "${dest}/.xray/state" 2>/dev/null || true
44
+ {
45
+ printf 'ts=%s event=%s cwd=%s mill=%s node=%s\n' \
46
+ "$(date -Iseconds)" "$EVENT" "$(pwd)" "${MILL:-MISSING}" "${NODE_BIN:-MISSING}"
47
+ } >> "${dest}/.xray/state/cursor-hook-invoke.log" 2>/dev/null || true
48
+ }
49
+
50
+ log_invoke "$(pwd)"
51
+ if [ -n "$MILL" ]; then
52
+ log_invoke "$MILL"
53
+ fi
54
+
55
+ if [ -z "$NODE_BIN" ] || [ -z "$MILL" ] || [ ! -f "${MILL}/${REL}" ]; then
56
+ printf '{"permission":"allow","user_message":"xray-cloud-hook: mill missing — fail open"}\n'
57
+ exit 0
58
+ fi
59
+
60
+ export XRAY_HOOK_EVENT="$EVENT"
61
+ export XRAY_AI_PATH="$MILL"
62
+ export XRAY_ROOT="$MILL"
63
+ exec "$NODE_BIN" "${MILL}/${REL}"
@@ -25,6 +25,7 @@ import {
25
25
  applyStationHeat,
26
26
  buildRepertoireResume as stationRepertoireResume,
27
27
  readExistingBoot,
28
+ retainCompactFields,
28
29
  writeStationMarkdown,
29
30
  } from '../../hooks/station-hook-runtime.mjs';
30
31
 
@@ -285,8 +286,8 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
285
286
  const frontier = gateFeatures.ceremony === 'lite';
286
287
  const existing = readExistingBoot(root);
287
288
  const heat = applyStationHeat(root, extra.host || 'grok', extra, existing);
289
+ const compactHold = retainCompactFields(existing, extra);
288
290
  return {
289
- hook: source,
290
291
  lead_dev_mode: features.lead_dev_mode,
291
292
  no_new_surface: features.no_new_surface,
292
293
  host: extra.host || 'grok',
@@ -306,10 +307,12 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
306
307
  ...(conferPending ? { conferPending: true, conferTrigger: 'analyze-complexity at synthesis checkpoint' } : {}),
307
308
  ...(userAsideBoot ?? {}),
308
309
  sessionId,
309
- timestamp: new Date().toISOString(),
310
- source,
311
310
  ...extra,
312
311
  ...heat,
312
+ ...compactHold,
313
+ hook: source,
314
+ source,
315
+ timestamp: new Date().toISOString(),
313
316
  };
314
317
  }
315
318
 
@@ -468,6 +468,20 @@ async function handlePreProcess(input, projectRoot, logDir) {
468
468
  const { tool, args } = input;
469
469
  const startTime = Date.now();
470
470
 
471
+ try {
472
+ const gate = await import('../hooks/delegation-gate-runtime.mjs');
473
+ if (typeof gate.maybeHeatHostStation === 'function') {
474
+ gate.maybeHeatHostStation(projectRoot, 'hermes', {
475
+ source: '0xray/hermes-pre-process',
476
+ sessionId: input.sessionId || 'hermes',
477
+ hookEvent: 'pre_tool_use',
478
+ ...(input.intent ? { intent: input.intent } : {}),
479
+ });
480
+ }
481
+ } catch {
482
+ /* live memory is best-effort — quality gate still runs */
483
+ }
484
+
471
485
  logToolEvent(logDir, "start", tool, args);
472
486
  logToActivity(logDir, `pre-process: tool=${tool}`);
473
487