0xray 4.0.12 → 4.0.14

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 (58) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +1 -1
  3. package/.xray/features.schema.json +1 -1
  4. package/AGENTS-consumer.md +4 -2
  5. package/AGENTS.md +8 -1
  6. package/README.md +1 -1
  7. package/SKILLS.md +1 -1
  8. package/dist/AGENTS.md +8 -1
  9. package/dist/CHANGELOG.md +25 -1
  10. package/dist/README.md +1 -1
  11. package/dist/SKILLS.md +1 -1
  12. package/dist/integrations/cursor/README.md +61 -0
  13. package/dist/integrations/cursor/hooks/after-file-edit.js +38 -0
  14. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +170 -0
  15. package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +298 -0
  16. package/dist/integrations/cursor/hooks/hooks.json +20 -0
  17. package/dist/integrations/cursor/hooks/pre-compact.js +88 -0
  18. package/dist/integrations/cursor/hooks/pre-tool-use.js +105 -0
  19. package/dist/integrations/hooks/station-hook-runtime.cjs +89 -1
  20. package/dist/integrations/hooks/station-hook-runtime.mjs +2 -0
  21. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  22. package/dist/nucleus/suit-temperament.d.ts +2 -2
  23. package/dist/nucleus/suit-temperament.js +3 -1
  24. package/llms.txt +6 -4
  25. package/package.json +4 -2
  26. package/scripts/foundry/CHANGELOG.md +37 -1
  27. package/scripts/foundry/README.md +13 -1
  28. package/scripts/foundry/assert-packed-dist-cli.mjs +97 -0
  29. package/scripts/foundry/blip-render.cjs +1237 -0
  30. package/scripts/foundry/blip-rippel.cjs +1963 -0
  31. package/scripts/foundry/blip.mjs +109 -0
  32. package/scripts/foundry/cli.mjs +8 -4
  33. package/scripts/foundry/inspect.mjs +117 -7
  34. package/scripts/foundry/mint-suit.cjs +215 -14
  35. package/scripts/foundry/package.json +7 -1
  36. package/scripts/foundry/plant/agents/blip-inspect.yml +5 -0
  37. package/scripts/foundry/plant/agents/blip.yml +5 -0
  38. package/scripts/foundry/plant/agents/inspect.yml +1 -1
  39. package/scripts/foundry/plant/agents/mill.yml +1 -1
  40. package/scripts/foundry/plant/agents/sound-inspect.yml +5 -0
  41. package/scripts/foundry/plant/agents/sound.yml +5 -0
  42. package/scripts/foundry/plant/motions/MOTION-REGISTRY.md +35 -0
  43. package/scripts/foundry/plant/motions/registry.json +81 -0
  44. package/scripts/foundry/plant/skills/blip/SKILL.md +39 -0
  45. package/scripts/foundry/plant/skills/blip-inspect/SKILL.md +32 -0
  46. package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -2
  47. package/scripts/foundry/plant/skills/sound/SKILL.md +29 -0
  48. package/scripts/foundry/plant/skills/sound-inspect/SKILL.md +28 -0
  49. package/scripts/foundry/release-gate.mjs +18 -10
  50. package/scripts/foundry/release.mjs +3 -0
  51. package/scripts/foundry/sound-bed.cjs +454 -0
  52. package/scripts/foundry/sound-rippel.cjs +1434 -0
  53. package/scripts/foundry/sound.mjs +93 -0
  54. package/scripts/node/assert-packed-dist-cli.mjs +4 -0
  55. package/scripts/node/consumer-install-smoke.mjs +20 -1
  56. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  57. package/xray/features.json +1 -1
  58. package/xray/features.schema.json +1 -1
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Cursor Cloud real-usage receipt — no delegation-gate / dist required.
3
+ * FILL / chars÷4 is never usage proof. Host fields or Cloud MCP / dashboard only.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { buildRepertoireResume } from '../../hooks/station-hook-runtime.mjs';
9
+
10
+ export const EVENT_CLASS_HOST = 'cursor-host-precompact';
11
+ export const EVENT_CLASS_FAIL = 'cursor-host-precompact-FAIL';
12
+
13
+ export const FORBIDDEN_USAGE_METHODS = new Set([
14
+ 'chars-div-4',
15
+ 'chars/4',
16
+ 'chars÷4',
17
+ 'fill-only',
18
+ 'fill',
19
+ 'utf8-bytes-div-4',
20
+ 'bytes-div-4',
21
+ ]);
22
+
23
+ export function invokeProbeLogPath(root) {
24
+ return path.join(root, '.xray', 'state', 'cursor-hook-invoke.log');
25
+ }
26
+
27
+ export function cursorUsageReceiptPath(root) {
28
+ return path.join(root, '.xray', 'state', 'cursor-usage-receipt.json');
29
+ }
30
+
31
+ export function parseInvokeProbeLog(root) {
32
+ const dest = invokeProbeLogPath(root);
33
+ const counts = { preToolUse: 0, preCompact: 0, afterFileEdit: 0, unknown: 0 };
34
+ if (!fs.existsSync(dest)) {
35
+ return { exists: false, path: dest, counts, hostPreCompactFired: false };
36
+ }
37
+ let text = '';
38
+ try {
39
+ text = fs.readFileSync(dest, 'utf8');
40
+ } catch {
41
+ return { exists: false, path: dest, counts, hostPreCompactFired: false };
42
+ }
43
+ for (const line of text.split(/\r?\n/)) {
44
+ if (!line.trim()) continue;
45
+ const match = /(?:^|\s)event=([A-Za-z]+)/.exec(line);
46
+ const event = match ? match[1] : 'unknown';
47
+ if (event === 'preToolUse') counts.preToolUse += 1;
48
+ else if (event === 'preCompact') counts.preCompact += 1;
49
+ else if (event === 'afterFileEdit') counts.afterFileEdit += 1;
50
+ else counts.unknown += 1;
51
+ }
52
+ return {
53
+ exists: true,
54
+ path: dest,
55
+ counts,
56
+ hostPreCompactFired: counts.preCompact > 0,
57
+ };
58
+ }
59
+
60
+ export function hostUsageFromPreCompactEvent(event = {}) {
61
+ const usage = { source: 'precompact-stdin', method: 'host-field' };
62
+ if (typeof event.context_tokens === 'number') usage.context_tokens = event.context_tokens;
63
+ if (typeof event.context_usage_percent === 'number') {
64
+ usage.context_usage_percent = event.context_usage_percent;
65
+ }
66
+ if (typeof event.context_window_size === 'number') {
67
+ usage.context_window_size = event.context_window_size;
68
+ }
69
+ if (typeof event.windowCite === 'string' && event.windowCite) usage.windowCite = event.windowCite;
70
+ if (typeof event.model === 'string' && event.model) usage.model = event.model;
71
+ if (typeof event.bcId === 'string' && event.bcId) usage.bcId = event.bcId;
72
+ return usage;
73
+ }
74
+
75
+ function looksLikeFillCite(cite = {}) {
76
+ const method = String(cite.method || '').trim().toLowerCase();
77
+ if (FORBIDDEN_USAGE_METHODS.has(method)) return true;
78
+ if (cite.fillBytes != null || cite.charsDiv4 != null || cite.fillOnly === true) return true;
79
+ return false;
80
+ }
81
+
82
+ function hasHostTokenFields(cite = {}) {
83
+ return (
84
+ typeof cite.context_tokens === 'number' ||
85
+ typeof cite.context_usage_percent === 'number' ||
86
+ typeof cite.context_window_size === 'number'
87
+ );
88
+ }
89
+
90
+ function hasMcpIdentity(cite = {}) {
91
+ const source = String(cite.source || '').trim();
92
+ if (source === 'cursor-cloud-run-info' || source === 'cursor-cloud-events' || source === 'dashboard') {
93
+ return true;
94
+ }
95
+ return Boolean(cite.model || cite.bcId);
96
+ }
97
+
98
+ export function classifyUsageCite(cite = {}) {
99
+ const method = String(cite.method || '').trim().toLowerCase();
100
+ const source = String(cite.source || '').trim() || null;
101
+ const windowCite = typeof cite.windowCite === 'string' && cite.windowCite ? cite.windowCite : null;
102
+ const model = typeof cite.model === 'string' && cite.model ? cite.model : null;
103
+ const bcId = typeof cite.bcId === 'string' && cite.bcId ? cite.bcId : null;
104
+ const tokens = typeof cite.context_tokens === 'number' ? cite.context_tokens : null;
105
+ const percent = typeof cite.context_usage_percent === 'number' ? cite.context_usage_percent : null;
106
+ const windowSize = typeof cite.context_window_size === 'number' ? cite.context_window_size : null;
107
+
108
+ if (looksLikeFillCite(cite)) {
109
+ return {
110
+ ok: false,
111
+ forbidden: true,
112
+ miss: false,
113
+ reason: 'FILL / chars÷4 is forbidden as usage proof',
114
+ method: method || 'fill',
115
+ source,
116
+ context_tokens: null,
117
+ context_usage_percent: null,
118
+ context_window_size: null,
119
+ windowCite,
120
+ model,
121
+ bcId,
122
+ };
123
+ }
124
+
125
+ if (hasHostTokenFields(cite)) {
126
+ return {
127
+ ok: true,
128
+ forbidden: false,
129
+ miss: false,
130
+ reason: null,
131
+ method: method || 'host-field',
132
+ source: source || 'precompact-stdin',
133
+ context_tokens: tokens,
134
+ context_usage_percent: percent,
135
+ context_window_size: windowSize,
136
+ windowCite,
137
+ model,
138
+ bcId,
139
+ };
140
+ }
141
+
142
+ if (hasMcpIdentity(cite)) {
143
+ return {
144
+ ok: true,
145
+ forbidden: false,
146
+ miss: true,
147
+ reason: 'MCP/dashboard identity present; token counts MISS (do not fill)',
148
+ method: method || source || 'mcp',
149
+ source,
150
+ context_tokens: null,
151
+ context_usage_percent: null,
152
+ context_window_size: null,
153
+ windowCite,
154
+ model,
155
+ bcId,
156
+ };
157
+ }
158
+
159
+ return {
160
+ ok: false,
161
+ forbidden: false,
162
+ miss: true,
163
+ reason: 'no host/MCP usage fields; do not fill',
164
+ method: method || null,
165
+ source,
166
+ context_tokens: null,
167
+ context_usage_percent: null,
168
+ context_window_size: null,
169
+ windowCite,
170
+ model,
171
+ bcId,
172
+ };
173
+ }
174
+
175
+ export function buildRepertoireFastenedProof(root) {
176
+ const resume = buildRepertoireResume(root);
177
+ const npmPath = path.join(root, 'node_modules', '@0xray', 'repertoire');
178
+ const vendorPath = path.join(root, 'vendor', '@0xray', 'repertoire');
179
+ const pkgFile = fs.existsSync(path.join(npmPath, 'package.json'))
180
+ ? path.join(npmPath, 'package.json')
181
+ : path.join(vendorPath, 'package.json');
182
+ let name = null;
183
+ let version = null;
184
+ try {
185
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
186
+ name = pkg.name || null;
187
+ version = pkg.version || null;
188
+ } catch {
189
+ /* organ absent */
190
+ }
191
+ const providerExists = fs.existsSync(
192
+ path.join(npmPath, 'dist', 'provider', 'memory-routing-provider.js'),
193
+ );
194
+ const signalsExists = fs.existsSync(path.join(npmPath, 'data', 'curated_signals.json'));
195
+ return {
196
+ fastened: name === '@0xray/repertoire' && providerExists,
197
+ resume,
198
+ name,
199
+ version,
200
+ npmPath: fs.existsSync(npmPath) ? npmPath : null,
201
+ vendorPath: fs.existsSync(vendorPath) ? vendorPath : null,
202
+ providerExists,
203
+ signalsExists,
204
+ };
205
+ }
206
+
207
+ export function buildCursorUsageReceipt(root, input = {}) {
208
+ const probe = parseInvokeProbeLog(root);
209
+ const usage = classifyUsageCite(input.usage || {});
210
+ const repertoire = buildRepertoireFastenedProof(root);
211
+ const eventClass =
212
+ typeof input.eventClass === 'string'
213
+ ? input.eventClass
214
+ : probe.hostPreCompactFired
215
+ ? EVENT_CLASS_HOST
216
+ : EVENT_CLASS_FAIL;
217
+ return {
218
+ ok: usage.ok && !usage.forbidden,
219
+ protocol: 'real-work-ab',
220
+ timestamp: new Date().toISOString(),
221
+ sessionId: input.sessionId || null,
222
+ compact: {
223
+ hostFired: probe.hostPreCompactFired,
224
+ preCompactCount: probe.counts.preCompact,
225
+ preToolUseCount: probe.counts.preToolUse,
226
+ afterFileEditCount: probe.counts.afterFileEdit,
227
+ eventClass,
228
+ probeLogExists: probe.exists,
229
+ },
230
+ usage,
231
+ repertoire,
232
+ hooksAtBoot: input.hooksAtBoot === true || input.hooksAtBoot === false ? input.hooksAtBoot : null,
233
+ };
234
+ }
235
+
236
+ function upsertStationKeyedLine(card, key, value) {
237
+ const line = `${key}: ${value}`;
238
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
239
+ const re = new RegExp(`^${escaped}:.*$`, 'm');
240
+ if (re.test(card)) return card.replace(re, line);
241
+ const marker = 'Continue this card.';
242
+ const idx = card.lastIndexOf(marker);
243
+ if (idx >= 0) return `${card.slice(0, idx)}${line}\n\n${card.slice(idx)}`;
244
+ return `${card.trimEnd()}\n${line}\n`;
245
+ }
246
+
247
+ /** Host numeric window first. Ticket `windowCite` is not a substitute. Do not derive from tokens÷percent. */
248
+ export function stationUsageWindowLabel(usage = {}) {
249
+ if (typeof usage.context_window_size === 'number') return String(usage.context_window_size);
250
+ if (typeof usage.windowCite === 'string' && usage.windowCite.trim()) return usage.windowCite.trim();
251
+ return 'MISS';
252
+ }
253
+
254
+ export function upsertStationCompactRow(root, receipt) {
255
+ const dest = path.join(root, '.xray', 'state', 'STATION.md');
256
+ if (!fs.existsSync(dest)) return null;
257
+ try {
258
+ const fired = receipt.compact?.hostFired ? 'Y' : 'N';
259
+ const count = receipt.compact?.preCompactCount ?? 0;
260
+ const usageBit = receipt.usage?.forbidden
261
+ ? 'FORBIDDEN-FILL'
262
+ : receipt.usage?.miss
263
+ ? 'MISS'
264
+ : 'host-field';
265
+ const tokens =
266
+ typeof receipt.usage?.context_tokens === 'number' ? String(receipt.usage.context_tokens) : 'MISS';
267
+ const heat = receipt.repertoire?.fastened ? 'fastened' : 'absent';
268
+ const windowLabel = stationUsageWindowLabel(receipt.usage || {});
269
+ let card = fs.readFileSync(dest, 'utf8');
270
+ card = upsertStationKeyedLine(
271
+ card,
272
+ 'Compact',
273
+ `preCompact ${fired} (count=${count}) · usage ${usageBit} · repertoire ${heat}`,
274
+ );
275
+ card = upsertStationKeyedLine(
276
+ card,
277
+ 'Usage',
278
+ `source=${receipt.usage?.source || 'none'} model=${receipt.usage?.model || 'MISS'} window=${windowLabel} tokens=${tokens}`,
279
+ );
280
+ fs.writeFileSync(dest, card);
281
+ return dest;
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+
287
+ export function writeCursorUsageReceipt(root, input = {}) {
288
+ try {
289
+ const receipt = buildCursorUsageReceipt(root, input);
290
+ const dest = cursorUsageReceiptPath(root);
291
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
292
+ fs.writeFileSync(dest, JSON.stringify(receipt, null, 2));
293
+ upsertStationCompactRow(root, receipt);
294
+ return dest;
295
+ } catch {
296
+ return null;
297
+ }
298
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "preToolUse": [
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\""
7
+ }
8
+ ],
9
+ "preCompact": [
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\""
12
+ }
13
+ ],
14
+ "afterFileEdit": [
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\""
17
+ }
18
+ ]
19
+ }
20
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cursor preCompact — Station writer. Observational; cannot block compaction.
4
+ * Host fire → event_class: cursor-host-precompact
5
+ * Manual/script invoke → pass --event-class=cursor-precompact-synthetic
6
+ */
7
+
8
+ import {
9
+ appendHookActivity,
10
+ buildSessionBootPayload,
11
+ classifyPreCompactEvent,
12
+ cursorSessionId,
13
+ cursorWorkspaceRoot,
14
+ readStdinJson,
15
+ writeCursorPrecompactReceipt,
16
+ writeSessionBoot,
17
+ } from './cursor-hook-utils.js';
18
+ import {
19
+ hostUsageFromPreCompactEvent,
20
+ writeCursorUsageReceipt,
21
+ } from './cursor-usage-receipt.js';
22
+
23
+ function extractIntent(event) {
24
+ return (
25
+ event.prompt ||
26
+ event.userMessage ||
27
+ event.user_prompt ||
28
+ event.compactContext ||
29
+ event.user_message ||
30
+ null
31
+ );
32
+ }
33
+
34
+ async function main() {
35
+ const fallbackRoot = cursorWorkspaceRoot();
36
+ let eventClass = 'cursor-precompact-synthetic';
37
+ try {
38
+ const event = await readStdinJson();
39
+ eventClass = classifyPreCompactEvent(event);
40
+ const eventRoot = cursorWorkspaceRoot(event);
41
+ const sessionId = cursorSessionId(event);
42
+ 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
+ });
64
+ appendHookActivity(eventRoot, 'cursor-pre-compact', 'station-written', 'success', {
65
+ bootPath,
66
+ receiptPath,
67
+ usagePath,
68
+ event_class: eventClass,
69
+ stationLine: payload.stationLine,
70
+ });
71
+ console.log(
72
+ JSON.stringify({
73
+ user_message:
74
+ `0xRay Station written. Read .xray/state/STATION.md — do not cold-start. event_class=${eventClass}`,
75
+ }),
76
+ );
77
+ process.exit(0);
78
+ } catch (err) {
79
+ appendHookActivity(fallbackRoot, 'cursor-pre-compact', 'station-error', 'error', {
80
+ error: err.message,
81
+ event_class: eventClass,
82
+ });
83
+ console.log(JSON.stringify({ user_message: '0xRay Station write failed; Read STATION.md if present.' }));
84
+ process.exit(0);
85
+ }
86
+ }
87
+
88
+ main();
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cursor preToolUse — 0xRay constitution gate.
4
+ * Contract: stdin JSON → stdout {"permission":"allow"|"deny"}
5
+ * Does not rely on sessionStart (unavailable on managed cloud).
6
+ */
7
+
8
+ import {
9
+ appendHookActivity,
10
+ checkCodexPatterns,
11
+ checkFullTestSuite,
12
+ cursorSessionId,
13
+ cursorToolContext,
14
+ cursorWorkspaceRoot,
15
+ ensureCursorSessionBoot,
16
+ evaluatePreToolGate,
17
+ isShellTool,
18
+ isWriteTool,
19
+ loadDelegationGateFeatures,
20
+ loadFeatures,
21
+ readStdinJson,
22
+ } from './cursor-hook-utils.js';
23
+
24
+ function finish(root, permission, reason, hint, toolName, extra = {}) {
25
+ const out = { permission };
26
+ if (reason) {
27
+ out.user_message = reason;
28
+ out.agent_message = reason;
29
+ }
30
+ if (hint && permission === 'allow') {
31
+ out.user_message = hint;
32
+ }
33
+ console.log(JSON.stringify(out));
34
+ appendHookActivity(root, 'cursor-pre-tool-use', permission, permission === 'deny' ? 'error' : 'info', {
35
+ tool: toolName,
36
+ reason: reason || hint || null,
37
+ gate: extra.gate || null,
38
+ livePath: true,
39
+ });
40
+ process.exit(0);
41
+ }
42
+
43
+ async function main() {
44
+ const fallbackRoot = cursorWorkspaceRoot();
45
+ let toolName = 'unknown';
46
+
47
+ try {
48
+ const event = await readStdinJson();
49
+ const eventRoot = cursorWorkspaceRoot(event);
50
+ ensureCursorSessionBoot(eventRoot, '0xray/cursor-pre-tool-use-boot', {
51
+ sessionId: cursorSessionId(event),
52
+ });
53
+
54
+ const features = loadFeatures(eventRoot);
55
+ const gateFeatures = loadDelegationGateFeatures(eventRoot, 'cursor');
56
+ const ctx = cursorToolContext(event);
57
+ toolName = ctx.toolName;
58
+ const { content, cmd, toolInput } = ctx;
59
+ const sessionId = cursorSessionId(event);
60
+
61
+ const gateBlock = evaluatePreToolGate(toolName, toolInput, {
62
+ projectRoot: eventRoot,
63
+ sessionId,
64
+ features: gateFeatures,
65
+ host: 'cursor',
66
+ });
67
+ if (!gateBlock.allow) {
68
+ finish(eventRoot, 'deny', gateBlock.reason, gateBlock.hint, toolName, { gate: gateBlock.gate });
69
+ }
70
+
71
+ if (isWriteTool(toolName) && content) {
72
+ const extraBlock = checkCodexPatterns(content, { terms: [11, 29] });
73
+ if (extraBlock) finish(eventRoot, 'deny', extraBlock, null, toolName);
74
+ }
75
+
76
+ if (isShellTool(toolName) && cmd) {
77
+ const testHint = checkFullTestSuite(cmd, features);
78
+ if (testHint) finish(eventRoot, 'allow', null, testHint, toolName);
79
+ }
80
+
81
+ if (gateBlock.reason) {
82
+ finish(eventRoot, 'allow', gateBlock.reason, gateBlock.hint, toolName, {
83
+ gate: gateBlock.gate,
84
+ warn: true,
85
+ });
86
+ }
87
+
88
+ finish(eventRoot, 'allow', null, null, toolName);
89
+ } catch (err) {
90
+ appendHookActivity(fallbackRoot, 'cursor-pre-tool-use', 'hook-error', 'error', {
91
+ tool: toolName,
92
+ error: err.message,
93
+ });
94
+ finish(
95
+ fallbackRoot,
96
+ 'deny',
97
+ `preToolUse hook error — blocked for safety: ${err.message}`,
98
+ null,
99
+ toolName,
100
+ { gate: 'hook-error' },
101
+ );
102
+ }
103
+ }
104
+
105
+ main();
@@ -2,6 +2,7 @@
2
2
  * Station heat — compaction / host-swap card.
3
3
  * SSOT remains session-boot.json. STATION.md is the projection the model Reads.
4
4
  * Grok ignores SessionStart/UserPromptSubmit stdout; disk + AGENTS.md is the contract.
5
+ * Heat writers merge stock fields; unknown keys and ## Durable / ## Seed survive.
5
6
  */
6
7
  const { execFileSync } = require("child_process");
7
8
  const { existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");
@@ -363,6 +364,80 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
363
364
  };
364
365
  }
365
366
 
367
+ const STOCK_STATION_PREFIXES = [
368
+ "hot-swap:",
369
+ "host:",
370
+ "intent:",
371
+ "plan:",
372
+ "git:",
373
+ "repertoire:",
374
+ "working:",
375
+ ];
376
+
377
+ const STOCK_STATION_FOOTERS = [
378
+ "continue this card. compaction and host change are the same cut. do not cold-start.",
379
+ "grok does not inject this file — read it. opencode injects. do not thicken the grok exo.",
380
+ ];
381
+
382
+ function isDurableStationHeading(line) {
383
+ return /^##\s+(durable|seed)\b/i.test(String(line || "").trim());
384
+ }
385
+
386
+ function isStockStationLine(line) {
387
+ const trimmed = String(line || "").trim();
388
+ if (!trimmed) return true;
389
+ if (/^#\s+station\s*$/i.test(trimmed)) return true;
390
+ const lower = trimmed.toLowerCase();
391
+ if (STOCK_STATION_FOOTERS.includes(lower)) return true;
392
+ return STOCK_STATION_PREFIXES.some((prefix) => lower.startsWith(prefix));
393
+ }
394
+
395
+ /** Keep unknown keys and ## Durable / ## Seed blocks across heat rewrites. */
396
+ function extractPreservedStationLines(existing) {
397
+ if (!existing || typeof existing !== "string") return [];
398
+ const preserved = [];
399
+ let inDurable = false;
400
+ for (const line of existing.split(/\r?\n/)) {
401
+ if (isDurableStationHeading(line)) {
402
+ inDurable = true;
403
+ preserved.push(line);
404
+ continue;
405
+ }
406
+ if (inDurable) {
407
+ const trimmed = String(line || "").trim();
408
+ const otherHeading = /^##\s+\S/.test(trimmed) && !isDurableStationHeading(line);
409
+ // Stock prefixes, blank lines, and stock footers close the section.
410
+ // Otherwise Seed swallows Compact/Usage/footers and each heat pastes another footer.
411
+ if (otherHeading || isStockStationLine(line)) {
412
+ inDurable = false;
413
+ } else {
414
+ preserved.push(line);
415
+ continue;
416
+ }
417
+ }
418
+ if (isStockStationLine(line)) continue;
419
+ preserved.push(line);
420
+ }
421
+ while (preserved.length && !preserved[0].trim()) preserved.shift();
422
+ while (preserved.length && !preserved[preserved.length - 1].trim()) preserved.pop();
423
+ return preserved;
424
+ }
425
+
426
+ function mergeStationMarkdown(stockMd, existingMd) {
427
+ const preserved = extractPreservedStationLines(existingMd);
428
+ if (!preserved.length) return stockMd;
429
+ const stock = String(stockMd || "");
430
+ const block = preserved.join("\n");
431
+ const marker = "Continue this card.";
432
+ const idx = stock.indexOf(marker);
433
+ if (idx === -1) {
434
+ return `${stock.replace(/\s*$/, "")}\n\n${block}\n`;
435
+ }
436
+ const head = stock.slice(0, idx).replace(/\s*$/, "");
437
+ const foot = stock.slice(idx);
438
+ return `${head}\n\n${block}\n\n${foot}`;
439
+ }
440
+
366
441
  function formatStationMarkdown(fields) {
367
442
  const host = fields.host || "unknown";
368
443
  const profile = fields.suit_profile || "guided";
@@ -389,12 +464,23 @@ function formatStationMarkdown(fields) {
389
464
  return lines.join("\n");
390
465
  }
391
466
 
467
+ function readExistingStationMarkdown(root) {
468
+ const dest = stationMarkdownPath(root);
469
+ if (!existsSync(dest)) return "";
470
+ try {
471
+ return readFileSync(dest, "utf8");
472
+ } catch {
473
+ return "";
474
+ }
475
+ }
476
+
392
477
  function writeStationMarkdown(root, fields) {
393
478
  try {
394
479
  const dir = join(root, ".xray", "state");
395
480
  mkdirSync(dir, { recursive: true });
396
481
  const dest = stationMarkdownPath(root);
397
- writeFileSync(dest, formatStationMarkdown(fields));
482
+ const next = mergeStationMarkdown(formatStationMarkdown(fields), readExistingStationMarkdown(root));
483
+ writeFileSync(dest, next);
398
484
  return dest;
399
485
  } catch {
400
486
  return null;
@@ -416,6 +502,8 @@ module.exports = {
416
502
  readRepertoireWorking,
417
503
  formatWorkingLine,
418
504
  applyStationHeat,
505
+ extractPreservedStationLines,
506
+ mergeStationMarkdown,
419
507
  formatStationMarkdown,
420
508
  writeStationMarkdown,
421
509
  };
@@ -20,5 +20,7 @@ export const persistRepertoireWorking = impl.persistRepertoireWorking;
20
20
  export const readRepertoireWorking = impl.readRepertoireWorking;
21
21
  export const formatWorkingLine = impl.formatWorkingLine;
22
22
  export const applyStationHeat = impl.applyStationHeat;
23
+ export const extractPreservedStationLines = impl.extractPreservedStationLines;
24
+ export const mergeStationMarkdown = impl.mergeStationMarkdown;
23
25
  export const formatStationMarkdown = impl.formatStationMarkdown;
24
26
  export const writeStationMarkdown = impl.writeStationMarkdown;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.12",
3
+ "version": "4.0.14",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -5,11 +5,11 @@
5
5
  * This module does not replace thinDispatch or Codex. It only scales *ceremony*
6
6
  * (spawn-plan deny, synthesis, confer) by host strength.
7
7
  *
8
- * Frontier (Grok 4.6 / Grok Build): engine available, spawn warns if no plan.
8
+ * Frontier (Grok 4.6 / Grok Build / Cursor cloud): engine available, spawn warns if no plan.
9
9
  * Guided (OpenCode / Hermes / OpenClaw, including free models): full v2 ceremony.
10
10
  * Missing config → guided (existing consumers unchanged).
11
11
  */
12
- export type SuitHost = 'grok' | 'hermes' | 'opencode' | 'openclaw' | 'generic';
12
+ export type SuitHost = 'grok' | 'cursor' | 'hermes' | 'opencode' | 'openclaw' | 'generic';
13
13
  export type SuitProfile = 'frontier' | 'guided' | 'strict';
14
14
  export type SuitProfileOrAuto = 'auto' | SuitProfile;
15
15
  export type CeremonyLevel = 'full' | 'lite';
@@ -5,7 +5,7 @@
5
5
  * This module does not replace thinDispatch or Codex. It only scales *ceremony*
6
6
  * (spawn-plan deny, synthesis, confer) by host strength.
7
7
  *
8
- * Frontier (Grok 4.6 / Grok Build): engine available, spawn warns if no plan.
8
+ * Frontier (Grok 4.6 / Grok Build / Cursor cloud): engine available, spawn warns if no plan.
9
9
  * Guided (OpenCode / Hermes / OpenClaw, including free models): full v2 ceremony.
10
10
  * Missing config → guided (existing consumers unchanged).
11
11
  */
@@ -16,6 +16,7 @@ const requireCjs = createRequire(import.meta.url);
16
16
  const stationHeat = requireCjs('../integrations/hooks/station-hook-runtime.cjs');
17
17
  export const DEFAULT_HOST_PROFILES = {
18
18
  grok: 'frontier',
19
+ cursor: 'frontier',
19
20
  hermes: 'guided',
20
21
  opencode: 'guided',
21
22
  openclaw: 'guided',
@@ -38,6 +39,7 @@ export function loadSuitTemperamentRaw(projectRoot) {
38
39
  }
39
40
  function isSuitHost(value) {
40
41
  return (value === 'grok' ||
42
+ value === 'cursor' ||
41
43
  value === 'hermes' ||
42
44
  value === 'opencode' ||
43
45
  value === 'openclaw' ||