0xray 4.0.23 → 4.0.24

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 (56) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +11 -1
  3. package/.xray/features.schema.json +17 -0
  4. package/AGENTS-consumer.md +1 -1
  5. package/AGENTS.md +1 -1
  6. package/README.md +2 -2
  7. package/SKILLS.md +1 -1
  8. package/dist/AGENTS.md +1 -1
  9. package/dist/CHANGELOG.md +20 -0
  10. package/dist/README.md +2 -2
  11. package/dist/SKILLS.md +1 -1
  12. package/dist/cli/index.js +131 -100
  13. package/dist/core/features-config.d.ts +4 -0
  14. package/dist/core/features-config.js +10 -0
  15. package/dist/inference/inference-accumulator.d.ts +2 -0
  16. package/dist/inference/inference-accumulator.js +74 -2
  17. package/dist/inference/inference-cycle.js +1 -0
  18. package/dist/inference/inference-proposal-generator.js +65 -7
  19. package/dist/inference/inference-run-gate.d.ts +8 -0
  20. package/dist/inference/inference-run-gate.js +10 -0
  21. package/dist/inference/session-capture.d.ts +27 -0
  22. package/dist/inference/session-capture.js +277 -1
  23. package/dist/integrations/cursor/hooks/pre-compact.js +89 -1
  24. package/dist/integrations/hooks/plates.cjs +165 -0
  25. package/dist/integrations/hooks/station-hook-runtime.cjs +29 -11
  26. package/dist/integrations/hooks/station-memory-ingest.mjs +109 -42
  27. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  28. package/dist/mcps/researcher-confidence.d.ts +5 -1
  29. package/dist/mcps/researcher-confidence.js +37 -0
  30. package/dist/mcps/researcher.server.js +2 -2
  31. package/dist/memory-routing/plates.d.ts +16 -0
  32. package/dist/memory-routing/plates.js +71 -0
  33. package/dist/memory-routing/types.d.ts +2 -0
  34. package/dist/memory-routing/types.js +2 -1
  35. package/dist/processors/implementations/inference-improvement-processor.d.ts +5 -0
  36. package/dist/processors/implementations/inference-improvement-processor.js +101 -2
  37. package/dist/processors/implementations/storytelling-trigger-processor.js +1 -11
  38. package/dist/processors/processor-manager.d.ts +1 -1
  39. package/dist/processors/processor-manager.js +52 -20
  40. package/llms.txt +1 -1
  41. package/package.json +1 -1
  42. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  43. package/scripts/node/consumer-install-smoke.mjs +3 -3
  44. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  45. package/src/mcps/researcher-confidence.ts +56 -3
  46. package/src/mcps/researcher.server.ts +4 -5
  47. package/vendor/@0xray/repertoire/README.md +3 -1
  48. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +23 -3
  49. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
  50. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +119 -16
  51. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
  52. package/vendor/@0xray/repertoire/dist/types.d.ts +1 -1
  53. package/vendor/@0xray/repertoire/dist/types.d.ts.map +1 -1
  54. package/vendor/@0xray/repertoire/package.json +1 -1
  55. package/xray/features.json +11 -1
  56. package/xray/features.schema.json +17 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Runtime plate reader. One implementation.
3
+ * src/memory-routing/plates.ts is the typed API and delegates here so
4
+ * Station (CJS) and the Cursor preCompact hook (plain node) cannot drift.
5
+ */
6
+ const { existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");
7
+ const { dirname, join } = require("path");
8
+
9
+ const PLATE_IDS = Object.freeze([
10
+ "routing",
11
+ "governance",
12
+ "boot",
13
+ "orchestration",
14
+ "processor",
15
+ "reporting",
16
+ "memory-recall",
17
+ ]);
18
+
19
+ /** Id token, then extra phrases. Equal top scores recall nothing. */
20
+ const CUES = {
21
+ routing: [
22
+ [/\brouting\b/i, 5],
23
+ [/thindispatch/i, 4],
24
+ [/task-skill-router|taskskillrouter/i, 4],
25
+ [/scoreandroute/i, 3],
26
+ [/\broute(?:s|d)?\b/i, 3],
27
+ ],
28
+ governance: [
29
+ [/\bgovernance\b/i, 5],
30
+ [/rule-enforcer|ruleenforcer/i, 4],
31
+ [/dynamo solar/i, 3],
32
+ ],
33
+ boot: [
34
+ [/\bboot\b/i, 5],
35
+ [/boot-orchestrator|bootorchestrator/i, 4],
36
+ [/executebootsequence/i, 4],
37
+ ],
38
+ orchestration: [
39
+ [/\borchestrat\w*/i, 5],
40
+ [/aside-?context/i, 4],
41
+ [/spawnaside|spawn aside/i, 3],
42
+ [/analyze-complexity/i, 2],
43
+ [/govern-and-apply/i, 2],
44
+ ],
45
+ processor: [
46
+ [/\bprocessors?\b/i, 5],
47
+ [/pre[\s-]+processors?/i, 6],
48
+ [/post[\s-]+processors?/i, 4],
49
+ [/executepreprocessors|execute-pre-processors/i, 6],
50
+ [/processor-?manager/i, 4],
51
+ ],
52
+ reporting: [
53
+ [/\breporting\b/i, 5],
54
+ [/framework-reporting|framework report/i, 4],
55
+ [/activity report/i, 4],
56
+ [/\breports?\b/i, 2],
57
+ ],
58
+ "memory-recall": [
59
+ [/\bmemory-recall\b/i, 5],
60
+ [/memory\s+recall/i, 5],
61
+ [/recall(?:s|ed|ing)?\s+(?:a\s+|one\s+|the\s+)?plate/i, 5],
62
+ [/stamp(?:ed|ing)?\s+plates?/i, 4],
63
+ ],
64
+ };
65
+
66
+ function platesDir() {
67
+ let dir = __dirname;
68
+ for (let depth = 0; depth < 8; depth += 1) {
69
+ const candidate = join(dir, "docs-site", "docs", "plates");
70
+ if (existsSync(join(candidate, "index.md"))) return candidate;
71
+ const parent = dirname(dir);
72
+ if (parent === dir) break;
73
+ dir = parent;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function assertPlateId(id) {
79
+ if (!PLATE_IDS.includes(id)) {
80
+ throw new Error(`unknown plate: ${id}`);
81
+ }
82
+ }
83
+
84
+ function plateSourcePath(id) {
85
+ assertPlateId(id);
86
+ const dir = platesDir();
87
+ if (!dir) throw new Error("plate docs missing");
88
+ const sourcePath = join(dir, `${id}.md`);
89
+ if (!existsSync(sourcePath)) throw new Error(`plate file missing: ${id}`);
90
+ return sourcePath;
91
+ }
92
+
93
+ function stripFrontmatter(markdown) {
94
+ return String(markdown).replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
95
+ }
96
+
97
+ function loadPlate(id) {
98
+ const sourcePath = plateSourcePath(id);
99
+ const raw = readFileSync(sourcePath, "utf8");
100
+ return {
101
+ id,
102
+ body: stripFrontmatter(raw),
103
+ sourcePath,
104
+ };
105
+ }
106
+
107
+ function scorePlate(id, text) {
108
+ const cues = CUES[id] || [];
109
+ let score = 0;
110
+ for (const [pattern, weight] of cues) {
111
+ if (pattern.test(text)) score += weight;
112
+ }
113
+ return score;
114
+ }
115
+
116
+ function recallPlate(intent) {
117
+ const text = String(intent || "").trim();
118
+ if (!text || !platesDir()) return null;
119
+ let bestId = null;
120
+ let best = 0;
121
+ let second = 0;
122
+ for (const id of PLATE_IDS) {
123
+ const score = scorePlate(id, text);
124
+ if (score > best) {
125
+ second = best;
126
+ best = score;
127
+ bestId = id;
128
+ } else if (score > second) {
129
+ second = score;
130
+ }
131
+ }
132
+ if (!bestId || best === 0 || best === second) return null;
133
+ return loadPlate(bestId);
134
+ }
135
+
136
+ function wornPlatePath(projectRoot, id) {
137
+ assertPlateId(id);
138
+ return join(projectRoot, ".xray", "state", "plates", `${id}.md`);
139
+ }
140
+
141
+ function stampPlateIfMissing(projectRoot, id) {
142
+ const path = wornPlatePath(projectRoot, id);
143
+ if (existsSync(path)) {
144
+ return { id, path, written: false };
145
+ }
146
+ const sourcePath = plateSourcePath(id);
147
+ mkdirSync(dirname(path), { recursive: true });
148
+ writeFileSync(path, readFileSync(sourcePath, "utf8"));
149
+ return { id, path, written: true };
150
+ }
151
+
152
+ function plateStockLine(intent) {
153
+ const plate = recallPlate(intent);
154
+ if (!plate) return null;
155
+ return `Plate: ${plate.id} — .xray/state/plates/${plate.id}.md`;
156
+ }
157
+
158
+ module.exports = {
159
+ PLATE_IDS,
160
+ loadPlate,
161
+ recallPlate,
162
+ stampPlateIfMissing,
163
+ plateStockLine,
164
+ wornPlatePath,
165
+ };
@@ -20,6 +20,7 @@ const {
20
20
  const { join } = require("path");
21
21
 
22
22
  const HOOKS_DIR = __dirname;
23
+ const { plateStockLine } = require("./plates.cjs");
23
24
 
24
25
  const INTENT_MAX = 240;
25
26
 
@@ -673,7 +674,11 @@ function heatLiveMemory(root) {
673
674
  }
674
675
  }
675
676
 
676
- /** Every floor that heats. Dedup per HEAD. Patterns come from git, not a leftover catalog. */
677
+ /**
678
+ * Station note on HEAD move. Dedup per HEAD.
679
+ * Commit subjects are match text for the card, not a graded lesson.
680
+ * Do not mint session-<date>-<short HEAD> under docs/inference.
681
+ */
677
682
  function maybeCaptureSessionOnHeadMove(root) {
678
683
  const cfg = readSessionCaptureConfig(root);
679
684
  if (!cfg.enabled) return null;
@@ -690,34 +695,28 @@ function maybeCaptureSessionOnHeadMove(root) {
690
695
  ? gitCommitSubjects(root, [`${stamp.head}..HEAD`])
691
696
  : gitCommitSubjects(root, ["-n", String(cfg.lookback)]);
692
697
  if (commits.length < cfg.minCommits) return null;
693
- const sessionId = `session-${new Date().toISOString().slice(0, 10)}-${git.head}`;
694
698
  const approaches = commits.map((row) => row.message).filter(Boolean);
695
699
  const span = {
696
700
  from: stamp && stamp.head ? stamp.head : commits[commits.length - 1] ? commits[commits.length - 1].hash : git.head,
697
701
  to: git.head,
698
702
  };
699
703
  const patterns = patternsFromGit(root, commits, span);
700
- const session = {
701
- sessionId,
704
+ const stationNote = {
702
705
  timestamp: new Date().toISOString(),
703
706
  span,
704
- problems: [],
705
707
  approaches,
706
- wrongTurns: [],
707
- solutions: [],
708
708
  patterns,
709
709
  matched_primitives: preferLawHits(patterns.map((row) => row && row.name)),
710
710
  metrics: { commits: commits.length },
711
711
  };
712
712
  const outDir = join(root, "docs", "inference");
713
713
  mkdirSync(outDir, { recursive: true });
714
- const filePath = join(outDir, `session-${sessionId.replace(/^session-/, "")}.json`);
715
- writeFileSync(filePath, `${JSON.stringify(session, null, 2)}\n`);
716
- writeFileSync(join(outDir, "latest-session.json"), `${JSON.stringify(session, null, 2)}\n`);
714
+ const filePath = join(outDir, "latest-session.json");
715
+ writeFileSync(filePath, `${JSON.stringify(stationNote, null, 2)}\n`);
717
716
  mkdirSync(join(root, ".xray", "state"), { recursive: true });
718
717
  writeFileSync(
719
718
  sessionCaptureStampPath(root),
720
- `${JSON.stringify({ head: git.head, sessionId, path: filePath, updatedAt: session.timestamp }, null, 2)}\n`,
719
+ `${JSON.stringify({ head: git.head, path: filePath, updatedAt: stationNote.timestamp }, null, 2)}\n`,
721
720
  );
722
721
  return filePath;
723
722
  }
@@ -1045,6 +1044,20 @@ const STOCK_STATION_PREFIXES = [
1045
1044
  "git:",
1046
1045
  "repertoire:",
1047
1046
  "working:",
1047
+ "plate:",
1048
+ ];
1049
+
1050
+ /** Stock design map. Exact lines so a later heat does not preserve a second copy. */
1051
+ const DESIGN_MAP_LINES = [
1052
+ "Subsystem map:",
1053
+ "+----------------------+------------------------------------------+",
1054
+ "| Subsystem | After compact |",
1055
+ "+----------------------+------------------------------------------+",
1056
+ "| Inference | proposals, reflection, Repertoire |",
1057
+ "| External Governance | Dynamo vote, Codex |",
1058
+ "| Autonomous Engine | thinDispatch, AsideContext |",
1059
+ "| Lessons | hot lines on the signal; this card |",
1060
+ "+----------------------+------------------------------------------+",
1048
1061
  ];
1049
1062
 
1050
1063
  const STOCK_STATION_FOOTERS = [
@@ -1099,6 +1112,7 @@ function isStockStationLine(line) {
1099
1112
  const trimmed = String(line || "").trim();
1100
1113
  if (!trimmed) return true;
1101
1114
  if (/^#\s+station\s*$/i.test(trimmed)) return true;
1115
+ if (DESIGN_MAP_LINES.includes(trimmed)) return true;
1102
1116
  const lower = trimmed.toLowerCase();
1103
1117
  if (STOCK_STATION_FOOTERS.includes(lower)) return true;
1104
1118
  return STOCK_STATION_PREFIXES.some((prefix) => lower.startsWith(prefix));
@@ -1171,6 +1185,10 @@ function formatStationMarkdown(fields) {
1171
1185
  if (fields.workingLine) {
1172
1186
  lines.push(fields.workingLine);
1173
1187
  }
1188
+ const plateLine = plateStockLine(fields.intent);
1189
+ if (plateLine) lines.push(plateLine);
1190
+ lines.push("");
1191
+ lines.push(...DESIGN_MAP_LINES);
1174
1192
  lines.push("");
1175
1193
  lines.push("Continue this card. Compaction and host change are the same cut. Do not cold-start.");
1176
1194
  lines.push("Grok does not inject this file — Read it. OpenCode injects. Do not thicken the Grok exo.");
@@ -3,10 +3,24 @@
3
3
  * Station organ write — compact ingest + wake grow.
4
4
  * Grow uses the worn RepertoireService. Groover field stays off.
5
5
  */
6
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
7
- import { join } from 'node:path';
6
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { join, resolve } from 'node:path';
8
8
  import { pathToFileURL } from 'node:url';
9
9
 
10
+ /** Tail cap for diary files the kernel list does not open. Heat only. */
11
+ const UNREAD_DIARY_CAP = 80_000;
12
+
13
+ /**
14
+ * Suit logs with a writer and no brain reader.
15
+ * Graded sessions and the five kernel-diary files stay on their existing readers.
16
+ * latest-session.json stays a station note, not a corpus session.
17
+ */
18
+ const UNREAD_DIARY_FILES = [
19
+ ['logs', 'framework', 'activity-report.json'],
20
+ ['logs', 'framework', 'plugin-tool-events.log'],
21
+ ['.xray', 'inference', 'postprocessor-light-latest.json'],
22
+ ];
23
+
10
24
  const root = process.argv[2];
11
25
  const mode = process.argv[3] || 'station';
12
26
 
@@ -95,6 +109,50 @@ function readCleanupDiary(projectRoot) {
95
109
  return { text: parts.join('\n'), sources };
96
110
  }
97
111
 
112
+ function pushMatching(dir, pattern, into) {
113
+ if (!existsSync(dir)) return;
114
+ let names = [];
115
+ try {
116
+ names = readdirSync(dir);
117
+ } catch {
118
+ return;
119
+ }
120
+ const matched = names.filter((name) => pattern.test(name)).sort().reverse();
121
+ for (const name of matched) into.push(join(dir, name));
122
+ }
123
+
124
+ /**
125
+ * Diary, activity, and workflow files the kernel candidate list does not open.
126
+ * Text is heat for last_seen. It is not a lesson and not a graded session.
127
+ */
128
+ export function collectUnreadSuitDiary(projectRoot) {
129
+ const files = UNREAD_DIARY_FILES.map((parts) => join(projectRoot, ...parts));
130
+ pushMatching(join(projectRoot, '.xray', 'inference'), /^workflow-\d+\.json$/, files);
131
+ pushMatching(join(projectRoot, 'logs', 'monitoring'), /^memory-monitor-.+\.log$/, files);
132
+ pushMatching(join(projectRoot, '.opencode', 'logs'), /^xray-plugin-.+\.log$/, files);
133
+ const sources = [];
134
+ const chunks = [];
135
+ let used = 0;
136
+ for (const file of files) {
137
+ const resolved = resolve(file);
138
+ if (!existsSync(resolved) || sources.includes(resolved)) continue;
139
+ let raw = '';
140
+ try {
141
+ raw = readFileSync(resolved, 'utf8');
142
+ } catch {
143
+ continue;
144
+ }
145
+ if (!raw.trim()) continue;
146
+ const remain = UNREAD_DIARY_CAP - used;
147
+ if (remain <= 0) break;
148
+ const slice = raw.length > remain ? raw.slice(-remain) : raw;
149
+ chunks.push(slice);
150
+ sources.push(resolved);
151
+ used += slice.length;
152
+ }
153
+ return { text: chunks.join('\n'), sources };
154
+ }
155
+
98
156
  async function growDest(projectRoot) {
99
157
  const routing = readRouting(projectRoot);
100
158
  if (isExplicitOptOut(routing)) {
@@ -123,9 +181,10 @@ async function growDest(projectRoot) {
123
181
  ? pathsMod.collectKernelDiaryText(projectRoot)
124
182
  : { text: '', sources: [] };
125
183
  const cleanup = readCleanupDiary(projectRoot);
184
+ const unread = collectUnreadSuitDiary(projectRoot);
126
185
  const diary = service.heatKernelDiary({
127
- text: [kernel.text, cleanup.text].filter(Boolean).join('\n'),
128
- sources: [...(kernel.sources || []), ...cleanup.sources],
186
+ text: [kernel.text, cleanup.text, unread.text].filter(Boolean).join('\n'),
187
+ sources: [...(kernel.sources || []), ...cleanup.sources, ...unread.sources],
129
188
  });
130
189
  const after = service.signalsManager.load().signals.length;
131
190
  const heatedNames = Array.isArray(diary.heated) ? diary.heated : [];
@@ -147,49 +206,57 @@ async function growDest(projectRoot) {
147
206
  process.stdout.write(`${JSON.stringify({ skipped: 'no-organ' })}\n`);
148
207
  }
149
208
 
150
- if (!root) {
151
- process.exit(0);
209
+ function invokedAsScript() {
210
+ const entry = process.argv[1];
211
+ if (!entry) return false;
212
+ return import.meta.url === pathToFileURL(entry).href;
152
213
  }
153
214
 
154
- if (mode === '--grow') {
155
- await growDest(root);
156
- process.exit(0);
157
- }
215
+ if (invokedAsScript()) {
216
+ if (!root) {
217
+ process.exit(0);
218
+ }
158
219
 
159
- const sessionId = mode;
160
- const hookEvent = process.argv[4] || 'post_compact';
161
- let signals = [];
162
- try {
163
- signals = JSON.parse(process.argv[5] || '[]');
164
- } catch {
165
- signals = [];
166
- }
167
- if (!Array.isArray(signals) || signals.length === 0) {
168
- process.exit(0);
169
- }
220
+ if (mode === '--grow') {
221
+ await growDest(root);
222
+ process.exit(0);
223
+ }
170
224
 
171
- let routing = { enabled: false, provider: 'null' };
172
- const featuresPath = join(root, '.xray', 'features.json');
173
- if (existsSync(featuresPath)) {
225
+ const sessionId = mode;
226
+ const hookEvent = process.argv[4] || 'post_compact';
227
+ let signals = [];
174
228
  try {
175
- routing = JSON.parse(readFileSync(featuresPath, 'utf8')).memory_routing || routing;
229
+ signals = JSON.parse(process.argv[5] || '[]');
176
230
  } catch {
177
- /* leftover */
231
+ signals = [];
232
+ }
233
+ if (!Array.isArray(signals) || signals.length === 0) {
234
+ process.exit(0);
178
235
  }
179
- }
180
236
 
181
- const { loadMemoryRoutingProvider } = await import('../../memory-routing/index.js');
182
- const provider = await loadMemoryRoutingProvider(routing, root);
183
- if (!provider || typeof provider.ingestFeedback !== 'function') process.exit(0);
184
-
185
- provider.ingestFeedback({
186
- timestamp: new Date().toISOString(),
187
- sessionId,
188
- taskId: `station-${hookEvent}`,
189
- assignedAgent: 'station',
190
- memorySignals: signals,
191
- complexity: 0,
192
- success: true,
193
- durationMs: 0,
194
- });
195
- process.exit(0);
237
+ let routing = { enabled: false, provider: 'null' };
238
+ const featuresPath = join(root, '.xray', 'features.json');
239
+ if (existsSync(featuresPath)) {
240
+ try {
241
+ routing = JSON.parse(readFileSync(featuresPath, 'utf8')).memory_routing || routing;
242
+ } catch {
243
+ /* leftover */
244
+ }
245
+ }
246
+
247
+ const { loadMemoryRoutingProvider } = await import('../../memory-routing/index.js');
248
+ const provider = await loadMemoryRoutingProvider(routing, root);
249
+ if (!provider || typeof provider.ingestFeedback !== 'function') process.exit(0);
250
+
251
+ provider.ingestFeedback({
252
+ timestamp: new Date().toISOString(),
253
+ sessionId,
254
+ taskId: `station-${hookEvent}`,
255
+ assignedAgent: 'station',
256
+ memorySignals: signals,
257
+ complexity: 0,
258
+ success: true,
259
+ durationMs: 0,
260
+ });
261
+ process.exit(0);
262
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.23",
3
+ "version": "4.0.24",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -1,10 +1,14 @@
1
- import type { MemoryRoutingContext, MemoryTaskConfidence } from '../memory-routing/types.js';
1
+ import { type MemoryRoutingContext, type MemorySignalLessons, type MemoryTaskConfidence } from '../memory-routing/types.js';
2
2
  export interface ResearcherMemoryContext {
3
3
  providerId: string;
4
4
  confidence: MemoryTaskConfidence;
5
5
  matchedSignals: string[];
6
6
  recommendedAgent: string | null;
7
7
  triggeredBy: 'trap-language' | 'high-confidence-primitives';
8
+ /** Graded lines for signals this task already matched. */
9
+ lessons?: MemorySignalLessons[];
10
+ /** Proposal text used to recall at most one pipeline plate. */
11
+ proposalText?: string;
8
12
  }
9
13
  export declare function shouldQueryRepertoireConfidence(description: string, proposalType?: string): boolean;
10
14
  export declare function hasHighConfidencePrimitiveMatch(routingContext: MemoryRoutingContext, minConfidence?: number): boolean;
@@ -1,5 +1,7 @@
1
1
  import { frameworkLogger } from '../core/framework-logger.js';
2
2
  import { getMemoryRoutingProvider } from '../memory-routing/index.js';
3
+ import { recallPlate } from '../memory-routing/plates.js';
4
+ import { LESSON_TEXT_CAP, } from '../memory-routing/types.js';
3
5
  const TRAP_LANGUAGE = /TYPE:\s*ontological-trap|ontological[- ]trap/i;
4
6
  const DEFAULT_MIN_CONFIDENCE_GATE = 0.55;
5
7
  export function shouldQueryRepertoireConfidence(description, proposalType = '') {
@@ -40,12 +42,15 @@ export async function resolveResearcherMemoryContext(input) {
40
42
  const matchedSignals = confidence.matchedSignals.length > 0
41
43
  ? confidence.matchedSignals
42
44
  : routingContext.matchedSignals;
45
+ const lessons = lessonsForMatchedSignals(routingContext.lessons, matchedSignals);
43
46
  const context = {
44
47
  providerId: provider.id,
45
48
  confidence,
46
49
  matchedSignals,
47
50
  recommendedAgent: confidence.recommendedAgent,
48
51
  triggeredBy: trapLanguageDetected ? 'trap-language' : 'high-confidence-primitives',
52
+ proposalText: taskText,
53
+ ...(lessons.length > 0 ? { lessons } : {}),
49
54
  };
50
55
  if (confidence.highConfidenceTrapPresent) {
51
56
  frameworkLogger.log('mcps/researcher', 'repertoire-trap-detected', 'info', {
@@ -73,6 +78,10 @@ export function buildMemoryRoutingEvidence(context) {
73
78
  if (recommendedAgent) {
74
79
  lines.push(`Recommended agent for trap handling: ${recommendedAgent}`);
75
80
  }
81
+ for (const line of lessonSpeechLines(context.lessons, matchedSignals)) {
82
+ lines.push(`Lesson: ${line}`);
83
+ }
84
+ appendRecalledPlate(lines, context.proposalText);
76
85
  return lines;
77
86
  }
78
87
  export function formatMemoryRoutingBlock(context) {
@@ -88,5 +97,33 @@ export function formatMemoryRoutingBlock(context) {
88
97
  if (recommendedAgent) {
89
98
  lines.push(` recommendedAgent: ${recommendedAgent}`);
90
99
  }
100
+ for (const line of lessonSpeechLines(context.lessons, matchedSignals)) {
101
+ lines.push(` lesson: ${line}`);
102
+ }
103
+ appendRecalledPlate(lines, context.proposalText);
91
104
  return lines.join('\n');
92
105
  }
106
+ function appendRecalledPlate(lines, proposalText) {
107
+ const plate = recallPlate(proposalText);
108
+ if (!plate)
109
+ return;
110
+ lines.push(`Plate: ${plate.id}`);
111
+ lines.push(plate.body);
112
+ }
113
+ function lessonsForMatchedSignals(lessons, matchedSignals) {
114
+ const matched = new Set(matchedSignals);
115
+ return (lessons ?? []).filter((lesson) => matched.has(lesson.name));
116
+ }
117
+ /** Stored lesson text for signals this task matched. Empty text is omitted. */
118
+ function lessonSpeechLines(lessons, matchedSignals) {
119
+ const speech = [];
120
+ for (const lesson of lessonsForMatchedSignals(lessons, matchedSignals)) {
121
+ for (const line of lesson.lines) {
122
+ const text = line.text.trim().slice(0, LESSON_TEXT_CAP);
123
+ if (text.length === 0)
124
+ continue;
125
+ speech.push(`${lesson.name}: ${text}`);
126
+ }
127
+ }
128
+ return speech;
129
+ }
@@ -388,7 +388,7 @@ class XrayLibrarianServer extends XrayKnowledgeSkillBase {
388
388
  proposalType,
389
389
  });
390
390
  const enrichedEvidence = [...(evidence || [])];
391
- if (memoryContext?.confidence.highConfidenceTrapPresent) {
391
+ if (memoryContext) {
392
392
  enrichedEvidence.push(...buildMemoryRoutingEvidence(memoryContext));
393
393
  }
394
394
  const vote = (await tryLLMGovernance("researcher", proposalTitle || "", proposalDescription || "", enrichedEvidence, proposalType || "")) ??
@@ -398,7 +398,7 @@ class XrayLibrarianServer extends XrayKnowledgeSkillBase {
398
398
  highConfidenceTrapPresent: memoryContext?.confidence.highConfidenceTrapPresent,
399
399
  recommendedAgent: memoryContext?.recommendedAgent,
400
400
  });
401
- const memoryRoutingBlock = memoryContext?.confidence.highConfidenceTrapPresent
401
+ const memoryRoutingBlock = memoryContext
402
402
  ? `\n${formatMemoryRoutingBlock(memoryContext)}`
403
403
  : "";
404
404
  return {
@@ -0,0 +1,16 @@
1
+ export declare const PLATE_IDS: readonly ["routing", "governance", "boot", "orchestration", "processor", "reporting", "memory-recall"];
2
+ export type PlateId = (typeof PLATE_IDS)[number];
3
+ export interface PlateStamp {
4
+ id: PlateId;
5
+ body: string;
6
+ sourcePath: string;
7
+ }
8
+ export interface PlateStampResult {
9
+ id: PlateId;
10
+ path: string;
11
+ written: boolean;
12
+ }
13
+ export declare function loadPlate(id: PlateId): PlateStamp;
14
+ export declare function recallPlate(intent: string | null | undefined): PlateStamp | null;
15
+ export declare function stampPlateIfMissing(projectRoot: string, id: PlateId): PlateStampResult;
16
+ export declare function plateStockLine(intent: string | null | undefined): string | null;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Typed plate API. The runtime reader is plates.cjs so Station and the
3
+ * Cursor preCompact hook share one implementation.
4
+ */
5
+ import { createRequire } from 'node:module';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ export const PLATE_IDS = [
9
+ 'routing',
10
+ 'governance',
11
+ 'boot',
12
+ 'orchestration',
13
+ 'processor',
14
+ 'reporting',
15
+ 'memory-recall',
16
+ ];
17
+ function isPlateId(value) {
18
+ return PLATE_IDS.includes(value);
19
+ }
20
+ function isRuntime(value) {
21
+ if (!value || typeof value !== 'object')
22
+ return false;
23
+ const row = value;
24
+ return (typeof row.loadPlate === 'function'
25
+ && typeof row.recallPlate === 'function'
26
+ && typeof row.stampPlateIfMissing === 'function'
27
+ && typeof row.plateStockLine === 'function'
28
+ && Array.isArray(row.PLATE_IDS));
29
+ }
30
+ function loadRuntime() {
31
+ const require = createRequire(import.meta.url);
32
+ const runtimePath = join(dirname(fileURLToPath(import.meta.url)), '../integrations/hooks/plates.cjs');
33
+ const loaded = require(runtimePath);
34
+ if (!isRuntime(loaded)) {
35
+ throw new Error('plate runtime missing');
36
+ }
37
+ return loaded;
38
+ }
39
+ const runtime = loadRuntime();
40
+ function asStamp(value) {
41
+ if (!value)
42
+ return null;
43
+ if (!isPlateId(value.id) || typeof value.body !== 'string') {
44
+ throw new Error('plate runtime returned an unknown stamp');
45
+ }
46
+ return value;
47
+ }
48
+ export function loadPlate(id) {
49
+ const stamp = asStamp(runtime.loadPlate(id));
50
+ if (!stamp)
51
+ throw new Error(`plate missing: ${id}`);
52
+ return stamp;
53
+ }
54
+ export function recallPlate(intent) {
55
+ return asStamp(runtime.recallPlate(intent));
56
+ }
57
+ export function stampPlateIfMissing(projectRoot, id) {
58
+ const result = runtime.stampPlateIfMissing(projectRoot, id);
59
+ if (!isPlateId(result.id) || typeof result.path !== 'string' || typeof result.written !== 'boolean') {
60
+ throw new Error('plate runtime returned an unknown stamp result');
61
+ }
62
+ return result;
63
+ }
64
+ export function plateStockLine(intent) {
65
+ const line = runtime.plateStockLine(intent);
66
+ if (line === null || line === undefined)
67
+ return null;
68
+ if (typeof line !== 'string')
69
+ throw new Error('plate stock line was not text');
70
+ return line;
71
+ }
@@ -33,6 +33,8 @@ export interface MemoryTaskConfidence {
33
33
  complexityBoost: number;
34
34
  recommendedAgent: string | null;
35
35
  }
36
+ /** Characters kept on one graded lesson line. lessonTrace stores with this cap. */
37
+ export declare const LESSON_TEXT_CAP = 400;
36
38
  export interface MemoryLessonLine {
37
39
  taskId: string;
38
40
  decision: 'success' | 'failure';
@@ -4,4 +4,5 @@
4
4
  * Repertoire is one implementation. Any provider exposing createMemoryRoutingProvider()
5
5
  * can be loaded via features.json memory_routing.module_path.
6
6
  */
7
- export {};
7
+ /** Characters kept on one graded lesson line. lessonTrace stores with this cap. */
8
+ export const LESSON_TEXT_CAP = 400;
@@ -33,4 +33,9 @@ export declare class InferenceImprovementProcessor extends PostProcessor {
33
33
  private generateCodeReviewerPrompt;
34
34
  private generateEnforcerPrompt;
35
35
  private triggerAgentWorkflow;
36
+ /**
37
+ * The operation is finished once its prompt result text is on disk.
38
+ * One session per operation id. Empty speech writes nothing.
39
+ */
40
+ private captureFinishedOperationSpeech;
36
41
  }