0xray 4.0.23 → 4.0.25
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/.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 +43 -0
- package/dist/README.md +2 -2
- package/dist/SKILLS.md +1 -1
- package/dist/cli/commands/validate.js +3 -0
- 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 +74 -2
- package/dist/inference/inference-cycle.js +1 -0
- package/dist/inference/inference-proposal-generator.js +65 -7
- 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 +27 -0
- package/dist/inference/session-capture.js +277 -1
- package/dist/integrations/cursor/hooks/pre-compact.js +89 -1
- 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/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/plates.d.ts +16 -0
- package/dist/memory-routing/plates.js +71 -0
- package/dist/memory-routing/types.d.ts +2 -0
- package/dist/memory-routing/types.js +2 -1
- 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/docs-site/docs/plates/boot.md +52 -0
- package/docs-site/docs/plates/governance.md +50 -0
- package/docs-site/docs/plates/index.md +16 -0
- package/docs-site/docs/plates/memory-recall.md +49 -0
- package/docs-site/docs/plates/orchestration.md +55 -0
- package/docs-site/docs/plates/processor.md +46 -0
- package/docs-site/docs/plates/reporting.md +42 -0
- package/docs-site/docs/plates/routing.md +49 -0
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/scripts/foundry/assert-packed-dist-cli.mjs +3 -0
- package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
- package/scripts/node/consumer-install-smoke.mjs +7 -4
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/mcps/researcher-confidence.ts +56 -3
- package/src/mcps/researcher.server.ts +4 -5
- package/vendor/@0xray/repertoire/README.md +3 -1
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +23 -3
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +119 -16
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/types.d.ts +1 -1
- 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,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import { captureReflectionInference } from "./session-capture.js";
|
|
3
4
|
export function shouldTriggerCycle(inferenceDir, lastCycleFile) {
|
|
4
|
-
const sessions =
|
|
5
|
+
const sessions = loadCorpusSessions(inferenceDir);
|
|
5
6
|
if (sessions.length < 1) {
|
|
6
7
|
return { trigger: false, reason: `no session files collected` };
|
|
7
8
|
}
|
|
@@ -20,7 +21,7 @@ export function shouldTriggerCycle(inferenceDir, lastCycleFile) {
|
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
export function accumulateCorpus(inferenceDir) {
|
|
23
|
-
const sessions =
|
|
24
|
+
const sessions = loadCorpusSessions(inferenceDir);
|
|
24
25
|
const totalCommits = sessions.reduce((sum, s) => sum + s.metrics.commits, 0);
|
|
25
26
|
const patternMap = new Map();
|
|
26
27
|
for (const session of sessions) {
|
|
@@ -89,6 +90,77 @@ export function accumulateCorpus(inferenceDir) {
|
|
|
89
90
|
collectedAt: new Date().toISOString(),
|
|
90
91
|
};
|
|
91
92
|
}
|
|
93
|
+
function loadCorpusSessions(inferenceDir) {
|
|
94
|
+
return [...loadSessionInferences(inferenceDir), ...loadReflectionInferences(inferenceDir)];
|
|
95
|
+
}
|
|
96
|
+
/** Reflection files are graded in memory. They are not copied into session-*.json. */
|
|
97
|
+
export function loadReflectionInferences(inferenceDir) {
|
|
98
|
+
const projectRoot = path.resolve(inferenceDir, "..", "..");
|
|
99
|
+
const signalNames = readDestSignalNames(projectRoot);
|
|
100
|
+
const root = path.join(projectRoot, "docs", "reflections");
|
|
101
|
+
const sessions = [];
|
|
102
|
+
for (const file of listMarkdown(root)) {
|
|
103
|
+
const relativePath = path.relative(root, file);
|
|
104
|
+
if (/template/i.test(path.basename(file)))
|
|
105
|
+
continue;
|
|
106
|
+
let text = "";
|
|
107
|
+
try {
|
|
108
|
+
text = fs.readFileSync(file, "utf-8");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const modifiedAt = fs.statSync(file).mtime;
|
|
114
|
+
const session = captureReflectionInference({ relativePath, text, modifiedAt, signalNames });
|
|
115
|
+
if (session)
|
|
116
|
+
sessions.push(session);
|
|
117
|
+
}
|
|
118
|
+
return sessions;
|
|
119
|
+
}
|
|
120
|
+
function listMarkdown(dir) {
|
|
121
|
+
if (!fs.existsSync(dir))
|
|
122
|
+
return [];
|
|
123
|
+
const found = [];
|
|
124
|
+
for (const name of fs.readdirSync(dir)) {
|
|
125
|
+
const full = path.join(dir, name);
|
|
126
|
+
let stat;
|
|
127
|
+
try {
|
|
128
|
+
stat = fs.statSync(full);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (stat.isDirectory()) {
|
|
134
|
+
found.push(...listMarkdown(full));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (name.endsWith(".md"))
|
|
138
|
+
found.push(full);
|
|
139
|
+
}
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
function readDestSignalNames(projectRoot) {
|
|
143
|
+
const dest = path.join(projectRoot, ".xray", "state", "repertoire", "curated_signals.json");
|
|
144
|
+
if (!fs.existsSync(dest))
|
|
145
|
+
return [];
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(fs.readFileSync(dest, "utf-8"));
|
|
148
|
+
if (!Array.isArray(parsed.signals))
|
|
149
|
+
return [];
|
|
150
|
+
const names = [];
|
|
151
|
+
for (const signal of parsed.signals) {
|
|
152
|
+
if (!signal || typeof signal !== "object")
|
|
153
|
+
continue;
|
|
154
|
+
const name = signal.name;
|
|
155
|
+
if (typeof name === "string" && name.trim().length > 0)
|
|
156
|
+
names.push(name.trim());
|
|
157
|
+
}
|
|
158
|
+
return names;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
92
164
|
export function loadSessionInferences(dir) {
|
|
93
165
|
if (!fs.existsSync(dir))
|
|
94
166
|
return [];
|
|
@@ -119,6 +119,7 @@ export class InferenceCycle {
|
|
|
119
119
|
p.status = "rejected";
|
|
120
120
|
for (const p of proposals)
|
|
121
121
|
InferenceCycle.governedProposalIds.add(p.id);
|
|
122
|
+
this.recordGovernedLessons(cycleId, proposals, votes);
|
|
122
123
|
this.setPhase("complete");
|
|
123
124
|
this.saveCycleState(cycleId);
|
|
124
125
|
const result = this.buildResult(cycleId, true, "external proposals", startTime, undefined, proposals, votes);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LESSON_TEXT_CAP } from "../memory-routing/types.js";
|
|
1
2
|
function primitivesForSessions(corpus, sessionIds) {
|
|
2
3
|
const wanted = new Set(sessionIds);
|
|
3
4
|
const names = [];
|
|
@@ -35,12 +36,19 @@ function resonanceForNamedSignal(corpus, sessionIds) {
|
|
|
35
36
|
}
|
|
36
37
|
return confidence;
|
|
37
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
|
+
}
|
|
38
47
|
function sessionsForWrongTurn(corpus, turn) {
|
|
39
48
|
return corpus.sessions
|
|
40
49
|
.filter((session) => session.wrongTurns.includes(turn))
|
|
41
50
|
.map((session) => session.sessionId);
|
|
42
51
|
}
|
|
43
|
-
const LESSON_TEXT_CAP = 400;
|
|
44
52
|
/** The trace a graded line stores. Wrong turns, then approaches, then solutions. */
|
|
45
53
|
function lessonTrace(corpus, sessionIds) {
|
|
46
54
|
const wanted = new Set(sessionIds);
|
|
@@ -165,13 +173,16 @@ function adjustConfidenceFromHistory(proposals, history) {
|
|
|
165
173
|
}
|
|
166
174
|
export function generateProposals(corpus, history) {
|
|
167
175
|
const proposals = [];
|
|
176
|
+
const proposalSessions = new Map();
|
|
168
177
|
for (const problem of corpus.recurringProblems) {
|
|
169
178
|
const prefix = `problem:${problem.pattern}:`;
|
|
170
179
|
const sessions = freshSessions(history, prefix, problem.sessions);
|
|
171
180
|
if (sessions.length === 0)
|
|
172
181
|
continue;
|
|
182
|
+
const id = `${prefix}${sessions.join(",")}`;
|
|
183
|
+
proposalSessions.set(id, sessions);
|
|
173
184
|
proposals.push({
|
|
174
|
-
id
|
|
185
|
+
id,
|
|
175
186
|
type: classifyProposalType(problem.pattern),
|
|
176
187
|
title: generateTitle(problem),
|
|
177
188
|
description: `Recurring across ${problem.occurrences} sessions: ${problem.pattern}`,
|
|
@@ -197,8 +208,10 @@ export function generateProposals(corpus, history) {
|
|
|
197
208
|
seenPatterns.add(normalized);
|
|
198
209
|
continue;
|
|
199
210
|
}
|
|
211
|
+
const id = `${prefix}${sessions.join(",")}`;
|
|
212
|
+
proposalSessions.set(id, sessions);
|
|
200
213
|
proposals.push({
|
|
201
|
-
id
|
|
214
|
+
id,
|
|
202
215
|
type: classifyProposalType(problem),
|
|
203
216
|
title: `Investigate: ${problem.substring(0, 80)}`,
|
|
204
217
|
description: `Observed in session ${session}: ${problem}`,
|
|
@@ -219,8 +232,10 @@ export function generateProposals(corpus, history) {
|
|
|
219
232
|
if (sessions.length === 0)
|
|
220
233
|
continue;
|
|
221
234
|
const type = patternToProposalType(pattern);
|
|
235
|
+
const id = `${prefix}${sessions.join(",")}`;
|
|
236
|
+
proposalSessions.set(id, sessions);
|
|
222
237
|
proposals.push({
|
|
223
|
-
id
|
|
238
|
+
id,
|
|
224
239
|
type,
|
|
225
240
|
title: `Codify ${pattern.name} pattern`,
|
|
226
241
|
description: `${pattern.name} detected across ${pattern.occurrences} sessions (avg confidence: ${Math.round(pattern.avgConfidence * 100)}%). ${pattern.description}`,
|
|
@@ -239,8 +254,10 @@ export function generateProposals(corpus, history) {
|
|
|
239
254
|
const sessions = freshSessions(history, prefix, sessionsForWrongTurn(corpus, wt));
|
|
240
255
|
if (sessions.length === 0)
|
|
241
256
|
continue;
|
|
257
|
+
const id = `${prefix}${sessions.join(",")}`;
|
|
258
|
+
proposalSessions.set(id, sessions);
|
|
242
259
|
proposals.push({
|
|
243
|
-
id
|
|
260
|
+
id,
|
|
244
261
|
type: "guard",
|
|
245
262
|
title: `Guard against: ${summary}`,
|
|
246
263
|
description: `Recurring wrong turn detected: ${wt}. Add a guard or validation to prevent this pattern.`,
|
|
@@ -253,12 +270,25 @@ export function generateProposals(corpus, history) {
|
|
|
253
270
|
});
|
|
254
271
|
}
|
|
255
272
|
const sorted = proposals.sort((a, b) => b.confidence - a.confidence).slice(0, 3);
|
|
256
|
-
const
|
|
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
|
+
}
|
|
257
285
|
const bySignal = new Map();
|
|
258
286
|
for (const session of corpus.sessions) {
|
|
259
287
|
const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
|
|
260
288
|
for (const name of listed) {
|
|
261
|
-
if (name.length === 0
|
|
289
|
+
if (name.length === 0)
|
|
290
|
+
continue;
|
|
291
|
+
if (sessionsAlreadyNamed.get(name)?.has(session.sessionId))
|
|
262
292
|
continue;
|
|
263
293
|
const sessions = bySignal.get(name) ?? [];
|
|
264
294
|
if (!sessions.includes(session.sessionId))
|
|
@@ -266,6 +296,34 @@ export function generateProposals(corpus, history) {
|
|
|
266
296
|
bySignal.set(name, sessions);
|
|
267
297
|
}
|
|
268
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
|
+
}
|
|
269
327
|
for (const [name, sessionIds] of bySignal) {
|
|
270
328
|
const sessions = freshSessions(history, `named:${name}:`, sessionIds);
|
|
271
329
|
if (sessions.length === 0)
|
|
@@ -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
|
+
}
|
|
@@ -17,6 +17,8 @@ export interface SessionInference {
|
|
|
17
17
|
matched_primitives?: string[];
|
|
18
18
|
/** Same list after load, so callers can read either field. */
|
|
19
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;
|
|
20
22
|
}
|
|
21
23
|
export interface ReasoningLink {
|
|
22
24
|
from: string;
|
|
@@ -32,5 +34,30 @@ export interface SessionMetrics {
|
|
|
32
34
|
filesDeleted: number;
|
|
33
35
|
uniqueDirs: number;
|
|
34
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;
|
|
35
39
|
export declare function captureSessionInference(fromRef: string, toRef: string): SessionInference | null;
|
|
36
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"))
|