0xray 4.0.22 → 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 (103) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/orchestrator/SKILL.md +7 -7
  3. package/.xray/features.json +11 -1
  4. package/.xray/features.schema.json +17 -0
  5. package/AGENTS-consumer.md +1 -1
  6. package/AGENTS.md +1 -1
  7. package/README.md +2 -2
  8. package/SKILLS.md +1 -1
  9. package/dist/AGENTS.md +1 -1
  10. package/dist/CHANGELOG.md +44 -1
  11. package/dist/README.md +2 -2
  12. package/dist/SKILLS.md +1 -1
  13. package/dist/cli/index.js +131 -100
  14. package/dist/core/features-config.d.ts +4 -0
  15. package/dist/core/features-config.js +10 -0
  16. package/dist/inference/inference-accumulator.d.ts +2 -0
  17. package/dist/inference/inference-accumulator.js +110 -6
  18. package/dist/inference/inference-cycle.d.ts +12 -0
  19. package/dist/inference/inference-cycle.js +159 -16
  20. package/dist/inference/inference-proposal-generator.js +206 -5
  21. package/dist/inference/inference-run-gate.d.ts +8 -0
  22. package/dist/inference/inference-run-gate.js +10 -0
  23. package/dist/inference/session-capture.d.ts +31 -0
  24. package/dist/inference/session-capture.js +277 -1
  25. package/dist/integrations/cursor/hooks/pre-compact.js +89 -1
  26. package/dist/integrations/governance/index.d.ts +5 -0
  27. package/dist/integrations/governance/index.js +22 -2
  28. package/dist/integrations/governance/types.d.ts +11 -0
  29. package/dist/integrations/governance/types.js +16 -0
  30. package/dist/integrations/hooks/plates.cjs +165 -0
  31. package/dist/integrations/hooks/station-hook-runtime.cjs +29 -11
  32. package/dist/integrations/hooks/station-memory-ingest.mjs +109 -42
  33. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  34. package/dist/mcps/governance.server.d.ts +2 -0
  35. package/dist/mcps/governance.server.js +12 -1
  36. package/dist/mcps/researcher-confidence.d.ts +5 -1
  37. package/dist/mcps/researcher-confidence.js +37 -0
  38. package/dist/mcps/researcher.server.js +2 -2
  39. package/dist/memory-routing/index.d.ts +1 -1
  40. package/dist/memory-routing/index.js +1 -1
  41. package/dist/memory-routing/lesson-pickup.d.ts +2 -0
  42. package/dist/memory-routing/lesson-pickup.js +57 -0
  43. package/dist/memory-routing/plates.d.ts +16 -0
  44. package/dist/memory-routing/plates.js +71 -0
  45. package/dist/memory-routing/provider-loader.d.ts +5 -0
  46. package/dist/memory-routing/provider-loader.js +46 -0
  47. package/dist/memory-routing/provider-registry.d.ts +7 -2
  48. package/dist/memory-routing/provider-registry.js +26 -6
  49. package/dist/memory-routing/record-lesson.d.ts +16 -0
  50. package/dist/memory-routing/record-lesson.js +26 -0
  51. package/dist/memory-routing/types.d.ts +17 -0
  52. package/dist/memory-routing/types.js +2 -1
  53. package/dist/nucleus/thin-dispatch.d.ts +2 -0
  54. package/dist/nucleus/thin-dispatch.js +3 -2
  55. package/dist/processors/implementations/inference-improvement-processor.d.ts +5 -0
  56. package/dist/processors/implementations/inference-improvement-processor.js +101 -2
  57. package/dist/processors/implementations/storytelling-trigger-processor.js +1 -11
  58. package/dist/processors/processor-manager.d.ts +1 -1
  59. package/dist/processors/processor-manager.js +52 -20
  60. package/dist/skills/orchestrator/SKILL.md +7 -7
  61. package/llms.txt +1 -1
  62. package/package.json +1 -1
  63. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  64. package/scripts/foundry/reconcile-version.mjs +23 -1
  65. package/scripts/foundry/release.mjs +1 -1
  66. package/scripts/foundry/version-manager.mjs +53 -0
  67. package/scripts/node/consumer-install-smoke.mjs +3 -3
  68. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  69. package/src/mcps/governance.server.ts +12 -1
  70. package/src/mcps/researcher-confidence.ts +56 -3
  71. package/src/mcps/researcher.server.ts +4 -5
  72. package/src/skills/orchestrator/SKILL.md +7 -7
  73. package/vendor/@0xray/repertoire/README.md +3 -1
  74. package/vendor/@0xray/repertoire/data/stack-overlay.json +4 -4
  75. package/vendor/@0xray/repertoire/data/subject-overlay.json +3 -3
  76. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts +4 -0
  77. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +1 -1
  78. package/vendor/@0xray/repertoire/dist/RepertoireService.js +4 -4
  79. package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +1 -1
  80. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +1 -1
  81. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +1 -1
  82. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +1 -1
  83. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js +5 -0
  84. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +1 -1
  85. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +1 -1
  86. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -0
  87. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +1 -1
  88. package/vendor/@0xray/repertoire/dist/paths.d.ts +3 -3
  89. package/vendor/@0xray/repertoire/dist/paths.d.ts.map +1 -1
  90. package/vendor/@0xray/repertoire/dist/paths.js.map +1 -1
  91. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +11 -0
  92. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +1 -1
  93. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +2 -0
  94. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +1 -1
  95. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +74 -4
  96. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
  97. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +431 -13
  98. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
  99. package/vendor/@0xray/repertoire/dist/types.d.ts +21 -0
  100. package/vendor/@0xray/repertoire/dist/types.d.ts.map +1 -1
  101. package/vendor/@0xray/repertoire/package.json +1 -1
  102. package/xray/features.json +11 -1
  103. package/xray/features.schema.json +17 -0
@@ -1,3 +1,95 @@
1
+ import { LESSON_TEXT_CAP } from "../memory-routing/types.js";
2
+ function primitivesForSessions(corpus, sessionIds) {
3
+ const wanted = new Set(sessionIds);
4
+ const names = [];
5
+ for (const session of corpus.sessions) {
6
+ if (!wanted.has(session.sessionId))
7
+ continue;
8
+ const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
9
+ for (const name of listed)
10
+ names.push(name);
11
+ }
12
+ return [...new Set(names.filter((name) => name.length > 0))];
13
+ }
14
+ function signalsForPattern(corpus, pattern) {
15
+ const names = primitivesForSessions(corpus, pattern.sessions);
16
+ if (pattern.name.length > 0)
17
+ names.push(pattern.name);
18
+ return [...new Set(names)];
19
+ }
20
+ /** Solid band starts at 0.82. Landed work with no wrong turn clears it. */
21
+ const LANDED_RESONANCE = 0.85;
22
+ const WRONG_TURN_RESONANCE = 0.4;
23
+ function resonanceForNamedSignal(corpus, sessionIds) {
24
+ const sessions = corpus.sessions.filter((session) => sessionIds.includes(session.sessionId));
25
+ if (sessions.some((session) => (session.wrongTurns?.length ?? 0) > 0))
26
+ return WRONG_TURN_RESONANCE;
27
+ if (sessions.some((session) => (session.approaches?.length ?? 0) > 0 || (session.solutions?.length ?? 0) > 0)) {
28
+ return LANDED_RESONANCE;
29
+ }
30
+ let confidence = 0.7;
31
+ for (const session of sessions) {
32
+ for (const pattern of session.patterns) {
33
+ if (pattern.confidence > confidence)
34
+ confidence = pattern.confidence;
35
+ }
36
+ }
37
+ return confidence;
38
+ }
39
+ function speechSlug(text) {
40
+ const words = text
41
+ .toLowerCase()
42
+ .split(/[^a-z0-9]+/)
43
+ .filter((word) => word.length >= 3)
44
+ .slice(0, 6);
45
+ return words.join("-").slice(0, 48).replace(/-+$/g, "");
46
+ }
47
+ function sessionsForWrongTurn(corpus, turn) {
48
+ return corpus.sessions
49
+ .filter((session) => session.wrongTurns.includes(turn))
50
+ .map((session) => session.sessionId);
51
+ }
52
+ /** The trace a graded line stores. Wrong turns, then approaches, then solutions. */
53
+ function lessonTrace(corpus, sessionIds) {
54
+ const wanted = new Set(sessionIds);
55
+ const lines = [];
56
+ for (const session of corpus.sessions) {
57
+ if (!wanted.has(session.sessionId))
58
+ continue;
59
+ for (const turn of session.wrongTurns ?? []) {
60
+ if (turn.trim())
61
+ lines.push(turn.trim());
62
+ }
63
+ for (const line of [...(session.approaches ?? []), ...(session.solutions ?? [])]) {
64
+ if (line.trim())
65
+ lines.push(line.trim());
66
+ }
67
+ }
68
+ return [...new Set(lines)].join("; ").slice(0, LESSON_TEXT_CAP);
69
+ }
70
+ function sortedSessionIds(sessionIds) {
71
+ return [...new Set(sessionIds.filter((sessionId) => sessionId.length > 0))].sort();
72
+ }
73
+ /** Sessions already included in a prior proposal of this exact prefix. */
74
+ function coveredSessions(history, prefix) {
75
+ const seen = new Set();
76
+ for (const cycle of history ?? []) {
77
+ for (const proposal of cycle.proposals) {
78
+ if (!proposal.id.startsWith(prefix))
79
+ continue;
80
+ for (const sessionId of proposal.id.slice(prefix.length).split(",")) {
81
+ if (sessionId.length > 0)
82
+ seen.add(sessionId);
83
+ }
84
+ }
85
+ }
86
+ return seen;
87
+ }
88
+ /** Sessions that have not already been graded under this prefix. */
89
+ function freshSessions(history, prefix, sessionIds) {
90
+ const covered = coveredSessions(history, prefix);
91
+ return sortedSessionIds(sessionIds).filter((sessionId) => !covered.has(sessionId));
92
+ }
1
93
  function classifyProposalType(problemPattern) {
2
94
  const lower = problemPattern.toLowerCase();
3
95
  if (lower.includes("bug") || lower.includes("fix") || lower.includes("stability"))
@@ -63,6 +155,8 @@ function adjustConfidenceFromHistory(proposals, history) {
63
155
  }
64
156
  }
65
157
  for (const proposal of proposals) {
158
+ if (proposal.id.startsWith("named:"))
159
+ continue;
66
160
  const approved = approvedTypes.get(proposal.type) ?? 0;
67
161
  const rejected = rejectedTypes.get(proposal.type) ?? 0;
68
162
  const total = approved + rejected;
@@ -79,16 +173,25 @@ function adjustConfidenceFromHistory(proposals, history) {
79
173
  }
80
174
  export function generateProposals(corpus, history) {
81
175
  const proposals = [];
176
+ const proposalSessions = new Map();
82
177
  for (const problem of corpus.recurringProblems) {
178
+ const prefix = `problem:${problem.pattern}:`;
179
+ const sessions = freshSessions(history, prefix, problem.sessions);
180
+ if (sessions.length === 0)
181
+ continue;
182
+ const id = `${prefix}${sessions.join(",")}`;
183
+ proposalSessions.set(id, sessions);
83
184
  proposals.push({
84
- id: `prop-${Date.now()}-${proposals.length}`,
185
+ id,
85
186
  type: classifyProposalType(problem.pattern),
86
187
  title: generateTitle(problem),
87
188
  description: `Recurring across ${problem.occurrences} sessions: ${problem.pattern}`,
88
- evidence: problem.sessions.map((s) => `Seen in session ${s}`),
189
+ evidence: sessions.map((s) => `Seen in session ${s}`),
89
190
  confidence: Math.min(0.95, 0.5 + problem.occurrences * 0.15),
90
191
  source: "recurring_problem",
91
192
  status: "pending",
193
+ namedSignals: primitivesForSessions(corpus, sessions),
194
+ lesson: lessonTrace(corpus, sessions),
92
195
  });
93
196
  }
94
197
  const seenPatterns = new Set(corpus.recurringProblems.map((p) => p.pattern));
@@ -99,8 +202,16 @@ export function generateProposals(corpus, history) {
99
202
  continue;
100
203
  if (proposals.length >= 5)
101
204
  break;
205
+ const prefix = `problem:${normalized}:`;
206
+ const sessions = freshSessions(history, prefix, [session]);
207
+ if (sessions.length === 0) {
208
+ seenPatterns.add(normalized);
209
+ continue;
210
+ }
211
+ const id = `${prefix}${sessions.join(",")}`;
212
+ proposalSessions.set(id, sessions);
102
213
  proposals.push({
103
- id: `prop-${Date.now()}-${proposals.length}`,
214
+ id,
104
215
  type: classifyProposalType(problem),
105
216
  title: `Investigate: ${problem.substring(0, 80)}`,
106
217
  description: `Observed in session ${session}: ${problem}`,
@@ -108,15 +219,23 @@ export function generateProposals(corpus, history) {
108
219
  confidence: 0.4,
109
220
  source: "recurring_problem",
110
221
  status: "pending",
222
+ namedSignals: primitivesForSessions(corpus, sessions),
223
+ lesson: lessonTrace(corpus, sessions),
111
224
  });
112
225
  seenPatterns.add(normalized);
113
226
  }
114
227
  for (const pattern of corpus.recurringPatterns) {
115
228
  if (pattern.occurrences < 2)
116
229
  continue;
230
+ const prefix = `pattern:${pattern.name}:`;
231
+ const sessions = freshSessions(history, prefix, pattern.sessions);
232
+ if (sessions.length === 0)
233
+ continue;
117
234
  const type = patternToProposalType(pattern);
235
+ const id = `${prefix}${sessions.join(",")}`;
236
+ proposalSessions.set(id, sessions);
118
237
  proposals.push({
119
- id: `prop-${Date.now()}-${proposals.length}`,
238
+ id,
120
239
  type,
121
240
  title: `Codify ${pattern.name} pattern`,
122
241
  description: `${pattern.name} detected across ${pattern.occurrences} sessions (avg confidence: ${Math.round(pattern.avgConfidence * 100)}%). ${pattern.description}`,
@@ -124,13 +243,21 @@ export function generateProposals(corpus, history) {
124
243
  confidence: pattern.avgConfidence,
125
244
  source: "recurring_pattern",
126
245
  status: "pending",
246
+ namedSignals: signalsForPattern(corpus, { ...pattern, sessions }),
247
+ lesson: lessonTrace(corpus, sessions),
127
248
  });
128
249
  }
129
250
  const wrongTurns = corpus.allWrongTurns.slice(0, 2);
130
251
  for (const wt of wrongTurns) {
131
252
  const summary = wt.length > 50 ? `${wt.substring(0, 47)}...` : wt;
253
+ const prefix = `wrong:${wt}:`;
254
+ const sessions = freshSessions(history, prefix, sessionsForWrongTurn(corpus, wt));
255
+ if (sessions.length === 0)
256
+ continue;
257
+ const id = `${prefix}${sessions.join(",")}`;
258
+ proposalSessions.set(id, sessions);
132
259
  proposals.push({
133
- id: `prop-${Date.now()}-${proposals.length}`,
260
+ id,
134
261
  type: "guard",
135
262
  title: `Guard against: ${summary}`,
136
263
  description: `Recurring wrong turn detected: ${wt}. Add a guard or validation to prevent this pattern.`,
@@ -138,9 +265,83 @@ export function generateProposals(corpus, history) {
138
265
  confidence: 0.7,
139
266
  source: "wrong_turn",
140
267
  status: "pending",
268
+ namedSignals: primitivesForSessions(corpus, sessions),
269
+ lesson: lessonTrace(corpus, sessions),
141
270
  });
142
271
  }
143
272
  const sorted = proposals.sort((a, b) => b.confidence - a.confidence).slice(0, 3);
273
+ const sessionsAlreadyNamed = new Map();
274
+ for (const proposal of sorted) {
275
+ const covered = proposalSessions.get(proposal.id) ?? [];
276
+ for (const name of proposal.namedSignals ?? []) {
277
+ if (name.length === 0)
278
+ continue;
279
+ const set = sessionsAlreadyNamed.get(name) ?? new Set();
280
+ for (const sessionId of covered)
281
+ set.add(sessionId);
282
+ sessionsAlreadyNamed.set(name, set);
283
+ }
284
+ }
285
+ const bySignal = new Map();
286
+ for (const session of corpus.sessions) {
287
+ const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
288
+ for (const name of listed) {
289
+ if (name.length === 0)
290
+ continue;
291
+ if (sessionsAlreadyNamed.get(name)?.has(session.sessionId))
292
+ continue;
293
+ const sessions = bySignal.get(name) ?? [];
294
+ if (!sessions.includes(session.sessionId))
295
+ sessions.push(session.sessionId);
296
+ bySignal.set(name, sessions);
297
+ }
298
+ }
299
+ const minted = new Set();
300
+ for (const session of corpus.sessions) {
301
+ if (minted.size >= 3)
302
+ break;
303
+ const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
304
+ if (listed.some((name) => name.length > 0))
305
+ continue;
306
+ const speech = lessonTrace(corpus, [session.sessionId]).trim();
307
+ const slug = speechSlug(speech);
308
+ if (slug.length === 0 || minted.has(slug))
309
+ continue;
310
+ const sessions = freshSessions(history, `mint:${slug}:`, [session.sessionId]);
311
+ if (sessions.length === 0)
312
+ continue;
313
+ minted.add(slug);
314
+ sorted.push({
315
+ id: `mint:${slug}:${sessions.join(",")}`,
316
+ type: "codify",
317
+ title: `Learn ${slug}`,
318
+ description: speech,
319
+ evidence: [speech],
320
+ confidence: resonanceForNamedSignal(corpus, sessions),
321
+ source: "recurring_pattern",
322
+ status: "pending",
323
+ namedSignals: [slug],
324
+ lesson: speech,
325
+ });
326
+ }
327
+ for (const [name, sessionIds] of bySignal) {
328
+ const sessions = freshSessions(history, `named:${name}:`, sessionIds);
329
+ if (sessions.length === 0)
330
+ continue;
331
+ const id = `named:${name}:${sessions.join(",")}`;
332
+ sorted.push({
333
+ id,
334
+ type: "codify",
335
+ title: `Grade named signal ${name}`,
336
+ description: `Sessions ${sessions.join(", ")} named ${name}.`,
337
+ evidence: sessions.map((sessionId) => `Seen in session ${sessionId}`),
338
+ confidence: resonanceForNamedSignal(corpus, sessions),
339
+ source: "recurring_pattern",
340
+ status: "pending",
341
+ namedSignals: [name],
342
+ lesson: lessonTrace(corpus, sessions),
343
+ });
344
+ }
144
345
  if (history && history.length > 0) {
145
346
  adjustConfidenceFromHistory(sorted, history);
146
347
  sorted.sort((a, b) => b.confidence - a.confidence);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Whether `inference:run` may enter InferenceCycle.
3
+ * A missing key is on: the cycle is how a suit grows memory.
4
+ * Explicit `enabled: false` stays off unless the caller passes force.
5
+ */
6
+ export declare function inferenceRunMayEnter(inference: {
7
+ enabled?: boolean;
8
+ } | undefined, force: boolean): boolean;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Whether `inference:run` may enter InferenceCycle.
3
+ * A missing key is on: the cycle is how a suit grows memory.
4
+ * Explicit `enabled: false` stays off unless the caller passes force.
5
+ */
6
+ export function inferenceRunMayEnter(inference, force) {
7
+ if (force)
8
+ return true;
9
+ return inference?.enabled !== false;
10
+ }
@@ -13,6 +13,12 @@ export interface SessionInference {
13
13
  reasoningChain: ReasoningLink[];
14
14
  patterns: StructuralPattern[];
15
15
  metrics: SessionMetrics;
16
+ /** Signals this session already named. Session JSON stores this list. */
17
+ matched_primitives?: string[];
18
+ /** Same list after load, so callers can read either field. */
19
+ matchedPrimitives?: string[];
20
+ /** Inference operation this session was captured from. A second completion of the same id does not write another file. */
21
+ operationId?: string;
16
22
  }
17
23
  export interface ReasoningLink {
18
24
  from: string;
@@ -28,5 +34,30 @@ export interface SessionMetrics {
28
34
  filesDeleted: number;
29
35
  uniqueDirs: number;
30
36
  }
37
+ /** Date stamp plus the operation id. Callers do not append a HEAD suffix. */
38
+ export declare function mintSessionId(now?: Date, distinct?: string): string;
31
39
  export declare function captureSessionInference(fromRef: string, toRef: string): SessionInference | null;
32
40
  export declare function saveSessionInference(inference: SessionInference, outputDir?: string): string;
41
+ export interface OperationCaptureInput {
42
+ operationId: string;
43
+ promptsDir: string;
44
+ inferenceDir: string;
45
+ signalNames: readonly string[];
46
+ }
47
+ /**
48
+ * One corpus session for a finished inference operation.
49
+ * Speech comes from that run's prompt result text. No speech, no file.
50
+ * Does not grade. The caller runs the cycle before it returns.
51
+ */
52
+ export declare function captureCompletedInferenceOperation(input: OperationCaptureInput): string | null;
53
+ /** Id, or that id with hyphens read as spaces. Same hit `patternsFromGit` stores for the proposal generator. */
54
+ export declare function reflectionSessionId(relativePath: string): string;
55
+ /** Reflection markdown is the speech. No session file is written. */
56
+ export declare function captureReflectionInference(input: {
57
+ relativePath: string;
58
+ text: string;
59
+ modifiedAt: Date;
60
+ signalNames: readonly string[];
61
+ }): SessionInference | null;
62
+ /** The gauge file. One session log per reflection, skipped when that id is already on disk. */
63
+ export declare function saveReflectionSession(session: SessionInference, inferenceDir: string): string | null;
@@ -2,6 +2,16 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { execSync } from "child_process";
4
4
  import { analyzeStructuralPatterns } from "./semantic-patterns.js";
5
+ /** Date stamp plus the operation id. Callers do not append a HEAD suffix. */
6
+ export function mintSessionId(now = new Date(), distinct) {
7
+ const day = now.toISOString().slice(0, 10);
8
+ const suffix = (distinct ?? "")
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, "-")
12
+ .replace(/^-+|-+$/g, "");
13
+ return suffix.length > 0 ? `session-${day}-${suffix}` : `session-${day}`;
14
+ }
5
15
  export function captureSessionInference(fromRef, toRef) {
6
16
  const commits = getCommitData(fromRef, toRef);
7
17
  if (commits.length === 0)
@@ -14,7 +24,7 @@ export function captureSessionInference(fromRef, toRef) {
14
24
  const solutions = extractSolutions(commits, patterns);
15
25
  const reasoningChain = buildReasoningChain(problems, approaches, wrongTurns, solutions);
16
26
  return {
17
- sessionId: `session-${new Date().toISOString().slice(0, 10)}`,
27
+ sessionId: mintSessionId(),
18
28
  timestamp: new Date().toISOString(),
19
29
  span: { from: fromRef, to: toRef },
20
30
  problems,
@@ -282,6 +292,272 @@ function buildReasoningChain(problems, approaches, wrongTurns, solutions) {
282
292
  }
283
293
  return chain;
284
294
  }
295
+ /** Same cap `lessonTrace` applies before a vote can store the line. */
296
+ const OPERATION_LESSON_CAP = 400;
297
+ /**
298
+ * One corpus session for a finished inference operation.
299
+ * Speech comes from that run's prompt result text. No speech, no file.
300
+ * Does not grade. The caller runs the cycle before it returns.
301
+ */
302
+ export function captureCompletedInferenceOperation(input) {
303
+ const operationId = input.operationId.trim();
304
+ if (!operationId)
305
+ return null;
306
+ if (operationAlreadyCaptured(input.inferenceDir, operationId))
307
+ return null;
308
+ const raw = readOperationSpeech(input.promptsDir, operationId);
309
+ const speech = parseOperationSpeech(raw);
310
+ const heard = speech.approaches.length + speech.solutions.length + speech.wrongTurns.length;
311
+ if (!raw.trim() || heard === 0)
312
+ return null;
313
+ const named = storedSignalsNamedBySpeech(raw, input.signalNames);
314
+ const now = new Date().toISOString();
315
+ const session = {
316
+ sessionId: mintSessionId(new Date(), operationId),
317
+ timestamp: now,
318
+ span: { from: operationId, to: operationId },
319
+ problems: [],
320
+ approaches: speech.approaches,
321
+ wrongTurns: speech.wrongTurns,
322
+ solutions: speech.solutions,
323
+ reasoningChain: [],
324
+ patterns: [],
325
+ metrics: {
326
+ commits: 0,
327
+ filesChanged: 0,
328
+ insertions: 0,
329
+ deletions: 0,
330
+ filesAdded: 0,
331
+ filesDeleted: 0,
332
+ uniqueDirs: 0,
333
+ },
334
+ matched_primitives: named,
335
+ matchedPrimitives: named,
336
+ operationId,
337
+ };
338
+ return saveSessionInference(session, input.inferenceDir);
339
+ }
340
+ function operationAlreadyCaptured(inferenceDir, operationId) {
341
+ if (!fs.existsSync(inferenceDir))
342
+ return false;
343
+ for (const name of fs.readdirSync(inferenceDir)) {
344
+ if (!name.startsWith("session-") || !name.endsWith(".json"))
345
+ continue;
346
+ try {
347
+ const parsed = JSON.parse(fs.readFileSync(path.join(inferenceDir, name), "utf8"));
348
+ if (parsed.operationId === operationId)
349
+ return true;
350
+ }
351
+ catch {
352
+ continue;
353
+ }
354
+ }
355
+ return false;
356
+ }
357
+ function readOperationSpeech(promptsDir, operationId) {
358
+ if (!fs.existsSync(promptsDir))
359
+ return "";
360
+ const chunks = [];
361
+ for (const name of fs.readdirSync(promptsDir).sort()) {
362
+ const full = path.join(promptsDir, name);
363
+ let stat;
364
+ try {
365
+ stat = fs.statSync(full);
366
+ }
367
+ catch {
368
+ continue;
369
+ }
370
+ if (!stat.isFile())
371
+ continue;
372
+ let text = "";
373
+ try {
374
+ text = fs.readFileSync(full, "utf8");
375
+ }
376
+ catch {
377
+ continue;
378
+ }
379
+ if (name === `${operationId}.result.md` || name === `${operationId}.result.txt`) {
380
+ chunks.push(text);
381
+ continue;
382
+ }
383
+ if (/\.result\.(md|txt)$/i.test(name) && operationDeclared(text) === operationId) {
384
+ chunks.push(text);
385
+ continue;
386
+ }
387
+ if (name.endsWith(".md") && !name.includes(".result.")) {
388
+ const section = resultSection(text);
389
+ if (section && (operationDeclared(section) === operationId || operationDeclared(text) === operationId)) {
390
+ chunks.push(section);
391
+ }
392
+ }
393
+ }
394
+ return chunks.join("\n").trim();
395
+ }
396
+ function operationDeclared(text) {
397
+ const match = /^operation:\s*(\S+)\s*$/im.exec(text);
398
+ return match?.[1] ?? null;
399
+ }
400
+ function resultSection(text) {
401
+ const parts = text.split(/\n##\s+(?:Result|Speech)\s*\n/i);
402
+ if (parts.length < 2)
403
+ return "";
404
+ return parts.slice(1).join("\n").trim();
405
+ }
406
+ function parseOperationSpeech(raw) {
407
+ const body = raw
408
+ .split("\n")
409
+ .filter((line) => !/^operation:\s*\S+\s*$/i.test(line.trim()))
410
+ .join("\n")
411
+ .trim();
412
+ const buckets = { approaches: [], solutions: [], wrongTurns: [] };
413
+ if (!body)
414
+ return buckets;
415
+ let current = null;
416
+ let labeled = false;
417
+ const sectionLines = [];
418
+ const flush = () => {
419
+ if (!current) {
420
+ sectionLines.length = 0;
421
+ return;
422
+ }
423
+ const text = fewSentences(sectionLines.join(" "));
424
+ sectionLines.length = 0;
425
+ if (text)
426
+ buckets[current].push(text);
427
+ };
428
+ for (const line of body.split("\n")) {
429
+ const trimmed = line.trim();
430
+ if (!trimmed)
431
+ continue;
432
+ const inline = /^([^:]{2,40}):\s+(.+)$/.exec(trimmed);
433
+ if (inline) {
434
+ const bucket = bucketForLabel(inline[1] ?? "");
435
+ if (bucket) {
436
+ flush();
437
+ current = bucket;
438
+ labeled = true;
439
+ sectionLines.push(inline[2] ?? "");
440
+ continue;
441
+ }
442
+ }
443
+ const heading = /^(?:#{1,3}\s*)(.+)$/.exec(trimmed);
444
+ if (heading) {
445
+ const bucket = bucketForLabel(heading[1] ?? "");
446
+ if (bucket) {
447
+ flush();
448
+ current = bucket;
449
+ labeled = true;
450
+ continue;
451
+ }
452
+ }
453
+ if (current)
454
+ sectionLines.push(trimmed);
455
+ }
456
+ flush();
457
+ if (!labeled) {
458
+ const text = fewSentences(body);
459
+ if (text)
460
+ buckets.approaches.push(text);
461
+ }
462
+ return buckets;
463
+ }
464
+ function bucketForLabel(label) {
465
+ const lower = label.toLowerCase().replace(/^#+\s*/, "").replace(/:$/, "").trim();
466
+ if (/^(tried|approaches?|what was tried)$/.test(lower))
467
+ return "approaches";
468
+ if (/^(worked|solutions?|what worked)$/.test(lower))
469
+ return "solutions";
470
+ if (/^(failed|wrong turns?|what failed)$/.test(lower))
471
+ return "wrongTurns";
472
+ return null;
473
+ }
474
+ function fewSentences(text) {
475
+ const flat = text.replace(/\s+/g, " ").trim();
476
+ if (!flat)
477
+ return "";
478
+ const sentences = flat.split(/(?<=[.!?])\s+/).filter((part) => part.length > 0).slice(0, 4);
479
+ const joined = sentences.join(" ");
480
+ if (joined.length <= OPERATION_LESSON_CAP)
481
+ return joined;
482
+ return joined.slice(0, OPERATION_LESSON_CAP);
483
+ }
484
+ /** Id, or that id with hyphens read as spaces. Same hit `patternsFromGit` stores for the proposal generator. */
485
+ export function reflectionSessionId(relativePath) {
486
+ const slug = relativePath
487
+ .replace(/\.md$/i, "")
488
+ .trim()
489
+ .toLowerCase()
490
+ .replace(/[^a-z0-9]+/g, "-")
491
+ .replace(/^-+|-+$/g, "");
492
+ return `session-reflection-${slug}`;
493
+ }
494
+ /** Reflection markdown is the speech. No session file is written. */
495
+ export function captureReflectionInference(input) {
496
+ const speech = parseOperationSpeech(input.text);
497
+ const heard = speech.approaches.length + speech.solutions.length + speech.wrongTurns.length;
498
+ if (heard === 0)
499
+ return null;
500
+ const named = storedSignalsNamedBySpeech([...speech.approaches, ...speech.solutions, ...speech.wrongTurns].join(" "), input.signalNames);
501
+ const emptyMetrics = {
502
+ commits: 0,
503
+ filesChanged: 0,
504
+ insertions: 0,
505
+ deletions: 0,
506
+ filesAdded: 0,
507
+ filesDeleted: 0,
508
+ uniqueDirs: 0,
509
+ };
510
+ return {
511
+ sessionId: reflectionSessionId(input.relativePath),
512
+ timestamp: input.modifiedAt.toISOString(),
513
+ span: { from: "reflection", to: input.relativePath },
514
+ problems: [],
515
+ approaches: speech.approaches,
516
+ wrongTurns: speech.wrongTurns,
517
+ solutions: speech.solutions,
518
+ reasoningChain: [],
519
+ patterns: [],
520
+ metrics: emptyMetrics,
521
+ matched_primitives: named,
522
+ matchedPrimitives: named,
523
+ };
524
+ }
525
+ /** The gauge file. One session log per reflection, skipped when that id is already on disk. */
526
+ export function saveReflectionSession(session, inferenceDir) {
527
+ if (reflectionAlreadySaved(inferenceDir, session.sessionId))
528
+ return null;
529
+ return saveSessionInference(session, inferenceDir);
530
+ }
531
+ function reflectionAlreadySaved(inferenceDir, sessionId) {
532
+ if (!fs.existsSync(inferenceDir))
533
+ return false;
534
+ for (const name of fs.readdirSync(inferenceDir)) {
535
+ if (!name.startsWith("session-") || !name.endsWith(".json") || name === "latest-session.json")
536
+ continue;
537
+ try {
538
+ const parsed = JSON.parse(fs.readFileSync(path.join(inferenceDir, name), "utf8"));
539
+ if (parsed.sessionId === sessionId)
540
+ return true;
541
+ }
542
+ catch {
543
+ continue;
544
+ }
545
+ }
546
+ return false;
547
+ }
548
+ function storedSignalsNamedBySpeech(speech, signalNames) {
549
+ const hay = speech.toLowerCase();
550
+ const named = [];
551
+ for (const name of signalNames) {
552
+ const id = name.trim().toLowerCase();
553
+ if (!id)
554
+ continue;
555
+ const spaced = id.replace(/-/g, " ");
556
+ if (hay.includes(id) || (spaced !== id && hay.includes(spaced)))
557
+ named.push(name.trim());
558
+ }
559
+ return [...new Set(named)];
560
+ }
285
561
  function approachKeyword(approach) {
286
562
  const lower = approach.toLowerCase();
287
563
  if (lower.includes("extract"))