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
@@ -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;
@@ -26,6 +26,7 @@ export declare class ComplexityHandler {
26
26
  */
27
27
  handleAnalyzeComplexity(args: {
28
28
  tasks: AnalyzeComplexityInput[];
29
+ sessionId?: string;
29
30
  }, asideId?: string): Promise<{
30
31
  content: Array<{
31
32
  type: string;
@@ -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;