0xray 3.5.2 → 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/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 +19 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/integrations/grok/hooks/post-tool-use.js +27 -14
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +6 -0
- package/dist/nucleus/delegation-gate.d.ts +8 -1
- package/dist/nucleus/delegation-gate.js +26 -5
- package/dist/nucleus/lead-dev-plan-persistence.js +22 -0
- package/dist/nucleus/synthesis-consult-receipt.d.ts +32 -0
- package/dist/nucleus/synthesis-consult-receipt.js +155 -0
- package/dist/plugin/xray-codex-injection.js +1 -1
- package/package.json +2 -1
- package/scripts/mjs/verify-grok-synthesis-gate.mjs +121 -0
package/AGENTS-consumer.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Quick reference for the 0xRay AI orchestration framework (shipped to your project on `npm install 0xray`).
|
|
6
6
|
|
|
7
|
-
**v3.5.
|
|
7
|
+
**v3.5.3** — 7 MCP servers · 45 skills · 68 codex terms · 4 platform bridges
|
|
8
8
|
|
|
9
9
|
## Postinstall (automatic)
|
|
10
10
|
|
package/AGENTS.md
CHANGED
package/README.md
CHANGED
package/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.5.
|
|
3
|
+
**v3.5.3** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
package/dist/AGENTS.md
CHANGED
package/dist/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ 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
|
+
|
|
17
|
+
## [3.5.3] - 2026-06-19
|
|
18
|
+
|
|
19
|
+
### 🔄 Changes
|
|
20
|
+
|
|
21
|
+
### 🔎 Other Changes
|
|
22
|
+
- fix(release): ship verify-grok-synthesis-gate.mjs in npm tarball (c2d32627e)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
7
26
|
## [3.5.2] - 2026-06-19
|
|
8
27
|
|
|
9
28
|
### 🐛 Bug Fixes
|
package/dist/README.md
CHANGED
package/dist/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.5.
|
|
3
|
+
**v3.5.3** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Grok PostToolUse — auto-chain observability +
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
49
|
+
const spawnResult = evaluatePostToolSpawn(toolName, toolInput, eventRoot, {
|
|
50
|
+
toolOutput: extractToolOutput(event),
|
|
51
|
+
sessionId,
|
|
52
|
+
});
|
|
41
53
|
|
|
42
|
-
|
|
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:
|
|
47
|
-
clearedAll:
|
|
48
|
-
planTodoId:
|
|
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
|
-
|
|
303
|
-
|
|
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
|
|
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
|
+
"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",
|
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
"scripts/mjs/validate-postinstall-config.mjs",
|
|
148
148
|
"scripts/mjs/verify-orchestrator-behavior.mjs",
|
|
149
149
|
"scripts/mjs/verify-grok-delegation-gate.mjs",
|
|
150
|
+
"scripts/mjs/verify-grok-synthesis-gate.mjs",
|
|
150
151
|
"scripts/mjs/verify-hermes-delegation-gate.mjs",
|
|
151
152
|
"scripts/mjs/verify-delegation-gate-core.mjs",
|
|
152
153
|
"scripts/mjs/delegation-gate-fixture.mjs",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hook fixture verify — PreToolUse synthesis gate via live Grok hook path.
|
|
4
|
+
* Run: node scripts/mjs/verify-grok-synthesis-gate.mjs
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import {
|
|
12
|
+
SYNTHESIS_FIXTURE_SESSION_ID,
|
|
13
|
+
seedSynthesisDueFixture,
|
|
14
|
+
} from './delegation-gate-fixture.mjs';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const packageRoot = join(__dirname, '../..');
|
|
18
|
+
const preToolHook = join(packageRoot, 'dist/integrations/grok/hooks/pre-tool-use.js');
|
|
19
|
+
|
|
20
|
+
let failed = 0;
|
|
21
|
+
function pass(n) {
|
|
22
|
+
console.log(`✅ ${n}`);
|
|
23
|
+
}
|
|
24
|
+
function fail(n, d = '') {
|
|
25
|
+
failed++;
|
|
26
|
+
console.error(`❌ ${n}${d ? ` — ${d}` : ''}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runHook(fixture, env = {}) {
|
|
30
|
+
const payload = JSON.stringify(fixture);
|
|
31
|
+
const out = execSync(`printf '%s' '${payload.replace(/'/g, "'\\''")}' | node "${preToolHook}"`, {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
env: { ...process.env, ...env },
|
|
34
|
+
});
|
|
35
|
+
return JSON.parse(out.trim());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const tmp = mkdtempSync(join(tmpdir(), 'xray-synth-grok-'));
|
|
39
|
+
seedSynthesisDueFixture(tmp);
|
|
40
|
+
|
|
41
|
+
const baseEnv = {
|
|
42
|
+
GROK_WORKSPACE_ROOT: tmp,
|
|
43
|
+
GROK_SESSION_ID: SYNTHESIS_FIXTURE_SESSION_ID,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const denyWrite = runHook(
|
|
48
|
+
{
|
|
49
|
+
toolName: 'search_replace',
|
|
50
|
+
workspaceRoot: tmp,
|
|
51
|
+
sessionId: SYNTHESIS_FIXTURE_SESSION_ID,
|
|
52
|
+
toolInput: { path: 'src/a.ts', new_string: 'x' },
|
|
53
|
+
},
|
|
54
|
+
baseEnv,
|
|
55
|
+
);
|
|
56
|
+
if (denyWrite.decision === 'deny' && denyWrite.gate === 'synthesis-checkpoint') {
|
|
57
|
+
pass('PreToolUse denies write when synthesis due');
|
|
58
|
+
} else {
|
|
59
|
+
fail('synthesis write deny', JSON.stringify(denyWrite));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const allowRead = runHook(
|
|
63
|
+
{
|
|
64
|
+
toolName: 'read_file',
|
|
65
|
+
workspaceRoot: tmp,
|
|
66
|
+
sessionId: SYNTHESIS_FIXTURE_SESSION_ID,
|
|
67
|
+
toolInput: { path: 'src/a.ts' },
|
|
68
|
+
},
|
|
69
|
+
baseEnv,
|
|
70
|
+
);
|
|
71
|
+
if (allowRead.decision === 'allow') pass('PreToolUse allows read when synthesis due');
|
|
72
|
+
else fail('synthesis read allow', JSON.stringify(allowRead));
|
|
73
|
+
|
|
74
|
+
const allowConsult = runHook(
|
|
75
|
+
{
|
|
76
|
+
toolName: 'CallMcpTool',
|
|
77
|
+
workspaceRoot: tmp,
|
|
78
|
+
sessionId: SYNTHESIS_FIXTURE_SESSION_ID,
|
|
79
|
+
toolInput: {
|
|
80
|
+
server: 'xray-orchestrator',
|
|
81
|
+
toolName: 'analyze-complexity',
|
|
82
|
+
arguments: { tasks: [{ description: 'synthesis reflect', type: 'plan' }] },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
baseEnv,
|
|
86
|
+
);
|
|
87
|
+
if (allowConsult.decision === 'allow') {
|
|
88
|
+
pass('PreToolUse allows analyze-complexity consult when synthesis due');
|
|
89
|
+
} else {
|
|
90
|
+
fail('synthesis consult allow', JSON.stringify(allowConsult));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const denyGovern = runHook(
|
|
94
|
+
{
|
|
95
|
+
toolName: 'CallMcpTool',
|
|
96
|
+
workspaceRoot: tmp,
|
|
97
|
+
sessionId: SYNTHESIS_FIXTURE_SESSION_ID,
|
|
98
|
+
toolInput: {
|
|
99
|
+
server: 'xray-orchestrator',
|
|
100
|
+
toolName: 'govern-and-apply',
|
|
101
|
+
arguments: { proposal: 'test' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
baseEnv,
|
|
105
|
+
);
|
|
106
|
+
if (denyGovern.decision === 'deny' && denyGovern.gate === 'synthesis-checkpoint') {
|
|
107
|
+
pass('PreToolUse denies govern-and-apply when synthesis due');
|
|
108
|
+
} else {
|
|
109
|
+
fail('synthesis govern deny', JSON.stringify(denyGovern));
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(
|
|
116
|
+
'\n' +
|
|
117
|
+
(failed === 0
|
|
118
|
+
? '🎉 Grok synthesis gate verify passed (4/4).'
|
|
119
|
+
: `⚠️ ${failed} synthesis gate check(s) failed.`),
|
|
120
|
+
);
|
|
121
|
+
process.exit(failed === 0 ? 0 : 1);
|