0xray 4.0.42 → 4.1.0
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/.plugin/plugin.json +1 -1
- package/.xray/features.json +5 -5
- package/AGENTS-consumer.md +8 -6
- package/AGENTS.md +17 -8
- package/LICENSE +1 -1
- package/README.md +15 -9
- package/SKILLS.md +3 -1
- package/dist/AGENTS.md +17 -8
- package/dist/CHANGELOG.md +83 -11
- package/dist/LICENSE +1 -1
- package/dist/README.md +15 -9
- package/dist/SKILLS.md +3 -1
- package/dist/cli/commands/plugin-commands.d.ts +4 -0
- package/dist/cli/commands/plugin-commands.js +84 -41
- package/dist/core/config-loader.js +4 -4
- package/dist/core/features-config.d.ts +1 -1
- package/dist/core/features-config.js +3 -3
- package/dist/integrations/cursor/hooks/hooks.json +6 -3
- package/dist/integrations/cursor/hooks/pre-tool-use.js +100 -37
- package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +9 -1
- package/dist/integrations/grok/hooks/pre-tool-use.js +2 -58
- package/dist/integrations/hermes-agent/bridge.mjs +14 -1
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +3 -2
- package/dist/integrations/hooks/goggles-mcp.mjs +35 -17
- package/dist/integrations/hooks/goggles-pipeline.mjs +46 -1
- package/dist/integrations/hooks/goggles-planes.json +7 -6
- package/dist/integrations/hooks/pre-tool-speak.mjs +76 -0
- package/dist/integrations/openclaw/hooks/pre-tool-gate-runtime.mjs +21 -4
- package/dist/integrations/openclaw/plugin/xray-pre-tool/index.js +17 -2
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/monitoring/{nudge-watchdog.d.ts → nudge-monitor.d.ts} +14 -5
- package/dist/monitoring/{nudge-watchdog.js → nudge-monitor.js} +42 -17
- package/dist/plugin/xray-codex-injection.js +18 -0
- package/dist/processors/implementations/nudge-processor.d.ts +1 -1
- package/dist/processors/implementations/nudge-processor.js +4 -4
- package/docs-site/docs/plates/activity-log.md +1 -1
- package/docs-site/docs/plates/bifrost.md +48 -0
- package/docs-site/docs/plates/boot.md +2 -2
- package/docs-site/docs/plates/centrifuge.md +1 -1
- package/docs-site/docs/plates/cursor-compact.md +1 -1
- package/docs-site/docs/plates/glossary.md +7 -3
- package/docs-site/docs/plates/goggles.md +13 -2
- package/docs-site/docs/plates/governance.md +1 -1
- package/docs-site/docs/plates/grok-compact.md +1 -1
- package/docs-site/docs/plates/grokbot.md +3 -1
- package/docs-site/docs/plates/host-pack.md +1 -1
- package/docs-site/docs/plates/house.md +1 -1
- package/docs-site/docs/plates/index.md +4 -2
- package/docs-site/docs/plates/inference-files.md +1 -1
- package/docs-site/docs/plates/kept-line.md +1 -1
- package/docs-site/docs/plates/kits.md +1 -1
- package/docs-site/docs/plates/lens-gate.md +3 -1
- package/docs-site/docs/plates/lens-page.md +1 -1
- package/docs-site/docs/plates/memory-recall.md +1 -1
- package/docs-site/docs/plates/notes-page.md +2 -2
- package/docs-site/docs/plates/orchestration.md +1 -1
- package/docs-site/docs/plates/package-face.md +1 -1
- package/docs-site/docs/plates/payload-heat.md +1 -1
- package/docs-site/docs/plates/pickup-stamp.md +1 -1
- package/docs-site/docs/plates/pre-tool.md +1 -1
- package/docs-site/docs/plates/processor.md +1 -1
- package/docs-site/docs/plates/record-map.md +1 -1
- package/docs-site/docs/plates/reflection-page.md +1 -1
- package/docs-site/docs/plates/reporting.md +1 -1
- package/docs-site/docs/plates/review.md +1 -1
- package/docs-site/docs/plates/routing.md +1 -1
- package/docs-site/docs/plates/session-capture.md +1 -1
- package/docs-site/docs/plates/site-manual.md +1 -1
- package/docs-site/docs/plates/stamp-plate.md +1 -1
- package/docs-site/docs/plates/station-card.md +1 -1
- package/docs-site/docs/plates/station-heat.md +1 -1
- package/docs-site/docs/plates/suit-organs.md +1 -1
- package/docs-site/docs/plates/suit-settings.md +1 -1
- package/docs-site/docs/plates/suit-wear.md +1 -1
- package/docs-site/docs/plates/suit.md +3 -3
- package/docs-site/docs/plates/trail-state.md +1 -1
- package/docs-site/docs/plates/work-fresh.md +1 -1
- package/docs-site/docs/plates/write-home.md +1 -1
- package/llms.txt +14 -6
- package/package.json +3 -3
- package/scripts/foundry/README.md +2 -2
- package/scripts/foundry/release-gate.mjs +111 -15
- package/scripts/foundry/release.mjs +8 -0
- package/scripts/mjs/verify-pipeline-facets.mjs +1 -1
- package/scripts/node/install-bridges.cjs +28 -12
- package/scripts/node/pack-tmp-suit-proof.mjs +14 -6
- package/src/integrations/openclaw/plugin/xray-pre-tool/index.js +17 -2
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/vendor/@0xray/repertoire/README.md +1 -1
- package/xray/agents_template.md +2 -2
- package/xray/features.json +5 -5
- package/docs-site/docs/plates/live-mesh.md +0 -41
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Tools: look, status_lens. Same organ as the CLI.
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import {
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
8
8
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -12,17 +12,9 @@ import * as bundled from './goggles-pipeline.mjs';
|
|
|
12
12
|
|
|
13
13
|
const HERE = fileURLToPath(new URL('.', import.meta.url));
|
|
14
14
|
const SERVER_NAME = 'goggles';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'house',
|
|
19
|
-
'boot',
|
|
20
|
-
'governance',
|
|
21
|
-
'memory-recall',
|
|
22
|
-
'orchestration',
|
|
23
|
-
'processor',
|
|
24
|
-
'reporting',
|
|
25
|
-
];
|
|
15
|
+
/** Card planes come from the organ, not a hand-kept copy. */
|
|
16
|
+
const CARD_PLANES = Array.isArray(bundled.CARD_PLANES) ? bundled.CARD_PLANES : [];
|
|
17
|
+
const CARD_OUTERS = ['digest', 'triage'];
|
|
26
18
|
const SCOPES = ['ecosystem', 'part', 'one flow', 'one artifact'];
|
|
27
19
|
|
|
28
20
|
const LOOK_SCHEMA = {
|
|
@@ -31,7 +23,7 @@ const LOOK_SCHEMA = {
|
|
|
31
23
|
properties: {
|
|
32
24
|
kind: {
|
|
33
25
|
type: 'string',
|
|
34
|
-
description: '
|
|
26
|
+
description: 'Use "0" or "actuality" for the lens. Send kind, outer, or plane: an empty look is refused. Never with outer: kind and outer together is refused.',
|
|
35
27
|
enum: ['0', 'actuality'],
|
|
36
28
|
},
|
|
37
29
|
outer: {
|
|
@@ -46,7 +38,7 @@ const LOOK_SCHEMA = {
|
|
|
46
38
|
},
|
|
47
39
|
plane: {
|
|
48
40
|
type: 'string',
|
|
49
|
-
description: 'Card plane for digest or triage only. Not the lens.',
|
|
41
|
+
description: 'Card plane for digest or triage only. Not the lens. A plane with any other outer is refused.',
|
|
50
42
|
enum: CARD_PLANES,
|
|
51
43
|
},
|
|
52
44
|
},
|
|
@@ -55,7 +47,7 @@ const LOOK_SCHEMA = {
|
|
|
55
47
|
export const TOOLS = [
|
|
56
48
|
{
|
|
57
49
|
name: 'look',
|
|
58
|
-
description: 'Kind 0 is quiet on a match. It checks the six outer names. One outer is a reading. digest and triage return a card.',
|
|
50
|
+
description: 'Kind 0 is quiet on a match. It checks the six outer names, then sweeps every card plane for missing shipped files and names the first drift. One outer is a reading. digest and triage return a card. Refused: an empty look; kind with outer; a plane with an outer other than digest or triage.',
|
|
59
51
|
inputSchema: LOOK_SCHEMA,
|
|
60
52
|
},
|
|
61
53
|
{
|
|
@@ -152,6 +144,15 @@ function checkArgs(args) {
|
|
|
152
144
|
if (args.plane != null && !CARD_PLANES.includes(args.plane)) {
|
|
153
145
|
return deny('invalid_args', 'plane is a card plane.');
|
|
154
146
|
}
|
|
147
|
+
if (args.kind == null && args.outer == null && args.plane == null) {
|
|
148
|
+
return deny('invalid_args', 'Name a kind, one outer, or one plane.');
|
|
149
|
+
}
|
|
150
|
+
if (args.kind != null && args.outer != null) {
|
|
151
|
+
return deny('invalid_args', 'Send kind or outer, not both.');
|
|
152
|
+
}
|
|
153
|
+
if (args.plane != null && args.outer != null && !CARD_OUTERS.includes(args.outer)) {
|
|
154
|
+
return deny('invalid_args', 'plane goes with digest or triage only.');
|
|
155
|
+
}
|
|
155
156
|
return null;
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -161,7 +162,7 @@ function publicCard(view) {
|
|
|
161
162
|
if (Array.isArray(view.empty) && view.empty.length) notes.push(`Empty: ${view.empty.join(', ')}`);
|
|
162
163
|
if (view.exam) notes.push(view.exam);
|
|
163
164
|
}
|
|
164
|
-
const skills = view.skills ?
|
|
165
|
+
const skills = view.skills ? String(view.skills) : '';
|
|
165
166
|
const cascade = view.cascade
|
|
166
167
|
? {
|
|
167
168
|
plane: view.cascade.plane,
|
|
@@ -227,7 +228,8 @@ export async function answerLook(raw, root) {
|
|
|
227
228
|
const here = projectRoot(root);
|
|
228
229
|
if (pureKind0(args)) {
|
|
229
230
|
api.maintainLens(here);
|
|
230
|
-
const line = api.actualityLine();
|
|
231
|
+
const line = api.actualityLine() || cardDrift(api, here);
|
|
232
|
+
if (line) writeLens(api, here, line);
|
|
231
233
|
if (!line) return quiet('kind0');
|
|
232
234
|
return { ok: true, quiet: false, mode: 'kind0', content: line };
|
|
233
235
|
}
|
|
@@ -269,6 +271,22 @@ export async function answerLook(raw, root) {
|
|
|
269
271
|
};
|
|
270
272
|
}
|
|
271
273
|
|
|
274
|
+
/** Kind 0 card sweep: triage's file check on every card plane, shipped paths only. */
|
|
275
|
+
function cardDrift(api, here) {
|
|
276
|
+
if (typeof api.shippedDrift !== 'function') return '';
|
|
277
|
+
try {
|
|
278
|
+
return api.shippedDrift(here);
|
|
279
|
+
} catch {
|
|
280
|
+
return '';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function writeLens(api, here, line) {
|
|
285
|
+
const file = api.lensPath(here);
|
|
286
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
287
|
+
writeFileSync(file, `${line}\n`);
|
|
288
|
+
}
|
|
289
|
+
|
|
272
290
|
function lensBody(root) {
|
|
273
291
|
const file = join(root, '.xray', 'state', 'LENS.md');
|
|
274
292
|
if (!existsSync(file)) return { found: false, text: '' };
|
|
@@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
18
18
|
const FIELD_ORDER = ['plate', 'entry', 'exit', 'files', 'skills', 'setup', 'teardown', 'worn'];
|
|
19
19
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const WORN_PLANES = ['dichotomy', 'syncopate', 'synthesis', 'digest', 'triage', 'loop'];
|
|
21
|
-
const CARD_PLANES = ['ground', 'routing', 'house', 'boot', 'governance', 'memory-recall', 'orchestration', 'processor', 'reporting', 'stamp-plate', 'record-map', 'write-home', 'activity-log', 'session-capture', 'suit-wear', 'suit-organs', 'station-card', 'notes-page', 'reflection-page', 'site-manual', 'package-face', 'suit-settings', 'trail-state', 'inference-files', 'grok-compact', 'payload-heat', 'station-heat', 'pickup-stamp', 'cursor-compact', 'work-fresh', 'kept-line', 'lens-gate', 'pre-tool', 'lens-page', 'centrifuge'];
|
|
21
|
+
export const CARD_PLANES = ['ground', 'routing', 'house', 'boot', 'governance', 'memory-recall', 'orchestration', 'processor', 'reporting', 'stamp-plate', 'record-map', 'write-home', 'activity-log', 'session-capture', 'suit-wear', 'suit-organs', 'station-card', 'notes-page', 'reflection-page', 'site-manual', 'package-face', 'suit-settings', 'trail-state', 'inference-files', 'grok-compact', 'payload-heat', 'station-heat', 'pickup-stamp', 'cursor-compact', 'work-fresh', 'kept-line', 'lens-gate', 'pre-tool', 'lens-page', 'centrifuge'];
|
|
22
22
|
const CARD_FLAVORS = ['digest', 'triage'];
|
|
23
23
|
const SCOPE_ZOOM = ['ecosystem', 'part', 'one flow', 'one artifact'];
|
|
24
24
|
|
|
@@ -1065,6 +1065,51 @@ export function growPlane(id, platesDir, root) {
|
|
|
1065
1065
|
return { ...base, ...grown };
|
|
1066
1066
|
}
|
|
1067
1067
|
|
|
1068
|
+
/** Paths the package ships: package.json files, plus what npm always packs. */
|
|
1069
|
+
function shippedSet(root) {
|
|
1070
|
+
try {
|
|
1071
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
1072
|
+
if (!Array.isArray(pkg.files)) return null;
|
|
1073
|
+
return pkg.files
|
|
1074
|
+
.map(String)
|
|
1075
|
+
.filter((entry) => entry && !entry.startsWith('!'))
|
|
1076
|
+
.map((entry) => entry.replace(/\/+$/, ''))
|
|
1077
|
+
.concat(['package.json', 'README.md', 'LICENSE']);
|
|
1078
|
+
} catch {
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function ships(shipped, rel) {
|
|
1084
|
+
if (!shipped) return true;
|
|
1085
|
+
const path = String(rel || '').replace(/\/+$/, '');
|
|
1086
|
+
if (!path) return false;
|
|
1087
|
+
return shipped.some((entry) => path === entry || path.startsWith(`${entry}/`) || entry.startsWith(`${path}/`));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Kind 0 card sweep: triage's file check on every card plane, limited to
|
|
1092
|
+
* paths the package ships, so a packed install and the repo read the same.
|
|
1093
|
+
* Returns the first drift line, or '' when every card holds.
|
|
1094
|
+
*/
|
|
1095
|
+
export function shippedDrift(projectRoot) {
|
|
1096
|
+
const platesDir = findPlatesDir(HERE);
|
|
1097
|
+
const root = sourceRoot(platesDir);
|
|
1098
|
+
const shipped = shippedSet(root);
|
|
1099
|
+
for (const id of CARD_PLANES) {
|
|
1100
|
+
const plane = growPlane(id, platesDir, projectRoot || stateRoot());
|
|
1101
|
+
const kept = {
|
|
1102
|
+
...plane,
|
|
1103
|
+
files: (Array.isArray(plane.files) ? plane.files : []).filter((rel) => ships(shipped, rel)),
|
|
1104
|
+
skills: ships(shipped, plane.skills) ? plane.skills : '',
|
|
1105
|
+
worn: ships(shipped, plane.worn) ? plane.worn : '',
|
|
1106
|
+
};
|
|
1107
|
+
const exam = examinePlane(kept, root);
|
|
1108
|
+
if (exam.kind === 'drift') return `Actuality. ${id}: ${exam.text}`;
|
|
1109
|
+
}
|
|
1110
|
+
return '';
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1068
1113
|
function repoRootFrom(platesDir) {
|
|
1069
1114
|
return dirname(dirname(dirname(platesDir)));
|
|
1070
1115
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"files": [
|
|
8
8
|
"src",
|
|
9
9
|
"grok-bot/OP-PROC.md",
|
|
10
|
+
"grok-bot/LEXICON.md",
|
|
10
11
|
"src/opencode/agents",
|
|
11
12
|
"AGENTS.md",
|
|
12
13
|
"src/integrations",
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
"plates": ["orchestration", "processor"]
|
|
29
30
|
},
|
|
30
31
|
"house": {
|
|
31
|
-
"files": ["grok-bot/lib/seat-doctor.cjs"],
|
|
32
|
+
"files": ["grok-bot/lib/seat-doctor.cjs", "grok-bot/templates/house/fleet/WAKE-ROUTINE.md", "grok-bot/templates/house/HOUSE.md", "src/__tests__/unit/grok-bot-house-template.test.ts", "src/__tests__/unit/grok-bot-house-migrate.test.ts"],
|
|
32
33
|
"skills": ".agents/skills/house/SKILL.md",
|
|
33
34
|
"plates": ["suit-wear"]
|
|
34
35
|
},
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"skills": ".agents/skills/governance/SKILL.md"
|
|
46
47
|
},
|
|
47
48
|
"memory-recall": {
|
|
48
|
-
"files": ["src/integrations/hooks/plates.cjs"],
|
|
49
|
+
"files": ["src/integrations/hooks/plates.cjs", "src/__tests__/unit/plates.test.ts"],
|
|
49
50
|
"skills": ".agents/skills/memory-recall/SKILL.md",
|
|
50
51
|
"plates": ["record-map"]
|
|
51
52
|
},
|
|
@@ -94,7 +95,7 @@
|
|
|
94
95
|
"plates": ["write-home", "inference-files", "suit-organs"]
|
|
95
96
|
},
|
|
96
97
|
"suit-wear": {
|
|
97
|
-
"files": ["src/core/features-config.ts"],
|
|
98
|
+
"files": ["src/core/features-config.ts", "scripts/node/install-bridges.cjs", "scripts/node/setup.cjs", "src/integrations/cursor/hooks/xray-cloud-hook.sh"],
|
|
98
99
|
"mark": "log_path",
|
|
99
100
|
"skills": ".agents/skills/suit-wear/SKILL.md",
|
|
100
101
|
"plates": ["suit-settings", "write-home", "package-face"]
|
|
@@ -124,13 +125,13 @@
|
|
|
124
125
|
"plates": ["record-map"]
|
|
125
126
|
},
|
|
126
127
|
"site-manual": {
|
|
127
|
-
"files": ["docs-site/docs/introduction.md"],
|
|
128
|
+
"files": ["docs-site/docs/introduction.md", "docs-site/docs/index.md", "docs-site/docs/full-reference.md", "docs-site/docs/guides/features-since-3.1.md"],
|
|
128
129
|
"mark": "survives the context window",
|
|
129
130
|
"skills": ".agents/skills/site-manual/SKILL.md",
|
|
130
131
|
"plates": ["record-map"]
|
|
131
132
|
},
|
|
132
133
|
"package-face": {
|
|
133
|
-
"files": ["package.json"],
|
|
134
|
+
"files": ["package.json", "grok-bot/README.md", "grok-bot/llms.txt", "grok-bot/CHANGELOG.md", "src/cli/index.ts", "src/cli/commands/plugin-commands.ts", "src/__tests__/unit/plugin-commands.test.ts"],
|
|
134
135
|
"mark": "\"name\": \"0xray\"",
|
|
135
136
|
"skills": ".agents/skills/package-face/SKILL.md",
|
|
136
137
|
"plates": ["record-map", "suit-wear"]
|
|
@@ -197,7 +198,7 @@
|
|
|
197
198
|
"plates": ["station-card", "work-fresh"]
|
|
198
199
|
},
|
|
199
200
|
"lens-gate": {
|
|
200
|
-
"files": ["src/integrations/hooks/goggles-pipeline.mjs"],
|
|
201
|
+
"files": ["src/integrations/hooks/goggles-pipeline.mjs", "src/__tests__/unit/goggles-pipeline.test.ts"],
|
|
201
202
|
"mark": "export function lensBeforeResearch",
|
|
202
203
|
"skills": ".agents/skills/lens-gate/SKILL.md",
|
|
203
204
|
"plates": ["pre-tool"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One pre-tool speak for every host.
|
|
3
|
+
* The lens denies an unnamed plane. The route speaks only when Repertoire changes the agent.
|
|
4
|
+
*/
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { lensBeforeResearch } from './goggles-pipeline.mjs';
|
|
10
|
+
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
let scoreAndRouteFn;
|
|
13
|
+
|
|
14
|
+
function loadScoreAndRoute() {
|
|
15
|
+
if (scoreAndRouteFn !== undefined) return scoreAndRouteFn;
|
|
16
|
+
scoreAndRouteFn = null;
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const candidates = [
|
|
19
|
+
join(here, '../../nucleus/thin-dispatch.js'),
|
|
20
|
+
join(here, '../../../dist/nucleus/thin-dispatch.js'),
|
|
21
|
+
join(here, '../../../nucleus/thin-dispatch.js'),
|
|
22
|
+
];
|
|
23
|
+
for (const candidate of candidates) {
|
|
24
|
+
if (!existsSync(candidate)) continue;
|
|
25
|
+
try {
|
|
26
|
+
const loaded = require(candidate);
|
|
27
|
+
if (typeof loaded.scoreAndRoute === 'function') {
|
|
28
|
+
scoreAndRouteFn = loaded.scoreAndRoute;
|
|
29
|
+
return scoreAndRouteFn;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
/* next built copy */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Speak only when the organ changes the agent. A match that leaves the agent is quiet. */
|
|
39
|
+
export function repertoireDecisionFields(routed) {
|
|
40
|
+
if (!routed?.memoryRouting?.overridden || !routed.agent) return null;
|
|
41
|
+
const signals = Array.isArray(routed.memoryRouting.signals)
|
|
42
|
+
? routed.memoryRouting.signals.slice(0, 4).filter(Boolean)
|
|
43
|
+
: [];
|
|
44
|
+
const why = signals.length ? ` on ${signals.join(', ')}` : '';
|
|
45
|
+
return {
|
|
46
|
+
agent: routed.agent,
|
|
47
|
+
gate: 'repertoire',
|
|
48
|
+
hookSpecificOutput: {
|
|
49
|
+
hookEventName: 'PreToolUse',
|
|
50
|
+
additionalContext: `Repertoire changed the route to ${routed.agent}${why}. Follow that agent for this work.`,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function repertoireRoute(operation) {
|
|
56
|
+
const text = String(operation || '').slice(0, 500).trim();
|
|
57
|
+
if (!text) return null;
|
|
58
|
+
try {
|
|
59
|
+
const scoreAndRoute = loadScoreAndRoute();
|
|
60
|
+
if (!scoreAndRoute) return null;
|
|
61
|
+
return repertoireDecisionFields(scoreAndRoute(text, {}));
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Deny when the lens stops the tool. Any other lens result is quiet. */
|
|
68
|
+
export function lensDecision(root, toolName, spoken) {
|
|
69
|
+
try {
|
|
70
|
+
const stop = lensBeforeResearch(root, toolName, spoken);
|
|
71
|
+
if (stop?.decision === 'deny') return stop;
|
|
72
|
+
return null;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -23,7 +23,7 @@ function resolveGate() {
|
|
|
23
23
|
].filter(Boolean);
|
|
24
24
|
const found = candidates.find((p) => existsSync(p));
|
|
25
25
|
if (!found) {
|
|
26
|
-
throw new Error('delegation-gate.js missing — build 0xray or npm install 0xray');
|
|
26
|
+
throw Object.assign(new Error('delegation-gate.js missing — build 0xray or npm install 0xray'), { code: 'ERR_MODULE_NOT_FOUND' });
|
|
27
27
|
}
|
|
28
28
|
return require(found);
|
|
29
29
|
}
|
|
@@ -49,7 +49,7 @@ function readStdin() {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function decide(event, gate) {
|
|
52
|
+
async function decide(event, gate) {
|
|
53
53
|
const projectRoot =
|
|
54
54
|
event.cwd || event.workspaceRoot || event.projectRoot || process.cwd();
|
|
55
55
|
const sessionId = event.sessionId || event.session_id || 'openclaw';
|
|
@@ -70,20 +70,37 @@ function decide(event, gate) {
|
|
|
70
70
|
gate: outcome.gate,
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
|
|
74
|
+
const spoken = `${toolName} ${JSON.stringify(args)}`.slice(0, 500);
|
|
75
|
+
const { lensDecision, repertoireRoute } = await import('../../hooks/pre-tool-speak.mjs');
|
|
76
|
+
const lens = lensDecision(projectRoot, toolName, spoken);
|
|
77
|
+
if (lens) {
|
|
78
|
+
return {
|
|
79
|
+
action: 'block',
|
|
80
|
+
allow: false,
|
|
81
|
+
reason: lens.reason,
|
|
82
|
+
gate: lens.gate || 'lens',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const route = repertoireRoute(spoken);
|
|
86
|
+
const allowed = {
|
|
74
87
|
action: 'allow',
|
|
75
88
|
allow: true,
|
|
76
89
|
reason: outcome.reason,
|
|
77
90
|
gate: outcome.gate,
|
|
78
91
|
warn: Boolean(outcome.reason),
|
|
79
92
|
};
|
|
93
|
+
if (route?.hookSpecificOutput) {
|
|
94
|
+
allowed.hookSpecificOutput = route.hookSpecificOutput;
|
|
95
|
+
}
|
|
96
|
+
return allowed;
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
async function main() {
|
|
83
100
|
try {
|
|
84
101
|
const event = await readStdin();
|
|
85
102
|
const gate = resolveGate();
|
|
86
|
-
const decision = decide(event, gate);
|
|
103
|
+
const decision = await decide(event, gate);
|
|
87
104
|
console.log(JSON.stringify(decision));
|
|
88
105
|
process.exit(decision.action === 'block' ? 2 : 0);
|
|
89
106
|
} catch (err) {
|
|
@@ -2,7 +2,7 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
|
|
7
7
|
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
@@ -39,7 +39,7 @@ export default definePluginEntry({
|
|
|
39
39
|
register(api) {
|
|
40
40
|
api.on(
|
|
41
41
|
'before_tool_call',
|
|
42
|
-
(event, ctx) => {
|
|
42
|
+
async (event, ctx) => {
|
|
43
43
|
try {
|
|
44
44
|
const xrayRoot = resolveXrayRoot();
|
|
45
45
|
if (!xrayRoot) {
|
|
@@ -61,6 +61,21 @@ export default definePluginEntry({
|
|
|
61
61
|
if (!outcome.allow) {
|
|
62
62
|
return { block: true, blockReason: outcome.reason || '0xRay constitution' };
|
|
63
63
|
}
|
|
64
|
+
const spoken = `${event.toolName} ${JSON.stringify(event.params || {})}`.slice(0, 500);
|
|
65
|
+
const speakPath = [
|
|
66
|
+
join(xrayRoot, 'dist/integrations/hooks/pre-tool-speak.mjs'),
|
|
67
|
+
join(xrayRoot, 'src/integrations/hooks/pre-tool-speak.mjs'),
|
|
68
|
+
].find((candidate) => existsSync(candidate));
|
|
69
|
+
if (!speakPath) return undefined;
|
|
70
|
+
const speak = await import(pathToFileURL(speakPath).href);
|
|
71
|
+
const lens = speak.lensDecision(projectRoot, event.toolName, spoken);
|
|
72
|
+
if (lens) {
|
|
73
|
+
return { block: true, blockReason: lens.reason };
|
|
74
|
+
}
|
|
75
|
+
const route = speak.repertoireRoute(spoken);
|
|
76
|
+
if (route?.hookSpecificOutput) {
|
|
77
|
+
return { block: false, hookSpecificOutput: route.hookSpecificOutput };
|
|
78
|
+
}
|
|
64
79
|
return undefined;
|
|
65
80
|
} catch (err) {
|
|
66
81
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Nudge
|
|
2
|
+
* Nudge Monitor - Detects and breaks stuck AI patterns
|
|
3
3
|
*
|
|
4
4
|
* Monitors agent actions and detects common failure patterns:
|
|
5
5
|
* - think-loop: AI keeps outputting thinking tags but no progress
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - tool-loop: Same tool called with similar args repeatedly
|
|
9
9
|
* - repair-failure: Processor indicates fix but error persists
|
|
10
10
|
*
|
|
11
|
-
* @module monitoring/nudge-
|
|
11
|
+
* @module monitoring/nudge-monitor
|
|
12
12
|
*/
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
14
|
export interface NudgeConfig {
|
|
@@ -33,7 +33,16 @@ export interface NudgeAction {
|
|
|
33
33
|
suggestion: string;
|
|
34
34
|
priority: number;
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/** Old config key from before the rename to nudge_monitor; still read as a deprecated alias. */
|
|
37
|
+
export declare const LEGACY_NUDGE_CONFIG_KEY = "nudge_watchdog";
|
|
38
|
+
/**
|
|
39
|
+
* features.nudge_monitor, or the deprecated features.nudge_watchdog when nudge_monitor is unset (the new key wins
|
|
40
|
+
* when both are set). Using the old key logs a one-time deprecation warning.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveNudgeConfig(features: Record<string, unknown> | undefined | null, warn?: (message: string) => void): Record<string, unknown> | undefined;
|
|
43
|
+
/** Test hook: let the one-time deprecation warning fire again. */
|
|
44
|
+
export declare function resetLegacyNudgeWarning(): void;
|
|
45
|
+
declare class NudgeMonitor extends EventEmitter {
|
|
37
46
|
private config;
|
|
38
47
|
private patterns;
|
|
39
48
|
private lastNudgeTime;
|
|
@@ -75,13 +84,13 @@ declare class NudgeWatchdog extends EventEmitter {
|
|
|
75
84
|
};
|
|
76
85
|
private log;
|
|
77
86
|
}
|
|
78
|
-
export declare const
|
|
87
|
+
export declare const nudgeMonitor: NudgeMonitor;
|
|
79
88
|
export declare function recordThinkTag(): void;
|
|
80
89
|
export declare function recordCodeChange(): void;
|
|
81
90
|
export declare function recordToolCall(tool: string, args: string): void;
|
|
82
91
|
export declare function recordFixAttempt(filePath: string, error: string): void;
|
|
83
92
|
export declare function recordExplanation(): void;
|
|
84
93
|
export declare function getNudgeSuggestion(type: NudgeType, context?: Record<string, unknown>): string;
|
|
85
|
-
export declare function
|
|
94
|
+
export declare function getNudgeMonitor(): NudgeMonitor;
|
|
86
95
|
export declare function recordProcessorFix(filePath: string, fixed: boolean, errorPersisted: boolean): void;
|
|
87
96
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Nudge
|
|
2
|
+
* Nudge Monitor - Detects and breaks stuck AI patterns
|
|
3
3
|
*
|
|
4
4
|
* Monitors agent actions and detects common failure patterns:
|
|
5
5
|
* - think-loop: AI keeps outputting thinking tags but no progress
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - tool-loop: Same tool called with similar args repeatedly
|
|
9
9
|
* - repair-failure: Processor indicates fix but error persists
|
|
10
10
|
*
|
|
11
|
-
* @module monitoring/nudge-
|
|
11
|
+
* @module monitoring/nudge-monitor
|
|
12
12
|
*/
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
14
|
import { frameworkLogger } from "../core/framework-logger.js";
|
|
@@ -53,7 +53,32 @@ const NUDGE_MESSAGES = {
|
|
|
53
53
|
priority: 5,
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
|
-
|
|
56
|
+
/** Old config key from before the rename to nudge_monitor; still read as a deprecated alias. */
|
|
57
|
+
export const LEGACY_NUDGE_CONFIG_KEY = "nudge_watchdog";
|
|
58
|
+
let legacyKeyWarned = false;
|
|
59
|
+
/**
|
|
60
|
+
* features.nudge_monitor, or the deprecated features.nudge_watchdog when nudge_monitor is unset (the new key wins
|
|
61
|
+
* when both are set). Using the old key logs a one-time deprecation warning.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveNudgeConfig(features, warn = (m) => frameworkLogger.log("nudge-monitor", "deprecated-config-key", "warning", { message: m })) {
|
|
64
|
+
const current = features?.nudge_monitor;
|
|
65
|
+
if (current !== undefined && current !== null)
|
|
66
|
+
return current;
|
|
67
|
+
const legacy = features?.[LEGACY_NUDGE_CONFIG_KEY];
|
|
68
|
+
if (legacy !== undefined && legacy !== null) {
|
|
69
|
+
if (!legacyKeyWarned) {
|
|
70
|
+
legacyKeyWarned = true;
|
|
71
|
+
warn(`features.${LEGACY_NUDGE_CONFIG_KEY} is deprecated; rename it to features.nudge_monitor`);
|
|
72
|
+
}
|
|
73
|
+
return legacy;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Test hook: let the one-time deprecation warning fire again. */
|
|
78
|
+
export function resetLegacyNudgeWarning() {
|
|
79
|
+
legacyKeyWarned = false;
|
|
80
|
+
}
|
|
81
|
+
class NudgeMonitor extends EventEmitter {
|
|
57
82
|
config;
|
|
58
83
|
patterns = new Map();
|
|
59
84
|
lastNudgeTime = 0;
|
|
@@ -73,7 +98,7 @@ class NudgeWatchdog extends EventEmitter {
|
|
|
73
98
|
loadConfigFromFeatures() {
|
|
74
99
|
try {
|
|
75
100
|
const features = featuresConfigLoader.loadConfig();
|
|
76
|
-
const nudgeConfig = features
|
|
101
|
+
const nudgeConfig = resolveNudgeConfig(features);
|
|
77
102
|
if (nudgeConfig) {
|
|
78
103
|
this.config = { ...this.config, ...nudgeConfig };
|
|
79
104
|
}
|
|
@@ -86,10 +111,10 @@ class NudgeWatchdog extends EventEmitter {
|
|
|
86
111
|
if (!this.config.enabled) {
|
|
87
112
|
return;
|
|
88
113
|
}
|
|
89
|
-
this.log("nudge-
|
|
114
|
+
this.log("nudge-monitor", "started", "info", { config: this.config });
|
|
90
115
|
}
|
|
91
116
|
stop() {
|
|
92
|
-
this.log("nudge-
|
|
117
|
+
this.log("nudge-monitor", "stopped", "info");
|
|
93
118
|
}
|
|
94
119
|
recordThinkTag() {
|
|
95
120
|
const now = Date.now();
|
|
@@ -256,7 +281,7 @@ class NudgeWatchdog extends EventEmitter {
|
|
|
256
281
|
if (!action)
|
|
257
282
|
return null;
|
|
258
283
|
this.emit("nudge", action);
|
|
259
|
-
this.log("nudge-
|
|
284
|
+
this.log("nudge-monitor", "nudge-triggered", "warning", {
|
|
260
285
|
type: action.type,
|
|
261
286
|
message: action.message,
|
|
262
287
|
suggestion: action.suggestion,
|
|
@@ -328,28 +353,28 @@ class NudgeWatchdog extends EventEmitter {
|
|
|
328
353
|
}
|
|
329
354
|
}
|
|
330
355
|
}
|
|
331
|
-
export const
|
|
356
|
+
export const nudgeMonitor = new NudgeMonitor();
|
|
332
357
|
export function recordThinkTag() {
|
|
333
|
-
|
|
358
|
+
nudgeMonitor.recordThinkTag();
|
|
334
359
|
}
|
|
335
360
|
export function recordCodeChange() {
|
|
336
|
-
|
|
361
|
+
nudgeMonitor.recordCodeChange();
|
|
337
362
|
}
|
|
338
363
|
export function recordToolCall(tool, args) {
|
|
339
|
-
|
|
364
|
+
nudgeMonitor.recordToolCall(tool, args);
|
|
340
365
|
}
|
|
341
366
|
export function recordFixAttempt(filePath, error) {
|
|
342
|
-
|
|
367
|
+
nudgeMonitor.recordFixAttempt(filePath, error);
|
|
343
368
|
}
|
|
344
369
|
export function recordExplanation() {
|
|
345
|
-
|
|
370
|
+
nudgeMonitor.recordExplanation();
|
|
346
371
|
}
|
|
347
372
|
export function getNudgeSuggestion(type, context) {
|
|
348
|
-
return
|
|
373
|
+
return nudgeMonitor.getNudgeSuggestion(type, context);
|
|
349
374
|
}
|
|
350
|
-
export function
|
|
351
|
-
return
|
|
375
|
+
export function getNudgeMonitor() {
|
|
376
|
+
return nudgeMonitor;
|
|
352
377
|
}
|
|
353
378
|
export function recordProcessorFix(filePath, fixed, errorPersisted) {
|
|
354
|
-
|
|
379
|
+
nudgeMonitor.recordProcessorFix(filePath, fixed, errorPersisted);
|
|
355
380
|
}
|
|
@@ -114,6 +114,24 @@ export default async function xrayCodexPlugin(input) {
|
|
|
114
114
|
logger.error(`🚫 Delegation gate: ${delegationGate.reason}`);
|
|
115
115
|
throw new Error(`DELEGATION GATE: ${delegationGate.reason}`);
|
|
116
116
|
}
|
|
117
|
+
let speak = null;
|
|
118
|
+
try {
|
|
119
|
+
speak = (await import(new URL("../integrations/hooks/pre-tool-speak.mjs", import.meta.url).href));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
speak = null;
|
|
123
|
+
}
|
|
124
|
+
if (speak) {
|
|
125
|
+
const spoken = `${tool} ${JSON.stringify(args ?? {})}`.slice(0, 500);
|
|
126
|
+
const lens = speak.lensDecision(directory, tool, spoken);
|
|
127
|
+
if (lens) {
|
|
128
|
+
throw new Error(`DELEGATION GATE: ${lens.reason ?? "lens"}`);
|
|
129
|
+
}
|
|
130
|
+
const additional = speak.repertoireRoute(spoken)?.hookSpecificOutput?.additionalContext;
|
|
131
|
+
if (additional) {
|
|
132
|
+
logger.log(additional);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
117
135
|
if (isWriteEditOperation(tool)) {
|
|
118
136
|
const _ProcessorManager = getProcessorManager();
|
|
119
137
|
const _XrayStateManager = getXrayStateManager();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Nudge Processor - Integrates Nudge
|
|
2
|
+
* Nudge Processor - Integrates Nudge Monitor with processor system
|
|
3
3
|
*
|
|
4
4
|
* Post-processor that monitors agent actions and triggers nudges
|
|
5
5
|
* when stuck patterns are detected.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { PostProcessor } from "../processor-interfaces.js";
|
|
10
10
|
import { frameworkLogger } from "../../core/framework-logger.js";
|
|
11
|
-
import {
|
|
11
|
+
import { nudgeMonitor, resolveNudgeConfig, recordThinkTag, recordCodeChange, recordToolCall, recordFixAttempt, recordExplanation, getNudgeSuggestion, } from "../../monitoring/nudge-monitor.js";
|
|
12
12
|
import { featuresConfigLoader } from "../../core/features-config.js";
|
|
13
13
|
export class NudgeProcessor extends PostProcessor {
|
|
14
14
|
name = "nudge";
|
|
@@ -21,7 +21,7 @@ export class NudgeProcessor extends PostProcessor {
|
|
|
21
21
|
loadConfig() {
|
|
22
22
|
try {
|
|
23
23
|
const features = featuresConfigLoader.loadConfig();
|
|
24
|
-
const nudgeConfig = features
|
|
24
|
+
const nudgeConfig = resolveNudgeConfig(features);
|
|
25
25
|
if (nudgeConfig) {
|
|
26
26
|
this.nudgeEnabled = nudgeConfig.enabled !== false;
|
|
27
27
|
}
|
|
@@ -71,7 +71,7 @@ export class NudgeProcessor extends PostProcessor {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
const stats =
|
|
74
|
+
const stats = nudgeMonitor.getStats();
|
|
75
75
|
if (stats.activePatterns > 0) {
|
|
76
76
|
const nudge = this.detectMostUrgentNudge(stats);
|
|
77
77
|
if (nudge) {
|
|
@@ -35,4 +35,4 @@ Today those writers append `logs/framework/activity.log`. The features default s
|
|
|
35
35
|
|
|
36
36
|
When: on the tool hook, on framework logger flush, and on boot when file logging is on. Why: the report and the pulse read one tail, not a pile of copies.
|
|
37
37
|
|
|
38
|
-
stamped · 0xray 4.0
|
|
38
|
+
stamped · 0xray 4.1.0
|