0xray 4.0.22 → 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 (65) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/orchestrator/SKILL.md +7 -7
  3. package/.xray/features.json +1 -1
  4. package/dist/CHANGELOG.md +24 -1
  5. package/dist/inference/inference-accumulator.js +36 -4
  6. package/dist/inference/inference-cycle.d.ts +12 -0
  7. package/dist/inference/inference-cycle.js +158 -16
  8. package/dist/inference/inference-proposal-generator.js +148 -5
  9. package/dist/inference/session-capture.d.ts +4 -0
  10. package/dist/integrations/governance/index.d.ts +5 -0
  11. package/dist/integrations/governance/index.js +22 -2
  12. package/dist/integrations/governance/types.d.ts +11 -0
  13. package/dist/integrations/governance/types.js +16 -0
  14. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  15. package/dist/mcps/governance.server.d.ts +2 -0
  16. package/dist/mcps/governance.server.js +12 -1
  17. package/dist/memory-routing/index.d.ts +1 -1
  18. package/dist/memory-routing/index.js +1 -1
  19. package/dist/memory-routing/lesson-pickup.d.ts +2 -0
  20. package/dist/memory-routing/lesson-pickup.js +57 -0
  21. package/dist/memory-routing/provider-loader.d.ts +5 -0
  22. package/dist/memory-routing/provider-loader.js +46 -0
  23. package/dist/memory-routing/provider-registry.d.ts +7 -2
  24. package/dist/memory-routing/provider-registry.js +26 -6
  25. package/dist/memory-routing/record-lesson.d.ts +16 -0
  26. package/dist/memory-routing/record-lesson.js +26 -0
  27. package/dist/memory-routing/types.d.ts +15 -0
  28. package/dist/nucleus/thin-dispatch.d.ts +2 -0
  29. package/dist/nucleus/thin-dispatch.js +3 -2
  30. package/dist/skills/orchestrator/SKILL.md +7 -7
  31. package/package.json +1 -1
  32. package/scripts/foundry/reconcile-version.mjs +23 -1
  33. package/scripts/foundry/release.mjs +1 -1
  34. package/scripts/foundry/version-manager.mjs +53 -0
  35. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  36. package/src/mcps/governance.server.ts +12 -1
  37. package/src/skills/orchestrator/SKILL.md +7 -7
  38. package/vendor/@0xray/repertoire/data/stack-overlay.json +4 -4
  39. package/vendor/@0xray/repertoire/data/subject-overlay.json +3 -3
  40. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts +4 -0
  41. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +1 -1
  42. package/vendor/@0xray/repertoire/dist/RepertoireService.js +4 -4
  43. package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +1 -1
  44. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +1 -1
  45. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +1 -1
  46. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +1 -1
  47. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js +5 -0
  48. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +1 -1
  49. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +1 -1
  50. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -0
  51. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +1 -1
  52. package/vendor/@0xray/repertoire/dist/paths.d.ts +3 -3
  53. package/vendor/@0xray/repertoire/dist/paths.d.ts.map +1 -1
  54. package/vendor/@0xray/repertoire/dist/paths.js.map +1 -1
  55. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +11 -0
  56. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +1 -1
  57. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +2 -0
  58. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +1 -1
  59. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +54 -4
  60. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
  61. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +326 -11
  62. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
  63. package/vendor/@0xray/repertoire/dist/types.d.ts +21 -0
  64. package/vendor/@0xray/repertoire/dist/types.d.ts.map +1 -1
  65. package/xray/features.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.22",
3
+ "version": "4.0.23",
4
4
  "description": "0xRay suit for Grok, Hermes, OpenClaw, OpenCode: mill+inspect, Codex, 7 MCP servers. Costume 45/42 is opt-in foundry, not the default wear.",
5
5
  "author": {
6
6
  "name": "0xRay AI",
@@ -7,15 +7,15 @@ version: 1.0.0
7
7
  schema_version: "1.0"
8
8
  tags: [orchestration, orchestrator, lead-dev]
9
9
  capabilities:
10
- - coordinate_agents
11
- - delegate_tasks
12
- - manage_workflow
10
+ - coordinate_agents
11
+ - delegate_tasks
12
+ - manage_workflow
13
13
  dependencies: []
14
14
 
15
15
  mcp:
16
- orchestrator:
17
- command: node
18
- args: [node_modules/0xray/dist/mcps/orchestrator.server.js]
16
+ orchestrator:
17
+ command: node
18
+ args: [node_modules/0xray/dist/mcps/orchestrator.server.js]
19
19
  ---
20
20
 
21
21
  # Orchestrator Skill — includes lead-dev operating model
@@ -77,7 +77,7 @@ Jargon is not the job. Review and cadence ask whether the change works. Do not s
77
77
  - **Live tick, not pacer:** `/loop` runs work now. Keep workstreams moving. Do not idle-wait 12 minutes. **Clean ticks every cycle** — unsubscribe then resubscribe so the prompt matches metal. Name-dedupe does not rewrite. A stale prompt (waiting URL after live, Hold after strip) is dirty: rewrite, do not act. **If the card and WAVEBOARD are idle, unsubscribe and do not resubscribe.**
78
78
  - **Board clock:** PR events + resume the same critic. Not two idle pacers. Merge after PASS+CI (gate A). Publish only after mill-gate D + `foundry gate`.
79
79
  - **Dispatch:** ticket + PR URLs + “Wear, then review.” The critic Reads `SEATS.md` Reviewer. Do not paste a command novel. When a law is spoken twice, encode it this wake.
80
- - **Cascade:** Chat is not the brain. A cascade that must survive compaction is a named repertoire signal on the stack overlay, hydrated onto the project law list. NOTES holds the same cascade for the second Read. Station stays the ticket. A reflection is not this capture. Capture priority: a law is an invariant you would otherwise re-read the source to relearn. One law per subsystem bone. Heat touches last_seen and does not append a confidence sample, so more hits do not raise conviction. Heat touches last_seen only when the diary contains that signal's name; two definition words are not a hit; a weak session score is not lifted to 0.55 and a sample below 0.55 is not recorded. Constructing the organ does not append workspace samples. Hero names: `heat-is-not-conviction`, `inference-cycle-invariant`, `governance-vote-invariant`, `thin-dispatch-invariant`. Repertoire mergeStackOverlay refreshes a changed stack definition and keeps observation stats.
80
+ - **Cascade:** Chat is not the brain. A cascade that must survive compaction is a named repertoire signal on the stack overlay, hydrated onto the project law list. NOTES holds the same cascade for the second Read. Station stays the ticket. A reflection is not this capture. Capture priority: a law is an invariant you would otherwise re-read the source to relearn. One law per subsystem bone. Heat touches last_seen and does not append a confidence sample, so more hits do not raise conviction. Heat touches last_seen when the diary contains that signal's name or a local clause of four definition content words; two definition words are not a hit; the same words scattered through a long diary are not a hit; a weak session score is not lifted to 0.55 and a sample below 0.55 is not recorded. Constructing the organ does not append workspace samples. Hero names: `heat-is-not-conviction`, `inference-cycle-invariant`, `governance-vote-invariant`, `thin-dispatch-invariant`. Repertoire mergeStackOverlay refreshes a changed stack definition and keeps observation stats. A floor observation count does not change a route. Feedback moves conviction by a tenth, and a failure can leave 0.55. learned-conviction.json beside dest restores that average when a wake flattens it to 0.55. It does not store the observation count.
81
81
  - **Cold brief:** a new critic or any new subagent that does not already have this thread gets the latest commit SHA on the branch under review (fetch it; do not reuse an earlier SHA from a previous pass) and the Station card (`.xray/state/STATION.md`: intent, plan, git). If the card is missing, give branch + HEAD + what just changed. If the branch moves after you brief them, send the new SHA before they verdict.
82
82
  - **Control:** The lead keeps control. Do not assume the subagent has the context. Give the duty, the latest SHA, the Station card, and the laws that apply. The subagent wears the applicable 0xRay suit before it works: the tree under review, built and installed. An older published package is not that suit. The lead checks the result.
83
83
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.22",
3
+ "version": "4.0.23",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",
package/dist/CHANGELOG.md CHANGED
@@ -10,11 +10,19 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
10
10
  - **Feat rebase.** When the mandated work turns into mantra, or the job has split across planes that no longer share one present, stop and answer the original message again from this seat. On `LEAD-CADENCE.md`, the orchestrator skill, and the ship-ready mill gate. Project dest grows the name `feat-rebase` from the stack overlay. Not a new skill.
11
11
 
12
12
  ### Fixed
13
+ - **A governed proposal teaches.** A route lookup does not write a lesson. When a vote finishes, the cycle grades the signals that proposal's sessions already named. A session that names a stored signal is graded even when that name appears once. Landed approaches and no wrong turn clear the solid band. A wrong turn stays below the cutoff. Earlier rejects of other codify proposals do not pull that grade down. Approve steps those signals up by a tenth. Reject steps them down by a tenth. Needs-revision, an abstain, a governance error, and a proposal whose sessions named nothing do not write. Proposal ids stay on the signal and the sessions not yet graded, so a later cycle does not teach those sessions again. A new session is a new id. Those ids remain after the history window of 50 fills, including with empty cycles. A proposal that carries an empty name list does not fall through to matching its text. A grade that passes appends one line — the task, the decision, and the approaches, solutions, or wrong turn from the sessions that named the law — on that signal and beside the average. The same task does not append again. The route for a law above 0.55 includes the law text and those lines, and Station's lessons section is refreshed from dest. Not a new skill.
14
+ - **Dynamo client on a required external call.** `govern_proposals` with external governance required starts `InferenceGovernanceIntegration` even when `inference_governance.enabled` is false. Boot still skips that client while the flag is off, and the inference cycle still uses the local matrix unless the flag is on or `XRAY_FORCE_MCP_GOVERNANCE=true`. An unreadable MCP payload is stored as the error text, not an abstain at 0.5. The MCP wait matches the governance budget (90s). Not a new skill.
15
+ - **Subsystem pass.** A session file with no `metrics.commits` is not a session, and a captured pattern with no `evidence` array is empty evidence, so the inference cycle no longer dies before it writes state. `scoreAndRoute` loads the memory organ on the first call with a synchronous require, before any dynamic import of that module. A cached null provider does not block that load, and a late null load does not replace an organ already in hand. Not a new skill.
16
+ - **Local governance pass.** When `inference_governance.enabled` is false, the inference cycle scores each proposal with the local decision matrix. That miss is not stored as a reject at confidence 0, and Dynamo is not called. The governance MCP path stays in place when the flag is on. Not a new skill.
17
+ - **Learned conviction.** A floor observation count does not change a route. Feedback moves conviction by a tenth, and a failure can leave 0.55. Retrieval hits a local clause of four definition content words. Two definition words are not a hit. The same words scattered through a long diary are not a hit. x402 prose alone is not a hit. `learned-conviction.json` beside dest restores the average when a wake flattens it to 0.55 and does not store the observation count. The vendored organ dist matches that behavior. Not a new skill.
18
+ - **Seeded conviction.** Load writes `learned-conviction.json` for dest averages already above 0.55, including historical averages with no evidence count. Evidence is the existing positive count, otherwise 1. Floor averages, including float dust at 0.55, stay out of the file. A later load restores those averages after a flatten. The vendored organ dist matches that behavior. Not a new skill.
19
+ - **Lock matches the cut.** `package-lock.json` root version and `packages[""].version` follow `package.json` on stamp. The 4.0.22 release left the lock at 4.0.21. Not a new skill.
20
+ - **Bumper writes the lock.** `reconcile --apply` updates `package.json` and the lock version fields together. `reconcile --check` (the docs mill in CI) fails when they differ. Not a new skill.
13
21
  - **Cursor gate root.** The suit mill already has synthesis off. Cursor tools follow that mill when it is in the heat set, so a sibling repo's leftover synthesis checkpoint does not deny the host. Not a new skill.
14
22
  - **Engine cadence.** Change the engine, ship it local (build and install the tree; a live cut stays live), test, review the diff, monitor dest and Station, then ship the PR. Missing moral fusion is not an isotopic reject; vote confidence is a clamped weighted mean; thinDispatch strategy follows the adjusted level; an empty inference cycle saves state and later getInstance options apply. On `LEAD-CADENCE.md`, the orchestrator skill, and the ship-ready mill gate. Not a new skill.
15
23
  - **Operating planes.** Code, OP-PROC, model, suit, mill, host, and test/ship. Test then ship is the hero; ship without that proof is the catastrophe. Project long-term memory refreshes a changed stack definition and keeps observation stats. Heat writes the matched names onto Station. Name `operating-planes` on the stack overlay. Not a new skill.
16
24
  - **Wake cascade.** Chat is not the brain. A cascade that must survive compaction is the repertoire name `wake-cascade` on the stack overlay, with the same text in NOTES for the second Read. A reflection is not this capture. Not a new skill.
17
- - **Hero invariants.** Capture priority is an invariant you would otherwise re-read the source to relearn. Names: `heat-is-not-conviction`, `inference-cycle-invariant`, `governance-vote-invariant`, `thin-dispatch-invariant`. Heat touches last_seen and does not append a confidence sample. Heat touches last_seen only when the diary contains that signal's name; two definition words are not a hit; a weak session score is not lifted to 0.55 and a sample below 0.55 is not recorded. Constructing the organ does not append workspace samples. Repertoire `mergeStackOverlay` refreshes a changed stack definition and keeps observation stats. Not a new skill.
25
+ - **Hero invariants.** Capture priority is an invariant you would otherwise re-read the source to relearn. Names: `heat-is-not-conviction`, `inference-cycle-invariant`, `governance-vote-invariant`, `thin-dispatch-invariant`. Heat touches last_seen and does not append a confidence sample. Heat touches last_seen when the diary contains that signal's name or a local clause of four definition content words; two definition words are not a hit; the same words scattered through a long diary are not a hit; a weak session score is not lifted to 0.55 and a sample below 0.55 is not recorded. Constructing the organ does not append workspace samples. Repertoire `mergeStackOverlay` refreshes a changed stack definition and keeps observation stats. Not a new skill.
18
26
  - **Stamper strip set.** Once a version is live, the stamper strips every present-tense patch pin from shipped guides and shipped OP-PROC, or reconcile advances the next cut. The stamper does not edit Station, NOTES, dest, or node_modules. It does not publish. The release docs check uses that same strip set, including `grok-bot/skills/ship-ready-mill-gate/SKILL.md`. Proposal text stays a keyword map over session problem strings, and `generateProposals` also emits recurring-pattern titles. Not a new skill.
19
27
  - **Heat + capture leftover** — module-missing resume is `Repertoire: module unresolved`, not “memory_routing stays off”. Session capture writes `matched_primitives` from existing dest laws in git. Grow receipt is post-prune dest count + heated length. Leftover `provider: null` still migrates old tarballs; explicit opt-out stays `enabled: false` + `provider: repertoire`. Does not republish.
20
28
 
@@ -29,6 +37,21 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
29
37
  - Archive root `integrations/` costume SKILL dump → `docs/archive/integrations-costume/`. Live wear is `src/integrations`.
30
38
  - Archive root `commands/` stale copy → `docs/archive/commands-copy/`. Live wear is `src/opencode/commands`.
31
39
 
40
+ ## [4.0.23] - 2026-09-23
41
+
42
+ ### 🔄 Changes
43
+
44
+ ### 🐛 Bug Fixes
45
+ - fix: start the Dynamo client when external governance is required (d17b9cf19)
46
+ - fix: score proposals locally when inference governance is off (ae8075116)
47
+ - fix: finish the inference cycle and load the organ before routing (69522e6c3)
48
+ - fix: vendor the organ that seeds conviction above the floor (103961128)
49
+ - fix: vendor the organ that routes on conviction, not a floor count (24d215a5a)
50
+ - fix: reconcile writes the lock version when it bumps (434ddafef)
51
+ - fix: package-lock version follows package.json on stamp (026c36d55)
52
+
53
+ ---
54
+
32
55
  ## [4.0.22] - 2026-09-23
33
56
 
34
57
  ### 🔄 Changes
@@ -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,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;
@@ -169,6 +194,7 @@ export class InferenceCycle {
169
194
  for (const p of proposals.filter((p) => p.status !== "approved")) {
170
195
  p.status = "rejected";
171
196
  }
197
+ this.recordGovernedLessons(cycleId, proposals, votes);
172
198
  if (approved.length > 0) {
173
199
  if (!this.options.skipApply) {
174
200
  this.setPhase("applying");
@@ -215,6 +241,39 @@ export class InferenceCycle {
215
241
  InferenceCycle.reEntryLock = false;
216
242
  }
217
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
+ }
218
277
  async governProposals(proposals) {
219
278
  // Governance MCP is the sole governance path (governance.server.ts + GovernanceService).
220
279
  // It calls individual skill MCPs (code-review, security-audit, researcher) internally
@@ -222,8 +281,9 @@ export class InferenceCycle {
222
281
  const useGovernanceMcp = process.env.XRAY_FORCE_MCP_GOVERNANCE === 'true' ||
223
282
  this.isGovernanceMcpPreferred();
224
283
  if (!useGovernanceMcp) {
225
- throw new Error("Governance MCP is required but not available");
284
+ return this.governWithLocalMatrix(proposals);
226
285
  }
286
+ const governanceMcpTimeoutMs = 90_000;
227
287
  const result = await Promise.race([
228
288
  mcpClientManager.callServerTool("governance", "govern_proposals", {
229
289
  proposals: proposals.map(p => ({
@@ -238,9 +298,12 @@ export class InferenceCycle {
238
298
  context: { source: "inference-cycle" },
239
299
  options: { require_external: true },
240
300
  }),
241
- 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)),
242
302
  ]);
243
303
  const text = governanceToolText(result);
304
+ if (governanceToolIsError(result)) {
305
+ throw new Error(text || "Governance MCP returned an error");
306
+ }
244
307
  const parsed = this.parseGovernanceMcpResponse(text, proposals);
245
308
  frameworkLogger.log("inference-cycle", "governance-mcp-primary-path", "info", {
246
309
  proposalCount: proposals.length,
@@ -248,6 +311,24 @@ export class InferenceCycle {
248
311
  });
249
312
  return parsed.votes;
250
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
+ }
251
332
  isGovernanceMcpPreferred() {
252
333
  try {
253
334
  const config = featuresConfigLoader.loadConfig();
@@ -275,17 +356,12 @@ export class InferenceCycle {
275
356
  return { votes, overallDecision: data.overallDecision || "needs_revision" };
276
357
  }
277
358
  catch {
359
+ const preview = text.substring(0, 300);
278
360
  frameworkLogger.log('inference-cycle', 'governance-mcp-parse-failed', 'warning', {
279
- textPreview: text.substring(0, 200),
361
+ textPreview: preview,
280
362
  proposalCount: proposals.length,
281
363
  });
282
- const votes = proposals.map(p => ({
283
- proposalId: p.id,
284
- decision: "abstain",
285
- confidence: 0.5,
286
- details: ["governance-mcp: parse-failed"],
287
- }));
288
- return { votes, overallDecision: "needs_revision" };
364
+ throw new Error(`governance-mcp: unreadable vote: ${preview || "empty governance response"}`);
289
365
  }
290
366
  }
291
367
  extractTextFromNdjson(output) {
@@ -357,21 +433,87 @@ export class InferenceCycle {
357
433
  }
358
434
  }
359
435
  history.push(result);
360
- if (history.length > 50)
361
- 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);
362
440
  fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
363
441
  }
364
- loadHistory() {
365
- const historyPath = path.join(this.stateDir, CYCLE_HISTORY_FILE);
366
- 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))
367
469
  return [];
368
470
  try {
369
- return JSON.parse(fs.readFileSync(historyPath, "utf-8"));
471
+ return lessonIdsFromUnknown(JSON.parse(fs.readFileSync(file, "utf-8")));
370
472
  }
371
473
  catch {
372
474
  return [];
373
475
  }
374
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
+ }
375
517
  buildBlockedResult(cycleId, reason) {
376
518
  return {
377
519
  cycleId,