0xray 3.5.4 → 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 +44 -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 +21 -0
- 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/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 +4 -0
- package/dist/nucleus/delegation-gate.js +90 -11
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
- package/dist/nucleus/lead-dev-plan-persistence.js +201 -19
- 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.js +18 -2
- 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/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,6 +6,7 @@
|
|
|
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';
|
|
10
11
|
export { buildReceiptFromConsultOutput, hasValidSynthesisConsultReceipt, isSynthesisConsultTodoId, loadSynthesisConsultReceipt, parseConsultVerdictFromText, tryRecordSynthesisConsultReceipt, writeSynthesisConsultReceipt, type SynthesisConsultReceipt, type SynthesisConsultVerdict, } from './synthesis-consult-receipt.js';
|
|
11
12
|
export declare const updatePlanTodoStatusInPlace: typeof updatePlanTodoStatus;
|
|
@@ -52,6 +53,9 @@ export interface PreToolGateDeny {
|
|
|
52
53
|
}
|
|
53
54
|
export interface PreToolGateAllow {
|
|
54
55
|
allow: true;
|
|
56
|
+
reason?: string;
|
|
57
|
+
gate?: string;
|
|
58
|
+
hint?: Record<string, unknown>;
|
|
55
59
|
}
|
|
56
60
|
export type PreToolGateResult = PreToolGateAllow | PreToolGateDeny;
|
|
57
61
|
export interface PostToolSpawnResult {
|
|
@@ -7,9 +7,29 @@ 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, getSynthesisConsultTodos,
|
|
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
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
|
+
}
|
|
13
33
|
export { validateSpawnMatchesTodo, updatePlanTodoStatus, savePersistedLeadDevPlan, } from './lead-dev-plan-persistence.js';
|
|
14
34
|
export { buildReceiptFromConsultOutput, hasValidSynthesisConsultReceipt, isSynthesisConsultTodoId, loadSynthesisConsultReceipt, parseConsultVerdictFromText, tryRecordSynthesisConsultReceipt, writeSynthesisConsultReceipt, } from './synthesis-consult-receipt.js';
|
|
15
35
|
export const updatePlanTodoStatusInPlace = updatePlanTodoStatus;
|
|
@@ -182,7 +202,29 @@ export function evaluateSpawnPlanGate(toolName, toolInput, ctx) {
|
|
|
182
202
|
if (!ctx.features.lead_dev_mode || !isSubagentTool(toolName)) {
|
|
183
203
|
return { allow: true };
|
|
184
204
|
}
|
|
185
|
-
|
|
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
|
+
}
|
|
186
228
|
return {
|
|
187
229
|
allow: false,
|
|
188
230
|
reason: 'Codex 59/67: call xray-orchestrator analyze-complexity first — ' +
|
|
@@ -191,17 +233,26 @@ export function evaluateSpawnPlanGate(toolName, toolInput, ctx) {
|
|
|
191
233
|
};
|
|
192
234
|
}
|
|
193
235
|
if (ctx.features.auto_chain_delegations === false) {
|
|
236
|
+
const cwdDeny = asideWorktreeCwdDenyIfNeeded(activeAside, toolInput, ctx.projectRoot);
|
|
237
|
+
if (cwdDeny)
|
|
238
|
+
return cwdDeny;
|
|
194
239
|
return { allow: true };
|
|
195
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) };
|
|
196
245
|
const pending = getActivePendingDelegations(ctx.sessionId, ctx.projectRoot);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
? 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
|
|
200
248
|
: null;
|
|
201
|
-
const validation = validateSpawnMatchesTodo(
|
|
249
|
+
const validation = validateSpawnMatchesTodo(normalized, ctx.projectRoot, expectedTodo, ctx.sessionId);
|
|
202
250
|
if (!validation.valid) {
|
|
203
251
|
return denyFromSpawnValidation(validation);
|
|
204
252
|
}
|
|
253
|
+
const cwdDeny = asideWorktreeCwdDenyIfNeeded(activeAside, toolInput, ctx.projectRoot);
|
|
254
|
+
if (cwdDeny)
|
|
255
|
+
return cwdDeny;
|
|
205
256
|
return { allow: true };
|
|
206
257
|
}
|
|
207
258
|
/** Block writes / spawns / shell while synthesis checkpoint is due. */
|
|
@@ -213,13 +264,32 @@ export function evaluateSynthesisGate(toolName, toolInput, ctx) {
|
|
|
213
264
|
return { allow: true };
|
|
214
265
|
if (isSynthesisAllowedOrchestratorConsult(toolName, toolInput))
|
|
215
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
|
+
}
|
|
216
286
|
const plan = loadPersistedLeadDevPlan(ctx.projectRoot);
|
|
217
287
|
if (plan &&
|
|
218
288
|
isSynthesisRealignmentPlan(plan) &&
|
|
219
289
|
!areSynthesisConsultTodosComplete(plan) &&
|
|
220
290
|
isSubagentTool(toolName)) {
|
|
221
291
|
const nextTodo = getNextRequiredTodo(plan);
|
|
222
|
-
const validation = validateSpawnMatchesTodo(normalizeHostToolInput(toolInput), ctx.projectRoot, nextTodo);
|
|
292
|
+
const validation = validateSpawnMatchesTodo(normalizeHostToolInput(toolInput), ctx.projectRoot, nextTodo, ctx.sessionId);
|
|
223
293
|
if (validation.valid)
|
|
224
294
|
return { allow: true };
|
|
225
295
|
}
|
|
@@ -252,6 +322,13 @@ export function evaluatePendingDelegationGate(toolName, toolInput, ctx) {
|
|
|
252
322
|
const pending = getActivePendingDelegations(ctx.sessionId, ctx.projectRoot);
|
|
253
323
|
if (pending.length === 0)
|
|
254
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
|
+
}
|
|
255
332
|
if (isSubagentTool(toolName))
|
|
256
333
|
return { allow: true };
|
|
257
334
|
if (isReadOnlyTool(toolName))
|
|
@@ -285,11 +362,14 @@ export function evaluatePreToolGate(toolName, toolInput, ctx) {
|
|
|
285
362
|
const spawnBlock = evaluateSpawnPlanGate(toolName, toolInput, ctx);
|
|
286
363
|
if (!spawnBlock.allow)
|
|
287
364
|
return spawnBlock;
|
|
288
|
-
return
|
|
365
|
+
return spawnBlock;
|
|
289
366
|
}
|
|
290
367
|
export function evaluatePostToolSpawn(toolName, toolInput, projectRoot, options = {}) {
|
|
291
368
|
const normalized = normalizeHostToolInput(toolInput);
|
|
292
|
-
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);
|
|
293
373
|
const satisfyInput = {
|
|
294
374
|
toolPrompt: normalized.prompt ?? '',
|
|
295
375
|
};
|
|
@@ -306,7 +386,6 @@ export function evaluatePostToolSpawn(toolName, toolInput, projectRoot, options
|
|
|
306
386
|
const expectedTodoId = spawnCheck.expectedTodoId ?? null;
|
|
307
387
|
if (spawnCheck.valid && expectedTodoId) {
|
|
308
388
|
const plan = loadPersistedLeadDevPlan(projectRoot);
|
|
309
|
-
const sessionId = options.sessionId ?? plan?.sessionId ?? null;
|
|
310
389
|
const consultTodo = plan
|
|
311
390
|
? getSynthesisConsultTodos(plan).find((t) => t.id === expectedTodoId)
|
|
312
391
|
: undefined;
|
|
@@ -317,7 +396,7 @@ export function evaluatePostToolSpawn(toolName, toolInput, projectRoot, options
|
|
|
317
396
|
const receipt = tryRecordSynthesisConsultReceipt(expectedTodoId, consultTodo.subagent, sessionId, options.toolOutput, projectRoot);
|
|
318
397
|
receiptRecorded = receipt != null;
|
|
319
398
|
}
|
|
320
|
-
if (updatePlanTodoStatus(expectedTodoId, 'completed', projectRoot)) {
|
|
399
|
+
if (updatePlanTodoStatus(expectedTodoId, 'completed', projectRoot, sessionId)) {
|
|
321
400
|
todoCompleted = true;
|
|
322
401
|
}
|
|
323
402
|
}
|
|
@@ -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;
|