0xray 4.0.38 → 4.0.39

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 (33) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +1 -1
  3. package/AGENTS-consumer.md +1 -1
  4. package/AGENTS.md +1 -1
  5. package/README.md +2 -2
  6. package/SKILLS.md +1 -1
  7. package/dist/AGENTS.md +1 -1
  8. package/dist/CHANGELOG.md +12 -0
  9. package/dist/README.md +2 -2
  10. package/dist/SKILLS.md +1 -1
  11. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  12. package/llms.txt +1 -1
  13. package/package.json +1 -1
  14. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  15. package/scripts/node/consumer-install-smoke.mjs +3 -3
  16. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  17. package/vendor/@0xray/repertoire/README.md +1 -1
  18. package/vendor/@0xray/repertoire/dist/RepertoireService.js +1 -0
  19. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts +5 -0
  20. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -6
  21. package/vendor/@0xray/repertoire/dist/paths.js +0 -4
  22. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +16 -6
  23. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +19 -2
  24. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +34 -2
  25. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.d.ts +3 -0
  26. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.js +25 -1
  27. package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.d.ts +37 -2
  28. package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.js +176 -19
  29. package/vendor/@0xray/repertoire/dist/types.d.ts +6 -0
  30. package/vendor/@0xray/repertoire/package.json +2 -2
  31. package/vendor/@0xray/repertoire/scripts/verify-grok-suit.mjs +9 -6
  32. package/vendor/@0xray/repertoire/scripts/verify-synthesis-dogfood.mjs +109 -5
  33. package/xray/features.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.38",
3
+ "version": "4.0.39",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.38",
3
+ "version": "4.0.39",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",
@@ -6,7 +6,7 @@ Quick reference for the 0xRay AI orchestration framework worn on **{{CONSUMER_NA
6
6
 
7
7
  **4.0** — a suit that survives the context window
8
8
 
9
- Same **three-subsystem OS** as v2 (Inference · External Governance · Autonomous Engine). Constitution always on. Temperament scales ceremony. Four chat bridges plus Cursor project hooks. **Repertoire is preferred** — vendored 0.2.8 ships **on**. Dest is named laws, not keywords. Station is the compact ticket, not a substitute. Explicit opt-out only: `"enabled": false, "provider": "repertoire"`. Trim fat, do not gut the design.
9
+ Same **three-subsystem OS** as v2 (Inference · External Governance · Autonomous Engine). Constitution always on. Temperament scales ceremony. Four chat bridges plus Cursor project hooks. **Repertoire is preferred** — vendored 0.2.9 ships **on**. Dest is named laws, not keywords. Station is the compact ticket, not a substitute. Explicit opt-out only: `"enabled": false, "provider": "repertoire"`. Trim fat, do not gut the design.
10
10
 
11
11
  **Governance always on** (Codex PreToolUse, no new surface, no `eval`). **Engine ceremony** (analyze-complexity before spawn, confer) is **guided** by default so free-model OpenCode/Hermes stay in check. Frontier hosts (Grok 4.6 class) with `suit_temperament.profile: auto` **warn** on spawn-without-plan instead of deny — the engine is still there. Existing `.xray/features.json` without `suit_temperament` stays **guided**. Docs: v3 from v2 · Suit temperament.
12
12
 
package/AGENTS.md CHANGED
@@ -4,7 +4,7 @@ Quick reference for the xray AI orchestration framework (**4.0**).
4
4
 
5
5
  **4.0** — a suit that survives the context window.
6
6
 
7
- **Exo, not catalog.** Three-subsystem OS (Inference · External Governance · Autonomous Engine) · constitution always on · temperament by host · 4 floors · **Repertoire preferred** (vendored 0.2.8, dest = laws).
7
+ **Exo, not catalog.** Three-subsystem OS (Inference · External Governance · Autonomous Engine) · constitution always on · temperament by host · 4 floors · **Repertoire preferred** (vendored 0.2.9, dest = laws).
8
8
 
9
9
  **Temperament:** ceremony (analyze-complexity before spawn, confer) is **guided** by default so free-model OpenCode/Hermes stay in check. Frontier hosts (Grok 4.6 class) with `suit_temperament.profile: auto` **warn** on spawn-without-plan; Codex 11/29/69 still **deny**. Missing `suit_temperament` stays guided. See `docs-site/docs/architecture/v3-from-v2.md`.
10
10
 
package/README.md CHANGED
@@ -14,13 +14,13 @@ Not a catalog of agents. The product is the **skeleton you wear**.
14
14
  - **Always on** — Codex 11 / 29 / 69, destructive shell, no new MCP/skill/handler surface
15
15
  - **Temperament** — `frontier` | `guided` | `strict` | `auto` — how loud the engine is, not whether governance exists
16
16
  - **Wear** — Grok, OpenCode, Hermes, OpenClaw, plus Cursor project hooks. One SSOT gate
17
- - **Muscle** — **Repertoire is preferred.** Vendored 0.2.8 ships **on**. Dest is named laws, not hangar `repo-*` or git slugs. Station is the compact-survival ticket — not a substitute for Repertoire.
17
+ - **Muscle** — **Repertoire is preferred.** Vendored 0.2.9 ships **on**. Dest is named laws, not hangar `repo-*` or git slugs. Station is the compact-survival ticket — not a substitute for Repertoire.
18
18
 
19
19
  ```bash
20
20
  npm install 0xray
21
21
  ```
22
22
 
23
- Consumer `npm install 0xray` auto-wires four chat bridges, Cursor project hooks, and seven MCP servers. **Repertoire is preferred** — vendored **0.2.8** ships on: factory + stack laws, in-process routing, extra host MCP `repertoire`. Not an eighth `xray-*` server. Dest is a judgment index. Explicit opt-out only: `"enabled": false, "provider": "repertoire"`. Do not pin 0.1.8.
23
+ Consumer `npm install 0xray` auto-wires four chat bridges, Cursor project hooks, and seven MCP servers. **Repertoire is preferred** — vendored **0.2.9** ships on: factory + stack laws, in-process routing, extra host MCP `repertoire`. Not an eighth `xray-*` server. Dest is a judgment index. Explicit opt-out only: `"enabled": false, "provider": "repertoire"`. Do not pin 0.1.8.
24
24
 
25
25
  ## Quick Start
26
26
 
package/SKILLS.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **45 skills** in this exo catalog. Default consumer plant is **`mill` + `inspect`**, not this dump. `npm install 0xray` does **not** sync 45 skills unless `foundry.json` `"costume": true`. Factory shop plant (`shop-extract`, `shop-witness`, `shop-pin`) coexists with mill plant when a hangar wears those shops. Extra shops: `foundry.json` `shopPlant`. Fasten is **foundry-plant/0**: builtin plant is mill+inspect only. `foundry.json` `"plant": "@0xray/blip"` (aliases `"plant": "blip"` / `"plant": "sound"`) fastens whatever that mill package declares — organs (looker, vibe, mixer) live in the mill, not this catalog. Nested mill `@0xray/foundry@0.1.12`. CLI shims `foundry blip|sound` still exist on foundry 0.1.11+. Agent map: [llms.txt](llms.txt).
6
6
 
7
- xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) as the **costume / self-plant catalog**. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. **Repertoire is preferred** for long-term judgment (vendored 0.2.8, dest = named laws). Station is the compact ticket, not a skill and not a keyword store. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`) or when you plant them. This file is the exo catalog — mill does **not** write it to the consumer root.
7
+ xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) as the **costume / self-plant catalog**. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. **Repertoire is preferred** for long-term judgment (vendored 0.2.9, dest = named laws). Station is the compact ticket, not a skill and not a keyword store. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`) or when you plant them. This file is the exo catalog — mill does **not** write it to the consumer root.
8
8
 
9
9
  ---
10
10
 
package/dist/AGENTS.md CHANGED
@@ -4,7 +4,7 @@ Quick reference for the xray AI orchestration framework (**4.0**).
4
4
 
5
5
  **4.0** — a suit that survives the context window.
6
6
 
7
- **Exo, not catalog.** Three-subsystem OS (Inference · External Governance · Autonomous Engine) · constitution always on · temperament by host · 4 floors · **Repertoire preferred** (vendored 0.2.8, dest = laws).
7
+ **Exo, not catalog.** Three-subsystem OS (Inference · External Governance · Autonomous Engine) · constitution always on · temperament by host · 4 floors · **Repertoire preferred** (vendored 0.2.9, dest = laws).
8
8
 
9
9
  **Temperament:** ceremony (analyze-complexity before spawn, confer) is **guided** by default so free-model OpenCode/Hermes stay in check. Frontier hosts (Grok 4.6 class) with `suit_temperament.profile: auto` **warn** on spawn-without-plan; Codex 11/29/69 still **deny**. Missing `suit_temperament` stays guided. See `docs-site/docs/architecture/v3-from-v2.md`.
10
10
 
package/dist/CHANGELOG.md CHANGED
@@ -42,6 +42,18 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
42
42
  - Archive root `integrations/` costume SKILL dump → `docs/archive/integrations-costume/`. Live wear is `src/integrations`.
43
43
  - Archive root `commands/` stale copy → `docs/archive/commands-copy/`. Live wear is `src/opencode/commands`.
44
44
 
45
+ ## [4.0.39] - 2026-10-03
46
+
47
+ ### 🔄 Changes
48
+
49
+ ### 🔎 Other Changes
50
+ - Keep the seven stack laws already worn in the suit. (427b0538e)
51
+ - Vendor repertoire 0.2.9. (b45ed22f9)
52
+ - Merge pull request #209 from 0xRayAI/cursor/evidence-before-grade-d703 (92a4d6dd9)
53
+ - house: add Evidence before grade rules under §3 (0fa229981)
54
+
55
+ ---
56
+
45
57
  ## [4.0.38] - 2026-10-03
46
58
 
47
59
  ### 🔄 Changes
package/dist/README.md CHANGED
@@ -14,13 +14,13 @@ Not a catalog of agents. The product is the **skeleton you wear**.
14
14
  - **Always on** — Codex 11 / 29 / 69, destructive shell, no new MCP/skill/handler surface
15
15
  - **Temperament** — `frontier` | `guided` | `strict` | `auto` — how loud the engine is, not whether governance exists
16
16
  - **Wear** — Grok, OpenCode, Hermes, OpenClaw, plus Cursor project hooks. One SSOT gate
17
- - **Muscle** — **Repertoire is preferred.** Vendored 0.2.8 ships **on**. Dest is named laws, not hangar `repo-*` or git slugs. Station is the compact-survival ticket — not a substitute for Repertoire.
17
+ - **Muscle** — **Repertoire is preferred.** Vendored 0.2.9 ships **on**. Dest is named laws, not hangar `repo-*` or git slugs. Station is the compact-survival ticket — not a substitute for Repertoire.
18
18
 
19
19
  ```bash
20
20
  npm install 0xray
21
21
  ```
22
22
 
23
- Consumer `npm install 0xray` auto-wires four chat bridges, Cursor project hooks, and seven MCP servers. **Repertoire is preferred** — vendored **0.2.8** ships on: factory + stack laws, in-process routing, extra host MCP `repertoire`. Not an eighth `xray-*` server. Dest is a judgment index. Explicit opt-out only: `"enabled": false, "provider": "repertoire"`. Do not pin 0.1.8.
23
+ Consumer `npm install 0xray` auto-wires four chat bridges, Cursor project hooks, and seven MCP servers. **Repertoire is preferred** — vendored **0.2.9** ships on: factory + stack laws, in-process routing, extra host MCP `repertoire`. Not an eighth `xray-*` server. Dest is a judgment index. Explicit opt-out only: `"enabled": false, "provider": "repertoire"`. Do not pin 0.1.8.
24
24
 
25
25
  ## Quick Start
26
26
 
package/dist/SKILLS.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **45 skills** in this exo catalog. Default consumer plant is **`mill` + `inspect`**, not this dump. `npm install 0xray` does **not** sync 45 skills unless `foundry.json` `"costume": true`. Factory shop plant (`shop-extract`, `shop-witness`, `shop-pin`) coexists with mill plant when a hangar wears those shops. Extra shops: `foundry.json` `shopPlant`. Fasten is **foundry-plant/0**: builtin plant is mill+inspect only. `foundry.json` `"plant": "@0xray/blip"` (aliases `"plant": "blip"` / `"plant": "sound"`) fastens whatever that mill package declares — organs (looker, vibe, mixer) live in the mill, not this catalog. Nested mill `@0xray/foundry@0.1.12`. CLI shims `foundry blip|sound` still exist on foundry 0.1.11+. Agent map: [llms.txt](llms.txt).
6
6
 
7
- xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) as the **costume / self-plant catalog**. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. **Repertoire is preferred** for long-term judgment (vendored 0.2.8, dest = named laws). Station is the compact ticket, not a skill and not a keyword store. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`) or when you plant them. This file is the exo catalog — mill does **not** write it to the consumer root.
7
+ xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) as the **costume / self-plant catalog**. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. **Repertoire is preferred** for long-term judgment (vendored 0.2.9, dest = named laws). Station is the compact ticket, not a skill and not a keyword store. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`) or when you plant them. This file is the exo catalog — mill does **not** write it to the consumer root.
8
8
 
9
9
  ---
10
10
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.38",
3
+ "version": "4.0.39",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
package/llms.txt CHANGED
@@ -41,7 +41,7 @@ Registered in `.mcp.json` as `npx -y 0xray mcp <cmd>`:
41
41
  | `xray-code-review` | Proposal quality |
42
42
  | `xray-architect-tools` | System design |
43
43
 
44
- Not an 8th `xray-*` server. **Repertoire is preferred** — vendored 0.2.8 ships on (`npx @0xray/repertoire mcp`). Dest is named laws, not hangar `repo-*` or git slugs. Survive compact is **Station** (Read `.xray/state/STATION.md`). Station is the ticket, not a substitute for Repertoire. Do not pin 0.1.8.
44
+ Not an 8th `xray-*` server. **Repertoire is preferred** — vendored 0.2.9 ships on (`npx @0xray/repertoire mcp`). Dest is named laws, not hangar `repo-*` or git slugs. Survive compact is **Station** (Read `.xray/state/STATION.md`). Station is the ticket, not a substitute for Repertoire. Do not pin 0.1.8.
45
45
 
46
46
  ## Codex 69 (always on)
47
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.38",
3
+ "version": "4.0.39",
4
4
  "description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -11,7 +11,7 @@ The mill **fastens** a suit. Default mill plant is **inspect** (inspecting AI wo
11
11
 
12
12
  **Mill-fill:** their keys/files win. Mill plant names they did not overlay stay. JSON always merges.
13
13
 
14
- **Factory:** **Repertoire is preferred** (vendored 0.2.8 ships on; dest = named laws). Seven generated MCP. Constitution (PPE) stays worn. Not a fifth MCP. Not 5.0.
14
+ **Factory:** **Repertoire is preferred** (vendored 0.2.9 ships on; dest = named laws). Seven generated MCP. Constitution (PPE) stays worn. Not a fifth MCP. Not 5.0.
15
15
 
16
16
  **Costume:** `foundry.json` `"costume": true` copies the 45/42 mill costume.
17
17
 
@@ -90,7 +90,7 @@ function assertOrganRequire(tmpRoot) {
90
90
  if (organPkg.name !== "@0xray/repertoire") {
91
91
  throw new Error(`required organ name ${organPkg.name}`);
92
92
  }
93
- if (organPkg.version !== "0.2.8") {
93
+ if (organPkg.version !== "0.2.9") {
94
94
  throw new Error(`required organ version ${organPkg.version}`);
95
95
  }
96
96
  if (fs.existsSync(path.join(tmpRoot, "vendor"))) {
@@ -163,8 +163,8 @@ function assertFreshInstallDefaults(tmpRoot, version) {
163
163
  throw new Error("vendored @0xray/repertoire missing from fresh tarball install");
164
164
  }
165
165
  const organ = JSON.parse(fs.readFileSync(organPkg, "utf-8"));
166
- if (organ.version !== "0.2.8") {
167
- throw new Error(`expected organ 0.2.8, got ${organ.version}`);
166
+ if (organ.version !== "0.2.9") {
167
+ throw new Error(`expected organ 0.2.9, got ${organ.version}`);
168
168
  }
169
169
  console.log(" ✅ .xray/features.json fresh-install defaults (opt-in off)");
170
170
  return { consumerFeaturesPath, features };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.38",
3
+ "version": "4.0.39",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -2,7 +2,7 @@
2
2
 
3
3
  Factory memory organ for [0xRay](https://github.com/0xRayAI/xray): seed primitives, in-process memory routing, extra MCP (not an 8th `xray-*` server).
4
4
 
5
- **v0.2.8** · 0xRay **4.0** wear. Runtime does **not** depend on `0xray` (optional peer for suit-verify scripts).
5
+ **v0.2.9** · 0xRay **4.0** wear. Runtime does **not** depend on `0xray` (optional peer for suit-verify scripts).
6
6
 
7
7
  ## Install
8
8
 
@@ -32,6 +32,7 @@ export class RepertoireService {
32
32
  this.metaInference = new MetaInferenceEngine({
33
33
  logDir: this.logDir,
34
34
  statePath: options.statePath ?? writable.statePath,
35
+ reportPath: join(this.projectRoot, 'logs', 'meta-inference', 'synthesis.md'),
35
36
  });
36
37
  this.feedbackIngester = new OrchestratorFeedbackIngester(options.feedbackDir ?? writable.feedbackDir);
37
38
  if (shouldAutoSyncField(options.syncField)) {
@@ -24,6 +24,11 @@ export declare class SignalInjector {
24
24
  */
25
25
  adjustComplexityScore(baseScore: number, context: RepertoireRoutingContext, confidenceContext?: ReturnType<typeof getConfidenceForTask>): number;
26
26
  private resolveSynthesisReportPath;
27
+ /**
28
+ * A dry or UNREVIEWED report is not inference. It must not become an excerpt
29
+ * or a routing flag.
30
+ */
31
+ private consumableSynthesisPath;
27
32
  private getSynthesisExcerpt;
28
33
  private readCodexExcerpt;
29
34
  private readPlanExcerpt;
@@ -177,13 +177,23 @@ export class SignalInjector {
177
177
  const custom = this.synthesisReportPath.startsWith('/')
178
178
  ? this.synthesisReportPath
179
179
  : join(this.projectRoot, this.synthesisReportPath);
180
- return existsSync(custom) ? custom : null;
180
+ return this.consumableSynthesisPath(custom);
181
181
  }
182
- const candidates = [
183
- join(this.projectRoot, 'logs/meta-inference/synthesis.md'),
184
- join(this.projectRoot, 'logs/meta-inference/dry-synthesis.md'),
185
- ];
186
- return candidates.find((candidate) => existsSync(candidate)) ?? null;
182
+ return this.consumableSynthesisPath(join(this.projectRoot, 'logs/meta-inference/synthesis.md'));
183
+ }
184
+ /**
185
+ * A dry or UNREVIEWED report is not inference. It must not become an excerpt
186
+ * or a routing flag.
187
+ */
188
+ consumableSynthesisPath(candidate) {
189
+ if (!existsSync(candidate))
190
+ return null;
191
+ const content = readFileSync(candidate, 'utf8');
192
+ if (content.trim().length === 0)
193
+ return null;
194
+ if (/\bUNREVIEWED\b/.test(content))
195
+ return null;
196
+ return candidate;
187
197
  }
188
198
  getSynthesisExcerpt(maxChars) {
189
199
  const reportPath = this.resolveSynthesisReportPath();
@@ -421,10 +421,6 @@ export function reloadOpProc(cwd = process.cwd()) {
421
421
  }
422
422
  function mergeProjectOverlays(destPath) {
423
423
  mergeStackOverlay(destPath);
424
- // Suit heat keeps repo-* off the project file. A wake that merges them
425
- // and then prunes them records a fake 59 → 45 drop on every compact.
426
- if (process.env.REPERTOIRE_SUBJECT_OVERLAY === '0')
427
- return;
428
424
  mergeSubjectOverlay(destPath);
429
425
  }
430
426
  export function hydrateWritableSignals(seedPath, cwd = process.cwd()) {
@@ -4,6 +4,7 @@
4
4
  * Loaded dynamically by 0xRay via features.json memory_routing.module_path.
5
5
  * Other providers can follow the same createMemoryRoutingProvider() export pattern.
6
6
  */
7
+ import { type MetaInferenceFailureReason, type MetaInferenceModel } from '../synthesis/meta-inference-engine.js';
7
8
  /** Mirrors 0xRay memory-routing/types.ts — kept local to avoid compile-time coupling */
8
9
  export interface MemoryAgentCapability {
9
10
  capabilities: string[];
@@ -80,6 +81,14 @@ export interface OrchestratorFeedbackEntry {
80
81
  dynamoResult?: Record<string, unknown>;
81
82
  lesson?: string;
82
83
  }
84
+ export type MetaInferenceRefreshReason = 'synthesized' | 'nothing_to_process' | MetaInferenceFailureReason;
85
+ export interface MetaInferenceRefreshResult {
86
+ refreshed: boolean;
87
+ /** Machine-readable outcome. A failed model is never `synthesized`. */
88
+ reason: MetaInferenceRefreshReason;
89
+ /** Present when the model was missing or the call failed. */
90
+ error?: string;
91
+ }
83
92
  export interface MemoryRoutingProviderConfig {
84
93
  dataDir?: string;
85
94
  signalsPath?: string;
@@ -87,6 +96,11 @@ export interface MemoryRoutingProviderConfig {
87
96
  statePath?: string;
88
97
  feedbackDir?: string;
89
98
  projectRoot?: string;
99
+ /**
100
+ * Meta-inference model. `null` means none is configured (refresh fails closed).
101
+ * Omit to keep the engine's hermes CLI default.
102
+ */
103
+ hermesCommand?: MetaInferenceModel | null;
90
104
  }
91
105
  export interface MemoryRoutingProvider {
92
106
  readonly id: string;
@@ -104,9 +118,7 @@ export interface MemoryRoutingProvider {
104
118
  projectRoot: string;
105
119
  dueReason?: string | null;
106
120
  }): Record<string, unknown> | null;
107
- refreshMetaInference?(): Promise<{
108
- refreshed: boolean;
109
- }>;
121
+ refreshMetaInference?(): Promise<MetaInferenceRefreshResult>;
110
122
  }
111
123
  export type ProviderAvailabilityReason = 'ok' | 'empty_registry' | 'signals_unreadable' | 'signals_missing';
112
124
  export interface ProviderAvailabilityStatus {
@@ -143,9 +155,7 @@ export declare class RepertoireMemoryRoutingProvider implements MemoryRoutingPro
143
155
  projectRoot: string;
144
156
  dueReason?: string | null;
145
157
  }): Record<string, unknown> | null;
146
- refreshMetaInference(): Promise<{
147
- refreshed: boolean;
148
- }>;
158
+ refreshMetaInference(): Promise<MetaInferenceRefreshResult>;
149
159
  }
150
160
  /** Factory export — 0xRay provider-loader calls this by name */
151
161
  export declare function createMemoryRoutingProvider(config?: MemoryRoutingProviderConfig): MemoryRoutingProvider;
@@ -7,6 +7,7 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import { DEFAULT_SIGNALS_PATH, defaultWritablePaths, hydrateWritableSignals, resolveReadableConfigPath, resolveWritableConfigPath, } from '../paths.js';
9
9
  import { RepertoireService } from '../RepertoireService.js';
10
+ import { MetaInferenceModelError, } from '../synthesis/meta-inference-engine.js';
10
11
  function toRepertoireCaps(caps) {
11
12
  return {
12
13
  capabilities: caps.capabilities,
@@ -71,6 +72,9 @@ export class RepertoireMemoryRoutingProvider {
71
72
  logDir: resolveWritableConfigPath(config.logDir, cwd, writable.logDir),
72
73
  feedbackDir: resolveWritableConfigPath(config.feedbackDir, cwd, writable.feedbackDir),
73
74
  });
75
+ if (config.hermesCommand !== undefined) {
76
+ this.service.metaInference.configureModel(config.hermesCommand);
77
+ }
74
78
  }
75
79
  getAvailabilityStatus() {
76
80
  try {
@@ -199,8 +203,21 @@ export class RepertoireMemoryRoutingProvider {
199
203
  return this.service.buildSynthesisContext(opts.projectRoot, opts.dueReason ?? null);
200
204
  }
201
205
  async refreshMetaInference() {
202
- const report = await this.service.runMetaInference();
203
- return { refreshed: report !== null };
206
+ try {
207
+ const report = await this.service.runMetaInference();
208
+ if (report === null) {
209
+ return { refreshed: false, reason: 'nothing_to_process' };
210
+ }
211
+ return { refreshed: true, reason: 'synthesized' };
212
+ }
213
+ catch (error) {
214
+ if (error instanceof MetaInferenceModelError) {
215
+ return { refreshed: false, reason: error.reason, error: error.message };
216
+ }
217
+ const message = error instanceof Error ? error.message : String(error);
218
+ process.stderr.write(`[meta-inference] state_error: ${message}\n`);
219
+ return { refreshed: false, reason: 'state_error', error: message };
220
+ }
204
221
  }
205
222
  }
206
223
  /** Factory export — 0xRay provider-loader calls this by name */
@@ -150,6 +150,38 @@ function isSpeechMint(signal) {
150
150
  return signal.status === 'proposed' && signal.tags.includes('learned');
151
151
  }
152
152
  const FIELD_PRIMITIVE_NAME = /^[A-Za-z][A-Za-z0-9_-]{2,119}$/;
153
+ /** A content word after a spaced law extends the id. A function word is the sentence. */
154
+ const PROSE_TAIL = new Set([
155
+ 'a', 'an', 'the', 'as', 'and', 'or', 'but', 'if', 'of', 'to', 'for', 'in', 'on', 'at', 'by',
156
+ 'with', 'from', 'into', 'over', 'after', 'before', 'about', 'than', 'then', 'that', 'this',
157
+ 'these', 'those', 'it', 'its', 'is', 'are', 'was', 'were', 'be', 'been', 'not', 'no', 'so',
158
+ 'when', 'while', 'where', 'which', 'who', 'what', 'how', 'also', 'only', 'just', 'still',
159
+ 'never', 'today',
160
+ ]);
161
+ /** A longer id does not count. Spaced prose may continue after a hyphenated id. */
162
+ function boundedId(text, id, spaced) {
163
+ if (!id)
164
+ return false;
165
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
166
+ const re = new RegExp(`(?:^|[^a-z0-9-])${escaped}`, 'g');
167
+ let match;
168
+ while ((match = re.exec(text)) !== null) {
169
+ const after = text.slice(match.index + match[0].length);
170
+ if (spaced ? spacedTailIsProse(after) : !/^[a-z0-9-]/.test(after))
171
+ return true;
172
+ if (match.index === re.lastIndex)
173
+ re.lastIndex += 1;
174
+ }
175
+ return false;
176
+ }
177
+ function spacedTailIsProse(after) {
178
+ if (/^-[a-z0-9]/.test(after) || /^[a-z0-9]/.test(after))
179
+ return false;
180
+ const word = /^\s*([a-z0-9]+)/.exec(after);
181
+ if (!word || !/^\s/.test(after))
182
+ return true;
183
+ return PROSE_TAIL.has(word[1]);
184
+ }
153
185
  /**
154
186
  * The diary names a law only when it contains the signal id, or the id with
155
187
  * hyphens read as spaces. A repo tail, leftover tokens, and two definition
@@ -160,10 +192,10 @@ export function signalNameInText(text, name) {
160
192
  const id = name.toLowerCase();
161
193
  if (!id)
162
194
  return false;
163
- if (normalized.includes(id))
195
+ if (boundedId(normalized, id, false))
164
196
  return true;
165
197
  const spaced = id.replace(/-/g, ' ');
166
- return spaced !== id && normalized.includes(spaced);
198
+ return spaced !== id && boundedId(normalized, spaced, true);
167
199
  }
168
200
  const GROOVER_EXPERIMENT_NAMES = new Set([
169
201
  'criteria_selection_gap',
@@ -3,6 +3,9 @@ export declare class InferenceStateManager {
3
3
  private readonly filePath;
4
4
  constructor(filePath?: string);
5
5
  load(): InferenceState;
6
+ /** Remember a failed model attempt without marking entries processed or touching lastRun. */
7
+ recordModelFailure(failedAt: string): void;
8
+ clearModelBackoff(): void;
6
9
  save(state: InferenceState): void;
7
10
  isProcessed(id: string): boolean;
8
11
  markProcessed(ids: string[], kind?: 'comment' | 'session' | 'post'): void;
@@ -10,12 +10,30 @@ export class InferenceStateManager {
10
10
  return this.createEmpty();
11
11
  }
12
12
  const raw = JSON.parse(readFileSync(this.filePath, 'utf8'));
13
- return {
13
+ const state = {
14
14
  processedCommentIds: raw.processedCommentIds ?? [],
15
15
  processedSessionIds: raw.processedSessionIds ?? [],
16
16
  processedPostIds: raw.processedPostIds ?? [],
17
17
  lastRun: raw.lastRun ?? null,
18
18
  };
19
+ const backoff = readBackoff(raw.modelBackoff);
20
+ if (backoff)
21
+ state.modelBackoff = backoff;
22
+ return state;
23
+ }
24
+ /** Remember a failed model attempt without marking entries processed or touching lastRun. */
25
+ recordModelFailure(failedAt) {
26
+ const state = this.load();
27
+ const failures = (state.modelBackoff?.failures ?? 0) + 1;
28
+ state.modelBackoff = { failedAt, failures };
29
+ this.save(state);
30
+ }
31
+ clearModelBackoff() {
32
+ const state = this.load();
33
+ if (!state.modelBackoff)
34
+ return;
35
+ delete state.modelBackoff;
36
+ this.save(state);
19
37
  }
20
38
  save(state) {
21
39
  const dir = dirname(this.filePath);
@@ -58,4 +76,10 @@ export class InferenceStateManager {
58
76
  };
59
77
  }
60
78
  }
79
+ function readBackoff(value) {
80
+ if (!value || typeof value.failedAt !== 'string')
81
+ return null;
82
+ const failures = typeof value.failures === 'number' && value.failures > 0 ? value.failures : 1;
83
+ return { failedAt: value.failedAt, failures };
84
+ }
61
85
  //# sourceMappingURL=InferenceStateManager.js.map
@@ -1,11 +1,32 @@
1
1
  import type { SynthesisReport } from '../types.js';
2
+ export type MetaInferenceModel = (prompt: string) => unknown;
3
+ export type MetaInferenceFailureReason = 'model_unavailable' | 'model_call_failed' | 'model_output_invalid' | 'model_backoff' | 'state_error';
4
+ /** First wait after a failed model attempt. Doubles until MODEL_BACKOFF_CAP_MS. */
5
+ export declare const MODEL_BACKOFF_BASE_MS: number;
6
+ /** Upper bound so a dead hermes CLI cannot push the wait without limit. */
7
+ export declare const MODEL_BACKOFF_CAP_MS: number;
8
+ export declare function modelBackoffDelayMs(failures: number): number;
9
+ export declare function formatMetaInferenceFailure(error: unknown): string;
10
+ /** Raised when synthesis cannot be produced by a model. Callers must not treat it as a report. */
11
+ export declare class MetaInferenceModelError extends Error {
12
+ readonly reason: MetaInferenceFailureReason;
13
+ constructor(reason: MetaInferenceFailureReason, message: string, options?: {
14
+ cause?: unknown;
15
+ });
16
+ }
2
17
  export interface MetaInferenceEngineOptions {
3
18
  logDir?: string;
4
19
  statePath?: string;
5
20
  reportPath?: string;
6
21
  batchSize?: number;
7
22
  maxEntries?: number;
8
- hermesCommand?: (prompt: string) => string;
23
+ /**
24
+ * Model call. Omit to use the hermes CLI.
25
+ * `null` means no model is configured — the run fails closed and does not call out.
26
+ */
27
+ hermesCommand?: MetaInferenceModel | null;
28
+ /** Pinned in tests. Defaults to the system clock. */
29
+ now?: () => Date;
9
30
  }
10
31
  export declare class MetaInferenceEngine {
11
32
  private readonly logDir;
@@ -14,10 +35,24 @@ export declare class MetaInferenceEngine {
14
35
  private readonly batchSize;
15
36
  private readonly maxEntries;
16
37
  private readonly promptBuilder;
17
- private readonly runHermes;
38
+ private readonly now;
39
+ private model;
18
40
  constructor(options?: MetaInferenceEngineOptions);
41
+ /** `null` configures no model. The next run fails closed instead of calling the hermes CLI. */
42
+ configureModel(command: MetaInferenceModel | null): void;
19
43
  run(): Promise<SynthesisReport | null>;
20
44
  private loadUnprocessedEntries;
45
+ private loadState;
46
+ private throwIfBackingOff;
47
+ /**
48
+ * A missing, failed, or junk model result must not produce a synthesis report.
49
+ * Entries stay unprocessed so a later run can retry them.
50
+ */
51
+ private callModelOrRecord;
52
+ private callModel;
53
+ private noteModelFailure;
54
+ private fail;
55
+ private logFailure;
21
56
  private appendReport;
22
57
  private defaultHermesCommand;
23
58
  }
@@ -3,6 +3,31 @@ import { join, dirname } from 'node:path';
3
3
  import { execSync } from 'node:child_process';
4
4
  import { SynthesisPromptBuilder } from './synthesis-prompt-builder.js';
5
5
  import { InferenceStateManager } from '../registry/InferenceStateManager.js';
6
+ /** First wait after a failed model attempt. Doubles until MODEL_BACKOFF_CAP_MS. */
7
+ export const MODEL_BACKOFF_BASE_MS = 15 * 60 * 1000;
8
+ /** Upper bound so a dead hermes CLI cannot push the wait without limit. */
9
+ export const MODEL_BACKOFF_CAP_MS = 4 * 60 * 60 * 1000;
10
+ export function modelBackoffDelayMs(failures) {
11
+ const exponent = Math.max(0, Math.min(failures, 16) - 1);
12
+ const doubled = MODEL_BACKOFF_BASE_MS * 2 ** exponent;
13
+ return Math.min(doubled, MODEL_BACKOFF_CAP_MS);
14
+ }
15
+ export function formatMetaInferenceFailure(error) {
16
+ if (error instanceof MetaInferenceModelError) {
17
+ return oneLine(`meta-inference failed: ${error.reason}: ${error.message}`);
18
+ }
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ return oneLine(`meta-inference failed: state_error: ${message}`);
21
+ }
22
+ /** Raised when synthesis cannot be produced by a model. Callers must not treat it as a report. */
23
+ export class MetaInferenceModelError extends Error {
24
+ reason;
25
+ constructor(reason, message, options) {
26
+ super(message, options);
27
+ this.name = 'MetaInferenceModelError';
28
+ this.reason = reason;
29
+ }
30
+ }
6
31
  const DEFAULT_BATCH_SIZE = 1;
7
32
  const DEFAULT_MAX_ENTRIES = 8;
8
33
  export class MetaInferenceEngine {
@@ -12,17 +37,26 @@ export class MetaInferenceEngine {
12
37
  batchSize;
13
38
  maxEntries;
14
39
  promptBuilder = new SynthesisPromptBuilder();
15
- runHermes;
40
+ now;
41
+ model;
16
42
  constructor(options = {}) {
17
43
  this.logDir = options.logDir ?? 'logs/groover-inference';
18
44
  this.stateManager = new InferenceStateManager(options.statePath ?? 'data/inference-state.json');
19
45
  this.reportPath = options.reportPath ?? 'logs/meta-inference/synthesis.md';
20
46
  this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
21
47
  this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
22
- this.runHermes = options.hermesCommand ?? this.defaultHermesCommand;
48
+ this.now = options.now ?? (() => new Date());
49
+ this.model =
50
+ options.hermesCommand === null
51
+ ? null
52
+ : (options.hermesCommand ?? ((prompt) => this.defaultHermesCommand(prompt)));
53
+ }
54
+ /** `null` configures no model. The next run fails closed instead of calling the hermes CLI. */
55
+ configureModel(command) {
56
+ this.model = command;
23
57
  }
24
58
  async run() {
25
- const state = this.stateManager.load();
59
+ const state = this.loadState();
26
60
  const processed = new Set([
27
61
  ...state.processedCommentIds,
28
62
  ...state.processedSessionIds,
@@ -36,6 +70,7 @@ export class MetaInferenceEngine {
36
70
  return null;
37
71
  }
38
72
  const entries = newEntries.slice(0, this.maxEntries);
73
+ this.throwIfBackingOff(state);
39
74
  const batchResults = [];
40
75
  let totalPass = 0;
41
76
  let totalReject = 0;
@@ -68,23 +103,13 @@ export class MetaInferenceEngine {
68
103
  analyzedSoFar: i + batch.length,
69
104
  },
70
105
  });
71
- try {
72
- batchResults.push(this.runHermes(prompt));
73
- }
74
- catch {
75
- // Continue with remaining batches
76
- }
106
+ batchResults.push(this.callModelOrRecord(prompt));
77
107
  }
78
108
  const avgResonance = resonanceCount > 0 ? (resonanceSum / resonanceCount).toFixed(3) : 'N/A';
79
109
  const finalPrompt = this.promptBuilder.buildFinalSynthesisPrompt(entries.length, { pass: totalPass, reject: totalReject, avgResonance }, batchResults, entries);
80
- let finalReport = '';
81
- try {
82
- finalReport = this.runHermes(finalPrompt);
83
- this.appendReport(entries.length, totalPass, resonanceCount > 0 ? resonanceSum / resonanceCount : null, finalReport);
84
- }
85
- catch {
86
- finalReport = batchResults.join('\n\n---\n\n');
87
- }
110
+ const finalReport = this.callModelOrRecord(finalPrompt);
111
+ this.stateManager.clearModelBackoff();
112
+ this.appendReport(entries.length, totalPass, resonanceCount > 0 ? resonanceSum / resonanceCount : null, finalReport);
88
113
  const ids = entries.map((e) => e.comment_id ?? e.post_id ?? e.session_id).filter(Boolean);
89
114
  const hasSession = entries.some((e) => e.session_id);
90
115
  this.stateManager.markProcessed(ids, hasSession ? 'session' : 'comment');
@@ -118,13 +143,96 @@ export class MetaInferenceEngine {
118
143
  processed.add(id);
119
144
  }
120
145
  }
121
- catch {
122
- // skip malformed
146
+ catch (error) {
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ this.logFailure('malformed_log_line', `skipped malformed log line in ${file}: ${message}`);
123
149
  }
124
150
  }
125
151
  }
126
152
  return entries;
127
153
  }
154
+ loadState() {
155
+ try {
156
+ return this.stateManager.load();
157
+ }
158
+ catch (error) {
159
+ throw this.fail('state_error', `meta-inference state could not be read: ${errorText(error)}`, error);
160
+ }
161
+ }
162
+ throwIfBackingOff(state) {
163
+ const backoff = state.modelBackoff;
164
+ if (!backoff)
165
+ return;
166
+ const failedAt = Date.parse(backoff.failedAt);
167
+ if (Number.isNaN(failedAt))
168
+ return;
169
+ const wait = modelBackoffDelayMs(backoff.failures);
170
+ if (this.now().getTime() - failedAt < wait) {
171
+ throw this.fail('model_backoff', `meta-inference model call skipped until backoff ends (${wait}ms)`);
172
+ }
173
+ }
174
+ /**
175
+ * A missing, failed, or junk model result must not produce a synthesis report.
176
+ * Entries stay unprocessed so a later run can retry them.
177
+ */
178
+ callModelOrRecord(prompt) {
179
+ try {
180
+ return this.callModel(prompt);
181
+ }
182
+ catch (error) {
183
+ this.noteModelFailure(error);
184
+ throw error;
185
+ }
186
+ }
187
+ callModel(prompt) {
188
+ if (this.model === null) {
189
+ throw this.fail('model_unavailable', 'meta-inference model is not configured');
190
+ }
191
+ let output;
192
+ try {
193
+ output = this.model(prompt);
194
+ }
195
+ catch (error) {
196
+ if (error instanceof MetaInferenceModelError) {
197
+ this.logFailure(error.reason, error.message);
198
+ throw error;
199
+ }
200
+ if (isMissingHermes(error)) {
201
+ throw this.fail('model_unavailable', 'meta-inference model is not available: hermes CLI not found', error);
202
+ }
203
+ throw this.fail('model_call_failed', `meta-inference model call failed: ${errorText(error)}`, error);
204
+ }
205
+ try {
206
+ return acceptModelOutput(output);
207
+ }
208
+ catch (error) {
209
+ if (error instanceof MetaInferenceModelError) {
210
+ this.logFailure(error.reason, error.message);
211
+ throw error;
212
+ }
213
+ throw error;
214
+ }
215
+ }
216
+ noteModelFailure(error) {
217
+ if (!(error instanceof MetaInferenceModelError))
218
+ return;
219
+ if (error.reason === 'model_backoff' || error.reason === 'state_error')
220
+ return;
221
+ try {
222
+ this.stateManager.recordModelFailure(this.now().toISOString());
223
+ }
224
+ catch (recordError) {
225
+ this.logFailure('state_error', `could not record model backoff: ${errorText(recordError)}`);
226
+ }
227
+ }
228
+ fail(reason, message, cause) {
229
+ const error = new MetaInferenceModelError(reason, message, cause === undefined ? undefined : { cause });
230
+ this.logFailure(reason, message);
231
+ return error;
232
+ }
233
+ logFailure(reason, message) {
234
+ process.stderr.write(`[meta-inference] ${reason}: ${message}\n`);
235
+ }
128
236
  appendReport(entryCount, passCount, avgResonance, report) {
129
237
  const dir = dirname(this.reportPath);
130
238
  if (!existsSync(dir))
@@ -146,4 +254,53 @@ export class MetaInferenceEngine {
146
254
  }).trim();
147
255
  }
148
256
  }
257
+ function oneLine(value) {
258
+ return value.replace(/\s+/g, ' ').trim();
259
+ }
260
+ function errorText(error) {
261
+ if (error instanceof Error)
262
+ return error.message;
263
+ return String(error);
264
+ }
265
+ function isMissingHermes(error) {
266
+ if (!error || typeof error !== 'object')
267
+ return false;
268
+ const record = error;
269
+ if (record.code === 'ENOENT')
270
+ return true;
271
+ return typeof record.message === 'string' && record.message.includes('ENOENT');
272
+ }
273
+ function isThenable(value) {
274
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function'))
275
+ return false;
276
+ return typeof value.then === 'function';
277
+ }
278
+ /**
279
+ * Synthesis text is a non-empty string. A value shaped like JSON must parse.
280
+ * Anything else is not a model result.
281
+ */
282
+ function acceptModelOutput(value) {
283
+ if (isThenable(value)) {
284
+ throw new MetaInferenceModelError('model_output_invalid', 'meta-inference model returned a Promise');
285
+ }
286
+ if (value === undefined || value === null) {
287
+ throw new MetaInferenceModelError('model_output_invalid', 'meta-inference model returned empty output');
288
+ }
289
+ if (typeof value !== 'string') {
290
+ throw new MetaInferenceModelError('model_output_invalid', 'meta-inference model returned a non-string');
291
+ }
292
+ const trimmed = value.trim();
293
+ if (trimmed.length === 0) {
294
+ throw new MetaInferenceModelError('model_output_invalid', 'meta-inference model returned empty output');
295
+ }
296
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
297
+ try {
298
+ JSON.parse(trimmed);
299
+ }
300
+ catch (error) {
301
+ throw new MetaInferenceModelError('model_output_invalid', `meta-inference model returned invalid JSON: ${errorText(error)}`, { cause: error });
302
+ }
303
+ }
304
+ return trimmed;
305
+ }
149
306
  //# sourceMappingURL=meta-inference-engine.js.map
@@ -100,11 +100,17 @@ export interface CuratedSignalsFile {
100
100
  };
101
101
  signals: CuratedSignal[];
102
102
  }
103
+ /** Clock for skipping another hermes call after a failed meta-inference run. */
104
+ export interface ModelBackoffState {
105
+ failedAt: string;
106
+ failures: number;
107
+ }
103
108
  export interface InferenceState {
104
109
  processedCommentIds: string[];
105
110
  processedSessionIds: string[];
106
111
  processedPostIds: string[];
107
112
  lastRun: string | null;
113
+ modelBackoff?: ModelBackoffState;
108
114
  }
109
115
  export interface SynthesisReport {
110
116
  entriesProcessed: number;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xray/repertoire",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Factory memory organ for 0xRay — seed primitives, memory routing, extra MCP",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -117,7 +117,7 @@
117
117
  }
118
118
  },
119
119
  "devDependencies": {
120
- "0xray": "^4.0.7",
120
+ "0xray": "^4.0.28",
121
121
  "@types/node": "^22.15.0",
122
122
  "tsx": "^4.19.0",
123
123
  "typescript": "^5.6.0",
@@ -114,10 +114,13 @@ try {
114
114
  fail('.mcp.json', e.message);
115
115
  }
116
116
 
117
- // 5. Grok plugin + UserPromptSubmit boot hook
118
- const grokPlugin = join(homedir(), '.grok/plugins/0xray');
119
- const grokHooks = join(grokPlugin, 'hooks/hooks.json');
120
- existsSync(grokPlugin) ? pass('Grok plugin', grokPlugin) : fail('Grok plugin', 'run: npx 0xray grok install --force');
117
+ // 5. Grok plugin + UserPromptSubmit boot hook.
118
+ // 0xray 4.0.28 installs the plugin into the project, not ~/.grok.
119
+ const grokPlugin = join(root, '.grok', 'plugins', '0xray');
120
+ const grokHooks = join(grokPlugin, 'hooks', 'hooks.json');
121
+ existsSync(grokPlugin)
122
+ ? pass('Grok plugin', grokPlugin)
123
+ : fail('Grok plugin', `missing ${grokPlugin} — run: npx 0xray grok install --force`);
121
124
  if (existsSync(grokHooks)) {
122
125
  const hooksRaw = readFileSync(grokHooks, 'utf8');
123
126
  hooksRaw.includes('session-start.js')
@@ -130,7 +133,7 @@ if (existsSync(grokHooks)) {
130
133
  ? pass('UserPromptSubmit boot hook')
131
134
  : fail('UserPromptSubmit boot hook', 'add session-start.js — npx 0xray grok install --force');
132
135
  } else {
133
- fail('Grok hooks.json', 'missing');
136
+ fail('Grok hooks.json', `missing ${grokHooks}`);
134
137
  }
135
138
 
136
139
  try {
@@ -297,7 +300,7 @@ if (existsSync(installedGrokHooks)) {
297
300
  fail('Grok plugin PostToolUse hook', 'run: npx 0xray grok install --force');
298
301
  }
299
302
  } else {
300
- fail('Grok plugin PostToolUse hook', 'hooks.json missing — npx 0xray grok install --force');
303
+ fail('Grok plugin PostToolUse hook', `missing ${installedGrokHooks} — run: npx 0xray grok install --force`);
301
304
  }
302
305
 
303
306
  console.log('\n' + (failed === 0 ? '🎉 Suit wearable — operate within 0xRay.' : `⚠️ ${failed} check(s) failed — fix before tuning.`));
@@ -58,15 +58,119 @@ async function main() {
58
58
  else fail('provider.buildSynthesisContext', 'null');
59
59
  }
60
60
 
61
- if (typeof provider.refreshMetaInference === 'function') {
61
+ if (typeof provider.refreshMetaInference !== 'function') {
62
+ fail('provider.refreshMetaInference missing');
63
+ } else {
62
64
  try {
63
- const { refreshed } = await provider.refreshMetaInference();
64
- pass('provider.refreshMetaInference', String(refreshed));
65
+ const refresh = await provider.refreshMetaInference();
66
+ if (refresh && typeof refresh.refreshed === 'boolean' && typeof refresh.reason === 'string') {
67
+ pass('provider.refreshMetaInference', `${refresh.refreshed} ${refresh.reason}`);
68
+ } else {
69
+ fail('provider.refreshMetaInference', JSON.stringify(refresh));
70
+ }
65
71
  } catch (e) {
66
72
  fail('provider.refreshMetaInference', e.message);
67
73
  }
68
- } else {
69
- fail('provider.refreshMetaInference missing');
74
+ }
75
+
76
+ // Reaches synthesis output with a stubbed model, or fails.
77
+ const synthTmp = mkdtempSync(join(tmpdir(), 'repertoire-synth-output-'));
78
+ try {
79
+ const logDir = join(synthTmp, 'logs');
80
+ mkdirSync(logDir, { recursive: true });
81
+ const synthSessionId = 'dogfood-synth-1';
82
+ writeFileSync(
83
+ join(logDir, 'entries.jsonl'),
84
+ `${JSON.stringify({
85
+ timestamp: new Date().toISOString(),
86
+ source: 'groover',
87
+ session_id: synthSessionId,
88
+ inference: 'stubbed synthesis must land in the report',
89
+ post_title: 'dogfood',
90
+ })}\n`,
91
+ );
92
+ const statePath = join(synthTmp, 'inference-state.json');
93
+ const reportPath = join(synthTmp, 'synthesis.md');
94
+ const { MetaInferenceEngine } = await import(
95
+ join(root, 'dist/synthesis/meta-inference-engine.js')
96
+ );
97
+ const engine = new MetaInferenceEngine({
98
+ logDir,
99
+ statePath,
100
+ reportPath,
101
+ hermesCommand: () => 'STUBBED SYNTHESIS OUTPUT',
102
+ });
103
+ const report = await engine.run();
104
+ const written = existsSync(reportPath) ? readFileSync(reportPath, 'utf8') : '';
105
+ const state = existsSync(statePath)
106
+ ? JSON.parse(readFileSync(statePath, 'utf8'))
107
+ : { processedSessionIds: [] };
108
+ const processed = Array.isArray(state.processedSessionIds) &&
109
+ state.processedSessionIds.includes(synthSessionId);
110
+ if (
111
+ report &&
112
+ typeof report.finalReport === 'string' &&
113
+ report.finalReport.includes('STUBBED SYNTHESIS OUTPUT') &&
114
+ written.includes('STUBBED SYNTHESIS OUTPUT') &&
115
+ !written.includes('UNREVIEWED') &&
116
+ processed
117
+ ) {
118
+ pass('synthesis output with stubbed model', 'report written and entry processed');
119
+ } else {
120
+ fail(
121
+ 'synthesis output with stubbed model',
122
+ `report=${Boolean(report)} bytes=${written.length} processed=${JSON.stringify(state.processedSessionIds)}`,
123
+ );
124
+ }
125
+ } catch (e) {
126
+ fail('synthesis output with stubbed model', e.message);
127
+ } finally {
128
+ rmSync(synthTmp, { recursive: true, force: true });
129
+ }
130
+
131
+ const failTmp = mkdtempSync(join(tmpdir(), 'repertoire-synth-fail-'));
132
+ try {
133
+ const failLogDir = join(failTmp, 'logs');
134
+ mkdirSync(failLogDir, { recursive: true });
135
+ const failSessionId = 'dogfood-synth-fail';
136
+ writeFileSync(
137
+ join(failLogDir, 'entries.jsonl'),
138
+ `${JSON.stringify({
139
+ timestamp: new Date().toISOString(),
140
+ source: 'groover',
141
+ session_id: failSessionId,
142
+ inference: 'a failed model must not consume this entry',
143
+ post_title: 'dogfood-fail',
144
+ })}\n`,
145
+ );
146
+ const failStatePath = join(failTmp, 'inference-state.json');
147
+ const failProvider = createMemoryRoutingProvider({
148
+ projectRoot: failTmp,
149
+ logDir: failLogDir,
150
+ statePath: failStatePath,
151
+ feedbackDir: join(failTmp, 'feedback'),
152
+ hermesCommand: () => {
153
+ throw new Error('dogfood model down');
154
+ },
155
+ });
156
+ const failed = await failProvider.refreshMetaInference();
157
+ const failState = existsSync(failStatePath)
158
+ ? JSON.parse(readFileSync(failStatePath, 'utf8'))
159
+ : { processedSessionIds: [], lastRun: null };
160
+ const stillOpen = !Array.isArray(failState.processedSessionIds)
161
+ || !failState.processedSessionIds.includes(failSessionId);
162
+ if (failed && failed.refreshed === false && failed.reason === 'model_call_failed' && stillOpen) {
163
+ pass('provider.refreshMetaInference failing model', `${failed.refreshed} ${failed.reason}`);
164
+ } else {
165
+ fail(
166
+ 'provider.refreshMetaInference failing model',
167
+ JSON.stringify({ failed, processed: failState.processedSessionIds }),
168
+ );
169
+ }
170
+ } catch (e) {
171
+ fail('provider.refreshMetaInference failing model', e.message);
172
+ } finally {
173
+ rmSync(failTmp, { recursive: true, force: true });
70
174
  }
71
175
 
72
176
  if (typeof provider.ingestFeedback === 'function') {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.38",
3
+ "version": "4.0.39",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",