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
|
@@ -6,6 +6,31 @@
|
|
|
6
6
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
import {
|
|
10
|
+
checkPendingDelegationGate,
|
|
11
|
+
checkSubagentGate,
|
|
12
|
+
satisfyDelegationsFromToolInput,
|
|
13
|
+
isSubagentTool,
|
|
14
|
+
isOrchestrateToolEvent,
|
|
15
|
+
isReadOnlyTool,
|
|
16
|
+
isShellTool,
|
|
17
|
+
getActivePendingDelegations,
|
|
18
|
+
validateSpawnMatchesTodo,
|
|
19
|
+
updatePlanTodoStatusInPlace,
|
|
20
|
+
} from '../../hooks/delegation-gate-runtime.mjs';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
checkPendingDelegationGate,
|
|
24
|
+
checkSubagentGate,
|
|
25
|
+
satisfyDelegationsFromToolInput,
|
|
26
|
+
isSubagentTool,
|
|
27
|
+
isOrchestrateToolEvent,
|
|
28
|
+
isReadOnlyTool,
|
|
29
|
+
isShellTool,
|
|
30
|
+
getActivePendingDelegations,
|
|
31
|
+
validateSpawnMatchesTodo,
|
|
32
|
+
updatePlanTodoStatusInPlace,
|
|
33
|
+
};
|
|
9
34
|
|
|
10
35
|
export const LEAD_DEV_RULES = [
|
|
11
36
|
'Phased plan + todos; assign best subagent; monitor output',
|
|
@@ -80,8 +105,8 @@ export function resolveCodexPath(root = workspaceRoot()) {
|
|
|
80
105
|
return null;
|
|
81
106
|
}
|
|
82
107
|
|
|
83
|
-
export function loadFeatures() {
|
|
84
|
-
const featuresPath = resolveFeaturesPath();
|
|
108
|
+
export function loadFeatures(root = workspaceRoot()) {
|
|
109
|
+
const featuresPath = resolveFeaturesPath(root);
|
|
85
110
|
if (!featuresPath) return { lead_dev_mode: true, no_new_surface: true, per_suite_triage: true };
|
|
86
111
|
try {
|
|
87
112
|
const data = JSON.parse(fs.readFileSync(featuresPath, 'utf8'));
|
|
@@ -116,140 +141,6 @@ export function loadBlockingCodexTerms() {
|
|
|
116
141
|
}
|
|
117
142
|
}
|
|
118
143
|
|
|
119
|
-
export function leadDevPlanPath(root = workspaceRoot()) {
|
|
120
|
-
return path.join(root, '.xray', 'state', 'lead-dev-plan.json');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const DEFAULT_PLAN_TTL_MS = 4 * 60 * 60 * 1000;
|
|
124
|
-
|
|
125
|
-
export function loadPersistedLeadDevPlan(root = workspaceRoot()) {
|
|
126
|
-
const planPath = leadDevPlanPath(root);
|
|
127
|
-
if (!fs.existsSync(planPath)) return null;
|
|
128
|
-
try {
|
|
129
|
-
return JSON.parse(fs.readFileSync(planPath, 'utf8'));
|
|
130
|
-
} catch {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function savePersistedLeadDevPlan(plan, root = workspaceRoot()) {
|
|
136
|
-
const planPath = leadDevPlanPath(root);
|
|
137
|
-
fs.mkdirSync(path.dirname(planPath), { recursive: true });
|
|
138
|
-
fs.writeFileSync(planPath, JSON.stringify(plan, null, 2));
|
|
139
|
-
return planPath;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function getOutstandingPlanTodos(plan) {
|
|
143
|
-
if (!plan?.phases) return [];
|
|
144
|
-
return plan.phases.flatMap((p) => p.todos).filter(
|
|
145
|
-
(t) => t.status === 'pending' || t.status === 'in_progress',
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function getNextRequiredPlanTodo(plan) {
|
|
150
|
-
if (!plan?.phases) return null;
|
|
151
|
-
for (const phase of plan.phases) {
|
|
152
|
-
const next = phase.todos.find(
|
|
153
|
-
(t) => t.status === 'pending' || t.status === 'in_progress',
|
|
154
|
-
);
|
|
155
|
-
if (next) return next;
|
|
156
|
-
}
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Todo-level persistence: plan stays valid while outstanding todos exist */
|
|
161
|
-
export function hasValidLeadDevPlanForSpawn(root = workspaceRoot()) {
|
|
162
|
-
const plan = loadPersistedLeadDevPlan(root);
|
|
163
|
-
if (!plan?.active) return false;
|
|
164
|
-
const outstanding = getOutstandingPlanTodos(plan);
|
|
165
|
-
if (outstanding.length > 0) return true;
|
|
166
|
-
const persistedAt = plan.persistedAt;
|
|
167
|
-
if (!persistedAt) {
|
|
168
|
-
try {
|
|
169
|
-
const stat = fs.statSync(leadDevPlanPath(root));
|
|
170
|
-
return Date.now() - stat.mtimeMs <= DEFAULT_PLAN_TTL_MS;
|
|
171
|
-
} catch {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return Date.now() - new Date(persistedAt).getTime() <= DEFAULT_PLAN_TTL_MS;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function hasFreshLeadDevPlan(maxAgeMs = DEFAULT_PLAN_TTL_MS) {
|
|
179
|
-
return hasValidLeadDevPlanForSpawn();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function normalizeSpawnAgent(agent) {
|
|
183
|
-
const key = String(agent || '').toLowerCase().trim();
|
|
184
|
-
const aliases = {
|
|
185
|
-
'bug-triage-specialist': 'bug-triage',
|
|
186
|
-
'code-reviewer': 'code-review',
|
|
187
|
-
};
|
|
188
|
-
return aliases[key] ?? key;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function agentsAlign(expected, actual) {
|
|
192
|
-
const e = normalizeSpawnAgent(expected);
|
|
193
|
-
const a = normalizeSpawnAgent(actual);
|
|
194
|
-
return e === a || e.includes(a) || a.includes(e);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function validateSpawnMatchesTodo(toolInput, root = workspaceRoot(), expectedTodo = null) {
|
|
198
|
-
const plan = loadPersistedLeadDevPlan(root);
|
|
199
|
-
const nextTodo = expectedTodo ?? (plan ? getNextRequiredPlanTodo(plan) : null);
|
|
200
|
-
if (!nextTodo) return { valid: true };
|
|
201
|
-
|
|
202
|
-
const prompt = String(
|
|
203
|
-
toolInput.prompt || toolInput.description || toolInput.task || '',
|
|
204
|
-
).toLowerCase();
|
|
205
|
-
const subagent = String(toolInput.subagent_type || toolInput.agent || '');
|
|
206
|
-
const explicitTodo = toolInput.planTodoId || toolInput.delegationId;
|
|
207
|
-
|
|
208
|
-
if (explicitTodo && explicitTodo === nextTodo.id) {
|
|
209
|
-
return { valid: true, expectedTodoId: nextTodo.id, hint: null };
|
|
210
|
-
}
|
|
211
|
-
if (prompt.includes(nextTodo.id.toLowerCase())) {
|
|
212
|
-
return { valid: true, expectedTodoId: nextTodo.id, hint: null };
|
|
213
|
-
}
|
|
214
|
-
if (prompt.includes(nextTodo.task.toLowerCase().slice(0, 30))) {
|
|
215
|
-
return { valid: true, expectedTodoId: nextTodo.id, hint: null };
|
|
216
|
-
}
|
|
217
|
-
if (subagent && agentsAlign(nextTodo.subagent, subagent) && prompt.length > 20) {
|
|
218
|
-
return { valid: true, expectedTodoId: nextTodo.id, hint: null };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
valid: false,
|
|
223
|
-
expectedTodoId: nextTodo.id,
|
|
224
|
-
reason: `Spawn must target plan todo ${nextTodo.id} before other work`,
|
|
225
|
-
gate: 'spawn-todo-persistence',
|
|
226
|
-
hint: {
|
|
227
|
-
tool: 'Task',
|
|
228
|
-
subagent_type: nextTodo.subagent,
|
|
229
|
-
planTodoId: nextTodo.id,
|
|
230
|
-
description: `Lead-dev todo ${nextTodo.id}: ${nextTodo.task}. Include plan todo id in Task prompt.`,
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function updatePlanTodoStatusInPlace(todoId, status, root = workspaceRoot()) {
|
|
236
|
-
const plan = loadPersistedLeadDevPlan(root);
|
|
237
|
-
if (!plan) return false;
|
|
238
|
-
let updated = false;
|
|
239
|
-
for (const phase of plan.phases) {
|
|
240
|
-
for (const todo of phase.todos) {
|
|
241
|
-
if (todo.id === todoId) {
|
|
242
|
-
todo.status = status;
|
|
243
|
-
updated = true;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (updated) {
|
|
248
|
-
fs.writeFileSync(leadDevPlanPath(root), JSON.stringify(plan, null, 2));
|
|
249
|
-
}
|
|
250
|
-
return updated;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
144
|
export function extractToolContext(event) {
|
|
254
145
|
const toolName = event.toolName || process.env.TOOL_NAME || 'unknown';
|
|
255
146
|
const toolInput = event.toolInput ?? {};
|
|
@@ -290,41 +181,6 @@ export function checkSurfaceArea(paths, root = workspaceRoot()) {
|
|
|
290
181
|
return null;
|
|
291
182
|
}
|
|
292
183
|
|
|
293
|
-
export function checkSubagentGate(toolName, features, root = workspaceRoot(), sessionId = null, toolInput = {}) {
|
|
294
|
-
if (!features.lead_dev_mode) return null;
|
|
295
|
-
if (!isSubagentTool(toolName)) return null;
|
|
296
|
-
|
|
297
|
-
if (!hasValidLeadDevPlanForSpawn(root)) {
|
|
298
|
-
return {
|
|
299
|
-
reason:
|
|
300
|
-
'Codex 59/67: call xray-orchestrator analyze-complexity first — ' +
|
|
301
|
-
'writes .xray/state/lead-dev-plan.json required before spawn_subagent',
|
|
302
|
-
gate: 'spawn-plan-missing',
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (features.auto_chain_delegations === false) return null;
|
|
307
|
-
|
|
308
|
-
const pending = getActivePendingDelegations(root, sessionId);
|
|
309
|
-
const expectedTodo =
|
|
310
|
-
pending[0]?.planTodoId && loadPersistedLeadDevPlan(root)
|
|
311
|
-
? loadPersistedLeadDevPlan(root).phases
|
|
312
|
-
.flatMap((p) => p.todos)
|
|
313
|
-
.find((t) => t.id === pending[0].planTodoId) ?? null
|
|
314
|
-
: null;
|
|
315
|
-
|
|
316
|
-
const validation = validateSpawnMatchesTodo(toolInput, root, expectedTodo);
|
|
317
|
-
if (!validation.valid) {
|
|
318
|
-
return {
|
|
319
|
-
reason: validation.reason,
|
|
320
|
-
gate: validation.gate,
|
|
321
|
-
hint: validation.hint,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
184
|
export function checkFullTestSuite(cmd, features) {
|
|
329
185
|
if (!features.per_suite_test_triage) return null;
|
|
330
186
|
const cmdText = cmd.toLowerCase();
|
|
@@ -343,10 +199,6 @@ export function isWriteTool(toolName) {
|
|
|
343
199
|
return WRITE_TOOLS.has(toolName) || /write|edit|replace/i.test(toolName);
|
|
344
200
|
}
|
|
345
201
|
|
|
346
|
-
export function isShellTool(toolName) {
|
|
347
|
-
return /terminal|bash|shell|run_terminal/i.test(toolName);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
202
|
export async function readStdinJson() {
|
|
351
203
|
const chunks = [];
|
|
352
204
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -408,50 +260,6 @@ export function ensureSessionBoot(root = workspaceRoot(), source = '0xray/grok-b
|
|
|
408
260
|
return writeSessionBoot(root, buildSessionBootPayload(root, source));
|
|
409
261
|
}
|
|
410
262
|
|
|
411
|
-
const READ_TOOLS = new Set([
|
|
412
|
-
'read_file',
|
|
413
|
-
'Read',
|
|
414
|
-
'grep',
|
|
415
|
-
'Grep',
|
|
416
|
-
'glob',
|
|
417
|
-
'Glob',
|
|
418
|
-
'list_dir',
|
|
419
|
-
'ListDir',
|
|
420
|
-
'web_search',
|
|
421
|
-
'WebSearch',
|
|
422
|
-
'codebase_search',
|
|
423
|
-
'SemanticSearch',
|
|
424
|
-
]);
|
|
425
|
-
|
|
426
|
-
const ORCHESTRATOR_CONSULT_TOOLS = new Set([
|
|
427
|
-
'analyze-complexity',
|
|
428
|
-
'analyze_complexity',
|
|
429
|
-
'get-orchestration-status',
|
|
430
|
-
'get_orchestration_status',
|
|
431
|
-
'orchestrate-task',
|
|
432
|
-
'orchestrate_task',
|
|
433
|
-
]);
|
|
434
|
-
|
|
435
|
-
export function pendingDelegationsPath(root = workspaceRoot()) {
|
|
436
|
-
return path.join(root, '.xray', 'state', 'pending-delegations.json');
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
export function loadPendingDelegationsState(root = workspaceRoot()) {
|
|
440
|
-
const filePath = pendingDelegationsPath(root);
|
|
441
|
-
if (!fs.existsSync(filePath)) return null;
|
|
442
|
-
try {
|
|
443
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
444
|
-
} catch {
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
export function savePendingDelegationsState(state, root = workspaceRoot()) {
|
|
450
|
-
const filePath = pendingDelegationsPath(root);
|
|
451
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
452
|
-
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
263
|
export function resolveSessionId(event) {
|
|
456
264
|
return (
|
|
457
265
|
event.sessionId ||
|
|
@@ -460,176 +268,15 @@ export function resolveSessionId(event) {
|
|
|
460
268
|
);
|
|
461
269
|
}
|
|
462
270
|
|
|
463
|
-
export function isPendingStateActive(state, sessionId) {
|
|
464
|
-
if (!state || !sessionId || state.sessionId !== sessionId) return false;
|
|
465
|
-
const age = Date.now() - new Date(state.createdAt).getTime();
|
|
466
|
-
if (age > (state.ttlMs ?? 4 * 60 * 60 * 1000)) return false;
|
|
467
|
-
return state.delegations?.some((d) => d.status === 'pending');
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export function getActivePendingDelegations(root, sessionId) {
|
|
471
|
-
const state = loadPendingDelegationsState(root);
|
|
472
|
-
if (!isPendingStateActive(state, sessionId)) return [];
|
|
473
|
-
return state.delegations.filter((d) => d.status === 'pending');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
271
|
export function clearPendingDelegationsForSessionChange(newSessionId, root = workspaceRoot()) {
|
|
477
|
-
const
|
|
478
|
-
if (!
|
|
479
|
-
if (newSessionId && state.sessionId === newSessionId) return false;
|
|
272
|
+
const filePath = path.join(root, '.xray', 'state', 'pending-delegations.json');
|
|
273
|
+
if (!fs.existsSync(filePath)) return false;
|
|
480
274
|
try {
|
|
481
|
-
fs.
|
|
275
|
+
const state = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
276
|
+
if (newSessionId && state.sessionId === newSessionId) return false;
|
|
277
|
+
fs.unlinkSync(filePath);
|
|
482
278
|
return true;
|
|
483
279
|
} catch {
|
|
484
280
|
return false;
|
|
485
281
|
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function isFocusedTestCommand(cmd) {
|
|
489
|
-
const cmdText = (cmd || '').toLowerCase();
|
|
490
|
-
return (
|
|
491
|
-
/\b(npm\s+(run\s+)?test|vitest\s+run|pnpm\s+test|yarn\s+test)\b/.test(cmdText) &&
|
|
492
|
-
(/--\s+\S+\.test/.test(cmdText) || /vitest\s+run\s+\S+\.test/.test(cmdText))
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function isReadOnlyTool(toolName) {
|
|
497
|
-
return READ_TOOLS.has(toolName) || /^(read|grep|glob|list)/i.test(toolName);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
export function isSubagentTool(toolName) {
|
|
501
|
-
return SUBAGENT_TOOLS.has(toolName) || /^task$/i.test(toolName) || toolName === 'spawn_subagent';
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function extractMcpToolName(toolInput) {
|
|
505
|
-
const candidates = [
|
|
506
|
-
toolInput.toolName,
|
|
507
|
-
toolInput.tool,
|
|
508
|
-
toolInput.name,
|
|
509
|
-
toolInput.mcpToolName,
|
|
510
|
-
];
|
|
511
|
-
for (const c of candidates) {
|
|
512
|
-
if (c) return String(c);
|
|
513
|
-
}
|
|
514
|
-
const server = String(toolInput.server || toolInput.mcpServer || '');
|
|
515
|
-
const args = toolInput.arguments ?? toolInput.args ?? {};
|
|
516
|
-
if (args.toolName) return String(args.toolName);
|
|
517
|
-
if (server.includes('orchestrator') && args.name) return String(args.name);
|
|
518
|
-
return '';
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function isOrchestratorConsultMcp(toolName, toolInput) {
|
|
522
|
-
if (!/mcp|CallMcpTool/i.test(toolName)) return false;
|
|
523
|
-
const inner = extractMcpToolName(toolInput).toLowerCase();
|
|
524
|
-
if (!inner) return false;
|
|
525
|
-
for (const t of ORCHESTRATOR_CONSULT_TOOLS) {
|
|
526
|
-
if (inner.includes(t) || inner.includes(t.replace(/-/g, '_'))) return true;
|
|
527
|
-
}
|
|
528
|
-
return false;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
export function isOrchestrateToolEvent(toolName, toolInput = {}) {
|
|
532
|
-
const inner = extractMcpToolName(toolInput).toLowerCase();
|
|
533
|
-
if (/orchestrate[-_]?task/.test(inner)) return true;
|
|
534
|
-
const blob = JSON.stringify(toolInput).toLowerCase();
|
|
535
|
-
return /orchestrate[-_]?task/.test(blob);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
export function satisfyDelegationsFromToolInput(toolInput, root = workspaceRoot()) {
|
|
539
|
-
const state = loadPendingDelegationsState(root);
|
|
540
|
-
if (!state) return { satisfied: [], clearedAll: false };
|
|
541
|
-
|
|
542
|
-
const prompt = String(
|
|
543
|
-
toolInput.prompt || toolInput.description || toolInput.task || '',
|
|
544
|
-
).toLowerCase();
|
|
545
|
-
const subagent = String(toolInput.subagent_type || toolInput.agent || '').toLowerCase();
|
|
546
|
-
const delegationId = toolInput.delegationId || toolInput.delegation_id;
|
|
547
|
-
|
|
548
|
-
const pending = state.delegations.filter((d) => d.status === 'pending');
|
|
549
|
-
if (pending.length === 0) return { satisfied: [], clearedAll: false };
|
|
550
|
-
|
|
551
|
-
let matches = [];
|
|
552
|
-
if (delegationId) {
|
|
553
|
-
matches = pending.filter((d) => d.id === delegationId);
|
|
554
|
-
} else {
|
|
555
|
-
for (const d of pending) {
|
|
556
|
-
if (d.planTodoId && prompt.includes(d.planTodoId.toLowerCase())) {
|
|
557
|
-
matches.push(d);
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
if (prompt.includes(d.taskId.toLowerCase())) {
|
|
561
|
-
matches.push(d);
|
|
562
|
-
break;
|
|
563
|
-
}
|
|
564
|
-
if (subagent && d.agent.toLowerCase().includes(subagent)) {
|
|
565
|
-
matches.push(d);
|
|
566
|
-
break;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (matches.length === 0 && pending.length === 1) {
|
|
572
|
-
const only = pending[0];
|
|
573
|
-
const hint = (only.spawnHint?.description || '').toLowerCase();
|
|
574
|
-
if (prompt && hint && prompt.includes(hint.slice(0, 30))) {
|
|
575
|
-
matches = [only];
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (matches.length === 0) return { satisfied: [], clearedAll: false };
|
|
580
|
-
|
|
581
|
-
const now = new Date().toISOString();
|
|
582
|
-
const ids = new Set(matches.map((m) => m.id));
|
|
583
|
-
for (const d of state.delegations) {
|
|
584
|
-
if (ids.has(d.id) && d.status === 'pending') {
|
|
585
|
-
d.status = 'satisfied';
|
|
586
|
-
d.satisfiedAt = now;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
savePendingDelegationsState(state, root);
|
|
590
|
-
for (const m of matches) {
|
|
591
|
-
if (m.planTodoId) {
|
|
592
|
-
updatePlanTodoStatusInPlace(m.planTodoId, 'in_progress', root);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
const clearedAll = !state.delegations.some((d) => d.status === 'pending');
|
|
596
|
-
return { satisfied: matches, clearedAll };
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Surgical gate: block writes and unrelated work while pending delegations exist.
|
|
601
|
-
* Allows read-only tools, orchestrator consult MCP, focused test shell, and Task/spawn.
|
|
602
|
-
*/
|
|
603
|
-
export function checkPendingDelegationGate(toolName, toolInput, features, root, sessionId) {
|
|
604
|
-
if (!features.lead_dev_mode || features.auto_chain_delegations === false) return null;
|
|
605
|
-
if (!sessionId) return null;
|
|
606
|
-
|
|
607
|
-
const pending = getActivePendingDelegations(root, sessionId);
|
|
608
|
-
if (pending.length === 0) return null;
|
|
609
|
-
|
|
610
|
-
if (isSubagentTool(toolName)) return null;
|
|
611
|
-
|
|
612
|
-
if (isReadOnlyTool(toolName)) return null;
|
|
613
|
-
|
|
614
|
-
if (isOrchestratorConsultMcp(toolName, toolInput)) return null;
|
|
615
|
-
|
|
616
|
-
if (isShellTool(toolName)) {
|
|
617
|
-
const cmd = String(toolInput.command || '');
|
|
618
|
-
if (isFocusedTestCommand(cmd)) return null;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const primary = pending[0];
|
|
622
|
-
return {
|
|
623
|
-
reason: 'Pending implementation delegation — spawn host Task before other work',
|
|
624
|
-
gate: 'auto-chain-pending',
|
|
625
|
-
hint: primary.spawnHint ?? {
|
|
626
|
-
tool: 'Task',
|
|
627
|
-
subagent_type: primary.agent,
|
|
628
|
-
description: primary.taskDescription,
|
|
629
|
-
planTodoId: primary.planTodoId,
|
|
630
|
-
delegationId: primary.id,
|
|
631
|
-
},
|
|
632
|
-
pendingCount: pending.length,
|
|
633
|
-
delegationId: primary.id,
|
|
634
|
-
};
|
|
635
282
|
}
|
|
@@ -36,7 +36,7 @@ async function main() {
|
|
|
36
36
|
if (isSubagentTool(toolName)) {
|
|
37
37
|
const spawnCheck = validateSpawnMatchesTodo(toolInput, eventRoot);
|
|
38
38
|
if (spawnCheck.valid && spawnCheck.expectedTodoId) {
|
|
39
|
-
updatePlanTodoStatusInPlace(spawnCheck.expectedTodoId, '
|
|
39
|
+
updatePlanTodoStatusInPlace(spawnCheck.expectedTodoId, 'completed', eventRoot);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const result = satisfyDelegationsFromToolInput(toolInput, eventRoot);
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* Contract: stdin JSON → stdout {"decision":"allow"} | {"decision":"deny","reason":"..."}
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
evaluatePreToolGate,
|
|
9
|
+
loadDelegationGateFeatures,
|
|
10
|
+
} from '../../hooks/delegation-gate-runtime.mjs';
|
|
7
11
|
import {
|
|
8
12
|
checkCodexPatterns,
|
|
9
13
|
checkFullTestSuite,
|
|
10
|
-
checkPendingDelegationGate,
|
|
11
|
-
checkSubagentGate,
|
|
12
14
|
checkSurfaceArea,
|
|
13
15
|
ensureSessionBoot,
|
|
14
16
|
isShellTool,
|
|
@@ -43,45 +45,27 @@ async function main() {
|
|
|
43
45
|
const eventRoot = event.workspaceRoot || event.cwd || root;
|
|
44
46
|
ensureSessionBoot(eventRoot, '0xray/grok-pre-tool-use-boot');
|
|
45
47
|
|
|
46
|
-
const features = loadFeatures();
|
|
48
|
+
const features = loadFeatures(eventRoot);
|
|
49
|
+
const gateFeatures = loadDelegationGateFeatures(eventRoot);
|
|
47
50
|
const ctx = extractFromEvent(event);
|
|
48
51
|
toolName = ctx.toolName;
|
|
49
52
|
const { paths, content, cmd, toolInput } = ctx;
|
|
50
53
|
const sessionId = resolveSessionId(event);
|
|
51
54
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
toolInput,
|
|
55
|
-
features,
|
|
56
|
-
eventRoot,
|
|
57
|
-
sessionId,
|
|
58
|
-
);
|
|
59
|
-
if (delegationBlock) {
|
|
60
|
-
finish(
|
|
61
|
-
eventRoot,
|
|
62
|
-
'deny',
|
|
63
|
-
delegationBlock.reason,
|
|
64
|
-
delegationBlock.hint,
|
|
65
|
-
toolName,
|
|
66
|
-
{ gate: delegationBlock.gate },
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const subagentBlock = checkSubagentGate(
|
|
71
|
-
toolName,
|
|
72
|
-
features,
|
|
73
|
-
eventRoot,
|
|
55
|
+
const gateBlock = evaluatePreToolGate(toolName, toolInput, {
|
|
56
|
+
projectRoot: eventRoot,
|
|
74
57
|
sessionId,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
58
|
+
features: gateFeatures,
|
|
59
|
+
host: 'grok',
|
|
60
|
+
});
|
|
61
|
+
if (!gateBlock.allow) {
|
|
78
62
|
finish(
|
|
79
63
|
eventRoot,
|
|
80
64
|
'deny',
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
gateBlock.reason,
|
|
66
|
+
gateBlock.hint,
|
|
83
67
|
toolName,
|
|
84
|
-
{ gate:
|
|
68
|
+
{ gate: gateBlock.gate },
|
|
85
69
|
);
|
|
86
70
|
}
|
|
87
71
|
|
|
@@ -110,7 +94,14 @@ async function main() {
|
|
|
110
94
|
tool: toolName,
|
|
111
95
|
error: err.message,
|
|
112
96
|
});
|
|
113
|
-
finish(
|
|
97
|
+
finish(
|
|
98
|
+
root,
|
|
99
|
+
'deny',
|
|
100
|
+
`PreToolUse hook error — blocked for safety: ${err.message}`,
|
|
101
|
+
null,
|
|
102
|
+
toolName,
|
|
103
|
+
{ gate: 'hook-error' },
|
|
104
|
+
);
|
|
114
105
|
}
|
|
115
106
|
}
|
|
116
107
|
|
|
@@ -14,15 +14,34 @@ import {
|
|
|
14
14
|
writeSessionBoot,
|
|
15
15
|
} from './grok-hook-utils.js';
|
|
16
16
|
import { appendHookActivity } from './grok-hook-activity.js';
|
|
17
|
+
import { recordSynthesisTurnSlice } from '../../hooks/synthesis-hook-runtime.mjs';
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
function resolveHookEvent(event) {
|
|
20
|
+
if (process.env.GROK_HOOK_EVENT) return process.env.GROK_HOOK_EVENT;
|
|
21
|
+
const flag = process.argv.find((a) => a.startsWith('--hook-event='));
|
|
22
|
+
if (flag) return flag.slice('--hook-event='.length);
|
|
23
|
+
if (
|
|
24
|
+
event.prompt != null ||
|
|
25
|
+
event.userMessage != null ||
|
|
26
|
+
event.user_prompt != null ||
|
|
27
|
+
event.hook === 'UserPromptSubmit'
|
|
28
|
+
) {
|
|
29
|
+
return 'user_prompt_submit';
|
|
30
|
+
}
|
|
31
|
+
return 'session_start';
|
|
32
|
+
}
|
|
19
33
|
|
|
20
34
|
async function main() {
|
|
21
35
|
const root = workspaceRoot();
|
|
36
|
+
let HOOK_EVENT = 'session_start';
|
|
22
37
|
try {
|
|
23
38
|
const event = await readStdinJson();
|
|
39
|
+
HOOK_EVENT = resolveHookEvent(event);
|
|
24
40
|
const eventRoot = event.workspaceRoot || event.cwd || root;
|
|
25
41
|
const sessionId = resolveSessionId(event);
|
|
42
|
+
if (HOOK_EVENT === 'user_prompt_submit' && sessionId) {
|
|
43
|
+
recordSynthesisTurnSlice(eventRoot, sessionId);
|
|
44
|
+
}
|
|
26
45
|
if (clearPendingDelegationsForSessionChange(sessionId, eventRoot)) {
|
|
27
46
|
appendHookActivity(eventRoot, 'grok-session-start', 'stale-pending-cleared', 'info', {
|
|
28
47
|
sessionId,
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"type": "command",
|
|
37
37
|
"command": "node",
|
|
38
38
|
"args": [
|
|
39
|
-
"${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/grok/hooks/session-start.js"
|
|
39
|
+
"${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/grok/hooks/session-start.js",
|
|
40
|
+
"--hook-event=user_prompt_submit"
|
|
40
41
|
],
|
|
41
42
|
"timeout": 15000
|
|
42
43
|
}
|
|
@@ -187,17 +187,52 @@ def _is_xray_mcp(tool_name: str) -> bool:
|
|
|
187
187
|
return tool_name.startswith("mcp_xray_")
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
def _delegation_gate_block(tool_name: str, args: dict, task_id: str, **kwargs):
|
|
191
|
+
"""3.5.1 multi-host parity — block via nucleus delegation-gate SSOT."""
|
|
192
|
+
session_id = (
|
|
193
|
+
kwargs.get("session_id")
|
|
194
|
+
or kwargs.get("sessionId")
|
|
195
|
+
or task_id
|
|
196
|
+
or ""
|
|
197
|
+
)
|
|
198
|
+
bridge_result = _call_bridge(
|
|
199
|
+
{
|
|
200
|
+
"command": "delegation-gate",
|
|
201
|
+
"phase": "pre",
|
|
202
|
+
"tool": tool_name,
|
|
203
|
+
"args": args or {},
|
|
204
|
+
"sessionId": session_id,
|
|
205
|
+
"host": "hermes",
|
|
206
|
+
},
|
|
207
|
+
timeout=10,
|
|
208
|
+
)
|
|
209
|
+
if bridge_result.get("allow") is False:
|
|
210
|
+
reason = bridge_result.get("reason", "Delegation gate blocked this tool")
|
|
211
|
+
gate = bridge_result.get("gate", "delegation-gate")
|
|
212
|
+
_log_to_file(
|
|
213
|
+
"activity.log",
|
|
214
|
+
f"[delegation-gate] deny tool={tool_name} gate={gate} reason={reason}",
|
|
215
|
+
)
|
|
216
|
+
return {"action": "block", "message": reason}
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
190
220
|
def _on_pre_tool_call(tool_name: str, args: dict, task_id: str, **kwargs):
|
|
191
221
|
"""Fires before ANY tool executes.
|
|
192
222
|
|
|
193
223
|
Pipeline:
|
|
194
224
|
1. Track stats
|
|
195
225
|
2. Log tool-start event to disk
|
|
196
|
-
3.
|
|
197
|
-
4. For
|
|
226
|
+
3. Delegation gate (pending + spawn todo) — may block tool
|
|
227
|
+
4. For code-producing tools: run quality gate + pre-processors via bridge
|
|
228
|
+
5. For non-code tools: nudge if Xray alternative exists
|
|
198
229
|
"""
|
|
199
230
|
_session_stats["total_tool_calls"] += 1
|
|
200
231
|
|
|
232
|
+
block = _delegation_gate_block(tool_name, args, task_id, **kwargs)
|
|
233
|
+
if block:
|
|
234
|
+
return block
|
|
235
|
+
|
|
201
236
|
# Log start event
|
|
202
237
|
_log_tool_event("start", tool_name, args)
|
|
203
238
|
|
|
@@ -329,9 +364,21 @@ def _on_post_tool_call(tool_name: str, args: dict, result, task_id: str, **kwarg
|
|
|
329
364
|
# Record outcome for the inference feedback loop
|
|
330
365
|
_record_tool_outcome(tool_name, args or {}, error is None)
|
|
331
366
|
|
|
332
|
-
# delegate_task:
|
|
367
|
+
# delegate_task: clear pending delegations + validate subagent changes
|
|
333
368
|
if tool_name == "delegate_task":
|
|
334
369
|
tid = kwargs.get("task_id", "") or args.get("task_id", "") or task_id
|
|
370
|
+
session_id = kwargs.get("session_id") or kwargs.get("sessionId") or tid
|
|
371
|
+
_call_bridge(
|
|
372
|
+
{
|
|
373
|
+
"command": "delegation-gate",
|
|
374
|
+
"phase": "post",
|
|
375
|
+
"tool": tool_name,
|
|
376
|
+
"args": args or {},
|
|
377
|
+
"sessionId": session_id,
|
|
378
|
+
"host": "hermes",
|
|
379
|
+
},
|
|
380
|
+
timeout=10,
|
|
381
|
+
)
|
|
335
382
|
if tid:
|
|
336
383
|
_validate_subagent_changes(tid)
|
|
337
384
|
return
|