0xray 3.5.0 → 3.5.2
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/.gitignore.default +8 -0
- package/.opencode/hooks/post-commit +7 -27
- package/.xray/features.json +6 -0
- package/.xray/features.schema.json +17 -0
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +1 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/.gitignore.default +8 -0
- package/dist/AGENTS.md +1 -1
- package/dist/CHANGELOG.md +32 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/core/bridge.mjs +10 -32
- package/dist/core/features-config.d.ts +16 -0
- package/dist/core/features-config.js +24 -0
- package/dist/integrations/grok/hooks/grok-hook-utils.js +32 -385
- package/dist/integrations/grok/hooks/post-tool-use.js +1 -1
- package/dist/integrations/grok/hooks/pre-tool-use.js +23 -32
- package/dist/integrations/grok/hooks/session-start.js +20 -1
- package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +2 -1
- package/dist/integrations/hermes-agent/__init__.py +50 -3
- package/dist/integrations/hermes-agent/bridge.mjs +71 -30
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +75 -0
- package/dist/integrations/hooks/synthesis-hook-runtime.mjs +50 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +3 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +25 -10
- package/dist/mcps/orchestrator/server.js +124 -4
- package/dist/memory-routing/types.d.ts +7 -0
- package/dist/nucleus/autonomy-kernel.d.ts +3 -0
- package/dist/nucleus/autonomy-kernel.js +40 -0
- package/dist/nucleus/delegation-gate.d.ts +88 -0
- package/dist/nucleus/delegation-gate.js +347 -0
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +3 -0
- package/dist/nucleus/lead-dev-plan-persistence.js +47 -4
- package/dist/nucleus/synthesis-completion.d.ts +6 -0
- package/dist/nucleus/synthesis-completion.js +29 -0
- package/dist/nucleus/synthesis-context.d.ts +31 -0
- package/dist/nucleus/synthesis-context.js +86 -0
- package/dist/nucleus/synthesis.d.ts +54 -0
- package/dist/nucleus/synthesis.js +196 -0
- package/dist/plugin/xray-codex-injection.js +50 -0
- package/package.json +11 -2
- package/scripts/helpers/find-project-root.mjs +92 -0
- package/scripts/hooks/install-hooks.cjs +136 -36
- package/scripts/hooks/run-hook.js +46 -9
- package/scripts/mjs/delegation-gate-fixture.mjs +121 -0
- package/scripts/mjs/verify-delegation-gate-core.mjs +99 -0
- package/scripts/mjs/verify-grok-delegation-gate.mjs +9 -69
- package/scripts/mjs/verify-hermes-delegation-gate.mjs +89 -0
- package/scripts/node/auto-reflection-generator.mjs +7 -0
- package/scripts/node/consumer-gitignore.cjs +101 -0
- package/scripts/node/load-reflection-config.mjs +77 -0
- package/scripts/node/postinstall.cjs +9 -6
- package/scripts/node/universal-version-manager.js +1 -1
- package/src/integrations/grok/plugin/0xray/hooks/hooks.json +2 -1
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +33 -8
- package/src/mcps/orchestrator/server.ts +181 -5
- package/xray/features.json +6 -0
- package/xray/features.schema.json +17 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collocated context for synthesis Aside — delegates to memory routing provider SSOT.
|
|
3
|
+
*/
|
|
4
|
+
import { getMemoryRoutingProvider } from '../memory-routing/index.js';
|
|
5
|
+
function fallbackContext(dueReason) {
|
|
6
|
+
const lines = [
|
|
7
|
+
'# Synthesis checkpoint',
|
|
8
|
+
dueReason ? `Due: ${dueReason}` : '',
|
|
9
|
+
'(Enable memory_routing with Repertoire for full signal + codex + plan collation)',
|
|
10
|
+
].filter(Boolean);
|
|
11
|
+
return {
|
|
12
|
+
primitive: 'synthesis',
|
|
13
|
+
dueReason,
|
|
14
|
+
collatedText: lines.join('\n\n'),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function normalizeProviderContext(raw, dueReason) {
|
|
18
|
+
const collatedText = typeof raw.collatedText === 'string' && raw.collatedText.trim()
|
|
19
|
+
? dueReason && !raw.collatedText.includes('Due:')
|
|
20
|
+
? `# Synthesis checkpoint\n\nDue: ${dueReason}\n\n${raw.collatedText}`
|
|
21
|
+
: raw.collatedText
|
|
22
|
+
: fallbackContext(dueReason).collatedText;
|
|
23
|
+
const matchedSignals = Array.isArray(raw.matchedSignals)
|
|
24
|
+
? raw.matchedSignals
|
|
25
|
+
: undefined;
|
|
26
|
+
return {
|
|
27
|
+
primitive: 'synthesis',
|
|
28
|
+
dueReason,
|
|
29
|
+
collatedText,
|
|
30
|
+
...(matchedSignals ? { matchedSignals } : {}),
|
|
31
|
+
...(typeof raw.codexTermCount === 'number' ? { codexTermCount: raw.codexTermCount } : {}),
|
|
32
|
+
...(typeof raw.codexExcerpt === 'string' ? { codexExcerpt: raw.codexExcerpt } : {}),
|
|
33
|
+
...(typeof raw.planExcerpt === 'string' ? { planExcerpt: raw.planExcerpt } : {}),
|
|
34
|
+
...(typeof raw.synthesisExcerpt === 'string'
|
|
35
|
+
? { synthesisExcerpt: raw.synthesisExcerpt }
|
|
36
|
+
: {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function buildSynthesisCollocatedContext(projectRoot = process.cwd(), dueReason = null) {
|
|
40
|
+
try {
|
|
41
|
+
const provider = await getMemoryRoutingProvider();
|
|
42
|
+
if (provider.isAvailable() && provider.buildSynthesisContext) {
|
|
43
|
+
const raw = provider.buildSynthesisContext({ projectRoot, dueReason });
|
|
44
|
+
if (raw)
|
|
45
|
+
return normalizeProviderContext(raw, dueReason);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// fall through to minimal fallback
|
|
50
|
+
}
|
|
51
|
+
return fallbackContext(dueReason);
|
|
52
|
+
}
|
|
53
|
+
/** Refresh meta-inference before collation so synthesis.md is current for reflect. */
|
|
54
|
+
export async function prepareSynthesisCollocatedContext(projectRoot = process.cwd(), dueReason = null) {
|
|
55
|
+
try {
|
|
56
|
+
const provider = await getMemoryRoutingProvider();
|
|
57
|
+
if (provider.refreshMetaInference) {
|
|
58
|
+
await provider.refreshMetaInference().catch(() => undefined);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// non-blocking — collation still proceeds
|
|
63
|
+
}
|
|
64
|
+
return buildSynthesisCollocatedContext(projectRoot, dueReason);
|
|
65
|
+
}
|
|
66
|
+
export function isAnalyzeComplexitySuccess(content) {
|
|
67
|
+
const text = content.map((c) => c.text).join('\n');
|
|
68
|
+
return !text.includes('❌') && !/^error handling tool/i.test(text);
|
|
69
|
+
}
|
|
70
|
+
export function appendSynthesisContextToResponse(content, collatedText) {
|
|
71
|
+
if (!collatedText.trim())
|
|
72
|
+
return content;
|
|
73
|
+
const synthesisSection = `
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Synthesis checkpoint — collocated context
|
|
78
|
+
|
|
79
|
+
${collatedText}
|
|
80
|
+
|
|
81
|
+
**Next:** Spawn Task for each mandatory consult todo (s.1 researcher, s.2 architect-tools, s.3 code-review). Checkpoint clears only when all consult todos complete.`;
|
|
82
|
+
if (content.length === 0) {
|
|
83
|
+
return [{ type: 'text', text: synthesisSection.trim() }];
|
|
84
|
+
}
|
|
85
|
+
return content.map((block, index) => index === 0 ? { ...block, text: `${block.text}${synthesisSection}` } : block);
|
|
86
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesis — periodic reflect & realign checkpoint SSOT.
|
|
3
|
+
* Slice counter + feature flag + `.xray/state/synthesis-checkpoint.json`.
|
|
4
|
+
* PR2 wires pre-tool gate deny via delegation-gate.ts.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SYNTHESIS_PRIMITIVE = "synthesis";
|
|
7
|
+
export type ExecutionSliceKind = 'gate' | 'turn' | 'todo_completed';
|
|
8
|
+
export interface SynthesisConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
every_n_gates: number;
|
|
11
|
+
every_n_turns: number;
|
|
12
|
+
every_n_todos_completed: number;
|
|
13
|
+
}
|
|
14
|
+
export interface SynthesisSliceCounters {
|
|
15
|
+
gates: number;
|
|
16
|
+
turns: number;
|
|
17
|
+
todosCompleted: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SynthesisCheckpointState {
|
|
20
|
+
version: 1;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
slicesSinceLastSynthesis: SynthesisSliceCounters;
|
|
23
|
+
lifetimeSlices: SynthesisSliceCounters;
|
|
24
|
+
synthesisDue: boolean;
|
|
25
|
+
dueReason: string | null;
|
|
26
|
+
lastSynthesisAt: string | null;
|
|
27
|
+
lastSliceAt: string;
|
|
28
|
+
synthesisCount: number;
|
|
29
|
+
}
|
|
30
|
+
export interface RecordSliceResult {
|
|
31
|
+
state: SynthesisCheckpointState;
|
|
32
|
+
becameDue: boolean;
|
|
33
|
+
dueReason: string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface SynthesisDueEvaluation {
|
|
36
|
+
due: boolean;
|
|
37
|
+
reason: string | null;
|
|
38
|
+
}
|
|
39
|
+
export declare function synthesisCheckpointPath(projectRoot?: string): string;
|
|
40
|
+
export declare function defaultSynthesisConfig(): SynthesisConfig;
|
|
41
|
+
export declare function loadSynthesisConfig(projectRoot?: string): SynthesisConfig;
|
|
42
|
+
export declare function createInitialSynthesisState(sessionId: string): SynthesisCheckpointState;
|
|
43
|
+
export declare function loadSynthesisCheckpointState(projectRoot?: string): SynthesisCheckpointState | null;
|
|
44
|
+
export declare function saveSynthesisCheckpointState(state: SynthesisCheckpointState, projectRoot?: string): string;
|
|
45
|
+
export declare function evaluateSynthesisDue(counters: SynthesisSliceCounters, config: SynthesisConfig): SynthesisDueEvaluation;
|
|
46
|
+
export declare function recordExecutionSlice(kind: ExecutionSliceKind, ctx: {
|
|
47
|
+
projectRoot?: string;
|
|
48
|
+
sessionId: string | null;
|
|
49
|
+
}): RecordSliceResult | null;
|
|
50
|
+
export declare function completeSynthesisCheckpoint(projectRoot?: string, sessionId?: string | null): SynthesisCheckpointState | null;
|
|
51
|
+
export declare function isSynthesisCheckpointDue(projectRoot?: string, sessionId?: string | null): boolean;
|
|
52
|
+
export declare function getSynthesisDueReason(projectRoot?: string, sessionId?: string | null): string | null;
|
|
53
|
+
/** Session bound to an active synthesis checkpoint (for MCP when host omits sessionId). */
|
|
54
|
+
export declare function getSynthesisCheckpointSessionId(projectRoot?: string): string | null;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesis — periodic reflect & realign checkpoint SSOT.
|
|
3
|
+
* Slice counter + feature flag + `.xray/state/synthesis-checkpoint.json`.
|
|
4
|
+
* PR2 wires pre-tool gate deny via delegation-gate.ts.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
export const SYNTHESIS_PRIMITIVE = 'synthesis';
|
|
9
|
+
const STATE_VERSION = 1;
|
|
10
|
+
export function synthesisCheckpointPath(projectRoot = process.cwd()) {
|
|
11
|
+
return path.join(projectRoot, '.xray', 'state', 'synthesis-checkpoint.json');
|
|
12
|
+
}
|
|
13
|
+
export function defaultSynthesisConfig() {
|
|
14
|
+
return {
|
|
15
|
+
enabled: false,
|
|
16
|
+
every_n_gates: 12,
|
|
17
|
+
every_n_turns: 0,
|
|
18
|
+
every_n_todos_completed: 0,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function loadSynthesisConfig(projectRoot = process.cwd()) {
|
|
22
|
+
const featuresPath = path.join(projectRoot, '.xray', 'features.json');
|
|
23
|
+
if (!fs.existsSync(featuresPath)) {
|
|
24
|
+
return defaultSynthesisConfig();
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(fs.readFileSync(featuresPath, 'utf8'));
|
|
28
|
+
const raw = data.synthesis ?? {};
|
|
29
|
+
const defaults = defaultSynthesisConfig();
|
|
30
|
+
return {
|
|
31
|
+
enabled: raw.enabled === true,
|
|
32
|
+
every_n_gates: normalizeThreshold(raw.every_n_gates, defaults.every_n_gates),
|
|
33
|
+
every_n_turns: normalizeThreshold(raw.every_n_turns, defaults.every_n_turns),
|
|
34
|
+
every_n_todos_completed: normalizeThreshold(raw.every_n_todos_completed, defaults.every_n_todos_completed),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return defaultSynthesisConfig();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function normalizeThreshold(value, fallback) {
|
|
42
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
return Math.floor(value);
|
|
46
|
+
}
|
|
47
|
+
function emptyCounters() {
|
|
48
|
+
return { gates: 0, turns: 0, todosCompleted: 0 };
|
|
49
|
+
}
|
|
50
|
+
export function createInitialSynthesisState(sessionId) {
|
|
51
|
+
const now = new Date().toISOString();
|
|
52
|
+
return {
|
|
53
|
+
version: STATE_VERSION,
|
|
54
|
+
sessionId,
|
|
55
|
+
slicesSinceLastSynthesis: emptyCounters(),
|
|
56
|
+
lifetimeSlices: emptyCounters(),
|
|
57
|
+
synthesisDue: false,
|
|
58
|
+
dueReason: null,
|
|
59
|
+
lastSynthesisAt: null,
|
|
60
|
+
lastSliceAt: now,
|
|
61
|
+
synthesisCount: 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function loadSynthesisCheckpointState(projectRoot = process.cwd()) {
|
|
65
|
+
const statePath = synthesisCheckpointPath(projectRoot);
|
|
66
|
+
if (!fs.existsSync(statePath))
|
|
67
|
+
return null;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
70
|
+
if (parsed.version !== STATE_VERSION)
|
|
71
|
+
return null;
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function saveSynthesisCheckpointState(state, projectRoot = process.cwd()) {
|
|
79
|
+
const statePath = synthesisCheckpointPath(projectRoot);
|
|
80
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
81
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
82
|
+
return statePath;
|
|
83
|
+
}
|
|
84
|
+
export function evaluateSynthesisDue(counters, config) {
|
|
85
|
+
if (!config.enabled) {
|
|
86
|
+
return { due: false, reason: null };
|
|
87
|
+
}
|
|
88
|
+
if (config.every_n_gates > 0 && counters.gates >= config.every_n_gates) {
|
|
89
|
+
return {
|
|
90
|
+
due: true,
|
|
91
|
+
reason: `gate threshold (${counters.gates}/${config.every_n_gates})`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (config.every_n_turns > 0 && counters.turns >= config.every_n_turns) {
|
|
95
|
+
return {
|
|
96
|
+
due: true,
|
|
97
|
+
reason: `turn threshold (${counters.turns}/${config.every_n_turns})`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (config.every_n_todos_completed > 0 &&
|
|
101
|
+
counters.todosCompleted >= config.every_n_todos_completed) {
|
|
102
|
+
return {
|
|
103
|
+
due: true,
|
|
104
|
+
reason: `todo threshold (${counters.todosCompleted}/${config.every_n_todos_completed})`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { due: false, reason: null };
|
|
108
|
+
}
|
|
109
|
+
function incrementCounter(counters, kind) {
|
|
110
|
+
const next = { ...counters };
|
|
111
|
+
if (kind === 'gate')
|
|
112
|
+
next.gates += 1;
|
|
113
|
+
else if (kind === 'turn')
|
|
114
|
+
next.turns += 1;
|
|
115
|
+
else
|
|
116
|
+
next.todosCompleted += 1;
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
export function recordExecutionSlice(kind, ctx) {
|
|
120
|
+
if (!ctx.sessionId)
|
|
121
|
+
return null;
|
|
122
|
+
const projectRoot = ctx.projectRoot ?? process.cwd();
|
|
123
|
+
const config = loadSynthesisConfig(projectRoot);
|
|
124
|
+
if (!config.enabled)
|
|
125
|
+
return null;
|
|
126
|
+
const existing = loadSynthesisCheckpointState(projectRoot);
|
|
127
|
+
const base = existing && existing.sessionId === ctx.sessionId
|
|
128
|
+
? existing
|
|
129
|
+
: createInitialSynthesisState(ctx.sessionId);
|
|
130
|
+
if (base.synthesisDue && kind !== 'todo_completed')
|
|
131
|
+
return null;
|
|
132
|
+
const now = new Date().toISOString();
|
|
133
|
+
const slicesSinceLastSynthesis = incrementCounter(base.slicesSinceLastSynthesis, kind);
|
|
134
|
+
const lifetimeSlices = incrementCounter(base.lifetimeSlices, kind);
|
|
135
|
+
let synthesisDue = base.synthesisDue;
|
|
136
|
+
let dueReason = base.dueReason;
|
|
137
|
+
let becameDue = false;
|
|
138
|
+
if (!synthesisDue) {
|
|
139
|
+
const evaluation = evaluateSynthesisDue(slicesSinceLastSynthesis, config);
|
|
140
|
+
if (evaluation.due) {
|
|
141
|
+
synthesisDue = true;
|
|
142
|
+
dueReason = evaluation.reason;
|
|
143
|
+
becameDue = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const state = {
|
|
147
|
+
...base,
|
|
148
|
+
slicesSinceLastSynthesis,
|
|
149
|
+
lifetimeSlices,
|
|
150
|
+
synthesisDue,
|
|
151
|
+
dueReason,
|
|
152
|
+
lastSliceAt: now,
|
|
153
|
+
};
|
|
154
|
+
saveSynthesisCheckpointState(state, projectRoot);
|
|
155
|
+
return { state, becameDue, dueReason };
|
|
156
|
+
}
|
|
157
|
+
export function completeSynthesisCheckpoint(projectRoot = process.cwd(), sessionId) {
|
|
158
|
+
const state = loadSynthesisCheckpointState(projectRoot);
|
|
159
|
+
if (!state)
|
|
160
|
+
return null;
|
|
161
|
+
if (sessionId && state.sessionId !== sessionId)
|
|
162
|
+
return null;
|
|
163
|
+
const completed = {
|
|
164
|
+
...state,
|
|
165
|
+
slicesSinceLastSynthesis: emptyCounters(),
|
|
166
|
+
synthesisDue: false,
|
|
167
|
+
dueReason: null,
|
|
168
|
+
lastSynthesisAt: new Date().toISOString(),
|
|
169
|
+
synthesisCount: state.synthesisCount + 1,
|
|
170
|
+
};
|
|
171
|
+
saveSynthesisCheckpointState(completed, projectRoot);
|
|
172
|
+
return completed;
|
|
173
|
+
}
|
|
174
|
+
export function isSynthesisCheckpointDue(projectRoot = process.cwd(), sessionId) {
|
|
175
|
+
const state = loadSynthesisCheckpointState(projectRoot);
|
|
176
|
+
if (!state?.synthesisDue)
|
|
177
|
+
return false;
|
|
178
|
+
if (sessionId && state.sessionId !== sessionId)
|
|
179
|
+
return false;
|
|
180
|
+
return loadSynthesisConfig(projectRoot).enabled;
|
|
181
|
+
}
|
|
182
|
+
export function getSynthesisDueReason(projectRoot = process.cwd(), sessionId) {
|
|
183
|
+
const state = loadSynthesisCheckpointState(projectRoot);
|
|
184
|
+
if (!state?.synthesisDue)
|
|
185
|
+
return null;
|
|
186
|
+
if (sessionId && state.sessionId !== sessionId)
|
|
187
|
+
return null;
|
|
188
|
+
return state.dueReason;
|
|
189
|
+
}
|
|
190
|
+
/** Session bound to an active synthesis checkpoint (for MCP when host omits sessionId). */
|
|
191
|
+
export function getSynthesisCheckpointSessionId(projectRoot = process.cwd()) {
|
|
192
|
+
const state = loadSynthesisCheckpointState(projectRoot);
|
|
193
|
+
if (!state?.synthesisDue)
|
|
194
|
+
return null;
|
|
195
|
+
return state.sessionId;
|
|
196
|
+
}
|
|
@@ -7,6 +7,9 @@ import { getOrCreateLogger } from "./plugin-logger.js";
|
|
|
7
7
|
import { resolveStateDir, importSystemPromptGenerator, loadXrayComponents, getProcessorManager, getXrayStateManager, getFeaturesConfigLoader, getDetectTaskType, getSystemPromptGenerator, } from "./plugin-modules.js";
|
|
8
8
|
import { getFrameworkIdentity, classifyTaskType, isWriteEditOperation, isPublishOperation, resolveAgentName, registerAllProcessors, registerAfterPostProcessors, logPreProcessorResults, logPostProcessorResults, logTestAutoCreationResult, spawnPromise, } from "./plugin-helpers.js";
|
|
9
9
|
import { runEnforcerQualityGate } from "./plugin-quality-gate.js";
|
|
10
|
+
import { evaluatePostToolSpawn, evaluatePreToolGate, isSubagentTool, loadDelegationGateFeatures, } from "../nucleus/delegation-gate.js";
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
10
13
|
// Re-export all public API for test compatibility
|
|
11
14
|
export { classifyTaskType } from "./plugin-helpers.js";
|
|
12
15
|
export { validateModulePath } from "./plugin-modules.js";
|
|
@@ -81,6 +84,25 @@ export default async function xrayCodexPlugin(input) {
|
|
|
81
84
|
throw new Error(`ENFORCER BLOCKED: ${qualityGateResult.violations.join("; ")}`);
|
|
82
85
|
}
|
|
83
86
|
logger.log(`✅ Quality gate passed for ${tool}`);
|
|
87
|
+
const features = loadDelegationGateFeatures(directory);
|
|
88
|
+
let sessionId = process.env.GROK_SESSION_ID ?? null;
|
|
89
|
+
try {
|
|
90
|
+
const boot = JSON.parse(readFileSync(join(directory, ".xray", "state", "session-boot.json"), "utf8"));
|
|
91
|
+
sessionId = boot.sessionId ?? sessionId;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* no session boot */
|
|
95
|
+
}
|
|
96
|
+
const delegationGate = evaluatePreToolGate(tool, (args ?? {}), {
|
|
97
|
+
projectRoot: directory,
|
|
98
|
+
sessionId,
|
|
99
|
+
features,
|
|
100
|
+
host: "opencode",
|
|
101
|
+
});
|
|
102
|
+
if (!delegationGate.allow) {
|
|
103
|
+
logger.error(`🚫 Delegation gate: ${delegationGate.reason}`);
|
|
104
|
+
throw new Error(`DELEGATION GATE: ${delegationGate.reason}`);
|
|
105
|
+
}
|
|
84
106
|
if (isWriteEditOperation(tool)) {
|
|
85
107
|
const _ProcessorManager = getProcessorManager();
|
|
86
108
|
const _XrayStateManager = getXrayStateManager();
|
|
@@ -158,6 +180,17 @@ export default async function xrayCodexPlugin(input) {
|
|
|
158
180
|
const logger = await getOrCreateLogger(directory);
|
|
159
181
|
await loadXrayComponents();
|
|
160
182
|
const { tool, args, result } = input;
|
|
183
|
+
if (isSubagentTool(tool)) {
|
|
184
|
+
try {
|
|
185
|
+
const spawnResult = evaluatePostToolSpawn(tool, (args ?? {}), directory);
|
|
186
|
+
if (spawnResult.satisfied.length > 0) {
|
|
187
|
+
logger.log(`🔗 Delegation gate post: satisfied ${spawnResult.satisfied.map((d) => d.id).join(", ")}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
logger.error("Delegation gate post-hook error", error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
161
194
|
try {
|
|
162
195
|
const { routingOutcomeTracker } = await import("../delegation/analytics/outcome-tracker.js");
|
|
163
196
|
const { TOOL_AGENT_MAP } = await import("./plugin-helpers.js");
|
|
@@ -248,6 +281,23 @@ export default async function xrayCodexPlugin(input) {
|
|
|
248
281
|
if (!textContent) {
|
|
249
282
|
return;
|
|
250
283
|
}
|
|
284
|
+
try {
|
|
285
|
+
const { recordExecutionSlice } = await import("../nucleus/synthesis.js");
|
|
286
|
+
let sessionId = process.env.GROK_SESSION_ID ?? null;
|
|
287
|
+
try {
|
|
288
|
+
const boot = JSON.parse(readFileSync(join(directory, ".xray", "state", "session-boot.json"), "utf8"));
|
|
289
|
+
sessionId = boot.sessionId ?? sessionId;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
/* no session boot */
|
|
293
|
+
}
|
|
294
|
+
if (sessionId) {
|
|
295
|
+
recordExecutionSlice("turn", { projectRoot: directory, sessionId });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* synthesis turn slice optional */
|
|
300
|
+
}
|
|
251
301
|
const agentMentionRegex = /@([\w-]+)(?:\s+(.+?))?(?=$|\n\n|\r\r)/g;
|
|
252
302
|
let match;
|
|
253
303
|
let hasAgentMention = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0xray",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.2",
|
|
4
4
|
"description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
|
|
5
5
|
"readme": "README.md",
|
|
6
6
|
"license": "MIT",
|
|
@@ -127,7 +127,13 @@
|
|
|
127
127
|
"publish": "npm run release:gate && npm run safe-publish && npm publish --access public",
|
|
128
128
|
"release:patch": "node scripts/node/release.mjs patch",
|
|
129
129
|
"verify:orchestrator-behavior": "npm run build && node scripts/mjs/verify-orchestrator-behavior.mjs",
|
|
130
|
-
"verify:grok-delegation-gate": "npm run build && node scripts/mjs/verify-grok-delegation-gate.mjs"
|
|
130
|
+
"verify:grok-delegation-gate": "npm run build && node scripts/mjs/verify-grok-delegation-gate.mjs && node scripts/mjs/verify-grok-synthesis-gate.mjs",
|
|
131
|
+
"verify:grok-synthesis-gate": "npm run build && node scripts/mjs/verify-grok-synthesis-gate.mjs",
|
|
132
|
+
"verify:hermes-delegation-gate": "npm run build && node scripts/mjs/verify-hermes-delegation-gate.mjs",
|
|
133
|
+
"verify:delegation-gate": "npm run build && node scripts/mjs/verify-delegation-gate-core.mjs --host=grok && node scripts/mjs/verify-delegation-gate-core.mjs --host=hermes && node scripts/mjs/verify-delegation-gate-core.mjs --host=opencode && node scripts/mjs/verify-grok-delegation-gate.mjs && node scripts/mjs/verify-grok-synthesis-gate.mjs && node scripts/mjs/verify-hermes-delegation-gate.mjs",
|
|
134
|
+
"verify:synthesis": "npm run build && node scripts/mjs/verify-synthesis-core.mjs && node scripts/mjs/verify-synthesis-gate-core.mjs && node scripts/mjs/verify-synthesis-outcome-core.mjs && node scripts/mjs/verify-synthesis-p2-core.mjs && node scripts/mjs/verify-synthesis-e2e-core.mjs",
|
|
135
|
+
"verify:find-project-root": "node scripts/mjs/verify-find-project-root-core.mjs",
|
|
136
|
+
"verify:consumer-gitignore": "node scripts/mjs/verify-consumer-gitignore-core.mjs"
|
|
131
137
|
},
|
|
132
138
|
"files": [
|
|
133
139
|
"dist/",
|
|
@@ -141,6 +147,9 @@
|
|
|
141
147
|
"scripts/mjs/validate-postinstall-config.mjs",
|
|
142
148
|
"scripts/mjs/verify-orchestrator-behavior.mjs",
|
|
143
149
|
"scripts/mjs/verify-grok-delegation-gate.mjs",
|
|
150
|
+
"scripts/mjs/verify-hermes-delegation-gate.mjs",
|
|
151
|
+
"scripts/mjs/verify-delegation-gate-core.mjs",
|
|
152
|
+
"scripts/mjs/delegation-gate-fixture.mjs",
|
|
144
153
|
"src/opencode/",
|
|
145
154
|
"src/opencode/codex.codex",
|
|
146
155
|
"src/opencode/commands/",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer project root SSOT for bridge hooks and installers.
|
|
3
|
+
* Prefers git root when 0xray is a dependency or devDependency.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
6
|
+
import { join, dirname, resolve } from 'node:path';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
export function hasXrayDependency(pkg) {
|
|
11
|
+
if (!pkg || typeof pkg !== 'object') return false;
|
|
12
|
+
return (
|
|
13
|
+
pkg.name === '0xray' ||
|
|
14
|
+
Boolean(pkg.dependencies?.['0xray']) ||
|
|
15
|
+
Boolean(pkg.devDependencies?.['0xray'])
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isConsumerWorkspace(dir) {
|
|
20
|
+
return (
|
|
21
|
+
existsSync(join(dir, '.xray', 'features.json')) ||
|
|
22
|
+
existsSync(join(dir, 'node_modules', '0xray', 'package.json'))
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizePath(dir) {
|
|
27
|
+
try {
|
|
28
|
+
return realpathSync(dir);
|
|
29
|
+
} catch {
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readPkg(dir) {
|
|
35
|
+
try {
|
|
36
|
+
const pkgPath = join(dir, 'package.json');
|
|
37
|
+
if (!existsSync(pkgPath)) return null;
|
|
38
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function findGitRoot(cwd = process.cwd()) {
|
|
45
|
+
try {
|
|
46
|
+
const gitRoot = execSync('git rev-parse --show-toplevel 2>/dev/null', {
|
|
47
|
+
cwd,
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
50
|
+
}).trim();
|
|
51
|
+
return gitRoot ? normalizePath(gitRoot) : null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function findProjectRoot(extraCandidates = []) {
|
|
58
|
+
const envHome = process.env.XRAY_HOME || process.env.XRAY_ROOT;
|
|
59
|
+
if (envHome) {
|
|
60
|
+
const resolved = normalizePath(resolve(envHome));
|
|
61
|
+
const pkg = readPkg(resolved);
|
|
62
|
+
if (pkg && (isConsumerWorkspace(resolved) || hasXrayDependency(pkg))) {
|
|
63
|
+
return resolved;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const gitRoot = findGitRoot();
|
|
68
|
+
if (gitRoot) {
|
|
69
|
+
const pkg = readPkg(gitRoot);
|
|
70
|
+
if (pkg && hasXrayDependency(pkg) && pkg.name !== '0xray') {
|
|
71
|
+
return gitRoot;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let dir = process.cwd();
|
|
76
|
+
for (let i = 0; i < 12; i++) {
|
|
77
|
+
if (isConsumerWorkspace(dir)) return dir;
|
|
78
|
+
const pkg = readPkg(dir);
|
|
79
|
+
if (pkg && hasXrayDependency(pkg) && pkg.name !== '0xray') return dir;
|
|
80
|
+
const parent = dirname(dir);
|
|
81
|
+
if (parent === dir) break;
|
|
82
|
+
dir = parent;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const candidates = [process.cwd(), join(homedir(), 'dev', 'xray'), ...extraCandidates];
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
const pkg = readPkg(candidate);
|
|
88
|
+
if (pkg && pkg.name === '0xray') return candidate;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return normalizePath(gitRoot || process.cwd());
|
|
92
|
+
}
|