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.
Files changed (76) hide show
  1. package/.xray/features.json +5 -1
  2. package/.xray/features.schema.json +56 -0
  3. package/AGENTS-consumer.md +1 -1
  4. package/AGENTS.md +11 -1
  5. package/README.md +1 -1
  6. package/SKILLS.md +1 -1
  7. package/dist/AGENTS.md +11 -1
  8. package/dist/CHANGELOG.md +54 -0
  9. package/dist/README.md +1 -1
  10. package/dist/SKILLS.md +1 -1
  11. package/dist/cli/commands/hermes-install.js +3 -1
  12. package/dist/cli/index.js +0 -0
  13. package/dist/core/features-config.d.ts +15 -0
  14. package/dist/governance/governance-service.d.ts +1 -0
  15. package/dist/governance/governance-service.js +81 -6
  16. package/dist/integrations/enforcement-gate.js +15 -5
  17. package/dist/integrations/grok/grok-cli.js +1 -10
  18. package/dist/integrations/grok/hooks/grok-hook-utils.js +46 -2
  19. package/dist/integrations/grok/hooks/post-tool-use.js +48 -14
  20. package/dist/integrations/grok/hooks/pre-tool-use.js +20 -0
  21. package/dist/integrations/grok/hooks/session-start.js +28 -0
  22. package/dist/integrations/hermes-agent/__init__.py +11 -1
  23. package/dist/integrations/hermes-agent/__pycache__/__init__.cpython-313.pyc +0 -0
  24. package/dist/integrations/hermes-agent/__pycache__/schemas.cpython-313.pyc +0 -0
  25. package/dist/integrations/hermes-agent/__pycache__/tools.cpython-313.pyc +0 -0
  26. package/dist/integrations/hermes-agent/bridge.mjs +73 -10
  27. package/dist/integrations/hooks/confer-hook-runtime.mjs +49 -0
  28. package/dist/integrations/hooks/delegation-gate-runtime.mjs +6 -0
  29. package/dist/integrations/hooks/pipeline-hook-runtime.mjs +211 -0
  30. package/dist/integrations/hooks/plan-hook-runtime.mjs +68 -0
  31. package/dist/integrations/hooks/user-aside-hook-runtime.mjs +49 -0
  32. package/dist/mcps/architect-tools.server.js +6 -1
  33. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +7 -0
  34. package/dist/mcps/orchestrator/handlers/complexity-handler.js +106 -7
  35. package/dist/mcps/orchestrator/handlers/task-handler.d.ts +6 -0
  36. package/dist/mcps/orchestrator/handlers/task-handler.js +75 -0
  37. package/dist/mcps/orchestrator/server.js +54 -5
  38. package/dist/memory-routing/provider-loader.js +30 -2
  39. package/dist/nucleus/aside-worktree.d.ts +41 -0
  40. package/dist/nucleus/aside-worktree.js +164 -0
  41. package/dist/nucleus/confer.d.ts +57 -0
  42. package/dist/nucleus/confer.js +308 -0
  43. package/dist/nucleus/delegation-gate.d.ts +12 -1
  44. package/dist/nucleus/delegation-gate.js +113 -13
  45. package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
  46. package/dist/nucleus/lead-dev-plan-persistence.js +222 -18
  47. package/dist/nucleus/spawn-plan-resolution.d.ts +20 -0
  48. package/dist/nucleus/spawn-plan-resolution.js +42 -0
  49. package/dist/nucleus/synthesis-consult-receipt.d.ts +32 -0
  50. package/dist/nucleus/synthesis-consult-receipt.js +171 -0
  51. package/dist/nucleus/synthesis-context.js +1 -1
  52. package/dist/nucleus/user-aside.d.ts +70 -0
  53. package/dist/nucleus/user-aside.js +393 -0
  54. package/dist/plugin/xray-codex-injection.js +1 -1
  55. package/dist/postprocessor/PostProcessor.d.ts +1 -0
  56. package/dist/postprocessor/PostProcessor.js +11 -0
  57. package/dist/processors/implementations/session-summary-processor.d.ts +1 -1
  58. package/dist/processors/implementations/session-summary-processor.js +9 -6
  59. package/package.json +14 -5
  60. package/scripts/hooks/install-hooks.cjs +3 -3
  61. package/scripts/hooks/run-hook.js +171 -86
  62. package/scripts/mjs/verify-confer-core.mjs +103 -0
  63. package/scripts/mjs/verify-delegation-gate-core.mjs +48 -2
  64. package/scripts/mjs/verify-hermes-session-start.mjs +137 -0
  65. package/scripts/mjs/verify-pre-push-diff-core.mjs +87 -0
  66. package/scripts/mjs/verify-user-aside-core.mjs +137 -0
  67. package/scripts/node/bridge-mcp-wiring.cjs +10 -0
  68. package/scripts/node/install-bridges.cjs +3 -11
  69. package/scripts/node/load-reflection-config.mjs +2 -2
  70. package/scripts/node/release-gate.mjs +8 -4
  71. package/src/mcps/architect-tools.server.ts +6 -1
  72. package/src/mcps/orchestrator/handlers/complexity-handler.ts +178 -20
  73. package/src/mcps/orchestrator/handlers/task-handler.ts +99 -0
  74. package/src/mcps/orchestrator/server.ts +56 -5
  75. package/xray/features.json +5 -1
  76. 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, hasValidLeadDevPlanForSpawn, isSynthesisRealignmentPlan, loadPersistedLeadDevPlan, getNextRequiredTodo, validateSpawnMatchesTodo, updatePlanTodoStatus, } from './lead-dev-plan-persistence.js';
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
- if (!hasValidLeadDevPlanForSpawn(ctx.projectRoot, ctx.sessionId)) {
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 plan = loadPersistedLeadDevPlan(ctx.projectRoot);
196
- const expectedTodo = pending[0]?.planTodoId && plan
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(normalizeHostToolInput(toolInput), ctx.projectRoot, expectedTodo);
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 { allow: true };
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 spawnCheck = validateSpawnMatchesTodo(normalized, projectRoot);
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
- if (spawnCheck.valid && spawnCheck.expectedTodoId) {
303
- updatePlanTodoStatus(spawnCheck.expectedTodoId, 'completed', projectRoot);
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: spawnCheck.expectedTodoId ?? null,
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;