0xray 4.0.41 → 4.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.grok-plugin/plugin.json +1 -1
- package/.opencode/skills/orchestrator/SKILL.md +12 -0
- package/.xray/features.json +1 -1
- package/.xray/features.schema.json +1 -1
- package/AGENTS-consumer.md +2 -0
- package/AGENTS.md +2 -0
- package/README.md +52 -12
- package/SKILLS.md +2 -0
- package/dist/AGENTS.md +2 -0
- package/dist/CHANGELOG.md +13 -0
- package/dist/README.md +52 -12
- package/dist/SKILLS.md +2 -0
- package/dist/cli/commands/openclaw-install.js +1 -1
- package/dist/cli/index.js +12 -0
- package/dist/enforcement/index.d.ts +4 -0
- package/dist/enforcement/index.js +4 -0
- package/dist/enforcement/validators/code-quality-validators.js +6 -4
- package/dist/governance/llm-governance-provider.d.ts +7 -1
- package/dist/governance/llm-governance-provider.js +72 -14
- package/dist/governance/local-confer.d.ts +4 -1
- package/dist/governance/local-confer.js +22 -1
- package/dist/integrations/grok/grok-cli.js +10 -37
- package/dist/integrations/grok/hooks/grok-hook-utils.js +8 -1
- package/dist/integrations/hooks/confer-hook-runtime.mjs +23 -1
- package/dist/integrations/hooks/goggles-planes.json +4 -1
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/mcps/architect-tools.server.d.ts +6 -0
- package/dist/mcps/architect-tools.server.js +19 -2
- package/dist/mcps/config/server-config-registry.js +8 -6
- package/dist/mcps/governance.server.d.ts +1 -0
- package/dist/mcps/governance.server.js +46 -0
- package/dist/mcps/knowledge-skills/code-review.server.js +7 -0
- package/dist/mcps/knowledge-skills/security-audit.server.js +5 -3
- package/dist/mcps/mcp-client.js +1 -1
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +2 -2
- package/dist/mcps/researcher.server.js +12 -3
- package/dist/nucleus/autonomy-kernel.d.ts +2 -0
- package/dist/nucleus/autonomy-kernel.js +17 -13
- package/dist/nucleus/confer-host.d.ts +35 -0
- package/dist/nucleus/confer-host.js +55 -0
- package/dist/nucleus/confer.d.ts +19 -3
- package/dist/nucleus/confer.js +111 -55
- package/dist/nucleus/lead-dev-plan-persistence.js +4 -2
- package/dist/nucleus/suit-temperament.d.ts +1 -1
- package/dist/nucleus/suit-temperament.js +2 -2
- package/dist/nucleus/synthesis-consult-receipt.d.ts +7 -1
- package/dist/nucleus/synthesis-consult-receipt.js +86 -35
- package/dist/reporting/framework-reporting-system.js +2 -5
- package/dist/reporting/log-parser.d.ts +2 -0
- package/dist/reporting/log-parser.js +7 -6
- package/dist/skills/orchestrator/SKILL.md +12 -0
- package/docs-site/docs/plates/activity-log.md +1 -1
- package/docs-site/docs/plates/boot.md +1 -1
- 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 +1 -1
- package/docs-site/docs/plates/goggles.md +1 -1
- 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 +1 -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 +1 -1
- 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 +1 -1
- package/docs-site/docs/plates/lens-page.md +1 -1
- package/docs-site/docs/plates/live-mesh.md +1 -1
- package/docs-site/docs/plates/memory-recall.md +1 -1
- package/docs-site/docs/plates/notes-page.md +1 -1
- 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 +1 -1
- 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 +2 -0
- package/package.json +2 -2
- package/scripts/mjs/verify-confer-core.mjs +153 -29
- package/scripts/mjs/verify-user-aside-core.mjs +3 -4
- package/scripts/node/bridge-mcp-wiring.cjs +236 -5
- package/scripts/node/confer-switch.cjs +66 -0
- package/scripts/node/install-bridges.cjs +16 -41
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/mcps/architect-tools.server.ts +26 -2
- package/src/mcps/config/__tests__/server-config-registry.test.ts +15 -6
- package/src/mcps/config/server-config-registry.ts +9 -6
- package/src/mcps/governance.server.ts +49 -0
- package/src/mcps/knowledge-skills/code-review.server.ts +8 -0
- package/src/mcps/knowledge-skills/security-audit.server.ts +11 -12
- package/src/mcps/mcp-client.ts +1 -1
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +2 -1
- package/src/mcps/researcher.server.ts +19 -10
- package/src/skills/orchestrator/SKILL.md +12 -0
- package/vendor/@0xray/repertoire/scripts/run-live-synthesis-checkpoint.mjs +5 -51
- package/xray/features.json +1 -1
- package/xray/features.schema.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Confer quorum fixture
|
|
3
|
+
* Confer quorum fixture.
|
|
4
|
+
* PASS completes. FAIL and no-LLM UNREVIEWED leave the todo open and the checkpoint due.
|
|
5
|
+
* Temp projects opt into synthesis so a due checkpoint can be observed. The repo default stays off.
|
|
4
6
|
*/
|
|
5
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
8
|
import { tmpdir } from 'node:os';
|
|
7
9
|
import { dirname, join } from 'node:path';
|
|
8
10
|
import { fileURLToPath } from 'node:url';
|
|
@@ -10,10 +12,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
const packageRoot = join(__dirname, '../..');
|
|
12
14
|
|
|
15
|
+
// Fixture PASS is refused unless this process is a test. The live orchestrate-task
|
|
16
|
+
// flag and XRAY_CONFER_FIXTURE stay unreachable when NODE_ENV is not test.
|
|
17
|
+
process.env.NODE_ENV = 'test';
|
|
18
|
+
|
|
13
19
|
const { buildSynthesisCheckpointPlan } = await import(
|
|
14
20
|
join(packageRoot, 'dist/nucleus/autonomy-kernel.js')
|
|
15
21
|
);
|
|
16
|
-
const { savePersistedLeadDevPlan, areSynthesisConsultTodosComplete } = await import(
|
|
22
|
+
const { savePersistedLeadDevPlan, areSynthesisConsultTodosComplete, loadPersistedLeadDevPlan } = await import(
|
|
17
23
|
join(packageRoot, 'dist/nucleus/lead-dev-plan-persistence.js')
|
|
18
24
|
);
|
|
19
25
|
const { recordExecutionSlice, isSynthesisCheckpointDue } = await import(
|
|
@@ -22,46 +28,103 @@ const { recordExecutionSlice, isSynthesisCheckpointDue } = await import(
|
|
|
22
28
|
const { runConferQuorum, loadConferCheckpoint, CONFER_AGENTS } = await import(
|
|
23
29
|
join(packageRoot, 'dist/nucleus/confer.js')
|
|
24
30
|
);
|
|
31
|
+
const { loadSynthesisConsultReceipt } = await import(
|
|
32
|
+
join(packageRoot, 'dist/nucleus/synthesis-consult-receipt.js')
|
|
33
|
+
);
|
|
34
|
+
const { readConferReviewHint } = await import(
|
|
35
|
+
join(packageRoot, 'src/integrations/hooks/confer-hook-runtime.mjs')
|
|
36
|
+
);
|
|
37
|
+
const { buildSessionBootPayload } = await import(
|
|
38
|
+
join(packageRoot, 'src/integrations/grok/hooks/grok-hook-utils.js')
|
|
39
|
+
);
|
|
25
40
|
|
|
26
|
-
const sessionId = 'verify-confer-core';
|
|
27
41
|
let failed = 0;
|
|
42
|
+
let checks = 0;
|
|
28
43
|
function pass(n) {
|
|
44
|
+
checks++;
|
|
29
45
|
console.log(`✅ ${n}`);
|
|
30
46
|
}
|
|
31
47
|
function fail(n, d = '') {
|
|
32
48
|
failed++;
|
|
49
|
+
checks++;
|
|
33
50
|
console.error(`❌ ${n}${d ? ` — ${d}` : ''}`);
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
multi_agent_orchestration: { lead_dev_mode: true, confer_on_synthesis: true },
|
|
42
|
-
synthesis: { enabled: true, every_n_gates: 1, every_n_turns: 0, every_n_todos_completed: 0 },
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
53
|
+
function assertRepoSynthesisStaysOff() {
|
|
54
|
+
const source = readFileSync(join(packageRoot, 'src/nucleus/synthesis.ts'), 'utf8');
|
|
55
|
+
const defaults = source.match(/function defaultSynthesisConfig\(\)[\s\S]*?return \{([\s\S]*?)\n \};/);
|
|
56
|
+
if (defaults && /enabled:\s*false/.test(defaults[1])) pass('repo defaultSynthesisConfig stays enabled: false');
|
|
57
|
+
else fail('repo defaultSynthesisConfig stays enabled: false');
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
for (const rel of ['xray/features.json', '.xray/features.json']) {
|
|
60
|
+
const data = JSON.parse(readFileSync(join(packageRoot, rel), 'utf8'));
|
|
61
|
+
if (data.synthesis?.enabled === false) pass(`${rel} synthesis.enabled stays false`);
|
|
62
|
+
else fail(`${rel} synthesis.enabled stays false`, JSON.stringify(data.synthesis));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
50
65
|
|
|
66
|
+
function makeProject() {
|
|
67
|
+
const tmp = mkdtempSync(join(tmpdir(), 'xray-confer-verify-'));
|
|
68
|
+
mkdirSync(join(tmp, '.xray', 'state'), { recursive: true });
|
|
69
|
+
writeFileSync(
|
|
70
|
+
join(tmp, '.xray', 'features.json'),
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
multi_agent_orchestration: { lead_dev_mode: true, confer_on_synthesis: true },
|
|
73
|
+
synthesis: { enabled: true, every_n_gates: 1, every_n_turns: 0, every_n_todos_completed: 0 },
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
return tmp;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function seedDuePlan(tmp, sessionId) {
|
|
80
|
+
recordExecutionSlice('gate', { projectRoot: tmp, sessionId });
|
|
81
|
+
const due = isSynthesisCheckpointDue(tmp, sessionId);
|
|
51
82
|
const plan = buildSynthesisCheckpointPlan('gate threshold (1/1)', tmp);
|
|
52
|
-
if (
|
|
53
|
-
else {
|
|
83
|
+
if (plan) {
|
|
54
84
|
savePersistedLeadDevPlan(
|
|
55
85
|
{ ...plan, persistedAt: new Date().toISOString(), sessionId },
|
|
56
86
|
tmp,
|
|
57
87
|
);
|
|
58
|
-
pass('step 2: synthesis plan persisted');
|
|
59
88
|
}
|
|
89
|
+
return { due, plan };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function blocksCheckpoint(label, tmp, sessionId, result, verdict) {
|
|
93
|
+
const receipt = loadSynthesisConsultReceipt('s.1', tmp);
|
|
94
|
+
if (result.status === 'partial' && result.agents[0]?.verdict === verdict && result.agents[0]?.todoCompleted === false) {
|
|
95
|
+
pass(`${label}: quorum stopped on ${verdict}`);
|
|
96
|
+
} else {
|
|
97
|
+
fail(`${label}: quorum stopped on ${verdict}`, JSON.stringify(result));
|
|
98
|
+
}
|
|
99
|
+
if (receipt?.verdict === verdict) pass(`${label}: receipt verdict ${verdict}`);
|
|
100
|
+
else fail(`${label}: receipt verdict ${verdict}`, JSON.stringify(receipt));
|
|
101
|
+
if (!areSynthesisConsultTodosComplete(loadPersistedLeadDevPlan(tmp))) {
|
|
102
|
+
pass(`${label}: consult todo stays open`);
|
|
103
|
+
} else {
|
|
104
|
+
fail(`${label}: consult todo stays open`);
|
|
105
|
+
}
|
|
106
|
+
if (isSynthesisCheckpointDue(tmp, sessionId)) pass(`${label}: synthesis checkpoint stays due`);
|
|
107
|
+
else fail(`${label}: synthesis checkpoint stays due`);
|
|
108
|
+
const state = loadConferCheckpoint(tmp);
|
|
109
|
+
if (state?.status === 'failed') pass(`${label}: confer checkpoint not cleared`);
|
|
110
|
+
else fail(`${label}: confer checkpoint not cleared`, JSON.stringify(state));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
assertRepoSynthesisStaysOff();
|
|
114
|
+
|
|
115
|
+
const passRoot = makeProject();
|
|
116
|
+
const passSession = 'verify-confer-core-pass';
|
|
117
|
+
try {
|
|
118
|
+
const seeded = seedDuePlan(passRoot, passSession);
|
|
119
|
+
if (!seeded.due) fail('step 1: synthesis due');
|
|
120
|
+
else pass('step 1: synthesis checkpoint due');
|
|
121
|
+
if (!seeded.plan) fail('step 2: synthesis plan');
|
|
122
|
+
else pass('step 2: synthesis plan persisted');
|
|
60
123
|
|
|
61
124
|
if (CONFER_AGENTS.length !== 3) fail('step 3: confer agent count', String(CONFER_AGENTS.length));
|
|
62
125
|
else pass('step 3: three confer agents defined');
|
|
63
126
|
|
|
64
|
-
const result = await runConferQuorum(
|
|
127
|
+
const result = await runConferQuorum(passRoot, passSession, {
|
|
65
128
|
collocatedText: 'verify confer fixture context',
|
|
66
129
|
dueReason: 'gate threshold',
|
|
67
130
|
fixture: true,
|
|
@@ -73,31 +136,92 @@ try {
|
|
|
73
136
|
fail('step 4: confer quorum', JSON.stringify(result));
|
|
74
137
|
}
|
|
75
138
|
|
|
76
|
-
const state = loadConferCheckpoint(
|
|
139
|
+
const state = loadConferCheckpoint(passRoot);
|
|
77
140
|
if (state?.status === 'completed' && state.completedAgents.length === 3) {
|
|
78
141
|
pass('step 5: confer checkpoint state completed');
|
|
79
142
|
} else {
|
|
80
143
|
fail('step 5: confer state', JSON.stringify(state));
|
|
81
144
|
}
|
|
82
145
|
|
|
83
|
-
if (areSynthesisConsultTodosComplete(
|
|
84
|
-
(await import(join(packageRoot, 'dist/nucleus/lead-dev-plan-persistence.js'))).loadPersistedLeadDevPlan(tmp),
|
|
85
|
-
)) {
|
|
146
|
+
if (areSynthesisConsultTodosComplete(loadPersistedLeadDevPlan(passRoot))) {
|
|
86
147
|
pass('step 6: synthesis consult todos complete after confer');
|
|
87
148
|
} else {
|
|
88
149
|
fail('step 6: consult todos incomplete');
|
|
89
150
|
}
|
|
90
151
|
|
|
91
|
-
if (!isSynthesisCheckpointDue(
|
|
152
|
+
if (!isSynthesisCheckpointDue(passRoot, passSession)) pass('step 7: synthesis checkpoint cleared');
|
|
92
153
|
else fail('step 7: checkpoint still due');
|
|
93
154
|
} finally {
|
|
94
|
-
rmSync(
|
|
155
|
+
rmSync(passRoot, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const failRoot = makeProject();
|
|
159
|
+
const failSession = 'verify-confer-core-fail';
|
|
160
|
+
try {
|
|
161
|
+
seedDuePlan(failRoot, failSession);
|
|
162
|
+
const result = await runConferQuorum(failRoot, failSession, {
|
|
163
|
+
fixture: true,
|
|
164
|
+
fixtureVerdict: 'FAIL',
|
|
165
|
+
});
|
|
166
|
+
blocksCheckpoint('FAIL', failRoot, failSession, result, 'FAIL');
|
|
167
|
+
} finally {
|
|
168
|
+
rmSync(failRoot, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const unreviewedRoot = makeProject();
|
|
172
|
+
const unreviewedSession = 'verify-confer-core-unreviewed';
|
|
173
|
+
try {
|
|
174
|
+
seedDuePlan(unreviewedRoot, unreviewedSession);
|
|
175
|
+
const result = await runConferQuorum(unreviewedRoot, unreviewedSession, {
|
|
176
|
+
fixture: true,
|
|
177
|
+
fixtureNoLlm: true,
|
|
178
|
+
});
|
|
179
|
+
blocksCheckpoint('no-LLM', unreviewedRoot, unreviewedSession, result, 'UNREVIEWED');
|
|
180
|
+
if (readConferReviewHint(unreviewedRoot) === 'UNREVIEWED') {
|
|
181
|
+
pass('no-LLM: confer review hint is UNREVIEWED');
|
|
182
|
+
} else {
|
|
183
|
+
fail('no-LLM: confer review hint is UNREVIEWED');
|
|
184
|
+
}
|
|
185
|
+
const boot = buildSessionBootPayload(unreviewedRoot, 'verify/session-start');
|
|
186
|
+
if (boot.conferReview === 'UNREVIEWED' && String(boot.conferReviewHint).includes('UNREVIEWED')) {
|
|
187
|
+
pass('no-LLM: session-boot hint records UNREVIEWED');
|
|
188
|
+
} else {
|
|
189
|
+
fail('no-LLM: session-boot hint records UNREVIEWED', JSON.stringify({
|
|
190
|
+
conferReview: boot.conferReview,
|
|
191
|
+
conferReviewHint: boot.conferReviewHint,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
} finally {
|
|
195
|
+
rmSync(unreviewedRoot, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const conditionalRoot = makeProject();
|
|
199
|
+
const conditionalSession = 'verify-confer-core-conditional';
|
|
200
|
+
try {
|
|
201
|
+
seedDuePlan(conditionalRoot, conditionalSession);
|
|
202
|
+
const result = await runConferQuorum(conditionalRoot, conditionalSession, {
|
|
203
|
+
fixture: true,
|
|
204
|
+
fixtureVerdict: 'CONDITIONAL',
|
|
205
|
+
});
|
|
206
|
+
const verdicts = result.agents.map((agent) => agent.verdict);
|
|
207
|
+
if (result.status === 'partial' && verdicts[0] === 'CONDITIONAL' && result.agents[0]?.todoCompleted === false) {
|
|
208
|
+
pass('CONDITIONAL: explicit conditional does not complete');
|
|
209
|
+
} else {
|
|
210
|
+
fail('CONDITIONAL: explicit conditional does not complete', JSON.stringify(result));
|
|
211
|
+
}
|
|
212
|
+
if (isSynthesisCheckpointDue(conditionalRoot, conditionalSession)) {
|
|
213
|
+
pass('CONDITIONAL: checkpoint stays due');
|
|
214
|
+
} else {
|
|
215
|
+
fail('CONDITIONAL: checkpoint stays due');
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
rmSync(conditionalRoot, { recursive: true, force: true });
|
|
95
219
|
}
|
|
96
220
|
|
|
97
221
|
console.log(
|
|
98
222
|
'\n' +
|
|
99
223
|
(failed === 0
|
|
100
|
-
?
|
|
224
|
+
? `🎉 Confer verify passed (${checks}/${checks}).`
|
|
101
225
|
: `⚠️ ${failed} confer check(s) failed.`),
|
|
102
226
|
);
|
|
103
|
-
process.exit(failed === 0 ? 0 : 1);
|
|
227
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -75,16 +75,15 @@ try {
|
|
|
75
75
|
35,
|
|
76
76
|
tmp,
|
|
77
77
|
);
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const todo = aside?.plan.phases.flatMap((phase) => phase.todos)[0];
|
|
79
|
+
if (todo?.id !== 'suit-nft.a.2.1') {
|
|
80
|
+
fail('step 3: build aside plan', todo?.id);
|
|
80
81
|
} else pass('step 3: namespaced aside todos');
|
|
81
82
|
|
|
82
83
|
saveUserAside(aside, tmp);
|
|
83
84
|
setActiveAsideId('suit-nft', tmp, 'verify-session');
|
|
84
85
|
if (getActiveAsideId(tmp, 'verify-session') !== 'suit-nft') fail('step 4: active pointer');
|
|
85
86
|
else pass('step 4: active aside pointer with session');
|
|
86
|
-
|
|
87
|
-
const todo = aside.plan.phases[0].todos[0];
|
|
88
87
|
const resolved = resolveSpawnPlan({ planTodoId: todo.id }, tmp, 'verify-session');
|
|
89
88
|
if (resolved.source !== 'aside') fail('step 5: resolve spawn plan', JSON.stringify(resolved));
|
|
90
89
|
else pass('step 5: spawn plan resolves to aside');
|
|
@@ -869,7 +869,125 @@ function resolveOpenClawPluginDir(packageRoot) {
|
|
|
869
869
|
].find((p) => fs.existsSync(path.join(p, "index.js"))) || null;
|
|
870
870
|
}
|
|
871
871
|
|
|
872
|
-
function
|
|
872
|
+
function isCheckoutOpenClawPluginPath(pluginPath) {
|
|
873
|
+
const normalized = String(pluginPath || "").replace(/\\/g, "/");
|
|
874
|
+
return (
|
|
875
|
+
normalized.includes("/src/integrations/openclaw/plugin/xray-pre-tool") &&
|
|
876
|
+
!normalized.includes("/node_modules/")
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function isInstalledOpenClawPluginPath(pluginPath) {
|
|
881
|
+
const normalized = String(pluginPath || "").replace(/\\/g, "/");
|
|
882
|
+
return (
|
|
883
|
+
normalized.includes("/node_modules/") &&
|
|
884
|
+
normalized.includes("/integrations/openclaw/plugin/xray-pre-tool")
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** Installed package first. A checkout src path drops once any installed plugin is present. */
|
|
889
|
+
function settleOpenClawPluginPaths(paths, wornPluginDir) {
|
|
890
|
+
const list = (Array.isArray(paths) ? paths : []).map((entry) => String(entry));
|
|
891
|
+
const wornIsInstalled = Boolean(wornPluginDir && isInstalledOpenClawPluginPath(wornPluginDir));
|
|
892
|
+
const dropCheckout =
|
|
893
|
+
wornIsInstalled || list.some((entry) => isInstalledOpenClawPluginPath(entry));
|
|
894
|
+
const next = [];
|
|
895
|
+
const seen = new Set();
|
|
896
|
+
const push = (entry) => {
|
|
897
|
+
if (!entry) return;
|
|
898
|
+
const resolved = path.resolve(String(entry));
|
|
899
|
+
if (dropCheckout && isCheckoutOpenClawPluginPath(resolved)) return;
|
|
900
|
+
if (seen.has(resolved)) return;
|
|
901
|
+
seen.add(resolved);
|
|
902
|
+
next.push(resolved);
|
|
903
|
+
};
|
|
904
|
+
if (wornIsInstalled) push(wornPluginDir);
|
|
905
|
+
for (const entry of list) push(entry);
|
|
906
|
+
return next;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function resolveWornOpenClawPlugin(targetDir, packageRoot) {
|
|
910
|
+
const candidates = [];
|
|
911
|
+
if (targetDir) {
|
|
912
|
+
candidates.push(
|
|
913
|
+
path.join(targetDir, "node_modules", "0xray", "dist", "integrations", "openclaw", "plugin", "xray-pre-tool"),
|
|
914
|
+
path.join(targetDir, "node_modules", "0xray", "src", "integrations", "openclaw", "plugin", "xray-pre-tool"),
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
const fromPackage = resolveOpenClawPluginDir(packageRoot);
|
|
918
|
+
if (fromPackage) candidates.push(fromPackage);
|
|
919
|
+
return candidates.find((dir) => fs.existsSync(path.join(dir, "index.js"))) || null;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function replaceJsonPathsArray(raw, before, next) {
|
|
923
|
+
const keyAt = raw.indexOf('"paths"');
|
|
924
|
+
if (keyAt < 0) return null;
|
|
925
|
+
const bracket = raw.indexOf("[", keyAt);
|
|
926
|
+
if (bracket < 0) return null;
|
|
927
|
+
let depth = 0;
|
|
928
|
+
let end = -1;
|
|
929
|
+
for (let i = bracket; i < raw.length; i += 1) {
|
|
930
|
+
const ch = raw[i];
|
|
931
|
+
if (ch === "[") depth += 1;
|
|
932
|
+
else if (ch === "]") {
|
|
933
|
+
depth -= 1;
|
|
934
|
+
if (depth === 0) {
|
|
935
|
+
end = i;
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (end < 0) return null;
|
|
941
|
+
let parsed;
|
|
942
|
+
try {
|
|
943
|
+
parsed = JSON.parse(raw.slice(bracket, end + 1));
|
|
944
|
+
} catch {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
if (JSON.stringify(parsed) !== JSON.stringify(before)) return null;
|
|
948
|
+
const lineStart = raw.lastIndexOf("\n", keyAt) + 1;
|
|
949
|
+
const indent = (raw.slice(lineStart, keyAt).match(/^\s*/) || [""])[0];
|
|
950
|
+
const itemIndent = `${indent} `;
|
|
951
|
+
const body = next
|
|
952
|
+
.map((entry) => `${itemIndent}${JSON.stringify(entry)}`)
|
|
953
|
+
.join(",\n");
|
|
954
|
+
const array = `[\n${body}\n${indent}]`;
|
|
955
|
+
return `${raw.slice(0, bracket)}${array}${raw.slice(end + 1)}`;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** Rewrite plugins.load.paths in place. Leaves the rest of the config file alone. */
|
|
959
|
+
function pinOpenClawPluginLoad(configPath, wornPluginDir) {
|
|
960
|
+
if (!configPath || !fs.existsSync(configPath)) return null;
|
|
961
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
962
|
+
const config = JSON.parse(raw);
|
|
963
|
+
if (!config.plugins || typeof config.plugins !== "object") return null;
|
|
964
|
+
if (!config.plugins.load || typeof config.plugins.load !== "object") config.plugins.load = {};
|
|
965
|
+
const before = Array.isArray(config.plugins.load.paths) ? config.plugins.load.paths : [];
|
|
966
|
+
const next = settleOpenClawPluginPaths(before, wornPluginDir);
|
|
967
|
+
if (JSON.stringify(before) === JSON.stringify(next)) return next;
|
|
968
|
+
const replaced = replaceJsonPathsArray(raw, before, next);
|
|
969
|
+
if (replaced == null) return null;
|
|
970
|
+
fs.writeFileSync(configPath, replaced);
|
|
971
|
+
return next;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function recordOpenClawProjectPlugin(targetDir, packageRoot) {
|
|
975
|
+
const pluginDir = resolveWornOpenClawPlugin(targetDir, packageRoot);
|
|
976
|
+
if (!pluginDir || !isInstalledOpenClawPluginPath(pluginDir)) return null;
|
|
977
|
+
const configPath = path.join(targetDir, ".xray", "config", "openclaw.json");
|
|
978
|
+
if (!fs.existsSync(configPath)) return null;
|
|
979
|
+
let config;
|
|
980
|
+
try {
|
|
981
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
982
|
+
} catch {
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
config.pluginPath = path.resolve(pluginDir);
|
|
986
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
987
|
+
return config.pluginPath;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function installOpenClawHostWear(packageRoot, targetDir) {
|
|
873
991
|
if (millSuit.isIsolatedHome()) return null;
|
|
874
992
|
const source = resolveOpenClawPreToolHookSource(packageRoot);
|
|
875
993
|
if (!source) return null;
|
|
@@ -878,6 +996,11 @@ function installOpenClawHostWear(packageRoot) {
|
|
|
878
996
|
fs.mkdirSync(hookDir, { recursive: true });
|
|
879
997
|
const dest = path.join(hookDir, "xray-pre-tool.mjs");
|
|
880
998
|
fs.copyFileSync(source, dest);
|
|
999
|
+
const worn = resolveWornOpenClawPlugin(targetDir, packageRoot);
|
|
1000
|
+
const hookRoot =
|
|
1001
|
+
worn && isInstalledOpenClawPluginPath(worn) && targetDir
|
|
1002
|
+
? path.join(path.resolve(targetDir), "node_modules", "0xray")
|
|
1003
|
+
: packageRoot;
|
|
881
1004
|
fs.writeFileSync(
|
|
882
1005
|
path.join(hookDir, "xray-pre-tool.json"),
|
|
883
1006
|
`${JSON.stringify(
|
|
@@ -887,17 +1010,16 @@ function installOpenClawHostWear(packageRoot) {
|
|
|
887
1010
|
args: [dest],
|
|
888
1011
|
stdin: "json",
|
|
889
1012
|
blockExitCode: 2,
|
|
890
|
-
env: { XRAY_AI_PATH:
|
|
1013
|
+
env: { XRAY_AI_PATH: hookRoot },
|
|
891
1014
|
},
|
|
892
1015
|
null,
|
|
893
1016
|
2,
|
|
894
1017
|
)}\n`,
|
|
895
1018
|
);
|
|
896
1019
|
|
|
897
|
-
|
|
898
|
-
if (pluginSrc) {
|
|
1020
|
+
if (worn && isInstalledOpenClawPluginPath(worn)) {
|
|
899
1021
|
try {
|
|
900
|
-
execFileSync("openclaw", ["plugins", "install", "-l",
|
|
1022
|
+
execFileSync("openclaw", ["plugins", "install", "-l", path.resolve(worn)], {
|
|
901
1023
|
stdio: "pipe",
|
|
902
1024
|
encoding: "utf8",
|
|
903
1025
|
timeout: 60000,
|
|
@@ -915,6 +1037,13 @@ function installOpenClawHostWear(packageRoot) {
|
|
|
915
1037
|
/* enable is best-effort */
|
|
916
1038
|
}
|
|
917
1039
|
}
|
|
1040
|
+
const configPath = resolveOpenClawConfigPath();
|
|
1041
|
+
if (fs.existsSync(configPath)) {
|
|
1042
|
+
pinOpenClawPluginLoad(
|
|
1043
|
+
configPath,
|
|
1044
|
+
worn && isInstalledOpenClawPluginPath(worn) ? worn : null,
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
918
1047
|
return dest;
|
|
919
1048
|
}
|
|
920
1049
|
|
|
@@ -979,6 +1108,102 @@ function wireOpenClawBridge(targetDir) {
|
|
|
979
1108
|
return syncOpenClawMcpRegistry(targetDir);
|
|
980
1109
|
}
|
|
981
1110
|
|
|
1111
|
+
function tomlString(value) {
|
|
1112
|
+
return JSON.stringify(String(value));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function suitServerNames() {
|
|
1116
|
+
return new Set(XRAY_MCP_SERVERS.map((server) => server.name));
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function isSuitMcpHeader(line, names) {
|
|
1120
|
+
const match = /^\[([^\]]+)\]\s*$/.exec(line.trim());
|
|
1121
|
+
if (!match) return false;
|
|
1122
|
+
const parts = match[1].split(".");
|
|
1123
|
+
return parts[0] === "mcp_servers" && names.has(parts[1]);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function stripSuitMcpBlocks(toml, names) {
|
|
1127
|
+
const kept = [];
|
|
1128
|
+
let skip = false;
|
|
1129
|
+
for (const line of String(toml || "").split(/\r?\n/)) {
|
|
1130
|
+
if (/^\s*\[/.test(line)) skip = isSuitMcpHeader(line, names);
|
|
1131
|
+
if (!skip) kept.push(line);
|
|
1132
|
+
}
|
|
1133
|
+
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function suitMcpBlock(server, launch) {
|
|
1137
|
+
const args = launch.args.map((arg) => ` ${tomlString(arg)},`).join("\n");
|
|
1138
|
+
const lines = [
|
|
1139
|
+
`[mcp_servers.${server.name}]`,
|
|
1140
|
+
`command = ${tomlString(launch.command)}`,
|
|
1141
|
+
"args = [",
|
|
1142
|
+
args,
|
|
1143
|
+
"]",
|
|
1144
|
+
"enabled = true",
|
|
1145
|
+
];
|
|
1146
|
+
const env = launch.env && typeof launch.env === "object" ? launch.env : {};
|
|
1147
|
+
const envKeys = Object.keys(env);
|
|
1148
|
+
if (envKeys.length > 0) {
|
|
1149
|
+
lines.push("", `[mcp_servers.${server.name}.env]`);
|
|
1150
|
+
for (const key of envKeys) lines.push(`${key} = ${tomlString(env[key])}`);
|
|
1151
|
+
}
|
|
1152
|
+
return lines.join("\n");
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Project `.grok/config.toml` replaces a same-named user server.
|
|
1157
|
+
* The launch is the worn package, not `npx -y 0xray`.
|
|
1158
|
+
*/
|
|
1159
|
+
function writeProjectSuitMcp(targetDir) {
|
|
1160
|
+
const root = path.resolve(targetDir);
|
|
1161
|
+
const names = suitServerNames();
|
|
1162
|
+
const blocks = XRAY_MCP_SERVERS.map((server) =>
|
|
1163
|
+
suitMcpBlock(server, scopedMcpLaunch(root, server.mcpCmd, server.env)),
|
|
1164
|
+
);
|
|
1165
|
+
const grokDir = path.join(root, ".grok");
|
|
1166
|
+
fs.mkdirSync(grokDir, { recursive: true });
|
|
1167
|
+
const tomlPath = path.join(grokDir, "config.toml");
|
|
1168
|
+
const existing = fs.existsSync(tomlPath) ? fs.readFileSync(tomlPath, "utf8") : "";
|
|
1169
|
+
const kept = stripSuitMcpBlocks(existing, names);
|
|
1170
|
+
const body = [kept, blocks.join("\n\n")].filter((part) => part && part.trim()).join("\n\n");
|
|
1171
|
+
fs.writeFileSync(tomlPath, `${body.trim()}\n`);
|
|
1172
|
+
return tomlPath;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function grokConfigHome(env) {
|
|
1176
|
+
const source = env || process.env;
|
|
1177
|
+
const home = source.HOME || source.USERPROFILE;
|
|
1178
|
+
return home ? path.resolve(home) : os.homedir();
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/** Grok 1.0.50 has no --trust flag. An untrusted folder skips project MCP. */
|
|
1182
|
+
function trustGrokFolder(targetDir, env) {
|
|
1183
|
+
const home = grokConfigHome(env);
|
|
1184
|
+
const grokDir = path.join(home, ".grok");
|
|
1185
|
+
fs.mkdirSync(grokDir, { recursive: true });
|
|
1186
|
+
const file = path.join(grokDir, "trusted_folders.toml");
|
|
1187
|
+
let text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
1188
|
+
const folders = [path.resolve(targetDir)];
|
|
1189
|
+
try {
|
|
1190
|
+
const real = fs.realpathSync(folders[0]);
|
|
1191
|
+
if (real !== folders[0]) folders.push(real);
|
|
1192
|
+
} catch {
|
|
1193
|
+
/* the path is the one Grok will see */
|
|
1194
|
+
}
|
|
1195
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1196
|
+
let added = 0;
|
|
1197
|
+
for (const folder of folders) {
|
|
1198
|
+
const header = `[folders.${tomlString(folder)}]`;
|
|
1199
|
+
if (text.includes(header)) continue;
|
|
1200
|
+
text = `${text.replace(/\s*$/, "")}\n${header}\ntrusted = true\ndecided_at = ${now}\n`;
|
|
1201
|
+
added += 1;
|
|
1202
|
+
}
|
|
1203
|
+
if (added > 0) fs.writeFileSync(file, text.endsWith("\n") ? text : `${text}\n`);
|
|
1204
|
+
return file;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
982
1207
|
module.exports = {
|
|
983
1208
|
XRAY_MCP_SERVERS,
|
|
984
1209
|
HERMES_CONFIG_PATH,
|
|
@@ -1025,5 +1250,11 @@ module.exports = {
|
|
|
1025
1250
|
resolveOpenClawPreToolHookSource,
|
|
1026
1251
|
resolveOpenClawPluginDir,
|
|
1027
1252
|
installOpenClawHostWear,
|
|
1253
|
+
settleOpenClawPluginPaths,
|
|
1254
|
+
pinOpenClawPluginLoad,
|
|
1255
|
+
recordOpenClawProjectPlugin,
|
|
1256
|
+
resolveWornOpenClawPlugin,
|
|
1028
1257
|
maybeWriteOpenClawCliBackend,
|
|
1258
|
+
writeProjectSuitMcp,
|
|
1259
|
+
trustGrokFolder,
|
|
1029
1260
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Turn confer on or off in the worn .xray/features.json.
|
|
4
|
+
* A missing key stays off until this runs. Synthesis stays untouched.
|
|
5
|
+
*/
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
function fail(message) {
|
|
12
|
+
process.stderr.write(`${message}\n`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function orchestrationBlock(data) {
|
|
17
|
+
const current = data.multi_agent_orchestration;
|
|
18
|
+
if (current && typeof current === "object" && !Array.isArray(current)) return current;
|
|
19
|
+
const created = {};
|
|
20
|
+
data.multi_agent_orchestration = created;
|
|
21
|
+
return created;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function conferBlock(orch) {
|
|
25
|
+
const current = orch.confer;
|
|
26
|
+
if (current && typeof current === "object" && !Array.isArray(current)) return current;
|
|
27
|
+
const created = {};
|
|
28
|
+
orch.confer = created;
|
|
29
|
+
return created;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function main() {
|
|
33
|
+
const state = process.argv[2];
|
|
34
|
+
if (state !== "on" && state !== "off") fail("confer: say on or off");
|
|
35
|
+
|
|
36
|
+
const featuresPath = path.join(process.cwd(), ".xray", "features.json");
|
|
37
|
+
if (!fs.existsSync(featuresPath)) {
|
|
38
|
+
fail("confer: .xray/features.json is missing. Run npx 0xray wear first.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let data;
|
|
42
|
+
try {
|
|
43
|
+
data = JSON.parse(fs.readFileSync(featuresPath, "utf8"));
|
|
44
|
+
} catch {
|
|
45
|
+
fail("confer: .xray/features.json is not valid JSON");
|
|
46
|
+
}
|
|
47
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
48
|
+
fail("confer: .xray/features.json is not valid JSON");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const orch = orchestrationBlock(data);
|
|
52
|
+
const confer = conferBlock(orch);
|
|
53
|
+
if (state === "on") {
|
|
54
|
+
confer.enabled = true;
|
|
55
|
+
if (confer.on_synthesis !== false) confer.on_synthesis = true;
|
|
56
|
+
} else {
|
|
57
|
+
confer.enabled = false;
|
|
58
|
+
confer.on_synthesis = false;
|
|
59
|
+
orch.confer_on_synthesis = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fs.writeFileSync(featuresPath, `${JSON.stringify(data, null, 2)}\n`);
|
|
63
|
+
process.stdout.write(`confer: ${state}\n`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (require.main === module) main();
|