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
@@ -3,7 +3,7 @@
3
3
  * Tools: look, status_lens. Same organ as the CLI.
4
4
  */
5
5
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
6
- import { basename, dirname, join, resolve } from 'node:path';
6
+ import { dirname, join, resolve } from 'node:path';
7
7
  import { pathToFileURL, fileURLToPath } from 'node:url';
8
8
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -12,17 +12,9 @@ import * as bundled from './goggles-pipeline.mjs';
12
12
 
13
13
  const HERE = fileURLToPath(new URL('.', import.meta.url));
14
14
  const SERVER_NAME = 'goggles';
15
- const CARD_PLANES = [
16
- 'ground',
17
- 'routing',
18
- 'house',
19
- 'boot',
20
- 'governance',
21
- 'memory-recall',
22
- 'orchestration',
23
- 'processor',
24
- 'reporting',
25
- ];
15
+ /** Card planes come from the organ, not a hand-kept copy. */
16
+ const CARD_PLANES = Array.isArray(bundled.CARD_PLANES) ? bundled.CARD_PLANES : [];
17
+ const CARD_OUTERS = ['digest', 'triage'];
26
18
  const SCOPES = ['ecosystem', 'part', 'one flow', 'one artifact'];
27
19
 
28
20
  const LOOK_SCHEMA = {
@@ -31,7 +23,7 @@ const LOOK_SCHEMA = {
31
23
  properties: {
32
24
  kind: {
33
25
  type: 'string',
34
- description: 'Optional. Use "0" or "actuality" for the lens. Omit when naming an outer.',
26
+ description: 'Use "0" or "actuality" for the lens. Send kind, outer, or plane: an empty look is refused. Never with outer: kind and outer together is refused.',
35
27
  enum: ['0', 'actuality'],
36
28
  },
37
29
  outer: {
@@ -46,7 +38,7 @@ const LOOK_SCHEMA = {
46
38
  },
47
39
  plane: {
48
40
  type: 'string',
49
- description: 'Card plane for digest or triage only. Not the lens.',
41
+ description: 'Card plane for digest or triage only. Not the lens. A plane with any other outer is refused.',
50
42
  enum: CARD_PLANES,
51
43
  },
52
44
  },
@@ -55,7 +47,7 @@ const LOOK_SCHEMA = {
55
47
  export const TOOLS = [
56
48
  {
57
49
  name: 'look',
58
- description: 'Kind 0 is quiet on a match. It checks the six outer names. One outer is a reading. digest and triage return a card.',
50
+ description: 'Kind 0 is quiet on a match. It checks the six outer names, then sweeps every card plane for missing shipped files and names the first drift. One outer is a reading. digest and triage return a card. Refused: an empty look; kind with outer; a plane with an outer other than digest or triage.',
59
51
  inputSchema: LOOK_SCHEMA,
60
52
  },
61
53
  {
@@ -152,6 +144,15 @@ function checkArgs(args) {
152
144
  if (args.plane != null && !CARD_PLANES.includes(args.plane)) {
153
145
  return deny('invalid_args', 'plane is a card plane.');
154
146
  }
147
+ if (args.kind == null && args.outer == null && args.plane == null) {
148
+ return deny('invalid_args', 'Name a kind, one outer, or one plane.');
149
+ }
150
+ if (args.kind != null && args.outer != null) {
151
+ return deny('invalid_args', 'Send kind or outer, not both.');
152
+ }
153
+ if (args.plane != null && args.outer != null && !CARD_OUTERS.includes(args.outer)) {
154
+ return deny('invalid_args', 'plane goes with digest or triage only.');
155
+ }
155
156
  return null;
156
157
  }
157
158
 
@@ -161,7 +162,7 @@ function publicCard(view) {
161
162
  if (Array.isArray(view.empty) && view.empty.length) notes.push(`Empty: ${view.empty.join(', ')}`);
162
163
  if (view.exam) notes.push(view.exam);
163
164
  }
164
- const skills = view.skills ? basename(String(view.skills)) : '';
165
+ const skills = view.skills ? String(view.skills) : '';
165
166
  const cascade = view.cascade
166
167
  ? {
167
168
  plane: view.cascade.plane,
@@ -227,7 +228,8 @@ export async function answerLook(raw, root) {
227
228
  const here = projectRoot(root);
228
229
  if (pureKind0(args)) {
229
230
  api.maintainLens(here);
230
- const line = api.actualityLine();
231
+ const line = api.actualityLine() || cardDrift(api, here);
232
+ if (line) writeLens(api, here, line);
231
233
  if (!line) return quiet('kind0');
232
234
  return { ok: true, quiet: false, mode: 'kind0', content: line };
233
235
  }
@@ -269,6 +271,22 @@ export async function answerLook(raw, root) {
269
271
  };
270
272
  }
271
273
 
274
+ /** Kind 0 card sweep: triage's file check on every card plane, shipped paths only. */
275
+ function cardDrift(api, here) {
276
+ if (typeof api.shippedDrift !== 'function') return '';
277
+ try {
278
+ return api.shippedDrift(here);
279
+ } catch {
280
+ return '';
281
+ }
282
+ }
283
+
284
+ function writeLens(api, here, line) {
285
+ const file = api.lensPath(here);
286
+ mkdirSync(dirname(file), { recursive: true });
287
+ writeFileSync(file, `${line}\n`);
288
+ }
289
+
272
290
  function lensBody(root) {
273
291
  const file = join(root, '.xray', 'state', 'LENS.md');
274
292
  if (!existsSync(file)) return { found: false, text: '' };
@@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url';
18
18
  const FIELD_ORDER = ['plate', 'entry', 'exit', 'files', 'skills', 'setup', 'teardown', 'worn'];
19
19
  const HERE = dirname(fileURLToPath(import.meta.url));
20
20
  const WORN_PLANES = ['dichotomy', 'syncopate', 'synthesis', 'digest', 'triage', 'loop'];
21
- const CARD_PLANES = ['ground', 'routing', 'house', 'boot', 'governance', 'memory-recall', 'orchestration', 'processor', 'reporting', 'stamp-plate', 'record-map', 'write-home', 'activity-log', 'session-capture', 'suit-wear', 'suit-organs', 'station-card', 'notes-page', 'reflection-page', 'site-manual', 'package-face', 'suit-settings', 'trail-state', 'inference-files', 'grok-compact', 'payload-heat', 'station-heat', 'pickup-stamp', 'cursor-compact', 'work-fresh', 'kept-line', 'lens-gate', 'pre-tool', 'lens-page', 'centrifuge'];
21
+ export const CARD_PLANES = ['ground', 'routing', 'house', 'boot', 'governance', 'memory-recall', 'orchestration', 'processor', 'reporting', 'stamp-plate', 'record-map', 'write-home', 'activity-log', 'session-capture', 'suit-wear', 'suit-organs', 'station-card', 'notes-page', 'reflection-page', 'site-manual', 'package-face', 'suit-settings', 'trail-state', 'inference-files', 'grok-compact', 'payload-heat', 'station-heat', 'pickup-stamp', 'cursor-compact', 'work-fresh', 'kept-line', 'lens-gate', 'pre-tool', 'lens-page', 'centrifuge'];
22
22
  const CARD_FLAVORS = ['digest', 'triage'];
23
23
  const SCOPE_ZOOM = ['ecosystem', 'part', 'one flow', 'one artifact'];
24
24
 
@@ -1065,6 +1065,51 @@ export function growPlane(id, platesDir, root) {
1065
1065
  return { ...base, ...grown };
1066
1066
  }
1067
1067
 
1068
+ /** Paths the package ships: package.json files, plus what npm always packs. */
1069
+ function shippedSet(root) {
1070
+ try {
1071
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
1072
+ if (!Array.isArray(pkg.files)) return null;
1073
+ return pkg.files
1074
+ .map(String)
1075
+ .filter((entry) => entry && !entry.startsWith('!'))
1076
+ .map((entry) => entry.replace(/\/+$/, ''))
1077
+ .concat(['package.json', 'README.md', 'LICENSE']);
1078
+ } catch {
1079
+ return null;
1080
+ }
1081
+ }
1082
+
1083
+ function ships(shipped, rel) {
1084
+ if (!shipped) return true;
1085
+ const path = String(rel || '').replace(/\/+$/, '');
1086
+ if (!path) return false;
1087
+ return shipped.some((entry) => path === entry || path.startsWith(`${entry}/`) || entry.startsWith(`${path}/`));
1088
+ }
1089
+
1090
+ /**
1091
+ * Kind 0 card sweep: triage's file check on every card plane, limited to
1092
+ * paths the package ships, so a packed install and the repo read the same.
1093
+ * Returns the first drift line, or '' when every card holds.
1094
+ */
1095
+ export function shippedDrift(projectRoot) {
1096
+ const platesDir = findPlatesDir(HERE);
1097
+ const root = sourceRoot(platesDir);
1098
+ const shipped = shippedSet(root);
1099
+ for (const id of CARD_PLANES) {
1100
+ const plane = growPlane(id, platesDir, projectRoot || stateRoot());
1101
+ const kept = {
1102
+ ...plane,
1103
+ files: (Array.isArray(plane.files) ? plane.files : []).filter((rel) => ships(shipped, rel)),
1104
+ skills: ships(shipped, plane.skills) ? plane.skills : '',
1105
+ worn: ships(shipped, plane.worn) ? plane.worn : '',
1106
+ };
1107
+ const exam = examinePlane(kept, root);
1108
+ if (exam.kind === 'drift') return `Actuality. ${id}: ${exam.text}`;
1109
+ }
1110
+ return '';
1111
+ }
1112
+
1068
1113
  function repoRootFrom(platesDir) {
1069
1114
  return dirname(dirname(dirname(platesDir)));
1070
1115
  }
@@ -7,6 +7,7 @@
7
7
  "files": [
8
8
  "src",
9
9
  "grok-bot/OP-PROC.md",
10
+ "grok-bot/LEXICON.md",
10
11
  "src/opencode/agents",
11
12
  "AGENTS.md",
12
13
  "src/integrations",
@@ -28,7 +29,7 @@
28
29
  "plates": ["orchestration", "processor"]
29
30
  },
30
31
  "house": {
31
- "files": ["grok-bot/lib/seat-doctor.cjs"],
32
+ "files": ["grok-bot/lib/seat-doctor.cjs", "grok-bot/templates/house/fleet/WAKE-ROUTINE.md", "grok-bot/templates/house/HOUSE.md", "src/__tests__/unit/grok-bot-house-template.test.ts", "src/__tests__/unit/grok-bot-house-migrate.test.ts"],
32
33
  "skills": ".agents/skills/house/SKILL.md",
33
34
  "plates": ["suit-wear"]
34
35
  },
@@ -45,7 +46,7 @@
45
46
  "skills": ".agents/skills/governance/SKILL.md"
46
47
  },
47
48
  "memory-recall": {
48
- "files": ["src/integrations/hooks/plates.cjs"],
49
+ "files": ["src/integrations/hooks/plates.cjs", "src/__tests__/unit/plates.test.ts"],
49
50
  "skills": ".agents/skills/memory-recall/SKILL.md",
50
51
  "plates": ["record-map"]
51
52
  },
@@ -94,7 +95,7 @@
94
95
  "plates": ["write-home", "inference-files", "suit-organs"]
95
96
  },
96
97
  "suit-wear": {
97
- "files": ["src/core/features-config.ts"],
98
+ "files": ["src/core/features-config.ts", "scripts/node/install-bridges.cjs", "scripts/node/setup.cjs", "src/integrations/cursor/hooks/xray-cloud-hook.sh"],
98
99
  "mark": "log_path",
99
100
  "skills": ".agents/skills/suit-wear/SKILL.md",
100
101
  "plates": ["suit-settings", "write-home", "package-face"]
@@ -124,13 +125,13 @@
124
125
  "plates": ["record-map"]
125
126
  },
126
127
  "site-manual": {
127
- "files": ["docs-site/docs/introduction.md"],
128
+ "files": ["docs-site/docs/introduction.md", "docs-site/docs/index.md", "docs-site/docs/full-reference.md", "docs-site/docs/guides/features-since-3.1.md"],
128
129
  "mark": "survives the context window",
129
130
  "skills": ".agents/skills/site-manual/SKILL.md",
130
131
  "plates": ["record-map"]
131
132
  },
132
133
  "package-face": {
133
- "files": ["package.json"],
134
+ "files": ["package.json", "grok-bot/README.md", "grok-bot/llms.txt", "grok-bot/CHANGELOG.md", "src/cli/index.ts", "src/cli/commands/plugin-commands.ts", "src/__tests__/unit/plugin-commands.test.ts"],
134
135
  "mark": "\"name\": \"0xray\"",
135
136
  "skills": ".agents/skills/package-face/SKILL.md",
136
137
  "plates": ["record-map", "suit-wear"]
@@ -197,7 +198,7 @@
197
198
  "plates": ["station-card", "work-fresh"]
198
199
  },
199
200
  "lens-gate": {
200
- "files": ["src/integrations/hooks/goggles-pipeline.mjs"],
201
+ "files": ["src/integrations/hooks/goggles-pipeline.mjs", "src/__tests__/unit/goggles-pipeline.test.ts"],
201
202
  "mark": "export function lensBeforeResearch",
202
203
  "skills": ".agents/skills/lens-gate/SKILL.md",
203
204
  "plates": ["pre-tool"]
@@ -0,0 +1,76 @@
1
+ /**
2
+ * One pre-tool speak for every host.
3
+ * The lens denies an unnamed plane. The route speaks only when Repertoire changes the agent.
4
+ */
5
+ import { createRequire } from 'node:module';
6
+ import { existsSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { lensBeforeResearch } from './goggles-pipeline.mjs';
10
+
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ let scoreAndRouteFn;
13
+
14
+ function loadScoreAndRoute() {
15
+ if (scoreAndRouteFn !== undefined) return scoreAndRouteFn;
16
+ scoreAndRouteFn = null;
17
+ const require = createRequire(import.meta.url);
18
+ const candidates = [
19
+ join(here, '../../nucleus/thin-dispatch.js'),
20
+ join(here, '../../../dist/nucleus/thin-dispatch.js'),
21
+ join(here, '../../../nucleus/thin-dispatch.js'),
22
+ ];
23
+ for (const candidate of candidates) {
24
+ if (!existsSync(candidate)) continue;
25
+ try {
26
+ const loaded = require(candidate);
27
+ if (typeof loaded.scoreAndRoute === 'function') {
28
+ scoreAndRouteFn = loaded.scoreAndRoute;
29
+ return scoreAndRouteFn;
30
+ }
31
+ } catch {
32
+ /* next built copy */
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /** Speak only when the organ changes the agent. A match that leaves the agent is quiet. */
39
+ export function repertoireDecisionFields(routed) {
40
+ if (!routed?.memoryRouting?.overridden || !routed.agent) return null;
41
+ const signals = Array.isArray(routed.memoryRouting.signals)
42
+ ? routed.memoryRouting.signals.slice(0, 4).filter(Boolean)
43
+ : [];
44
+ const why = signals.length ? ` on ${signals.join(', ')}` : '';
45
+ return {
46
+ agent: routed.agent,
47
+ gate: 'repertoire',
48
+ hookSpecificOutput: {
49
+ hookEventName: 'PreToolUse',
50
+ additionalContext: `Repertoire changed the route to ${routed.agent}${why}. Follow that agent for this work.`,
51
+ },
52
+ };
53
+ }
54
+
55
+ export function repertoireRoute(operation) {
56
+ const text = String(operation || '').slice(0, 500).trim();
57
+ if (!text) return null;
58
+ try {
59
+ const scoreAndRoute = loadScoreAndRoute();
60
+ if (!scoreAndRoute) return null;
61
+ return repertoireDecisionFields(scoreAndRoute(text, {}));
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /** Deny when the lens stops the tool. Any other lens result is quiet. */
68
+ export function lensDecision(root, toolName, spoken) {
69
+ try {
70
+ const stop = lensBeforeResearch(root, toolName, spoken);
71
+ if (stop?.decision === 'deny') return stop;
72
+ return null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
@@ -23,7 +23,7 @@ function resolveGate() {
23
23
  ].filter(Boolean);
24
24
  const found = candidates.find((p) => existsSync(p));
25
25
  if (!found) {
26
- throw new Error('delegation-gate.js missing — build 0xray or npm install 0xray');
26
+ throw Object.assign(new Error('delegation-gate.js missing — build 0xray or npm install 0xray'), { code: 'ERR_MODULE_NOT_FOUND' });
27
27
  }
28
28
  return require(found);
29
29
  }
@@ -49,7 +49,7 @@ function readStdin() {
49
49
  });
50
50
  }
51
51
 
52
- function decide(event, gate) {
52
+ async function decide(event, gate) {
53
53
  const projectRoot =
54
54
  event.cwd || event.workspaceRoot || event.projectRoot || process.cwd();
55
55
  const sessionId = event.sessionId || event.session_id || 'openclaw';
@@ -70,20 +70,37 @@ function decide(event, gate) {
70
70
  gate: outcome.gate,
71
71
  };
72
72
  }
73
- return {
73
+
74
+ const spoken = `${toolName} ${JSON.stringify(args)}`.slice(0, 500);
75
+ const { lensDecision, repertoireRoute } = await import('../../hooks/pre-tool-speak.mjs');
76
+ const lens = lensDecision(projectRoot, toolName, spoken);
77
+ if (lens) {
78
+ return {
79
+ action: 'block',
80
+ allow: false,
81
+ reason: lens.reason,
82
+ gate: lens.gate || 'lens',
83
+ };
84
+ }
85
+ const route = repertoireRoute(spoken);
86
+ const allowed = {
74
87
  action: 'allow',
75
88
  allow: true,
76
89
  reason: outcome.reason,
77
90
  gate: outcome.gate,
78
91
  warn: Boolean(outcome.reason),
79
92
  };
93
+ if (route?.hookSpecificOutput) {
94
+ allowed.hookSpecificOutput = route.hookSpecificOutput;
95
+ }
96
+ return allowed;
80
97
  }
81
98
 
82
99
  async function main() {
83
100
  try {
84
101
  const event = await readStdin();
85
102
  const gate = resolveGate();
86
- const decision = decide(event, gate);
103
+ const decision = await decide(event, gate);
87
104
  console.log(JSON.stringify(decision));
88
105
  process.exit(decision.action === 'block' ? 2 : 0);
89
106
  } catch (err) {
@@ -2,7 +2,7 @@ import { createRequire } from 'node:module';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
7
7
 
8
8
  const require = createRequire(import.meta.url);
@@ -39,7 +39,7 @@ export default definePluginEntry({
39
39
  register(api) {
40
40
  api.on(
41
41
  'before_tool_call',
42
- (event, ctx) => {
42
+ async (event, ctx) => {
43
43
  try {
44
44
  const xrayRoot = resolveXrayRoot();
45
45
  if (!xrayRoot) {
@@ -61,6 +61,21 @@ export default definePluginEntry({
61
61
  if (!outcome.allow) {
62
62
  return { block: true, blockReason: outcome.reason || '0xRay constitution' };
63
63
  }
64
+ const spoken = `${event.toolName} ${JSON.stringify(event.params || {})}`.slice(0, 500);
65
+ const speakPath = [
66
+ join(xrayRoot, 'dist/integrations/hooks/pre-tool-speak.mjs'),
67
+ join(xrayRoot, 'src/integrations/hooks/pre-tool-speak.mjs'),
68
+ ].find((candidate) => existsSync(candidate));
69
+ if (!speakPath) return undefined;
70
+ const speak = await import(pathToFileURL(speakPath).href);
71
+ const lens = speak.lensDecision(projectRoot, event.toolName, spoken);
72
+ if (lens) {
73
+ return { block: true, blockReason: lens.reason };
74
+ }
75
+ const route = speak.repertoireRoute(spoken);
76
+ if (route?.hookSpecificOutput) {
77
+ return { block: false, hookSpecificOutput: route.hookSpecificOutput };
78
+ }
64
79
  return undefined;
65
80
  } catch (err) {
66
81
  return {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.42",
3
+ "version": "4.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Nudge Watchdog - Detects and breaks stuck AI patterns
2
+ * Nudge Monitor - Detects and breaks stuck AI patterns
3
3
  *
4
4
  * Monitors agent actions and detects common failure patterns:
5
5
  * - think-loop: AI keeps outputting thinking tags but no progress
@@ -8,7 +8,7 @@
8
8
  * - tool-loop: Same tool called with similar args repeatedly
9
9
  * - repair-failure: Processor indicates fix but error persists
10
10
  *
11
- * @module monitoring/nudge-watchdog
11
+ * @module monitoring/nudge-monitor
12
12
  */
13
13
  import { EventEmitter } from "events";
14
14
  export interface NudgeConfig {
@@ -33,7 +33,16 @@ export interface NudgeAction {
33
33
  suggestion: string;
34
34
  priority: number;
35
35
  }
36
- declare class NudgeWatchdog extends EventEmitter {
36
+ /** Old config key from before the rename to nudge_monitor; still read as a deprecated alias. */
37
+ export declare const LEGACY_NUDGE_CONFIG_KEY = "nudge_watchdog";
38
+ /**
39
+ * features.nudge_monitor, or the deprecated features.nudge_watchdog when nudge_monitor is unset (the new key wins
40
+ * when both are set). Using the old key logs a one-time deprecation warning.
41
+ */
42
+ export declare function resolveNudgeConfig(features: Record<string, unknown> | undefined | null, warn?: (message: string) => void): Record<string, unknown> | undefined;
43
+ /** Test hook: let the one-time deprecation warning fire again. */
44
+ export declare function resetLegacyNudgeWarning(): void;
45
+ declare class NudgeMonitor extends EventEmitter {
37
46
  private config;
38
47
  private patterns;
39
48
  private lastNudgeTime;
@@ -75,13 +84,13 @@ declare class NudgeWatchdog extends EventEmitter {
75
84
  };
76
85
  private log;
77
86
  }
78
- export declare const nudgeWatchdog: NudgeWatchdog;
87
+ export declare const nudgeMonitor: NudgeMonitor;
79
88
  export declare function recordThinkTag(): void;
80
89
  export declare function recordCodeChange(): void;
81
90
  export declare function recordToolCall(tool: string, args: string): void;
82
91
  export declare function recordFixAttempt(filePath: string, error: string): void;
83
92
  export declare function recordExplanation(): void;
84
93
  export declare function getNudgeSuggestion(type: NudgeType, context?: Record<string, unknown>): string;
85
- export declare function getNudgeWatchdog(): NudgeWatchdog;
94
+ export declare function getNudgeMonitor(): NudgeMonitor;
86
95
  export declare function recordProcessorFix(filePath: string, fixed: boolean, errorPersisted: boolean): void;
87
96
  export {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Nudge Watchdog - Detects and breaks stuck AI patterns
2
+ * Nudge Monitor - Detects and breaks stuck AI patterns
3
3
  *
4
4
  * Monitors agent actions and detects common failure patterns:
5
5
  * - think-loop: AI keeps outputting thinking tags but no progress
@@ -8,7 +8,7 @@
8
8
  * - tool-loop: Same tool called with similar args repeatedly
9
9
  * - repair-failure: Processor indicates fix but error persists
10
10
  *
11
- * @module monitoring/nudge-watchdog
11
+ * @module monitoring/nudge-monitor
12
12
  */
13
13
  import { EventEmitter } from "events";
14
14
  import { frameworkLogger } from "../core/framework-logger.js";
@@ -53,7 +53,32 @@ const NUDGE_MESSAGES = {
53
53
  priority: 5,
54
54
  },
55
55
  };
56
- class NudgeWatchdog extends EventEmitter {
56
+ /** Old config key from before the rename to nudge_monitor; still read as a deprecated alias. */
57
+ export const LEGACY_NUDGE_CONFIG_KEY = "nudge_watchdog";
58
+ let legacyKeyWarned = false;
59
+ /**
60
+ * features.nudge_monitor, or the deprecated features.nudge_watchdog when nudge_monitor is unset (the new key wins
61
+ * when both are set). Using the old key logs a one-time deprecation warning.
62
+ */
63
+ export function resolveNudgeConfig(features, warn = (m) => frameworkLogger.log("nudge-monitor", "deprecated-config-key", "warning", { message: m })) {
64
+ const current = features?.nudge_monitor;
65
+ if (current !== undefined && current !== null)
66
+ return current;
67
+ const legacy = features?.[LEGACY_NUDGE_CONFIG_KEY];
68
+ if (legacy !== undefined && legacy !== null) {
69
+ if (!legacyKeyWarned) {
70
+ legacyKeyWarned = true;
71
+ warn(`features.${LEGACY_NUDGE_CONFIG_KEY} is deprecated; rename it to features.nudge_monitor`);
72
+ }
73
+ return legacy;
74
+ }
75
+ return undefined;
76
+ }
77
+ /** Test hook: let the one-time deprecation warning fire again. */
78
+ export function resetLegacyNudgeWarning() {
79
+ legacyKeyWarned = false;
80
+ }
81
+ class NudgeMonitor extends EventEmitter {
57
82
  config;
58
83
  patterns = new Map();
59
84
  lastNudgeTime = 0;
@@ -73,7 +98,7 @@ class NudgeWatchdog extends EventEmitter {
73
98
  loadConfigFromFeatures() {
74
99
  try {
75
100
  const features = featuresConfigLoader.loadConfig();
76
- const nudgeConfig = features?.nudge_watchdog;
101
+ const nudgeConfig = resolveNudgeConfig(features);
77
102
  if (nudgeConfig) {
78
103
  this.config = { ...this.config, ...nudgeConfig };
79
104
  }
@@ -86,10 +111,10 @@ class NudgeWatchdog extends EventEmitter {
86
111
  if (!this.config.enabled) {
87
112
  return;
88
113
  }
89
- this.log("nudge-watchdog", "started", "info", { config: this.config });
114
+ this.log("nudge-monitor", "started", "info", { config: this.config });
90
115
  }
91
116
  stop() {
92
- this.log("nudge-watchdog", "stopped", "info");
117
+ this.log("nudge-monitor", "stopped", "info");
93
118
  }
94
119
  recordThinkTag() {
95
120
  const now = Date.now();
@@ -256,7 +281,7 @@ class NudgeWatchdog extends EventEmitter {
256
281
  if (!action)
257
282
  return null;
258
283
  this.emit("nudge", action);
259
- this.log("nudge-watchdog", "nudge-triggered", "warning", {
284
+ this.log("nudge-monitor", "nudge-triggered", "warning", {
260
285
  type: action.type,
261
286
  message: action.message,
262
287
  suggestion: action.suggestion,
@@ -328,28 +353,28 @@ class NudgeWatchdog extends EventEmitter {
328
353
  }
329
354
  }
330
355
  }
331
- export const nudgeWatchdog = new NudgeWatchdog();
356
+ export const nudgeMonitor = new NudgeMonitor();
332
357
  export function recordThinkTag() {
333
- nudgeWatchdog.recordThinkTag();
358
+ nudgeMonitor.recordThinkTag();
334
359
  }
335
360
  export function recordCodeChange() {
336
- nudgeWatchdog.recordCodeChange();
361
+ nudgeMonitor.recordCodeChange();
337
362
  }
338
363
  export function recordToolCall(tool, args) {
339
- nudgeWatchdog.recordToolCall(tool, args);
364
+ nudgeMonitor.recordToolCall(tool, args);
340
365
  }
341
366
  export function recordFixAttempt(filePath, error) {
342
- nudgeWatchdog.recordFixAttempt(filePath, error);
367
+ nudgeMonitor.recordFixAttempt(filePath, error);
343
368
  }
344
369
  export function recordExplanation() {
345
- nudgeWatchdog.recordExplanation();
370
+ nudgeMonitor.recordExplanation();
346
371
  }
347
372
  export function getNudgeSuggestion(type, context) {
348
- return nudgeWatchdog.getNudgeSuggestion(type, context);
373
+ return nudgeMonitor.getNudgeSuggestion(type, context);
349
374
  }
350
- export function getNudgeWatchdog() {
351
- return nudgeWatchdog;
375
+ export function getNudgeMonitor() {
376
+ return nudgeMonitor;
352
377
  }
353
378
  export function recordProcessorFix(filePath, fixed, errorPersisted) {
354
- nudgeWatchdog.recordProcessorFix(filePath, fixed, errorPersisted);
379
+ nudgeMonitor.recordProcessorFix(filePath, fixed, errorPersisted);
355
380
  }
@@ -114,6 +114,24 @@ export default async function xrayCodexPlugin(input) {
114
114
  logger.error(`🚫 Delegation gate: ${delegationGate.reason}`);
115
115
  throw new Error(`DELEGATION GATE: ${delegationGate.reason}`);
116
116
  }
117
+ let speak = null;
118
+ try {
119
+ speak = (await import(new URL("../integrations/hooks/pre-tool-speak.mjs", import.meta.url).href));
120
+ }
121
+ catch {
122
+ speak = null;
123
+ }
124
+ if (speak) {
125
+ const spoken = `${tool} ${JSON.stringify(args ?? {})}`.slice(0, 500);
126
+ const lens = speak.lensDecision(directory, tool, spoken);
127
+ if (lens) {
128
+ throw new Error(`DELEGATION GATE: ${lens.reason ?? "lens"}`);
129
+ }
130
+ const additional = speak.repertoireRoute(spoken)?.hookSpecificOutput?.additionalContext;
131
+ if (additional) {
132
+ logger.log(additional);
133
+ }
134
+ }
117
135
  if (isWriteEditOperation(tool)) {
118
136
  const _ProcessorManager = getProcessorManager();
119
137
  const _XrayStateManager = getXrayStateManager();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Nudge Processor - Integrates Nudge Watchdog with processor system
2
+ * Nudge Processor - Integrates Nudge Monitor with processor system
3
3
  *
4
4
  * Post-processor that monitors agent actions and triggers nudges
5
5
  * when stuck patterns are detected.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Nudge Processor - Integrates Nudge Watchdog with processor system
2
+ * Nudge Processor - Integrates Nudge Monitor with processor system
3
3
  *
4
4
  * Post-processor that monitors agent actions and triggers nudges
5
5
  * when stuck patterns are detected.
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { PostProcessor } from "../processor-interfaces.js";
10
10
  import { frameworkLogger } from "../../core/framework-logger.js";
11
- import { nudgeWatchdog, recordThinkTag, recordCodeChange, recordToolCall, recordFixAttempt, recordExplanation, getNudgeSuggestion, } from "../../monitoring/nudge-watchdog.js";
11
+ import { nudgeMonitor, resolveNudgeConfig, recordThinkTag, recordCodeChange, recordToolCall, recordFixAttempt, recordExplanation, getNudgeSuggestion, } from "../../monitoring/nudge-monitor.js";
12
12
  import { featuresConfigLoader } from "../../core/features-config.js";
13
13
  export class NudgeProcessor extends PostProcessor {
14
14
  name = "nudge";
@@ -21,7 +21,7 @@ export class NudgeProcessor extends PostProcessor {
21
21
  loadConfig() {
22
22
  try {
23
23
  const features = featuresConfigLoader.loadConfig();
24
- const nudgeConfig = features?.nudge_watchdog;
24
+ const nudgeConfig = resolveNudgeConfig(features);
25
25
  if (nudgeConfig) {
26
26
  this.nudgeEnabled = nudgeConfig.enabled !== false;
27
27
  }
@@ -71,7 +71,7 @@ export class NudgeProcessor extends PostProcessor {
71
71
  }
72
72
  }
73
73
  }
74
- const stats = nudgeWatchdog.getStats();
74
+ const stats = nudgeMonitor.getStats();
75
75
  if (stats.activePatterns > 0) {
76
76
  const nudge = this.detectMostUrgentNudge(stats);
77
77
  if (nudge) {
@@ -35,4 +35,4 @@ Today those writers append `logs/framework/activity.log`. The features default s
35
35
 
36
36
  When: on the tool hook, on framework logger flush, and on boot when file logging is on. Why: the report and the pulse read one tail, not a pile of copies.
37
37
 
38
- stamped · 0xray 4.0.42
38
+ stamped · 0xray 4.1.0