0xray 4.0.42 → 4.1.0

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 (93) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.plugin/plugin.json +1 -1
  3. package/.xray/features.json +5 -5
  4. package/AGENTS-consumer.md +8 -6
  5. package/AGENTS.md +17 -8
  6. package/LICENSE +1 -1
  7. package/README.md +15 -9
  8. package/SKILLS.md +3 -1
  9. package/dist/AGENTS.md +17 -8
  10. package/dist/CHANGELOG.md +83 -11
  11. package/dist/LICENSE +1 -1
  12. package/dist/README.md +15 -9
  13. package/dist/SKILLS.md +3 -1
  14. package/dist/cli/commands/plugin-commands.d.ts +4 -0
  15. package/dist/cli/commands/plugin-commands.js +84 -41
  16. package/dist/core/config-loader.js +4 -4
  17. package/dist/core/features-config.d.ts +1 -1
  18. package/dist/core/features-config.js +3 -3
  19. package/dist/integrations/cursor/hooks/hooks.json +6 -3
  20. package/dist/integrations/cursor/hooks/pre-tool-use.js +100 -37
  21. package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +9 -1
  22. package/dist/integrations/grok/hooks/pre-tool-use.js +2 -58
  23. package/dist/integrations/hermes-agent/bridge.mjs +14 -1
  24. package/dist/integrations/hooks/delegation-gate-runtime.mjs +3 -2
  25. package/dist/integrations/hooks/goggles-mcp.mjs +35 -17
  26. package/dist/integrations/hooks/goggles-pipeline.mjs +46 -1
  27. package/dist/integrations/hooks/goggles-planes.json +7 -6
  28. package/dist/integrations/hooks/pre-tool-speak.mjs +76 -0
  29. package/dist/integrations/openclaw/hooks/pre-tool-gate-runtime.mjs +21 -4
  30. package/dist/integrations/openclaw/plugin/xray-pre-tool/index.js +17 -2
  31. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  32. package/dist/monitoring/{nudge-watchdog.d.ts → nudge-monitor.d.ts} +14 -5
  33. package/dist/monitoring/{nudge-watchdog.js → nudge-monitor.js} +42 -17
  34. package/dist/plugin/xray-codex-injection.js +18 -0
  35. package/dist/processors/implementations/nudge-processor.d.ts +1 -1
  36. package/dist/processors/implementations/nudge-processor.js +4 -4
  37. package/docs-site/docs/plates/activity-log.md +1 -1
  38. package/docs-site/docs/plates/bifrost.md +48 -0
  39. package/docs-site/docs/plates/boot.md +2 -2
  40. package/docs-site/docs/plates/centrifuge.md +1 -1
  41. package/docs-site/docs/plates/cursor-compact.md +1 -1
  42. package/docs-site/docs/plates/glossary.md +7 -3
  43. package/docs-site/docs/plates/goggles.md +13 -2
  44. package/docs-site/docs/plates/governance.md +1 -1
  45. package/docs-site/docs/plates/grok-compact.md +1 -1
  46. package/docs-site/docs/plates/grokbot.md +3 -1
  47. package/docs-site/docs/plates/host-pack.md +1 -1
  48. package/docs-site/docs/plates/house.md +1 -1
  49. package/docs-site/docs/plates/index.md +4 -2
  50. package/docs-site/docs/plates/inference-files.md +1 -1
  51. package/docs-site/docs/plates/kept-line.md +1 -1
  52. package/docs-site/docs/plates/kits.md +1 -1
  53. package/docs-site/docs/plates/lens-gate.md +3 -1
  54. package/docs-site/docs/plates/lens-page.md +1 -1
  55. package/docs-site/docs/plates/memory-recall.md +1 -1
  56. package/docs-site/docs/plates/notes-page.md +2 -2
  57. package/docs-site/docs/plates/orchestration.md +1 -1
  58. package/docs-site/docs/plates/package-face.md +1 -1
  59. package/docs-site/docs/plates/payload-heat.md +1 -1
  60. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  61. package/docs-site/docs/plates/pre-tool.md +1 -1
  62. package/docs-site/docs/plates/processor.md +1 -1
  63. package/docs-site/docs/plates/record-map.md +1 -1
  64. package/docs-site/docs/plates/reflection-page.md +1 -1
  65. package/docs-site/docs/plates/reporting.md +1 -1
  66. package/docs-site/docs/plates/review.md +1 -1
  67. package/docs-site/docs/plates/routing.md +1 -1
  68. package/docs-site/docs/plates/session-capture.md +1 -1
  69. package/docs-site/docs/plates/site-manual.md +1 -1
  70. package/docs-site/docs/plates/stamp-plate.md +1 -1
  71. package/docs-site/docs/plates/station-card.md +1 -1
  72. package/docs-site/docs/plates/station-heat.md +1 -1
  73. package/docs-site/docs/plates/suit-organs.md +1 -1
  74. package/docs-site/docs/plates/suit-settings.md +1 -1
  75. package/docs-site/docs/plates/suit-wear.md +1 -1
  76. package/docs-site/docs/plates/suit.md +3 -3
  77. package/docs-site/docs/plates/trail-state.md +1 -1
  78. package/docs-site/docs/plates/work-fresh.md +1 -1
  79. package/docs-site/docs/plates/write-home.md +1 -1
  80. package/llms.txt +14 -6
  81. package/package.json +3 -3
  82. package/scripts/foundry/README.md +2 -2
  83. package/scripts/foundry/release-gate.mjs +111 -15
  84. package/scripts/foundry/release.mjs +8 -0
  85. package/scripts/mjs/verify-pipeline-facets.mjs +1 -1
  86. package/scripts/node/install-bridges.cjs +28 -12
  87. package/scripts/node/pack-tmp-suit-proof.mjs +14 -6
  88. package/src/integrations/openclaw/plugin/xray-pre-tool/index.js +17 -2
  89. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  90. package/vendor/@0xray/repertoire/README.md +1 -1
  91. package/xray/agents_template.md +2 -2
  92. package/xray/features.json +5 -5
  93. package/docs-site/docs/plates/live-mesh.md +0 -41
@@ -152,24 +152,25 @@ export async function pluginStatusCommand(pluginName) {
152
152
  console.log(` Type: ${m.type}`);
153
153
  console.log(` Description: ${m.description}`);
154
154
  console.log(` License: ${m.license || "N/A"}`);
155
- if (m.capabilities) {
155
+ if (Array.isArray(m.capabilities)) {
156
156
  console.log("\n Capabilities:");
157
157
  for (const cap of m.capabilities) {
158
- console.log(` - ${cap}`);
158
+ console.log(` - ${String(cap)}`);
159
159
  }
160
160
  }
161
- if (m.tools) {
161
+ if (Array.isArray(m.tools)) {
162
162
  console.log("\n Tools:");
163
163
  for (const tool of m.tools) {
164
- console.log(` - ${tool.name}: ${tool.description}`);
164
+ const row = tool;
165
+ console.log(` - ${String(row.name)}: ${String(row.description)}`);
165
166
  }
166
167
  }
167
- if (m.runtime) {
168
+ if (m.runtime && typeof m.runtime === "object" && !Array.isArray(m.runtime)) {
168
169
  const runtime = m.runtime;
169
170
  console.log("\n Runtime:");
170
- console.log(` Command: ${runtime.command}`);
171
- if (runtime.args) {
172
- console.log(` Args: ${runtime.args.join(" ")}`);
171
+ console.log(` Command: ${String(runtime.command)}`);
172
+ if (Array.isArray(runtime.args)) {
173
+ console.log(` Args: ${runtime.args.map((arg) => String(arg)).join(" ")}`);
173
174
  }
174
175
  }
175
176
  console.log("\n" + "═".repeat(50) + "\n");
@@ -195,51 +196,93 @@ function validateManifest(manifest) {
195
196
  }
196
197
  }
197
198
  }
198
- function parseYaml(content) {
199
- const lines = content.split("\n");
200
- const result = {};
201
- let currentKey = "";
202
- const stack = [
203
- { indent: -1, obj: result },
204
- ];
205
- for (const line of lines) {
199
+ function parseScalar(raw) {
200
+ const value = raw.trim();
201
+ if ((value.startsWith('"') && value.endsWith('"')) ||
202
+ (value.startsWith("'") && value.endsWith("'"))) {
203
+ return value.slice(1, -1);
204
+ }
205
+ if (value.startsWith("[") && value.endsWith("]")) {
206
+ const inner = value.slice(1, -1).trim();
207
+ if (!inner)
208
+ return [];
209
+ return inner.split(",").map((part) => parseScalar(part));
210
+ }
211
+ if (value === "true")
212
+ return true;
213
+ if (value === "false")
214
+ return false;
215
+ if (/^-?\d+$/.test(value))
216
+ return Number(value);
217
+ return value;
218
+ }
219
+ /** Subset of YAML used by plugin.yaml: maps, scalar lists, and lists of maps. */
220
+ export function parseYaml(content) {
221
+ const root = {};
222
+ const stack = [{ indent: -1, container: root }];
223
+ for (const line of content.split("\n")) {
206
224
  const trimmed = line.trim();
207
225
  if (!trimmed || trimmed.startsWith("#"))
208
226
  continue;
209
227
  const indent = line.search(/\S/);
210
- const isListItem = trimmed.startsWith("- ");
211
- if (isListItem) {
212
- const value = trimmed.slice(2).trim();
228
+ while (stack.length > 1) {
213
229
  const top = stack[stack.length - 1];
214
- if (top && currentKey && !Array.isArray(top.obj[currentKey])) {
215
- top.obj[currentKey] = [];
216
- }
217
- if (top && currentKey) {
218
- top.obj[currentKey].push(value);
219
- }
220
- continue;
230
+ if (!top || indent > top.indent)
231
+ break;
232
+ stack.pop();
221
233
  }
222
- const colonIndex = trimmed.indexOf(":");
223
- if (colonIndex > 0) {
224
- const key = trimmed.slice(0, colonIndex).trim();
225
- const value = trimmed.slice(colonIndex + 1).trim();
226
- const top = stack[stack.length - 1];
227
- while (top && stack.length > 1 && indent <= top.indent) {
234
+ if (trimmed.startsWith("- ")) {
235
+ let owner = stack[stack.length - 1];
236
+ if (owner &&
237
+ !Array.isArray(owner.container) &&
238
+ Object.keys(owner.container).length === 0 &&
239
+ stack.length > 1) {
240
+ const holder = stack[stack.length - 2];
241
+ const emptyIndent = owner.indent;
228
242
  stack.pop();
243
+ if (holder && !Array.isArray(holder.container) && holder.key) {
244
+ const list = [];
245
+ holder.container[holder.key] = list;
246
+ stack.push({ indent: emptyIndent, container: list });
247
+ }
248
+ owner = stack[stack.length - 1];
229
249
  }
230
- const current = stack[stack.length - 1];
231
- if (!current)
250
+ if (!owner || !Array.isArray(owner.container))
232
251
  continue;
233
- const parent = current.obj;
234
- if (value) {
235
- parent[key] = value;
252
+ const rest = trimmed.slice(2).trim();
253
+ const colon = rest.indexOf(":");
254
+ const quoted = rest.startsWith('"') || rest.startsWith("'");
255
+ if (!quoted && colon > 0) {
256
+ const item = {};
257
+ const key = rest.slice(0, colon).trim();
258
+ const value = rest.slice(colon + 1).trim();
259
+ item[key] = value ? parseScalar(value) : {};
260
+ owner.container.push(item);
261
+ stack.push({ indent, container: item, key });
236
262
  }
237
263
  else {
238
- parent[key] = {};
239
- stack.push({ indent, obj: parent[key] });
264
+ owner.container.push(parseScalar(rest));
240
265
  }
241
- currentKey = key;
266
+ continue;
267
+ }
268
+ const colonIndex = trimmed.indexOf(":");
269
+ if (colonIndex <= 0)
270
+ continue;
271
+ const key = trimmed.slice(0, colonIndex).trim();
272
+ const value = trimmed.slice(colonIndex + 1).trim();
273
+ const current = stack[stack.length - 1];
274
+ if (!current || Array.isArray(current.container))
275
+ continue;
276
+ if (value) {
277
+ current.container[key] = parseScalar(value);
278
+ current.key = key;
279
+ }
280
+ else {
281
+ const child = {};
282
+ current.container[key] = child;
283
+ current.key = key;
284
+ stack.push({ indent, container: child });
242
285
  }
243
286
  }
244
- return result;
287
+ return root;
245
288
  }
@@ -93,9 +93,9 @@ export class XrayConfigLoader {
93
93
  }
94
94
  const cfg = config;
95
95
  return {
96
- enabled: typeof cfg.enabled === 'boolean' ? cfg.enabled : true,
96
+ enabled: typeof cfg.enabled === 'boolean' ? cfg.enabled : false,
97
97
  interval_minutes: Math.max(5, Math.min(1440, typeof cfg.interval_minutes === 'number' ? cfg.interval_minutes : 60)),
98
- auto_schedule: typeof cfg.auto_schedule === 'boolean' ? cfg.auto_schedule : true,
98
+ auto_schedule: typeof cfg.auto_schedule === 'boolean' ? cfg.auto_schedule : false,
99
99
  include_health_assessment: typeof cfg.include_health_assessment === 'boolean' ? cfg.include_health_assessment : true,
100
100
  include_agent_activities: typeof cfg.include_agent_activities === 'boolean' ? cfg.include_agent_activities : true,
101
101
  include_pipeline_operations: typeof cfg.include_pipeline_operations === 'boolean' ? cfg.include_pipeline_operations : true,
@@ -123,9 +123,9 @@ export class XrayConfigLoader {
123
123
  session_persistence: true,
124
124
  },
125
125
  autonomous_reporting: {
126
- enabled: true,
126
+ enabled: false,
127
127
  interval_minutes: 60,
128
- auto_schedule: true,
128
+ auto_schedule: false,
129
129
  include_health_assessment: true,
130
130
  include_agent_activities: true,
131
131
  include_pipeline_operations: true,
@@ -406,7 +406,7 @@ export interface FeaturesConfig {
406
406
  enabled?: boolean;
407
407
  };
408
408
  memory_routing?: MemoryRoutingConfig;
409
- /** P2.2 — lightweight Grok write-tool postprocessor marker */
409
+ /** Marker is opt-in and the shipped default is off. */
410
410
  grok_postprocessor_light?: boolean;
411
411
  /** P2 — git post-commit session inference capture */
412
412
  inference_session_capture?: {
@@ -342,9 +342,9 @@ export class FeaturesConfigLoader {
342
342
  no_new_surface: true,
343
343
  },
344
344
  autonomous_reporting: {
345
- enabled: true,
345
+ enabled: false,
346
346
  interval_minutes: 60,
347
- auto_schedule: true,
347
+ auto_schedule: false,
348
348
  include_health_assessment: true,
349
349
  include_agent_activities: true,
350
350
  include_pipeline_operations: true,
@@ -410,7 +410,7 @@ export class FeaturesConfigLoader {
410
410
  max_cache_size_mb: 50,
411
411
  },
412
412
  inference: {
413
- description: "Self-improvement cycle that grades a new named session into repertoire",
413
+ description: "The cycle runs from the CLI command inference:run and the session wake does not start it.",
414
414
  enabled: true,
415
415
  workflow_dir: ".xray/inference",
416
416
  reports_dir: ".xray/reports",
@@ -3,7 +3,8 @@
3
3
  "hooks": {
4
4
  "preToolUse": [
5
5
  {
6
- "command": ".cursor/hooks/pre-tool-use.sh"
6
+ "command": ".cursor/hooks/pre-tool-use.sh",
7
+ "failClosed": true
7
8
  }
8
9
  ],
9
10
  "preCompact": [
@@ -18,12 +19,14 @@
18
19
  ],
19
20
  "beforeShellExecution": [
20
21
  {
21
- "command": ".cursor/hooks/before-shell-execution.sh"
22
+ "command": ".cursor/hooks/before-shell-execution.sh",
23
+ "failClosed": true
22
24
  }
23
25
  ],
24
26
  "beforeReadFile": [
25
27
  {
26
- "command": ".cursor/hooks/before-read-file.sh"
28
+ "command": ".cursor/hooks/before-read-file.sh",
29
+ "failClosed": true
27
30
  }
28
31
  ]
29
32
  }
@@ -5,23 +5,40 @@
5
5
  * Does not rely on sessionStart (unavailable on managed cloud).
6
6
  */
7
7
 
8
- import {
9
- appendHookActivity,
10
- checkCodexPatterns,
11
- checkFullTestSuite,
12
- cursorGateRoot,
13
- cursorHeatRoots,
14
- cursorSessionId,
15
- cursorToolContext,
16
- cursorWorkspaceRoot,
17
- ensureCursorSessionBoot,
18
- evaluatePreToolGate,
19
- isShellTool,
20
- isWriteTool,
21
- loadDelegationGateFeatures,
22
- loadFeatures,
23
- readStdinJson,
24
- } from './cursor-hook-utils.js';
8
+ // Gate modules load inside main()'s try (dynamic import): a missing or broken module must deny, not crash
9
+ // before the handler with no JSON (Cursor would then fail open). Nothing here may import them statically.
10
+ let appendHookActivity = () => {};
11
+
12
+ /** Deny JSON on stdout and exit 0. Used when the gate cannot run at all. */
13
+ function denyHard(reason) {
14
+ try {
15
+ process.stdout.write(`${JSON.stringify({ permission: 'deny', user_message: reason, agent_message: reason })}\n`);
16
+ } finally {
17
+ process.exit(0);
18
+ }
19
+ }
20
+
21
+ export function cursorHookErrorPermission(err) {
22
+ if (err?.code === 'EACCES') return 'allow'; // code only: a message mentioning EACCES is not a permission error
23
+ return 'deny';
24
+ }
25
+
26
+ // Bootstrap: a fresh checkout has no build yet, so gate modules can be missing.
27
+ // Only the exact build commands may run then, so the hook cannot block the build that creates it.
28
+ const BOOTSTRAP_CMDS = new Set(['npm ci', 'npm run build', 'npm ci && npm run build', 'npm install && npm run build']);
29
+ export function cursorBootstrapPermission(err, event) {
30
+ if (err?.code !== 'ERR_MODULE_NOT_FOUND') return 'deny';
31
+ const cmd = String(event?.tool_input?.command ?? event?.command ?? '').trim().replace(/\s+/g, ' ');
32
+ return BOOTSTRAP_CMDS.has(cmd) ? 'allow' : 'deny';
33
+ }
34
+
35
+ async function readRawEvent() {
36
+ try {
37
+ let raw = '';
38
+ for await (const c of process.stdin) raw += c;
39
+ return JSON.parse(raw);
40
+ } catch { return null; }
41
+ }
25
42
 
26
43
  function finish(root, permission, reason, hint, toolName, extra = {}) {
27
44
  const out = { permission };
@@ -32,22 +49,38 @@ function finish(root, permission, reason, hint, toolName, extra = {}) {
32
49
  if (hint && permission === 'allow') {
33
50
  out.user_message = hint;
34
51
  }
52
+ if (extra.hookSpecificOutput) out.hookSpecificOutput = extra.hookSpecificOutput;
35
53
  console.log(JSON.stringify(out));
36
- appendHookActivity(root, 'cursor-pre-tool-use', permission, permission === 'deny' ? 'error' : 'info', {
37
- tool: toolName,
38
- reason: reason || hint || null,
39
- gate: extra.gate || null,
40
- livePath: true,
41
- });
54
+ try {
55
+ appendHookActivity(root, 'cursor-pre-tool-use', permission, permission === 'deny' ? 'error' : 'info', {
56
+ tool: toolName,
57
+ reason: reason || hint || null,
58
+ gate: extra.gate || null,
59
+ livePath: true,
60
+ });
61
+ } catch { /* the verdict is already on stdout; activity logging must not change it */ }
42
62
  process.exit(0);
43
63
  }
44
64
 
45
65
  async function main() {
46
- const fallbackRoot = cursorWorkspaceRoot();
66
+ let fallbackRoot = process.cwd();
47
67
  let toolName = 'unknown';
48
68
 
49
69
  try {
70
+ const u = await import('./cursor-hook-utils.js');
71
+ const { lensDecision, repertoireRoute } = await import('../../hooks/pre-tool-speak.mjs');
72
+ const {
73
+ checkCodexPatterns, checkFullTestSuite, cursorGateRoot, cursorHeatRoots, cursorSessionId, cursorToolContext,
74
+ cursorWorkspaceRoot, ensureCursorSessionBoot, evaluatePreToolGate, isShellTool, isWriteTool,
75
+ loadDelegationGateFeatures, loadFeatures, readStdinJson,
76
+ } = u;
77
+ appendHookActivity = u.appendHookActivity;
78
+ fallbackRoot = cursorWorkspaceRoot();
50
79
  const event = await readStdinJson();
80
+ // readStdinJson returns {} for empty or malformed stdin; Cursor always sends a JSON object, so deny those.
81
+ if (!event || typeof event !== 'object' || Array.isArray(event) || Object.keys(event).length === 0) {
82
+ finish(fallbackRoot, 'deny', 'preToolUse: empty or malformed hook input — denied', null, toolName, { gate: 'bad-input' });
83
+ }
51
84
  const eventRoot = cursorWorkspaceRoot(event);
52
85
  const sessionId = cursorSessionId(event);
53
86
  const heatRoots = cursorHeatRoots(event);
@@ -60,7 +93,7 @@ async function main() {
60
93
  const gateFeatures = loadDelegationGateFeatures(gateRoot, 'cursor');
61
94
  const ctx = cursorToolContext(event);
62
95
  toolName = ctx.toolName;
63
- const { content, cmd, toolInput } = ctx;
96
+ const { content, cmd, toolInput, paths } = ctx;
64
97
 
65
98
  const gateBlock = evaluatePreToolGate(toolName, toolInput, {
66
99
  projectRoot: gateRoot,
@@ -77,35 +110,65 @@ async function main() {
77
110
  if (extraBlock) finish(gateRoot, 'deny', extraBlock, null, toolName);
78
111
  }
79
112
 
113
+ const spoken = [content, cmd, toolInput.pattern, toolInput.query, ...(paths || [])].filter(Boolean).join('\n');
114
+ const lensStop = lensDecision(gateRoot, toolName, spoken);
115
+ if (lensStop?.decision === 'deny') {
116
+ finish(gateRoot, 'deny', lensStop.reason, null, toolName, { gate: lensStop.gate });
117
+ }
118
+ const route = repertoireRoute([toolName, cmd, content].filter(Boolean).join(' '));
119
+
80
120
  if (isShellTool(toolName) && cmd) {
81
121
  const testHint = checkFullTestSuite(cmd, features);
82
- if (testHint) finish(gateRoot, 'allow', null, testHint, toolName);
122
+ if (testHint) finish(gateRoot, 'allow', null, testHint, toolName, { ...(route || {}) });
83
123
  }
84
124
 
85
125
  if (gateBlock.reason) {
86
126
  finish(gateRoot, 'allow', gateBlock.reason, gateBlock.hint, toolName, {
87
127
  gate: gateBlock.gate,
88
128
  warn: true,
129
+ ...(route || {}),
89
130
  });
90
131
  }
91
132
 
92
- finish(gateRoot, 'allow', null, null, toolName);
133
+ finish(gateRoot, 'allow', null, null, toolName, { ...(route || {}) });
93
134
  } catch (err) {
94
- appendHookActivity(fallbackRoot, 'cursor-pre-tool-use', 'hook-error', 'error', {
95
- tool: toolName,
96
- error: err.message,
97
- failOpen: true,
98
- });
99
- // Dest EACCES on a Cloud workspace wrapper must not deny every tool.
135
+ if (err?.code === 'ERR_MODULE_NOT_FOUND' && cursorBootstrapPermission(err, await readRawEvent()) === 'allow') {
136
+ console.log(JSON.stringify({ permission: 'allow', user_message: 'preToolUse: build not present — bootstrap build allowed' }));
137
+ process.exit(0);
138
+ }
139
+ const failOpen = cursorHookErrorPermission(err) === 'allow';
140
+ try {
141
+ appendHookActivity(fallbackRoot, 'cursor-pre-tool-use', 'hook-error', 'error', {
142
+ tool: toolName,
143
+ error: err.message,
144
+ failOpen,
145
+ });
146
+ } catch { /* logging must not turn a deny into a crash */ }
147
+ if (failOpen) {
148
+ // Dest EACCES on a Cloud workspace wrapper must not deny every tool.
149
+ finish(
150
+ fallbackRoot,
151
+ 'allow',
152
+ `preToolUse hook error — fail open: ${err.message}`,
153
+ null,
154
+ toolName,
155
+ { gate: 'hook-error-fail-open' },
156
+ );
157
+ }
100
158
  finish(
101
159
  fallbackRoot,
102
- 'allow',
103
- `preToolUse hook error — fail open: ${err.message}`,
160
+ 'deny',
161
+ `preToolUse hook error blocked the tool: ${err.message}`,
104
162
  null,
105
163
  toolName,
106
- { gate: 'hook-error-fail-open' },
164
+ { gate: 'hook-error' },
107
165
  );
108
166
  }
109
167
  }
110
168
 
111
- main();
169
+ const launchedAsCli = Boolean(process.argv[1] && process.argv[1].endsWith('pre-tool-use.js'));
170
+ if (launchedAsCli) {
171
+ process.on('uncaughtException', (err) => denyHard(`preToolUse hook crashed — denied: ${err?.message || err}`));
172
+ process.on('unhandledRejection', (err) => denyHard(`preToolUse hook crashed — denied: ${err?.message || err}`));
173
+ main().catch((err) => denyHard(`preToolUse hook crashed — denied: ${err?.message || err}`));
174
+ }
@@ -110,7 +110,15 @@ if [ -n "$MILL" ]; then
110
110
  fi
111
111
 
112
112
  if [ -z "$NODE_BIN" ] || [ -z "$MILL" ] || [ ! -f "${MILL}/${REL}" ]; then
113
- printf '{"permission":"allow","user_message":"xray-cloud-hook: mill missing — fail open"}\n'
113
+ # Gate events fail closed: no node or no hook file means the gate cannot run, so the tool is denied.
114
+ case "$EVENT" in
115
+ preToolUse|beforeShellExecution|beforeReadFile)
116
+ printf '{"permission":"deny","user_message":"xray-cloud-hook: node or hook file missing — denied (fail closed)","agent_message":"xray-cloud-hook: node or hook file missing — denied (fail closed)"}\n'
117
+ ;;
118
+ *)
119
+ printf '{"permission":"allow","user_message":"xray-cloud-hook: node or hook file missing — %s skipped"}\n' "$EVENT"
120
+ ;;
121
+ esac
114
122
  exit 0
115
123
  fi
116
124
 
@@ -22,65 +22,9 @@ import {
22
22
  } from './grok-hook-utils.js';
23
23
  import { appendHookActivity } from './grok-hook-activity.js';
24
24
  import { cardStop, lensBeforeResearch } from '../../hooks/goggles-pipeline.mjs';
25
- import { createRequire } from 'node:module';
26
- import { existsSync } from 'node:fs';
27
- import { dirname, join } from 'node:path';
28
- import { fileURLToPath } from 'node:url';
29
-
30
- const hookDir = dirname(fileURLToPath(import.meta.url));
31
- let scoreAndRouteFn;
32
-
33
- function loadScoreAndRoute() {
34
- if (scoreAndRouteFn !== undefined) return scoreAndRouteFn;
35
- scoreAndRouteFn = null;
36
- const require = createRequire(import.meta.url);
37
- const candidates = [
38
- join(hookDir, '../../../nucleus/thin-dispatch.js'),
39
- join(hookDir, '../../../../dist/nucleus/thin-dispatch.js'),
40
- ];
41
- for (const candidate of candidates) {
42
- if (!existsSync(candidate)) continue;
43
- try {
44
- const loaded = require(candidate);
45
- if (typeof loaded.scoreAndRoute === 'function') {
46
- scoreAndRouteFn = loaded.scoreAndRoute;
47
- return scoreAndRouteFn;
48
- }
49
- } catch {
50
- /* next built copy */
51
- }
52
- }
53
- return null;
54
- }
55
-
56
- /** Speak only when the organ changes the agent. A match that leaves the agent is quiet. */
57
- export function repertoireDecisionFields(routed) {
58
- if (!routed?.memoryRouting?.overridden || !routed.agent) return null;
59
- const signals = Array.isArray(routed.memoryRouting.signals)
60
- ? routed.memoryRouting.signals.slice(0, 4).filter(Boolean)
61
- : [];
62
- const why = signals.length ? ` on ${signals.join(', ')}` : '';
63
- return {
64
- agent: routed.agent,
65
- gate: 'repertoire',
66
- hookSpecificOutput: {
67
- hookEventName: 'PreToolUse',
68
- additionalContext: `Repertoire changed the route to ${routed.agent}${why}. Follow that agent for this work.`,
69
- },
70
- };
71
- }
25
+ import { repertoireDecisionFields, repertoireRoute } from '../../hooks/pre-tool-speak.mjs';
72
26
 
73
- function repertoireRoute(operation) {
74
- const text = String(operation || '').slice(0, 500).trim();
75
- if (!text) return null;
76
- try {
77
- const scoreAndRoute = loadScoreAndRoute();
78
- if (!scoreAndRoute) return null;
79
- return repertoireDecisionFields(scoreAndRoute(text, {}));
80
- } catch {
81
- return null;
82
- }
83
- }
27
+ export { repertoireDecisionFields };
84
28
 
85
29
  function finish(root, decision, reason, hint, toolName, extra = {}) {
86
30
  const out = { decision, ...extra };
@@ -914,9 +914,22 @@ async function handleDelegationGate(command, projectRoot, logDir) {
914
914
  logDir,
915
915
  `[delegation-gate] deny tool=${tool} gate=${outcome.gate} reason=${outcome.reason}`,
916
916
  );
917
+ return { phase: "pre", ...outcome };
917
918
  }
918
919
 
919
- return { phase: "pre", ...outcome };
920
+ const spoken = `${tool} ${JSON.stringify(args)}`.slice(0, 500);
921
+ const { lensDecision, repertoireRoute } = await import("../hooks/pre-tool-speak.mjs");
922
+ const lens = lensDecision(projectRoot, tool, spoken);
923
+ if (lens) {
924
+ return {
925
+ allow: false,
926
+ phase: "pre",
927
+ gate: lens.gate || "lens",
928
+ reason: lens.reason,
929
+ };
930
+ }
931
+ const route = repertoireRoute(spoken);
932
+ return { phase: "pre", ...outcome, ...(route || {}) };
920
933
  } catch (error) {
921
934
  const message = `delegation-gate failed: ${error.message || error}`;
922
935
  logToActivity(logDir, `[delegation-gate] error ${message}`);
@@ -68,8 +68,9 @@ function loadDelegationGate() {
68
68
  ].filter(Boolean);
69
69
  const found = candidates.find((p) => existsSync(p));
70
70
  if (!found) {
71
- throw new Error(
72
- 'delegation-gate.js missing — run npm run build in 0xray or npm install 0xray',
71
+ throw Object.assign(
72
+ new Error('delegation-gate.js missing — run npm run build in 0xray or npm install 0xray'),
73
+ { code: 'ERR_MODULE_NOT_FOUND' },
73
74
  );
74
75
  }
75
76
  return createRequire(import.meta.url)(found);