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.
Files changed (73) 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 +44 -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 +21 -0
  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/pipeline-hook-runtime.mjs +211 -0
  29. package/dist/integrations/hooks/plan-hook-runtime.mjs +68 -0
  30. package/dist/integrations/hooks/user-aside-hook-runtime.mjs +49 -0
  31. package/dist/mcps/architect-tools.server.js +6 -1
  32. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +7 -0
  33. package/dist/mcps/orchestrator/handlers/complexity-handler.js +106 -7
  34. package/dist/mcps/orchestrator/handlers/task-handler.d.ts +6 -0
  35. package/dist/mcps/orchestrator/handlers/task-handler.js +75 -0
  36. package/dist/mcps/orchestrator/server.js +54 -5
  37. package/dist/memory-routing/provider-loader.js +30 -2
  38. package/dist/nucleus/aside-worktree.d.ts +41 -0
  39. package/dist/nucleus/aside-worktree.js +164 -0
  40. package/dist/nucleus/confer.d.ts +57 -0
  41. package/dist/nucleus/confer.js +308 -0
  42. package/dist/nucleus/delegation-gate.d.ts +4 -0
  43. package/dist/nucleus/delegation-gate.js +90 -11
  44. package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
  45. package/dist/nucleus/lead-dev-plan-persistence.js +201 -19
  46. package/dist/nucleus/spawn-plan-resolution.d.ts +20 -0
  47. package/dist/nucleus/spawn-plan-resolution.js +42 -0
  48. package/dist/nucleus/synthesis-consult-receipt.js +18 -2
  49. package/dist/nucleus/synthesis-context.js +1 -1
  50. package/dist/nucleus/user-aside.d.ts +70 -0
  51. package/dist/nucleus/user-aside.js +393 -0
  52. package/dist/postprocessor/PostProcessor.d.ts +1 -0
  53. package/dist/postprocessor/PostProcessor.js +11 -0
  54. package/dist/processors/implementations/session-summary-processor.d.ts +1 -1
  55. package/dist/processors/implementations/session-summary-processor.js +9 -6
  56. package/package.json +14 -5
  57. package/scripts/hooks/install-hooks.cjs +3 -3
  58. package/scripts/hooks/run-hook.js +171 -86
  59. package/scripts/mjs/verify-confer-core.mjs +103 -0
  60. package/scripts/mjs/verify-delegation-gate-core.mjs +48 -2
  61. package/scripts/mjs/verify-hermes-session-start.mjs +137 -0
  62. package/scripts/mjs/verify-pre-push-diff-core.mjs +87 -0
  63. package/scripts/mjs/verify-user-aside-core.mjs +137 -0
  64. package/scripts/node/bridge-mcp-wiring.cjs +10 -0
  65. package/scripts/node/install-bridges.cjs +3 -11
  66. package/scripts/node/load-reflection-config.mjs +2 -2
  67. package/scripts/node/release-gate.mjs +8 -4
  68. package/src/mcps/architect-tools.server.ts +6 -1
  69. package/src/mcps/orchestrator/handlers/complexity-handler.ts +178 -20
  70. package/src/mcps/orchestrator/handlers/task-handler.ts +99 -0
  71. package/src/mcps/orchestrator/server.ts +56 -5
  72. package/xray/features.json +5 -1
  73. 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, 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
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
- 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
+ }
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 plan = loadPersistedLeadDevPlan(ctx.projectRoot);
198
- const expectedTodo = pending[0]?.planTodoId && plan
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(normalizeHostToolInput(toolInput), ctx.projectRoot, expectedTodo);
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 { allow: true };
365
+ return spawnBlock;
289
366
  }
290
367
  export function evaluatePostToolSpawn(toolName, toolInput, projectRoot, options = {}) {
291
368
  const normalized = normalizeHostToolInput(toolInput);
292
- 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);
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;