0xray 4.0.18 → 4.0.20

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 (75) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +1 -1
  3. package/dist/CHANGELOG.md +25 -0
  4. package/dist/integrations/cursor/hooks/after-file-edit.js +6 -3
  5. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +40 -20
  6. package/dist/integrations/cursor/hooks/pre-compact.js +30 -22
  7. package/dist/integrations/cursor/hooks/pre-tool-use.js +5 -4
  8. package/dist/integrations/grok/hooks/grok-hook-utils.js +3 -1
  9. package/dist/integrations/hermes-agent/bridge.mjs +14 -0
  10. package/dist/integrations/hooks/station-hook-runtime.cjs +243 -13
  11. package/dist/integrations/hooks/station-hook-runtime.mjs +6 -0
  12. package/dist/integrations/hooks/station-memory-ingest.mjs +153 -4
  13. package/dist/integrations/hooks/station-memory-match.mjs +8 -8
  14. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  15. package/dist/nucleus/delegation-gate.js +10 -11
  16. package/dist/nucleus/suit-temperament.js +5 -1
  17. package/package.json +1 -1
  18. package/scripts/node/consumer-install-smoke.mjs +3 -3
  19. package/scripts/node/wear-vendored-repertoire.cjs +39 -1
  20. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  21. package/vendor/@0xray/repertoire/AGENTS.md +8 -2
  22. package/vendor/@0xray/repertoire/README.md +35 -8
  23. package/vendor/@0xray/repertoire/data/stack-overlay.json +750 -0
  24. package/vendor/@0xray/repertoire/data/subject-overlay.json +297 -0
  25. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts +36 -1
  26. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +1 -1
  27. package/vendor/@0xray/repertoire/dist/RepertoireService.js +108 -18
  28. package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +1 -1
  29. package/vendor/@0xray/repertoire/dist/index.d.ts +3 -1
  30. package/vendor/@0xray/repertoire/dist/index.d.ts.map +1 -1
  31. package/vendor/@0xray/repertoire/dist/index.js +3 -1
  32. package/vendor/@0xray/repertoire/dist/index.js.map +1 -1
  33. package/vendor/@0xray/repertoire/dist/ingestion/groover-log-ingester.d.ts +4 -0
  34. package/vendor/@0xray/repertoire/dist/ingestion/groover-log-ingester.d.ts.map +1 -1
  35. package/vendor/@0xray/repertoire/dist/ingestion/groover-log-ingester.js +15 -1
  36. package/vendor/@0xray/repertoire/dist/ingestion/groover-log-ingester.js.map +1 -1
  37. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts +22 -8
  38. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +1 -1
  39. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js +56 -5
  40. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +1 -1
  41. package/vendor/@0xray/repertoire/dist/mcp/server.js +8 -13
  42. package/vendor/@0xray/repertoire/dist/mcp/server.js.map +1 -1
  43. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +1 -1
  44. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js +24 -7
  45. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +1 -1
  46. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +1 -1
  47. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +2 -1
  48. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +1 -1
  49. package/vendor/@0xray/repertoire/dist/paths.d.ts +65 -2
  50. package/vendor/@0xray/repertoire/dist/paths.d.ts.map +1 -1
  51. package/vendor/@0xray/repertoire/dist/paths.js +377 -6
  52. package/vendor/@0xray/repertoire/dist/paths.js.map +1 -1
  53. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +1 -1
  54. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +6 -8
  55. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +1 -1
  56. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +20 -2
  57. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
  58. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +121 -8
  59. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
  60. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.d.ts +2 -1
  61. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.d.ts.map +1 -1
  62. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.js +21 -3
  63. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.js.map +1 -1
  64. package/vendor/@0xray/repertoire/dist/registry/confidence-decay.d.ts +40 -0
  65. package/vendor/@0xray/repertoire/dist/registry/confidence-decay.d.ts.map +1 -0
  66. package/vendor/@0xray/repertoire/dist/registry/confidence-decay.js +89 -0
  67. package/vendor/@0xray/repertoire/dist/registry/confidence-decay.js.map +1 -0
  68. package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.d.ts.map +1 -1
  69. package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.js +1 -0
  70. package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.js.map +1 -1
  71. package/vendor/@0xray/repertoire/dist/types.d.ts +5 -0
  72. package/vendor/@0xray/repertoire/dist/types.d.ts.map +1 -1
  73. package/vendor/@0xray/repertoire/package.json +4 -2
  74. package/vendor/@0xray/repertoire/scripts/verify-synthesis-dogfood.mjs +2 -2
  75. package/xray/features.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.18",
3
+ "version": "4.0.20",
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.18",
3
+ "version": "4.0.20",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",
package/dist/CHANGELOG.md CHANGED
@@ -7,6 +7,9 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
7
7
  ## [Unreleased]
8
8
 
9
9
  ### ✨ Features
10
+ - **Cleanup is memory** — `applyStationHeat` grows the project copy when Repertoire is on: kernel sessions, sibling `repo-*` names, and NOTES/CLEANSE diary heat. Groover field stays off. Opt-out still skips seed and grow. Station stays the pickup card. Not a new MCP/skill.
11
+ - **Live context memory** — capture then grow. Session `patterns` come from real git (commit slugs + structural names), not a leftover catalog. Cursor skip / afterFileEdit, Grok heat, Hermes pre-process, and OpenClaw pre-tool share one dest lock so parallel seats write one project copy. Not a new MCP/skill.
12
+ - **Vendored Repertoire 0.2.5** — `vendor/@0xray/repertoire` is the published 0.2.5 package (overlays included). Wear replaces an older leftover in `node_modules`. A newer user-installed copy is left alone. Boot reads worn `node_modules`, then vendor, then a sibling checkout last. Repertoire stays optional (`enabled: false` + `provider: repertoire`); Station is the memory when it is off.
10
13
  - **Cursor fifth wear** — `install-bridges.cjs` fastens project `.cursor/hooks.json` (preToolUse / preCompact / afterFileEdit) so Cursor Cloud heats Station and gates tools. Leaves an existing file alone. Not a fifth chat TUI. No new MCP/skill surface. Pack gate (`REQUIRED_PACK_PATHS`) now requires `dist/integrations/cursor/hooks/{hooks.json,pre-tool-use.js,pre-compact.js,after-file-edit.js,cursor-hook-utils.js,cursor-usage-receipt.js}` so a published tarball cannot ship a dead consumer command. `preToolUse` / `afterFileEdit` rewrite the card when git HEAD or Repertoire count diverges — Ticket / Durable / Seed stay. OP-PROC stays in the hook, not on Station.
11
14
 
12
15
  ### 📚 Documentation
@@ -34,6 +37,28 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
34
37
 
35
38
  ---
36
39
 
40
+ ## [4.0.20] - 2026-09-22
41
+
42
+ ### 🔄 Changes
43
+
44
+ ### 🐛 Bug Fixes
45
+ - fix: rewrite session-boot when HEAD moves — first-wins skip froze metal (same session still heats dest)
46
+ - fix: grow dest from live git on every floor (a7685eb09)
47
+ - fix: heat grows the project copy — cleanup is memory (be10c82eb)
48
+
49
+ ---
50
+
51
+ ## [4.0.19] - 2026-09-22
52
+
53
+ ### ✨ Features
54
+ - **Vendored Repertoire 0.2.5** — published 0.2.5 lives in `vendor/@0xray/repertoire`. Wear replaces older leftovers. Optional; Station is the memory when it is off.
55
+
56
+ ### 🔎 Other Changes
57
+ - Load worn Repertoire first; sibling checkout last. (f8a90b531)
58
+ - Put published Repertoire 0.2.5 inside 0xray vendor and wear it. (a90b0a7a4)
59
+
60
+ ---
61
+
37
62
  ## [4.0.18] - 2026-09-22
38
63
 
39
64
  ### 🔄 Changes
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {
8
8
  appendHookActivity,
9
+ cursorHeatRoots,
9
10
  cursorSessionId,
10
11
  cursorWorkspaceRoot,
11
12
  ensureCursorSessionBoot,
@@ -17,9 +18,11 @@ async function main() {
17
18
  try {
18
19
  const event = await readStdinJson();
19
20
  const eventRoot = cursorWorkspaceRoot(event);
20
- const bootPath = ensureCursorSessionBoot(eventRoot, '0xray/cursor-after-file-edit-boot', {
21
- sessionId: cursorSessionId(event),
22
- });
21
+ const sessionId = cursorSessionId(event);
22
+ let bootPath = null;
23
+ for (const root of cursorHeatRoots(event)) {
24
+ bootPath = ensureCursorSessionBoot(root, '0xray/cursor-after-file-edit-boot', { sessionId });
25
+ }
23
26
  appendHookActivity(eventRoot, 'cursor-after-file-edit', 'session-boot', 'info', {
24
27
  bootPath,
25
28
  file: event.file_path || event.path || null,
@@ -23,13 +23,12 @@ import {
23
23
  } from '../../hooks/delegation-gate-runtime.mjs';
24
24
  import { appendHookActivity } from '../../grok/hooks/grok-hook-activity.js';
25
25
  import {
26
- buildRepertoireResume,
26
+ heatLiveMemory,
27
27
  maybeCaptureSessionOnHeadMove,
28
- readGitBrief,
29
- stationDurableHoldsNpm,
28
+ stationBootNeedsRefresh,
30
29
  } from '../../hooks/station-hook-runtime.mjs';
31
30
 
32
- export { maybeCaptureSessionOnHeadMove };
31
+ export { heatLiveMemory, maybeCaptureSessionOnHeadMove };
33
32
 
34
33
  export const CURSOR_HOST = 'cursor';
35
34
  export const EVENT_CLASS_HOST = 'cursor-host-precompact';
@@ -77,6 +76,40 @@ export function cursorWorkspaceRoot(event = {}) {
77
76
  );
78
77
  }
79
78
 
79
+ function addHeatRoot(out, seen, value) {
80
+ if (!value) return;
81
+ const resolved = path.resolve(String(value));
82
+ if (seen.has(resolved)) return;
83
+ seen.add(resolved);
84
+ out.push(resolved);
85
+ }
86
+
87
+ /** Every live context that already has a Station card — cwd plus mill when they differ. */
88
+ export function cursorHeatRoots(event = {}) {
89
+ const seen = new Set();
90
+ const out = [];
91
+ addHeatRoot(out, seen, event.cwd);
92
+ addHeatRoot(out, seen, event.workspaceRoot);
93
+ const listed = event.workspace_roots || event.workspaceRoots;
94
+ if (Array.isArray(listed)) {
95
+ for (const row of listed) {
96
+ if (typeof row === 'string') addHeatRoot(out, seen, row);
97
+ else if (row && typeof row === 'object' && row.path) addHeatRoot(out, seen, row.path);
98
+ }
99
+ }
100
+ addHeatRoot(out, seen, process.env.CURSOR_PROJECT_DIR);
101
+ const mill = process.env.XRAY_AI_PATH;
102
+ if (mill) {
103
+ const resolved = path.resolve(String(mill));
104
+ const millHasCard =
105
+ fs.existsSync(path.join(resolved, '.xray', 'state', 'STATION.md')) ||
106
+ fs.existsSync(path.join(resolved, '.xray', 'features.json'));
107
+ if (millHasCard) addHeatRoot(out, seen, resolved);
108
+ }
109
+ if (out.length === 0) addHeatRoot(out, seen, cursorWorkspaceRoot(event));
110
+ return out;
111
+ }
112
+
80
113
  export function cursorSessionId(event = {}) {
81
114
  return (
82
115
  event.conversation_id ||
@@ -141,18 +174,7 @@ export function classifyPreCompactEvent(event = {}, argv = process.argv) {
141
174
  }
142
175
 
143
176
  export function cursorBootNeedsRefresh(existing, root) {
144
- if (!existing || typeof existing !== 'object') return true;
145
- if (existing.host !== CURSOR_HOST) return true;
146
- if (!existing.suit_profile) return true;
147
- if (existing.workspaceRoot && existing.workspaceRoot !== root) return true;
148
- if (!existing.stationLine) return true;
149
- const liveGit = readGitBrief(root);
150
- const bootHead = existing.git && existing.git.head ? String(existing.git.head) : '';
151
- if (liveGit && liveGit.head && bootHead !== liveGit.head) return true;
152
- const liveResume = buildRepertoireResume(root);
153
- if (liveResume && existing.repertoireResume && liveResume !== existing.repertoireResume) return true;
154
- if (stationDurableHoldsNpm(root)) return true;
155
- return false;
177
+ return stationBootNeedsRefresh(existing, root, CURSOR_HOST);
156
178
  }
157
179
 
158
180
  export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-use-boot', extra = {}) {
@@ -161,7 +183,7 @@ export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-us
161
183
  try {
162
184
  const existing = JSON.parse(fs.readFileSync(bootPath, 'utf8'));
163
185
  if (!cursorBootNeedsRefresh(existing, root)) {
164
- maybeCaptureSessionOnHeadMove(root);
186
+ heatLiveMemory(root);
165
187
  return bootPath;
166
188
  }
167
189
  } catch {
@@ -172,9 +194,7 @@ export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-us
172
194
  host: CURSOR_HOST,
173
195
  ...extra,
174
196
  });
175
- const written = writeSessionBoot(root, payload) || bootPath;
176
- maybeCaptureSessionOnHeadMove(root);
177
- return written;
197
+ return writeSessionBoot(root, payload) || bootPath;
178
198
  }
179
199
 
180
200
  export function writeCursorPrecompactReceipt(root, fields) {
@@ -9,6 +9,7 @@ import {
9
9
  appendHookActivity,
10
10
  buildSessionBootPayload,
11
11
  classifyPreCompactEvent,
12
+ cursorHeatRoots,
12
13
  cursorSessionId,
13
14
  cursorWorkspaceRoot,
14
15
  readStdinJson,
@@ -40,33 +41,40 @@ async function main() {
40
41
  const eventRoot = cursorWorkspaceRoot(event);
41
42
  const sessionId = cursorSessionId(event);
42
43
  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
- });
44
+ let bootPath = null;
45
+ let receiptPath = null;
46
+ let usagePath = null;
47
+ let stationLine = null;
48
+ for (const root of cursorHeatRoots(event)) {
49
+ const payload = buildSessionBootPayload(root, '0xray/cursor-compact', {
50
+ host: 'cursor',
51
+ hookEvent: 'pre_compact',
52
+ event_class: eventClass,
53
+ sessionId,
54
+ ...(intent ? { intent } : {}),
55
+ });
56
+ bootPath = writeSessionBoot(root, payload);
57
+ stationLine = payload.stationLine;
58
+ receiptPath = writeCursorPrecompactReceipt(root, {
59
+ event_class: eventClass,
60
+ hookEvent: 'pre_compact',
61
+ trigger: event.trigger || 'synthetic',
62
+ sessionId,
63
+ bootPath,
64
+ timestamp: new Date().toISOString(),
65
+ });
66
+ usagePath = writeCursorUsageReceipt(root, {
67
+ eventClass,
68
+ sessionId,
69
+ usage: hostUsageFromPreCompactEvent(event),
70
+ });
71
+ }
64
72
  appendHookActivity(eventRoot, 'cursor-pre-compact', 'station-written', 'success', {
65
73
  bootPath,
66
74
  receiptPath,
67
75
  usagePath,
68
76
  event_class: eventClass,
69
- stationLine: payload.stationLine,
77
+ stationLine,
70
78
  });
71
79
  console.log(
72
80
  JSON.stringify({
@@ -9,6 +9,7 @@ import {
9
9
  appendHookActivity,
10
10
  checkCodexPatterns,
11
11
  checkFullTestSuite,
12
+ cursorHeatRoots,
12
13
  cursorSessionId,
13
14
  cursorToolContext,
14
15
  cursorWorkspaceRoot,
@@ -47,16 +48,16 @@ async function main() {
47
48
  try {
48
49
  const event = await readStdinJson();
49
50
  const eventRoot = cursorWorkspaceRoot(event);
50
- ensureCursorSessionBoot(eventRoot, '0xray/cursor-pre-tool-use-boot', {
51
- sessionId: cursorSessionId(event),
52
- });
51
+ const sessionId = cursorSessionId(event);
52
+ for (const root of cursorHeatRoots(event)) {
53
+ ensureCursorSessionBoot(root, '0xray/cursor-pre-tool-use-boot', { sessionId });
54
+ }
53
55
 
54
56
  const features = loadFeatures(eventRoot);
55
57
  const gateFeatures = loadDelegationGateFeatures(eventRoot, 'cursor');
56
58
  const ctx = cursorToolContext(event);
57
59
  toolName = ctx.toolName;
58
60
  const { content, cmd, toolInput } = ctx;
59
- const sessionId = cursorSessionId(event);
60
61
 
61
62
  const gateBlock = evaluatePreToolGate(toolName, toolInput, {
62
63
  projectRoot: eventRoot,
@@ -306,10 +306,12 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
306
306
  ...(conferPending ? { conferPending: true, conferTrigger: 'analyze-complexity at synthesis checkpoint' } : {}),
307
307
  ...(userAsideBoot ?? {}),
308
308
  sessionId,
309
- timestamp: new Date().toISOString(),
310
309
  source,
311
310
  ...extra,
312
311
  ...heat,
312
+ hook: source,
313
+ source,
314
+ timestamp: new Date().toISOString(),
313
315
  };
314
316
  }
315
317
 
@@ -468,6 +468,20 @@ async function handlePreProcess(input, projectRoot, logDir) {
468
468
  const { tool, args } = input;
469
469
  const startTime = Date.now();
470
470
 
471
+ try {
472
+ const gate = await import('../hooks/delegation-gate-runtime.mjs');
473
+ if (typeof gate.maybeHeatHostStation === 'function') {
474
+ gate.maybeHeatHostStation(projectRoot, 'hermes', {
475
+ source: '0xray/hermes-pre-process',
476
+ sessionId: input.sessionId || 'hermes',
477
+ hookEvent: 'pre_tool_use',
478
+ ...(input.intent ? { intent: input.intent } : {}),
479
+ });
480
+ }
481
+ } catch {
482
+ /* live memory is best-effort — quality gate still runs */
483
+ }
484
+
471
485
  logToolEvent(logDir, "start", tool, args);
472
486
  logToActivity(logDir, `pre-process: tool=${tool}`);
473
487
 
@@ -5,7 +5,17 @@
5
5
  * Heat writers merge stock fields; unknown keys and ## Durable / ## Seed survive.
6
6
  */
7
7
  const { execFileSync } = require("child_process");
8
- const { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");
8
+ const {
9
+ closeSync,
10
+ copyFileSync,
11
+ existsSync,
12
+ mkdirSync,
13
+ openSync,
14
+ readFileSync,
15
+ statSync,
16
+ unlinkSync,
17
+ writeFileSync,
18
+ } = require("fs");
9
19
  const { join } = require("path");
10
20
 
11
21
  const HOOKS_DIR = __dirname;
@@ -229,8 +239,8 @@ function notesPath(root) {
229
239
  function repertoirePackageRoots(root) {
230
240
  const candidates = [
231
241
  join(root, "node_modules", "@0xray", "repertoire"),
232
- join(root, "..", "repertoire"),
233
242
  join(root, "vendor", "@0xray", "repertoire"),
243
+ join(root, "..", "repertoire"),
234
244
  ];
235
245
  const out = [];
236
246
  for (const pkgRoot of candidates) {
@@ -285,7 +295,12 @@ function mergeMissingSignals(destPath, incoming) {
285
295
  for (const signal of incoming) {
286
296
  const name = String(signal.name).trim();
287
297
  if (!name || have.has(name)) continue;
288
- data.signals.push(signal);
298
+ data.signals.push({
299
+ ...signal,
300
+ name,
301
+ tags: Array.isArray(signal.tags) ? signal.tags : [],
302
+ definition: typeof signal.definition === "string" ? signal.definition : name,
303
+ });
289
304
  have.add(name);
290
305
  added += 1;
291
306
  }
@@ -374,7 +389,151 @@ function gitCommitSubjects(root, args) {
374
389
  }
375
390
  }
376
391
 
377
- /** Every floor that heats. Dedup per HEAD. Cursor-only copy was the leftover. */
392
+ function destLockPath(root) {
393
+ return join(root, ".xray", "state", "repertoire", "dest.lock");
394
+ }
395
+
396
+ function sleepMs(ms) {
397
+ try {
398
+ const buf = new Int32Array(new SharedArrayBuffer(4));
399
+ Atomics.wait(buf, 0, 0, ms);
400
+ } catch {
401
+ /* lock retry is best-effort */
402
+ }
403
+ }
404
+
405
+ /** Parallel live contexts share one project copy. Exclusive create; stale after 60s. */
406
+ function withDestLock(root, fn) {
407
+ const lockPath = destLockPath(root);
408
+ mkdirSync(join(root, ".xray", "state", "repertoire"), { recursive: true });
409
+ const started = Date.now();
410
+ while (Date.now() - started < 50000) {
411
+ try {
412
+ const fd = openSync(lockPath, "wx");
413
+ try {
414
+ writeFileSync(fd, `${JSON.stringify({ pid: process.pid, at: new Date().toISOString() })}\n`);
415
+ return fn();
416
+ } finally {
417
+ try {
418
+ closeSync(fd);
419
+ } catch {
420
+ /* leftover */
421
+ }
422
+ try {
423
+ unlinkSync(lockPath);
424
+ } catch {
425
+ /* leftover */
426
+ }
427
+ }
428
+ } catch (err) {
429
+ if (!err || err.code !== "EEXIST") return fn();
430
+ try {
431
+ const st = statSync(lockPath);
432
+ if (Date.now() - st.mtimeMs > 60000) unlinkSync(lockPath);
433
+ } catch {
434
+ /* leftover */
435
+ }
436
+ sleepMs(40);
437
+ }
438
+ }
439
+ return fn();
440
+ }
441
+
442
+ function slugPrimitiveName(raw) {
443
+ const trimmed = String(raw || "").trim();
444
+ if (!trimmed) return null;
445
+ const bare = trimmed.replace(/^@[^/]+\//, "");
446
+ const slug = bare
447
+ .toLowerCase()
448
+ .replace(/[^a-z0-9]+/g, "-")
449
+ .replace(/^-+|-+$/g, "");
450
+ if (!/^[a-z][a-z0-9-]{2,119}$/.test(slug)) return null;
451
+ if (/^phase-\d/.test(slug)) return null;
452
+ if (/^\d/.test(slug)) return null;
453
+ if (
454
+ slug === "criteria-selection-gap" ||
455
+ slug === "external-norm-smuggling-risk" ||
456
+ slug === "model-latent-geometry-as-true-invariant"
457
+ ) {
458
+ return null;
459
+ }
460
+ return slug;
461
+ }
462
+
463
+ function stripConventionalSubject(message) {
464
+ return String(message || "")
465
+ .replace(/^(feat|fix|chore|docs|test|refactor|perf|style|ci|build|revert)(\([^)]+\))?:\s*/i, "")
466
+ .trim();
467
+ }
468
+
469
+ /** Real git → dest names. Not a leftover catalog. Kernel structural names when the diff shows them. */
470
+ function patternsFromGit(root, commits, span) {
471
+ const patterns = [];
472
+ const seen = new Set();
473
+ const add = (raw, confidence, description) => {
474
+ const slug = slugPrimitiveName(raw);
475
+ if (!slug || seen.has(slug)) return;
476
+ seen.add(slug);
477
+ patterns.push({ name: slug, confidence, description: description || String(raw) });
478
+ };
479
+ for (const row of commits) {
480
+ add(stripConventionalSubject(row.message), 0.55, row.message);
481
+ }
482
+ try {
483
+ const from = span && span.from ? String(span.from) : "";
484
+ const to = span && span.to ? String(span.to) : "HEAD";
485
+ if (from && !from.startsWith("HEAD~")) {
486
+ const out = execFileSync("git", ["diff", "--name-status", `${from}..${to}`], {
487
+ cwd: root,
488
+ encoding: "utf8",
489
+ timeout: 4000,
490
+ stdio: ["ignore", "pipe", "pipe"],
491
+ });
492
+ const lines = String(out).split("\n").filter(Boolean);
493
+ if (lines.some((line) => /\.(test|spec)\.[jt]sx?|__tests__|\/tests\//.test(line))) {
494
+ add("test-expansion", 0.55, "git test files");
495
+ }
496
+ if (lines.some((line) => /^D[\t ]/.test(line))) {
497
+ add("dead-code-removal", 0.55, "git deletions");
498
+ }
499
+ if (lines.some((line) => /hooks\/station|session-capture|semantic-patterns/.test(line))) {
500
+ add("station-heat", 0.55, "heat path");
501
+ }
502
+ }
503
+ } catch {
504
+ /* structural names are extra — commit slugs still grow dest */
505
+ }
506
+ return patterns;
507
+ }
508
+
509
+ function latestSessionNewerThanGrow(root) {
510
+ const latest = join(root, "docs", "inference", "latest-session.json");
511
+ if (!existsSync(latest)) return false;
512
+ const working = readRepertoireWorking(root);
513
+ const grownAt = working && working.grow && (working.grow.at || working.updatedAt);
514
+ if (!grownAt) return true;
515
+ try {
516
+ return statSync(latest).mtimeMs > Date.parse(String(grownAt));
517
+ } catch {
518
+ return true;
519
+ }
520
+ }
521
+
522
+ /** Capture then grow. Skip-path for floors that already have a live card. */
523
+ function heatLiveMemory(root) {
524
+ const mr = readMemoryRoutingConfig(root);
525
+ if (isExplicitMemoryRoutingOptOut(mr)) {
526
+ return { captured: null, grow: null };
527
+ }
528
+ return withDestLock(root, () => {
529
+ const captured = maybeCaptureSessionOnHeadMove(root);
530
+ const grow = captured || latestSessionNewerThanGrow(root) ? growDestOnWake(root) : null;
531
+ if (grow) persistRepertoireWorking(root, { grow });
532
+ return { captured, grow };
533
+ });
534
+ }
535
+
536
+ /** Every floor that heats. Dedup per HEAD. Patterns come from git, not a leftover catalog. */
378
537
  function maybeCaptureSessionOnHeadMove(root) {
379
538
  const cfg = readSessionCaptureConfig(root);
380
539
  if (!cfg.enabled) return null;
@@ -393,18 +552,19 @@ function maybeCaptureSessionOnHeadMove(root) {
393
552
  if (commits.length < cfg.minCommits) return null;
394
553
  const sessionId = `session-${new Date().toISOString().slice(0, 10)}-${git.head}`;
395
554
  const approaches = commits.map((row) => row.message).filter(Boolean);
555
+ const span = {
556
+ from: stamp && stamp.head ? stamp.head : commits[commits.length - 1] ? commits[commits.length - 1].hash : git.head,
557
+ to: git.head,
558
+ };
396
559
  const session = {
397
560
  sessionId,
398
561
  timestamp: new Date().toISOString(),
399
- span: {
400
- from: stamp && stamp.head ? stamp.head : `HEAD~${cfg.lookback}`,
401
- to: git.head,
402
- },
562
+ span,
403
563
  problems: [],
404
564
  approaches,
405
565
  wrongTurns: [],
406
566
  solutions: [],
407
- patterns: [],
567
+ patterns: patternsFromGit(root, commits, span),
408
568
  matched_primitives: [],
409
569
  metrics: { commits: commits.length },
410
570
  };
@@ -476,6 +636,24 @@ function matchStationSignalsSync(root, intent) {
476
636
  }
477
637
  }
478
638
 
639
+ function growDestOnWake(root) {
640
+ const helper = join(HOOKS_DIR, "station-memory-ingest.mjs");
641
+ if (!existsSync(helper)) return null;
642
+ try {
643
+ const out = execFileSync(process.execPath, [helper, root, "--grow"], {
644
+ encoding: "utf8",
645
+ timeout: 45000,
646
+ stdio: ["ignore", "pipe", "pipe"],
647
+ env: { ...process.env, REPERTOIRE_FIELD_SYNC: "0", REPERTOIRE_DEST_LOCK: "held" },
648
+ });
649
+ const line = String(out).trim().split("\n").filter(Boolean).at(-1) || "{}";
650
+ const parsed = JSON.parse(line);
651
+ return parsed && typeof parsed === "object" ? parsed : null;
652
+ } catch {
653
+ return null;
654
+ }
655
+ }
656
+
479
657
  function ingestCompactFeedbackSync(root, sessionId, hookEvent, signals) {
480
658
  if (!signals.length) return;
481
659
  const helper = join(HOOKS_DIR, "station-memory-ingest.mjs");
@@ -535,8 +713,25 @@ function buildRepertoireResume(root) {
535
713
  }
536
714
 
537
715
  function applyStationHeat(root, host, extra = {}, existing = {}) {
538
- const hydrate = hydrateDestOnWake(root);
539
- const captured = maybeCaptureSessionOnHeadMove(root);
716
+ const mr = readMemoryRoutingConfig(root);
717
+ const memoryOff = isExplicitMemoryRoutingOptOut(mr);
718
+ const live = memoryOff
719
+ ? { hydrate: { dest: null, added: 0, destCount: 0 }, captured: null, grow: null }
720
+ : withDestLock(root, () => {
721
+ const hydrateInner = hydrateDestOnWake(root);
722
+ const capturedInner = maybeCaptureSessionOnHeadMove(root);
723
+ const growInner = growDestOnWake(root);
724
+ return { hydrate: hydrateInner, captured: capturedInner, grow: growInner };
725
+ });
726
+ const hydrate = live.hydrate;
727
+ const captured = live.captured;
728
+ const grow = live.grow;
729
+ const destCountAfterGrow =
730
+ grow && typeof grow.after === "number"
731
+ ? grow.after
732
+ : existsSync(destSignalsPath(root))
733
+ ? countCuratedSignals(destSignalsPath(root)) || 0
734
+ : hydrate.destCount;
540
735
  const pickup = readNotesPickup(root);
541
736
  const approaches = readLatestSessionApproaches(root);
542
737
  const prevHost = typeof existing.host === "string" ? existing.host : null;
@@ -568,8 +763,11 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
568
763
  matchedSignals = preferSubjectHits(extra.matchedSignals, 8);
569
764
  }
570
765
  const priorWorking = readRepertoireWorking(root);
571
- const destCount = hydrate.destCount || (existsSync(destSignalsPath(root)) ? countCuratedSignals(destSignalsPath(root)) : 0);
572
- if (!matchedSignals.length && matchText) {
766
+ const destCount =
767
+ destCountAfterGrow ||
768
+ hydrate.destCount ||
769
+ (existsSync(destSignalsPath(root)) ? countCuratedSignals(destSignalsPath(root)) : 0);
770
+ if (!memoryOff && !matchedSignals.length && matchText) {
573
771
  if (
574
772
  !rematch &&
575
773
  priorWorking &&
@@ -602,12 +800,22 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
602
800
  if (pickup) workingSnapshot.pickup = pickup;
603
801
  if (matchText) workingSnapshot.matchText = matchText;
604
802
  if (captured) workingSnapshot.sessionCapture = captured;
803
+ if (grow && grow.skipped) workingSnapshot.grow = grow.skipped;
804
+ if (grow && typeof grow.after === "number") {
805
+ workingSnapshot.grow = {
806
+ before: grow.before,
807
+ after: grow.after,
808
+ imported: grow.imported,
809
+ observed: Array.isArray(grow.observed) ? grow.observed.length : 0,
810
+ };
811
+ }
605
812
  if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals.slice(0, 8);
606
813
  const opProcNames = readOpProcNames(root);
607
814
  if (opProcNames.length) workingSnapshot.opProcNames = opProcNames;
608
815
  const working = persistRepertoireWorking(root, workingSnapshot);
609
816
  const pickupChanged = Boolean(pickup && (!priorWorking || priorWorking.pickup !== pickup));
610
817
  if (
818
+ !memoryOff &&
611
819
  matchedSignals.length &&
612
820
  (isCompactHook(extra) || hydrate.added > 0 || pickupChanged)
613
821
  ) {
@@ -663,6 +871,22 @@ function stripHoldNpm(line) {
663
871
  .replace(/[ \t]+$/g, "");
664
872
  }
665
873
 
874
+ /** Same-session skip must still rewrite when HEAD, organ count, or Hold npm diverges. */
875
+ function stationBootNeedsRefresh(existing, root, host) {
876
+ if (!existing || typeof existing !== "object") return true;
877
+ if (host && existing.host && existing.host !== host) return true;
878
+ if (!existing.suit_profile) return true;
879
+ if (existing.workspaceRoot && existing.workspaceRoot !== root) return true;
880
+ if (!existing.stationLine) return true;
881
+ const liveGit = readGitBrief(root);
882
+ const bootHead = existing.git && existing.git.head ? String(existing.git.head) : "";
883
+ if (liveGit && liveGit.head && bootHead !== liveGit.head) return true;
884
+ const liveResume = buildRepertoireResume(root);
885
+ if (liveResume && existing.repertoireResume && liveResume !== existing.repertoireResume) return true;
886
+ if (stationDurableHoldsNpm(root)) return true;
887
+ return false;
888
+ }
889
+
666
890
  function stationDurableHoldsNpm(root) {
667
891
  return readExistingStationMarkdown(root)
668
892
  .split(/\r?\n/)
@@ -790,6 +1014,11 @@ module.exports = {
790
1014
  readRepertoireWorking,
791
1015
  readOpProcNames,
792
1016
  hydrateDestOnWake,
1017
+ growDestOnWake,
1018
+ heatLiveMemory,
1019
+ withDestLock,
1020
+ slugPrimitiveName,
1021
+ patternsFromGit,
793
1022
  readNotesPickup,
794
1023
  maybeCaptureSessionOnHeadMove,
795
1024
  formatWorkingLine,
@@ -800,4 +1029,5 @@ module.exports = {
800
1029
  writeStationMarkdown,
801
1030
  isHoldNpmLine,
802
1031
  stationDurableHoldsNpm,
1032
+ stationBootNeedsRefresh,
803
1033
  };
@@ -20,6 +20,11 @@ export const persistRepertoireWorking = impl.persistRepertoireWorking;
20
20
  export const readRepertoireWorking = impl.readRepertoireWorking;
21
21
  export const readOpProcNames = impl.readOpProcNames;
22
22
  export const hydrateDestOnWake = impl.hydrateDestOnWake;
23
+ export const growDestOnWake = impl.growDestOnWake;
24
+ export const heatLiveMemory = impl.heatLiveMemory;
25
+ export const withDestLock = impl.withDestLock;
26
+ export const slugPrimitiveName = impl.slugPrimitiveName;
27
+ export const patternsFromGit = impl.patternsFromGit;
23
28
  export const readNotesPickup = impl.readNotesPickup;
24
29
  export const maybeCaptureSessionOnHeadMove = impl.maybeCaptureSessionOnHeadMove;
25
30
  export const formatWorkingLine = impl.formatWorkingLine;
@@ -30,3 +35,4 @@ export const formatStationMarkdown = impl.formatStationMarkdown;
30
35
  export const writeStationMarkdown = impl.writeStationMarkdown;
31
36
  export const isHoldNpmLine = impl.isHoldNpmLine;
32
37
  export const stationDurableHoldsNpm = impl.stationDurableHoldsNpm;
38
+ export const stationBootNeedsRefresh = impl.stationBootNeedsRefresh;