0xray 4.0.21 → 4.0.23

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 (103) 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 +28 -6
  4. package/.xray/features.json +1 -1
  5. package/AGENTS-consumer.md +11 -11
  6. package/AGENTS.md +8 -8
  7. package/README.md +27 -27
  8. package/SKILLS.md +1 -1
  9. package/dist/AGENTS.md +8 -8
  10. package/dist/CHANGELOG.md +98 -0
  11. package/dist/README.md +27 -27
  12. package/dist/SKILLS.md +1 -1
  13. package/dist/governance/governance-core.d.ts +2 -1
  14. package/dist/governance/governance-core.js +20 -11
  15. package/dist/governance/governance-service.js +9 -7
  16. package/dist/inference/inference-accumulator.js +36 -4
  17. package/dist/inference/inference-cycle.d.ts +12 -0
  18. package/dist/inference/inference-cycle.js +180 -21
  19. package/dist/inference/inference-proposal-generator.js +148 -5
  20. package/dist/inference/session-capture.d.ts +4 -0
  21. package/dist/integrations/cursor/README.md +1 -1
  22. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +19 -0
  23. package/dist/integrations/cursor/hooks/pre-tool-use.js +2 -1
  24. package/dist/integrations/governance/index.d.ts +5 -0
  25. package/dist/integrations/governance/index.js +22 -2
  26. package/dist/integrations/governance/types.d.ts +11 -0
  27. package/dist/integrations/governance/types.js +16 -0
  28. package/dist/integrations/hooks/station-hook-runtime.cjs +90 -30
  29. package/dist/integrations/hooks/station-hook-runtime.mjs +1 -0
  30. package/dist/integrations/hooks/station-memory-ingest.mjs +4 -2
  31. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  32. package/dist/mcps/governance.server.d.ts +2 -0
  33. package/dist/mcps/governance.server.js +12 -1
  34. package/dist/memory-routing/index.d.ts +1 -1
  35. package/dist/memory-routing/index.js +1 -1
  36. package/dist/memory-routing/lesson-pickup.d.ts +2 -0
  37. package/dist/memory-routing/lesson-pickup.js +57 -0
  38. package/dist/memory-routing/provider-loader.d.ts +8 -1
  39. package/dist/memory-routing/provider-loader.js +49 -1
  40. package/dist/memory-routing/provider-registry.d.ts +7 -2
  41. package/dist/memory-routing/provider-registry.js +26 -6
  42. package/dist/memory-routing/record-lesson.d.ts +16 -0
  43. package/dist/memory-routing/record-lesson.js +26 -0
  44. package/dist/memory-routing/types.d.ts +15 -0
  45. package/dist/nucleus/thin-dispatch.d.ts +2 -0
  46. package/dist/nucleus/thin-dispatch.js +14 -4
  47. package/dist/processors/implementations/agents-md-validation-processor.js +1 -1
  48. package/dist/skills/hermes-agent/SKILL.md +1 -1
  49. package/dist/skills/orchestrator/SKILL.md +28 -6
  50. package/package.json +2 -2
  51. package/scripts/foundry/CHANGELOG.md +2 -0
  52. package/scripts/foundry/README.md +2 -2
  53. package/scripts/foundry/cli.mjs +1 -1
  54. package/scripts/foundry/inspect.mjs +160 -5
  55. package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -0
  56. package/scripts/foundry/reconcile-version.mjs +23 -1
  57. package/scripts/foundry/release.mjs +1 -1
  58. package/scripts/foundry/validate-release-docs.mjs +24 -20
  59. package/scripts/foundry/version-manager.mjs +180 -7
  60. package/scripts/mjs/test-consumer-readiness.mjs +10 -3
  61. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  62. package/src/mcps/governance.server.ts +12 -1
  63. package/src/skills/hermes-agent/SKILL.md +1 -1
  64. package/src/skills/orchestrator/SKILL.md +28 -6
  65. package/vendor/@0xray/repertoire/data/stack-overlay.json +156 -3
  66. package/vendor/@0xray/repertoire/data/subject-overlay.json +3 -3
  67. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts +4 -0
  68. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +1 -1
  69. package/vendor/@0xray/repertoire/dist/RepertoireService.js +7 -6
  70. package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +1 -1
  71. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +1 -1
  72. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js +10 -11
  73. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +1 -1
  74. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +1 -1
  75. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js +5 -0
  76. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +1 -1
  77. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +1 -1
  78. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -0
  79. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +1 -1
  80. package/vendor/@0xray/repertoire/dist/paths.d.ts +3 -3
  81. package/vendor/@0xray/repertoire/dist/paths.d.ts.map +1 -1
  82. package/vendor/@0xray/repertoire/dist/paths.js +66 -4
  83. package/vendor/@0xray/repertoire/dist/paths.js.map +1 -1
  84. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +11 -0
  85. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +1 -1
  86. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +2 -0
  87. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +1 -1
  88. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +54 -4
  89. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
  90. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +375 -20
  91. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
  92. package/vendor/@0xray/repertoire/dist/types.d.ts +21 -0
  93. package/vendor/@0xray/repertoire/dist/types.d.ts.map +1 -1
  94. package/xray/agents_template.md +10 -5
  95. package/xray/features.json +1 -1
  96. package/dist/mcps/shared/prompt-security-validator.d.ts +0 -44
  97. package/dist/mcps/shared/prompt-security-validator.js +0 -148
  98. package/dist/mcps/shared/security-scanner.d.ts +0 -119
  99. package/dist/mcps/shared/security-scanner.js +0 -429
  100. package/dist/scripts/activate-kernel-pipeline.d.ts +0 -7
  101. package/dist/scripts/activate-kernel-pipeline.js +0 -101
  102. package/src/mcps/shared/prompt-security-validator.ts +0 -199
  103. package/src/mcps/shared/security-scanner.ts +0 -599
@@ -39,6 +39,11 @@ export declare class InferenceGovernanceIntegration extends BaseIntegration {
39
39
  * Check if governance is enabled and available
40
40
  */
41
41
  isAvailable(): boolean;
42
+ /**
43
+ * Start the Dynamo client when a caller required external governance
44
+ * and boot skipped it because inference_governance.enabled is false.
45
+ */
46
+ ensureDynamoClient(): Promise<void>;
42
47
  /**
43
48
  * Check a single inference proposal via the appropriate governance tool
44
49
  *
@@ -7,7 +7,7 @@
7
7
  * @since 2026-05-11
8
8
  */
9
9
  import { BaseIntegration } from '../base/index.js';
10
- import { DEFAULT_GOVERNANCE_CONFIG } from './types.js';
10
+ import { DEFAULT_GOVERNANCE_CONFIG, resolveDynamoBaseUrl } from './types.js';
11
11
  import { GovernanceClient } from './governance-client.js';
12
12
  import { frameworkLogger } from '../../core/framework-logger.js';
13
13
  import { featuresConfigLoader } from '../../core/features-config.js';
@@ -84,7 +84,27 @@ export class InferenceGovernanceIntegration extends BaseIntegration {
84
84
  * Check if governance is enabled and available
85
85
  */
86
86
  isAvailable() {
87
- return this.configData.enabled && this.client !== null && this.status === 'initialized';
87
+ return this.client !== null && this.status === 'initialized';
88
+ }
89
+ /**
90
+ * Start the Dynamo client when a caller required external governance
91
+ * and boot skipped it because inference_governance.enabled is false.
92
+ */
93
+ async ensureDynamoClient() {
94
+ if (this.status !== 'initialized') {
95
+ await this.initialize();
96
+ }
97
+ if (this.client !== null) {
98
+ return;
99
+ }
100
+ const endpoint = resolveDynamoBaseUrl(this.configData.endpointUrl);
101
+ this.client = new GovernanceClient({
102
+ baseUrl: endpoint,
103
+ timeoutMs: this.configData.requestTimeoutMs,
104
+ });
105
+ await this.log('info', 'Dynamo client armed for required external governance', {
106
+ endpoint,
107
+ });
88
108
  }
89
109
  /**
90
110
  * Check a single inference proposal via the appropriate governance tool
@@ -56,6 +56,17 @@ export interface GovernanceIntegrationConfig {
56
56
  voteWeightMultiplier: number;
57
57
  };
58
58
  }
59
+ /**
60
+ * Public Dynamo Solar SSOT. Used only when a caller requires external
61
+ * governance and neither features nor GOVERNANCE_ENDPOINT names a host.
62
+ * The client posts to /call_connected_tool on this origin.
63
+ */
64
+ export declare const PUBLIC_DYNAMO_SSOT = "https://mcp-production-80e2.up.railway.app";
65
+ /**
66
+ * Origin for POST /call_connected_tool.
67
+ * Preference: configured endpoint, then GOVERNANCE_ENDPOINT, then the public SSOT.
68
+ */
69
+ export declare function resolveDynamoBaseUrl(configured: string): string;
59
70
  /**
60
71
  * Default governance configuration
61
72
  */
@@ -6,6 +6,22 @@
6
6
  *
7
7
  * @since 2026-05-11
8
8
  */
9
+ /**
10
+ * Public Dynamo Solar SSOT. Used only when a caller requires external
11
+ * governance and neither features nor GOVERNANCE_ENDPOINT names a host.
12
+ * The client posts to /call_connected_tool on this origin.
13
+ */
14
+ export const PUBLIC_DYNAMO_SSOT = 'https://mcp-production-80e2.up.railway.app';
15
+ /**
16
+ * Origin for POST /call_connected_tool.
17
+ * Preference: configured endpoint, then GOVERNANCE_ENDPOINT, then the public SSOT.
18
+ */
19
+ export function resolveDynamoBaseUrl(configured) {
20
+ const fromConfig = configured.trim();
21
+ const fromEnv = (process.env.GOVERNANCE_ENDPOINT || '').trim();
22
+ const raw = fromConfig || fromEnv || PUBLIC_DYNAMO_SSOT;
23
+ return raw.replace(/\/governance\/?$/, '').replace(/\/$/, '');
24
+ }
9
25
  /**
10
26
  * Default governance configuration
11
27
  */
@@ -395,6 +395,30 @@ function pruneKeywordDest(root) {
395
395
  return { removed, kept: kept.length };
396
396
  }
397
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
+
398
422
  function mergeMissingSignals(destPath, incoming) {
399
423
  if (!incoming.length || !existsSync(destPath)) return 0;
400
424
  let data;
@@ -404,24 +428,35 @@ function mergeMissingSignals(destPath, incoming) {
404
428
  return 0;
405
429
  }
406
430
  if (!Array.isArray(data.signals)) return 0;
407
- const have = new Set(
408
- data.signals.map((signal) => String((signal && signal.name) || "").trim()).filter(Boolean),
409
- );
410
- 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;
411
437
  for (const signal of incoming) {
412
438
  const name = String(signal.name).trim();
413
- if (!name || have.has(name)) continue;
414
- data.signals.push({
415
- ...signal,
416
- name,
417
- tags: Array.isArray(signal.tags) ? signal.tags : [],
418
- definition: typeof signal.definition === "string" ? signal.definition : name,
419
- });
420
- have.add(name);
421
- 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;
454
+ }
455
+ if (changed) {
456
+ data.last_updated = new Date().toISOString();
457
+ writeFileSync(destPath, `${JSON.stringify(data, null, 2)}\n`);
422
458
  }
423
- if (added) writeFileSync(destPath, `${JSON.stringify(data, null, 2)}\n`);
424
- return added;
459
+ return changed;
425
460
  }
426
461
 
427
462
  /** Hydrate project dest from seed + stack. Subject repo-* stay off dest. */
@@ -661,6 +696,7 @@ function maybeCaptureSessionOnHeadMove(root) {
661
696
  from: stamp && stamp.head ? stamp.head : commits[commits.length - 1] ? commits[commits.length - 1].hash : git.head,
662
697
  to: git.head,
663
698
  };
699
+ const patterns = patternsFromGit(root, commits, span);
664
700
  const session = {
665
701
  sessionId,
666
702
  timestamp: new Date().toISOString(),
@@ -669,8 +705,8 @@ function maybeCaptureSessionOnHeadMove(root) {
669
705
  approaches,
670
706
  wrongTurns: [],
671
707
  solutions: [],
672
- patterns: patternsFromGit(root, commits, span),
673
- matched_primitives: [],
708
+ patterns,
709
+ matched_primitives: preferLawHits(patterns.map((row) => row && row.name)),
674
710
  metrics: { commits: commits.length },
675
711
  };
676
712
  const outDir = join(root, "docs", "inference");
@@ -754,8 +790,16 @@ function growDestOnWake(root) {
754
790
  const line = String(out).trim().split("\n").filter(Boolean).at(-1) || "{}";
755
791
  const parsed = JSON.parse(line);
756
792
  const pruned = pruneKeywordDest(root);
757
- if (!parsed || typeof parsed !== "object") return { pruned };
758
- return { ...parsed, pruned: pruned.removed, destCount: countCuratedSignals(destSignalsPath(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
+ };
759
803
  } catch {
760
804
  return null;
761
805
  }
@@ -811,6 +855,28 @@ function retainCompactFields(existing, extra = {}) {
811
855
  return kept;
812
856
  }
813
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
+
814
880
  function formatWorkingLine(working) {
815
881
  if (!working || typeof working !== "object") return null;
816
882
  const matched = stationSafeSignals(working.matchedSignals);
@@ -826,7 +892,7 @@ function buildRepertoireResume(root) {
826
892
  const modulePath = resolveRepertoireProviderModule(root);
827
893
  const mr = readMemoryRoutingConfig(root);
828
894
  if (!modulePath) {
829
- return "Repertoire: not installed (memory_routing stays off)";
895
+ return "Repertoire: module unresolved";
830
896
  }
831
897
  const destPath = destSignalsPath(root);
832
898
  let signalsPath = existsSync(destPath) ? destPath : mr.config && mr.config.signalsPath;
@@ -932,15 +998,8 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
932
998
  if (pickup) workingSnapshot.pickup = pickup;
933
999
  if (matchText) workingSnapshot.matchText = matchText;
934
1000
  if (captured) workingSnapshot.sessionCapture = captured;
935
- if (grow && grow.skipped) workingSnapshot.grow = grow.skipped;
936
- if (grow && typeof grow.after === "number") {
937
- workingSnapshot.grow = {
938
- before: grow.before,
939
- after: grow.after,
940
- imported: grow.imported,
941
- observed: Array.isArray(grow.observed) ? grow.observed.length : 0,
942
- };
943
- }
1001
+ const growReceipt = workingGrowSnapshot(grow);
1002
+ if (growReceipt) workingSnapshot.grow = growReceipt;
944
1003
  if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals.slice(0, 8);
945
1004
  const opProcNames = readOpProcNames(root);
946
1005
  if (opProcNames.length) workingSnapshot.opProcNames = opProcNames;
@@ -1108,7 +1167,7 @@ function formatStationMarkdown(fields) {
1108
1167
  } else {
1109
1168
  lines.push("Git: n/a");
1110
1169
  }
1111
- lines.push(fields.repertoireResume || "Repertoire: not installed (memory_routing stays off)");
1170
+ lines.push(fields.repertoireResume || "Repertoire: module unresolved");
1112
1171
  if (fields.workingLine) {
1113
1172
  lines.push(fields.workingLine);
1114
1173
  }
@@ -1165,6 +1224,7 @@ module.exports = {
1165
1224
  readNotesPickup,
1166
1225
  maybeCaptureSessionOnHeadMove,
1167
1226
  formatWorkingLine,
1227
+ workingGrowSnapshot,
1168
1228
  applyStationHeat,
1169
1229
  isStockTicket,
1170
1230
  readStationTicketField,
@@ -28,6 +28,7 @@ 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;
32
33
  export const isStockTicket = impl.isStockTicket;
33
34
  export const readStationTicketField = impl.readStationTicketField;
@@ -128,13 +128,15 @@ async function growDest(projectRoot) {
128
128
  sources: [...(kernel.sources || []), ...cleanup.sources],
129
129
  });
130
130
  const after = service.signalsManager.load().signals.length;
131
+ const heatedNames = Array.isArray(diary.heated) ? diary.heated : [];
131
132
  process.stdout.write(
132
133
  `${JSON.stringify({
133
134
  before,
134
135
  after,
135
136
  imported: sessions.imported,
136
- observed: 0,
137
- heated: diary.heated,
137
+ observed: heatedNames.length,
138
+ heated: heatedNames,
139
+ destCount: after,
138
140
  })}\n`,
139
141
  );
140
142
  return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.21",
3
+ "version": "4.0.23",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -20,6 +20,8 @@ declare class GovernanceServer extends XrayKnowledgeSkillBase {
20
20
  protected setupToolHandlers(): void;
21
21
  /** Align tool default with features.json inference_governance.enabled (P0.2). */
22
22
  private resolveRequireExternal;
23
+ /** Boot stays flag-gated. A required external call starts the client on demand. */
24
+ private armRequiredDynamo;
23
25
  private handleGovernProposals;
24
26
  private handleGovernReflection;
25
27
  private handleGetActiveCodex;
@@ -196,8 +196,19 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
196
196
  return false;
197
197
  }
198
198
  }
199
+ /** Boot stays flag-gated. A required external call starts the client on demand. */
200
+ async armRequiredDynamo(requireExternal) {
201
+ if (!requireExternal)
202
+ return;
203
+ const integration = await initializeGovernanceIntegration();
204
+ if (!integration.isAvailable()) {
205
+ await integration.ensureDynamoClient();
206
+ }
207
+ }
199
208
  async handleGovernProposals(args) {
200
209
  const service = getGovernanceService();
210
+ const requireExternal = this.resolveRequireExternal(args.options?.require_external);
211
+ await this.armRequiredDynamo(requireExternal);
201
212
  const request = {
202
213
  proposals: args.proposals.map((p, i) => ({
203
214
  id: p.id || `prop-${Date.now()}-${i}`,
@@ -210,7 +221,7 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
210
221
  })),
211
222
  context: args.context || {},
212
223
  options: {
213
- requireExternalDynamo: this.resolveRequireExternal(args.options?.require_external),
224
+ requireExternalDynamo: requireExternal,
214
225
  },
215
226
  };
216
227
  frameworkLogger.log("governance-mcp", "delegating-to-governance-service", "info", {
@@ -1,5 +1,5 @@
1
1
  export type { MemoryRoutingProvider, MemoryRoutingContext, MemoryInheritedContext, MemoryAgentCapability, MemoryOrchestrationTask, MemoryThinDispatchResult, MemoryRoutingConfig, OrchestratorFeedbackEntry, } from './types.js';
2
- export { getMemoryRoutingProvider, getMemoryRoutingProviderSync, getMemoryRoutingConfig, resetMemoryRoutingProvider, initializeMemoryRouting, } from './provider-registry.js';
2
+ export { getMemoryRoutingProvider, getMemoryRoutingProviderSync, ensureMemoryRoutingProviderSync, getMemoryRoutingConfig, resetMemoryRoutingProvider, initializeMemoryRouting, } from './provider-registry.js';
3
3
  export { NullMemoryRoutingProvider, createMemoryRoutingProvider } from './null-provider.js';
4
4
  export { loadMemoryRoutingProvider, isLeftoverMemoryRoutingOff, isExplicitMemoryRoutingOptOut, resolveLeftoverEnabledConfig, } from './provider-loader.js';
5
5
  export { validateMemoryRoutingConfig } from './validate-config.js';
@@ -1,4 +1,4 @@
1
- export { getMemoryRoutingProvider, getMemoryRoutingProviderSync, getMemoryRoutingConfig, resetMemoryRoutingProvider, initializeMemoryRouting, } from './provider-registry.js';
1
+ export { getMemoryRoutingProvider, getMemoryRoutingProviderSync, ensureMemoryRoutingProviderSync, getMemoryRoutingConfig, resetMemoryRoutingProvider, initializeMemoryRouting, } from './provider-registry.js';
2
2
  export { NullMemoryRoutingProvider, createMemoryRoutingProvider } from './null-provider.js';
3
3
  export { loadMemoryRoutingProvider, isLeftoverMemoryRoutingOff, isExplicitMemoryRoutingOptOut, resolveLeftoverEnabledConfig, } from './provider-loader.js';
4
4
  export { validateMemoryRoutingConfig } from './validate-config.js';
@@ -0,0 +1,2 @@
1
+ /** Refresh the Station lessons section from dest. Leaves the rest of the card alone. */
2
+ export declare function writeLessonPickup(projectRoot: string): void;
@@ -0,0 +1,57 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ const START = "<!-- lessons -->";
4
+ const END = "<!-- /lessons -->";
5
+ const DEST = path.join(".xray", "state", "repertoire", "curated_signals.json");
6
+ const FLOOR = 0.55;
7
+ function aboveFloor(value) {
8
+ return typeof value === "number" && value > FLOOR && Math.abs(value - FLOOR) > 1e-4;
9
+ }
10
+ function lessonBlock(signals) {
11
+ const lines = ["## Lessons"];
12
+ const graded = signals.filter((signal) => typeof signal.name === "string"
13
+ && aboveFloor(signal.observation_stats?.avg_confidence)
14
+ && (signal.lessons?.length ?? 0) > 0);
15
+ if (graded.length === 0) {
16
+ lines.push("No graded lines above the floor.");
17
+ return lines.join("\n");
18
+ }
19
+ for (const signal of graded) {
20
+ lines.push(`### ${signal.name}`);
21
+ if (typeof signal.definition === "string" && signal.definition.length > 0) {
22
+ lines.push(signal.definition);
23
+ }
24
+ for (const lesson of signal.lessons ?? []) {
25
+ const text = lesson.text.length > 0 ? lesson.text : "graded";
26
+ lines.push(`- ${lesson.decision} \`${lesson.taskId}\` — ${text}`);
27
+ }
28
+ }
29
+ return lines.join("\n");
30
+ }
31
+ /** Refresh the Station lessons section from dest. Leaves the rest of the card alone. */
32
+ export function writeLessonPickup(projectRoot) {
33
+ const stationPath = path.join(projectRoot, ".xray", "state", "STATION.md");
34
+ if (!fs.existsSync(stationPath))
35
+ return;
36
+ const destPath = path.join(projectRoot, DEST);
37
+ let signals = [];
38
+ if (fs.existsSync(destPath)) {
39
+ try {
40
+ const parsed = JSON.parse(fs.readFileSync(destPath, "utf-8"));
41
+ if (parsed && typeof parsed === "object" && "signals" in parsed && Array.isArray(parsed.signals)) {
42
+ signals = parsed.signals;
43
+ }
44
+ }
45
+ catch {
46
+ signals = [];
47
+ }
48
+ }
49
+ const section = `${START}\n${lessonBlock(signals)}\n${END}`;
50
+ const current = fs.readFileSync(stationPath, "utf-8");
51
+ const pattern = /<!-- lessons -->[\s\S]*?<!-- \/lessons -->/;
52
+ const next = pattern.test(current)
53
+ ? current.replace(pattern, section)
54
+ : `${current.trimEnd()}\n\n${section}\n`;
55
+ if (next !== current)
56
+ fs.writeFileSync(stationPath, next);
57
+ }
@@ -1,6 +1,13 @@
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;
8
+ /**
9
+ * First route cannot wait on the async import. Node loads this ESM with require.
10
+ * Returns null when routing is off or the module cannot be constructed.
11
+ */
12
+ export declare function loadMemoryRoutingProviderSync(config: MemoryRoutingConfig, cwd?: string): MemoryRoutingProvider | null;
6
13
  export declare function loadMemoryRoutingProvider(config: MemoryRoutingConfig, cwd?: string): Promise<MemoryRoutingProvider>;
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
2
3
  import { resolve, dirname, join } from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
  import { frameworkLogger } from '../core/framework-logger.js';
@@ -19,6 +20,7 @@ function resolveUnavailableReason(provider) {
19
20
  return null;
20
21
  }
21
22
  }
23
+ const require = createRequire(import.meta.url);
22
24
  const REPERTOIRE_CANDIDATE_PATHS = [
23
25
  'node_modules/@0xray/repertoire/dist/provider/memory-routing-provider.js',
24
26
  'vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js',
@@ -59,7 +61,9 @@ function defaultPathForProvider(provider, cwd) {
59
61
  export function isExplicitMemoryRoutingOptOut(config) {
60
62
  return config.enabled === false && config.provider === 'repertoire';
61
63
  }
62
- /** Leftover shipped default: enabled false + provider null. Not an explicit opt-out. */
64
+ /** Leftover shipped default: enabled false + provider null. Not an explicit opt-out.
65
+ * resolveLeftoverEnabledConfig migrates old tarballs when the organ is on disk.
66
+ * Explicit opt-out is enabled: false + provider: repertoire. */
63
67
  export function isLeftoverMemoryRoutingOff(config) {
64
68
  if (config.enabled === true)
65
69
  return false;
@@ -97,6 +101,50 @@ export function resolveLeftoverEnabledConfig(config, cwd) {
97
101
  enabled.config = config.config;
98
102
  return enabled;
99
103
  }
104
+ function providerFromModule(mod, modulePath, effective) {
105
+ const factory = mod.createMemoryRoutingProvider ?? mod.default?.createMemoryRoutingProvider;
106
+ if (typeof factory !== 'function') {
107
+ throw new Error(`Module ${modulePath} must export createMemoryRoutingProvider()`);
108
+ }
109
+ const provider = factory((effective.config ?? {}));
110
+ if (!provider?.id || typeof provider.isAvailable !== 'function') {
111
+ throw new Error(`Invalid memory routing provider from ${modulePath}`);
112
+ }
113
+ return provider;
114
+ }
115
+ function resolveEffectiveProvider(config, cwd) {
116
+ const validation = validateMemoryRoutingConfig(config);
117
+ if (!validation.valid)
118
+ return null;
119
+ const leftover = resolveLeftoverEnabledConfig(validation.normalized, cwd);
120
+ const effective = leftover ?? validation.normalized;
121
+ if (!effective.enabled || effective.provider === 'null')
122
+ return null;
123
+ const modulePath = resolveModulePath(effective.module_path, cwd) ??
124
+ defaultPathForProvider(effective.provider, cwd);
125
+ if (!modulePath)
126
+ return null;
127
+ return { effective, modulePath };
128
+ }
129
+ /**
130
+ * First route cannot wait on the async import. Node loads this ESM with require.
131
+ * Returns null when routing is off or the module cannot be constructed.
132
+ */
133
+ export function loadMemoryRoutingProviderSync(config, cwd = process.cwd()) {
134
+ const resolved = resolveEffectiveProvider(config, cwd);
135
+ if (!resolved)
136
+ return null;
137
+ try {
138
+ const mod = require(resolved.modulePath);
139
+ const provider = providerFromModule(mod, resolved.modulePath, resolved.effective);
140
+ if (!provider.isAvailable())
141
+ return null;
142
+ return provider;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
100
148
  export async function loadMemoryRoutingProvider(config, cwd = process.cwd()) {
101
149
  const validation = validateMemoryRoutingConfig(config);
102
150
  if (!validation.valid) {
@@ -1,8 +1,13 @@
1
1
  import type { MemoryRoutingConfig, MemoryRoutingProvider } from './types.js';
2
2
  export declare function getMemoryRoutingConfig(): MemoryRoutingConfig;
3
3
  export declare function getMemoryRoutingProvider(forceReload?: boolean): Promise<MemoryRoutingProvider>;
4
- /** Synchronous accessor — returns shared null singleton until async load completes. */
4
+ /** Synchronous accessor — returns shared null singleton until a load completes. */
5
5
  export declare function getMemoryRoutingProviderSync(): MemoryRoutingProvider;
6
+ /**
7
+ * The first scoreAndRoute call loads the organ before it routes.
8
+ * A later sync read sees the same provider. Off or a missing module stays null.
9
+ */
10
+ export declare function ensureMemoryRoutingProviderSync(): MemoryRoutingProvider;
6
11
  export declare function resetMemoryRoutingProvider(): void;
7
- /** Kick off background load at module init. */
12
+ /** Kick off load at module init. Sync require first so a later route does not require() an ESM module while its dynamic import is still in flight. */
8
13
  export declare function initializeMemoryRouting(): void;
@@ -1,6 +1,6 @@
1
1
  import { featuresConfigLoader } from '../core/features-config.js';
2
2
  import { NullMemoryRoutingProvider } from './null-provider.js';
3
- import { loadMemoryRoutingProvider, resolveLeftoverEnabledConfig } from './provider-loader.js';
3
+ import { loadMemoryRoutingProvider, loadMemoryRoutingProviderSync, resolveLeftoverEnabledConfig, } from './provider-loader.js';
4
4
  import { validateMemoryRoutingConfig } from './validate-config.js';
5
5
  let cachedProvider = null;
6
6
  let loadPromise = null;
@@ -12,30 +12,50 @@ export function getMemoryRoutingConfig() {
12
12
  validation.normalized);
13
13
  }
14
14
  export async function getMemoryRoutingProvider(forceReload = false) {
15
- if (!forceReload && cachedProvider)
15
+ if (!forceReload && cachedProvider && cachedProvider.id !== 'null')
16
16
  return cachedProvider;
17
17
  if (!forceReload && loadPromise)
18
18
  return loadPromise;
19
19
  const config = getMemoryRoutingConfig();
20
20
  loadPromise = loadMemoryRoutingProvider(config).then((provider) => {
21
+ if (provider.id === 'null' && cachedProvider && cachedProvider.id !== 'null') {
22
+ loadPromise = null;
23
+ return cachedProvider;
24
+ }
21
25
  cachedProvider = provider;
22
26
  loadPromise = null;
23
27
  return provider;
24
28
  });
25
29
  return loadPromise;
26
30
  }
27
- /** Synchronous accessor — returns shared null singleton until async load completes. */
31
+ /** Synchronous accessor — returns shared null singleton until a load completes. */
28
32
  export function getMemoryRoutingProviderSync() {
29
33
  return cachedProvider ?? nullProviderSingleton;
30
34
  }
35
+ /**
36
+ * The first scoreAndRoute call loads the organ before it routes.
37
+ * A later sync read sees the same provider. Off or a missing module stays null.
38
+ */
39
+ export function ensureMemoryRoutingProviderSync() {
40
+ if (cachedProvider && cachedProvider.id !== 'null')
41
+ return cachedProvider;
42
+ const config = getMemoryRoutingConfig();
43
+ if (!config.enabled)
44
+ return cachedProvider ?? nullProviderSingleton;
45
+ const loaded = loadMemoryRoutingProviderSync(config);
46
+ if (!loaded || loaded.id === 'null')
47
+ return cachedProvider ?? nullProviderSingleton;
48
+ cachedProvider = loaded;
49
+ return loaded;
50
+ }
31
51
  export function resetMemoryRoutingProvider() {
32
52
  cachedProvider = null;
33
53
  loadPromise = null;
34
54
  }
35
- /** Kick off background load at module init. */
55
+ /** Kick off load at module init. Sync require first so a later route does not require() an ESM module while its dynamic import is still in flight. */
36
56
  export function initializeMemoryRouting() {
37
- const config = getMemoryRoutingConfig();
38
- if (config.enabled) {
57
+ const loaded = ensureMemoryRoutingProviderSync();
58
+ if (loaded.id === 'null' && getMemoryRoutingConfig().enabled) {
39
59
  void getMemoryRoutingProvider();
40
60
  }
41
61
  }
@@ -0,0 +1,16 @@
1
+ export interface LessonInput {
2
+ operation: string;
3
+ success: boolean;
4
+ taskId: string;
5
+ assignedAgent: string;
6
+ sessionId: string;
7
+ /** Signals the finished task already named. When set, text matching is not used. */
8
+ signals?: string[];
9
+ /** Approaches, solutions, or the wrong turn from the sessions that named the law. */
10
+ lesson?: string;
11
+ }
12
+ /**
13
+ * A finished task names the signals its text actually matched and says
14
+ * whether the outcome was good. That is the one-tenth step. No match, no write.
15
+ */
16
+ export declare function recordLesson(input: LessonInput): string[];
@@ -0,0 +1,26 @@
1
+ import { ensureMemoryRoutingProviderSync } from "./provider-registry.js";
2
+ /**
3
+ * A finished task names the signals its text actually matched and says
4
+ * whether the outcome was good. That is the one-tenth step. No match, no write.
5
+ */
6
+ export function recordLesson(input) {
7
+ const provider = ensureMemoryRoutingProviderSync();
8
+ if (provider.id === "null" || !provider.ingestFeedback)
9
+ return [];
10
+ const named = input.signals ?? provider.buildRoutingContext(input.operation).matchedSignals;
11
+ const names = [...new Set(named.filter((name) => name.length > 0))];
12
+ if (names.length === 0)
13
+ return [];
14
+ provider.ingestFeedback({
15
+ timestamp: new Date().toISOString(),
16
+ sessionId: input.sessionId,
17
+ taskId: input.taskId,
18
+ assignedAgent: input.assignedAgent,
19
+ memorySignals: names,
20
+ complexity: 0,
21
+ success: input.success,
22
+ durationMs: 0,
23
+ lesson: input.lesson ?? "",
24
+ });
25
+ return names;
26
+ }