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 +10 -0
- 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 +1 -1
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 +
|
|
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
|
}
|