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
|
@@ -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;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { frameworkLogger } from '../../../core/framework-logger.js';
|
|
7
7
|
import { buildLeadDevPlan, isLeadDevModeActive, persistLeadDevPlan, } from '../../../nucleus/autonomy-kernel.js';
|
|
8
|
+
import { bindPlanToSession } from '../../../nucleus/lead-dev-plan-persistence.js';
|
|
9
|
+
import { clearPendingDelegations } from '../../../nucleus/pending-delegations.js';
|
|
8
10
|
import { getExecutionPlanner } from '../execution/execution-planner.js';
|
|
9
11
|
import { addObservations, extractComplexityObservations } from '../aside-context.js';
|
|
10
12
|
function normalizeAnalyzeTasks(raw) {
|
|
@@ -63,9 +65,14 @@ export class ComplexityHandler {
|
|
|
63
65
|
: null;
|
|
64
66
|
if (leadDevPlan) {
|
|
65
67
|
try {
|
|
68
|
+
const sessionId = args.sessionId ?? `analyze-${Date.now()}`;
|
|
69
|
+
clearPendingDelegations();
|
|
66
70
|
const planPath = persistLeadDevPlan(leadDevPlan);
|
|
71
|
+
const bound = bindPlanToSession(sessionId);
|
|
67
72
|
await frameworkLogger.log('orchestrator.server', 'lead-dev-plan-persisted', 'info', {
|
|
68
73
|
planPath,
|
|
74
|
+
sessionId,
|
|
75
|
+
planGeneration: bound?.planGeneration,
|
|
69
76
|
phaseCount: leadDevPlan.phases.length,
|
|
70
77
|
todoCount: leadDevPlan.phases.reduce((n, p) => n + p.todos.length, 0),
|
|
71
78
|
});
|
|
@@ -8,6 +8,7 @@ import { getExecutionPlanner } from '../execution/execution-planner.js';
|
|
|
8
8
|
import { mcpClientManager } from '../../../mcps/mcp-client.js';
|
|
9
9
|
import { addObservations, extractOrchestrationObservations, spawnAside } from '../aside-context.js';
|
|
10
10
|
import { getProvider } from '../config/memory-routing-bridge.js';
|
|
11
|
+
import { delegationSummaryForResponse, recordDeferredDelegations, } from '../../../nucleus/pending-delegations.js';
|
|
11
12
|
/**
|
|
12
13
|
* Task Handler
|
|
13
14
|
* Processes orchestrate-task requests
|
|
@@ -31,6 +32,7 @@ export class TaskHandler {
|
|
|
31
32
|
recommendations: [],
|
|
32
33
|
};
|
|
33
34
|
const startTime = Date.now();
|
|
35
|
+
let pendingDelegations = [];
|
|
34
36
|
try {
|
|
35
37
|
// Validate tasks
|
|
36
38
|
const validation = this.planner.validateTasks(tasks);
|
|
@@ -50,6 +52,10 @@ export class TaskHandler {
|
|
|
50
52
|
});
|
|
51
53
|
}
|
|
52
54
|
const results = await this.executePlan(executionPlan, sessionId, timeout);
|
|
55
|
+
if (results.deferredRecords.length > 0) {
|
|
56
|
+
pendingDelegations = recordDeferredDelegations(sessionId, results.deferredRecords);
|
|
57
|
+
await frameworkLogger.log('orchestrator.task-handler', 'pending-delegations-recorded', 'info', { count: pendingDelegations.length, sessionId });
|
|
58
|
+
}
|
|
53
59
|
// Feed per-task outcomes back to memory provider (not aggregate session stats)
|
|
54
60
|
if (memoryProvider.ingestFeedback) {
|
|
55
61
|
const outcomeByKey = new Map(results.taskOutcomes.map((o) => [`${o.agent}:${o.taskId}`, o]));
|
|
@@ -100,7 +106,7 @@ export class TaskHandler {
|
|
|
100
106
|
orchestrationResult.recommendations.push(`Orchestration error: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
107
|
}
|
|
102
108
|
orchestrationResult.duration = Date.now() - startTime;
|
|
103
|
-
const response = this.formatOrchestrationResponse(description, orchestrationResult, executionMode);
|
|
109
|
+
const response = this.formatOrchestrationResponse(description, orchestrationResult, executionMode, pendingDelegations);
|
|
104
110
|
return { content: [{ type: 'text', text: response }] };
|
|
105
111
|
}
|
|
106
112
|
/**
|
|
@@ -114,6 +120,7 @@ export class TaskHandler {
|
|
|
114
120
|
let failedTasks = 0;
|
|
115
121
|
let deferredTasks = 0;
|
|
116
122
|
const agentOutputs = {};
|
|
123
|
+
const deferredRecords = [];
|
|
117
124
|
const taskOutcomes = [];
|
|
118
125
|
const skillServers = new Set([
|
|
119
126
|
'code-review', 'code-reviewer',
|
|
@@ -167,6 +174,12 @@ export class TaskHandler {
|
|
|
167
174
|
else {
|
|
168
175
|
for (const task of tasks) {
|
|
169
176
|
deferredTasks++;
|
|
177
|
+
deferredRecords.push({
|
|
178
|
+
taskId: task.id,
|
|
179
|
+
agent,
|
|
180
|
+
description: task.description,
|
|
181
|
+
type: task.type,
|
|
182
|
+
});
|
|
170
183
|
taskOutcomes.push({
|
|
171
184
|
taskId: task.id,
|
|
172
185
|
agent,
|
|
@@ -199,6 +212,7 @@ export class TaskHandler {
|
|
|
199
212
|
bottlenecks,
|
|
200
213
|
recommendations,
|
|
201
214
|
agentOutputs,
|
|
215
|
+
deferredRecords,
|
|
202
216
|
taskOutcomes,
|
|
203
217
|
};
|
|
204
218
|
}
|
|
@@ -232,7 +246,10 @@ export class TaskHandler {
|
|
|
232
246
|
/**
|
|
233
247
|
* Format orchestration response
|
|
234
248
|
*/
|
|
235
|
-
formatOrchestrationResponse(description, result, executionMode) {
|
|
249
|
+
formatOrchestrationResponse(description, result, executionMode, pendingDelegations = []) {
|
|
250
|
+
const delegationsBlock = pendingDelegations.length > 0
|
|
251
|
+
? `\n**Delegations (host Task required):**\n\`\`\`json\n${JSON.stringify({ delegations: delegationSummaryForResponse(pendingDelegations) }, null, 2)}\n\`\`\`\n`
|
|
252
|
+
: '';
|
|
236
253
|
return `🎯 Orchestration Complete: "${description}"
|
|
237
254
|
|
|
238
255
|
**Session ID:** ${result.sessionId}
|
|
@@ -257,6 +274,6 @@ ${result.recommendations.length > 0 ? result.recommendations.map((r) => `•
|
|
|
257
274
|
|
|
258
275
|
${result.agentOutputs && Object.keys(result.agentOutputs).length > 0
|
|
259
276
|
? `**Agent Outputs (real MCP responses):**\n${Object.entries(result.agentOutputs).map(([k, v]) => `• ${k}: ${v.substring(0, 200)}${v.length > 200 ? '...' : ''}`).join('\n')}\n`
|
|
260
|
-
: ''}`;
|
|
277
|
+
: ''}${delegationsBlock}`;
|
|
261
278
|
}
|
|
262
279
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo-level lead-dev plan persistence — spawn gate survives beyond 4h mtime
|
|
3
|
+
* while outstanding todos remain. SSOT for plan todo status sync.
|
|
4
|
+
*/
|
|
5
|
+
import type { LeadDevPlan, LeadDevTodo } from './autonomy-kernel.js';
|
|
6
|
+
export interface PersistedLeadDevPlan extends LeadDevPlan {
|
|
7
|
+
persistedAt?: string;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
planGeneration?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SpawnTodoValidation {
|
|
12
|
+
valid: boolean;
|
|
13
|
+
reason?: string;
|
|
14
|
+
gate?: string;
|
|
15
|
+
hint?: Record<string, unknown>;
|
|
16
|
+
expectedTodoId?: string | null;
|
|
17
|
+
}
|
|
18
|
+
export declare function leadDevPlanStatePath(projectRoot?: string): string;
|
|
19
|
+
export declare function loadPersistedLeadDevPlan(projectRoot?: string): PersistedLeadDevPlan | null;
|
|
20
|
+
export declare function savePersistedLeadDevPlan(plan: PersistedLeadDevPlan, projectRoot?: string): string;
|
|
21
|
+
export declare function allPlanTodos(plan: PersistedLeadDevPlan): LeadDevTodo[];
|
|
22
|
+
export declare function getOutstandingTodos(plan: PersistedLeadDevPlan): LeadDevTodo[];
|
|
23
|
+
/** First outstanding todo in phase order */
|
|
24
|
+
export declare function getNextRequiredTodo(plan: PersistedLeadDevPlan): LeadDevTodo | null;
|
|
25
|
+
/**
|
|
26
|
+
* Plan valid for spawn when active and either:
|
|
27
|
+
* - outstanding todos remain (ignores 4h mtime), or
|
|
28
|
+
* - all todos complete and persistedAt within TTL
|
|
29
|
+
*/
|
|
30
|
+
export declare function hasValidLeadDevPlanForSpawn(projectRoot?: string, _sessionId?: string | null): boolean;
|
|
31
|
+
export declare function updatePlanTodoStatus(todoId: string, status: LeadDevTodo['status'], projectRoot?: string): boolean;
|
|
32
|
+
export declare function bindPlanToSession(sessionId: string, projectRoot?: string): PersistedLeadDevPlan | null;
|
|
33
|
+
export interface SpawnToolInput {
|
|
34
|
+
prompt?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
task?: string;
|
|
37
|
+
subagent_type?: string;
|
|
38
|
+
agent?: string;
|
|
39
|
+
delegationId?: string;
|
|
40
|
+
planTodoId?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function validateSpawnMatchesTodo(toolInput: SpawnToolInput, projectRoot?: string, expectedTodo?: LeadDevTodo | null): SpawnTodoValidation;
|
|
43
|
+
export declare function markTodoInProgressOnSpawn(toolInput: SpawnToolInput, projectRoot?: string): string | null;
|