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.
Files changed (60) hide show
  1. package/.gitignore.default +8 -0
  2. package/.opencode/hooks/post-commit +7 -27
  3. package/.xray/features.json +6 -0
  4. package/.xray/features.schema.json +17 -0
  5. package/AGENTS-consumer.md +1 -1
  6. package/AGENTS.md +1 -1
  7. package/README.md +1 -1
  8. package/SKILLS.md +1 -1
  9. package/dist/.gitignore.default +8 -0
  10. package/dist/AGENTS.md +1 -1
  11. package/dist/CHANGELOG.md +32 -0
  12. package/dist/README.md +1 -1
  13. package/dist/SKILLS.md +1 -1
  14. package/dist/core/bridge.mjs +10 -32
  15. package/dist/core/features-config.d.ts +16 -0
  16. package/dist/core/features-config.js +24 -0
  17. package/dist/integrations/grok/hooks/grok-hook-utils.js +32 -385
  18. package/dist/integrations/grok/hooks/post-tool-use.js +1 -1
  19. package/dist/integrations/grok/hooks/pre-tool-use.js +23 -32
  20. package/dist/integrations/grok/hooks/session-start.js +20 -1
  21. package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +2 -1
  22. package/dist/integrations/hermes-agent/__init__.py +50 -3
  23. package/dist/integrations/hermes-agent/bridge.mjs +71 -30
  24. package/dist/integrations/hooks/delegation-gate-runtime.mjs +75 -0
  25. package/dist/integrations/hooks/synthesis-hook-runtime.mjs +50 -0
  26. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +3 -0
  27. package/dist/mcps/orchestrator/handlers/complexity-handler.js +25 -10
  28. package/dist/mcps/orchestrator/server.js +124 -4
  29. package/dist/memory-routing/types.d.ts +7 -0
  30. package/dist/nucleus/autonomy-kernel.d.ts +3 -0
  31. package/dist/nucleus/autonomy-kernel.js +40 -0
  32. package/dist/nucleus/delegation-gate.d.ts +88 -0
  33. package/dist/nucleus/delegation-gate.js +347 -0
  34. package/dist/nucleus/lead-dev-plan-persistence.d.ts +3 -0
  35. package/dist/nucleus/lead-dev-plan-persistence.js +47 -4
  36. package/dist/nucleus/synthesis-completion.d.ts +6 -0
  37. package/dist/nucleus/synthesis-completion.js +29 -0
  38. package/dist/nucleus/synthesis-context.d.ts +31 -0
  39. package/dist/nucleus/synthesis-context.js +86 -0
  40. package/dist/nucleus/synthesis.d.ts +54 -0
  41. package/dist/nucleus/synthesis.js +196 -0
  42. package/dist/plugin/xray-codex-injection.js +50 -0
  43. package/package.json +11 -2
  44. package/scripts/helpers/find-project-root.mjs +92 -0
  45. package/scripts/hooks/install-hooks.cjs +136 -36
  46. package/scripts/hooks/run-hook.js +46 -9
  47. package/scripts/mjs/delegation-gate-fixture.mjs +121 -0
  48. package/scripts/mjs/verify-delegation-gate-core.mjs +99 -0
  49. package/scripts/mjs/verify-grok-delegation-gate.mjs +9 -69
  50. package/scripts/mjs/verify-hermes-delegation-gate.mjs +89 -0
  51. package/scripts/node/auto-reflection-generator.mjs +7 -0
  52. package/scripts/node/consumer-gitignore.cjs +101 -0
  53. package/scripts/node/load-reflection-config.mjs +77 -0
  54. package/scripts/node/postinstall.cjs +9 -6
  55. package/scripts/node/universal-version-manager.js +1 -1
  56. package/src/integrations/grok/plugin/0xray/hooks/hooks.json +2 -1
  57. package/src/mcps/orchestrator/handlers/complexity-handler.ts +33 -8
  58. package/src/mcps/orchestrator/server.ts +181 -5
  59. package/xray/features.json +6 -0
  60. 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.0",
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
+ }