0xray 3.5.3 → 3.5.5
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/.xray/features.json +5 -1
- package/.xray/features.schema.json +56 -0
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +11 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +11 -1
- package/dist/CHANGELOG.md +54 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/cli/commands/hermes-install.js +3 -1
- package/dist/cli/index.js +0 -0
- package/dist/core/features-config.d.ts +15 -0
- package/dist/governance/governance-service.d.ts +1 -0
- package/dist/governance/governance-service.js +81 -6
- package/dist/integrations/enforcement-gate.js +15 -5
- package/dist/integrations/grok/grok-cli.js +1 -10
- package/dist/integrations/grok/hooks/grok-hook-utils.js +46 -2
- package/dist/integrations/grok/hooks/post-tool-use.js +48 -14
- package/dist/integrations/grok/hooks/pre-tool-use.js +20 -0
- package/dist/integrations/grok/hooks/session-start.js +28 -0
- package/dist/integrations/hermes-agent/__init__.py +11 -1
- package/dist/integrations/hermes-agent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/__pycache__/schemas.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/__pycache__/tools.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/bridge.mjs +73 -10
- package/dist/integrations/hooks/confer-hook-runtime.mjs +49 -0
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +6 -0
- package/dist/integrations/hooks/pipeline-hook-runtime.mjs +211 -0
- package/dist/integrations/hooks/plan-hook-runtime.mjs +68 -0
- package/dist/integrations/hooks/user-aside-hook-runtime.mjs +49 -0
- package/dist/mcps/architect-tools.server.js +6 -1
- package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +7 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +106 -7
- package/dist/mcps/orchestrator/handlers/task-handler.d.ts +6 -0
- package/dist/mcps/orchestrator/handlers/task-handler.js +75 -0
- package/dist/mcps/orchestrator/server.js +54 -5
- package/dist/memory-routing/provider-loader.js +30 -2
- package/dist/nucleus/aside-worktree.d.ts +41 -0
- package/dist/nucleus/aside-worktree.js +164 -0
- package/dist/nucleus/confer.d.ts +57 -0
- package/dist/nucleus/confer.js +308 -0
- package/dist/nucleus/delegation-gate.d.ts +12 -1
- package/dist/nucleus/delegation-gate.js +113 -13
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
- package/dist/nucleus/lead-dev-plan-persistence.js +222 -18
- package/dist/nucleus/spawn-plan-resolution.d.ts +20 -0
- package/dist/nucleus/spawn-plan-resolution.js +42 -0
- package/dist/nucleus/synthesis-consult-receipt.d.ts +32 -0
- package/dist/nucleus/synthesis-consult-receipt.js +171 -0
- package/dist/nucleus/synthesis-context.js +1 -1
- package/dist/nucleus/user-aside.d.ts +70 -0
- package/dist/nucleus/user-aside.js +393 -0
- package/dist/plugin/xray-codex-injection.js +1 -1
- package/dist/postprocessor/PostProcessor.d.ts +1 -0
- package/dist/postprocessor/PostProcessor.js +11 -0
- package/dist/processors/implementations/session-summary-processor.d.ts +1 -1
- package/dist/processors/implementations/session-summary-processor.js +9 -6
- package/package.json +14 -5
- package/scripts/hooks/install-hooks.cjs +3 -3
- package/scripts/hooks/run-hook.js +171 -86
- package/scripts/mjs/verify-confer-core.mjs +103 -0
- package/scripts/mjs/verify-delegation-gate-core.mjs +48 -2
- package/scripts/mjs/verify-hermes-session-start.mjs +137 -0
- package/scripts/mjs/verify-pre-push-diff-core.mjs +87 -0
- package/scripts/mjs/verify-user-aside-core.mjs +137 -0
- package/scripts/node/bridge-mcp-wiring.cjs +10 -0
- package/scripts/node/install-bridges.cjs +3 -11
- package/scripts/node/load-reflection-config.mjs +2 -2
- package/scripts/node/release-gate.mjs +8 -4
- package/src/mcps/architect-tools.server.ts +6 -1
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +178 -20
- package/src/mcps/orchestrator/handlers/task-handler.ts +99 -0
- package/src/mcps/orchestrator/server.ts +56 -5
- package/xray/features.json +5 -1
- package/xray/features.schema.json +56 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confer — mandatory 3-agent quorum SSOT (researcher / architect-tools / code-review).
|
|
3
|
+
* Triggered automatically at synthesis checkpoint; records consult receipts + plan todos.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { MANDATORY_MAJOR_CONSULTS } from './autonomy-kernel.js';
|
|
8
|
+
import { areSynthesisConsultTodosComplete, getNextRequiredTodo, getSynthesisConsultTodos, isSynthesisRealignmentPlan, loadPersistedLeadDevPlan, updatePlanTodoStatus, } from './lead-dev-plan-persistence.js';
|
|
9
|
+
import { buildReceiptFromConsultOutput, tryRecordSynthesisConsultReceipt, parseConsultVerdictFromText, } from './synthesis-consult-receipt.js';
|
|
10
|
+
import { isSynthesisCheckpointDue } from './synthesis.js';
|
|
11
|
+
export const CONFER_AGENTS = [...MANDATORY_MAJOR_CONSULTS];
|
|
12
|
+
const STATE_VERSION = 1;
|
|
13
|
+
export function conferCheckpointPath(projectRoot = process.cwd()) {
|
|
14
|
+
return path.join(projectRoot, '.xray', 'state', 'confer-checkpoint.json');
|
|
15
|
+
}
|
|
16
|
+
export function defaultConferConfig() {
|
|
17
|
+
return { enabled: true, on_synthesis: true };
|
|
18
|
+
}
|
|
19
|
+
export function loadConferConfig(projectRoot = process.cwd()) {
|
|
20
|
+
const featuresPath = path.join(projectRoot, '.xray', 'features.json');
|
|
21
|
+
if (!fs.existsSync(featuresPath))
|
|
22
|
+
return defaultConferConfig();
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(fs.readFileSync(featuresPath, 'utf8'));
|
|
25
|
+
const orch = data.multi_agent_orchestration ?? {};
|
|
26
|
+
const raw = orch.confer ?? {};
|
|
27
|
+
return {
|
|
28
|
+
enabled: raw.enabled !== false && orch.confer_on_synthesis !== false,
|
|
29
|
+
on_synthesis: raw.on_synthesis !== false && orch.confer_on_synthesis !== false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return defaultConferConfig();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function isConferEnabled(projectRoot = process.cwd()) {
|
|
37
|
+
return loadConferConfig(projectRoot).enabled;
|
|
38
|
+
}
|
|
39
|
+
export function loadConferCheckpoint(projectRoot = process.cwd()) {
|
|
40
|
+
const statePath = conferCheckpointPath(projectRoot);
|
|
41
|
+
if (!fs.existsSync(statePath))
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function saveConferCheckpoint(state, projectRoot = process.cwd()) {
|
|
51
|
+
const statePath = conferCheckpointPath(projectRoot);
|
|
52
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
53
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
54
|
+
return statePath;
|
|
55
|
+
}
|
|
56
|
+
export function triggerConferCheckpoint(sessionId, dueReason, projectRoot = process.cwd()) {
|
|
57
|
+
const state = {
|
|
58
|
+
version: STATE_VERSION,
|
|
59
|
+
sessionId,
|
|
60
|
+
triggeredAt: new Date().toISOString(),
|
|
61
|
+
dueReason,
|
|
62
|
+
status: 'pending',
|
|
63
|
+
completedAgents: [],
|
|
64
|
+
};
|
|
65
|
+
saveConferCheckpoint(state, projectRoot);
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
export function isConferPending(projectRoot = process.cwd(), sessionId) {
|
|
69
|
+
const plan = loadPersistedLeadDevPlan(projectRoot);
|
|
70
|
+
if (!plan || !isSynthesisRealignmentPlan(plan))
|
|
71
|
+
return false;
|
|
72
|
+
if (areSynthesisConsultTodosComplete(plan))
|
|
73
|
+
return false;
|
|
74
|
+
if (sessionId && isSynthesisCheckpointDue(projectRoot, sessionId))
|
|
75
|
+
return true;
|
|
76
|
+
const state = loadConferCheckpoint(projectRoot);
|
|
77
|
+
if (!state || state.status === 'completed')
|
|
78
|
+
return false;
|
|
79
|
+
if (sessionId && state.sessionId !== sessionId)
|
|
80
|
+
return false;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
export function buildConferPrompt(subagent, todoTask, collocatedText, dueReason) {
|
|
84
|
+
const reasonLine = dueReason ? `Due: ${dueReason}\n\n` : '';
|
|
85
|
+
return `${reasonLine}## Confer quorum — ${subagent}
|
|
86
|
+
|
|
87
|
+
${todoTask}
|
|
88
|
+
|
|
89
|
+
Review the collocated synthesis context and return a structured verdict.
|
|
90
|
+
|
|
91
|
+
**Required in your response:**
|
|
92
|
+
- Verdict: PASS | CONDITIONAL | FAIL
|
|
93
|
+
- Top risks (bulleted)
|
|
94
|
+
- Hardening note / recommendations
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
${collocatedText}`;
|
|
99
|
+
}
|
|
100
|
+
/** Map confer subagent → MCP server + tool. */
|
|
101
|
+
export function conferAgentMcpTarget(subagent) {
|
|
102
|
+
switch (subagent) {
|
|
103
|
+
case 'architect-tools':
|
|
104
|
+
return {
|
|
105
|
+
server: 'architect-tools',
|
|
106
|
+
tool: 'architecture-assessment',
|
|
107
|
+
args: (prompt, projectRoot) => ({
|
|
108
|
+
projectRoot,
|
|
109
|
+
assessmentType: 'comprehensive',
|
|
110
|
+
focusMetrics: ['complexity', 'coupling', 'cohesion', 'testability', 'scalability'],
|
|
111
|
+
conferPrompt: prompt,
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
case 'researcher':
|
|
115
|
+
return {
|
|
116
|
+
server: 'researcher',
|
|
117
|
+
tool: 'analyze_proposal',
|
|
118
|
+
args: (prompt) => ({
|
|
119
|
+
proposalTitle: 'Synthesis confer — researcher',
|
|
120
|
+
proposalDescription: prompt,
|
|
121
|
+
proposalType: 'synthesis-confer',
|
|
122
|
+
evidence: [],
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
case 'code-review':
|
|
126
|
+
default:
|
|
127
|
+
return {
|
|
128
|
+
server: 'code-review',
|
|
129
|
+
tool: 'analyze_proposal',
|
|
130
|
+
args: (prompt) => ({
|
|
131
|
+
proposalTitle: 'Synthesis confer — code-review',
|
|
132
|
+
proposalDescription: prompt,
|
|
133
|
+
proposalType: 'synthesis-confer',
|
|
134
|
+
evidence: [],
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function extractMcpText(result) {
|
|
140
|
+
const content = result?.content;
|
|
141
|
+
if (Array.isArray(content)) {
|
|
142
|
+
return content.map((c) => c.text ?? '').join('\n');
|
|
143
|
+
}
|
|
144
|
+
if (typeof result === 'string')
|
|
145
|
+
return result;
|
|
146
|
+
try {
|
|
147
|
+
return JSON.stringify(result);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return String(result);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function invokeConferAgent(subagent, prompt, projectRoot = process.cwd()) {
|
|
154
|
+
const { mcpClientManager } = await import('../mcps/mcp-client.js');
|
|
155
|
+
const target = conferAgentMcpTarget(subagent);
|
|
156
|
+
const args = target.args(prompt, projectRoot);
|
|
157
|
+
const result = await mcpClientManager.callServerTool(target.server, target.tool, args);
|
|
158
|
+
const text = extractMcpText(result);
|
|
159
|
+
if (!text.trim()) {
|
|
160
|
+
throw new Error(`Empty confer response from ${subagent}`);
|
|
161
|
+
}
|
|
162
|
+
if (!parseConsultVerdictFromText(text)) {
|
|
163
|
+
throw new Error(`Confer response from ${subagent} missing parseable verdict (expected Verdict: PASS|CONDITIONAL|FAIL or DECISION: approve|reject|abstain)`);
|
|
164
|
+
}
|
|
165
|
+
return text;
|
|
166
|
+
}
|
|
167
|
+
export function applyConferConsultResult(todoId, subagent, sessionId, outputText, projectRoot = process.cwd()) {
|
|
168
|
+
const receipt = tryRecordSynthesisConsultReceipt(todoId, subagent, sessionId, outputText, projectRoot);
|
|
169
|
+
let todoCompleted = false;
|
|
170
|
+
if (receipt && receipt.verdict !== 'FAIL') {
|
|
171
|
+
todoCompleted = updatePlanTodoStatus(todoId, 'completed', projectRoot);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
todoId,
|
|
175
|
+
subagent,
|
|
176
|
+
outputText,
|
|
177
|
+
receiptRecorded: receipt != null,
|
|
178
|
+
todoCompleted,
|
|
179
|
+
verdict: receipt?.verdict ?? buildReceiptFromConsultOutput(todoId, subagent, sessionId, outputText)?.verdict ?? null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function writeFixtureConferReceipt(todoId, subagent, sessionId, projectRoot = process.cwd()) {
|
|
183
|
+
const output = `Verdict: PASS\nTop risks: none\nHardening: confer fixture quorum for ${subagent}`;
|
|
184
|
+
return applyConferConsultResult(todoId, subagent, sessionId, output, projectRoot);
|
|
185
|
+
}
|
|
186
|
+
export async function runConferQuorum(projectRoot = process.cwd(), sessionId, options = {}) {
|
|
187
|
+
const cfg = loadConferConfig(projectRoot);
|
|
188
|
+
if (!cfg.enabled || !cfg.on_synthesis) {
|
|
189
|
+
return {
|
|
190
|
+
status: 'skipped',
|
|
191
|
+
agents: [],
|
|
192
|
+
message: 'Confer disabled in features.json',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const plan = loadPersistedLeadDevPlan(projectRoot);
|
|
196
|
+
if (!plan || !isSynthesisRealignmentPlan(plan)) {
|
|
197
|
+
return {
|
|
198
|
+
status: 'skipped',
|
|
199
|
+
agents: [],
|
|
200
|
+
message: 'No synthesis realignment plan — confer not applicable',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (areSynthesisConsultTodosComplete(plan)) {
|
|
204
|
+
return {
|
|
205
|
+
status: 'completed',
|
|
206
|
+
agents: [],
|
|
207
|
+
message: 'Confer already complete — all consult todos finished',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const state = triggerConferCheckpoint(sessionId, options.dueReason ?? null, projectRoot);
|
|
211
|
+
state.status = 'in_progress';
|
|
212
|
+
saveConferCheckpoint(state, projectRoot);
|
|
213
|
+
const collocated = options.collocatedText?.trim() ||
|
|
214
|
+
'# Synthesis confer\n\nReview checkpoint context and realign plan.';
|
|
215
|
+
const agents = [];
|
|
216
|
+
for (const todo of getSynthesisConsultTodos(plan)) {
|
|
217
|
+
if (todo.status === 'completed')
|
|
218
|
+
continue;
|
|
219
|
+
try {
|
|
220
|
+
let agentResult;
|
|
221
|
+
if (options.fixture) {
|
|
222
|
+
agentResult = writeFixtureConferReceipt(todo.id, todo.subagent, sessionId, projectRoot);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const prompt = buildConferPrompt(todo.subagent, todo.task, collocated, options.dueReason ?? null);
|
|
226
|
+
const outputText = await invokeConferAgent(todo.subagent, prompt, projectRoot);
|
|
227
|
+
agentResult = applyConferConsultResult(todo.id, todo.subagent, sessionId, outputText, projectRoot);
|
|
228
|
+
}
|
|
229
|
+
agents.push(agentResult);
|
|
230
|
+
if (agentResult.receiptRecorded && agentResult.todoCompleted) {
|
|
231
|
+
state.completedAgents.push(todo.subagent);
|
|
232
|
+
saveConferCheckpoint(state, projectRoot);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
state.status = 'failed';
|
|
236
|
+
state.lastError = `Receipt or todo completion failed for ${todo.id}`;
|
|
237
|
+
saveConferCheckpoint(state, projectRoot);
|
|
238
|
+
return {
|
|
239
|
+
status: 'partial',
|
|
240
|
+
agents,
|
|
241
|
+
message: state.lastError,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
247
|
+
state.status = 'failed';
|
|
248
|
+
state.lastError = message;
|
|
249
|
+
saveConferCheckpoint(state, projectRoot);
|
|
250
|
+
agents.push({
|
|
251
|
+
todoId: todo.id,
|
|
252
|
+
subagent: todo.subagent,
|
|
253
|
+
outputText: '',
|
|
254
|
+
receiptRecorded: false,
|
|
255
|
+
todoCompleted: false,
|
|
256
|
+
verdict: null,
|
|
257
|
+
error: message,
|
|
258
|
+
});
|
|
259
|
+
return { status: 'failed', agents, message };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const refreshed = loadPersistedLeadDevPlan(projectRoot);
|
|
263
|
+
const done = refreshed &&
|
|
264
|
+
isSynthesisRealignmentPlan(refreshed) &&
|
|
265
|
+
areSynthesisConsultTodosComplete(refreshed);
|
|
266
|
+
state.status = done ? 'completed' : 'failed';
|
|
267
|
+
if (!done)
|
|
268
|
+
state.lastError = 'Consult todos remain after confer loop';
|
|
269
|
+
saveConferCheckpoint(state, projectRoot);
|
|
270
|
+
return {
|
|
271
|
+
status: done ? 'completed' : 'partial',
|
|
272
|
+
agents,
|
|
273
|
+
message: done
|
|
274
|
+
? 'Confer quorum complete — researcher, architect-tools, code-review consulted'
|
|
275
|
+
: (state.lastError ?? 'Confer incomplete'),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const CONFER_AGENT_EMOJI = {
|
|
279
|
+
researcher: '🔍',
|
|
280
|
+
'architect-tools': '🏗️',
|
|
281
|
+
'code-review': '✅',
|
|
282
|
+
'code-reviewer': '✅',
|
|
283
|
+
architect: '🏗️',
|
|
284
|
+
strategist: '🎯',
|
|
285
|
+
};
|
|
286
|
+
function conferAgentEmoji(subagent) {
|
|
287
|
+
return CONFER_AGENT_EMOJI[subagent] ?? '🤖';
|
|
288
|
+
}
|
|
289
|
+
export function formatConferQuorumReport(result) {
|
|
290
|
+
if (result.status === 'skipped') {
|
|
291
|
+
return `ℹ️ Confer skipped: ${result.message}`;
|
|
292
|
+
}
|
|
293
|
+
const lines = [
|
|
294
|
+
`## Confer quorum — ${result.status.toUpperCase()}`,
|
|
295
|
+
result.message,
|
|
296
|
+
'',
|
|
297
|
+
];
|
|
298
|
+
for (const agent of result.agents) {
|
|
299
|
+
const emoji = conferAgentEmoji(agent.subagent);
|
|
300
|
+
lines.push(`- ${emoji} **${agent.todoId}** (${agent.subagent}): verdict=${agent.verdict ?? 'n/a'} receipt=${agent.receiptRecorded} todo=${agent.todoCompleted}${agent.error ? ` error=${agent.error}` : ''}`);
|
|
301
|
+
}
|
|
302
|
+
const plan = loadPersistedLeadDevPlan();
|
|
303
|
+
const next = plan ? getNextRequiredTodo(plan) : null;
|
|
304
|
+
if (next) {
|
|
305
|
+
lines.push('', `**Next todo:** ${next.id} (${next.subagent})`);
|
|
306
|
+
}
|
|
307
|
+
return lines.join('\n');
|
|
308
|
+
}
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
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
|
+
export { extractSpawnCwd, validateAsideWorktreeCwd, provisionGitWorktree } from './aside-worktree.js';
|
|
9
10
|
export { validateSpawnMatchesTodo, updatePlanTodoStatus, savePersistedLeadDevPlan, } from './lead-dev-plan-persistence.js';
|
|
11
|
+
export { buildReceiptFromConsultOutput, hasValidSynthesisConsultReceipt, isSynthesisConsultTodoId, loadSynthesisConsultReceipt, parseConsultVerdictFromText, tryRecordSynthesisConsultReceipt, writeSynthesisConsultReceipt, type SynthesisConsultReceipt, type SynthesisConsultVerdict, } from './synthesis-consult-receipt.js';
|
|
10
12
|
export declare const updatePlanTodoStatusInPlace: typeof updatePlanTodoStatus;
|
|
11
13
|
export type DelegationGateHost = 'grok' | 'hermes' | 'opencode' | 'openclaw' | 'generic';
|
|
12
14
|
export interface DelegationGateFeatures {
|
|
@@ -51,12 +53,21 @@ export interface PreToolGateDeny {
|
|
|
51
53
|
}
|
|
52
54
|
export interface PreToolGateAllow {
|
|
53
55
|
allow: true;
|
|
56
|
+
reason?: string;
|
|
57
|
+
gate?: string;
|
|
58
|
+
hint?: Record<string, unknown>;
|
|
54
59
|
}
|
|
55
60
|
export type PreToolGateResult = PreToolGateAllow | PreToolGateDeny;
|
|
56
61
|
export interface PostToolSpawnResult {
|
|
57
62
|
satisfied: PendingDelegation[];
|
|
58
63
|
clearedAll: boolean;
|
|
59
64
|
expectedTodoId?: string | null;
|
|
65
|
+
receiptRecorded?: boolean;
|
|
66
|
+
todoCompleted?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface PostToolSpawnOptions {
|
|
69
|
+
toolOutput?: unknown;
|
|
70
|
+
sessionId?: string | null;
|
|
60
71
|
}
|
|
61
72
|
export declare function loadDelegationGateFeatures(projectRoot?: string): DelegationGateFeatures;
|
|
62
73
|
export declare function normalizeHostToolInput(toolInput: ToolGateInput): SpawnToolInput & {
|
|
@@ -77,7 +88,7 @@ export declare function evaluatePendingDelegationGate(toolName: string, toolInpu
|
|
|
77
88
|
* pending gate, then spawn todo gate for subagent tools.
|
|
78
89
|
*/
|
|
79
90
|
export declare function evaluatePreToolGate(toolName: string, toolInput: ToolGateInput, ctx: PreToolGateContext): PreToolGateResult;
|
|
80
|
-
export declare function evaluatePostToolSpawn(toolName: string, toolInput: ToolGateInput, projectRoot: string): PostToolSpawnResult;
|
|
91
|
+
export declare function evaluatePostToolSpawn(toolName: string, toolInput: ToolGateInput, projectRoot: string, options?: PostToolSpawnOptions): PostToolSpawnResult;
|
|
81
92
|
export declare function isOrchestrateToolEvent(toolName: string, toolInput?: ToolGateInput): boolean;
|
|
82
93
|
/** Back-compat shims for Grok hook-utils and tests. */
|
|
83
94
|
export declare function checkPendingDelegationGate(toolName: string, toolInput: ToolGateInput, features: DelegationGateFeatures, projectRoot: string, sessionId: string | null): PreToolGateDeny | null;
|
|
@@ -7,9 +7,31 @@ 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,
|
|
10
|
+
import { allPlanTodos, areSynthesisConsultTodosComplete, findRecentStalePlanArchive, getSynthesisConsultTodos, isLeadDevPlanStale, 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';
|
|
13
|
+
import { resolveSpawnPlan, hasValidSpawnPlanContext } from './spawn-plan-resolution.js';
|
|
14
|
+
import { isUserAsidesEnabled, isUserAsideTodoId, loadActiveAsidePointer, loadActiveUserAside, } from './user-aside.js';
|
|
15
|
+
import { validateAsideWorktreeCwd, } from './aside-worktree.js';
|
|
16
|
+
export { extractSpawnCwd, validateAsideWorktreeCwd, provisionGitWorktree } from './aside-worktree.js';
|
|
17
|
+
function denyAsideWorktreeCwd(cwdCheck) {
|
|
18
|
+
const base = {
|
|
19
|
+
allow: false,
|
|
20
|
+
reason: cwdCheck.reason ?? 'Aside spawn cwd must match worktree',
|
|
21
|
+
gate: cwdCheck.gate ?? 'aside-worktree-cwd-missing',
|
|
22
|
+
};
|
|
23
|
+
return cwdCheck.hint ? { ...base, hint: cwdCheck.hint } : base;
|
|
24
|
+
}
|
|
25
|
+
function asideWorktreeCwdDenyIfNeeded(activeAside, toolInput, projectRoot) {
|
|
26
|
+
if (!activeAside?.worktree)
|
|
27
|
+
return undefined;
|
|
28
|
+
const cwdCheck = validateAsideWorktreeCwd(activeAside.worktree, toolInput, projectRoot);
|
|
29
|
+
if (!cwdCheck.valid)
|
|
30
|
+
return denyAsideWorktreeCwd(cwdCheck);
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
12
33
|
export { validateSpawnMatchesTodo, updatePlanTodoStatus, savePersistedLeadDevPlan, } from './lead-dev-plan-persistence.js';
|
|
34
|
+
export { buildReceiptFromConsultOutput, hasValidSynthesisConsultReceipt, isSynthesisConsultTodoId, loadSynthesisConsultReceipt, parseConsultVerdictFromText, tryRecordSynthesisConsultReceipt, writeSynthesisConsultReceipt, } from './synthesis-consult-receipt.js';
|
|
13
35
|
export const updatePlanTodoStatusInPlace = updatePlanTodoStatus;
|
|
14
36
|
const READ_TOOLS = new Set([
|
|
15
37
|
'read_file',
|
|
@@ -180,7 +202,29 @@ export function evaluateSpawnPlanGate(toolName, toolInput, ctx) {
|
|
|
180
202
|
if (!ctx.features.lead_dev_mode || !isSubagentTool(toolName)) {
|
|
181
203
|
return { allow: true };
|
|
182
204
|
}
|
|
183
|
-
|
|
205
|
+
const activeAside = isUserAsidesEnabled(ctx.projectRoot)
|
|
206
|
+
? loadActiveUserAside(ctx.projectRoot, ctx.sessionId)
|
|
207
|
+
: null;
|
|
208
|
+
const plan = loadPersistedLeadDevPlan(ctx.projectRoot);
|
|
209
|
+
if (!activeAside && plan && isLeadDevPlanStale(plan, ctx.projectRoot)) {
|
|
210
|
+
return {
|
|
211
|
+
allow: false,
|
|
212
|
+
reason: 'Lead-dev plan is stale (unstarted todos exceeded TTL) — ' +
|
|
213
|
+
're-run xray-orchestrator analyze-complexity to refresh the plan',
|
|
214
|
+
gate: 'spawn-plan-stale',
|
|
215
|
+
hint: { tool: 'analyze-complexity', mcp: 'xray-orchestrator' },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (!hasValidSpawnPlanContext(ctx.projectRoot, ctx.sessionId)) {
|
|
219
|
+
if (findRecentStalePlanArchive(ctx.projectRoot)) {
|
|
220
|
+
return {
|
|
221
|
+
allow: false,
|
|
222
|
+
reason: 'Lead-dev plan was stale and archived — ' +
|
|
223
|
+
're-run xray-orchestrator analyze-complexity to refresh the plan',
|
|
224
|
+
gate: 'spawn-plan-stale',
|
|
225
|
+
hint: { tool: 'analyze-complexity', mcp: 'xray-orchestrator' },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
184
228
|
return {
|
|
185
229
|
allow: false,
|
|
186
230
|
reason: 'Codex 59/67: call xray-orchestrator analyze-complexity first — ' +
|
|
@@ -189,17 +233,26 @@ export function evaluateSpawnPlanGate(toolName, toolInput, ctx) {
|
|
|
189
233
|
};
|
|
190
234
|
}
|
|
191
235
|
if (ctx.features.auto_chain_delegations === false) {
|
|
236
|
+
const cwdDeny = asideWorktreeCwdDenyIfNeeded(activeAside, toolInput, ctx.projectRoot);
|
|
237
|
+
if (cwdDeny)
|
|
238
|
+
return cwdDeny;
|
|
192
239
|
return { allow: true };
|
|
193
240
|
}
|
|
241
|
+
const normalized = normalizeHostToolInput(toolInput);
|
|
242
|
+
const resolved = isUserAsidesEnabled(ctx.projectRoot)
|
|
243
|
+
? resolveSpawnPlan(normalized, ctx.projectRoot, ctx.sessionId)
|
|
244
|
+
: { source: 'main', plan: loadPersistedLeadDevPlan(ctx.projectRoot) };
|
|
194
245
|
const pending = getActivePendingDelegations(ctx.sessionId, ctx.projectRoot);
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
? allPlanTodos(plan).find((t) => t.id === pending[0].planTodoId) ?? null
|
|
246
|
+
const expectedTodo = pending[0]?.planTodoId && resolved.plan
|
|
247
|
+
? allPlanTodos(resolved.plan).find((t) => t.id === pending[0].planTodoId) ?? null
|
|
198
248
|
: null;
|
|
199
|
-
const validation = validateSpawnMatchesTodo(
|
|
249
|
+
const validation = validateSpawnMatchesTodo(normalized, ctx.projectRoot, expectedTodo, ctx.sessionId);
|
|
200
250
|
if (!validation.valid) {
|
|
201
251
|
return denyFromSpawnValidation(validation);
|
|
202
252
|
}
|
|
253
|
+
const cwdDeny = asideWorktreeCwdDenyIfNeeded(activeAside, toolInput, ctx.projectRoot);
|
|
254
|
+
if (cwdDeny)
|
|
255
|
+
return cwdDeny;
|
|
203
256
|
return { allow: true };
|
|
204
257
|
}
|
|
205
258
|
/** Block writes / spawns / shell while synthesis checkpoint is due. */
|
|
@@ -211,13 +264,32 @@ export function evaluateSynthesisGate(toolName, toolInput, ctx) {
|
|
|
211
264
|
return { allow: true };
|
|
212
265
|
if (isSynthesisAllowedOrchestratorConsult(toolName, toolInput))
|
|
213
266
|
return { allow: true };
|
|
267
|
+
// Active user aside: allow aside-track subagent spawns during main synthesis checkpoint.
|
|
268
|
+
if (isUserAsidesEnabled(ctx.projectRoot)) {
|
|
269
|
+
const normalized = normalizeHostToolInput(toolInput);
|
|
270
|
+
const resolved = resolveSpawnPlan(normalized, ctx.projectRoot, ctx.sessionId);
|
|
271
|
+
if (resolved.source === 'aside' &&
|
|
272
|
+
isSubagentTool(toolName) &&
|
|
273
|
+
(normalized.planTodoId
|
|
274
|
+
? isUserAsideTodoId(normalized.planTodoId)
|
|
275
|
+
: Boolean(resolved.asideId))) {
|
|
276
|
+
const validation = validateSpawnMatchesTodo(normalized, ctx.projectRoot, undefined, ctx.sessionId);
|
|
277
|
+
if (validation.valid) {
|
|
278
|
+
const activeAside = loadActiveUserAside(ctx.projectRoot, ctx.sessionId);
|
|
279
|
+
const cwdDeny = asideWorktreeCwdDenyIfNeeded(activeAside, toolInput, ctx.projectRoot);
|
|
280
|
+
if (cwdDeny)
|
|
281
|
+
return cwdDeny;
|
|
282
|
+
return { allow: true };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
214
286
|
const plan = loadPersistedLeadDevPlan(ctx.projectRoot);
|
|
215
287
|
if (plan &&
|
|
216
288
|
isSynthesisRealignmentPlan(plan) &&
|
|
217
289
|
!areSynthesisConsultTodosComplete(plan) &&
|
|
218
290
|
isSubagentTool(toolName)) {
|
|
219
291
|
const nextTodo = getNextRequiredTodo(plan);
|
|
220
|
-
const validation = validateSpawnMatchesTodo(normalizeHostToolInput(toolInput), ctx.projectRoot, nextTodo);
|
|
292
|
+
const validation = validateSpawnMatchesTodo(normalizeHostToolInput(toolInput), ctx.projectRoot, nextTodo, ctx.sessionId);
|
|
221
293
|
if (validation.valid)
|
|
222
294
|
return { allow: true };
|
|
223
295
|
}
|
|
@@ -250,6 +322,13 @@ export function evaluatePendingDelegationGate(toolName, toolInput, ctx) {
|
|
|
250
322
|
const pending = getActivePendingDelegations(ctx.sessionId, ctx.projectRoot);
|
|
251
323
|
if (pending.length === 0)
|
|
252
324
|
return { allow: true };
|
|
325
|
+
// Aside-track work: pending main delegations do not block aside spawns.
|
|
326
|
+
if (isUserAsidesEnabled(ctx.projectRoot) && isSubagentTool(toolName)) {
|
|
327
|
+
const normalized = normalizeHostToolInput(toolInput);
|
|
328
|
+
const resolved = resolveSpawnPlan(normalized, ctx.projectRoot, ctx.sessionId);
|
|
329
|
+
if (resolved.source === 'aside')
|
|
330
|
+
return { allow: true };
|
|
331
|
+
}
|
|
253
332
|
if (isSubagentTool(toolName))
|
|
254
333
|
return { allow: true };
|
|
255
334
|
if (isReadOnlyTool(toolName))
|
|
@@ -283,11 +362,14 @@ export function evaluatePreToolGate(toolName, toolInput, ctx) {
|
|
|
283
362
|
const spawnBlock = evaluateSpawnPlanGate(toolName, toolInput, ctx);
|
|
284
363
|
if (!spawnBlock.allow)
|
|
285
364
|
return spawnBlock;
|
|
286
|
-
return
|
|
365
|
+
return spawnBlock;
|
|
287
366
|
}
|
|
288
|
-
export function evaluatePostToolSpawn(toolName, toolInput, projectRoot) {
|
|
367
|
+
export function evaluatePostToolSpawn(toolName, toolInput, projectRoot, options = {}) {
|
|
289
368
|
const normalized = normalizeHostToolInput(toolInput);
|
|
290
|
-
const
|
|
369
|
+
const pointer = loadActiveAsidePointer(projectRoot);
|
|
370
|
+
const mainPlan = loadPersistedLeadDevPlan(projectRoot);
|
|
371
|
+
const sessionId = options.sessionId ?? mainPlan?.sessionId ?? pointer?.sessionId ?? null;
|
|
372
|
+
const spawnCheck = validateSpawnMatchesTodo(normalized, projectRoot, undefined, sessionId);
|
|
291
373
|
const satisfyInput = {
|
|
292
374
|
toolPrompt: normalized.prompt ?? '',
|
|
293
375
|
};
|
|
@@ -299,12 +381,30 @@ export function evaluatePostToolSpawn(toolName, toolInput, projectRoot) {
|
|
|
299
381
|
if (normalized.planTodoId)
|
|
300
382
|
satisfyInput.planTodoId = normalized.planTodoId;
|
|
301
383
|
const result = satisfyDelegation(satisfyInput, projectRoot);
|
|
302
|
-
|
|
303
|
-
|
|
384
|
+
let receiptRecorded = false;
|
|
385
|
+
let todoCompleted = false;
|
|
386
|
+
const expectedTodoId = spawnCheck.expectedTodoId ?? null;
|
|
387
|
+
if (spawnCheck.valid && expectedTodoId) {
|
|
388
|
+
const plan = loadPersistedLeadDevPlan(projectRoot);
|
|
389
|
+
const consultTodo = plan
|
|
390
|
+
? getSynthesisConsultTodos(plan).find((t) => t.id === expectedTodoId)
|
|
391
|
+
: undefined;
|
|
392
|
+
if (isSynthesisConsultTodoId(expectedTodoId) &&
|
|
393
|
+
consultTodo &&
|
|
394
|
+
sessionId &&
|
|
395
|
+
options.toolOutput != null) {
|
|
396
|
+
const receipt = tryRecordSynthesisConsultReceipt(expectedTodoId, consultTodo.subagent, sessionId, options.toolOutput, projectRoot);
|
|
397
|
+
receiptRecorded = receipt != null;
|
|
398
|
+
}
|
|
399
|
+
if (updatePlanTodoStatus(expectedTodoId, 'completed', projectRoot, sessionId)) {
|
|
400
|
+
todoCompleted = true;
|
|
401
|
+
}
|
|
304
402
|
}
|
|
305
403
|
return {
|
|
306
404
|
...result,
|
|
307
|
-
expectedTodoId
|
|
405
|
+
expectedTodoId,
|
|
406
|
+
receiptRecorded,
|
|
407
|
+
todoCompleted,
|
|
308
408
|
};
|
|
309
409
|
}
|
|
310
410
|
export function isOrchestrateToolEvent(toolName, toolInput = {}) {
|
|
@@ -15,7 +15,31 @@ export interface SpawnTodoValidation {
|
|
|
15
15
|
hint?: Record<string, unknown>;
|
|
16
16
|
expectedTodoId?: string | null;
|
|
17
17
|
}
|
|
18
|
+
export declare const DEFAULT_PLAN_STALE_MS: number;
|
|
19
|
+
export declare const DEFAULT_PLAN_ARCHIVE_MARKER_MS: number;
|
|
18
20
|
export declare function leadDevPlanStatePath(projectRoot?: string): string;
|
|
21
|
+
export declare function loadLeadDevPlanStaleMs(projectRoot?: string): number;
|
|
22
|
+
export declare function loadLeadDevPlanArchiveMarkerMs(projectRoot?: string): number;
|
|
23
|
+
/**
|
|
24
|
+
* Mandatory synthesis consults (s.1–s.3) must stay spawnable until complete —
|
|
25
|
+
* never treat an active realignment plan as stale while consult todos remain.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isProtectedSynthesisRealignmentPlan(plan: PersistedLeadDevPlan): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Stale when every outstanding todo is still pending and plan age exceeds TTL.
|
|
30
|
+
* In-progress or completed todos imply active work — plan stays valid.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isLeadDevPlanStale(plan: PersistedLeadDevPlan, projectRoot?: string): boolean;
|
|
33
|
+
export declare function archiveStaleLeadDevPlan(projectRoot?: string): {
|
|
34
|
+
archived: boolean;
|
|
35
|
+
archivePath?: string;
|
|
36
|
+
reason?: string;
|
|
37
|
+
};
|
|
38
|
+
/** Recent stale archival — keeps spawn-plan-stale gate after session-start cleanup. */
|
|
39
|
+
export declare function findRecentStalePlanArchive(projectRoot?: string, maxAgeMs?: number): {
|
|
40
|
+
archivePath: string;
|
|
41
|
+
archivedAt: string;
|
|
42
|
+
} | null;
|
|
19
43
|
export declare function loadPersistedLeadDevPlan(projectRoot?: string): PersistedLeadDevPlan | null;
|
|
20
44
|
export declare function savePersistedLeadDevPlan(plan: PersistedLeadDevPlan, projectRoot?: string): string;
|
|
21
45
|
export declare function allPlanTodos(plan: PersistedLeadDevPlan): LeadDevTodo[];
|
|
@@ -31,7 +55,7 @@ export declare function hasValidLeadDevPlanForSpawn(projectRoot?: string, _sessi
|
|
|
31
55
|
export declare function isSynthesisRealignmentPlan(plan: PersistedLeadDevPlan): boolean;
|
|
32
56
|
export declare function getSynthesisConsultTodos(plan: PersistedLeadDevPlan): LeadDevTodo[];
|
|
33
57
|
export declare function areSynthesisConsultTodosComplete(plan: PersistedLeadDevPlan): boolean;
|
|
34
|
-
export declare function updatePlanTodoStatus(todoId: string, status: LeadDevTodo['status'], projectRoot?: string): boolean;
|
|
58
|
+
export declare function updatePlanTodoStatus(todoId: string, status: LeadDevTodo['status'], projectRoot?: string, sessionId?: string | null): boolean;
|
|
35
59
|
export declare function bindPlanToSession(sessionId: string, projectRoot?: string): PersistedLeadDevPlan | null;
|
|
36
60
|
export interface SpawnToolInput {
|
|
37
61
|
prompt?: string;
|
|
@@ -42,5 +66,5 @@ export interface SpawnToolInput {
|
|
|
42
66
|
delegationId?: string;
|
|
43
67
|
planTodoId?: string;
|
|
44
68
|
}
|
|
45
|
-
export declare function validateSpawnMatchesTodo(toolInput: SpawnToolInput, projectRoot?: string, expectedTodo?: LeadDevTodo | null): SpawnTodoValidation;
|
|
46
|
-
export declare function markTodoInProgressOnSpawn(toolInput: SpawnToolInput, projectRoot?: string): string | null;
|
|
69
|
+
export declare function validateSpawnMatchesTodo(toolInput: SpawnToolInput, projectRoot?: string, expectedTodo?: LeadDevTodo | null, sessionId?: string | null): SpawnTodoValidation;
|
|
70
|
+
export declare function markTodoInProgressOnSpawn(toolInput: SpawnToolInput, projectRoot?: string, sessionId?: string | null): string | null;
|