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.
Files changed (103) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/hermes-agent/SKILL.md +1 -1
  3. package/.opencode/skills/orchestrator/SKILL.md +28 -6
  4. package/.xray/features.json +1 -1
  5. package/AGENTS-consumer.md +11 -11
  6. package/AGENTS.md +8 -8
  7. package/README.md +27 -27
  8. package/SKILLS.md +1 -1
  9. package/dist/AGENTS.md +8 -8
  10. package/dist/CHANGELOG.md +98 -0
  11. package/dist/README.md +27 -27
  12. package/dist/SKILLS.md +1 -1
  13. package/dist/governance/governance-core.d.ts +2 -1
  14. package/dist/governance/governance-core.js +20 -11
  15. package/dist/governance/governance-service.js +9 -7
  16. package/dist/inference/inference-accumulator.js +36 -4
  17. package/dist/inference/inference-cycle.d.ts +12 -0
  18. package/dist/inference/inference-cycle.js +180 -21
  19. package/dist/inference/inference-proposal-generator.js +148 -5
  20. package/dist/inference/session-capture.d.ts +4 -0
  21. package/dist/integrations/cursor/README.md +1 -1
  22. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +19 -0
  23. package/dist/integrations/cursor/hooks/pre-tool-use.js +2 -1
  24. package/dist/integrations/governance/index.d.ts +5 -0
  25. package/dist/integrations/governance/index.js +22 -2
  26. package/dist/integrations/governance/types.d.ts +11 -0
  27. package/dist/integrations/governance/types.js +16 -0
  28. package/dist/integrations/hooks/station-hook-runtime.cjs +90 -30
  29. package/dist/integrations/hooks/station-hook-runtime.mjs +1 -0
  30. package/dist/integrations/hooks/station-memory-ingest.mjs +4 -2
  31. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  32. package/dist/mcps/governance.server.d.ts +2 -0
  33. package/dist/mcps/governance.server.js +12 -1
  34. package/dist/memory-routing/index.d.ts +1 -1
  35. package/dist/memory-routing/index.js +1 -1
  36. package/dist/memory-routing/lesson-pickup.d.ts +2 -0
  37. package/dist/memory-routing/lesson-pickup.js +57 -0
  38. package/dist/memory-routing/provider-loader.d.ts +8 -1
  39. package/dist/memory-routing/provider-loader.js +49 -1
  40. package/dist/memory-routing/provider-registry.d.ts +7 -2
  41. package/dist/memory-routing/provider-registry.js +26 -6
  42. package/dist/memory-routing/record-lesson.d.ts +16 -0
  43. package/dist/memory-routing/record-lesson.js +26 -0
  44. package/dist/memory-routing/types.d.ts +15 -0
  45. package/dist/nucleus/thin-dispatch.d.ts +2 -0
  46. package/dist/nucleus/thin-dispatch.js +14 -4
  47. package/dist/processors/implementations/agents-md-validation-processor.js +1 -1
  48. package/dist/skills/hermes-agent/SKILL.md +1 -1
  49. package/dist/skills/orchestrator/SKILL.md +28 -6
  50. package/package.json +2 -2
  51. package/scripts/foundry/CHANGELOG.md +2 -0
  52. package/scripts/foundry/README.md +2 -2
  53. package/scripts/foundry/cli.mjs +1 -1
  54. package/scripts/foundry/inspect.mjs +160 -5
  55. package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -0
  56. package/scripts/foundry/reconcile-version.mjs +23 -1
  57. package/scripts/foundry/release.mjs +1 -1
  58. package/scripts/foundry/validate-release-docs.mjs +24 -20
  59. package/scripts/foundry/version-manager.mjs +180 -7
  60. package/scripts/mjs/test-consumer-readiness.mjs +10 -3
  61. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  62. package/src/mcps/governance.server.ts +12 -1
  63. package/src/skills/hermes-agent/SKILL.md +1 -1
  64. package/src/skills/orchestrator/SKILL.md +28 -6
  65. package/vendor/@0xray/repertoire/data/stack-overlay.json +156 -3
  66. package/vendor/@0xray/repertoire/data/subject-overlay.json +3 -3
  67. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts +4 -0
  68. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +1 -1
  69. package/vendor/@0xray/repertoire/dist/RepertoireService.js +7 -6
  70. package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +1 -1
  71. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +1 -1
  72. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js +10 -11
  73. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +1 -1
  74. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +1 -1
  75. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js +5 -0
  76. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +1 -1
  77. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +1 -1
  78. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -0
  79. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +1 -1
  80. package/vendor/@0xray/repertoire/dist/paths.d.ts +3 -3
  81. package/vendor/@0xray/repertoire/dist/paths.d.ts.map +1 -1
  82. package/vendor/@0xray/repertoire/dist/paths.js +66 -4
  83. package/vendor/@0xray/repertoire/dist/paths.js.map +1 -1
  84. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +11 -0
  85. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +1 -1
  86. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +2 -0
  87. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +1 -1
  88. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +54 -4
  89. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
  90. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +375 -20
  91. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
  92. package/vendor/@0xray/repertoire/dist/types.d.ts +21 -0
  93. package/vendor/@0xray/repertoire/dist/types.d.ts.map +1 -1
  94. package/xray/agents_template.md +10 -5
  95. package/xray/features.json +1 -1
  96. package/dist/mcps/shared/prompt-security-validator.d.ts +0 -44
  97. package/dist/mcps/shared/prompt-security-validator.js +0 -148
  98. package/dist/mcps/shared/security-scanner.d.ts +0 -119
  99. package/dist/mcps/shared/security-scanner.js +0 -429
  100. package/dist/scripts/activate-kernel-pipeline.d.ts +0 -7
  101. package/dist/scripts/activate-kernel-pipeline.js +0 -101
  102. package/src/mcps/shared/prompt-security-validator.ts +0 -199
  103. 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 p.evidence) {
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: [...p.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
- return JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
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 !== null);
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
- if (!InferenceCycle.instances.has(root)) {
20
- InferenceCycle.instances.set(root, new InferenceCycle(root, undefined, options));
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
- return InferenceCycle.instances.get(root);
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
- return this.buildResult(cycleId, true, threshold.reason, startTime, corpus, proposals);
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
- throw new Error("Governance MCP is required but not available");
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("Governance MCP timed out after 8s")), 8000)),
301
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Governance MCP timed out after ${governanceMcpTimeoutMs / 1000}s`)), governanceMcpTimeoutMs)),
225
302
  ]);
226
- const text = result?.content?.[0]?.text || "";
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: text.substring(0, 200),
361
+ textPreview: preview,
263
362
  proposalCount: proposals.length,
264
363
  });
265
- const votes = proposals.map(p => ({
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
- if (history.length > 50)
344
- history = history.slice(-50);
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
- loadHistory() {
348
- const historyPath = path.join(this.stateDir, CYCLE_HISTORY_FILE);
349
- if (!fs.existsSync(historyPath))
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(historyPath, "utf-8"));
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: `prop-${Date.now()}-${proposals.length}`,
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: problem.sessions.map((s) => `Seen in session ${s}`),
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: `prop-${Date.now()}-${proposals.length}`,
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: `prop-${Date.now()}-${proposals.length}`,
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: `prop-${Date.now()}-${proposals.length}`,
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 MCP is optional. Station + gates work without a full memory-routing rewire.
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[0] || eventRoot;
57
+ const gateRoot = cursorGateRoot(heatRoots, eventRoot);
57
58
 
58
59
  const features = loadFeatures(gateRoot);
59
60
  const gateFeatures = loadDelegationGateFeatures(gateRoot, 'cursor');