0xray 4.0.42 → 4.1.1

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 (138) hide show
  1. package/.gitignore.default +4 -1
  2. package/.grok-plugin/plugin.json +1 -1
  3. package/.opencode/skills/orchestrator/SKILL.md +1 -7
  4. package/.plugin/plugin.json +1 -1
  5. package/.xray/features.json +8 -5
  6. package/.xray/features.schema.json +8 -0
  7. package/AGENTS-consumer.md +9 -7
  8. package/AGENTS.md +19 -10
  9. package/LICENSE +1 -1
  10. package/README.md +15 -9
  11. package/SKILLS.md +3 -1
  12. package/dist/.gitignore.default +4 -1
  13. package/dist/AGENTS.md +19 -10
  14. package/dist/CHANGELOG.md +109 -11
  15. package/dist/LICENSE +1 -1
  16. package/dist/README.md +15 -9
  17. package/dist/SKILLS.md +3 -1
  18. package/dist/cli/commands/opencode-install.js +3 -9
  19. package/dist/cli/commands/plugin-commands.d.ts +4 -0
  20. package/dist/cli/commands/plugin-commands.js +84 -41
  21. package/dist/cli/commands/skill-install.js +10 -5
  22. package/dist/cli/goggles-status.d.ts +2 -5
  23. package/dist/cli/goggles-status.js +5 -53
  24. package/dist/cli/index.js +2 -2
  25. package/dist/core/config-loader.js +4 -4
  26. package/dist/core/features-config.d.ts +1 -1
  27. package/dist/core/features-config.js +3 -3
  28. package/dist/enforcement/enforcer-tools.d.ts +1 -1
  29. package/dist/enforcement/enforcer-tools.js +1 -1
  30. package/dist/integrations/cursor/README.md +1 -1
  31. package/dist/integrations/cursor/hooks/hooks.json +6 -3
  32. package/dist/integrations/cursor/hooks/pre-tool-use.js +104 -37
  33. package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +9 -1
  34. package/dist/integrations/grok/grok-cli.js +5 -7
  35. package/dist/integrations/grok/hooks/grok-hook-utils.js +4 -1
  36. package/dist/integrations/grok/hooks/pre-tool-use.js +6 -76
  37. package/dist/integrations/grok/hooks/session-start.js +4 -2
  38. package/dist/integrations/hermes-agent/bridge.mjs +17 -1
  39. package/dist/integrations/hooks/delegation-gate-runtime.mjs +3 -2
  40. package/dist/integrations/hooks/goggles-mcp.mjs +23 -181
  41. package/dist/integrations/hooks/goggles-pipeline.mjs +606 -571
  42. package/dist/integrations/hooks/goggles-planes.json +95 -46
  43. package/dist/integrations/hooks/plates.cjs +84 -0
  44. package/dist/integrations/hooks/pre-tool-speak.mjs +74 -0
  45. package/dist/integrations/hooks/skill-seek.mjs +115 -0
  46. package/dist/integrations/hooks/station-hook-runtime.cjs +4 -1
  47. package/dist/integrations/openclaw/hooks/pre-tool-gate-runtime.mjs +24 -7
  48. package/dist/integrations/openclaw/plugin/xray-pre-tool/index.js +26 -2
  49. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  50. package/dist/memory-routing/plates.d.ts +1 -1
  51. package/dist/memory-routing/plates.js +14 -0
  52. package/dist/monitoring/{nudge-watchdog.d.ts → nudge-monitor.d.ts} +14 -5
  53. package/dist/monitoring/{nudge-watchdog.js → nudge-monitor.js} +42 -17
  54. package/dist/nucleus/thin-dispatch.d.ts +1 -1
  55. package/dist/nucleus/thin-dispatch.js +1 -1
  56. package/dist/nucleus/work-freshness.mjs +10 -0
  57. package/dist/plugin/xray-codex-injection.js +20 -0
  58. package/dist/processors/implementations/nudge-processor.d.ts +1 -1
  59. package/dist/processors/implementations/nudge-processor.js +4 -4
  60. package/dist/skills/orchestrator/SKILL.md +1 -7
  61. package/docs-site/docs/plates/activity-log.md +1 -1
  62. package/docs-site/docs/plates/asides.md +35 -0
  63. package/docs-site/docs/plates/bifrost.md +48 -0
  64. package/docs-site/docs/plates/boot.md +2 -2
  65. package/docs-site/docs/plates/centrifuge.md +1 -1
  66. package/docs-site/docs/plates/confer.md +35 -0
  67. package/docs-site/docs/plates/consumer-mcp.md +35 -0
  68. package/docs-site/docs/plates/cursor-compact.md +1 -1
  69. package/docs-site/docs/plates/delegation-gate.md +35 -0
  70. package/docs-site/docs/plates/dynamo.md +35 -0
  71. package/docs-site/docs/plates/foundry.md +35 -0
  72. package/docs-site/docs/plates/glossary.md +7 -3
  73. package/docs-site/docs/plates/goggles.md +12 -1
  74. package/docs-site/docs/plates/governance.md +1 -1
  75. package/docs-site/docs/plates/grok-compact.md +1 -1
  76. package/docs-site/docs/plates/grokbot.md +3 -1
  77. package/docs-site/docs/plates/ground.md +35 -0
  78. package/docs-site/docs/plates/hermes.md +35 -0
  79. package/docs-site/docs/plates/host-pack.md +1 -1
  80. package/docs-site/docs/plates/house.md +1 -1
  81. package/docs-site/docs/plates/index.md +16 -7
  82. package/docs-site/docs/plates/inference-cycle.md +35 -0
  83. package/docs-site/docs/plates/inference-files.md +1 -1
  84. package/docs-site/docs/plates/kept-line.md +1 -1
  85. package/docs-site/docs/plates/kits.md +1 -1
  86. package/docs-site/docs/plates/lens-gate.md +8 -6
  87. package/docs-site/docs/plates/lens-page.md +1 -1
  88. package/docs-site/docs/plates/memory-recall.md +1 -1
  89. package/docs-site/docs/plates/notes-page.md +2 -2
  90. package/docs-site/docs/plates/openclaw.md +35 -0
  91. package/docs-site/docs/plates/opencode.md +35 -0
  92. package/docs-site/docs/plates/orchestration.md +1 -1
  93. package/docs-site/docs/plates/package-face.md +1 -1
  94. package/docs-site/docs/plates/payload-heat.md +1 -1
  95. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  96. package/docs-site/docs/plates/pre-tool.md +1 -1
  97. package/docs-site/docs/plates/processor.md +1 -1
  98. package/docs-site/docs/plates/record-map.md +1 -1
  99. package/docs-site/docs/plates/reflection-page.md +1 -1
  100. package/docs-site/docs/plates/repertoire.md +35 -0
  101. package/docs-site/docs/plates/reporting.md +1 -1
  102. package/docs-site/docs/plates/review.md +1 -1
  103. package/docs-site/docs/plates/routing.md +1 -1
  104. package/docs-site/docs/plates/session-capture.md +1 -1
  105. package/docs-site/docs/plates/site-manual.md +1 -1
  106. package/docs-site/docs/plates/stamp-plate.md +1 -1
  107. package/docs-site/docs/plates/station-card.md +1 -1
  108. package/docs-site/docs/plates/station-heat.md +1 -1
  109. package/docs-site/docs/plates/suit-organs.md +1 -1
  110. package/docs-site/docs/plates/suit-settings.md +1 -1
  111. package/docs-site/docs/plates/suit-wear.md +1 -1
  112. package/docs-site/docs/plates/suit.md +3 -3
  113. package/docs-site/docs/plates/synthesis.md +35 -0
  114. package/docs-site/docs/plates/temperament.md +35 -0
  115. package/docs-site/docs/plates/trail-state.md +2 -2
  116. package/docs-site/docs/plates/work-fresh.md +1 -1
  117. package/docs-site/docs/plates/write-home.md +1 -1
  118. package/llms.txt +14 -6
  119. package/package.json +3 -3
  120. package/scripts/foundry/README.md +5 -3
  121. package/scripts/foundry/mint-suit.cjs +120 -2
  122. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  123. package/scripts/foundry/reconcile-version.mjs +10 -3
  124. package/scripts/foundry/release-gate.mjs +111 -15
  125. package/scripts/foundry/release.mjs +9 -1
  126. package/scripts/foundry/validate-release-docs.mjs +2 -2
  127. package/scripts/foundry/version-manager.mjs +12 -4
  128. package/scripts/mjs/verify-pipeline-facets.mjs +1 -1
  129. package/scripts/node/install-bridges.cjs +91 -46
  130. package/scripts/node/pack-tmp-suit-proof.mjs +37 -10
  131. package/src/integrations/openclaw/plugin/xray-pre-tool/index.js +26 -2
  132. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  133. package/src/skills/orchestrator/SKILL.md +1 -7
  134. package/vendor/@0xray/repertoire/README.md +1 -1
  135. package/xray/agents_template.md +2 -2
  136. package/xray/features.json +8 -5
  137. package/xray/features.schema.json +8 -0
  138. package/docs-site/docs/plates/live-mesh.md +0 -41
@@ -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 };
@@ -31,23 +48,40 @@ function finish(root, permission, reason, hint, toolName, extra = {}) {
31
48
  }
32
49
  if (hint && permission === 'allow') {
33
50
  out.user_message = hint;
51
+ out.agent_message = hint;
34
52
  }
53
+ if (extra.hookSpecificOutput) out.hookSpecificOutput = extra.hookSpecificOutput;
35
54
  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
- });
55
+ try {
56
+ appendHookActivity(root, 'cursor-pre-tool-use', permission, permission === 'deny' ? 'error' : 'info', {
57
+ tool: toolName,
58
+ reason: reason || hint || null,
59
+ gate: extra.gate || null,
60
+ livePath: true,
61
+ });
62
+ } catch { /* the verdict is already on stdout; activity logging must not change it */ }
42
63
  process.exit(0);
43
64
  }
44
65
 
45
66
  async function main() {
46
- const fallbackRoot = cursorWorkspaceRoot();
67
+ let fallbackRoot = process.cwd();
47
68
  let toolName = 'unknown';
48
69
 
49
70
  try {
71
+ const u = await import('./cursor-hook-utils.js');
72
+ const { lensDecision, repertoireRoute } = await import('../../hooks/pre-tool-speak.mjs');
73
+ const {
74
+ checkCodexPatterns, checkFullTestSuite, cursorGateRoot, cursorHeatRoots, cursorSessionId, cursorToolContext,
75
+ cursorWorkspaceRoot, ensureCursorSessionBoot, evaluatePreToolGate, isShellTool, isWriteTool,
76
+ loadDelegationGateFeatures, loadFeatures, readStdinJson,
77
+ } = u;
78
+ appendHookActivity = u.appendHookActivity;
79
+ fallbackRoot = cursorWorkspaceRoot();
50
80
  const event = await readStdinJson();
81
+ // readStdinJson returns {} for empty or malformed stdin; Cursor always sends a JSON object, so deny those.
82
+ if (!event || typeof event !== 'object' || Array.isArray(event) || Object.keys(event).length === 0) {
83
+ finish(fallbackRoot, 'deny', 'preToolUse: empty or malformed hook input — denied', null, toolName, { gate: 'bad-input' });
84
+ }
51
85
  const eventRoot = cursorWorkspaceRoot(event);
52
86
  const sessionId = cursorSessionId(event);
53
87
  const heatRoots = cursorHeatRoots(event);
@@ -60,7 +94,7 @@ async function main() {
60
94
  const gateFeatures = loadDelegationGateFeatures(gateRoot, 'cursor');
61
95
  const ctx = cursorToolContext(event);
62
96
  toolName = ctx.toolName;
63
- const { content, cmd, toolInput } = ctx;
97
+ const { content, cmd, toolInput, paths } = ctx;
64
98
 
65
99
  const gateBlock = evaluatePreToolGate(toolName, toolInput, {
66
100
  projectRoot: gateRoot,
@@ -77,35 +111,68 @@ async function main() {
77
111
  if (extraBlock) finish(gateRoot, 'deny', extraBlock, null, toolName);
78
112
  }
79
113
 
114
+ const spoken = [content, cmd, toolInput.pattern, toolInput.query, ...(paths || [])].filter(Boolean).join('\n');
115
+ const lensStop = lensDecision(gateRoot, toolName, spoken);
116
+ if (lensStop?.decision === 'deny') {
117
+ finish(gateRoot, 'deny', lensStop.reason, null, toolName, { gate: lensStop.gate });
118
+ }
119
+ if (lensStop?.decision === 'allow' && lensStop.reason) {
120
+ finish(gateRoot, 'allow', lensStop.reason, lensStop.reason, toolName, { gate: lensStop.gate });
121
+ }
122
+ const route = repertoireRoute([toolName, cmd, content].filter(Boolean).join(' '));
123
+
80
124
  if (isShellTool(toolName) && cmd) {
81
125
  const testHint = checkFullTestSuite(cmd, features);
82
- if (testHint) finish(gateRoot, 'allow', null, testHint, toolName);
126
+ if (testHint) finish(gateRoot, 'allow', null, testHint, toolName, { ...(route || {}) });
83
127
  }
84
128
 
85
129
  if (gateBlock.reason) {
86
130
  finish(gateRoot, 'allow', gateBlock.reason, gateBlock.hint, toolName, {
87
131
  gate: gateBlock.gate,
88
132
  warn: true,
133
+ ...(route || {}),
89
134
  });
90
135
  }
91
136
 
92
- finish(gateRoot, 'allow', null, null, toolName);
137
+ finish(gateRoot, 'allow', null, null, toolName, { ...(route || {}) });
93
138
  } 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.
139
+ if (err?.code === 'ERR_MODULE_NOT_FOUND' && cursorBootstrapPermission(err, await readRawEvent()) === 'allow') {
140
+ console.log(JSON.stringify({ permission: 'allow', user_message: 'preToolUse: build not present — bootstrap build allowed' }));
141
+ process.exit(0);
142
+ }
143
+ const failOpen = cursorHookErrorPermission(err) === 'allow';
144
+ try {
145
+ appendHookActivity(fallbackRoot, 'cursor-pre-tool-use', 'hook-error', 'error', {
146
+ tool: toolName,
147
+ error: err.message,
148
+ failOpen,
149
+ });
150
+ } catch { /* logging must not turn a deny into a crash */ }
151
+ if (failOpen) {
152
+ // Dest EACCES on a Cloud workspace wrapper must not deny every tool.
153
+ finish(
154
+ fallbackRoot,
155
+ 'allow',
156
+ `preToolUse hook error — fail open: ${err.message}`,
157
+ null,
158
+ toolName,
159
+ { gate: 'hook-error-fail-open' },
160
+ );
161
+ }
100
162
  finish(
101
163
  fallbackRoot,
102
- 'allow',
103
- `preToolUse hook error — fail open: ${err.message}`,
164
+ 'deny',
165
+ `preToolUse hook error blocked the tool: ${err.message}`,
104
166
  null,
105
167
  toolName,
106
- { gate: 'hook-error-fail-open' },
168
+ { gate: 'hook-error' },
107
169
  );
108
170
  }
109
171
  }
110
172
 
111
- main();
173
+ const launchedAsCli = Boolean(process.argv[1] && process.argv[1].endsWith('pre-tool-use.js'));
174
+ if (launchedAsCli) {
175
+ process.on('uncaughtException', (err) => denyHard(`preToolUse hook crashed — denied: ${err?.message || err}`));
176
+ process.on('unhandledRejection', (err) => denyHard(`preToolUse hook crashed — denied: ${err?.message || err}`));
177
+ main().catch((err) => denyHard(`preToolUse hook crashed — denied: ${err?.message || err}`));
178
+ }
@@ -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
 
@@ -80,17 +80,15 @@ export async function installForGrokCLI(options = {}) {
80
80
  frameworkLogger.log('grok-integration', 'plugin-copied', 'info', { destination: dest });
81
81
  console.log(`\x1b[32m✓ Copied Grok plugin to ${dest}\x1b[0m`);
82
82
  }
83
- if (millSuit.wantsCostume(targetDir)) {
84
- const skillsCopied = syncBuiltinSkills(path.join(dest, 'skills'));
85
- if (skillsCopied > 0) {
86
- console.log(`\x1b[32m✓ Synced ${skillsCopied} builtin skills to Grok plugin\x1b[0m`);
87
- }
88
- frameworkLogger.log('grok-integration', 'skills-synced', 'info', { count: skillsCopied });
83
+ const skillsCopied = syncBuiltinSkills(path.join(dest, 'skills'));
84
+ if (skillsCopied > 0) {
85
+ console.log(`\x1b[32m✓ Synced ${skillsCopied} builtin skills to Grok plugin\x1b[0m`);
89
86
  }
87
+ frameworkLogger.log('grok-integration', 'skills-synced', 'info', { count: skillsCopied });
90
88
  pinGrokPluginToInstalledDist(dest, packageRoot, targetDir);
91
89
  wearGrokHookCommands(dest, packageRoot, targetDir);
92
90
  }
93
- if (isolated && millSuit.wantsCostume(targetDir)) {
91
+ if (isolated) {
94
92
  const home = millSuit.processHome(env);
95
93
  const globalCopied = syncBuiltinSkills(path.join(home, '.grok', 'skills'));
96
94
  if (globalCopied > 0) {
@@ -28,7 +28,7 @@ import {
28
28
  retainCompactFields,
29
29
  writeStationMarkdown,
30
30
  } from '../../hooks/station-hook-runtime.mjs';
31
- import { handCard } from '../../hooks/goggles-pipeline.mjs';
31
+ import { handCard, planeCard } from '../../hooks/goggles-pipeline.mjs';
32
32
 
33
33
  export {
34
34
  checkPendingDelegationGate,
@@ -315,6 +315,7 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
315
315
  const existing = readExistingBoot(root);
316
316
  const heat = applyStationHeat(root, extra.host || 'grok', extra, existing);
317
317
  const compactHold = retainCompactFields(existing, extra);
318
+ const shown = planeCard(root);
318
319
  return {
319
320
  lead_dev_mode: features.lead_dev_mode,
320
321
  no_new_surface: features.no_new_surface,
@@ -348,6 +349,8 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
348
349
  hook: source,
349
350
  source,
350
351
  timestamp: new Date().toISOString(),
352
+ plane: shown.plane,
353
+ card: shown.card,
351
354
  };
352
355
  }
353
356
 
@@ -21,66 +21,10 @@ import {
21
21
  workspaceRoot,
22
22
  } from './grok-hook-utils.js';
23
23
  import { appendHookActivity } from './grok-hook-activity.js';
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
- }
24
+ import { lensBeforeResearch } from '../../hooks/goggles-pipeline.mjs';
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 };
@@ -137,28 +81,14 @@ async function main() {
137
81
  if (extraBlock) finish(eventRoot, 'deny', extraBlock, null, toolName);
138
82
  }
139
83
 
140
- const stop = cardStop(
141
- eventRoot,
142
- toolName,
143
- [cmd, content, toolInput.pattern, toolInput.query, ...(ctx.paths || [])].filter(Boolean).join('\n'),
144
- ctx.paths || [],
145
- toolInput,
146
- );
147
- if (stop?.decision === 'deny') {
148
- finish(eventRoot, 'deny', stop.reason, null, toolName, { gate: stop.gate });
149
- }
150
- if (stop?.decision === 'allow') {
151
- finish(eventRoot, 'allow', null, null, toolName, {
152
- gate: stop.gate,
153
- hookSpecificOutput: stop.hookSpecificOutput,
154
- });
155
- }
156
-
157
84
  const spoken = [cmd, content, toolInput.pattern, toolInput.query, ...(ctx.paths || [])].filter(Boolean).join('\n');
158
85
  const lensStop = lensBeforeResearch(eventRoot, toolName, spoken);
159
86
  if (lensStop?.decision === 'deny') {
160
87
  finish(eventRoot, 'deny', lensStop.reason, null, toolName, { gate: lensStop.gate });
161
88
  }
89
+ if (lensStop?.decision === 'allow' && lensStop.reason) {
90
+ finish(eventRoot, 'allow', lensStop.reason, lensStop.reason, toolName, { gate: lensStop.gate });
91
+ }
162
92
 
163
93
  const route = repertoireRoute([toolName, cmd, content].filter(Boolean).join(' '));
164
94
 
@@ -24,7 +24,7 @@ import {
24
24
  runInferenceImprovementLight,
25
25
  } from '../../hooks/pipeline-hook-runtime.mjs';
26
26
  import { maintainLens } from '../../hooks/goggles-pipeline.mjs';
27
- import { readSavedFreshness, refreshFreshness } from '../../../nucleus/work-freshness.mjs';
27
+ import { freshnessNeedsRefresh, readSavedFreshness, refreshFreshness } from '../../../nucleus/work-freshness.mjs';
28
28
 
29
29
  function extractIntent(event) {
30
30
  return event.prompt || event.userMessage || event.user_prompt || event.compactContext || null;
@@ -137,7 +137,9 @@ async function main() {
137
137
  let freshnessLine = null;
138
138
  if (!compact) {
139
139
  try {
140
- freshnessLine = HOOK_EVENT === 'session_start'
140
+ const refresh = HOOK_EVENT === 'session_start'
141
+ || (HOOK_EVENT === 'user_prompt_submit' && freshnessNeedsRefresh(eventRoot));
142
+ freshnessLine = refresh
141
143
  ? refreshFreshness(eventRoot).freshnessLine
142
144
  : readSavedFreshness(eventRoot);
143
145
  } catch {
@@ -914,9 +914,25 @@ 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?.decision === "deny") {
924
+ return {
925
+ allow: false,
926
+ phase: "pre",
927
+ gate: lens.gate || "lens",
928
+ reason: lens.reason,
929
+ };
930
+ }
931
+ if (lens?.decision === "allow" && lens.reason) {
932
+ return { phase: "pre", ...outcome, gate: "lens", reason: lens.reason };
933
+ }
934
+ const route = repertoireRoute(spoken);
935
+ return { phase: "pre", ...outcome, ...(route || {}) };
920
936
  } catch (error) {
921
937
  const message = `delegation-gate failed: ${error.message || error}`;
922
938
  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);