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.
- package/.grok-plugin/plugin.json +1 -1
- package/.xray/features.json +1 -1
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +1 -1
- package/README.md +2 -2
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +1 -1
- package/dist/CHANGELOG.md +12 -0
- package/dist/README.md +2 -2
- package/dist/SKILLS.md +1 -1
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
- package/scripts/node/consumer-install-smoke.mjs +3 -3
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/vendor/@0xray/repertoire/README.md +1 -1
- package/vendor/@0xray/repertoire/dist/RepertoireService.js +1 -0
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts +5 -0
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -6
- package/vendor/@0xray/repertoire/dist/paths.js +0 -4
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +16 -6
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +19 -2
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +34 -2
- package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.d.ts +3 -0
- package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.js +25 -1
- package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.d.ts +37 -2
- package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.js +176 -19
- package/vendor/@0xray/repertoire/dist/types.d.ts +6 -0
- package/vendor/@0xray/repertoire/package.json +2 -2
- package/vendor/@0xray/repertoire/scripts/verify-grok-suit.mjs +9 -6
- package/vendor/@0xray/repertoire/scripts/verify-synthesis-dogfood.mjs +109 -5
- package/xray/features.json +1 -1
package/.grok-plugin/plugin.json
CHANGED
package/.xray/features.json
CHANGED
package/AGENTS-consumer.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/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.
|
|
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
|
@@ -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.
|
|
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.
|
|
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.
|
|
167
|
-
throw new Error(`expected organ 0.2.
|
|
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 };
|
|
@@ -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.
|
|
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
|
|
180
|
+
return this.consumableSynthesisPath(custom);
|
|
181
181
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
|
195
|
+
if (boundedId(normalized, id, false))
|
|
164
196
|
return true;
|
|
165
197
|
const spaced = id.replace(/-/g, ' ');
|
|
166
|
-
return spaced !== id && normalized
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
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',
|
|
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',
|
|
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
|
|
61
|
+
if (typeof provider.refreshMetaInference !== 'function') {
|
|
62
|
+
fail('provider.refreshMetaInference missing');
|
|
63
|
+
} else {
|
|
62
64
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
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
|
-
}
|
|
69
|
-
|
|
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') {
|