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.
- package/.grok-plugin/plugin.json +1 -1
- package/.opencode/skills/orchestrator/SKILL.md +7 -7
- package/.xray/features.json +11 -1
- package/.xray/features.schema.json +17 -0
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +1 -1
- package/README.md +2 -2
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +1 -1
- package/dist/CHANGELOG.md +44 -1
- package/dist/README.md +2 -2
- package/dist/SKILLS.md +1 -1
- package/dist/cli/index.js +131 -100
- package/dist/core/features-config.d.ts +4 -0
- package/dist/core/features-config.js +10 -0
- package/dist/inference/inference-accumulator.d.ts +2 -0
- package/dist/inference/inference-accumulator.js +110 -6
- package/dist/inference/inference-cycle.d.ts +12 -0
- package/dist/inference/inference-cycle.js +159 -16
- package/dist/inference/inference-proposal-generator.js +206 -5
- package/dist/inference/inference-run-gate.d.ts +8 -0
- package/dist/inference/inference-run-gate.js +10 -0
- package/dist/inference/session-capture.d.ts +31 -0
- package/dist/inference/session-capture.js +277 -1
- package/dist/integrations/cursor/hooks/pre-compact.js +89 -1
- package/dist/integrations/governance/index.d.ts +5 -0
- package/dist/integrations/governance/index.js +22 -2
- package/dist/integrations/governance/types.d.ts +11 -0
- package/dist/integrations/governance/types.js +16 -0
- package/dist/integrations/hooks/plates.cjs +165 -0
- package/dist/integrations/hooks/station-hook-runtime.cjs +29 -11
- package/dist/integrations/hooks/station-memory-ingest.mjs +109 -42
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/mcps/governance.server.d.ts +2 -0
- package/dist/mcps/governance.server.js +12 -1
- package/dist/mcps/researcher-confidence.d.ts +5 -1
- package/dist/mcps/researcher-confidence.js +37 -0
- package/dist/mcps/researcher.server.js +2 -2
- package/dist/memory-routing/index.d.ts +1 -1
- package/dist/memory-routing/index.js +1 -1
- package/dist/memory-routing/lesson-pickup.d.ts +2 -0
- package/dist/memory-routing/lesson-pickup.js +57 -0
- package/dist/memory-routing/plates.d.ts +16 -0
- package/dist/memory-routing/plates.js +71 -0
- package/dist/memory-routing/provider-loader.d.ts +5 -0
- package/dist/memory-routing/provider-loader.js +46 -0
- package/dist/memory-routing/provider-registry.d.ts +7 -2
- package/dist/memory-routing/provider-registry.js +26 -6
- package/dist/memory-routing/record-lesson.d.ts +16 -0
- package/dist/memory-routing/record-lesson.js +26 -0
- package/dist/memory-routing/types.d.ts +17 -0
- package/dist/memory-routing/types.js +2 -1
- package/dist/nucleus/thin-dispatch.d.ts +2 -0
- package/dist/nucleus/thin-dispatch.js +3 -2
- package/dist/processors/implementations/inference-improvement-processor.d.ts +5 -0
- package/dist/processors/implementations/inference-improvement-processor.js +101 -2
- package/dist/processors/implementations/storytelling-trigger-processor.js +1 -11
- package/dist/processors/processor-manager.d.ts +1 -1
- package/dist/processors/processor-manager.js +52 -20
- package/dist/skills/orchestrator/SKILL.md +7 -7
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
- package/scripts/foundry/reconcile-version.mjs +23 -1
- package/scripts/foundry/release.mjs +1 -1
- package/scripts/foundry/version-manager.mjs +53 -0
- package/scripts/node/consumer-install-smoke.mjs +3 -3
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/mcps/governance.server.ts +12 -1
- package/src/mcps/researcher-confidence.ts +56 -3
- package/src/mcps/researcher.server.ts +4 -5
- package/src/skills/orchestrator/SKILL.md +7 -7
- package/vendor/@0xray/repertoire/README.md +3 -1
- package/vendor/@0xray/repertoire/data/stack-overlay.json +4 -4
- package/vendor/@0xray/repertoire/data/subject-overlay.json +3 -3
- package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts +4 -0
- package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/RepertoireService.js +4 -4
- package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js +5 -0
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -0
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/paths.d.ts +3 -3
- package/vendor/@0xray/repertoire/dist/paths.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/paths.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +11 -0
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +2 -0
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +74 -4
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +431 -13
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/types.d.ts +21 -0
- package/vendor/@0xray/repertoire/dist/types.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/package.json +1 -1
- package/xray/features.json +11 -1
- 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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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"))
|