0xray 3.5.0 → 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/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 +16 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/integrations/grok/hooks/grok-hook-utils.js +30 -383
- 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/nucleus/delegation-gate.d.ts +86 -0
- package/dist/nucleus/delegation-gate.js +275 -0
- package/dist/nucleus/lead-dev-plan-persistence.js +3 -3
- package/dist/plugin/xray-codex-injection.js +33 -0
- package/package.json +7 -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 +9 -69
- package/scripts/mjs/verify-hermes-delegation-gate.mjs +89 -0
- package/scripts/node/universal-version-manager.js +1 -1
|
@@ -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
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Load compiled delegation-gate SSOT from consumer or package dist.
|
|
4
|
+
* Used by Grok hooks, Hermes bridge, and verify fixtures.
|
|
5
|
+
*/
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
/** 0xray package root — never use consumer workspace env (GROK_WORKSPACE_ROOT / XRAY_ROOT). */
|
|
14
|
+
function resolvePackageRoot() {
|
|
15
|
+
let dir = __dirname;
|
|
16
|
+
for (let i = 0; i < 10; i++) {
|
|
17
|
+
const pkg = join(dir, 'package.json');
|
|
18
|
+
if (existsSync(pkg)) {
|
|
19
|
+
try {
|
|
20
|
+
const require = createRequire(join(dir, 'package.json'));
|
|
21
|
+
const name = require('./package.json').name;
|
|
22
|
+
if (name === '0xray') return dir;
|
|
23
|
+
} catch {
|
|
24
|
+
/* continue */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const nm = join(dir, 'node_modules', '0xray', 'package.json');
|
|
28
|
+
if (existsSync(nm)) {
|
|
29
|
+
return dirname(nm);
|
|
30
|
+
}
|
|
31
|
+
const parent = dirname(dir);
|
|
32
|
+
if (parent === dir) break;
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
return resolve(__dirname, '../../..');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadDelegationGate() {
|
|
39
|
+
const root = resolvePackageRoot();
|
|
40
|
+
const candidates = [
|
|
41
|
+
join(__dirname, '../../nucleus/delegation-gate.js'),
|
|
42
|
+
join(root, 'dist/nucleus/delegation-gate.js'),
|
|
43
|
+
join(process.cwd(), 'node_modules/0xray/dist/nucleus/delegation-gate.js'),
|
|
44
|
+
];
|
|
45
|
+
const found = candidates.find((p) => existsSync(p));
|
|
46
|
+
if (!found) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'delegation-gate.js missing — run npm run build in 0xray or npm install 0xray@^3.5.1',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return createRequire(import.meta.url)(found);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const gate = loadDelegationGate();
|
|
55
|
+
|
|
56
|
+
export const {
|
|
57
|
+
loadDelegationGateFeatures,
|
|
58
|
+
normalizeHostToolInput,
|
|
59
|
+
evaluatePreToolGate,
|
|
60
|
+
evaluatePendingDelegationGate,
|
|
61
|
+
evaluateSpawnPlanGate,
|
|
62
|
+
evaluatePostToolSpawn,
|
|
63
|
+
checkPendingDelegationGate,
|
|
64
|
+
checkSubagentGate,
|
|
65
|
+
satisfyDelegationsFromToolInput,
|
|
66
|
+
isSubagentTool,
|
|
67
|
+
isOrchestrateToolEvent,
|
|
68
|
+
isReadOnlyTool,
|
|
69
|
+
isWriteTool,
|
|
70
|
+
isShellTool,
|
|
71
|
+
getActivePendingDelegations,
|
|
72
|
+
validateSpawnMatchesTodo,
|
|
73
|
+
updatePlanTodoStatus,
|
|
74
|
+
updatePlanTodoStatusInPlace,
|
|
75
|
+
} = gate;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-host delegation gate SSOT — pending-delegations + spawn todo enforcement.
|
|
3
|
+
* Grok / Hermes / OpenCode adapters call evaluatePreToolGate; Grok-only logic was
|
|
4
|
+
* previously duplicated in grok-hook-utils.js.
|
|
5
|
+
*/
|
|
6
|
+
import { type PendingDelegation } from './pending-delegations.js';
|
|
7
|
+
export { getActivePendingDelegations } from './pending-delegations.js';
|
|
8
|
+
import { updatePlanTodoStatus, type SpawnToolInput } from './lead-dev-plan-persistence.js';
|
|
9
|
+
export { validateSpawnMatchesTodo, updatePlanTodoStatus, savePersistedLeadDevPlan, } from './lead-dev-plan-persistence.js';
|
|
10
|
+
export declare const updatePlanTodoStatusInPlace: typeof updatePlanTodoStatus;
|
|
11
|
+
export type DelegationGateHost = 'grok' | 'hermes' | 'opencode' | 'openclaw' | 'generic';
|
|
12
|
+
export interface DelegationGateFeatures {
|
|
13
|
+
lead_dev_mode: boolean;
|
|
14
|
+
auto_chain_delegations: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ToolGateInput {
|
|
17
|
+
prompt?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
task?: string;
|
|
20
|
+
subagent_type?: string;
|
|
21
|
+
agent?: string;
|
|
22
|
+
delegationId?: string;
|
|
23
|
+
delegation_id?: string;
|
|
24
|
+
planTodoId?: string;
|
|
25
|
+
command?: string;
|
|
26
|
+
path?: string;
|
|
27
|
+
file_path?: string;
|
|
28
|
+
filePath?: string;
|
|
29
|
+
toolName?: string;
|
|
30
|
+
tool?: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
server?: string;
|
|
33
|
+
mcpServer?: string;
|
|
34
|
+
arguments?: Record<string, unknown>;
|
|
35
|
+
args?: Record<string, unknown>;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
export interface PreToolGateContext {
|
|
39
|
+
projectRoot: string;
|
|
40
|
+
sessionId: string | null;
|
|
41
|
+
features: DelegationGateFeatures;
|
|
42
|
+
host?: DelegationGateHost;
|
|
43
|
+
}
|
|
44
|
+
export interface PreToolGateDeny {
|
|
45
|
+
allow: false;
|
|
46
|
+
reason: string;
|
|
47
|
+
gate: string;
|
|
48
|
+
hint?: Record<string, unknown>;
|
|
49
|
+
pendingCount?: number;
|
|
50
|
+
delegationId?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface PreToolGateAllow {
|
|
53
|
+
allow: true;
|
|
54
|
+
}
|
|
55
|
+
export type PreToolGateResult = PreToolGateAllow | PreToolGateDeny;
|
|
56
|
+
export interface PostToolSpawnResult {
|
|
57
|
+
satisfied: PendingDelegation[];
|
|
58
|
+
clearedAll: boolean;
|
|
59
|
+
expectedTodoId?: string | null;
|
|
60
|
+
}
|
|
61
|
+
export declare function loadDelegationGateFeatures(projectRoot?: string): DelegationGateFeatures;
|
|
62
|
+
export declare function normalizeHostToolInput(toolInput: ToolGateInput): SpawnToolInput & {
|
|
63
|
+
command?: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function isSubagentTool(toolName: string): boolean;
|
|
66
|
+
export declare function isShellTool(toolName: string): boolean;
|
|
67
|
+
export declare function isWriteTool(toolName: string, host?: DelegationGateHost): boolean;
|
|
68
|
+
export declare function isReadOnlyTool(toolName: string): boolean;
|
|
69
|
+
/** Spawn plan missing — applies before auto_chain opt-out for subagent tools. */
|
|
70
|
+
export declare function evaluateSpawnPlanGate(toolName: string, toolInput: ToolGateInput, ctx: PreToolGateContext): PreToolGateResult;
|
|
71
|
+
/** Block writes / unrelated work while pending delegations exist. */
|
|
72
|
+
export declare function evaluatePendingDelegationGate(toolName: string, toolInput: ToolGateInput, ctx: PreToolGateContext): PreToolGateResult;
|
|
73
|
+
/**
|
|
74
|
+
* Full pre-tool evaluation — same order as Grok PreToolUse:
|
|
75
|
+
* pending gate first, then spawn todo gate for subagent tools.
|
|
76
|
+
*/
|
|
77
|
+
export declare function evaluatePreToolGate(toolName: string, toolInput: ToolGateInput, ctx: PreToolGateContext): PreToolGateResult;
|
|
78
|
+
export declare function evaluatePostToolSpawn(toolName: string, toolInput: ToolGateInput, projectRoot: string): PostToolSpawnResult;
|
|
79
|
+
export declare function isOrchestrateToolEvent(toolName: string, toolInput?: ToolGateInput): boolean;
|
|
80
|
+
/** Back-compat shims for Grok hook-utils and tests. */
|
|
81
|
+
export declare function checkPendingDelegationGate(toolName: string, toolInput: ToolGateInput, features: DelegationGateFeatures, projectRoot: string, sessionId: string | null): PreToolGateDeny | null;
|
|
82
|
+
export declare function checkSubagentGate(toolName: string, features: DelegationGateFeatures, projectRoot: string, sessionId: string | null, toolInput?: ToolGateInput): PreToolGateDeny | null;
|
|
83
|
+
export declare function satisfyDelegationsFromToolInput(toolInput: ToolGateInput, projectRoot: string): {
|
|
84
|
+
satisfied: PendingDelegation[];
|
|
85
|
+
clearedAll: boolean;
|
|
86
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-host delegation gate SSOT — pending-delegations + spawn todo enforcement.
|
|
3
|
+
* Grok / Hermes / OpenCode adapters call evaluatePreToolGate; Grok-only logic was
|
|
4
|
+
* previously duplicated in grok-hook-utils.js.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { getActivePendingDelegations, satisfyDelegation, } from './pending-delegations.js';
|
|
9
|
+
export { getActivePendingDelegations } from './pending-delegations.js';
|
|
10
|
+
import { allPlanTodos, hasValidLeadDevPlanForSpawn, loadPersistedLeadDevPlan, validateSpawnMatchesTodo, updatePlanTodoStatus, } from './lead-dev-plan-persistence.js';
|
|
11
|
+
export { validateSpawnMatchesTodo, updatePlanTodoStatus, savePersistedLeadDevPlan, } from './lead-dev-plan-persistence.js';
|
|
12
|
+
export const updatePlanTodoStatusInPlace = updatePlanTodoStatus;
|
|
13
|
+
const READ_TOOLS = new Set([
|
|
14
|
+
'read_file',
|
|
15
|
+
'Read',
|
|
16
|
+
'read',
|
|
17
|
+
'grep',
|
|
18
|
+
'Grep',
|
|
19
|
+
'glob',
|
|
20
|
+
'Glob',
|
|
21
|
+
'list_dir',
|
|
22
|
+
'ListDir',
|
|
23
|
+
'web_search',
|
|
24
|
+
'WebSearch',
|
|
25
|
+
'codebase_search',
|
|
26
|
+
'SemanticSearch',
|
|
27
|
+
'search_files',
|
|
28
|
+
]);
|
|
29
|
+
const SUBAGENT_TOOLS = new Set(['spawn_subagent', 'task', 'Task', 'delegate_task']);
|
|
30
|
+
const ORCHESTRATOR_CONSULT_TOOLS = new Set([
|
|
31
|
+
'analyze-complexity',
|
|
32
|
+
'analyze_complexity',
|
|
33
|
+
'get-orchestration-status',
|
|
34
|
+
'get_orchestration_status',
|
|
35
|
+
'orchestrate-task',
|
|
36
|
+
'orchestrate_task',
|
|
37
|
+
]);
|
|
38
|
+
const HERMES_WRITE_TOOLS = new Set(['write_file', 'patch', 'write', 'edit']);
|
|
39
|
+
const OPENCODE_WRITE_TOOLS = new Set(['write', 'edit', 'multiedit']);
|
|
40
|
+
export function loadDelegationGateFeatures(projectRoot = process.cwd()) {
|
|
41
|
+
const featuresPath = path.join(projectRoot, '.xray', 'features.json');
|
|
42
|
+
if (!fs.existsSync(featuresPath)) {
|
|
43
|
+
return { lead_dev_mode: true, auto_chain_delegations: true };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const data = JSON.parse(fs.readFileSync(featuresPath, 'utf8'));
|
|
47
|
+
const orch = data.multi_agent_orchestration ?? {};
|
|
48
|
+
return {
|
|
49
|
+
lead_dev_mode: orch.enabled !== false && orch.lead_dev_mode !== false,
|
|
50
|
+
auto_chain_delegations: orch.auto_chain_delegations !== false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { lead_dev_mode: true, auto_chain_delegations: true };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function normalizeHostToolInput(toolInput) {
|
|
58
|
+
const normalized = {
|
|
59
|
+
prompt: String(toolInput.prompt ?? toolInput.description ?? toolInput.task ?? ''),
|
|
60
|
+
subagent_type: String(toolInput.subagent_type ?? toolInput.agent ?? ''),
|
|
61
|
+
};
|
|
62
|
+
if (toolInput.description != null)
|
|
63
|
+
normalized.description = String(toolInput.description);
|
|
64
|
+
if (toolInput.task != null)
|
|
65
|
+
normalized.task = String(toolInput.task);
|
|
66
|
+
if (toolInput.agent != null)
|
|
67
|
+
normalized.agent = String(toolInput.agent);
|
|
68
|
+
const delegationId = toolInput.delegationId ?? toolInput.delegation_id;
|
|
69
|
+
if (delegationId != null)
|
|
70
|
+
normalized.delegationId = String(delegationId);
|
|
71
|
+
if (toolInput.planTodoId != null)
|
|
72
|
+
normalized.planTodoId = String(toolInput.planTodoId);
|
|
73
|
+
if (toolInput.command != null)
|
|
74
|
+
normalized.command = String(toolInput.command);
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
export function isSubagentTool(toolName) {
|
|
78
|
+
return SUBAGENT_TOOLS.has(toolName) || /^task$/i.test(toolName) || toolName === 'spawn_subagent';
|
|
79
|
+
}
|
|
80
|
+
export function isShellTool(toolName) {
|
|
81
|
+
return /terminal|bash|shell|run_terminal/i.test(toolName);
|
|
82
|
+
}
|
|
83
|
+
export function isWriteTool(toolName, host = 'generic') {
|
|
84
|
+
if (host === 'hermes' && HERMES_WRITE_TOOLS.has(toolName))
|
|
85
|
+
return true;
|
|
86
|
+
if (host === 'opencode' && OPENCODE_WRITE_TOOLS.has(toolName))
|
|
87
|
+
return true;
|
|
88
|
+
return (/write|edit|replace|patch|notebook/i.test(toolName) &&
|
|
89
|
+
!isReadOnlyTool(toolName));
|
|
90
|
+
}
|
|
91
|
+
export function isReadOnlyTool(toolName) {
|
|
92
|
+
return READ_TOOLS.has(toolName) || /^(read|grep|glob|list|search_files)/i.test(toolName);
|
|
93
|
+
}
|
|
94
|
+
function extractMcpToolName(toolInput) {
|
|
95
|
+
const candidates = [
|
|
96
|
+
toolInput.toolName,
|
|
97
|
+
toolInput.tool,
|
|
98
|
+
toolInput.name,
|
|
99
|
+
toolInput.mcpToolName,
|
|
100
|
+
];
|
|
101
|
+
for (const c of candidates) {
|
|
102
|
+
if (c)
|
|
103
|
+
return String(c);
|
|
104
|
+
}
|
|
105
|
+
const server = String(toolInput.server ?? toolInput.mcpServer ?? '');
|
|
106
|
+
const args = (toolInput.arguments ?? toolInput.args ?? {});
|
|
107
|
+
if (args.toolName)
|
|
108
|
+
return String(args.toolName);
|
|
109
|
+
if (server.includes('orchestrator') && args.name)
|
|
110
|
+
return String(args.name);
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
function isOrchestratorConsultMcp(toolName, toolInput) {
|
|
114
|
+
if (!/mcp|CallMcpTool/i.test(toolName))
|
|
115
|
+
return false;
|
|
116
|
+
const inner = extractMcpToolName(toolInput).toLowerCase();
|
|
117
|
+
if (!inner)
|
|
118
|
+
return false;
|
|
119
|
+
for (const t of ORCHESTRATOR_CONSULT_TOOLS) {
|
|
120
|
+
if (inner.includes(t) || inner.includes(t.replace(/-/g, '_')))
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function isFocusedTestCommand(cmd) {
|
|
126
|
+
const cmdText = (cmd || '').toLowerCase();
|
|
127
|
+
return (/\b(npm\s+(run\s+)?test|vitest\s+run|pnpm\s+test|yarn\s+test)\b/.test(cmdText) &&
|
|
128
|
+
(/--\s+\S+\.test/.test(cmdText) || /vitest\s+run\s+\S+\.test/.test(cmdText)));
|
|
129
|
+
}
|
|
130
|
+
function denyFromSpawnValidation(validation) {
|
|
131
|
+
const deny = {
|
|
132
|
+
allow: false,
|
|
133
|
+
reason: validation.reason ?? 'Spawn blocked by lead-dev plan',
|
|
134
|
+
gate: validation.gate ?? 'spawn-todo-persistence',
|
|
135
|
+
};
|
|
136
|
+
if (validation.hint)
|
|
137
|
+
deny.hint = validation.hint;
|
|
138
|
+
return deny;
|
|
139
|
+
}
|
|
140
|
+
function denyFromPending(primary, pendingCount) {
|
|
141
|
+
return {
|
|
142
|
+
allow: false,
|
|
143
|
+
reason: 'Pending implementation delegation — spawn host Task before other work',
|
|
144
|
+
gate: 'auto-chain-pending',
|
|
145
|
+
hint: { ...(primary.spawnHint ?? {
|
|
146
|
+
tool: 'Task',
|
|
147
|
+
subagent_type: primary.agent,
|
|
148
|
+
description: primary.taskDescription,
|
|
149
|
+
planTodoId: primary.planTodoId,
|
|
150
|
+
delegationId: primary.id,
|
|
151
|
+
}) },
|
|
152
|
+
pendingCount,
|
|
153
|
+
delegationId: primary.id,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Spawn plan missing — applies before auto_chain opt-out for subagent tools. */
|
|
157
|
+
export function evaluateSpawnPlanGate(toolName, toolInput, ctx) {
|
|
158
|
+
if (!ctx.features.lead_dev_mode || !isSubagentTool(toolName)) {
|
|
159
|
+
return { allow: true };
|
|
160
|
+
}
|
|
161
|
+
if (!hasValidLeadDevPlanForSpawn(ctx.projectRoot, ctx.sessionId)) {
|
|
162
|
+
return {
|
|
163
|
+
allow: false,
|
|
164
|
+
reason: 'Codex 59/67: call xray-orchestrator analyze-complexity first — ' +
|
|
165
|
+
'writes .xray/state/lead-dev-plan.json required before spawn_subagent',
|
|
166
|
+
gate: 'spawn-plan-missing',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (ctx.features.auto_chain_delegations === false) {
|
|
170
|
+
return { allow: true };
|
|
171
|
+
}
|
|
172
|
+
const pending = getActivePendingDelegations(ctx.sessionId, ctx.projectRoot);
|
|
173
|
+
const plan = loadPersistedLeadDevPlan(ctx.projectRoot);
|
|
174
|
+
const expectedTodo = pending[0]?.planTodoId && plan
|
|
175
|
+
? allPlanTodos(plan).find((t) => t.id === pending[0].planTodoId) ?? null
|
|
176
|
+
: null;
|
|
177
|
+
const validation = validateSpawnMatchesTodo(normalizeHostToolInput(toolInput), ctx.projectRoot, expectedTodo);
|
|
178
|
+
if (!validation.valid) {
|
|
179
|
+
return denyFromSpawnValidation(validation);
|
|
180
|
+
}
|
|
181
|
+
return { allow: true };
|
|
182
|
+
}
|
|
183
|
+
/** Block writes / unrelated work while pending delegations exist. */
|
|
184
|
+
export function evaluatePendingDelegationGate(toolName, toolInput, ctx) {
|
|
185
|
+
if (!ctx.features.lead_dev_mode || ctx.features.auto_chain_delegations === false) {
|
|
186
|
+
return { allow: true };
|
|
187
|
+
}
|
|
188
|
+
if (!ctx.sessionId)
|
|
189
|
+
return { allow: true };
|
|
190
|
+
const pending = getActivePendingDelegations(ctx.sessionId, ctx.projectRoot);
|
|
191
|
+
if (pending.length === 0)
|
|
192
|
+
return { allow: true };
|
|
193
|
+
if (isSubagentTool(toolName))
|
|
194
|
+
return { allow: true };
|
|
195
|
+
if (isReadOnlyTool(toolName))
|
|
196
|
+
return { allow: true };
|
|
197
|
+
if (isOrchestratorConsultMcp(toolName, toolInput))
|
|
198
|
+
return { allow: true };
|
|
199
|
+
if (isShellTool(toolName)) {
|
|
200
|
+
const cmd = String(toolInput.command ?? '');
|
|
201
|
+
if (isFocusedTestCommand(cmd))
|
|
202
|
+
return { allow: true };
|
|
203
|
+
}
|
|
204
|
+
return denyFromPending(pending[0], pending.length);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Full pre-tool evaluation — same order as Grok PreToolUse:
|
|
208
|
+
* pending gate first, then spawn todo gate for subagent tools.
|
|
209
|
+
*/
|
|
210
|
+
export function evaluatePreToolGate(toolName, toolInput, ctx) {
|
|
211
|
+
const pendingBlock = evaluatePendingDelegationGate(toolName, toolInput, ctx);
|
|
212
|
+
if (!pendingBlock.allow)
|
|
213
|
+
return pendingBlock;
|
|
214
|
+
const spawnBlock = evaluateSpawnPlanGate(toolName, toolInput, ctx);
|
|
215
|
+
if (!spawnBlock.allow)
|
|
216
|
+
return spawnBlock;
|
|
217
|
+
return { allow: true };
|
|
218
|
+
}
|
|
219
|
+
export function evaluatePostToolSpawn(toolName, toolInput, projectRoot) {
|
|
220
|
+
const normalized = normalizeHostToolInput(toolInput);
|
|
221
|
+
const spawnCheck = validateSpawnMatchesTodo(normalized, projectRoot);
|
|
222
|
+
const satisfyInput = {
|
|
223
|
+
toolPrompt: normalized.prompt ?? '',
|
|
224
|
+
};
|
|
225
|
+
if (normalized.delegationId)
|
|
226
|
+
satisfyInput.delegationId = normalized.delegationId;
|
|
227
|
+
const agent = normalized.subagent_type || normalized.agent;
|
|
228
|
+
if (agent)
|
|
229
|
+
satisfyInput.agent = agent;
|
|
230
|
+
if (normalized.planTodoId)
|
|
231
|
+
satisfyInput.planTodoId = normalized.planTodoId;
|
|
232
|
+
const result = satisfyDelegation(satisfyInput, projectRoot);
|
|
233
|
+
return {
|
|
234
|
+
...result,
|
|
235
|
+
expectedTodoId: spawnCheck.expectedTodoId ?? null,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export function isOrchestrateToolEvent(toolName, toolInput = {}) {
|
|
239
|
+
const inner = extractMcpToolName(toolInput).toLowerCase();
|
|
240
|
+
if (/orchestrate[-_]?task/.test(inner))
|
|
241
|
+
return true;
|
|
242
|
+
const blob = JSON.stringify(toolInput).toLowerCase();
|
|
243
|
+
return /orchestrate[-_]?task/.test(blob);
|
|
244
|
+
}
|
|
245
|
+
/** Back-compat shims for Grok hook-utils and tests. */
|
|
246
|
+
export function checkPendingDelegationGate(toolName, toolInput, features, projectRoot, sessionId) {
|
|
247
|
+
const result = evaluatePendingDelegationGate(toolName, toolInput, {
|
|
248
|
+
projectRoot,
|
|
249
|
+
sessionId,
|
|
250
|
+
features,
|
|
251
|
+
});
|
|
252
|
+
return result.allow ? null : result;
|
|
253
|
+
}
|
|
254
|
+
export function checkSubagentGate(toolName, features, projectRoot, sessionId, toolInput = {}) {
|
|
255
|
+
const result = evaluateSpawnPlanGate(toolName, toolInput, {
|
|
256
|
+
projectRoot,
|
|
257
|
+
sessionId,
|
|
258
|
+
features,
|
|
259
|
+
});
|
|
260
|
+
return result.allow ? null : result;
|
|
261
|
+
}
|
|
262
|
+
export function satisfyDelegationsFromToolInput(toolInput, projectRoot) {
|
|
263
|
+
const normalized = normalizeHostToolInput(toolInput);
|
|
264
|
+
const satisfyInput = {
|
|
265
|
+
toolPrompt: normalized.prompt ?? '',
|
|
266
|
+
};
|
|
267
|
+
if (normalized.delegationId)
|
|
268
|
+
satisfyInput.delegationId = normalized.delegationId;
|
|
269
|
+
const agent = normalized.subagent_type || normalized.agent;
|
|
270
|
+
if (agent)
|
|
271
|
+
satisfyInput.agent = agent;
|
|
272
|
+
if (normalized.planTodoId)
|
|
273
|
+
satisfyInput.planTodoId = normalized.planTodoId;
|
|
274
|
+
return satisfyDelegation(satisfyInput, projectRoot);
|
|
275
|
+
}
|
|
@@ -26,15 +26,15 @@ export function savePersistedLeadDevPlan(plan, projectRoot = process.cwd()) {
|
|
|
26
26
|
return planPath;
|
|
27
27
|
}
|
|
28
28
|
export function allPlanTodos(plan) {
|
|
29
|
-
return plan.phases.flatMap((p) => p.todos);
|
|
29
|
+
return (plan.phases ?? []).flatMap((p) => p.todos ?? []);
|
|
30
30
|
}
|
|
31
31
|
export function getOutstandingTodos(plan) {
|
|
32
32
|
return allPlanTodos(plan).filter((t) => t.status === 'pending' || t.status === 'in_progress');
|
|
33
33
|
}
|
|
34
34
|
/** First outstanding todo in phase order */
|
|
35
35
|
export function getNextRequiredTodo(plan) {
|
|
36
|
-
for (const phase of plan.phases) {
|
|
37
|
-
const next = phase.todos.find((t) => t.status === 'pending' || t.status === 'in_progress');
|
|
36
|
+
for (const phase of plan.phases ?? []) {
|
|
37
|
+
const next = (phase.todos ?? []).find((t) => t.status === 'pending' || t.status === 'in_progress');
|
|
38
38
|
if (next)
|
|
39
39
|
return next;
|
|
40
40
|
}
|
|
@@ -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");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0xray",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.1",
|
|
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,9 @@
|
|
|
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",
|
|
131
|
+
"verify:hermes-delegation-gate": "npm run build && node scripts/mjs/verify-hermes-delegation-gate.mjs",
|
|
132
|
+
"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-hermes-delegation-gate.mjs"
|
|
131
133
|
},
|
|
132
134
|
"files": [
|
|
133
135
|
"dist/",
|
|
@@ -141,6 +143,9 @@
|
|
|
141
143
|
"scripts/mjs/validate-postinstall-config.mjs",
|
|
142
144
|
"scripts/mjs/verify-orchestrator-behavior.mjs",
|
|
143
145
|
"scripts/mjs/verify-grok-delegation-gate.mjs",
|
|
146
|
+
"scripts/mjs/verify-hermes-delegation-gate.mjs",
|
|
147
|
+
"scripts/mjs/verify-delegation-gate-core.mjs",
|
|
148
|
+
"scripts/mjs/delegation-gate-fixture.mjs",
|
|
144
149
|
"src/opencode/",
|
|
145
150
|
"src/opencode/codex.codex",
|
|
146
151
|
"src/opencode/commands/",
|