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.
Files changed (39) hide show
  1. package/.xray/features.json +2 -1
  2. package/AGENTS-consumer.md +1 -1
  3. package/AGENTS.md +1 -1
  4. package/README.md +1 -1
  5. package/SKILLS.md +1 -1
  6. package/dist/AGENTS.md +1 -1
  7. package/dist/CHANGELOG.md +36 -0
  8. package/dist/README.md +1 -1
  9. package/dist/SKILLS.md +1 -1
  10. package/dist/core/features-config.d.ts +2 -0
  11. package/dist/integrations/grok/hooks/grok-hook-utils.js +53 -32
  12. package/dist/integrations/grok/hooks/post-tool-use.js +62 -0
  13. package/dist/integrations/grok/hooks/pre-tool-use.js +43 -5
  14. package/dist/integrations/grok/hooks/session-start.js +8 -0
  15. package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +15 -0
  16. package/dist/integrations/hermes-agent/__init__.py +50 -3
  17. package/dist/integrations/hermes-agent/bridge.mjs +78 -9
  18. package/dist/integrations/hooks/delegation-gate-runtime.mjs +75 -0
  19. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +1 -0
  20. package/dist/mcps/orchestrator/handlers/complexity-handler.js +7 -0
  21. package/dist/mcps/orchestrator/handlers/task-handler.js +20 -3
  22. package/dist/nucleus/delegation-gate.d.ts +86 -0
  23. package/dist/nucleus/delegation-gate.js +275 -0
  24. package/dist/nucleus/lead-dev-plan-persistence.d.ts +43 -0
  25. package/dist/nucleus/lead-dev-plan-persistence.js +155 -0
  26. package/dist/nucleus/pending-delegations.d.ts +71 -0
  27. package/dist/nucleus/pending-delegations.js +285 -0
  28. package/dist/plugin/xray-codex-injection.js +33 -0
  29. package/package.json +9 -2
  30. package/scripts/mjs/delegation-gate-fixture.mjs +84 -0
  31. package/scripts/mjs/verify-delegation-gate-core.mjs +99 -0
  32. package/scripts/mjs/verify-grok-delegation-gate.mjs +113 -0
  33. package/scripts/mjs/verify-hermes-delegation-gate.mjs +89 -0
  34. package/scripts/mjs/verify-orchestrator-behavior.mjs +47 -1
  35. package/scripts/node/universal-version-manager.js +1 -1
  36. package/src/integrations/grok/plugin/0xray/hooks/hooks.json +15 -0
  37. package/src/mcps/orchestrator/handlers/complexity-handler.ts +8 -1
  38. package/src/mcps/orchestrator/handlers/task-handler.ts +53 -3
  39. package/xray/features.json +2 -1
@@ -35,7 +35,8 @@
35
35
  "per_suite_test_triage": true,
36
36
  "phased_plan_threshold": 25,
37
37
  "auto_consult_major_work": true,
38
- "no_new_surface": true
38
+ "no_new_surface": true,
39
+ "auto_chain_delegations": true
39
40
  },
40
41
  "autonomous_reporting": {
41
42
  "enabled": true,
@@ -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.4.10** — 7 MCP servers · 45 skills · 68 codex terms · 4 platform bridges
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
@@ -1,6 +1,6 @@
1
1
  # xray Agents
2
2
 
3
- Quick reference for the xray AI orchestration framework (**v3.4.10**).
3
+ Quick reference for the xray AI orchestration framework (**v3.5.1**).
4
4
 
5
5
  **42 agents** · **45 skills** · **7 consumer MCP servers** · **68 codex terms** · **3,226 tests**
6
6
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # xray — MCP-Centric AI Governance OS
2
2
 
3
- **v3.4.10** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
3
+ **v3.5.1** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
4
4
 
5
5
  ## Your AI Power Suit
6
6
 
package/SKILLS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 0xray Skills
2
2
 
3
- **v3.4.10** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
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
@@ -1,6 +1,6 @@
1
1
  # xray Agents
2
2
 
3
- Quick reference for the xray AI orchestration framework (**v3.4.10**).
3
+ Quick reference for the xray AI orchestration framework (**v3.5.1**).
4
4
 
5
5
  **42 agents** · **45 skills** · **7 consumer MCP servers** · **68 codex terms** · **3,226 tests**
6
6
 
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
@@ -1,6 +1,6 @@
1
1
  # xray — MCP-Centric AI Governance OS
2
2
 
3
- **v3.4.10** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
3
+ **v3.5.1** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
4
4
 
5
5
  ## Your AI Power Suit
6
6
 
package/dist/SKILLS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 0xray Skills
2
2
 
3
- **v3.4.10** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
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 { lead_dev_mode: true, no_new_surface: true, per_suite_triage: true };
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(toolName, features);
49
- if (subagentBlock) finish(eventRoot, 'deny', subagentBlock, null, toolName);
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. For code-producing tools: run quality gate + pre-processors via bridge
197
- 4. For non-code tools: nudge if Xray alternative exists
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: validate all files the subagent changed
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 && existsSync(join(envHome, "package.json"))) return 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"))) return marked;
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 -> project root
94
+ join(dirname(__dirname), "..", "..", "..", ".."), // plugin dir -> package root
73
95
  ];
74
96
 
75
- for (const dir of candidates) {
97
+ for (const candidate of candidates) {
76
98
  try {
77
- const pkgPath = join(dir, "package.json");
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"] || pkg.name === "0xray" || pkg.dependencies?.["0xray"]) {
81
- return dir;
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
  }