0xray 4.0.20 → 4.0.22

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 (74) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/hermes-agent/SKILL.md +1 -1
  3. package/.opencode/skills/orchestrator/SKILL.md +23 -1
  4. package/.xray/features.json +1 -1
  5. package/AGENTS-consumer.md +15 -15
  6. package/AGENTS.md +12 -12
  7. package/README.md +40 -40
  8. package/SKILLS.md +4 -4
  9. package/dist/AGENTS.md +12 -12
  10. package/dist/CHANGELOG.md +91 -8
  11. package/dist/README.md +40 -40
  12. package/dist/SKILLS.md +4 -4
  13. package/dist/cli/commands/validate.d.ts +1 -1
  14. package/dist/cli/commands/validate.js +7 -1
  15. package/dist/governance/governance-core.d.ts +2 -1
  16. package/dist/governance/governance-core.js +20 -11
  17. package/dist/governance/governance-service.js +9 -7
  18. package/dist/inference/inference-cycle.js +22 -5
  19. package/dist/integrations/cursor/README.md +7 -6
  20. package/dist/integrations/cursor/hooks/after-file-edit.sh +3 -0
  21. package/dist/integrations/cursor/hooks/before-read-file.sh +3 -0
  22. package/dist/integrations/cursor/hooks/before-shell-execution.sh +3 -0
  23. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +112 -12
  24. package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +3 -0
  25. package/dist/integrations/cursor/hooks/hooks.json +13 -3
  26. package/dist/integrations/cursor/hooks/pre-compact.js +9 -0
  27. package/dist/integrations/cursor/hooks/pre-compact.sh +3 -0
  28. package/dist/integrations/cursor/hooks/pre-tool-use.js +17 -12
  29. package/dist/integrations/cursor/hooks/pre-tool-use.sh +3 -0
  30. package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +63 -0
  31. package/dist/integrations/grok/hooks/grok-hook-utils.js +3 -2
  32. package/dist/integrations/hooks/station-hook-runtime.cjs +310 -98
  33. package/dist/integrations/hooks/station-hook-runtime.mjs +10 -0
  34. package/dist/integrations/hooks/station-memory-ingest.mjs +4 -3
  35. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  36. package/dist/memory-routing/provider-loader.d.ts +3 -1
  37. package/dist/memory-routing/provider-loader.js +3 -1
  38. package/dist/nucleus/suit-temperament.d.ts +0 -5
  39. package/dist/nucleus/suit-temperament.js +13 -1
  40. package/dist/nucleus/thin-dispatch.js +11 -2
  41. package/dist/processors/implementations/agents-md-validation-processor.js +1 -1
  42. package/dist/skills/hermes-agent/SKILL.md +1 -1
  43. package/dist/skills/orchestrator/SKILL.md +23 -1
  44. package/llms.txt +1 -1
  45. package/package.json +3 -3
  46. package/scripts/foundry/CHANGELOG.md +2 -0
  47. package/scripts/foundry/README.md +4 -4
  48. package/scripts/foundry/assert-packed-dist-cli.mjs +4 -0
  49. package/scripts/foundry/cli.mjs +1 -1
  50. package/scripts/foundry/inspect.mjs +160 -5
  51. package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -0
  52. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  53. package/scripts/foundry/validate-release-docs.mjs +48 -0
  54. package/scripts/foundry/version-manager.mjs +127 -7
  55. package/scripts/mjs/test-consumer-readiness.mjs +10 -3
  56. package/scripts/node/install-bridges.cjs +256 -10
  57. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  58. package/src/skills/hermes-agent/SKILL.md +1 -1
  59. package/src/skills/orchestrator/SKILL.md +23 -1
  60. package/vendor/@0xray/repertoire/data/stack-overlay.json +154 -1
  61. package/vendor/@0xray/repertoire/dist/RepertoireService.js +7 -6
  62. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js +10 -11
  63. package/vendor/@0xray/repertoire/dist/paths.js +66 -4
  64. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +54 -14
  65. package/xray/agents_template.md +10 -5
  66. package/xray/features.json +1 -1
  67. package/dist/mcps/shared/prompt-security-validator.d.ts +0 -44
  68. package/dist/mcps/shared/prompt-security-validator.js +0 -148
  69. package/dist/mcps/shared/security-scanner.d.ts +0 -119
  70. package/dist/mcps/shared/security-scanner.js +0 -429
  71. package/dist/scripts/activate-kernel-pipeline.d.ts +0 -7
  72. package/dist/scripts/activate-kernel-pipeline.js +0 -101
  73. package/src/mcps/shared/prompt-security-validator.ts +0 -199
  74. package/src/mcps/shared/security-scanner.ts +0 -599
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Station heat — compaction / host-swap card.
3
- * SSOT remains session-boot.json. STATION.md is the projection the model Reads.
3
+ * STATION.md is the pickup ticket. session-boot.json is the snapshot.
4
+ * Heat must not paint leftover boot extras onto the card.
4
5
  * Grok ignores SessionStart/UserPromptSubmit stdout; disk + AGENTS.md is the contract.
5
6
  * Heat writers merge stock fields; unknown keys and ## Durable / ## Seed survive.
6
7
  */
@@ -104,6 +105,63 @@ function readPlanLine(root) {
104
105
  return readGitSubject(root);
105
106
  }
106
107
 
108
+ function isStockTicket(value) {
109
+ const text = String(value || "").trim().toLowerCase();
110
+ return !text || text === "(none)" || text === "(none yet)";
111
+ }
112
+
113
+ function readStationTicketField(root, field) {
114
+ const md = readExistingStationMarkdown(root);
115
+ const re = field === "Plan" ? /^Plan:\s*(.*)$/im : /^Intent:\s*(.*)$/im;
116
+ const match = md.match(re);
117
+ if (!match) return null;
118
+ const value = clipIntent(match[1]);
119
+ if (!value || isStockTicket(value)) return null;
120
+ return value;
121
+ }
122
+
123
+ function bootHeadMoved(root, existing) {
124
+ const live = readGitBrief(root);
125
+ const bootHead = existing && existing.git && existing.git.head ? String(existing.git.head) : "";
126
+ return Boolean(live && live.head && bootHead && bootHead !== live.head);
127
+ }
128
+
129
+ /** Live ticket beats leftover boot. Extra spoken intent still wins. */
130
+ function resolveHeatIntent(root, extra, existing) {
131
+ const incoming = clipIntent(
132
+ extra.intent || extra.prompt || extra.userMessage || extra.user_prompt,
133
+ );
134
+ if (incoming) return { intent: incoming, rematch: true };
135
+ const card = readStationTicketField(root, "Intent");
136
+ const pickup = clipIntent(readNotesPickup(root));
137
+ const bootIntent = typeof existing.intent === "string" ? clipIntent(existing.intent) : null;
138
+ const cardIsBootEcho = Boolean(card && bootIntent && card === bootIntent);
139
+ if (pickup && cardIsBootEcho && pickup !== bootIntent) {
140
+ return { intent: pickup, rematch: true };
141
+ }
142
+ if (card && !cardIsBootEcho) return { intent: card, rematch: card !== bootIntent };
143
+ if (pickup && pickup !== bootIntent) return { intent: pickup, rematch: true };
144
+ if (card) return { intent: card, rematch: false };
145
+ if (bootIntent) return { intent: bootIntent, rematch: false };
146
+ const git = readGitSubject(root);
147
+ return { intent: git, rematch: Boolean(git) };
148
+ }
149
+
150
+ function resolveHeatPlan(root, extra, existing) {
151
+ const incoming = clipIntent(extra.plan || extra.planLine);
152
+ if (incoming) return incoming;
153
+ const card = readStationTicketField(root, "Plan");
154
+ const bootPlan = typeof existing.planLine === "string" ? clipIntent(existing.planLine) : null;
155
+ const live = readPlanLine(root);
156
+ const cardIsBootEcho = Boolean(card && bootPlan && card === bootPlan);
157
+ if (card && !cardIsBootEcho) return card;
158
+ if (bootHeadMoved(root, existing) && bootPlan && live === bootPlan) {
159
+ return readGitSubject(root) || card || live;
160
+ }
161
+ if (card) return card;
162
+ return live;
163
+ }
164
+
107
165
  function resolveRepertoireProviderModule(root) {
108
166
  const mr = readMemoryRoutingConfig(root);
109
167
  if (mr && typeof mr.module_path === "string" && mr.module_path) {
@@ -200,32 +258,44 @@ function readRepertoireWorking(root) {
200
258
  }
201
259
  }
202
260
 
261
+ function isKeywordDestName(name) {
262
+ const n = String(name || "").trim().toLowerCase();
263
+ if (!n) return true;
264
+ if (n.startsWith("repo-") || n.startsWith("bedrock-")) return true;
265
+ if (n === "test-expansion" || n === "dead-code-removal" || n === "station-heat") return true;
266
+ if (/^release-v?\d/.test(n) || /^stamp-/.test(n) || /^drop-stale-/.test(n)) return true;
267
+ if (/\b4-0-\d{2}\b/.test(n) && /npm|package-lock|release|stamp/.test(n)) return true;
268
+ return false;
269
+ }
270
+
271
+ function isGenericFieldObservedSignal(signal) {
272
+ const definition = String((signal && signal.definition) || "");
273
+ return /field-observed domain primitive/i.test(definition);
274
+ }
275
+
203
276
  function stationSafeSignals(names) {
204
277
  if (!Array.isArray(names)) return [];
205
278
  const out = [];
206
279
  for (const raw of names) {
207
280
  const name = String(raw || "").trim();
208
- if (!name) continue;
209
- if (name.toLowerCase().startsWith("bedrock-")) continue;
281
+ if (!name || isKeywordDestName(name)) continue;
210
282
  out.push(name);
211
283
  if (out.length >= 4) break;
212
284
  }
213
285
  return out;
214
286
  }
215
287
 
216
- /** Keep subject repo-* in the short list the card and working file hold. */
217
- function preferSubjectHits(names, cap = 8) {
288
+ /** Laws only. Hangar repo-* and git-slug keywords stay off the card. */
289
+ function preferLawHits(names, cap = 8) {
218
290
  if (!Array.isArray(names)) return [];
219
291
  const unique = [];
220
292
  for (const raw of names) {
221
293
  const name = String(raw || "").trim();
222
- if (!name || name.toLowerCase().startsWith("bedrock-")) continue;
294
+ if (!name || isKeywordDestName(name)) continue;
223
295
  if (unique.includes(name)) continue;
224
296
  unique.push(name);
225
297
  }
226
- const subject = unique.filter((name) => name.startsWith("repo-"));
227
- const rest = unique.filter((name) => !name.startsWith("repo-"));
228
- return [...subject, ...rest].slice(0, cap);
298
+ return unique.slice(0, cap);
229
299
  }
230
300
 
231
301
  function destSignalsPath(root) {
@@ -279,6 +349,76 @@ function signalNameSet(filePath) {
279
349
  return new Set(readSignalRecords(filePath).map((signal) => String(signal.name).trim()));
280
350
  }
281
351
 
352
+ function destLawNameSet(root) {
353
+ const names = new Set();
354
+ for (const fileName of ["curated_signals.json", "stack-overlay.json"]) {
355
+ for (const name of signalNameSet(repertoireDataFile(root, fileName))) {
356
+ if (!isKeywordDestName(name)) names.add(name);
357
+ }
358
+ }
359
+ for (const name of signalNameSet(destSignalsPath(root))) {
360
+ if (!isKeywordDestName(name)) names.add(name);
361
+ }
362
+ return names;
363
+ }
364
+
365
+ /** Drop hangar repo-* and git-slug keywords. Keep factory + stack laws. */
366
+ function pruneKeywordDest(root) {
367
+ const dest = destSignalsPath(root);
368
+ if (!dest || !existsSync(dest)) return { removed: 0, kept: 0 };
369
+ let data;
370
+ try {
371
+ data = JSON.parse(readFileSync(dest, "utf8"));
372
+ } catch {
373
+ return { removed: 0, kept: 0 };
374
+ }
375
+ if (!Array.isArray(data.signals)) return { removed: 0, kept: 0 };
376
+ const factory = signalNameSet(repertoireDataFile(root, "curated_signals.json"));
377
+ const stack = signalNameSet(repertoireDataFile(root, "stack-overlay.json"));
378
+ const kept = [];
379
+ let removed = 0;
380
+ for (const signal of data.signals) {
381
+ const name = String((signal && signal.name) || "").trim();
382
+ if (!name) continue;
383
+ const protectedLaw = factory.has(name) || stack.has(name);
384
+ if (!protectedLaw && (isKeywordDestName(name) || isGenericFieldObservedSignal(signal))) {
385
+ removed += 1;
386
+ continue;
387
+ }
388
+ kept.push(signal);
389
+ }
390
+ if (removed) {
391
+ data.signals = kept;
392
+ data.last_updated = new Date().toISOString();
393
+ writeFileSync(dest, `${JSON.stringify(data, null, 2)}\n`);
394
+ }
395
+ return { removed, kept: kept.length };
396
+ }
397
+
398
+ const STACK_LAW_FIELDS = [
399
+ "definition",
400
+ "tags",
401
+ "priority",
402
+ "evaluation_criteria",
403
+ "validation_experiment",
404
+ "example_inference_snippet",
405
+ "implementation_notes",
406
+ ];
407
+
408
+ /** Stack text cadences onto an existing project law. Observation stats stay. */
409
+ function refreshStackLaw(existing, signal) {
410
+ let dirty = false;
411
+ for (const field of STACK_LAW_FIELDS) {
412
+ if (signal[field] == null) continue;
413
+ const next = signal[field];
414
+ const prev = existing[field];
415
+ if (JSON.stringify(prev) === JSON.stringify(next)) continue;
416
+ existing[field] = next;
417
+ dirty = true;
418
+ }
419
+ return dirty;
420
+ }
421
+
282
422
  function mergeMissingSignals(destPath, incoming) {
283
423
  if (!incoming.length || !existsSync(destPath)) return 0;
284
424
  let data;
@@ -288,27 +428,38 @@ function mergeMissingSignals(destPath, incoming) {
288
428
  return 0;
289
429
  }
290
430
  if (!Array.isArray(data.signals)) return 0;
291
- const have = new Set(
292
- data.signals.map((signal) => String((signal && signal.name) || "").trim()).filter(Boolean),
293
- );
294
- let added = 0;
431
+ const byName = new Map();
432
+ for (const signal of data.signals) {
433
+ const name = String((signal && signal.name) || "").trim();
434
+ if (name) byName.set(name, signal);
435
+ }
436
+ let changed = 0;
295
437
  for (const signal of incoming) {
296
438
  const name = String(signal.name).trim();
297
- if (!name || have.has(name)) continue;
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
- });
304
- have.add(name);
305
- added += 1;
439
+ if (!name) continue;
440
+ const existing = byName.get(name);
441
+ if (!existing) {
442
+ const created = {
443
+ ...signal,
444
+ name,
445
+ tags: Array.isArray(signal.tags) ? signal.tags : [],
446
+ definition: typeof signal.definition === "string" ? signal.definition : name,
447
+ };
448
+ data.signals.push(created);
449
+ byName.set(name, created);
450
+ changed += 1;
451
+ continue;
452
+ }
453
+ if (refreshStackLaw(existing, signal)) changed += 1;
306
454
  }
307
- if (added) writeFileSync(destPath, `${JSON.stringify(data, null, 2)}\n`);
308
- return added;
455
+ if (changed) {
456
+ data.last_updated = new Date().toISOString();
457
+ writeFileSync(destPath, `${JSON.stringify(data, null, 2)}\n`);
458
+ }
459
+ return changed;
309
460
  }
310
461
 
311
- /** Hydrate project dest from seed + stack + subject overlays. Never write the tarball. */
462
+ /** Hydrate project dest from seed + stack. Subject repo-* stay off dest. */
312
463
  function hydrateDestOnWake(root) {
313
464
  const dest = destSignalsPath(root);
314
465
  const seed = repertoireDataFile(root, "curated_signals.json");
@@ -317,10 +468,12 @@ function hydrateDestOnWake(root) {
317
468
  copyFileSync(seed, dest);
318
469
  }
319
470
  if (!existsSync(dest)) return { dest: null, added: 0, destCount: 0 };
320
- const added =
321
- mergeMissingSignals(dest, readSignalRecords(repertoireDataFile(root, "stack-overlay.json"))) +
322
- mergeMissingSignals(dest, readSignalRecords(repertoireDataFile(root, "subject-overlay.json")));
323
- return { dest, added, destCount: countCuratedSignals(dest) || 0 };
471
+ const added = mergeMissingSignals(
472
+ dest,
473
+ readSignalRecords(repertoireDataFile(root, "stack-overlay.json")),
474
+ );
475
+ const pruned = pruneKeywordDest(root);
476
+ return { dest, added, destCount: countCuratedSignals(dest) || 0, pruned };
324
477
  }
325
478
 
326
479
  function readNotesPickup(root) {
@@ -405,7 +558,12 @@ function sleepMs(ms) {
405
558
  /** Parallel live contexts share one project copy. Exclusive create; stale after 60s. */
406
559
  function withDestLock(root, fn) {
407
560
  const lockPath = destLockPath(root);
408
- mkdirSync(join(root, ".xray", "state", "repertoire"), { recursive: true });
561
+ try {
562
+ mkdirSync(join(root, ".xray", "state", "repertoire"), { recursive: true });
563
+ } catch (err) {
564
+ if (err && (err.code === "EACCES" || err.code === "EPERM")) return null;
565
+ throw err;
566
+ }
409
567
  const started = Date.now();
410
568
  while (Date.now() - started < 50000) {
411
569
  try {
@@ -466,42 +624,18 @@ function stripConventionalSubject(message) {
466
624
  .trim();
467
625
  }
468
626
 
469
- /** Real git → dest names. Not a leftover catalog. Kernel structural names when the diff shows them. */
470
- function patternsFromGit(root, commits, span) {
627
+ /** Observe existing laws mentioned in git. Do not mint commit slugs. */
628
+ function patternsFromGit(root, commits) {
629
+ const text = (Array.isArray(commits) ? commits : [])
630
+ .map((row) => String((row && row.message) || ""))
631
+ .join("\n")
632
+ .toLowerCase();
471
633
  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
- }
634
+ if (!text.trim()) return patterns;
635
+ for (const name of destLawNameSet(root)) {
636
+ if (text.includes(name) || text.includes(name.replace(/-/g, " "))) {
637
+ patterns.push({ name, confidence: 0.7, description: "observe existing law from git" });
502
638
  }
503
- } catch {
504
- /* structural names are extra — commit slugs still grow dest */
505
639
  }
506
640
  return patterns;
507
641
  }
@@ -521,16 +655,22 @@ function latestSessionNewerThanGrow(root) {
521
655
 
522
656
  /** Capture then grow. Skip-path for floors that already have a live card. */
523
657
  function heatLiveMemory(root) {
524
- const mr = readMemoryRoutingConfig(root);
525
- if (isExplicitMemoryRoutingOptOut(mr)) {
658
+ try {
659
+ const mr = readMemoryRoutingConfig(root);
660
+ if (isExplicitMemoryRoutingOptOut(mr)) {
661
+ return { captured: null, grow: null };
662
+ }
663
+ const result = withDestLock(root, () => {
664
+ const captured = maybeCaptureSessionOnHeadMove(root);
665
+ const grow = captured || latestSessionNewerThanGrow(root) ? growDestOnWake(root) : null;
666
+ if (grow) persistRepertoireWorking(root, { grow });
667
+ return { captured, grow };
668
+ });
669
+ return result || { captured: null, grow: null };
670
+ } catch {
671
+ /* dest EACCES / unwritable heat root must not deny the host tool */
526
672
  return { captured: null, grow: null };
527
673
  }
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
674
  }
535
675
 
536
676
  /** Every floor that heats. Dedup per HEAD. Patterns come from git, not a leftover catalog. */
@@ -556,6 +696,7 @@ function maybeCaptureSessionOnHeadMove(root) {
556
696
  from: stamp && stamp.head ? stamp.head : commits[commits.length - 1] ? commits[commits.length - 1].hash : git.head,
557
697
  to: git.head,
558
698
  };
699
+ const patterns = patternsFromGit(root, commits, span);
559
700
  const session = {
560
701
  sessionId,
561
702
  timestamp: new Date().toISOString(),
@@ -564,8 +705,8 @@ function maybeCaptureSessionOnHeadMove(root) {
564
705
  approaches,
565
706
  wrongTurns: [],
566
707
  solutions: [],
567
- patterns: patternsFromGit(root, commits, span),
568
- matched_primitives: [],
708
+ patterns,
709
+ matched_primitives: preferLawHits(patterns.map((row) => row && row.name)),
569
710
  metrics: { commits: commits.length },
570
711
  };
571
712
  const outDir = join(root, "docs", "inference");
@@ -648,7 +789,17 @@ function growDestOnWake(root) {
648
789
  });
649
790
  const line = String(out).trim().split("\n").filter(Boolean).at(-1) || "{}";
650
791
  const parsed = JSON.parse(line);
651
- return parsed && typeof parsed === "object" ? parsed : null;
792
+ const pruned = pruneKeywordDest(root);
793
+ const destCount = countCuratedSignals(destSignalsPath(root)) || 0;
794
+ if (!parsed || typeof parsed !== "object") return { pruned: pruned.removed, destCount };
795
+ const heated = Array.isArray(parsed.heated) ? parsed.heated.length : 0;
796
+ return {
797
+ ...parsed,
798
+ observed: heated,
799
+ pruned: pruned.removed,
800
+ destCount,
801
+ after: destCount,
802
+ };
652
803
  } catch {
653
804
  return null;
654
805
  }
@@ -679,6 +830,53 @@ function isCompactHook(extra) {
679
830
  return /compact/i.test(hook);
680
831
  }
681
832
 
833
+ function isCompactEventName(value) {
834
+ return /compact/i.test(String(value || ""));
835
+ }
836
+
837
+ /** Keep Compact boot fields after later preToolUse / HEAD rewrite. */
838
+ function retainCompactFields(existing, extra = {}) {
839
+ const incomingHook = extra && (extra.hookEvent || extra.source || extra.hook);
840
+ if (isCompactEventName(incomingHook) || (extra && extra.event_class)) {
841
+ const kept = {};
842
+ if (extra.hookEvent) kept.hookEvent = extra.hookEvent;
843
+ if (extra.conversation_id) kept.conversation_id = extra.conversation_id;
844
+ if (extra.generation_id) kept.generation_id = extra.generation_id;
845
+ if (extra.event_class) kept.event_class = extra.event_class;
846
+ return kept;
847
+ }
848
+ const prior = existing && typeof existing === "object" ? existing : {};
849
+ if (!isCompactEventName(prior.hookEvent) && !prior.event_class) return {};
850
+ const kept = {};
851
+ if (prior.hookEvent) kept.hookEvent = prior.hookEvent;
852
+ if (prior.conversation_id) kept.conversation_id = prior.conversation_id;
853
+ if (prior.generation_id) kept.generation_id = prior.generation_id;
854
+ if (prior.event_class) kept.event_class = prior.event_class;
855
+ return kept;
856
+ }
857
+
858
+ /** Persist heated length. A number stays a number. An array is a count. Never drop a number to 0. */
859
+ function workingGrowSnapshot(grow) {
860
+ if (!grow || typeof grow !== "object") return null;
861
+ if (typeof grow.after !== "number") {
862
+ return typeof grow.skipped === "string" ? grow.skipped : null;
863
+ }
864
+ const observed =
865
+ typeof grow.observed === "number"
866
+ ? grow.observed
867
+ : Array.isArray(grow.observed)
868
+ ? grow.observed.length
869
+ : Array.isArray(grow.heated)
870
+ ? grow.heated.length
871
+ : 0;
872
+ return {
873
+ before: grow.before,
874
+ after: grow.after,
875
+ imported: grow.imported,
876
+ observed,
877
+ };
878
+ }
879
+
682
880
  function formatWorkingLine(working) {
683
881
  if (!working || typeof working !== "object") return null;
684
882
  const matched = stationSafeSignals(working.matchedSignals);
@@ -694,7 +892,7 @@ function buildRepertoireResume(root) {
694
892
  const modulePath = resolveRepertoireProviderModule(root);
695
893
  const mr = readMemoryRoutingConfig(root);
696
894
  if (!modulePath) {
697
- return "Repertoire: not installed (memory_routing stays off)";
895
+ return "Repertoire: module unresolved";
698
896
  }
699
897
  const destPath = destSignalsPath(root);
700
898
  let signalsPath = existsSync(destPath) ? destPath : mr.config && mr.config.signalsPath;
@@ -744,14 +942,12 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
744
942
  ? existing.hotSwap
745
943
  : null;
746
944
  const hotSwap = nextSwap || keptSwap;
747
- const incomingIntent = clipIntent(
748
- extra.intent || extra.prompt || extra.userMessage || extra.user_prompt,
749
- );
750
- const intent = incomingIntent || (typeof existing.intent === "string" ? existing.intent : null);
945
+ const resolved = resolveHeatIntent(root, extra, existing);
946
+ const intent = resolved.intent;
947
+ const rematch = resolved.rematch;
751
948
  const matchText = clipIntent([intent, pickup, approaches].filter(Boolean).join(" "));
752
- const rematch = Boolean(incomingIntent);
753
949
  const git = readGitBrief(root);
754
- const planLine = readPlanLine(root);
950
+ const planLine = resolveHeatPlan(root, extra, existing);
755
951
  const repertoireResume =
756
952
  typeof extra.repertoireResume === "string" ? extra.repertoireResume : buildRepertoireResume(root);
757
953
  const swapBit = hotSwap ? `hot-swap ${hotSwap.from} → ${hotSwap.to}` : `host ${host}`;
@@ -760,7 +956,7 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
760
956
  const planBit = planLine ? `plan: ${planLine}` : "plan: (none)";
761
957
  let matchedSignals = [];
762
958
  if (Array.isArray(extra.matchedSignals) && extra.matchedSignals.length) {
763
- matchedSignals = preferSubjectHits(extra.matchedSignals, 8);
959
+ matchedSignals = preferLawHits(extra.matchedSignals, 8);
764
960
  }
765
961
  const priorWorking = readRepertoireWorking(root);
766
962
  const destCount =
@@ -776,10 +972,10 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
776
972
  Array.isArray(priorWorking.matchedSignals) &&
777
973
  priorWorking.matchedSignals.length
778
974
  ) {
779
- matchedSignals = preferSubjectHits(priorWorking.matchedSignals, 8);
975
+ matchedSignals = preferLawHits(priorWorking.matchedSignals, 8);
780
976
  } else {
781
- matchedSignals = preferSubjectHits(
782
- matchStationSignalsSync(root, rematch && incomingIntent ? incomingIntent : matchText),
977
+ matchedSignals = preferLawHits(
978
+ matchStationSignalsSync(root, rematch && intent ? intent : matchText),
783
979
  8,
784
980
  );
785
981
  }
@@ -787,32 +983,26 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
787
983
  if (!matchedSignals.length && priorWorking) {
788
984
  matchedSignals = stationSafeSignals(priorWorking.matchedSignals);
789
985
  }
986
+ const compactHold = retainCompactFields(existing, extra);
987
+ const nextHook = compactHold.hookEvent || extra.hookEvent || extra.source || null;
790
988
  const workingSnapshot = {
791
989
  host,
792
990
  intent,
793
991
  git,
794
992
  hotSwap,
795
- hookEvent: extra.hookEvent || extra.source || null,
796
993
  memoryRouting: repertoireResume.startsWith("Repertoire: on") ? "on" : "off",
797
994
  repertoireResume,
798
995
  destCount,
799
996
  };
997
+ if (nextHook) workingSnapshot.hookEvent = nextHook;
800
998
  if (pickup) workingSnapshot.pickup = pickup;
801
999
  if (matchText) workingSnapshot.matchText = matchText;
802
1000
  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
- }
1001
+ const growReceipt = workingGrowSnapshot(grow);
1002
+ if (growReceipt) workingSnapshot.grow = growReceipt;
812
1003
  if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals.slice(0, 8);
813
1004
  const opProcNames = readOpProcNames(root);
814
1005
  if (opProcNames.length) workingSnapshot.opProcNames = opProcNames;
815
- const working = persistRepertoireWorking(root, workingSnapshot);
816
1006
  const pickupChanged = Boolean(pickup && (!priorWorking || priorWorking.pickup !== pickup));
817
1007
  if (
818
1008
  !memoryOff &&
@@ -826,6 +1016,11 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
826
1016
  matchedSignals,
827
1017
  );
828
1018
  }
1019
+ if (!memoryOff) {
1020
+ const latePrune = pruneKeywordDest(root);
1021
+ if (latePrune.kept) workingSnapshot.destCount = latePrune.kept;
1022
+ }
1023
+ const working = persistRepertoireWorking(root, workingSnapshot);
829
1024
  const workingLine = formatWorkingLine(working);
830
1025
  const workingBit = workingLine ? workingLine : "working: (none)";
831
1026
  const stationLine = `${swapBit}. ${intentBit}. ${planBit}. ${gitBit}. ${repertoireResume}. ${workingBit}`;
@@ -838,6 +1033,7 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
838
1033
  repertoireResume,
839
1034
  workingLine,
840
1035
  stationLine,
1036
+ ...compactHold,
841
1037
  };
842
1038
  }
843
1039
 
@@ -884,6 +1080,12 @@ function stationBootNeedsRefresh(existing, root, host) {
884
1080
  const liveResume = buildRepertoireResume(root);
885
1081
  if (liveResume && existing.repertoireResume && liveResume !== existing.repertoireResume) return true;
886
1082
  if (stationDurableHoldsNpm(root)) return true;
1083
+ const cardIntent = readStationTicketField(root, "Intent");
1084
+ const bootIntent = typeof existing.intent === "string" ? clipIntent(existing.intent) : null;
1085
+ if (cardIntent && bootIntent && cardIntent !== bootIntent) return true;
1086
+ const cardPlan = readStationTicketField(root, "Plan");
1087
+ const bootPlan = typeof existing.planLine === "string" ? clipIntent(existing.planLine) : null;
1088
+ if (cardPlan && bootPlan && cardPlan !== bootPlan) return true;
887
1089
  return false;
888
1090
  }
889
1091
 
@@ -965,7 +1167,7 @@ function formatStationMarkdown(fields) {
965
1167
  } else {
966
1168
  lines.push("Git: n/a");
967
1169
  }
968
- lines.push(fields.repertoireResume || "Repertoire: not installed (memory_routing stays off)");
1170
+ lines.push(fields.repertoireResume || "Repertoire: module unresolved");
969
1171
  if (fields.workingLine) {
970
1172
  lines.push(fields.workingLine);
971
1173
  }
@@ -1022,7 +1224,12 @@ module.exports = {
1022
1224
  readNotesPickup,
1023
1225
  maybeCaptureSessionOnHeadMove,
1024
1226
  formatWorkingLine,
1227
+ workingGrowSnapshot,
1025
1228
  applyStationHeat,
1229
+ isStockTicket,
1230
+ readStationTicketField,
1231
+ resolveHeatIntent,
1232
+ resolveHeatPlan,
1026
1233
  extractPreservedStationLines,
1027
1234
  mergeStationMarkdown,
1028
1235
  formatStationMarkdown,
@@ -1030,4 +1237,9 @@ module.exports = {
1030
1237
  isHoldNpmLine,
1031
1238
  stationDurableHoldsNpm,
1032
1239
  stationBootNeedsRefresh,
1240
+ isKeywordDestName,
1241
+ pruneKeywordDest,
1242
+ preferLawHits,
1243
+ retainCompactFields,
1244
+ destLawNameSet,
1033
1245
  };
@@ -28,7 +28,12 @@ export const patternsFromGit = impl.patternsFromGit;
28
28
  export const readNotesPickup = impl.readNotesPickup;
29
29
  export const maybeCaptureSessionOnHeadMove = impl.maybeCaptureSessionOnHeadMove;
30
30
  export const formatWorkingLine = impl.formatWorkingLine;
31
+ export const workingGrowSnapshot = impl.workingGrowSnapshot;
31
32
  export const applyStationHeat = impl.applyStationHeat;
33
+ export const isStockTicket = impl.isStockTicket;
34
+ export const readStationTicketField = impl.readStationTicketField;
35
+ export const resolveHeatIntent = impl.resolveHeatIntent;
36
+ export const resolveHeatPlan = impl.resolveHeatPlan;
32
37
  export const extractPreservedStationLines = impl.extractPreservedStationLines;
33
38
  export const mergeStationMarkdown = impl.mergeStationMarkdown;
34
39
  export const formatStationMarkdown = impl.formatStationMarkdown;
@@ -36,3 +41,8 @@ export const writeStationMarkdown = impl.writeStationMarkdown;
36
41
  export const isHoldNpmLine = impl.isHoldNpmLine;
37
42
  export const stationDurableHoldsNpm = impl.stationDurableHoldsNpm;
38
43
  export const stationBootNeedsRefresh = impl.stationBootNeedsRefresh;
44
+ export const isKeywordDestName = impl.isKeywordDestName;
45
+ export const pruneKeywordDest = impl.pruneKeywordDest;
46
+ export const preferLawHits = impl.preferLawHits;
47
+ export const retainCompactFields = impl.retainCompactFields;
48
+ export const destLawNameSet = impl.destLawNameSet;
@@ -118,7 +118,6 @@ async function growDest(projectRoot) {
118
118
  ensureDestShape(destSignals);
119
119
  const before = service.signalsManager.load().signals.length;
120
120
  const sessions = service.syncXrayMemory();
121
- const siblings = service.syncWorkspaceRepos();
122
121
  const kernel =
123
122
  typeof pathsMod.collectKernelDiaryText === 'function'
124
123
  ? pathsMod.collectKernelDiaryText(projectRoot)
@@ -129,13 +128,15 @@ async function growDest(projectRoot) {
129
128
  sources: [...(kernel.sources || []), ...cleanup.sources],
130
129
  });
131
130
  const after = service.signalsManager.load().signals.length;
131
+ const heatedNames = Array.isArray(diary.heated) ? diary.heated : [];
132
132
  process.stdout.write(
133
133
  `${JSON.stringify({
134
134
  before,
135
135
  after,
136
136
  imported: sessions.imported,
137
- observed: siblings.observed,
138
- heated: diary.heated,
137
+ observed: heatedNames.length,
138
+ heated: heatedNames,
139
+ destCount: after,
139
140
  })}\n`,
140
141
  );
141
142
  return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.20",
3
+ "version": "4.0.22",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -1,6 +1,8 @@
1
1
  import type { MemoryRoutingConfig, MemoryRoutingProvider } from './types.js';
2
2
  export declare function isExplicitMemoryRoutingOptOut(config: MemoryRoutingConfig): boolean;
3
- /** Leftover shipped default: enabled false + provider null. Not an explicit opt-out. */
3
+ /** Leftover shipped default: enabled false + provider null. Not an explicit opt-out.
4
+ * resolveLeftoverEnabledConfig migrates old tarballs when the organ is on disk.
5
+ * Explicit opt-out is enabled: false + provider: repertoire. */
4
6
  export declare function isLeftoverMemoryRoutingOff(config: MemoryRoutingConfig): boolean;
5
7
  export declare function resolveLeftoverEnabledConfig(config: MemoryRoutingConfig, cwd: string): MemoryRoutingConfig | null;
6
8
  export declare function loadMemoryRoutingProvider(config: MemoryRoutingConfig, cwd?: string): Promise<MemoryRoutingProvider>;