0xray 3.5.3 → 3.5.4

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/dist/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Conventional Commits](https://www.conventionalcommits.org/).
6
6
 
7
+ ## [3.5.4] - 2026-06-19
8
+
9
+ ### ✨ Features
10
+ - **Synthesis consult receipt gate** — s.1–s.3 todos require `.xray/state/synthesis-consult-{id}.json` before completion; `evaluatePostToolSpawn` records receipts from subagent output; Grok post-tool hook wired
11
+
12
+ ### ✅ Tests
13
+ - `synthesis-consult-receipt.test.ts`; E2E/outcome verifiers write fixture receipts
14
+
15
+ ---
16
+
7
17
  ## [3.5.3] - 2026-06-19
8
18
 
9
19
  ### 🔄 Changes
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Grok PostToolUse — auto-chain observability + targeted clear on Task spawn.
3
+ * Grok PostToolUse — auto-chain observability + consult receipt gate on Task spawn.
4
4
  * Passive hook: stdout ignored by Grok; side effects only.
5
5
  */
6
6
 
@@ -10,12 +10,24 @@ import {
10
10
  isSubagentTool,
11
11
  readStdinJson,
12
12
  resolveSessionId,
13
- satisfyDelegationsFromToolInput,
14
- validateSpawnMatchesTodo,
15
- updatePlanTodoStatusInPlace,
16
13
  workspaceRoot,
17
14
  } from './grok-hook-utils.js';
18
15
  import { appendHookActivity } from './grok-hook-activity.js';
16
+ import {
17
+ evaluatePostToolSpawn,
18
+ isOrchestrateToolEvent as gateIsOrchestrateToolEvent,
19
+ } from '../../hooks/delegation-gate-runtime.mjs';
20
+
21
+ function extractToolOutput(event) {
22
+ return (
23
+ event.toolOutput ??
24
+ event.toolResult ??
25
+ event.result ??
26
+ event.output ??
27
+ event.tool_output ??
28
+ null
29
+ );
30
+ }
19
31
 
20
32
  async function main() {
21
33
  const root = workspaceRoot();
@@ -25,7 +37,7 @@ async function main() {
25
37
  const sessionId = resolveSessionId(event);
26
38
  const { toolName, toolInput } = extractToolContext(event);
27
39
 
28
- if (isOrchestrateToolEvent(toolName, toolInput)) {
40
+ if (isOrchestrateToolEvent(toolName, toolInput) || gateIsOrchestrateToolEvent(toolName, toolInput)) {
29
41
  appendHookActivity(eventRoot, 'grok-post-tool-use', 'auto-chain-pending', 'info', {
30
42
  tool: toolName,
31
43
  sessionId,
@@ -34,18 +46,19 @@ async function main() {
34
46
  }
35
47
 
36
48
  if (isSubagentTool(toolName)) {
37
- const spawnCheck = validateSpawnMatchesTodo(toolInput, eventRoot);
38
- if (spawnCheck.valid && spawnCheck.expectedTodoId) {
39
- updatePlanTodoStatusInPlace(spawnCheck.expectedTodoId, 'completed', eventRoot);
40
- }
49
+ const spawnResult = evaluatePostToolSpawn(toolName, toolInput, eventRoot, {
50
+ toolOutput: extractToolOutput(event),
51
+ sessionId,
52
+ });
41
53
 
42
- const result = satisfyDelegationsFromToolInput(toolInput, eventRoot);
43
- if (result.satisfied.length > 0) {
54
+ if (spawnResult.satisfied.length > 0 || spawnResult.expectedTodoId) {
44
55
  appendHookActivity(eventRoot, 'grok-post-tool-use', 'auto-chain-cleared', 'success', {
45
56
  tool: toolName,
46
- satisfied: result.satisfied.map((d) => d.id),
47
- clearedAll: result.clearedAll,
48
- planTodoId: spawnCheck.expectedTodoId ?? null,
57
+ satisfied: spawnResult.satisfied.map((d) => d.id),
58
+ clearedAll: spawnResult.clearedAll,
59
+ planTodoId: spawnResult.expectedTodoId ?? null,
60
+ receiptRecorded: spawnResult.receiptRecorded ?? false,
61
+ todoCompleted: spawnResult.todoCompleted ?? false,
49
62
  });
50
63
  }
51
64
  }
@@ -72,4 +72,10 @@ export const {
72
72
  validateSpawnMatchesTodo,
73
73
  updatePlanTodoStatus,
74
74
  updatePlanTodoStatusInPlace,
75
+ writeSynthesisConsultReceipt,
76
+ hasValidSynthesisConsultReceipt,
77
+ tryRecordSynthesisConsultReceipt,
78
+ buildReceiptFromConsultOutput,
79
+ parseConsultVerdictFromText,
80
+ isSynthesisConsultTodoId,
75
81
  } = gate;
@@ -7,6 +7,7 @@ import { type PendingDelegation } from './pending-delegations.js';
7
7
  export { getActivePendingDelegations } from './pending-delegations.js';
8
8
  import { updatePlanTodoStatus, type SpawnToolInput } from './lead-dev-plan-persistence.js';
9
9
  export { validateSpawnMatchesTodo, updatePlanTodoStatus, savePersistedLeadDevPlan, } from './lead-dev-plan-persistence.js';
10
+ export { buildReceiptFromConsultOutput, hasValidSynthesisConsultReceipt, isSynthesisConsultTodoId, loadSynthesisConsultReceipt, parseConsultVerdictFromText, tryRecordSynthesisConsultReceipt, writeSynthesisConsultReceipt, type SynthesisConsultReceipt, type SynthesisConsultVerdict, } from './synthesis-consult-receipt.js';
10
11
  export declare const updatePlanTodoStatusInPlace: typeof updatePlanTodoStatus;
11
12
  export type DelegationGateHost = 'grok' | 'hermes' | 'opencode' | 'openclaw' | 'generic';
12
13
  export interface DelegationGateFeatures {
@@ -57,6 +58,12 @@ export interface PostToolSpawnResult {
57
58
  satisfied: PendingDelegation[];
58
59
  clearedAll: boolean;
59
60
  expectedTodoId?: string | null;
61
+ receiptRecorded?: boolean;
62
+ todoCompleted?: boolean;
63
+ }
64
+ export interface PostToolSpawnOptions {
65
+ toolOutput?: unknown;
66
+ sessionId?: string | null;
60
67
  }
61
68
  export declare function loadDelegationGateFeatures(projectRoot?: string): DelegationGateFeatures;
62
69
  export declare function normalizeHostToolInput(toolInput: ToolGateInput): SpawnToolInput & {
@@ -77,7 +84,7 @@ export declare function evaluatePendingDelegationGate(toolName: string, toolInpu
77
84
  * pending gate, then spawn todo gate for subagent tools.
78
85
  */
79
86
  export declare function evaluatePreToolGate(toolName: string, toolInput: ToolGateInput, ctx: PreToolGateContext): PreToolGateResult;
80
- export declare function evaluatePostToolSpawn(toolName: string, toolInput: ToolGateInput, projectRoot: string): PostToolSpawnResult;
87
+ export declare function evaluatePostToolSpawn(toolName: string, toolInput: ToolGateInput, projectRoot: string, options?: PostToolSpawnOptions): PostToolSpawnResult;
81
88
  export declare function isOrchestrateToolEvent(toolName: string, toolInput?: ToolGateInput): boolean;
82
89
  /** Back-compat shims for Grok hook-utils and tests. */
83
90
  export declare function checkPendingDelegationGate(toolName: string, toolInput: ToolGateInput, features: DelegationGateFeatures, projectRoot: string, sessionId: string | null): PreToolGateDeny | null;
@@ -7,9 +7,11 @@ import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { getActivePendingDelegations, satisfyDelegation, } from './pending-delegations.js';
9
9
  export { getActivePendingDelegations } from './pending-delegations.js';
10
- import { allPlanTodos, areSynthesisConsultTodosComplete, hasValidLeadDevPlanForSpawn, isSynthesisRealignmentPlan, loadPersistedLeadDevPlan, getNextRequiredTodo, validateSpawnMatchesTodo, updatePlanTodoStatus, } from './lead-dev-plan-persistence.js';
10
+ import { allPlanTodos, areSynthesisConsultTodosComplete, getSynthesisConsultTodos, hasValidLeadDevPlanForSpawn, isSynthesisRealignmentPlan, loadPersistedLeadDevPlan, getNextRequiredTodo, validateSpawnMatchesTodo, updatePlanTodoStatus, } from './lead-dev-plan-persistence.js';
11
11
  import { getSynthesisDueReason, isSynthesisCheckpointDue, recordExecutionSlice, } from './synthesis.js';
12
+ import { isSynthesisConsultTodoId, tryRecordSynthesisConsultReceipt, } from './synthesis-consult-receipt.js';
12
13
  export { validateSpawnMatchesTodo, updatePlanTodoStatus, savePersistedLeadDevPlan, } from './lead-dev-plan-persistence.js';
14
+ export { buildReceiptFromConsultOutput, hasValidSynthesisConsultReceipt, isSynthesisConsultTodoId, loadSynthesisConsultReceipt, parseConsultVerdictFromText, tryRecordSynthesisConsultReceipt, writeSynthesisConsultReceipt, } from './synthesis-consult-receipt.js';
13
15
  export const updatePlanTodoStatusInPlace = updatePlanTodoStatus;
14
16
  const READ_TOOLS = new Set([
15
17
  'read_file',
@@ -285,7 +287,7 @@ export function evaluatePreToolGate(toolName, toolInput, ctx) {
285
287
  return spawnBlock;
286
288
  return { allow: true };
287
289
  }
288
- export function evaluatePostToolSpawn(toolName, toolInput, projectRoot) {
290
+ export function evaluatePostToolSpawn(toolName, toolInput, projectRoot, options = {}) {
289
291
  const normalized = normalizeHostToolInput(toolInput);
290
292
  const spawnCheck = validateSpawnMatchesTodo(normalized, projectRoot);
291
293
  const satisfyInput = {
@@ -299,12 +301,31 @@ export function evaluatePostToolSpawn(toolName, toolInput, projectRoot) {
299
301
  if (normalized.planTodoId)
300
302
  satisfyInput.planTodoId = normalized.planTodoId;
301
303
  const result = satisfyDelegation(satisfyInput, projectRoot);
302
- if (spawnCheck.valid && spawnCheck.expectedTodoId) {
303
- updatePlanTodoStatus(spawnCheck.expectedTodoId, 'completed', projectRoot);
304
+ let receiptRecorded = false;
305
+ let todoCompleted = false;
306
+ const expectedTodoId = spawnCheck.expectedTodoId ?? null;
307
+ if (spawnCheck.valid && expectedTodoId) {
308
+ const plan = loadPersistedLeadDevPlan(projectRoot);
309
+ const sessionId = options.sessionId ?? plan?.sessionId ?? null;
310
+ const consultTodo = plan
311
+ ? getSynthesisConsultTodos(plan).find((t) => t.id === expectedTodoId)
312
+ : undefined;
313
+ if (isSynthesisConsultTodoId(expectedTodoId) &&
314
+ consultTodo &&
315
+ sessionId &&
316
+ options.toolOutput != null) {
317
+ const receipt = tryRecordSynthesisConsultReceipt(expectedTodoId, consultTodo.subagent, sessionId, options.toolOutput, projectRoot);
318
+ receiptRecorded = receipt != null;
319
+ }
320
+ if (updatePlanTodoStatus(expectedTodoId, 'completed', projectRoot)) {
321
+ todoCompleted = true;
322
+ }
304
323
  }
305
324
  return {
306
325
  ...result,
307
- expectedTodoId: spawnCheck.expectedTodoId ?? null,
326
+ expectedTodoId,
327
+ receiptRecorded,
328
+ todoCompleted,
308
329
  };
309
330
  }
310
331
  export function isOrchestrateToolEvent(toolName, toolInput = {}) {
@@ -7,6 +7,7 @@ import * as path from 'path';
7
7
  import { SYNTHESIS_REALIGNMENT_PHASE_ID } from './autonomy-kernel.js';
8
8
  import { DEFAULT_PENDING_TTL_MS } from './pending-delegations.js';
9
9
  import { completeSynthesisCheckpoint, getSynthesisCheckpointSessionId, isSynthesisCheckpointDue, recordExecutionSlice, } from './synthesis.js';
10
+ import { hasValidSynthesisConsultReceipt, isSynthesisConsultTodoId, } from './synthesis-consult-receipt.js';
10
11
  export function leadDevPlanStatePath(projectRoot = process.cwd()) {
11
12
  return path.join(projectRoot, '.xray', 'state', 'lead-dev-plan.json');
12
13
  }
@@ -101,6 +102,27 @@ export function updatePlanTodoStatus(todoId, status, projectRoot = process.cwd()
101
102
  const plan = loadPersistedLeadDevPlan(projectRoot);
102
103
  if (!plan)
103
104
  return false;
105
+ let targetTodo;
106
+ for (const phase of plan.phases) {
107
+ for (const todo of phase.todos) {
108
+ if (todo.id === todoId) {
109
+ targetTodo = todo;
110
+ break;
111
+ }
112
+ }
113
+ if (targetTodo)
114
+ break;
115
+ }
116
+ if (status === 'completed' && isSynthesisConsultTodoId(todoId)) {
117
+ const receiptExpected = {};
118
+ if (plan.sessionId !== undefined)
119
+ receiptExpected.sessionId = plan.sessionId;
120
+ if (targetTodo?.subagent)
121
+ receiptExpected.subagent = targetTodo.subagent;
122
+ if (!hasValidSynthesisConsultReceipt(todoId, projectRoot, receiptExpected)) {
123
+ return false;
124
+ }
125
+ }
104
126
  let updated = false;
105
127
  for (const phase of plan.phases) {
106
128
  for (const todo of phase.todos) {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Synthesis consult receipt SSOT — outcome-based completion for s.N todos.
3
+ * Receipts live at `.xray/state/synthesis-consult-{todoId}.json`.
4
+ */
5
+ export type SynthesisConsultVerdict = 'PASS' | 'CONDITIONAL' | 'FAIL';
6
+ export interface SynthesisConsultReceipt {
7
+ sessionId: string;
8
+ subagent: string;
9
+ verdict: SynthesisConsultVerdict;
10
+ topRisks: string[];
11
+ hardeningNote: string;
12
+ todoId?: string;
13
+ recordedAt?: string;
14
+ }
15
+ export declare function isSynthesisConsultTodoId(todoId: string): boolean;
16
+ export declare function synthesisConsultReceiptPath(todoId: string, projectRoot?: string): string;
17
+ export declare function loadSynthesisConsultReceipt(todoId: string, projectRoot?: string): SynthesisConsultReceipt | null;
18
+ export declare function writeSynthesisConsultReceipt(todoId: string, receipt: SynthesisConsultReceipt, projectRoot?: string): string;
19
+ export declare function validateSynthesisConsultReceipt(receipt: SynthesisConsultReceipt, todoId: string, expected?: {
20
+ sessionId?: string | null;
21
+ subagent?: string;
22
+ }): boolean;
23
+ export declare function hasValidSynthesisConsultReceipt(todoId: string, projectRoot?: string, expected?: {
24
+ sessionId?: string | null;
25
+ subagent?: string;
26
+ }): boolean;
27
+ export declare function parseConsultVerdictFromText(text: string): SynthesisConsultVerdict | null;
28
+ export declare function extractTopRisksFromText(text: string, max?: number): string[];
29
+ export declare function extractHardeningNoteFromText(text: string, maxChars?: number): string;
30
+ export declare function coerceToolOutputText(output: unknown): string;
31
+ export declare function buildReceiptFromConsultOutput(todoId: string, subagent: string, sessionId: string, outputText: string): SynthesisConsultReceipt | null;
32
+ export declare function tryRecordSynthesisConsultReceipt(todoId: string, subagent: string, sessionId: string, output: unknown, projectRoot?: string): SynthesisConsultReceipt | null;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Synthesis consult receipt SSOT — outcome-based completion for s.N todos.
3
+ * Receipts live at `.xray/state/synthesis-consult-{todoId}.json`.
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ const CONSULT_TODO_PATTERN = /^s\.\d+$/;
8
+ export function isSynthesisConsultTodoId(todoId) {
9
+ return CONSULT_TODO_PATTERN.test(todoId);
10
+ }
11
+ export function synthesisConsultReceiptPath(todoId, projectRoot = process.cwd()) {
12
+ return path.join(projectRoot, '.xray', 'state', `synthesis-consult-${todoId}.json`);
13
+ }
14
+ export function loadSynthesisConsultReceipt(todoId, projectRoot = process.cwd()) {
15
+ const receiptPath = synthesisConsultReceiptPath(todoId, projectRoot);
16
+ if (!fs.existsSync(receiptPath))
17
+ return null;
18
+ try {
19
+ return JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function writeSynthesisConsultReceipt(todoId, receipt, projectRoot = process.cwd()) {
26
+ const receiptPath = synthesisConsultReceiptPath(todoId, projectRoot);
27
+ fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
28
+ const payload = {
29
+ ...receipt,
30
+ todoId,
31
+ recordedAt: receipt.recordedAt ?? new Date().toISOString(),
32
+ };
33
+ fs.writeFileSync(receiptPath, JSON.stringify(payload, null, 2));
34
+ return receiptPath;
35
+ }
36
+ function normalizeSubagent(agent) {
37
+ const key = agent.toLowerCase().trim();
38
+ const aliases = {
39
+ 'bug-triage-specialist': 'bug-triage',
40
+ 'code-reviewer': 'code-review',
41
+ };
42
+ return aliases[key] ?? key;
43
+ }
44
+ function subagentsAlign(expected, actual) {
45
+ const e = normalizeSubagent(expected);
46
+ const a = normalizeSubagent(actual);
47
+ return e === a || e.includes(a) || a.includes(e);
48
+ }
49
+ function isValidVerdict(value) {
50
+ return value === 'PASS' || value === 'CONDITIONAL' || value === 'FAIL';
51
+ }
52
+ export function validateSynthesisConsultReceipt(receipt, todoId, expected) {
53
+ if (!receipt.sessionId || !receipt.subagent || !isValidVerdict(receipt.verdict)) {
54
+ return false;
55
+ }
56
+ if (!Array.isArray(receipt.topRisks))
57
+ return false;
58
+ if (typeof receipt.hardeningNote !== 'string')
59
+ return false;
60
+ if (expected?.sessionId && receipt.sessionId !== expected.sessionId)
61
+ return false;
62
+ if (expected?.subagent && !subagentsAlign(expected.subagent, receipt.subagent)) {
63
+ return false;
64
+ }
65
+ if (receipt.todoId && receipt.todoId !== todoId)
66
+ return false;
67
+ return true;
68
+ }
69
+ export function hasValidSynthesisConsultReceipt(todoId, projectRoot = process.cwd(), expected) {
70
+ const receipt = loadSynthesisConsultReceipt(todoId, projectRoot);
71
+ if (!receipt)
72
+ return false;
73
+ return validateSynthesisConsultReceipt(receipt, todoId, expected);
74
+ }
75
+ export function parseConsultVerdictFromText(text) {
76
+ const normalized = text.toUpperCase();
77
+ if (/\bCONDITIONAL(\s+PASS)?\b/.test(normalized))
78
+ return 'CONDITIONAL';
79
+ if (/\b(?:PASS|SHIP)\b/.test(normalized))
80
+ return 'PASS';
81
+ if (/\bFAIL\b/.test(normalized))
82
+ return 'FAIL';
83
+ return null;
84
+ }
85
+ export function extractTopRisksFromText(text, max = 5) {
86
+ const risks = [];
87
+ const lines = text.split('\n');
88
+ for (const line of lines) {
89
+ const trimmed = line.trim();
90
+ if (!trimmed)
91
+ continue;
92
+ if (/^(?:[-*•]|\d+\.)\s*(?:risk|top\s*risk)/i.test(trimmed)) {
93
+ risks.push(trimmed.replace(/^[-*•\d.]+\s*/i, '').slice(0, 240));
94
+ }
95
+ else if (/^risk\s*:/i.test(trimmed)) {
96
+ risks.push(trimmed.replace(/^risk\s*:\s*/i, '').slice(0, 240));
97
+ }
98
+ if (risks.length >= max)
99
+ break;
100
+ }
101
+ return risks;
102
+ }
103
+ export function extractHardeningNoteFromText(text, maxChars = 500) {
104
+ const match = text.match(/(?:hardening|recommend(?:ation)?s?)\s*[:\n]([\s\S]{0,800})/i);
105
+ if (match?.[1])
106
+ return match[1].trim().slice(0, maxChars);
107
+ const lines = text.split('\n').filter((l) => l.trim().length > 0);
108
+ return lines.slice(-3).join(' ').slice(0, maxChars);
109
+ }
110
+ export function coerceToolOutputText(output) {
111
+ if (typeof output === 'string')
112
+ return output;
113
+ if (output == null)
114
+ return '';
115
+ if (typeof output === 'object') {
116
+ const obj = output;
117
+ if (typeof obj.text === 'string')
118
+ return obj.text;
119
+ if (typeof obj.content === 'string')
120
+ return obj.content;
121
+ if (typeof obj.message === 'string')
122
+ return obj.message;
123
+ try {
124
+ return JSON.stringify(output);
125
+ }
126
+ catch {
127
+ return String(output);
128
+ }
129
+ }
130
+ return String(output);
131
+ }
132
+ export function buildReceiptFromConsultOutput(todoId, subagent, sessionId, outputText) {
133
+ const text = outputText.trim();
134
+ if (!text)
135
+ return null;
136
+ const verdict = parseConsultVerdictFromText(text);
137
+ if (!verdict)
138
+ return null;
139
+ return {
140
+ sessionId,
141
+ subagent,
142
+ verdict,
143
+ topRisks: extractTopRisksFromText(text),
144
+ hardeningNote: extractHardeningNoteFromText(text),
145
+ todoId,
146
+ };
147
+ }
148
+ export function tryRecordSynthesisConsultReceipt(todoId, subagent, sessionId, output, projectRoot = process.cwd()) {
149
+ const receipt = buildReceiptFromConsultOutput(todoId, subagent, sessionId, coerceToolOutputText(output));
150
+ if (!receipt || !validateSynthesisConsultReceipt(receipt, todoId, { sessionId, subagent })) {
151
+ return null;
152
+ }
153
+ writeSynthesisConsultReceipt(todoId, receipt, projectRoot);
154
+ return receipt;
155
+ }
@@ -182,7 +182,7 @@ export default async function xrayCodexPlugin(input) {
182
182
  const { tool, args, result } = input;
183
183
  if (isSubagentTool(tool)) {
184
184
  try {
185
- const spawnResult = evaluatePostToolSpawn(tool, (args ?? {}), directory);
185
+ const spawnResult = evaluatePostToolSpawn(tool, (args ?? {}), directory, { toolOutput: result });
186
186
  if (spawnResult.satisfied.length > 0) {
187
187
  logger.log(`🔗 Delegation gate post: satisfied ${spawnResult.satisfied.map((d) => d.id).join(", ")}`);
188
188
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "3.5.3",
3
+ "version": "3.5.4",
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",