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.
- package/.grok-plugin/plugin.json +1 -1
- package/.opencode/skills/orchestrator/SKILL.md +7 -7
- package/.xray/features.json +1 -1
- package/dist/CHANGELOG.md +24 -1
- package/dist/inference/inference-accumulator.js +36 -4
- package/dist/inference/inference-cycle.d.ts +12 -0
- package/dist/inference/inference-cycle.js +158 -16
- package/dist/inference/inference-proposal-generator.js +148 -5
- package/dist/inference/session-capture.d.ts +4 -0
- 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/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 +5 -0
- package/dist/memory-routing/provider-loader.js +46 -0
- package/dist/memory-routing/provider-registry.d.ts +7 -2
- package/dist/memory-routing/provider-registry.js +26 -6
- package/dist/memory-routing/record-lesson.d.ts +16 -0
- package/dist/memory-routing/record-lesson.js +26 -0
- package/dist/memory-routing/types.d.ts +15 -0
- package/dist/nucleus/thin-dispatch.d.ts +2 -0
- package/dist/nucleus/thin-dispatch.js +3 -2
- package/dist/skills/orchestrator/SKILL.md +7 -7
- package/package.json +1 -1
- package/scripts/foundry/reconcile-version.mjs +23 -1
- package/scripts/foundry/release.mjs +1 -1
- package/scripts/foundry/version-manager.mjs +53 -0
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/mcps/governance.server.ts +12 -1
- package/src/skills/orchestrator/SKILL.md +7 -7
- package/vendor/@0xray/repertoire/data/stack-overlay.json +4 -4
- package/vendor/@0xray/repertoire/data/subject-overlay.json +3 -3
- package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts +4 -0
- package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/RepertoireService.js +4 -4
- package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js +5 -0
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -0
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/paths.d.ts +3 -3
- package/vendor/@0xray/repertoire/dist/paths.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/paths.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +11 -0
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +2 -0
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +1 -1
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +54 -4
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +326 -11
- 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/features.json +1 -1
package/.grok-plugin/plugin.json
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
- coordinate_agents
|
|
11
|
+
- delegate_tasks
|
|
12
|
+
- manage_workflow
|
|
13
13
|
dependencies: []
|
|
14
14
|
|
|
15
15
|
mcp:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
|
package/.xray/features.json
CHANGED
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
|
|
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
|
|
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,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
|
-
|
|
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(
|
|
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:
|
|
361
|
+
textPreview: preview,
|
|
280
362
|
proposalCount: proposals.length,
|
|
281
363
|
});
|
|
282
|
-
|
|
283
|
-
proposalId: p.id,
|
|
284
|
-
decision: "abstain",
|
|
285
|
-
confidence: 0.5,
|
|
286
|
-
details: ["governance-mcp: parse-failed"],
|
|
287
|
-
}));
|
|
288
|
-
return { votes, overallDecision: "needs_revision" };
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
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))
|
|
367
469
|
return [];
|
|
368
470
|
try {
|
|
369
|
-
return JSON.parse(fs.readFileSync(
|
|
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,
|