0xray 3.4.10 → 3.5.0

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.
@@ -7,6 +7,7 @@
7
7
  import {
8
8
  checkCodexPatterns,
9
9
  checkFullTestSuite,
10
+ checkPendingDelegationGate,
10
11
  checkSubagentGate,
11
12
  checkSurfaceArea,
12
13
  ensureSessionBoot,
@@ -14,18 +15,20 @@ import {
14
15
  isWriteTool,
15
16
  loadFeatures,
16
17
  readStdinJson,
18
+ resolveSessionId,
17
19
  workspaceRoot,
18
20
  } from './grok-hook-utils.js';
19
21
  import { appendHookActivity } from './grok-hook-activity.js';
20
22
 
21
- function finish(root, decision, reason, hint, toolName) {
22
- const out = { decision };
23
+ function finish(root, decision, reason, hint, toolName, extra = {}) {
24
+ const out = { decision, ...extra };
23
25
  if (reason) out.reason = reason;
24
26
  if (hint) out.hint = hint;
25
27
  console.log(JSON.stringify(out));
26
28
  appendHookActivity(root, 'grok-pre-tool-use', decision, decision === 'deny' ? 'error' : 'info', {
27
29
  tool: toolName,
28
30
  reason: reason || hint || null,
31
+ gate: extra.gate || null,
29
32
  livePath: true,
30
33
  });
31
34
  process.exit(0);
@@ -43,10 +46,44 @@ async function main() {
43
46
  const features = loadFeatures();
44
47
  const ctx = extractFromEvent(event);
45
48
  toolName = ctx.toolName;
46
- const { paths, content, cmd } = ctx;
49
+ const { paths, content, cmd, toolInput } = ctx;
50
+ const sessionId = resolveSessionId(event);
51
+
52
+ const delegationBlock = checkPendingDelegationGate(
53
+ toolName,
54
+ toolInput,
55
+ features,
56
+ eventRoot,
57
+ sessionId,
58
+ );
59
+ if (delegationBlock) {
60
+ finish(
61
+ eventRoot,
62
+ 'deny',
63
+ delegationBlock.reason,
64
+ delegationBlock.hint,
65
+ toolName,
66
+ { gate: delegationBlock.gate },
67
+ );
68
+ }
47
69
 
48
- const subagentBlock = checkSubagentGate(toolName, features);
49
- if (subagentBlock) finish(eventRoot, 'deny', subagentBlock, null, toolName);
70
+ const subagentBlock = checkSubagentGate(
71
+ toolName,
72
+ features,
73
+ eventRoot,
74
+ sessionId,
75
+ toolInput,
76
+ );
77
+ if (subagentBlock) {
78
+ finish(
79
+ eventRoot,
80
+ 'deny',
81
+ subagentBlock.reason || subagentBlock,
82
+ subagentBlock.hint || null,
83
+ toolName,
84
+ { gate: subagentBlock.gate || 'spawn-gate' },
85
+ );
86
+ }
50
87
 
51
88
  if (features.no_new_surface && isWriteTool(toolName) && paths.length) {
52
89
  const surfaceBlock = checkSurfaceArea(paths, eventRoot);
@@ -95,6 +132,7 @@ function extractFromEvent(event) {
95
132
 
96
133
  return {
97
134
  toolName,
135
+ toolInput,
98
136
  paths,
99
137
  content,
100
138
  cmd: String(toolInput.command || ''),
@@ -6,8 +6,10 @@
6
6
 
7
7
  import {
8
8
  buildSessionBootPayload,
9
+ clearPendingDelegationsForSessionChange,
9
10
  ensureSessionBoot,
10
11
  readStdinJson,
12
+ resolveSessionId,
11
13
  workspaceRoot,
12
14
  writeSessionBoot,
13
15
  } from './grok-hook-utils.js';
@@ -20,6 +22,12 @@ async function main() {
20
22
  try {
21
23
  const event = await readStdinJson();
22
24
  const eventRoot = event.workspaceRoot || event.cwd || root;
25
+ const sessionId = resolveSessionId(event);
26
+ if (clearPendingDelegationsForSessionChange(sessionId, eventRoot)) {
27
+ appendHookActivity(eventRoot, 'grok-session-start', 'stale-pending-cleared', 'info', {
28
+ sessionId,
29
+ });
30
+ }
23
31
  const source =
24
32
  HOOK_EVENT === 'user_prompt_submit'
25
33
  ? '0xray/grok-user-prompt-submit'
@@ -42,6 +42,21 @@
42
42
  }
43
43
  ]
44
44
  }
45
+ ],
46
+ "PostToolUse": [
47
+ {
48
+ "matcher": ".*",
49
+ "hooks": [
50
+ {
51
+ "type": "command",
52
+ "command": "node",
53
+ "args": [
54
+ "${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/grok/hooks/post-tool-use.js"
55
+ ],
56
+ "timeout": 15000
57
+ }
58
+ ]
59
+ }
45
60
  ]
46
61
  }
47
62
  }
@@ -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,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;
@@ -0,0 +1,155 @@
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 * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { DEFAULT_PENDING_TTL_MS } from './pending-delegations.js';
8
+ export function leadDevPlanStatePath(projectRoot = process.cwd()) {
9
+ return path.join(projectRoot, '.xray', 'state', 'lead-dev-plan.json');
10
+ }
11
+ export function loadPersistedLeadDevPlan(projectRoot = process.cwd()) {
12
+ const planPath = leadDevPlanStatePath(projectRoot);
13
+ if (!fs.existsSync(planPath))
14
+ return null;
15
+ try {
16
+ return JSON.parse(fs.readFileSync(planPath, 'utf8'));
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function savePersistedLeadDevPlan(plan, projectRoot = process.cwd()) {
23
+ const planPath = leadDevPlanStatePath(projectRoot);
24
+ fs.mkdirSync(path.dirname(planPath), { recursive: true });
25
+ fs.writeFileSync(planPath, JSON.stringify(plan, null, 2));
26
+ return planPath;
27
+ }
28
+ export function allPlanTodos(plan) {
29
+ return plan.phases.flatMap((p) => p.todos);
30
+ }
31
+ export function getOutstandingTodos(plan) {
32
+ return allPlanTodos(plan).filter((t) => t.status === 'pending' || t.status === 'in_progress');
33
+ }
34
+ /** First outstanding todo in phase order */
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');
38
+ if (next)
39
+ return next;
40
+ }
41
+ return null;
42
+ }
43
+ /**
44
+ * Plan valid for spawn when active and either:
45
+ * - outstanding todos remain (ignores 4h mtime), or
46
+ * - all todos complete and persistedAt within TTL
47
+ */
48
+ export function hasValidLeadDevPlanForSpawn(projectRoot = process.cwd(), _sessionId) {
49
+ const plan = loadPersistedLeadDevPlan(projectRoot);
50
+ if (!plan?.active)
51
+ return false;
52
+ const outstanding = getOutstandingTodos(plan);
53
+ if (outstanding.length > 0)
54
+ return true;
55
+ const persistedAt = plan.persistedAt;
56
+ if (!persistedAt) {
57
+ try {
58
+ const stat = fs.statSync(leadDevPlanStatePath(projectRoot));
59
+ return Date.now() - stat.mtimeMs <= DEFAULT_PENDING_TTL_MS;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ return Date.now() - new Date(persistedAt).getTime() <= DEFAULT_PENDING_TTL_MS;
66
+ }
67
+ export function updatePlanTodoStatus(todoId, status, projectRoot = process.cwd()) {
68
+ const plan = loadPersistedLeadDevPlan(projectRoot);
69
+ if (!plan)
70
+ return false;
71
+ let updated = false;
72
+ for (const phase of plan.phases) {
73
+ for (const todo of phase.todos) {
74
+ if (todo.id === todoId) {
75
+ todo.status = status;
76
+ updated = true;
77
+ }
78
+ }
79
+ }
80
+ if (updated)
81
+ savePersistedLeadDevPlan(plan, projectRoot);
82
+ return updated;
83
+ }
84
+ export function bindPlanToSession(sessionId, projectRoot = process.cwd()) {
85
+ const plan = loadPersistedLeadDevPlan(projectRoot);
86
+ if (!plan)
87
+ return null;
88
+ const generation = (plan.planGeneration ?? 0) + 1;
89
+ const next = {
90
+ ...plan,
91
+ sessionId,
92
+ planGeneration: generation,
93
+ persistedAt: new Date().toISOString(),
94
+ };
95
+ savePersistedLeadDevPlan(next, projectRoot);
96
+ return next;
97
+ }
98
+ function normalizeAgent(agent) {
99
+ const key = agent.toLowerCase().trim();
100
+ const aliases = {
101
+ 'bug-triage-specialist': 'bug-triage',
102
+ 'code-reviewer': 'code-review',
103
+ };
104
+ return aliases[key] ?? key;
105
+ }
106
+ function agentsAlign(expected, actual) {
107
+ const e = normalizeAgent(expected);
108
+ const a = normalizeAgent(actual);
109
+ return e === a || e.includes(a) || a.includes(e);
110
+ }
111
+ export function validateSpawnMatchesTodo(toolInput, projectRoot = process.cwd(), expectedTodo) {
112
+ const plan = loadPersistedLeadDevPlan(projectRoot);
113
+ const nextTodo = expectedTodo ?? (plan ? getNextRequiredTodo(plan) : null);
114
+ if (!nextTodo) {
115
+ return { valid: true };
116
+ }
117
+ const prompt = String(toolInput.prompt || toolInput.description || toolInput.task || '').toLowerCase();
118
+ const subagent = String(toolInput.subagent_type || toolInput.agent || '');
119
+ const explicitTodo = toolInput.planTodoId || toolInput.delegationId;
120
+ if (explicitTodo && explicitTodo === nextTodo.id) {
121
+ return { valid: true, expectedTodoId: nextTodo.id };
122
+ }
123
+ if (prompt.includes(nextTodo.id.toLowerCase())) {
124
+ return { valid: true, expectedTodoId: nextTodo.id };
125
+ }
126
+ if (prompt.includes(nextTodo.task.toLowerCase().slice(0, 30))) {
127
+ return { valid: true, expectedTodoId: nextTodo.id };
128
+ }
129
+ if (subagent && agentsAlign(nextTodo.subagent, subagent) && prompt.length > 20) {
130
+ return { valid: true, expectedTodoId: nextTodo.id };
131
+ }
132
+ return {
133
+ valid: false,
134
+ reason: `Spawn must target plan todo ${nextTodo.id} before other work`,
135
+ gate: 'spawn-todo-persistence',
136
+ expectedTodoId: nextTodo.id,
137
+ hint: {
138
+ tool: 'Task',
139
+ subagent_type: nextTodo.subagent,
140
+ planTodoId: nextTodo.id,
141
+ description: `Lead-dev todo ${nextTodo.id}: ${nextTodo.task}. ` +
142
+ `Include plan todo id in Task prompt.`,
143
+ },
144
+ };
145
+ }
146
+ export function markTodoInProgressOnSpawn(toolInput, projectRoot = process.cwd()) {
147
+ const plan = loadPersistedLeadDevPlan(projectRoot);
148
+ if (!plan)
149
+ return null;
150
+ const validation = validateSpawnMatchesTodo(toolInput, projectRoot);
151
+ if (!validation.valid || !validation.expectedTodoId)
152
+ return null;
153
+ updatePlanTodoStatus(validation.expectedTodoId, 'in_progress', projectRoot);
154
+ return validation.expectedTodoId;
155
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Pending delegations SSOT — written by task-handler on delegate-deferred.
3
+ * Grok hooks read the same file; PostToolUse never re-parses MCP output.
4
+ */
5
+ import type { LeadDevPlan, LeadDevTodo } from './autonomy-kernel.js';
6
+ export declare const DEFAULT_PENDING_TTL_MS: number;
7
+ export type DelegationMatchMethod = 'taskId' | 'description' | 'agent-only' | 'none';
8
+ export interface SpawnHint {
9
+ tool: 'Task';
10
+ subagent_type: string;
11
+ description: string;
12
+ planTodoId: string | null;
13
+ delegationId: string;
14
+ }
15
+ export interface PendingDelegation {
16
+ id: string;
17
+ taskId: string;
18
+ agent: string;
19
+ taskDescription: string;
20
+ taskType: string;
21
+ sessionId: string;
22
+ planTodoId: string | null;
23
+ planTodoTask: string | null;
24
+ matchMethod: DelegationMatchMethod;
25
+ matchConfidence: number;
26
+ status: 'pending' | 'satisfied';
27
+ createdAt: string;
28
+ satisfiedAt: string | null;
29
+ spawnHint: SpawnHint;
30
+ }
31
+ export interface PendingDelegationsState {
32
+ sessionId: string;
33
+ createdAt: string;
34
+ ttlMs: number;
35
+ delegations: PendingDelegation[];
36
+ }
37
+ export interface DeferredTaskRecord {
38
+ taskId: string;
39
+ agent: string;
40
+ description: string;
41
+ type?: string;
42
+ }
43
+ export declare function pendingDelegationsPath(projectRoot?: string): string;
44
+ export declare function leadDevPlanPath(projectRoot?: string): string;
45
+ export declare function isAutoChainDelegationsEnabled(projectRoot?: string): boolean;
46
+ export declare function matchPlanTodo(agent: string, taskDescription: string, taskId: string, plan: LeadDevPlan | null): {
47
+ todo: LeadDevTodo | null;
48
+ method: DelegationMatchMethod;
49
+ confidence: number;
50
+ };
51
+ export declare function mapSubagentToTaskType(agent: string): string;
52
+ export declare function buildSpawnHint(delegation: Pick<PendingDelegation, 'id' | 'agent' | 'taskDescription' | 'planTodoId' | 'planTodoTask'>): SpawnHint;
53
+ export declare function loadPendingDelegationsState(projectRoot?: string): PendingDelegationsState | null;
54
+ export declare function savePendingDelegationsState(state: PendingDelegationsState, projectRoot?: string): string;
55
+ export declare function recordDeferredDelegations(sessionId: string, deferrals: DeferredTaskRecord[], projectRoot?: string): PendingDelegation[];
56
+ export declare function isPendingStateActive(state: PendingDelegationsState | null, sessionId: string | null | undefined): boolean;
57
+ export declare function getActivePendingDelegations(sessionId: string | null | undefined, projectRoot?: string): PendingDelegation[];
58
+ export interface SatisfyDelegationInput {
59
+ delegationId?: string;
60
+ taskId?: string;
61
+ planTodoId?: string;
62
+ agent?: string;
63
+ toolPrompt?: string;
64
+ }
65
+ export declare function satisfyDelegation(input: SatisfyDelegationInput, projectRoot?: string): {
66
+ satisfied: PendingDelegation[];
67
+ clearedAll: boolean;
68
+ };
69
+ export declare function clearPendingDelegations(projectRoot?: string): boolean;
70
+ export declare function clearPendingDelegationsForSessionChange(newSessionId: string | null | undefined, projectRoot?: string): boolean;
71
+ export declare function delegationSummaryForResponse(delegations: PendingDelegation[]): Array<Record<string, unknown>>;