0xray 4.0.17 → 4.0.18

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.17",
3
+ "version": "4.0.18",
4
4
  "description": "0xRay suit for Grok, Hermes, OpenClaw, OpenCode: mill+inspect, Codex, 7 MCP servers. Costume 45/42 is opt-in foundry, not the default wear.",
5
5
  "author": {
6
6
  "name": "0xRay AI",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.17",
3
+ "version": "4.0.18",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",
package/dist/CHANGELOG.md CHANGED
@@ -34,6 +34,22 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
34
34
 
35
35
  ---
36
36
 
37
+ ## [4.0.18] - 2026-09-22
38
+
39
+ ### 🔄 Changes
40
+
41
+ ### ✨ Features
42
+ - feat: wake heat joins bookmark, index, and mind (ad596aae2)
43
+
44
+ ### 🐛 Bug Fixes
45
+ - fix: pin subject hits on wake rematch (65de9d37a)
46
+ - fix: wake match loads 0.2.5 and keeps subject hits (80b61236d)
47
+
48
+ ### 📚 Documentation
49
+ - docs: memory-wake join — BRAIN-003 (1c128e69c)
50
+
51
+ ---
52
+
37
53
  ## [4.0.17] - 2026-09-22
38
54
 
39
55
  ### 🔄 Changes
@@ -4,7 +4,6 @@
4
4
  * sessionStart is unavailable on managed cloud; first preToolUse / afterFileEdit boots.
5
5
  */
6
6
 
7
- import { execFileSync } from 'node:child_process';
8
7
  import fs from 'fs';
9
8
  import path from 'path';
10
9
  import {
@@ -25,10 +24,13 @@ import {
25
24
  import { appendHookActivity } from '../../grok/hooks/grok-hook-activity.js';
26
25
  import {
27
26
  buildRepertoireResume,
27
+ maybeCaptureSessionOnHeadMove,
28
28
  readGitBrief,
29
29
  stationDurableHoldsNpm,
30
30
  } from '../../hooks/station-hook-runtime.mjs';
31
31
 
32
+ export { maybeCaptureSessionOnHeadMove };
33
+
32
34
  export const CURSOR_HOST = 'cursor';
33
35
  export const EVENT_CLASS_HOST = 'cursor-host-precompact';
34
36
  export const EVENT_CLASS_SYNTHETIC = 'cursor-precompact-synthetic';
@@ -153,105 +155,6 @@ export function cursorBootNeedsRefresh(existing, root) {
153
155
  return false;
154
156
  }
155
157
 
156
- function sessionCaptureStampPath(root) {
157
- return path.join(root, '.xray', 'state', 'session-capture-stamp.json');
158
- }
159
-
160
- function readSessionCaptureConfig(root) {
161
- let cfg = {};
162
- try {
163
- const raw = JSON.parse(fs.readFileSync(path.join(root, '.xray', 'features.json'), 'utf8'));
164
- cfg = raw.inference_session_capture || {};
165
- } catch {
166
- cfg = {};
167
- }
168
- return {
169
- enabled: cfg.enabled === true,
170
- minCommits: typeof cfg.min_commits === 'number' && cfg.min_commits > 0 ? cfg.min_commits : 3,
171
- lookback: typeof cfg.lookback_commits === 'number' && cfg.lookback_commits > 0 ? cfg.lookback_commits : 20,
172
- };
173
- }
174
-
175
- function readSessionCaptureStamp(root) {
176
- try {
177
- return JSON.parse(fs.readFileSync(sessionCaptureStampPath(root), 'utf8'));
178
- } catch {
179
- return null;
180
- }
181
- }
182
-
183
- function gitCommitSubjects(root, args) {
184
- try {
185
- const out = execFileSync('git', ['log', '--format=%h||%s', '--no-merges', ...args], {
186
- cwd: root,
187
- encoding: 'utf8',
188
- timeout: 5000,
189
- stdio: ['ignore', 'pipe', 'pipe'],
190
- });
191
- return String(out)
192
- .split('\n')
193
- .map((line) => line.trim())
194
- .filter(Boolean)
195
- .map((line) => {
196
- const idx = line.indexOf('||');
197
- return {
198
- hash: idx === -1 ? line : line.slice(0, idx),
199
- message: idx === -1 ? '' : line.slice(idx + 2),
200
- };
201
- });
202
- } catch {
203
- return [];
204
- }
205
- }
206
-
207
- /**
208
- * Cursor never runs the git post-commit hook. When HEAD moves, write the same
209
- * session-*.json the kernel already knows how to ingest. Dedup per HEAD.
210
- */
211
- export function maybeCaptureSessionOnHeadMove(root) {
212
- const cfg = readSessionCaptureConfig(root);
213
- if (!cfg.enabled) return null;
214
- const git = readGitBrief(root);
215
- if (!git || !git.head) return null;
216
- const stamp = readSessionCaptureStamp(root);
217
- if (stamp && stamp.head === git.head) return null;
218
-
219
- const commits = stamp && stamp.head
220
- ? gitCommitSubjects(root, [`${stamp.head}..HEAD`])
221
- : gitCommitSubjects(root, ['-n', String(cfg.lookback)]);
222
- if (commits.length < cfg.minCommits) return null;
223
-
224
- const sessionId = `session-${new Date().toISOString().slice(0, 10)}-${git.head}`;
225
- const approaches = commits.map((c) => c.message).filter(Boolean);
226
- const session = {
227
- sessionId,
228
- timestamp: new Date().toISOString(),
229
- span: {
230
- from: stamp && stamp.head ? stamp.head : `HEAD~${cfg.lookback}`,
231
- to: git.head,
232
- },
233
- problems: [],
234
- approaches,
235
- wrongTurns: [],
236
- solutions: [],
237
- patterns: [],
238
- matched_primitives: [],
239
- metrics: { commits: commits.length },
240
- };
241
-
242
- const outDir = path.join(root, 'docs', 'inference');
243
- fs.mkdirSync(outDir, { recursive: true });
244
- const filePath = path.join(outDir, `session-${sessionId.replace(/^session-/, '')}.json`);
245
- fs.writeFileSync(filePath, `${JSON.stringify(session, null, 2)}\n`);
246
- fs.writeFileSync(path.join(outDir, 'latest-session.json'), `${JSON.stringify(session, null, 2)}\n`);
247
- fs.mkdirSync(path.dirname(sessionCaptureStampPath(root)), { recursive: true });
248
- fs.writeFileSync(
249
- sessionCaptureStampPath(root),
250
- `${JSON.stringify({ head: git.head, sessionId, path: filePath, updatedAt: session.timestamp }, null, 2)}\n`,
251
- );
252
- return filePath;
253
- }
254
-
255
158
  export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-use-boot', extra = {}) {
256
159
  const bootPath = sessionBootPath(root);
257
160
  if (fs.existsSync(bootPath)) {
@@ -5,7 +5,7 @@
5
5
  * Heat writers merge stock fields; unknown keys and ## Durable / ## Seed survive.
6
6
  */
7
7
  const { execFileSync } = require("child_process");
8
- const { existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");
8
+ const { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");
9
9
  const { join } = require("path");
10
10
 
11
11
  const HOOKS_DIR = __dirname;
@@ -203,10 +203,231 @@ function stationSafeSignals(names) {
203
203
  return out;
204
204
  }
205
205
 
206
- /** Overlay + factory names on dest. OP-PROC lives here — not on Station.md. */
206
+ /** Keep subject repo-* in the short list the card and working file hold. */
207
+ function preferSubjectHits(names, cap = 8) {
208
+ if (!Array.isArray(names)) return [];
209
+ const unique = [];
210
+ for (const raw of names) {
211
+ const name = String(raw || "").trim();
212
+ if (!name || name.toLowerCase().startsWith("bedrock-")) continue;
213
+ if (unique.includes(name)) continue;
214
+ unique.push(name);
215
+ }
216
+ const subject = unique.filter((name) => name.startsWith("repo-"));
217
+ const rest = unique.filter((name) => !name.startsWith("repo-"));
218
+ return [...subject, ...rest].slice(0, cap);
219
+ }
220
+
221
+ function destSignalsPath(root) {
222
+ return join(root, ".xray", "state", "repertoire", "curated_signals.json");
223
+ }
224
+
225
+ function notesPath(root) {
226
+ return join(root, ".xray", "state", "NOTES.md");
227
+ }
228
+
229
+ function repertoirePackageRoots(root) {
230
+ const candidates = [
231
+ join(root, "node_modules", "@0xray", "repertoire"),
232
+ join(root, "..", "repertoire"),
233
+ join(root, "vendor", "@0xray", "repertoire"),
234
+ ];
235
+ const out = [];
236
+ for (const pkgRoot of candidates) {
237
+ const pkgFile = join(pkgRoot, "package.json");
238
+ if (!existsSync(pkgFile)) continue;
239
+ try {
240
+ const pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
241
+ if (pkg.name === "@0xray/repertoire" || pkg.name === "repertoire") out.push(pkgRoot);
242
+ } catch {
243
+ /* skip */
244
+ }
245
+ }
246
+ return out;
247
+ }
248
+
249
+ function repertoireDataFile(root, fileName) {
250
+ for (const pkgRoot of repertoirePackageRoots(root)) {
251
+ const dest = join(pkgRoot, "data", fileName);
252
+ if (existsSync(dest)) return dest;
253
+ }
254
+ return null;
255
+ }
256
+
257
+ function readSignalRecords(filePath) {
258
+ if (!filePath || !existsSync(filePath)) return [];
259
+ try {
260
+ const data = JSON.parse(readFileSync(filePath, "utf8"));
261
+ const signals = Array.isArray(data.signals) ? data.signals : [];
262
+ return signals.filter((signal) => signal && String(signal.name || "").trim());
263
+ } catch {
264
+ return [];
265
+ }
266
+ }
267
+
268
+ function signalNameSet(filePath) {
269
+ return new Set(readSignalRecords(filePath).map((signal) => String(signal.name).trim()));
270
+ }
271
+
272
+ function mergeMissingSignals(destPath, incoming) {
273
+ if (!incoming.length || !existsSync(destPath)) return 0;
274
+ let data;
275
+ try {
276
+ data = JSON.parse(readFileSync(destPath, "utf8"));
277
+ } catch {
278
+ return 0;
279
+ }
280
+ if (!Array.isArray(data.signals)) return 0;
281
+ const have = new Set(
282
+ data.signals.map((signal) => String((signal && signal.name) || "").trim()).filter(Boolean),
283
+ );
284
+ let added = 0;
285
+ for (const signal of incoming) {
286
+ const name = String(signal.name).trim();
287
+ if (!name || have.has(name)) continue;
288
+ data.signals.push(signal);
289
+ have.add(name);
290
+ added += 1;
291
+ }
292
+ if (added) writeFileSync(destPath, `${JSON.stringify(data, null, 2)}\n`);
293
+ return added;
294
+ }
295
+
296
+ /** Hydrate project dest from seed + stack + subject overlays. Never write the tarball. */
297
+ function hydrateDestOnWake(root) {
298
+ const dest = destSignalsPath(root);
299
+ const seed = repertoireDataFile(root, "curated_signals.json");
300
+ if (!existsSync(dest) && seed) {
301
+ mkdirSync(join(root, ".xray", "state", "repertoire"), { recursive: true });
302
+ copyFileSync(seed, dest);
303
+ }
304
+ if (!existsSync(dest)) return { dest: null, added: 0, destCount: 0 };
305
+ const added =
306
+ mergeMissingSignals(dest, readSignalRecords(repertoireDataFile(root, "stack-overlay.json"))) +
307
+ mergeMissingSignals(dest, readSignalRecords(repertoireDataFile(root, "subject-overlay.json")));
308
+ return { dest, added, destCount: countCuratedSignals(dest) || 0 };
309
+ }
310
+
311
+ function readNotesPickup(root) {
312
+ const dest = notesPath(root);
313
+ if (!existsSync(dest)) return null;
314
+ try {
315
+ const text = readFileSync(dest, "utf8");
316
+ const match = text.match(/\*\*Pickup line:\*\*\s*([^\n]+)/);
317
+ return clipIntent(match && match[1] ? match[1] : null);
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+
323
+ function readLatestSessionApproaches(root) {
324
+ const latest = join(root, "docs", "inference", "latest-session.json");
325
+ if (!existsSync(latest)) return null;
326
+ try {
327
+ const data = JSON.parse(readFileSync(latest, "utf8"));
328
+ const approaches = Array.isArray(data.approaches) ? data.approaches : [];
329
+ return clipIntent(approaches.filter(Boolean).join(" "));
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+
335
+ function sessionCaptureStampPath(root) {
336
+ return join(root, ".xray", "state", "session-capture-stamp.json");
337
+ }
338
+
339
+ function readSessionCaptureConfig(root) {
340
+ let cfg = {};
341
+ try {
342
+ cfg = JSON.parse(readFileSync(join(root, ".xray", "features.json"), "utf8")).inference_session_capture || {};
343
+ } catch {
344
+ cfg = {};
345
+ }
346
+ return {
347
+ enabled: cfg.enabled === true,
348
+ minCommits: typeof cfg.min_commits === "number" && cfg.min_commits > 0 ? cfg.min_commits : 3,
349
+ lookback: typeof cfg.lookback_commits === "number" && cfg.lookback_commits > 0 ? cfg.lookback_commits : 20,
350
+ };
351
+ }
352
+
353
+ function gitCommitSubjects(root, args) {
354
+ try {
355
+ const out = execFileSync("git", ["log", "--format=%h||%s", "--no-merges", ...args], {
356
+ cwd: root,
357
+ encoding: "utf8",
358
+ timeout: 5000,
359
+ stdio: ["ignore", "pipe", "pipe"],
360
+ });
361
+ return String(out)
362
+ .split("\n")
363
+ .map((line) => line.trim())
364
+ .filter(Boolean)
365
+ .map((line) => {
366
+ const idx = line.indexOf("||");
367
+ return {
368
+ hash: idx === -1 ? line : line.slice(0, idx),
369
+ message: idx === -1 ? "" : line.slice(idx + 2),
370
+ };
371
+ });
372
+ } catch {
373
+ return [];
374
+ }
375
+ }
376
+
377
+ /** Every floor that heats. Dedup per HEAD. Cursor-only copy was the leftover. */
378
+ function maybeCaptureSessionOnHeadMove(root) {
379
+ const cfg = readSessionCaptureConfig(root);
380
+ if (!cfg.enabled) return null;
381
+ const git = readGitBrief(root);
382
+ if (!git || !git.head) return null;
383
+ let stamp = null;
384
+ try {
385
+ stamp = JSON.parse(readFileSync(sessionCaptureStampPath(root), "utf8"));
386
+ } catch {
387
+ stamp = null;
388
+ }
389
+ if (stamp && stamp.head === git.head) return null;
390
+ const commits = stamp && stamp.head
391
+ ? gitCommitSubjects(root, [`${stamp.head}..HEAD`])
392
+ : gitCommitSubjects(root, ["-n", String(cfg.lookback)]);
393
+ if (commits.length < cfg.minCommits) return null;
394
+ const sessionId = `session-${new Date().toISOString().slice(0, 10)}-${git.head}`;
395
+ const approaches = commits.map((row) => row.message).filter(Boolean);
396
+ const session = {
397
+ sessionId,
398
+ timestamp: new Date().toISOString(),
399
+ span: {
400
+ from: stamp && stamp.head ? stamp.head : `HEAD~${cfg.lookback}`,
401
+ to: git.head,
402
+ },
403
+ problems: [],
404
+ approaches,
405
+ wrongTurns: [],
406
+ solutions: [],
407
+ patterns: [],
408
+ matched_primitives: [],
409
+ metrics: { commits: commits.length },
410
+ };
411
+ const outDir = join(root, "docs", "inference");
412
+ mkdirSync(outDir, { recursive: true });
413
+ const filePath = join(outDir, `session-${sessionId.replace(/^session-/, "")}.json`);
414
+ writeFileSync(filePath, `${JSON.stringify(session, null, 2)}\n`);
415
+ writeFileSync(join(outDir, "latest-session.json"), `${JSON.stringify(session, null, 2)}\n`);
416
+ mkdirSync(join(root, ".xray", "state"), { recursive: true });
417
+ writeFileSync(
418
+ sessionCaptureStampPath(root),
419
+ `${JSON.stringify({ head: git.head, sessionId, path: filePath, updatedAt: session.timestamp }, null, 2)}\n`,
420
+ );
421
+ return filePath;
422
+ }
423
+
424
+ /** Overlay + factory names on dest. Subject repo-* stay off this list. */
207
425
  function readOpProcNames(root) {
208
- const dest = join(root, ".xray", "state", "repertoire", "curated_signals.json");
426
+ const dest = destSignalsPath(root);
209
427
  if (!existsSync(dest)) return [];
428
+ const factory = signalNameSet(repertoireDataFile(root, "curated_signals.json"));
429
+ const stack = signalNameSet(repertoireDataFile(root, "stack-overlay.json"));
430
+ const filter = factory.size || stack.size;
210
431
  try {
211
432
  const data = JSON.parse(readFileSync(dest, "utf8"));
212
433
  if (!Array.isArray(data.signals)) return [];
@@ -214,6 +435,8 @@ function readOpProcNames(root) {
214
435
  for (const signal of data.signals) {
215
436
  const name = String(signal && signal.name ? signal.name : "").trim();
216
437
  if (!name || name.toLowerCase().startsWith("bedrock-")) continue;
438
+ if (name.toLowerCase().startsWith("repo-")) continue;
439
+ if (filter && !factory.has(name) && !stack.has(name)) continue;
217
440
  names.push(name);
218
441
  }
219
442
  return names;
@@ -295,8 +518,9 @@ function buildRepertoireResume(root) {
295
518
  if (!modulePath) {
296
519
  return "Repertoire: not installed (memory_routing stays off)";
297
520
  }
298
- let signalsPath = mr.config && mr.config.signalsPath;
299
- if (signalsPath && !signalsPath.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(signalsPath)) {
521
+ const destPath = destSignalsPath(root);
522
+ let signalsPath = existsSync(destPath) ? destPath : mr.config && mr.config.signalsPath;
523
+ if (signalsPath && signalsPath !== destPath && !signalsPath.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(signalsPath)) {
300
524
  signalsPath = join(root, signalsPath);
301
525
  }
302
526
  if (!signalsPath) {
@@ -311,6 +535,10 @@ function buildRepertoireResume(root) {
311
535
  }
312
536
 
313
537
  function applyStationHeat(root, host, extra = {}, existing = {}) {
538
+ const hydrate = hydrateDestOnWake(root);
539
+ const captured = maybeCaptureSessionOnHeadMove(root);
540
+ const pickup = readNotesPickup(root);
541
+ const approaches = readLatestSessionApproaches(root);
314
542
  const prevHost = typeof existing.host === "string" ? existing.host : null;
315
543
  const nextSwap = prevHost && host && prevHost !== host ? { from: prevHost, to: host } : null;
316
544
  const keptSwap =
@@ -321,9 +549,12 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
321
549
  ? existing.hotSwap
322
550
  : null;
323
551
  const hotSwap = nextSwap || keptSwap;
324
- const intent =
325
- clipIntent(extra.intent || extra.prompt || extra.userMessage || extra.user_prompt) ||
326
- (typeof existing.intent === "string" ? existing.intent : null);
552
+ const incomingIntent = clipIntent(
553
+ extra.intent || extra.prompt || extra.userMessage || extra.user_prompt,
554
+ );
555
+ const intent = incomingIntent || (typeof existing.intent === "string" ? existing.intent : null);
556
+ const matchText = clipIntent([intent, pickup, approaches].filter(Boolean).join(" "));
557
+ const rematch = Boolean(incomingIntent);
327
558
  const git = readGitBrief(root);
328
559
  const planLine = readPlanLine(root);
329
560
  const repertoireResume =
@@ -332,18 +563,27 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
332
563
  const intentBit = intent ? `intent: ${intent}` : "intent: (none yet)";
333
564
  const gitBit = git ? `git ${git.branch}@${git.head}` : "git: n/a";
334
565
  const planBit = planLine ? `plan: ${planLine}` : "plan: (none)";
335
- let matchedSignals = stationSafeSignals(extra.matchedSignals);
566
+ let matchedSignals = [];
567
+ if (Array.isArray(extra.matchedSignals) && extra.matchedSignals.length) {
568
+ matchedSignals = preferSubjectHits(extra.matchedSignals, 8);
569
+ }
336
570
  const priorWorking = readRepertoireWorking(root);
337
- if (!matchedSignals.length && intent) {
571
+ const destCount = hydrate.destCount || (existsSync(destSignalsPath(root)) ? countCuratedSignals(destSignalsPath(root)) : 0);
572
+ if (!matchedSignals.length && matchText) {
338
573
  if (
574
+ !rematch &&
339
575
  priorWorking &&
340
- priorWorking.intent === intent &&
576
+ priorWorking.matchText === matchText &&
577
+ priorWorking.destCount === destCount &&
341
578
  Array.isArray(priorWorking.matchedSignals) &&
342
579
  priorWorking.matchedSignals.length
343
580
  ) {
344
- matchedSignals = stationSafeSignals(priorWorking.matchedSignals);
581
+ matchedSignals = preferSubjectHits(priorWorking.matchedSignals, 8);
345
582
  } else {
346
- matchedSignals = stationSafeSignals(matchStationSignalsSync(root, intent));
583
+ matchedSignals = preferSubjectHits(
584
+ matchStationSignalsSync(root, rematch && incomingIntent ? incomingIntent : matchText),
585
+ 8,
586
+ );
347
587
  }
348
588
  }
349
589
  if (!matchedSignals.length && priorWorking) {
@@ -357,16 +597,24 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
357
597
  hookEvent: extra.hookEvent || extra.source || null,
358
598
  memoryRouting: repertoireResume.startsWith("Repertoire: on") ? "on" : "off",
359
599
  repertoireResume,
600
+ destCount,
360
601
  };
361
- if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals;
602
+ if (pickup) workingSnapshot.pickup = pickup;
603
+ if (matchText) workingSnapshot.matchText = matchText;
604
+ if (captured) workingSnapshot.sessionCapture = captured;
605
+ if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals.slice(0, 8);
362
606
  const opProcNames = readOpProcNames(root);
363
607
  if (opProcNames.length) workingSnapshot.opProcNames = opProcNames;
364
608
  const working = persistRepertoireWorking(root, workingSnapshot);
365
- if (isCompactHook(extra) && matchedSignals.length) {
609
+ const pickupChanged = Boolean(pickup && (!priorWorking || priorWorking.pickup !== pickup));
610
+ if (
611
+ matchedSignals.length &&
612
+ (isCompactHook(extra) || hydrate.added > 0 || pickupChanged)
613
+ ) {
366
614
  ingestCompactFeedbackSync(
367
615
  root,
368
616
  typeof extra.sessionId === "string" ? extra.sessionId : "station",
369
- extra.hookEvent || extra.source || "post_compact",
617
+ extra.hookEvent || extra.source || (isCompactHook(extra) ? "post_compact" : "wake"),
370
618
  matchedSignals,
371
619
  );
372
620
  }
@@ -541,6 +789,9 @@ module.exports = {
541
789
  persistRepertoireWorking,
542
790
  readRepertoireWorking,
543
791
  readOpProcNames,
792
+ hydrateDestOnWake,
793
+ readNotesPickup,
794
+ maybeCaptureSessionOnHeadMove,
544
795
  formatWorkingLine,
545
796
  applyStationHeat,
546
797
  extractPreservedStationLines,
@@ -19,6 +19,9 @@ export const buildRepertoireResume = impl.buildRepertoireResume;
19
19
  export const persistRepertoireWorking = impl.persistRepertoireWorking;
20
20
  export const readRepertoireWorking = impl.readRepertoireWorking;
21
21
  export const readOpProcNames = impl.readOpProcNames;
22
+ export const hydrateDestOnWake = impl.hydrateDestOnWake;
23
+ export const readNotesPickup = impl.readNotesPickup;
24
+ export const maybeCaptureSessionOnHeadMove = impl.maybeCaptureSessionOnHeadMove;
22
25
  export const formatWorkingLine = impl.formatWorkingLine;
23
26
  export const applyStationHeat = impl.applyStationHeat;
24
27
  export const extractPreservedStationLines = impl.extractPreservedStationLines;
@@ -2,9 +2,11 @@
2
2
  /**
3
3
  * Sync helper for station-hook-runtime.cjs — match Repertoire signals for an intent.
4
4
  * Prints a JSON string array. Used on every floor's station heat, not only Grok.
5
+ * Loads the worn provider from dest, not xray src TypeScript.
5
6
  */
6
7
  import { existsSync, readFileSync } from 'node:fs';
7
8
  import { join } from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
8
10
 
9
11
  const root = process.argv[2];
10
12
  const intent = process.argv[3] || '';
@@ -23,8 +25,52 @@ if (existsSync(featuresPath)) {
23
25
  }
24
26
  }
25
27
 
26
- const { loadMemoryRoutingProvider } = await import('../../memory-routing/index.js');
27
- const provider = await loadMemoryRoutingProvider(routing, root);
28
+ function providerCandidates() {
29
+ const configured = routing.module_path
30
+ ? routing.module_path.startsWith('/') || /^[A-Za-z]:[\\/]/.test(routing.module_path)
31
+ ? routing.module_path
32
+ : join(root, routing.module_path)
33
+ : null;
34
+ const raw = [
35
+ join(root, '..', 'repertoire', 'dist', 'provider', 'memory-routing-provider.js'),
36
+ join(root, 'node_modules', '@0xray', 'repertoire', 'dist', 'provider', 'memory-routing-provider.js'),
37
+ configured,
38
+ join(root, 'vendor', '@0xray', 'repertoire', 'dist', 'provider', 'memory-routing-provider.js'),
39
+ ].filter(Boolean);
40
+ const withSubject = [];
41
+ const rest = [];
42
+ for (const dest of raw) {
43
+ const overlay = dest.replace(/dist\/provider\/memory-routing-provider\.js$/, 'data/subject-overlay.json');
44
+ if (existsSync(overlay)) withSubject.push(dest);
45
+ else rest.push(dest);
46
+ }
47
+ return [...withSubject, ...rest];
48
+ }
49
+
50
+ async function loadProvider() {
51
+ for (const dest of providerCandidates()) {
52
+ if (!existsSync(dest)) continue;
53
+ try {
54
+ const mod = await import(pathToFileURL(dest).href);
55
+ if (typeof mod.createMemoryRoutingProvider === 'function') {
56
+ return mod.createMemoryRoutingProvider({
57
+ projectRoot: root,
58
+ signalsPath: routing.config && routing.config.signalsPath,
59
+ statePath: routing.config && routing.config.statePath,
60
+ feedbackDir: routing.config && routing.config.feedbackDir,
61
+ });
62
+ }
63
+ if (typeof mod.loadMemoryRoutingProvider === 'function') {
64
+ return mod.loadMemoryRoutingProvider(routing, root);
65
+ }
66
+ } catch {
67
+ /* try next */
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ const provider = await loadProvider();
28
74
  if (!provider || provider.id === 'null' || typeof provider.getTaskConfidence !== 'function') {
29
75
  process.stdout.write('[]\n');
30
76
  process.exit(0);
@@ -41,7 +87,7 @@ for (const signalName of names) {
41
87
  const name = String(signalName || '').trim();
42
88
  if (!name || name.toLowerCase().startsWith('bedrock-')) continue;
43
89
  safe.push(name);
44
- if (safe.length >= 4) break;
90
+ if (safe.length >= 12) break;
45
91
  }
46
92
  process.stdout.write(`${JSON.stringify(safe)}\n`);
47
93
  process.exit(0);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.17",
3
+ "version": "4.0.18",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.17",
3
+ "version": "4.0.18",
4
4
  "description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.17",
3
+ "version": "4.0.18",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.17",
3
+ "version": "4.0.18",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",