0xray 3.4.10 → 3.5.1
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/.xray/features.json +2 -1
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +1 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +1 -1
- package/dist/CHANGELOG.md +36 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/core/features-config.d.ts +2 -0
- package/dist/integrations/grok/hooks/grok-hook-utils.js +53 -32
- package/dist/integrations/grok/hooks/post-tool-use.js +62 -0
- package/dist/integrations/grok/hooks/pre-tool-use.js +43 -5
- package/dist/integrations/grok/hooks/session-start.js +8 -0
- package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +15 -0
- package/dist/integrations/hermes-agent/__init__.py +50 -3
- package/dist/integrations/hermes-agent/bridge.mjs +78 -9
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +75 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +1 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +7 -0
- package/dist/mcps/orchestrator/handlers/task-handler.js +20 -3
- package/dist/nucleus/delegation-gate.d.ts +86 -0
- package/dist/nucleus/delegation-gate.js +275 -0
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +43 -0
- package/dist/nucleus/lead-dev-plan-persistence.js +155 -0
- package/dist/nucleus/pending-delegations.d.ts +71 -0
- package/dist/nucleus/pending-delegations.js +285 -0
- package/dist/plugin/xray-codex-injection.js +33 -0
- package/package.json +9 -2
- package/scripts/mjs/delegation-gate-fixture.mjs +84 -0
- package/scripts/mjs/verify-delegation-gate-core.mjs +99 -0
- package/scripts/mjs/verify-grok-delegation-gate.mjs +113 -0
- package/scripts/mjs/verify-hermes-delegation-gate.mjs +89 -0
- package/scripts/mjs/verify-orchestrator-behavior.mjs +47 -1
- package/scripts/node/universal-version-manager.js +1 -1
- package/src/integrations/grok/plugin/0xray/hooks/hooks.json +15 -0
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +8 -1
- package/src/mcps/orchestrator/handlers/task-handler.ts +53 -3
- package/xray/features.json +2 -1
package/.xray/features.json
CHANGED
package/AGENTS-consumer.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Quick reference for the 0xRay AI orchestration framework (shipped to your project on `npm install 0xray`).
|
|
6
6
|
|
|
7
|
-
**v3.
|
|
7
|
+
**v3.5.1** — 7 MCP servers · 45 skills · 68 codex terms · 4 platform bridges
|
|
8
8
|
|
|
9
9
|
## Postinstall (automatic)
|
|
10
10
|
|
package/AGENTS.md
CHANGED
package/README.md
CHANGED
package/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.
|
|
3
|
+
**v3.5.1** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
package/dist/AGENTS.md
CHANGED
package/dist/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Conventional Commits](https://www.conventionalcommits.org/).
|
|
6
6
|
|
|
7
|
+
## [3.5.1] - 2026-06-19
|
|
8
|
+
|
|
9
|
+
### ✨ Features
|
|
10
|
+
- **Multi-host delegation gates** — `src/nucleus/delegation-gate.ts` SSOT; `evaluatePreToolGate` / `evaluatePostToolSpawn` shared across Grok, Hermes, OpenCode
|
|
11
|
+
- **Hermes** — `pre_tool_call` returns `{"action":"block"}` via bridge `delegation-gate`; `post_tool_call` clears pending on `delegate_task`
|
|
12
|
+
- **OpenCode** — `tool.execute.before` / `after` enforce delegation gate + post-spawn clear
|
|
13
|
+
|
|
14
|
+
### ✅ Tests
|
|
15
|
+
- `delegation-gate.test.ts`; `verify-delegation-gate-core.mjs`; `verify-hermes-delegation-gate.mjs`; shared `delegation-gate-fixture.mjs`
|
|
16
|
+
- Grok hook-utils deduped — re-exports nucleus SSOT via `delegation-gate-runtime.mjs`
|
|
17
|
+
|
|
18
|
+
### 📋 Deferred
|
|
19
|
+
- **OpenClaw host veto** — no consumer tool-block API; observability-only until platform supports pre-tool deny
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [3.5.0] - 2026-06-19
|
|
24
|
+
|
|
25
|
+
### ✨ Features
|
|
26
|
+
- **Auto-chain bridge (PR1)** — `pending-delegations.json` SSOT on `delegate-deferred`; PreToolUse surgical gate blocks writes while pending; PostToolUse logs + targeted clear on Task; session-bound TTL; `delegations[]` in orchestrate response
|
|
27
|
+
- **Spawn todo persistence (PR2)** — lead-dev plan stays enforced while outstanding todos remain (beyond 4h mtime); `checkSubagentGate` validates Task targets next plan todo; plan todo `in_progress` sync on matched spawn; new intake clears pending delegations + binds `sessionId`/`planGeneration`
|
|
28
|
+
- **`auto_chain_delegations: false`** — disables PR1 delegation gate and PR2 spawn-todo validation (opt-out of full auto-chain enforcement)
|
|
29
|
+
|
|
30
|
+
### 🐛 Bug Fixes
|
|
31
|
+
- **verify-orchestrator-behavior** — checks `pending-delegations.json` in consumer cwd (not package root)
|
|
32
|
+
|
|
33
|
+
### ✅ Tests
|
|
34
|
+
- `pending-delegations`, `lead-dev-plan-persistence`, `grok-pending-delegation-gate` unit tests; `verify-grok-delegation-gate.mjs` hook fixture (4/4); orchestrator behavior verify 8/8
|
|
35
|
+
|
|
36
|
+
### 📋 Deferred (3.5.1+)
|
|
37
|
+
- **#3 Observability** — full session trail (`sessionId` / `taskId` / `planTodoId` / gate deny count correlation)
|
|
38
|
+
- **#4 Rule 1 vs Rule 2** — lead-dev arbitration when read/triage conflicts with spawn-first gate
|
|
39
|
+
- **#5 Codex hard-fail** — silent fallback removal (independent of auto-chain)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
7
43
|
## [3.4.10] - 2026-06-19
|
|
8
44
|
|
|
9
45
|
### 🐛 Bug Fixes
|
package/dist/README.md
CHANGED
package/dist/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.
|
|
3
|
+
**v3.5.1** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
|
@@ -63,6 +63,8 @@ export interface MultiAgentOrchestrationConfig {
|
|
|
63
63
|
auto_consult_major_work?: boolean;
|
|
64
64
|
/** Codex 69 — block new MCP/skill/handler files via PreToolUse (default true) */
|
|
65
65
|
no_new_surface?: boolean;
|
|
66
|
+
/** 3.5.0 — PreToolUse gate after delegate-deferred (default true when lead_dev_mode) */
|
|
67
|
+
auto_chain_delegations?: boolean;
|
|
66
68
|
}
|
|
67
69
|
export interface AutonomousReportingConfig {
|
|
68
70
|
enabled: boolean;
|
|
@@ -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',
|
|
@@ -90,9 +115,15 @@ export function loadFeatures() {
|
|
|
90
115
|
lead_dev_mode: orch.enabled !== false && orch.lead_dev_mode !== false,
|
|
91
116
|
no_new_surface: orch.no_new_surface !== false,
|
|
92
117
|
per_suite_test_triage: orch.per_suite_test_triage !== false,
|
|
118
|
+
auto_chain_delegations: orch.auto_chain_delegations !== false,
|
|
93
119
|
};
|
|
94
120
|
} catch {
|
|
95
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
lead_dev_mode: true,
|
|
123
|
+
no_new_surface: true,
|
|
124
|
+
per_suite_triage: true,
|
|
125
|
+
auto_chain_delegations: true,
|
|
126
|
+
};
|
|
96
127
|
}
|
|
97
128
|
}
|
|
98
129
|
|
|
@@ -110,23 +141,6 @@ export function loadBlockingCodexTerms() {
|
|
|
110
141
|
}
|
|
111
142
|
}
|
|
112
143
|
|
|
113
|
-
export function leadDevPlanPath(root = workspaceRoot()) {
|
|
114
|
-
return path.join(root, '.xray', 'state', 'lead-dev-plan.json');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function hasFreshLeadDevPlan(maxAgeMs = 4 * 60 * 60 * 1000) {
|
|
118
|
-
const planPath = leadDevPlanPath();
|
|
119
|
-
if (!fs.existsSync(planPath)) return false;
|
|
120
|
-
try {
|
|
121
|
-
const stat = fs.statSync(planPath);
|
|
122
|
-
if (Date.now() - stat.mtimeMs > maxAgeMs) return false;
|
|
123
|
-
const plan = JSON.parse(fs.readFileSync(planPath, 'utf8'));
|
|
124
|
-
return plan.active === true;
|
|
125
|
-
} catch {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
144
|
export function extractToolContext(event) {
|
|
131
145
|
const toolName = event.toolName || process.env.TOOL_NAME || 'unknown';
|
|
132
146
|
const toolInput = event.toolInput ?? {};
|
|
@@ -167,16 +181,6 @@ export function checkSurfaceArea(paths, root = workspaceRoot()) {
|
|
|
167
181
|
return null;
|
|
168
182
|
}
|
|
169
183
|
|
|
170
|
-
export function checkSubagentGate(toolName, features) {
|
|
171
|
-
if (!features.lead_dev_mode) return null;
|
|
172
|
-
if (!SUBAGENT_TOOLS.has(toolName)) return null;
|
|
173
|
-
if (hasFreshLeadDevPlan()) return null;
|
|
174
|
-
return (
|
|
175
|
-
'Codex 59/67: call xray-orchestrator analyze-complexity first — ' +
|
|
176
|
-
'writes .xray/state/lead-dev-plan.json required before spawn_subagent'
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
184
|
export function checkFullTestSuite(cmd, features) {
|
|
181
185
|
if (!features.per_suite_test_triage) return null;
|
|
182
186
|
const cmdText = cmd.toLowerCase();
|
|
@@ -195,10 +199,6 @@ export function isWriteTool(toolName) {
|
|
|
195
199
|
return WRITE_TOOLS.has(toolName) || /write|edit|replace/i.test(toolName);
|
|
196
200
|
}
|
|
197
201
|
|
|
198
|
-
export function isShellTool(toolName) {
|
|
199
|
-
return /terminal|bash|shell|run_terminal/i.test(toolName);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
202
|
export async function readStdinJson() {
|
|
203
203
|
const chunks = [];
|
|
204
204
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -258,4 +258,25 @@ export function ensureSessionBoot(root = workspaceRoot(), source = '0xray/grok-b
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
return writeSessionBoot(root, buildSessionBootPayload(root, source));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function resolveSessionId(event) {
|
|
264
|
+
return (
|
|
265
|
+
event.sessionId ||
|
|
266
|
+
process.env.GROK_SESSION_ID ||
|
|
267
|
+
null
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function clearPendingDelegationsForSessionChange(newSessionId, root = workspaceRoot()) {
|
|
272
|
+
const filePath = path.join(root, '.xray', 'state', 'pending-delegations.json');
|
|
273
|
+
if (!fs.existsSync(filePath)) return false;
|
|
274
|
+
try {
|
|
275
|
+
const state = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
276
|
+
if (newSessionId && state.sessionId === newSessionId) return false;
|
|
277
|
+
fs.unlinkSync(filePath);
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
261
282
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Grok PostToolUse — auto-chain observability + targeted clear on Task spawn.
|
|
4
|
+
* Passive hook: stdout ignored by Grok; side effects only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
extractToolContext,
|
|
9
|
+
isOrchestrateToolEvent,
|
|
10
|
+
isSubagentTool,
|
|
11
|
+
readStdinJson,
|
|
12
|
+
resolveSessionId,
|
|
13
|
+
satisfyDelegationsFromToolInput,
|
|
14
|
+
validateSpawnMatchesTodo,
|
|
15
|
+
updatePlanTodoStatusInPlace,
|
|
16
|
+
workspaceRoot,
|
|
17
|
+
} from './grok-hook-utils.js';
|
|
18
|
+
import { appendHookActivity } from './grok-hook-activity.js';
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const root = workspaceRoot();
|
|
22
|
+
try {
|
|
23
|
+
const event = await readStdinJson();
|
|
24
|
+
const eventRoot = event.workspaceRoot || event.cwd || root;
|
|
25
|
+
const sessionId = resolveSessionId(event);
|
|
26
|
+
const { toolName, toolInput } = extractToolContext(event);
|
|
27
|
+
|
|
28
|
+
if (isOrchestrateToolEvent(toolName, toolInput)) {
|
|
29
|
+
appendHookActivity(eventRoot, 'grok-post-tool-use', 'auto-chain-pending', 'info', {
|
|
30
|
+
tool: toolName,
|
|
31
|
+
sessionId,
|
|
32
|
+
note: 'orchestrate-task completed — pending-delegations.json written by task-handler',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isSubagentTool(toolName)) {
|
|
37
|
+
const spawnCheck = validateSpawnMatchesTodo(toolInput, eventRoot);
|
|
38
|
+
if (spawnCheck.valid && spawnCheck.expectedTodoId) {
|
|
39
|
+
updatePlanTodoStatusInPlace(spawnCheck.expectedTodoId, 'in_progress', eventRoot);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = satisfyDelegationsFromToolInput(toolInput, eventRoot);
|
|
43
|
+
if (result.satisfied.length > 0) {
|
|
44
|
+
appendHookActivity(eventRoot, 'grok-post-tool-use', 'auto-chain-cleared', 'success', {
|
|
45
|
+
tool: toolName,
|
|
46
|
+
satisfied: result.satisfied.map((d) => d.id),
|
|
47
|
+
clearedAll: result.clearedAll,
|
|
48
|
+
planTodoId: spawnCheck.expectedTodoId ?? null,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.exit(0);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
appendHookActivity(root, 'grok-post-tool-use', 'hook-error', 'error', {
|
|
56
|
+
error: err.message,
|
|
57
|
+
});
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import {
|
|
8
8
|
checkCodexPatterns,
|
|
9
9
|
checkFullTestSuite,
|
|
10
|
+
checkPendingDelegationGate,
|
|
10
11
|
checkSubagentGate,
|
|
11
12
|
checkSurfaceArea,
|
|
12
13
|
ensureSessionBoot,
|
|
@@ -14,18 +15,20 @@ import {
|
|
|
14
15
|
isWriteTool,
|
|
15
16
|
loadFeatures,
|
|
16
17
|
readStdinJson,
|
|
18
|
+
resolveSessionId,
|
|
17
19
|
workspaceRoot,
|
|
18
20
|
} from './grok-hook-utils.js';
|
|
19
21
|
import { appendHookActivity } from './grok-hook-activity.js';
|
|
20
22
|
|
|
21
|
-
function finish(root, decision, reason, hint, toolName) {
|
|
22
|
-
const out = { decision };
|
|
23
|
+
function finish(root, decision, reason, hint, toolName, extra = {}) {
|
|
24
|
+
const out = { decision, ...extra };
|
|
23
25
|
if (reason) out.reason = reason;
|
|
24
26
|
if (hint) out.hint = hint;
|
|
25
27
|
console.log(JSON.stringify(out));
|
|
26
28
|
appendHookActivity(root, 'grok-pre-tool-use', decision, decision === 'deny' ? 'error' : 'info', {
|
|
27
29
|
tool: toolName,
|
|
28
30
|
reason: reason || hint || null,
|
|
31
|
+
gate: extra.gate || null,
|
|
29
32
|
livePath: true,
|
|
30
33
|
});
|
|
31
34
|
process.exit(0);
|
|
@@ -43,10 +46,44 @@ async function main() {
|
|
|
43
46
|
const features = loadFeatures();
|
|
44
47
|
const ctx = extractFromEvent(event);
|
|
45
48
|
toolName = ctx.toolName;
|
|
46
|
-
const { paths, content, cmd } = ctx;
|
|
49
|
+
const { paths, content, cmd, toolInput } = ctx;
|
|
50
|
+
const sessionId = resolveSessionId(event);
|
|
51
|
+
|
|
52
|
+
const delegationBlock = checkPendingDelegationGate(
|
|
53
|
+
toolName,
|
|
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
|
+
}
|
|
47
69
|
|
|
48
|
-
const subagentBlock = checkSubagentGate(
|
|
49
|
-
|
|
70
|
+
const subagentBlock = checkSubagentGate(
|
|
71
|
+
toolName,
|
|
72
|
+
features,
|
|
73
|
+
eventRoot,
|
|
74
|
+
sessionId,
|
|
75
|
+
toolInput,
|
|
76
|
+
);
|
|
77
|
+
if (subagentBlock) {
|
|
78
|
+
finish(
|
|
79
|
+
eventRoot,
|
|
80
|
+
'deny',
|
|
81
|
+
subagentBlock.reason || subagentBlock,
|
|
82
|
+
subagentBlock.hint || null,
|
|
83
|
+
toolName,
|
|
84
|
+
{ gate: subagentBlock.gate || 'spawn-gate' },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
50
87
|
|
|
51
88
|
if (features.no_new_surface && isWriteTool(toolName) && paths.length) {
|
|
52
89
|
const surfaceBlock = checkSurfaceArea(paths, eventRoot);
|
|
@@ -95,6 +132,7 @@ function extractFromEvent(event) {
|
|
|
95
132
|
|
|
96
133
|
return {
|
|
97
134
|
toolName,
|
|
135
|
+
toolInput,
|
|
98
136
|
paths,
|
|
99
137
|
content,
|
|
100
138
|
cmd: String(toolInput.command || ''),
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
buildSessionBootPayload,
|
|
9
|
+
clearPendingDelegationsForSessionChange,
|
|
9
10
|
ensureSessionBoot,
|
|
10
11
|
readStdinJson,
|
|
12
|
+
resolveSessionId,
|
|
11
13
|
workspaceRoot,
|
|
12
14
|
writeSessionBoot,
|
|
13
15
|
} from './grok-hook-utils.js';
|
|
@@ -20,6 +22,12 @@ async function main() {
|
|
|
20
22
|
try {
|
|
21
23
|
const event = await readStdinJson();
|
|
22
24
|
const eventRoot = event.workspaceRoot || event.cwd || root;
|
|
25
|
+
const sessionId = resolveSessionId(event);
|
|
26
|
+
if (clearPendingDelegationsForSessionChange(sessionId, eventRoot)) {
|
|
27
|
+
appendHookActivity(eventRoot, 'grok-session-start', 'stale-pending-cleared', 'info', {
|
|
28
|
+
sessionId,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
23
31
|
const source =
|
|
24
32
|
HOOK_EVENT === 'user_prompt_submit'
|
|
25
33
|
? '0xray/grok-user-prompt-submit'
|
|
@@ -42,6 +42,21 @@
|
|
|
42
42
|
}
|
|
43
43
|
]
|
|
44
44
|
}
|
|
45
|
+
],
|
|
46
|
+
"PostToolUse": [
|
|
47
|
+
{
|
|
48
|
+
"matcher": ".*",
|
|
49
|
+
"hooks": [
|
|
50
|
+
{
|
|
51
|
+
"type": "command",
|
|
52
|
+
"command": "node",
|
|
53
|
+
"args": [
|
|
54
|
+
"${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/grok/hooks/post-tool-use.js"
|
|
55
|
+
],
|
|
56
|
+
"timeout": 15000
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
45
60
|
]
|
|
46
61
|
}
|
|
47
62
|
}
|
|
@@ -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
|
|
@@ -52,33 +52,55 @@ let frameworkReady = false;
|
|
|
52
52
|
let frameworkLoadAttempted = false;
|
|
53
53
|
|
|
54
54
|
// ── Project root detection ───────────────────────────────────
|
|
55
|
+
function isConsumerWorkspace(dir) {
|
|
56
|
+
return (
|
|
57
|
+
existsSync(join(dir, ".xray", "features.json")) ||
|
|
58
|
+
existsSync(join(dir, "node_modules", "0xray", "package.json"))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
function findProjectRoot() {
|
|
56
63
|
const envHome = process.env.XRAY_HOME || process.env.XRAY_ROOT;
|
|
57
|
-
if (envHome
|
|
64
|
+
if (envHome) {
|
|
65
|
+
const resolved = join(envHome);
|
|
66
|
+
if (existsSync(join(resolved, "package.json")) || isConsumerWorkspace(resolved)) {
|
|
67
|
+
return resolved;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
58
70
|
|
|
59
71
|
const consumerMarker = join(__dirname, "xray-consumer-root.txt");
|
|
60
72
|
if (existsSync(consumerMarker)) {
|
|
61
73
|
try {
|
|
62
74
|
const marked = readFileSync(consumerMarker, "utf-8").trim();
|
|
63
|
-
if (marked && existsSync(join(marked, "package.json"))
|
|
75
|
+
if (marked && (existsSync(join(marked, "package.json")) || isConsumerWorkspace(marked))) {
|
|
76
|
+
return marked;
|
|
77
|
+
}
|
|
64
78
|
} catch {
|
|
65
79
|
// fall through
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
|
|
83
|
+
// Prefer consumer workspace (--cwd / process.chdir) over framework package root
|
|
84
|
+
let dir = process.cwd();
|
|
85
|
+
for (let i = 0; i < 12; i++) {
|
|
86
|
+
if (isConsumerWorkspace(dir)) return dir;
|
|
87
|
+
const parent = dirname(dir);
|
|
88
|
+
if (parent === dir) break;
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
|
|
69
92
|
const candidates = [
|
|
70
|
-
process.cwd(),
|
|
71
93
|
join(homedir(), "dev", "xray"),
|
|
72
|
-
join(dirname(__dirname), "..", "..", "..", ".."), // plugin dir ->
|
|
94
|
+
join(dirname(__dirname), "..", "..", "..", ".."), // plugin dir -> package root
|
|
73
95
|
];
|
|
74
96
|
|
|
75
|
-
for (const
|
|
97
|
+
for (const candidate of candidates) {
|
|
76
98
|
try {
|
|
77
|
-
const pkgPath = join(
|
|
99
|
+
const pkgPath = join(candidate, "package.json");
|
|
78
100
|
if (existsSync(pkgPath)) {
|
|
79
101
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
80
|
-
if (pkg.name === "0xray" || pkg.dependencies?.["0xray"]
|
|
81
|
-
return
|
|
102
|
+
if (pkg.name === "0xray" || pkg.dependencies?.["0xray"]) {
|
|
103
|
+
return candidate;
|
|
82
104
|
}
|
|
83
105
|
}
|
|
84
106
|
} catch {
|
|
@@ -786,9 +808,53 @@ async function handleApply(input, projectRoot, logDir) {
|
|
|
786
808
|
// ── Known commands for positional-arg mode ──────────────────
|
|
787
809
|
const KNOWN_COMMANDS = new Set([
|
|
788
810
|
"health", "stats", "pre-process", "post-process", "validate", "codex-check", "hooks", "govern", "apply",
|
|
789
|
-
"skill-install", "skill-registry",
|
|
811
|
+
"skill-install", "skill-registry", "delegation-gate",
|
|
790
812
|
]);
|
|
791
813
|
|
|
814
|
+
async function handleDelegationGate(command, projectRoot, logDir) {
|
|
815
|
+
try {
|
|
816
|
+
const gate = await import("../hooks/delegation-gate-runtime.mjs");
|
|
817
|
+
const phase = command.phase || "pre";
|
|
818
|
+
const tool = command.tool || command.toolName || "unknown";
|
|
819
|
+
const args = command.args || command.toolInput || {};
|
|
820
|
+
const sessionId = command.sessionId || command.taskId || null;
|
|
821
|
+
const host = command.host || "hermes";
|
|
822
|
+
|
|
823
|
+
if (phase === "post") {
|
|
824
|
+
if (!gate.isSubagentTool(tool)) {
|
|
825
|
+
return { allow: true, phase: "post", skipped: true };
|
|
826
|
+
}
|
|
827
|
+
const result = gate.evaluatePostToolSpawn(tool, args, projectRoot);
|
|
828
|
+
if (result.satisfied.length > 0) {
|
|
829
|
+
logToActivity(
|
|
830
|
+
logDir,
|
|
831
|
+
`[delegation-gate-post] satisfied=${result.satisfied.map((d) => d.id).join(",")} clearedAll=${result.clearedAll}`,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return { allow: true, phase: "post", ...result };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const features = gate.loadDelegationGateFeatures(projectRoot);
|
|
838
|
+
const outcome = gate.evaluatePreToolGate(tool, args, {
|
|
839
|
+
projectRoot,
|
|
840
|
+
sessionId,
|
|
841
|
+
features,
|
|
842
|
+
host,
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
if (!outcome.allow) {
|
|
846
|
+
logToActivity(
|
|
847
|
+
logDir,
|
|
848
|
+
`[delegation-gate] deny tool=${tool} gate=${outcome.gate} reason=${outcome.reason}`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return { phase: "pre", ...outcome };
|
|
853
|
+
} catch (error) {
|
|
854
|
+
return { error: `delegation-gate failed: ${error.message || error}` };
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
792
858
|
// ── Skill management ──────────────────────────────────────────
|
|
793
859
|
|
|
794
860
|
async function handleSkillInstall(command, projectRoot, logDir) {
|
|
@@ -918,6 +984,9 @@ async function main() {
|
|
|
918
984
|
case "skill-registry":
|
|
919
985
|
response = await handleSkillRegistry(command, projectRoot, logDir);
|
|
920
986
|
break;
|
|
987
|
+
case "delegation-gate":
|
|
988
|
+
response = await handleDelegationGate(command, projectRoot, logDir);
|
|
989
|
+
break;
|
|
921
990
|
default:
|
|
922
991
|
response = { error: `Unknown command: ${cmd}` };
|
|
923
992
|
}
|