0xray 4.0.21 → 4.0.23
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/hermes-agent/SKILL.md +1 -1
- package/.opencode/skills/orchestrator/SKILL.md +28 -6
- package/.xray/features.json +1 -1
- package/AGENTS-consumer.md +11 -11
- package/AGENTS.md +8 -8
- package/README.md +27 -27
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +8 -8
- package/dist/CHANGELOG.md +98 -0
- package/dist/README.md +27 -27
- package/dist/SKILLS.md +1 -1
- package/dist/governance/governance-core.d.ts +2 -1
- package/dist/governance/governance-core.js +20 -11
- package/dist/governance/governance-service.js +9 -7
- package/dist/inference/inference-accumulator.js +36 -4
- package/dist/inference/inference-cycle.d.ts +12 -0
- package/dist/inference/inference-cycle.js +180 -21
- package/dist/inference/inference-proposal-generator.js +148 -5
- package/dist/inference/session-capture.d.ts +4 -0
- package/dist/integrations/cursor/README.md +1 -1
- package/dist/integrations/cursor/hooks/cursor-hook-utils.js +19 -0
- package/dist/integrations/cursor/hooks/pre-tool-use.js +2 -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/station-hook-runtime.cjs +90 -30
- package/dist/integrations/hooks/station-hook-runtime.mjs +1 -0
- package/dist/integrations/hooks/station-memory-ingest.mjs +4 -2
- 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/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/provider-loader.d.ts +8 -1
- package/dist/memory-routing/provider-loader.js +49 -1
- 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 +15 -0
- package/dist/nucleus/thin-dispatch.d.ts +2 -0
- package/dist/nucleus/thin-dispatch.js +14 -4
- package/dist/processors/implementations/agents-md-validation-processor.js +1 -1
- package/dist/skills/hermes-agent/SKILL.md +1 -1
- package/dist/skills/orchestrator/SKILL.md +28 -6
- package/package.json +2 -2
- package/scripts/foundry/CHANGELOG.md +2 -0
- package/scripts/foundry/README.md +2 -2
- package/scripts/foundry/cli.mjs +1 -1
- package/scripts/foundry/inspect.mjs +160 -5
- package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -0
- package/scripts/foundry/reconcile-version.mjs +23 -1
- package/scripts/foundry/release.mjs +1 -1
- package/scripts/foundry/validate-release-docs.mjs +24 -20
- package/scripts/foundry/version-manager.mjs +180 -7
- package/scripts/mjs/test-consumer-readiness.mjs +10 -3
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/mcps/governance.server.ts +12 -1
- package/src/skills/hermes-agent/SKILL.md +1 -1
- package/src/skills/orchestrator/SKILL.md +28 -6
- package/vendor/@0xray/repertoire/data/stack-overlay.json +156 -3
- 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 +7 -6
- 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 +10 -11
- 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 +66 -4
- 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 +54 -4
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +375 -20
- 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/xray/agents_template.md +10 -5
- package/xray/features.json +1 -1
- package/dist/mcps/shared/prompt-security-validator.d.ts +0 -44
- package/dist/mcps/shared/prompt-security-validator.js +0 -148
- package/dist/mcps/shared/security-scanner.d.ts +0 -119
- package/dist/mcps/shared/security-scanner.js +0 -429
- package/dist/scripts/activate-kernel-pipeline.d.ts +0 -7
- package/dist/scripts/activate-kernel-pipeline.js +0 -101
- package/src/mcps/shared/prompt-security-validator.ts +0 -199
- package/src/mcps/shared/security-scanner.ts +0 -599
|
@@ -26,11 +26,12 @@ export function accumulateCorpus(inferenceDir) {
|
|
|
26
26
|
for (const session of sessions) {
|
|
27
27
|
for (const p of session.patterns) {
|
|
28
28
|
const existing = patternMap.get(p.name);
|
|
29
|
+
const evidence = patternEvidence(p);
|
|
29
30
|
if (existing) {
|
|
30
31
|
existing.count++;
|
|
31
32
|
existing.confidence.push(p.confidence);
|
|
32
33
|
existing.sessions.push(session.sessionId);
|
|
33
|
-
for (const e of
|
|
34
|
+
for (const e of evidence) {
|
|
34
35
|
if (!existing.evidence.includes(e))
|
|
35
36
|
existing.evidence.push(e);
|
|
36
37
|
}
|
|
@@ -40,7 +41,7 @@ export function accumulateCorpus(inferenceDir) {
|
|
|
40
41
|
count: 1,
|
|
41
42
|
confidence: [p.confidence],
|
|
42
43
|
sessions: [session.sessionId],
|
|
43
|
-
evidence: [...
|
|
44
|
+
evidence: [...evidence],
|
|
44
45
|
description: p.description,
|
|
45
46
|
});
|
|
46
47
|
}
|
|
@@ -95,13 +96,44 @@ export function loadSessionInferences(dir) {
|
|
|
95
96
|
.filter((f) => f.startsWith("session-") && f.endsWith(".json"))
|
|
96
97
|
.map((f) => {
|
|
97
98
|
try {
|
|
98
|
-
|
|
99
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
|
100
|
+
const named = readMatchedPrimitives(parsed);
|
|
101
|
+
parsed.matched_primitives = named;
|
|
102
|
+
parsed.matchedPrimitives = named;
|
|
103
|
+
return parsed;
|
|
99
104
|
}
|
|
100
105
|
catch {
|
|
101
106
|
return null;
|
|
102
107
|
}
|
|
103
108
|
})
|
|
104
|
-
.filter((s) => s
|
|
109
|
+
.filter((s) => isSessionInference(s));
|
|
110
|
+
}
|
|
111
|
+
/** Captured patterns sometimes omit evidence. That is an empty list, not a throw. */
|
|
112
|
+
function readMatchedPrimitives(value) {
|
|
113
|
+
if (!value || typeof value !== "object")
|
|
114
|
+
return [];
|
|
115
|
+
const raw = value.matched_primitives
|
|
116
|
+
?? value.matchedPrimitives;
|
|
117
|
+
if (!Array.isArray(raw))
|
|
118
|
+
return [];
|
|
119
|
+
return [...new Set(raw.filter((item) => typeof item === "string" && item.length > 0))];
|
|
120
|
+
}
|
|
121
|
+
function patternEvidence(pattern) {
|
|
122
|
+
if (!Array.isArray(pattern.evidence))
|
|
123
|
+
return [];
|
|
124
|
+
return pattern.evidence.filter((item) => typeof item === "string");
|
|
125
|
+
}
|
|
126
|
+
/** Notes and partial JSON under docs/inference are not sessions. */
|
|
127
|
+
function isSessionInference(value) {
|
|
128
|
+
if (!value || typeof value !== "object")
|
|
129
|
+
return false;
|
|
130
|
+
const session = value;
|
|
131
|
+
return typeof session.sessionId === "string"
|
|
132
|
+
&& Array.isArray(session.patterns)
|
|
133
|
+
&& Array.isArray(session.problems)
|
|
134
|
+
&& Array.isArray(session.approaches)
|
|
135
|
+
&& Array.isArray(session.wrongTurns)
|
|
136
|
+
&& typeof session.metrics?.commits === "number";
|
|
105
137
|
}
|
|
106
138
|
function loadLastCycleDate(filePath) {
|
|
107
139
|
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,18 +8,57 @@ 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
|
+
}
|
|
32
|
+
function governanceToolText(result) {
|
|
33
|
+
if (typeof result !== "object" || result === null || !("content" in result)) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
const content = result.content;
|
|
37
|
+
const text = content?.[0]?.text;
|
|
38
|
+
return typeof text === "string" ? text : "";
|
|
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
|
+
}
|
|
13
46
|
export class InferenceCycle {
|
|
14
47
|
static instances = new Map();
|
|
15
48
|
static reEntryLock = false;
|
|
16
49
|
static governedProposalIds = new Set();
|
|
17
50
|
static getInstance(projectRoot, options) {
|
|
18
51
|
const root = path.resolve(projectRoot || process.cwd());
|
|
19
|
-
|
|
20
|
-
|
|
52
|
+
const existing = InferenceCycle.instances.get(root);
|
|
53
|
+
if (existing) {
|
|
54
|
+
if (options) {
|
|
55
|
+
existing.options = { ...existing.options, ...options };
|
|
56
|
+
}
|
|
57
|
+
return existing;
|
|
21
58
|
}
|
|
22
|
-
|
|
59
|
+
const created = new InferenceCycle(root, undefined, options);
|
|
60
|
+
InferenceCycle.instances.set(root, created);
|
|
61
|
+
return created;
|
|
23
62
|
}
|
|
24
63
|
static resetInstance() {
|
|
25
64
|
InferenceCycle.instances.clear();
|
|
@@ -121,7 +160,10 @@ export class InferenceCycle {
|
|
|
121
160
|
const proposals = generateProposals(corpus, this.loadHistory());
|
|
122
161
|
if (proposals.length === 0) {
|
|
123
162
|
this.setPhase("complete");
|
|
124
|
-
|
|
163
|
+
this.saveCycleState(cycleId);
|
|
164
|
+
const emptyResult = this.buildResult(cycleId, true, threshold.reason, startTime, corpus, proposals);
|
|
165
|
+
this.appendHistory(emptyResult);
|
|
166
|
+
return emptyResult;
|
|
125
167
|
}
|
|
126
168
|
this.setPhase("governing");
|
|
127
169
|
let votes = [];
|
|
@@ -152,6 +194,7 @@ export class InferenceCycle {
|
|
|
152
194
|
for (const p of proposals.filter((p) => p.status !== "approved")) {
|
|
153
195
|
p.status = "rejected";
|
|
154
196
|
}
|
|
197
|
+
this.recordGovernedLessons(cycleId, proposals, votes);
|
|
155
198
|
if (approved.length > 0) {
|
|
156
199
|
if (!this.options.skipApply) {
|
|
157
200
|
this.setPhase("applying");
|
|
@@ -198,6 +241,39 @@ export class InferenceCycle {
|
|
|
198
241
|
InferenceCycle.reEntryLock = false;
|
|
199
242
|
}
|
|
200
243
|
}
|
|
244
|
+
/** Approve or reject grades the signals that proposal's sessions already named. needs-revision does not write. */
|
|
245
|
+
recordGovernedLessons(cycleId, proposals, votes) {
|
|
246
|
+
const alreadyGraded = new Set(this.loadHistory().flatMap((cycle) => (cycle.proposals ?? []).map((proposal) => proposal.id)));
|
|
247
|
+
for (const proposal of proposals) {
|
|
248
|
+
if (alreadyGraded.has(proposal.id))
|
|
249
|
+
continue;
|
|
250
|
+
const vote = votes.find((item) => item.proposalId === proposal.id);
|
|
251
|
+
if (!vote)
|
|
252
|
+
continue;
|
|
253
|
+
if (vote.decision !== "approve" && vote.decision !== "reject")
|
|
254
|
+
continue;
|
|
255
|
+
if (vote.details.some((line) => line.includes("governance error")))
|
|
256
|
+
continue;
|
|
257
|
+
const named = proposal.namedSignals ?? [];
|
|
258
|
+
const taught = recordLesson({
|
|
259
|
+
operation: [proposal.title, proposal.description, ...proposal.evidence].join("\n"),
|
|
260
|
+
success: vote.decision === "approve",
|
|
261
|
+
taskId: proposal.id,
|
|
262
|
+
assignedAgent: "inference-cycle",
|
|
263
|
+
sessionId: cycleId,
|
|
264
|
+
signals: named,
|
|
265
|
+
lesson: proposal.lesson ?? "",
|
|
266
|
+
});
|
|
267
|
+
if (taught.length > 0) {
|
|
268
|
+
frameworkLogger.log("inference-cycle", "lesson-recorded", "info", {
|
|
269
|
+
proposalId: proposal.id,
|
|
270
|
+
decision: vote.decision,
|
|
271
|
+
signals: taught,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
writeLessonPickup(this.projectRoot);
|
|
276
|
+
}
|
|
201
277
|
async governProposals(proposals) {
|
|
202
278
|
// Governance MCP is the sole governance path (governance.server.ts + GovernanceService).
|
|
203
279
|
// It calls individual skill MCPs (code-review, security-audit, researcher) internally
|
|
@@ -205,8 +281,9 @@ export class InferenceCycle {
|
|
|
205
281
|
const useGovernanceMcp = process.env.XRAY_FORCE_MCP_GOVERNANCE === 'true' ||
|
|
206
282
|
this.isGovernanceMcpPreferred();
|
|
207
283
|
if (!useGovernanceMcp) {
|
|
208
|
-
|
|
284
|
+
return this.governWithLocalMatrix(proposals);
|
|
209
285
|
}
|
|
286
|
+
const governanceMcpTimeoutMs = 90_000;
|
|
210
287
|
const result = await Promise.race([
|
|
211
288
|
mcpClientManager.callServerTool("governance", "govern_proposals", {
|
|
212
289
|
proposals: proposals.map(p => ({
|
|
@@ -221,9 +298,12 @@ export class InferenceCycle {
|
|
|
221
298
|
context: { source: "inference-cycle" },
|
|
222
299
|
options: { require_external: true },
|
|
223
300
|
}),
|
|
224
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(
|
|
301
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Governance MCP timed out after ${governanceMcpTimeoutMs / 1000}s`)), governanceMcpTimeoutMs)),
|
|
225
302
|
]);
|
|
226
|
-
const text = result
|
|
303
|
+
const text = governanceToolText(result);
|
|
304
|
+
if (governanceToolIsError(result)) {
|
|
305
|
+
throw new Error(text || "Governance MCP returned an error");
|
|
306
|
+
}
|
|
227
307
|
const parsed = this.parseGovernanceMcpResponse(text, proposals);
|
|
228
308
|
frameworkLogger.log("inference-cycle", "governance-mcp-primary-path", "info", {
|
|
229
309
|
proposalCount: proposals.length,
|
|
@@ -231,6 +311,24 @@ export class InferenceCycle {
|
|
|
231
311
|
});
|
|
232
312
|
return parsed.votes;
|
|
233
313
|
}
|
|
314
|
+
/** Flag off: the local matrix is the pass. Dynamo is not called and the miss is not stored as a reject. */
|
|
315
|
+
governWithLocalMatrix(proposals) {
|
|
316
|
+
return proposals.map((proposal) => {
|
|
317
|
+
const resonance = Number.isFinite(proposal.confidence) ? proposal.confidence : 0.8;
|
|
318
|
+
const matrix = applyDecisionMatrix({ resonance });
|
|
319
|
+
const decision = matrix.recommendation === "PASS"
|
|
320
|
+
? "approve"
|
|
321
|
+
: matrix.recommendation === "REJECT"
|
|
322
|
+
? "reject"
|
|
323
|
+
: "needs_revision";
|
|
324
|
+
return {
|
|
325
|
+
proposalId: proposal.id,
|
|
326
|
+
decision,
|
|
327
|
+
confidence: matrix.confidence,
|
|
328
|
+
details: ["local-matrix: inference_governance disabled", ...matrix.reasons],
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
}
|
|
234
332
|
isGovernanceMcpPreferred() {
|
|
235
333
|
try {
|
|
236
334
|
const config = featuresConfigLoader.loadConfig();
|
|
@@ -258,17 +356,12 @@ export class InferenceCycle {
|
|
|
258
356
|
return { votes, overallDecision: data.overallDecision || "needs_revision" };
|
|
259
357
|
}
|
|
260
358
|
catch {
|
|
359
|
+
const preview = text.substring(0, 300);
|
|
261
360
|
frameworkLogger.log('inference-cycle', 'governance-mcp-parse-failed', 'warning', {
|
|
262
|
-
textPreview:
|
|
361
|
+
textPreview: preview,
|
|
263
362
|
proposalCount: proposals.length,
|
|
264
363
|
});
|
|
265
|
-
|
|
266
|
-
proposalId: p.id,
|
|
267
|
-
decision: "abstain",
|
|
268
|
-
confidence: 0.5,
|
|
269
|
-
details: ["governance-mcp: parse-failed"],
|
|
270
|
-
}));
|
|
271
|
-
return { votes, overallDecision: "needs_revision" };
|
|
364
|
+
throw new Error(`governance-mcp: unreadable vote: ${preview || "empty governance response"}`);
|
|
272
365
|
}
|
|
273
366
|
}
|
|
274
367
|
extractTextFromNdjson(output) {
|
|
@@ -340,21 +433,87 @@ export class InferenceCycle {
|
|
|
340
433
|
}
|
|
341
434
|
}
|
|
342
435
|
history.push(result);
|
|
343
|
-
|
|
344
|
-
|
|
436
|
+
const dropped = history.length > HISTORY_CAP ? history.slice(0, history.length - HISTORY_CAP) : [];
|
|
437
|
+
if (history.length > HISTORY_CAP)
|
|
438
|
+
history = history.slice(-HISTORY_CAP);
|
|
439
|
+
this.retainDroppedLessonIds(dropped, history);
|
|
345
440
|
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
346
441
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (
|
|
442
|
+
/** Lesson ids that leave the 50-entry window stay covered. Empty cycles do not erase them. */
|
|
443
|
+
retainDroppedLessonIds(dropped, kept) {
|
|
444
|
+
if (dropped.length === 0)
|
|
445
|
+
return;
|
|
446
|
+
const keptIds = new Set(kept.flatMap((cycle) => (cycle.proposals ?? []).map((proposal) => proposal.id)));
|
|
447
|
+
const retained = new Set(this.readRetainedLessonIds());
|
|
448
|
+
let changed = false;
|
|
449
|
+
for (const cycle of dropped) {
|
|
450
|
+
for (const proposal of cycle.proposals ?? []) {
|
|
451
|
+
if (!isLessonProposalId(proposal.id))
|
|
452
|
+
continue;
|
|
453
|
+
if (keptIds.has(proposal.id) || retained.has(proposal.id))
|
|
454
|
+
continue;
|
|
455
|
+
retained.add(proposal.id);
|
|
456
|
+
changed = true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (!changed)
|
|
460
|
+
return;
|
|
461
|
+
if (!fs.existsSync(this.stateDir)) {
|
|
462
|
+
fs.mkdirSync(this.stateDir, { recursive: true });
|
|
463
|
+
}
|
|
464
|
+
fs.writeFileSync(path.join(this.stateDir, GRADED_LESSON_IDS_FILE), JSON.stringify([...retained].sort(), null, 2));
|
|
465
|
+
}
|
|
466
|
+
readRetainedLessonIds() {
|
|
467
|
+
const file = path.join(this.stateDir, GRADED_LESSON_IDS_FILE);
|
|
468
|
+
if (!fs.existsSync(file))
|
|
350
469
|
return [];
|
|
351
470
|
try {
|
|
352
|
-
return JSON.parse(fs.readFileSync(
|
|
471
|
+
return lessonIdsFromUnknown(JSON.parse(fs.readFileSync(file, "utf-8")));
|
|
353
472
|
}
|
|
354
473
|
catch {
|
|
355
474
|
return [];
|
|
356
475
|
}
|
|
357
476
|
}
|
|
477
|
+
retainedLessonCycle(ids) {
|
|
478
|
+
return {
|
|
479
|
+
cycleId: "retained-lessons",
|
|
480
|
+
triggered: false,
|
|
481
|
+
triggerReason: "lesson ids kept past the history cap",
|
|
482
|
+
corpusSummary: { sessions: 0, totalCommits: 0, recurringPatterns: 0, recurringProblems: 0 },
|
|
483
|
+
proposals: ids.map((id) => ({
|
|
484
|
+
id,
|
|
485
|
+
type: "codify",
|
|
486
|
+
title: id,
|
|
487
|
+
description: "",
|
|
488
|
+
evidence: [],
|
|
489
|
+
confidence: 0,
|
|
490
|
+
source: "recurring_pattern",
|
|
491
|
+
status: "pending",
|
|
492
|
+
})),
|
|
493
|
+
votes: [],
|
|
494
|
+
phase: "complete",
|
|
495
|
+
completedAt: "1970-01-01T00:00:00.000Z",
|
|
496
|
+
duration: 0,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
loadHistory() {
|
|
500
|
+
const historyPath = path.join(this.stateDir, CYCLE_HISTORY_FILE);
|
|
501
|
+
let history = [];
|
|
502
|
+
if (fs.existsSync(historyPath)) {
|
|
503
|
+
try {
|
|
504
|
+
const parsed = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
505
|
+
history = Array.isArray(parsed) ? parsed : [];
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
history = [];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const present = new Set(history.flatMap((cycle) => (cycle.proposals ?? []).map((proposal) => proposal.id)));
|
|
512
|
+
const missing = this.readRetainedLessonIds().filter((id) => !present.has(id));
|
|
513
|
+
if (missing.length === 0)
|
|
514
|
+
return history;
|
|
515
|
+
return [this.retainedLessonCycle(missing), ...history];
|
|
516
|
+
}
|
|
358
517
|
buildBlockedResult(cycleId, reason) {
|
|
359
518
|
return {
|
|
360
519
|
cycleId,
|
|
@@ -1,3 +1,87 @@
|
|
|
1
|
+
function primitivesForSessions(corpus, sessionIds) {
|
|
2
|
+
const wanted = new Set(sessionIds);
|
|
3
|
+
const names = [];
|
|
4
|
+
for (const session of corpus.sessions) {
|
|
5
|
+
if (!wanted.has(session.sessionId))
|
|
6
|
+
continue;
|
|
7
|
+
const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
|
|
8
|
+
for (const name of listed)
|
|
9
|
+
names.push(name);
|
|
10
|
+
}
|
|
11
|
+
return [...new Set(names.filter((name) => name.length > 0))];
|
|
12
|
+
}
|
|
13
|
+
function signalsForPattern(corpus, pattern) {
|
|
14
|
+
const names = primitivesForSessions(corpus, pattern.sessions);
|
|
15
|
+
if (pattern.name.length > 0)
|
|
16
|
+
names.push(pattern.name);
|
|
17
|
+
return [...new Set(names)];
|
|
18
|
+
}
|
|
19
|
+
/** Solid band starts at 0.82. Landed work with no wrong turn clears it. */
|
|
20
|
+
const LANDED_RESONANCE = 0.85;
|
|
21
|
+
const WRONG_TURN_RESONANCE = 0.4;
|
|
22
|
+
function resonanceForNamedSignal(corpus, sessionIds) {
|
|
23
|
+
const sessions = corpus.sessions.filter((session) => sessionIds.includes(session.sessionId));
|
|
24
|
+
if (sessions.some((session) => (session.wrongTurns?.length ?? 0) > 0))
|
|
25
|
+
return WRONG_TURN_RESONANCE;
|
|
26
|
+
if (sessions.some((session) => (session.approaches?.length ?? 0) > 0 || (session.solutions?.length ?? 0) > 0)) {
|
|
27
|
+
return LANDED_RESONANCE;
|
|
28
|
+
}
|
|
29
|
+
let confidence = 0.7;
|
|
30
|
+
for (const session of sessions) {
|
|
31
|
+
for (const pattern of session.patterns) {
|
|
32
|
+
if (pattern.confidence > confidence)
|
|
33
|
+
confidence = pattern.confidence;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return confidence;
|
|
37
|
+
}
|
|
38
|
+
function sessionsForWrongTurn(corpus, turn) {
|
|
39
|
+
return corpus.sessions
|
|
40
|
+
.filter((session) => session.wrongTurns.includes(turn))
|
|
41
|
+
.map((session) => session.sessionId);
|
|
42
|
+
}
|
|
43
|
+
const LESSON_TEXT_CAP = 400;
|
|
44
|
+
/** The trace a graded line stores. Wrong turns, then approaches, then solutions. */
|
|
45
|
+
function lessonTrace(corpus, sessionIds) {
|
|
46
|
+
const wanted = new Set(sessionIds);
|
|
47
|
+
const lines = [];
|
|
48
|
+
for (const session of corpus.sessions) {
|
|
49
|
+
if (!wanted.has(session.sessionId))
|
|
50
|
+
continue;
|
|
51
|
+
for (const turn of session.wrongTurns ?? []) {
|
|
52
|
+
if (turn.trim())
|
|
53
|
+
lines.push(turn.trim());
|
|
54
|
+
}
|
|
55
|
+
for (const line of [...(session.approaches ?? []), ...(session.solutions ?? [])]) {
|
|
56
|
+
if (line.trim())
|
|
57
|
+
lines.push(line.trim());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...new Set(lines)].join("; ").slice(0, LESSON_TEXT_CAP);
|
|
61
|
+
}
|
|
62
|
+
function sortedSessionIds(sessionIds) {
|
|
63
|
+
return [...new Set(sessionIds.filter((sessionId) => sessionId.length > 0))].sort();
|
|
64
|
+
}
|
|
65
|
+
/** Sessions already included in a prior proposal of this exact prefix. */
|
|
66
|
+
function coveredSessions(history, prefix) {
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const cycle of history ?? []) {
|
|
69
|
+
for (const proposal of cycle.proposals) {
|
|
70
|
+
if (!proposal.id.startsWith(prefix))
|
|
71
|
+
continue;
|
|
72
|
+
for (const sessionId of proposal.id.slice(prefix.length).split(",")) {
|
|
73
|
+
if (sessionId.length > 0)
|
|
74
|
+
seen.add(sessionId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return seen;
|
|
79
|
+
}
|
|
80
|
+
/** Sessions that have not already been graded under this prefix. */
|
|
81
|
+
function freshSessions(history, prefix, sessionIds) {
|
|
82
|
+
const covered = coveredSessions(history, prefix);
|
|
83
|
+
return sortedSessionIds(sessionIds).filter((sessionId) => !covered.has(sessionId));
|
|
84
|
+
}
|
|
1
85
|
function classifyProposalType(problemPattern) {
|
|
2
86
|
const lower = problemPattern.toLowerCase();
|
|
3
87
|
if (lower.includes("bug") || lower.includes("fix") || lower.includes("stability"))
|
|
@@ -63,6 +147,8 @@ function adjustConfidenceFromHistory(proposals, history) {
|
|
|
63
147
|
}
|
|
64
148
|
}
|
|
65
149
|
for (const proposal of proposals) {
|
|
150
|
+
if (proposal.id.startsWith("named:"))
|
|
151
|
+
continue;
|
|
66
152
|
const approved = approvedTypes.get(proposal.type) ?? 0;
|
|
67
153
|
const rejected = rejectedTypes.get(proposal.type) ?? 0;
|
|
68
154
|
const total = approved + rejected;
|
|
@@ -80,15 +166,21 @@ function adjustConfidenceFromHistory(proposals, history) {
|
|
|
80
166
|
export function generateProposals(corpus, history) {
|
|
81
167
|
const proposals = [];
|
|
82
168
|
for (const problem of corpus.recurringProblems) {
|
|
169
|
+
const prefix = `problem:${problem.pattern}:`;
|
|
170
|
+
const sessions = freshSessions(history, prefix, problem.sessions);
|
|
171
|
+
if (sessions.length === 0)
|
|
172
|
+
continue;
|
|
83
173
|
proposals.push({
|
|
84
|
-
id:
|
|
174
|
+
id: `${prefix}${sessions.join(",")}`,
|
|
85
175
|
type: classifyProposalType(problem.pattern),
|
|
86
176
|
title: generateTitle(problem),
|
|
87
177
|
description: `Recurring across ${problem.occurrences} sessions: ${problem.pattern}`,
|
|
88
|
-
evidence:
|
|
178
|
+
evidence: sessions.map((s) => `Seen in session ${s}`),
|
|
89
179
|
confidence: Math.min(0.95, 0.5 + problem.occurrences * 0.15),
|
|
90
180
|
source: "recurring_problem",
|
|
91
181
|
status: "pending",
|
|
182
|
+
namedSignals: primitivesForSessions(corpus, sessions),
|
|
183
|
+
lesson: lessonTrace(corpus, sessions),
|
|
92
184
|
});
|
|
93
185
|
}
|
|
94
186
|
const seenPatterns = new Set(corpus.recurringProblems.map((p) => p.pattern));
|
|
@@ -99,8 +191,14 @@ export function generateProposals(corpus, history) {
|
|
|
99
191
|
continue;
|
|
100
192
|
if (proposals.length >= 5)
|
|
101
193
|
break;
|
|
194
|
+
const prefix = `problem:${normalized}:`;
|
|
195
|
+
const sessions = freshSessions(history, prefix, [session]);
|
|
196
|
+
if (sessions.length === 0) {
|
|
197
|
+
seenPatterns.add(normalized);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
102
200
|
proposals.push({
|
|
103
|
-
id:
|
|
201
|
+
id: `${prefix}${sessions.join(",")}`,
|
|
104
202
|
type: classifyProposalType(problem),
|
|
105
203
|
title: `Investigate: ${problem.substring(0, 80)}`,
|
|
106
204
|
description: `Observed in session ${session}: ${problem}`,
|
|
@@ -108,15 +206,21 @@ export function generateProposals(corpus, history) {
|
|
|
108
206
|
confidence: 0.4,
|
|
109
207
|
source: "recurring_problem",
|
|
110
208
|
status: "pending",
|
|
209
|
+
namedSignals: primitivesForSessions(corpus, sessions),
|
|
210
|
+
lesson: lessonTrace(corpus, sessions),
|
|
111
211
|
});
|
|
112
212
|
seenPatterns.add(normalized);
|
|
113
213
|
}
|
|
114
214
|
for (const pattern of corpus.recurringPatterns) {
|
|
115
215
|
if (pattern.occurrences < 2)
|
|
116
216
|
continue;
|
|
217
|
+
const prefix = `pattern:${pattern.name}:`;
|
|
218
|
+
const sessions = freshSessions(history, prefix, pattern.sessions);
|
|
219
|
+
if (sessions.length === 0)
|
|
220
|
+
continue;
|
|
117
221
|
const type = patternToProposalType(pattern);
|
|
118
222
|
proposals.push({
|
|
119
|
-
id:
|
|
223
|
+
id: `${prefix}${sessions.join(",")}`,
|
|
120
224
|
type,
|
|
121
225
|
title: `Codify ${pattern.name} pattern`,
|
|
122
226
|
description: `${pattern.name} detected across ${pattern.occurrences} sessions (avg confidence: ${Math.round(pattern.avgConfidence * 100)}%). ${pattern.description}`,
|
|
@@ -124,13 +228,19 @@ export function generateProposals(corpus, history) {
|
|
|
124
228
|
confidence: pattern.avgConfidence,
|
|
125
229
|
source: "recurring_pattern",
|
|
126
230
|
status: "pending",
|
|
231
|
+
namedSignals: signalsForPattern(corpus, { ...pattern, sessions }),
|
|
232
|
+
lesson: lessonTrace(corpus, sessions),
|
|
127
233
|
});
|
|
128
234
|
}
|
|
129
235
|
const wrongTurns = corpus.allWrongTurns.slice(0, 2);
|
|
130
236
|
for (const wt of wrongTurns) {
|
|
131
237
|
const summary = wt.length > 50 ? `${wt.substring(0, 47)}...` : wt;
|
|
238
|
+
const prefix = `wrong:${wt}:`;
|
|
239
|
+
const sessions = freshSessions(history, prefix, sessionsForWrongTurn(corpus, wt));
|
|
240
|
+
if (sessions.length === 0)
|
|
241
|
+
continue;
|
|
132
242
|
proposals.push({
|
|
133
|
-
id:
|
|
243
|
+
id: `${prefix}${sessions.join(",")}`,
|
|
134
244
|
type: "guard",
|
|
135
245
|
title: `Guard against: ${summary}`,
|
|
136
246
|
description: `Recurring wrong turn detected: ${wt}. Add a guard or validation to prevent this pattern.`,
|
|
@@ -138,9 +248,42 @@ export function generateProposals(corpus, history) {
|
|
|
138
248
|
confidence: 0.7,
|
|
139
249
|
source: "wrong_turn",
|
|
140
250
|
status: "pending",
|
|
251
|
+
namedSignals: primitivesForSessions(corpus, sessions),
|
|
252
|
+
lesson: lessonTrace(corpus, sessions),
|
|
141
253
|
});
|
|
142
254
|
}
|
|
143
255
|
const sorted = proposals.sort((a, b) => b.confidence - a.confidence).slice(0, 3);
|
|
256
|
+
const alreadyNamed = new Set(sorted.flatMap((proposal) => proposal.namedSignals ?? []));
|
|
257
|
+
const bySignal = new Map();
|
|
258
|
+
for (const session of corpus.sessions) {
|
|
259
|
+
const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
|
|
260
|
+
for (const name of listed) {
|
|
261
|
+
if (name.length === 0 || alreadyNamed.has(name))
|
|
262
|
+
continue;
|
|
263
|
+
const sessions = bySignal.get(name) ?? [];
|
|
264
|
+
if (!sessions.includes(session.sessionId))
|
|
265
|
+
sessions.push(session.sessionId);
|
|
266
|
+
bySignal.set(name, sessions);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const [name, sessionIds] of bySignal) {
|
|
270
|
+
const sessions = freshSessions(history, `named:${name}:`, sessionIds);
|
|
271
|
+
if (sessions.length === 0)
|
|
272
|
+
continue;
|
|
273
|
+
const id = `named:${name}:${sessions.join(",")}`;
|
|
274
|
+
sorted.push({
|
|
275
|
+
id,
|
|
276
|
+
type: "codify",
|
|
277
|
+
title: `Grade named signal ${name}`,
|
|
278
|
+
description: `Sessions ${sessions.join(", ")} named ${name}.`,
|
|
279
|
+
evidence: sessions.map((sessionId) => `Seen in session ${sessionId}`),
|
|
280
|
+
confidence: resonanceForNamedSignal(corpus, sessions),
|
|
281
|
+
source: "recurring_pattern",
|
|
282
|
+
status: "pending",
|
|
283
|
+
namedSignals: [name],
|
|
284
|
+
lesson: lessonTrace(corpus, sessions),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
144
287
|
if (history && history.length > 0) {
|
|
145
288
|
adjustConfidenceFromHistory(sorted, history);
|
|
146
289
|
sorted.sort((a, b) => b.confidence - a.confidence);
|
|
@@ -13,6 +13,10 @@ 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[];
|
|
16
20
|
}
|
|
17
21
|
export interface ReasoningLink {
|
|
18
22
|
from: string;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Thin adapter. Repo `.cursor/hooks.json` (schema **version 1**) calls 0xRay gates and the Station PreCompact writer. **sessionStart is not used** — managed Cursor cloud does not fire it.
|
|
4
4
|
|
|
5
|
-
Repertoire
|
|
5
|
+
Repertoire is preferred (vendored, dest = named laws). Station + Codex gates still run if memory-routing is explicitly off — that is fallback, not the preferred brain.
|
|
6
6
|
|
|
7
7
|
## How cloud picks up project hooks
|
|
8
8
|
|
|
@@ -182,6 +182,25 @@ export function cursorHeatRoots(event = {}) {
|
|
|
182
182
|
return out;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
function packageNameAt(root) {
|
|
186
|
+
try {
|
|
187
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
188
|
+
return typeof pkg.name === 'string' ? pkg.name : '';
|
|
189
|
+
} catch {
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Constitution follows the suit mill when that package is in the heat set.
|
|
196
|
+
* A sibling repo's leftover synthesis checkpoint must not deny the host.
|
|
197
|
+
*/
|
|
198
|
+
export function cursorGateRoot(heatRoots, eventRoot) {
|
|
199
|
+
const mill = heatRoots.find((root) => packageNameAt(root) === '0xray');
|
|
200
|
+
if (mill) return mill;
|
|
201
|
+
return heatRoots[0] || eventRoot;
|
|
202
|
+
}
|
|
203
|
+
|
|
185
204
|
export function cursorSessionId(event = {}) {
|
|
186
205
|
return (
|
|
187
206
|
event.conversation_id ||
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
appendHookActivity,
|
|
10
10
|
checkCodexPatterns,
|
|
11
11
|
checkFullTestSuite,
|
|
12
|
+
cursorGateRoot,
|
|
12
13
|
cursorHeatRoots,
|
|
13
14
|
cursorSessionId,
|
|
14
15
|
cursorToolContext,
|
|
@@ -53,7 +54,7 @@ async function main() {
|
|
|
53
54
|
for (const root of heatRoots) {
|
|
54
55
|
ensureCursorSessionBoot(root, '0xray/cursor-pre-tool-use-boot', { sessionId });
|
|
55
56
|
}
|
|
56
|
-
const gateRoot = heatRoots
|
|
57
|
+
const gateRoot = cursorGateRoot(heatRoots, eventRoot);
|
|
57
58
|
|
|
58
59
|
const features = loadFeatures(gateRoot);
|
|
59
60
|
const gateFeatures = loadDelegationGateFeatures(gateRoot, 'cursor');
|