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,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,17 +21,18 @@ 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) {
|
|
27
28
|
for (const p of session.patterns) {
|
|
28
29
|
const existing = patternMap.get(p.name);
|
|
30
|
+
const evidence = patternEvidence(p);
|
|
29
31
|
if (existing) {
|
|
30
32
|
existing.count++;
|
|
31
33
|
existing.confidence.push(p.confidence);
|
|
32
34
|
existing.sessions.push(session.sessionId);
|
|
33
|
-
for (const e of
|
|
35
|
+
for (const e of evidence) {
|
|
34
36
|
if (!existing.evidence.includes(e))
|
|
35
37
|
existing.evidence.push(e);
|
|
36
38
|
}
|
|
@@ -40,7 +42,7 @@ export function accumulateCorpus(inferenceDir) {
|
|
|
40
42
|
count: 1,
|
|
41
43
|
confidence: [p.confidence],
|
|
42
44
|
sessions: [session.sessionId],
|
|
43
|
-
evidence: [...
|
|
45
|
+
evidence: [...evidence],
|
|
44
46
|
description: p.description,
|
|
45
47
|
});
|
|
46
48
|
}
|
|
@@ -88,6 +90,77 @@ export function accumulateCorpus(inferenceDir) {
|
|
|
88
90
|
collectedAt: new Date().toISOString(),
|
|
89
91
|
};
|
|
90
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
|
+
}
|
|
91
164
|
export function loadSessionInferences(dir) {
|
|
92
165
|
if (!fs.existsSync(dir))
|
|
93
166
|
return [];
|
|
@@ -95,13 +168,44 @@ export function loadSessionInferences(dir) {
|
|
|
95
168
|
.filter((f) => f.startsWith("session-") && f.endsWith(".json"))
|
|
96
169
|
.map((f) => {
|
|
97
170
|
try {
|
|
98
|
-
|
|
171
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
|
172
|
+
const named = readMatchedPrimitives(parsed);
|
|
173
|
+
parsed.matched_primitives = named;
|
|
174
|
+
parsed.matchedPrimitives = named;
|
|
175
|
+
return parsed;
|
|
99
176
|
}
|
|
100
177
|
catch {
|
|
101
178
|
return null;
|
|
102
179
|
}
|
|
103
180
|
})
|
|
104
|
-
.filter((s) => s
|
|
181
|
+
.filter((s) => isSessionInference(s));
|
|
182
|
+
}
|
|
183
|
+
/** Captured patterns sometimes omit evidence. That is an empty list, not a throw. */
|
|
184
|
+
function readMatchedPrimitives(value) {
|
|
185
|
+
if (!value || typeof value !== "object")
|
|
186
|
+
return [];
|
|
187
|
+
const raw = value.matched_primitives
|
|
188
|
+
?? value.matchedPrimitives;
|
|
189
|
+
if (!Array.isArray(raw))
|
|
190
|
+
return [];
|
|
191
|
+
return [...new Set(raw.filter((item) => typeof item === "string" && item.length > 0))];
|
|
192
|
+
}
|
|
193
|
+
function patternEvidence(pattern) {
|
|
194
|
+
if (!Array.isArray(pattern.evidence))
|
|
195
|
+
return [];
|
|
196
|
+
return pattern.evidence.filter((item) => typeof item === "string");
|
|
197
|
+
}
|
|
198
|
+
/** Notes and partial JSON under docs/inference are not sessions. */
|
|
199
|
+
function isSessionInference(value) {
|
|
200
|
+
if (!value || typeof value !== "object")
|
|
201
|
+
return false;
|
|
202
|
+
const session = value;
|
|
203
|
+
return typeof session.sessionId === "string"
|
|
204
|
+
&& Array.isArray(session.patterns)
|
|
205
|
+
&& Array.isArray(session.problems)
|
|
206
|
+
&& Array.isArray(session.approaches)
|
|
207
|
+
&& Array.isArray(session.wrongTurns)
|
|
208
|
+
&& typeof session.metrics?.commits === "number";
|
|
105
209
|
}
|
|
106
210
|
function loadLastCycleDate(filePath) {
|
|
107
211
|
if (!fs.existsSync(filePath))
|
|
@@ -8,6 +8,10 @@ export interface InferenceProposal {
|
|
|
8
8
|
confidence: number;
|
|
9
9
|
source: "recurring_problem" | "recurring_pattern" | "wrong_turn";
|
|
10
10
|
status: "pending" | "approved" | "rejected" | "applied" | "failed";
|
|
11
|
+
/** Repertoire signals the sessions behind this proposal already named. */
|
|
12
|
+
namedSignals?: string[];
|
|
13
|
+
/** Approaches, solutions, or the wrong turn from those sessions. */
|
|
14
|
+
lesson?: string;
|
|
11
15
|
}
|
|
12
16
|
export interface InferenceCycleResult {
|
|
13
17
|
cycleId: string;
|
|
@@ -55,7 +59,11 @@ export declare class InferenceCycle {
|
|
|
55
59
|
constructor(projectRoot?: string, agentInvoker?: AgentInvoker, options?: InferenceCycleOptions);
|
|
56
60
|
governExternalProposals(proposals: InferenceProposal[]): Promise<InferenceCycleResult>;
|
|
57
61
|
maybeRunCycle(): Promise<InferenceCycleResult>;
|
|
62
|
+
/** Approve or reject grades the signals that proposal's sessions already named. needs-revision does not write. */
|
|
63
|
+
private recordGovernedLessons;
|
|
58
64
|
private governProposals;
|
|
65
|
+
/** Flag off: the local matrix is the pass. Dynamo is not called and the miss is not stored as a reject. */
|
|
66
|
+
private governWithLocalMatrix;
|
|
59
67
|
private isGovernanceMcpPreferred;
|
|
60
68
|
private parseGovernanceMcpResponse;
|
|
61
69
|
private extractTextFromNdjson;
|
|
@@ -64,6 +72,10 @@ export declare class InferenceCycle {
|
|
|
64
72
|
getPhase(): CyclePhase;
|
|
65
73
|
private saveCycleState;
|
|
66
74
|
private appendHistory;
|
|
75
|
+
/** Lesson ids that leave the 50-entry window stay covered. Empty cycles do not erase them. */
|
|
76
|
+
private retainDroppedLessonIds;
|
|
77
|
+
private readRetainedLessonIds;
|
|
78
|
+
private retainedLessonCycle;
|
|
67
79
|
private loadHistory;
|
|
68
80
|
private buildBlockedResult;
|
|
69
81
|
private buildResult;
|
|
@@ -8,8 +8,27 @@ import { mcpClientManager } from "../mcps/mcp-client.js";
|
|
|
8
8
|
import { getConfigDir } from "../core/config-paths.js";
|
|
9
9
|
import { generateProposals } from "./inference-proposal-generator.js";
|
|
10
10
|
import { applyProposals as applyProposalsEx } from "./inference-applier.js";
|
|
11
|
+
import { applyDecisionMatrix } from "../governance/governance-core.js";
|
|
12
|
+
import { recordLesson } from "../memory-routing/record-lesson.js";
|
|
13
|
+
import { writeLessonPickup } from "../memory-routing/lesson-pickup.js";
|
|
11
14
|
const CYCLE_STATE_FILE = "inference-cycle-state.json";
|
|
12
15
|
const CYCLE_HISTORY_FILE = "inference-cycle-history.json";
|
|
16
|
+
const GRADED_LESSON_IDS_FILE = "inference-cycle-graded-ids.json";
|
|
17
|
+
const HISTORY_CAP = 50;
|
|
18
|
+
const LESSON_ID_PREFIXES = ["named:", "pattern:", "problem:", "wrong:"];
|
|
19
|
+
function isLessonProposalId(id) {
|
|
20
|
+
return LESSON_ID_PREFIXES.some((prefix) => id.startsWith(prefix));
|
|
21
|
+
}
|
|
22
|
+
function lessonIdsFromUnknown(parsed) {
|
|
23
|
+
if (!Array.isArray(parsed))
|
|
24
|
+
return [];
|
|
25
|
+
const ids = [];
|
|
26
|
+
for (const item of parsed) {
|
|
27
|
+
if (typeof item === "string" && isLessonProposalId(item))
|
|
28
|
+
ids.push(item);
|
|
29
|
+
}
|
|
30
|
+
return ids;
|
|
31
|
+
}
|
|
13
32
|
function governanceToolText(result) {
|
|
14
33
|
if (typeof result !== "object" || result === null || !("content" in result)) {
|
|
15
34
|
return "";
|
|
@@ -18,6 +37,12 @@ function governanceToolText(result) {
|
|
|
18
37
|
const text = content?.[0]?.text;
|
|
19
38
|
return typeof text === "string" ? text : "";
|
|
20
39
|
}
|
|
40
|
+
function governanceToolIsError(result) {
|
|
41
|
+
if (typeof result !== "object" || result === null || !("isError" in result)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return result.isError === true;
|
|
45
|
+
}
|
|
21
46
|
export class InferenceCycle {
|
|
22
47
|
static instances = new Map();
|
|
23
48
|
static reEntryLock = false;
|
|
@@ -94,6 +119,7 @@ export class InferenceCycle {
|
|
|
94
119
|
p.status = "rejected";
|
|
95
120
|
for (const p of proposals)
|
|
96
121
|
InferenceCycle.governedProposalIds.add(p.id);
|
|
122
|
+
this.recordGovernedLessons(cycleId, proposals, votes);
|
|
97
123
|
this.setPhase("complete");
|
|
98
124
|
this.saveCycleState(cycleId);
|
|
99
125
|
const result = this.buildResult(cycleId, true, "external proposals", startTime, undefined, proposals, votes);
|
|
@@ -169,6 +195,7 @@ export class InferenceCycle {
|
|
|
169
195
|
for (const p of proposals.filter((p) => p.status !== "approved")) {
|
|
170
196
|
p.status = "rejected";
|
|
171
197
|
}
|
|
198
|
+
this.recordGovernedLessons(cycleId, proposals, votes);
|
|
172
199
|
if (approved.length > 0) {
|
|
173
200
|
if (!this.options.skipApply) {
|
|
174
201
|
this.setPhase("applying");
|
|
@@ -215,6 +242,39 @@ export class InferenceCycle {
|
|
|
215
242
|
InferenceCycle.reEntryLock = false;
|
|
216
243
|
}
|
|
217
244
|
}
|
|
245
|
+
/** Approve or reject grades the signals that proposal's sessions already named. needs-revision does not write. */
|
|
246
|
+
recordGovernedLessons(cycleId, proposals, votes) {
|
|
247
|
+
const alreadyGraded = new Set(this.loadHistory().flatMap((cycle) => (cycle.proposals ?? []).map((proposal) => proposal.id)));
|
|
248
|
+
for (const proposal of proposals) {
|
|
249
|
+
if (alreadyGraded.has(proposal.id))
|
|
250
|
+
continue;
|
|
251
|
+
const vote = votes.find((item) => item.proposalId === proposal.id);
|
|
252
|
+
if (!vote)
|
|
253
|
+
continue;
|
|
254
|
+
if (vote.decision !== "approve" && vote.decision !== "reject")
|
|
255
|
+
continue;
|
|
256
|
+
if (vote.details.some((line) => line.includes("governance error")))
|
|
257
|
+
continue;
|
|
258
|
+
const named = proposal.namedSignals ?? [];
|
|
259
|
+
const taught = recordLesson({
|
|
260
|
+
operation: [proposal.title, proposal.description, ...proposal.evidence].join("\n"),
|
|
261
|
+
success: vote.decision === "approve",
|
|
262
|
+
taskId: proposal.id,
|
|
263
|
+
assignedAgent: "inference-cycle",
|
|
264
|
+
sessionId: cycleId,
|
|
265
|
+
signals: named,
|
|
266
|
+
lesson: proposal.lesson ?? "",
|
|
267
|
+
});
|
|
268
|
+
if (taught.length > 0) {
|
|
269
|
+
frameworkLogger.log("inference-cycle", "lesson-recorded", "info", {
|
|
270
|
+
proposalId: proposal.id,
|
|
271
|
+
decision: vote.decision,
|
|
272
|
+
signals: taught,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
writeLessonPickup(this.projectRoot);
|
|
277
|
+
}
|
|
218
278
|
async governProposals(proposals) {
|
|
219
279
|
// Governance MCP is the sole governance path (governance.server.ts + GovernanceService).
|
|
220
280
|
// It calls individual skill MCPs (code-review, security-audit, researcher) internally
|
|
@@ -222,8 +282,9 @@ export class InferenceCycle {
|
|
|
222
282
|
const useGovernanceMcp = process.env.XRAY_FORCE_MCP_GOVERNANCE === 'true' ||
|
|
223
283
|
this.isGovernanceMcpPreferred();
|
|
224
284
|
if (!useGovernanceMcp) {
|
|
225
|
-
|
|
285
|
+
return this.governWithLocalMatrix(proposals);
|
|
226
286
|
}
|
|
287
|
+
const governanceMcpTimeoutMs = 90_000;
|
|
227
288
|
const result = await Promise.race([
|
|
228
289
|
mcpClientManager.callServerTool("governance", "govern_proposals", {
|
|
229
290
|
proposals: proposals.map(p => ({
|
|
@@ -238,9 +299,12 @@ export class InferenceCycle {
|
|
|
238
299
|
context: { source: "inference-cycle" },
|
|
239
300
|
options: { require_external: true },
|
|
240
301
|
}),
|
|
241
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(
|
|
302
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Governance MCP timed out after ${governanceMcpTimeoutMs / 1000}s`)), governanceMcpTimeoutMs)),
|
|
242
303
|
]);
|
|
243
304
|
const text = governanceToolText(result);
|
|
305
|
+
if (governanceToolIsError(result)) {
|
|
306
|
+
throw new Error(text || "Governance MCP returned an error");
|
|
307
|
+
}
|
|
244
308
|
const parsed = this.parseGovernanceMcpResponse(text, proposals);
|
|
245
309
|
frameworkLogger.log("inference-cycle", "governance-mcp-primary-path", "info", {
|
|
246
310
|
proposalCount: proposals.length,
|
|
@@ -248,6 +312,24 @@ export class InferenceCycle {
|
|
|
248
312
|
});
|
|
249
313
|
return parsed.votes;
|
|
250
314
|
}
|
|
315
|
+
/** Flag off: the local matrix is the pass. Dynamo is not called and the miss is not stored as a reject. */
|
|
316
|
+
governWithLocalMatrix(proposals) {
|
|
317
|
+
return proposals.map((proposal) => {
|
|
318
|
+
const resonance = Number.isFinite(proposal.confidence) ? proposal.confidence : 0.8;
|
|
319
|
+
const matrix = applyDecisionMatrix({ resonance });
|
|
320
|
+
const decision = matrix.recommendation === "PASS"
|
|
321
|
+
? "approve"
|
|
322
|
+
: matrix.recommendation === "REJECT"
|
|
323
|
+
? "reject"
|
|
324
|
+
: "needs_revision";
|
|
325
|
+
return {
|
|
326
|
+
proposalId: proposal.id,
|
|
327
|
+
decision,
|
|
328
|
+
confidence: matrix.confidence,
|
|
329
|
+
details: ["local-matrix: inference_governance disabled", ...matrix.reasons],
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
}
|
|
251
333
|
isGovernanceMcpPreferred() {
|
|
252
334
|
try {
|
|
253
335
|
const config = featuresConfigLoader.loadConfig();
|
|
@@ -275,17 +357,12 @@ export class InferenceCycle {
|
|
|
275
357
|
return { votes, overallDecision: data.overallDecision || "needs_revision" };
|
|
276
358
|
}
|
|
277
359
|
catch {
|
|
360
|
+
const preview = text.substring(0, 300);
|
|
278
361
|
frameworkLogger.log('inference-cycle', 'governance-mcp-parse-failed', 'warning', {
|
|
279
|
-
textPreview:
|
|
362
|
+
textPreview: preview,
|
|
280
363
|
proposalCount: proposals.length,
|
|
281
364
|
});
|
|
282
|
-
|
|
283
|
-
proposalId: p.id,
|
|
284
|
-
decision: "abstain",
|
|
285
|
-
confidence: 0.5,
|
|
286
|
-
details: ["governance-mcp: parse-failed"],
|
|
287
|
-
}));
|
|
288
|
-
return { votes, overallDecision: "needs_revision" };
|
|
365
|
+
throw new Error(`governance-mcp: unreadable vote: ${preview || "empty governance response"}`);
|
|
289
366
|
}
|
|
290
367
|
}
|
|
291
368
|
extractTextFromNdjson(output) {
|
|
@@ -357,21 +434,87 @@ export class InferenceCycle {
|
|
|
357
434
|
}
|
|
358
435
|
}
|
|
359
436
|
history.push(result);
|
|
360
|
-
|
|
361
|
-
|
|
437
|
+
const dropped = history.length > HISTORY_CAP ? history.slice(0, history.length - HISTORY_CAP) : [];
|
|
438
|
+
if (history.length > HISTORY_CAP)
|
|
439
|
+
history = history.slice(-HISTORY_CAP);
|
|
440
|
+
this.retainDroppedLessonIds(dropped, history);
|
|
362
441
|
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
363
442
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (
|
|
443
|
+
/** Lesson ids that leave the 50-entry window stay covered. Empty cycles do not erase them. */
|
|
444
|
+
retainDroppedLessonIds(dropped, kept) {
|
|
445
|
+
if (dropped.length === 0)
|
|
446
|
+
return;
|
|
447
|
+
const keptIds = new Set(kept.flatMap((cycle) => (cycle.proposals ?? []).map((proposal) => proposal.id)));
|
|
448
|
+
const retained = new Set(this.readRetainedLessonIds());
|
|
449
|
+
let changed = false;
|
|
450
|
+
for (const cycle of dropped) {
|
|
451
|
+
for (const proposal of cycle.proposals ?? []) {
|
|
452
|
+
if (!isLessonProposalId(proposal.id))
|
|
453
|
+
continue;
|
|
454
|
+
if (keptIds.has(proposal.id) || retained.has(proposal.id))
|
|
455
|
+
continue;
|
|
456
|
+
retained.add(proposal.id);
|
|
457
|
+
changed = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (!changed)
|
|
461
|
+
return;
|
|
462
|
+
if (!fs.existsSync(this.stateDir)) {
|
|
463
|
+
fs.mkdirSync(this.stateDir, { recursive: true });
|
|
464
|
+
}
|
|
465
|
+
fs.writeFileSync(path.join(this.stateDir, GRADED_LESSON_IDS_FILE), JSON.stringify([...retained].sort(), null, 2));
|
|
466
|
+
}
|
|
467
|
+
readRetainedLessonIds() {
|
|
468
|
+
const file = path.join(this.stateDir, GRADED_LESSON_IDS_FILE);
|
|
469
|
+
if (!fs.existsSync(file))
|
|
367
470
|
return [];
|
|
368
471
|
try {
|
|
369
|
-
return JSON.parse(fs.readFileSync(
|
|
472
|
+
return lessonIdsFromUnknown(JSON.parse(fs.readFileSync(file, "utf-8")));
|
|
370
473
|
}
|
|
371
474
|
catch {
|
|
372
475
|
return [];
|
|
373
476
|
}
|
|
374
477
|
}
|
|
478
|
+
retainedLessonCycle(ids) {
|
|
479
|
+
return {
|
|
480
|
+
cycleId: "retained-lessons",
|
|
481
|
+
triggered: false,
|
|
482
|
+
triggerReason: "lesson ids kept past the history cap",
|
|
483
|
+
corpusSummary: { sessions: 0, totalCommits: 0, recurringPatterns: 0, recurringProblems: 0 },
|
|
484
|
+
proposals: ids.map((id) => ({
|
|
485
|
+
id,
|
|
486
|
+
type: "codify",
|
|
487
|
+
title: id,
|
|
488
|
+
description: "",
|
|
489
|
+
evidence: [],
|
|
490
|
+
confidence: 0,
|
|
491
|
+
source: "recurring_pattern",
|
|
492
|
+
status: "pending",
|
|
493
|
+
})),
|
|
494
|
+
votes: [],
|
|
495
|
+
phase: "complete",
|
|
496
|
+
completedAt: "1970-01-01T00:00:00.000Z",
|
|
497
|
+
duration: 0,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
loadHistory() {
|
|
501
|
+
const historyPath = path.join(this.stateDir, CYCLE_HISTORY_FILE);
|
|
502
|
+
let history = [];
|
|
503
|
+
if (fs.existsSync(historyPath)) {
|
|
504
|
+
try {
|
|
505
|
+
const parsed = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
506
|
+
history = Array.isArray(parsed) ? parsed : [];
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
history = [];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const present = new Set(history.flatMap((cycle) => (cycle.proposals ?? []).map((proposal) => proposal.id)));
|
|
513
|
+
const missing = this.readRetainedLessonIds().filter((id) => !present.has(id));
|
|
514
|
+
if (missing.length === 0)
|
|
515
|
+
return history;
|
|
516
|
+
return [this.retainedLessonCycle(missing), ...history];
|
|
517
|
+
}
|
|
375
518
|
buildBlockedResult(cycleId, reason) {
|
|
376
519
|
return {
|
|
377
520
|
cycleId,
|