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
@@ -29,6 +29,13 @@ export declare class ComplexityHandler {
29
29
  sessionId?: string;
30
30
  synthesisCheckpoint?: boolean;
31
31
  synthesisDueReason?: string | null;
32
+ collocatedText?: string | null;
33
+ conferFixture?: boolean;
34
+ userAsideId?: string;
35
+ userAsideTitle?: string;
36
+ worktree?: string;
37
+ branch?: string;
38
+ setActiveAside?: boolean;
32
39
  }, asideId?: string): Promise<{
33
40
  content: Array<{
34
41
  type: string;
@@ -5,8 +5,11 @@
5
5
  */
6
6
  import { frameworkLogger } from '../../../core/framework-logger.js';
7
7
  import { buildLeadDevPlan, buildSynthesisCheckpointPlan, isLeadDevModeActive, persistLeadDevPlan, } from '../../../nucleus/autonomy-kernel.js';
8
+ import { formatConferQuorumReport, isConferEnabled, runConferQuorum, } from '../../../nucleus/confer.js';
8
9
  import { bindPlanToSession } from '../../../nucleus/lead-dev-plan-persistence.js';
9
10
  import { clearPendingDelegations } from '../../../nucleus/pending-delegations.js';
11
+ import { isAutoProvisionWorktreeEnabled, provisionGitWorktree, } from '../../../nucleus/aside-worktree.js';
12
+ import { buildUserAsidePlan, formatUserAsideSummary, isUserAsidesEnabled, loadUserAside, mergeUserAsideIntake, saveUserAside, setActiveAsideId, UserAsideValidationError, } from '../../../nucleus/user-aside.js';
10
13
  import { getExecutionPlanner } from '../execution/execution-planner.js';
11
14
  import { addObservations, extractComplexityObservations } from '../aside-context.js';
12
15
  function normalizeAnalyzeTasks(raw) {
@@ -61,13 +64,89 @@ export class ComplexityHandler {
61
64
  .filter((t) => t.description?.trim())
62
65
  .map((t) => ({ description: t.description, type: t.type }));
63
66
  const synthesisCheckpoint = args.synthesisCheckpoint === true;
64
- const leadDevPlan = synthesisCheckpoint
67
+ const projectRoot = process.cwd();
68
+ const userAsideIntake = !synthesisCheckpoint &&
69
+ Boolean(args.userAsideId?.trim()) &&
70
+ isUserAsidesEnabled(projectRoot);
71
+ let userAsideReport = '';
72
+ let leadDevPlan = synthesisCheckpoint
65
73
  ? buildSynthesisCheckpointPlan(args.synthesisDueReason ?? null)
66
- : isLeadDevModeActive() && (primaryDescription || planTaskInputs.length > 0)
67
- ? buildLeadDevPlan(primaryDescription, taskTypes.length ? taskTypes : ['implement'], planTaskInputs, analysis.overallComplexity)
68
- : null;
69
- let planPersisted = synthesisCheckpoint ? leadDevPlan !== null : !leadDevPlan;
70
- if (leadDevPlan) {
74
+ : userAsideIntake
75
+ ? null
76
+ : isLeadDevModeActive() && (primaryDescription || planTaskInputs.length > 0)
77
+ ? buildLeadDevPlan(primaryDescription, taskTypes.length ? taskTypes : ['implement'], planTaskInputs, analysis.overallComplexity)
78
+ : null;
79
+ let planPersisted = synthesisCheckpoint
80
+ ? leadDevPlan !== null
81
+ : userAsideIntake
82
+ ? false
83
+ : !leadDevPlan;
84
+ if (userAsideIntake && args.userAsideId) {
85
+ try {
86
+ const asideId = args.userAsideId.trim();
87
+ const title = args.userAsideTitle?.trim() || asideId;
88
+ const existing = loadUserAside(asideId, projectRoot);
89
+ const userAside = existing
90
+ ? mergeUserAsideIntake(existing, primaryDescription || existing.description, planTaskInputs, analysis.overallComplexity, projectRoot)
91
+ : buildUserAsidePlan(asideId, title, primaryDescription || title, planTaskInputs, analysis.overallComplexity, projectRoot);
92
+ if (!userAside) {
93
+ planPersisted = false;
94
+ }
95
+ else {
96
+ if (args.worktree)
97
+ userAside.worktree = args.worktree;
98
+ if (args.branch)
99
+ userAside.branch = args.branch;
100
+ if (args.sessionId)
101
+ userAside.sessionId = args.sessionId;
102
+ if (args.userAsideTitle)
103
+ userAside.title = title;
104
+ let worktreeProvisionNote = '';
105
+ if (args.worktree?.trim() &&
106
+ args.branch?.trim() &&
107
+ isAutoProvisionWorktreeEnabled(projectRoot)) {
108
+ const provision = provisionGitWorktree({
109
+ projectRoot,
110
+ worktree: args.worktree.trim(),
111
+ branch: args.branch.trim(),
112
+ });
113
+ if (!provision.ok) {
114
+ throw new UserAsideValidationError(`Worktree auto-provision failed: ${provision.message}`);
115
+ }
116
+ worktreeProvisionNote = provision.created
117
+ ? `\n\n**Worktree provisioned:** \`${provision.path}\` (branch \`${args.branch.trim()}\`)`
118
+ : `\n\n**Worktree ready:** \`${provision.path}\``;
119
+ }
120
+ const asidePath = saveUserAside(userAside, projectRoot);
121
+ const activate = args.setActiveAside !== false;
122
+ if (activate) {
123
+ clearPendingDelegations(projectRoot);
124
+ setActiveAsideId(asideId, projectRoot, args.sessionId ?? null);
125
+ }
126
+ planPersisted = true;
127
+ userAsideReport = `\n\n## User aside\n\n${formatUserAsideSummary(userAside)}${worktreeProvisionNote}`;
128
+ if (activate) {
129
+ userAsideReport += `\n\n**Active aside:** \`${asideId}\` — spawns route to \`${asideId}.a.*\` todos.`;
130
+ }
131
+ await frameworkLogger.log('orchestrator.server', 'user-aside-persisted', 'info', {
132
+ asidePath,
133
+ asideId,
134
+ todoCount: userAside.plan.phases.reduce((n, p) => n + p.todos.length, 0),
135
+ activated: activate,
136
+ });
137
+ }
138
+ }
139
+ catch (err) {
140
+ planPersisted = false;
141
+ const message = err instanceof Error ? err.message : String(err);
142
+ userAsideReport = `\n\n❌ User aside intake failed: ${message}`;
143
+ await frameworkLogger.log('orchestrator.server', 'user-aside-persist-failed', 'warning', {
144
+ error: message,
145
+ validation: err instanceof UserAsideValidationError,
146
+ });
147
+ }
148
+ }
149
+ else if (leadDevPlan) {
71
150
  try {
72
151
  const sessionId = args.sessionId ?? `analyze-${Date.now()}`;
73
152
  if (!synthesisCheckpoint) {
@@ -90,13 +169,33 @@ export class ComplexityHandler {
90
169
  await frameworkLogger.log('orchestrator.server', synthesisCheckpoint ? 'synthesis-plan-persist-failed' : 'lead-dev-plan-persist-failed', 'warning', { error: err instanceof Error ? err.message : String(err) });
91
170
  }
92
171
  }
172
+ let conferReport = '';
173
+ if (synthesisCheckpoint &&
174
+ leadDevPlan &&
175
+ planPersisted &&
176
+ isConferEnabled() &&
177
+ args.sessionId) {
178
+ const conferOpts = {
179
+ dueReason: args.synthesisDueReason ?? null,
180
+ fixture: args.conferFixture === true,
181
+ };
182
+ if (args.collocatedText)
183
+ conferOpts.collocatedText = args.collocatedText;
184
+ const conferResult = await runConferQuorum(process.cwd(), args.sessionId, conferOpts);
185
+ conferReport = `\n\n${formatConferQuorumReport(conferResult)}`;
186
+ await frameworkLogger.log('orchestrator.server', 'confer-quorum', conferResult.status === 'completed' ? 'info' : 'warning', {
187
+ status: conferResult.status,
188
+ agentCount: conferResult.agents.length,
189
+ sessionId: args.sessionId,
190
+ });
191
+ }
93
192
  const ok = planPersisted;
94
193
  return {
95
194
  ok,
96
195
  content: [
97
196
  {
98
197
  type: 'text',
99
- text: this.formatComplexityResponse(analysis, recommendations, tasks.length, leadDevPlan, synthesisCheckpoint),
198
+ text: this.formatComplexityResponse(analysis, recommendations, tasks.length, leadDevPlan, synthesisCheckpoint) + conferReport + userAsideReport,
100
199
  },
101
200
  ],
102
201
  };
@@ -31,6 +31,12 @@ export declare class TaskHandler {
31
31
  sessionId?: string;
32
32
  executionMode?: string;
33
33
  timeout?: number;
34
+ confer?: boolean;
35
+ conferFixture?: boolean;
36
+ collocatedText?: string;
37
+ userAsideId?: string;
38
+ setActiveAside?: boolean;
39
+ clearActiveAside?: boolean;
34
40
  }, deps: TaskHandlerDeps): Promise<{
35
41
  content: Array<{
36
42
  type: string;
@@ -19,6 +19,81 @@ export class TaskHandler {
19
19
  * Handle orchestrate-task request
20
20
  */
21
21
  async handleOrchestrateTask(args, deps) {
22
+ if (args.clearActiveAside === true) {
23
+ const projectRoot = process.cwd();
24
+ const { clearActiveAside, getActiveAsideId } = await import('../../../nucleus/user-aside.js');
25
+ const prior = getActiveAsideId(projectRoot, args.sessionId ?? null);
26
+ clearActiveAside(projectRoot);
27
+ return {
28
+ content: [
29
+ {
30
+ type: 'text',
31
+ text: prior
32
+ ? `✅ Cleared active aside \`${prior}\` — spawns resume main lead-dev plan.`
33
+ : 'ℹ️ No active aside was set.',
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ if (args.userAsideId?.trim()) {
39
+ const projectRoot = process.cwd();
40
+ const { setActiveAsideId, loadUserAside, formatUserAsideSummary, isUserAsidesEnabled, UserAsideValidationError, } = await import('../../../nucleus/user-aside.js');
41
+ const { clearPendingDelegations } = await import('../../../nucleus/pending-delegations.js');
42
+ if (!isUserAsidesEnabled(projectRoot)) {
43
+ return {
44
+ content: [{ type: 'text', text: '❌ User asides disabled in features.json' }],
45
+ };
46
+ }
47
+ const asideId = args.userAsideId.trim();
48
+ const aside = loadUserAside(asideId, projectRoot);
49
+ if (!aside) {
50
+ return {
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: `❌ User aside \`${asideId}\` not found — run analyze-complexity with userAsideId first.`,
55
+ },
56
+ ],
57
+ };
58
+ }
59
+ if (args.setActiveAside !== false) {
60
+ try {
61
+ clearPendingDelegations(projectRoot);
62
+ setActiveAsideId(asideId, projectRoot, args.sessionId ?? null);
63
+ }
64
+ catch (err) {
65
+ const message = err instanceof UserAsideValidationError ? err.message : String(err);
66
+ return { content: [{ type: 'text', text: `❌ ${message}` }] };
67
+ }
68
+ }
69
+ return {
70
+ content: [
71
+ {
72
+ type: 'text',
73
+ text: `✅ Active aside: \`${asideId}\`\n\n${formatUserAsideSummary(aside)}`,
74
+ },
75
+ ],
76
+ };
77
+ }
78
+ if (args.confer === true) {
79
+ const { runConferQuorum, formatConferQuorumReport, isConferEnabled } = await import('../../../nucleus/confer.js');
80
+ const sid = args.sessionId ?? `confer_${Date.now()}`;
81
+ if (!isConferEnabled()) {
82
+ return {
83
+ content: [{ type: 'text', text: '❌ Confer disabled in features.json' }],
84
+ };
85
+ }
86
+ const conferOpts = {
87
+ dueReason: args.description,
88
+ fixture: args.conferFixture === true,
89
+ };
90
+ if (args.collocatedText)
91
+ conferOpts.collocatedText = args.collocatedText;
92
+ const result = await runConferQuorum(process.cwd(), sid, conferOpts);
93
+ return {
94
+ content: [{ type: 'text', text: formatConferQuorumReport(result) }],
95
+ };
96
+ }
22
97
  const { description, tasks = [], sessionId = `session_${Date.now()}`, executionMode = 'optimized', timeout = 300000 } = args;
23
98
  await frameworkLogger.log('orchestrator.server', 'orchestrate-task-start', 'info', { description, taskCount: tasks.length, sessionId, executionMode });
24
99
  const orchestrationResult = {
@@ -99,13 +99,37 @@ export class OrchestratorServer {
99
99
  default: 'optimized',
100
100
  },
101
101
  timeout: { type: 'number', default: 300000 },
102
+ confer: {
103
+ type: 'boolean',
104
+ description: 'Run mandatory 3-agent confer quorum (researcher, architect-tools, code-review) instead of task execution',
105
+ },
106
+ conferFixture: {
107
+ type: 'boolean',
108
+ description: 'Use fixture confer receipts (CI/verify)',
109
+ },
110
+ collocatedText: {
111
+ type: 'string',
112
+ description: 'Collocated synthesis context for confer prompts',
113
+ },
114
+ userAsideId: {
115
+ type: 'string',
116
+ description: 'Activate an existing user aside for spawn routing',
117
+ },
118
+ setActiveAside: {
119
+ type: 'boolean',
120
+ description: 'Set userAsideId as the active aside (default true)',
121
+ },
122
+ clearActiveAside: {
123
+ type: 'boolean',
124
+ description: 'Clear active aside — resume main lead-dev plan spawns',
125
+ },
102
126
  },
103
- required: ['description', 'tasks'],
127
+ required: ['description'],
104
128
  },
105
129
  },
106
130
  {
107
131
  name: 'analyze-complexity',
108
- description: 'Analyze task complexity and recommend orchestration strategy. When lead_dev_mode is on (features.json), includes phased plan + todos + subagent routes. During synthesis checkpoint, returns collocated context and mandatory-consult realignment plan — pass sessionId from the active session.',
132
+ description: 'Analyze task complexity and recommend orchestration strategy. When lead_dev_mode is on (features.json), includes phased plan + todos + subagent routes. Pass userAsideId to intake a parallel user aside ({asideId}.a.* namespaced todos, worktree-like). During synthesis checkpoint, returns collocated context and mandatory-consult realignment plan — pass sessionId from the active session.',
109
133
  inputSchema: {
110
134
  type: 'object',
111
135
  properties: {
@@ -113,6 +137,26 @@ export class OrchestratorServer {
113
137
  type: 'string',
114
138
  description: 'Active session id (required to clear synthesis checkpoint; inferred from checkpoint state when omitted)',
115
139
  },
140
+ userAsideId: {
141
+ type: 'string',
142
+ description: 'User aside id — persists .xray/state/asides/{id}.json with {asideId}.a.* todos (parallel track)',
143
+ },
144
+ userAsideTitle: {
145
+ type: 'string',
146
+ description: 'Display title for the user aside (defaults to userAsideId)',
147
+ },
148
+ worktree: {
149
+ type: 'string',
150
+ description: 'Optional worktree path for aside isolation',
151
+ },
152
+ branch: {
153
+ type: 'string',
154
+ description: 'Optional git branch for the aside',
155
+ },
156
+ setActiveAside: {
157
+ type: 'boolean',
158
+ description: 'Activate aside for spawn routing (default true when userAsideId set)',
159
+ },
116
160
  tasks: {
117
161
  type: 'array',
118
162
  items: {
@@ -308,18 +352,23 @@ export class OrchestratorServer {
308
352
  ...complexityArgs,
309
353
  ...(sessionId ? { sessionId } : {}),
310
354
  ...(synthesisDue
311
- ? { synthesisCheckpoint: true, synthesisDueReason: dueReason }
355
+ ? {
356
+ synthesisCheckpoint: true,
357
+ synthesisDueReason: dueReason,
358
+ collocatedText: collocated?.collatedText ?? null,
359
+ conferFixture: process.env.XRAY_CONFER_FIXTURE === '1',
360
+ }
312
361
  : {}),
313
362
  }, aside.asideId);
314
363
  let content = result.content;
315
364
  if (synthesisDue && collocated?.collatedText) {
316
365
  content = appendSynthesisContextToResponse(content, collocated.collatedText);
317
366
  }
318
- if (synthesisDue && result.ok) {
367
+ if (synthesisDue && result.ok && !result.content[0]?.text?.includes('Confer quorum')) {
319
368
  content = content.map((block, index) => index === 0
320
369
  ? {
321
370
  ...block,
322
- text: `${block.text}\n\n**Synthesis checkpoint remains active** until mandatory consult todos (s.1–s.3) are completed via Task spawns.`,
371
+ text: `${block.text}\n\n**Confer** runs automatically at synthesis checkpoint (researcher → architect-tools → code-review). Checkpoint clears when all consult todos complete.`,
323
372
  }
324
373
  : block);
325
374
  }
@@ -4,6 +4,21 @@ import { pathToFileURL } from 'node:url';
4
4
  import { frameworkLogger } from '../core/framework-logger.js';
5
5
  import { createMemoryRoutingProvider as createNullProvider } from './null-provider.js';
6
6
  import { validateMemoryRoutingConfig } from './validate-config.js';
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ function resolveUnavailableReason(provider) {
11
+ const statusProvider = provider;
12
+ if (typeof statusProvider.getAvailabilityStatus !== 'function') {
13
+ return null;
14
+ }
15
+ try {
16
+ return statusProvider.getAvailabilityStatus().reason ?? null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
7
22
  const REPERTOIRE_CANDIDATE_PATHS = [
8
23
  '../repertoire/dist/provider/memory-routing-provider.js',
9
24
  '../../repertoire/dist/provider/memory-routing-provider.js',
@@ -68,13 +83,26 @@ export async function loadMemoryRoutingProvider(config, cwd = process.cwd()) {
68
83
  if (!provider?.id || typeof provider.isAvailable !== 'function') {
69
84
  throw new Error(`Invalid memory routing provider from ${modulePath}`);
70
85
  }
86
+ const availabilityReason = resolveUnavailableReason(provider);
71
87
  if (!provider.isAvailable()) {
72
88
  await frameworkLogger.log('memory-routing', 'provider-unavailable', 'error', {
73
89
  providerId: provider.id,
74
90
  modulePath,
75
- message: `Provider "${provider.id}" loaded but isAvailable() returned false. Falling back to null provider.`,
91
+ unavailableReason: availabilityReason,
92
+ message: `Provider "${provider.id}" loaded but isAvailable() returned false (${availabilityReason ?? 'unknown'}). Falling back to null provider.`,
76
93
  });
77
- return createNullProvider();
94
+ if (availabilityReason === 'empty_registry') {
95
+ await sleep(100);
96
+ if (provider.isAvailable()) {
97
+ await frameworkLogger.log('memory-routing', 'provider-retry-ok', 'info', { providerId: provider.id, modulePath });
98
+ }
99
+ else {
100
+ return createNullProvider();
101
+ }
102
+ }
103
+ else {
104
+ return createNullProvider();
105
+ }
78
106
  }
79
107
  await frameworkLogger.log('memory-routing', 'provider-loaded', 'info', { providerId: provider.id, name: provider.name, modulePath });
80
108
  return provider;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * User-aside worktree path resolution, spawn cwd validation, optional git provision.
3
+ */
4
+ /** Minimal spawn tool input for cwd extraction (avoids circular import with delegation-gate). */
5
+ export interface SpawnCwdToolInput {
6
+ cwd?: string;
7
+ working_directory?: string;
8
+ workingDirectory?: string;
9
+ isolation?: string;
10
+ isolation_mode?: string;
11
+ worktree_path?: string;
12
+ worktreePath?: string;
13
+ worktree?: string;
14
+ [key: string]: unknown;
15
+ }
16
+ export interface AsideWorktreeCwdResult {
17
+ valid: boolean;
18
+ gate?: 'aside-worktree-cwd-missing' | 'aside-worktree-cwd-mismatch';
19
+ reason?: string;
20
+ hint?: Record<string, unknown>;
21
+ }
22
+ export interface WorktreeProvisionResult {
23
+ ok: boolean;
24
+ path: string;
25
+ created: boolean;
26
+ message: string;
27
+ }
28
+ export declare function normalizeWorktreePath(worktree: string, projectRoot: string): string;
29
+ export declare function isAutoProvisionWorktreeEnabled(projectRoot?: string): boolean;
30
+ /** Extract spawn cwd from host Task / spawn_subagent tool input. */
31
+ export declare function extractSpawnCwd(toolInput: SpawnCwdToolInput, projectRoot: string): string | null;
32
+ export declare function validateAsideWorktreeCwd(worktree: string | undefined, toolInput: SpawnCwdToolInput, projectRoot: string): AsideWorktreeCwdResult;
33
+ /**
34
+ * Guarded `git worktree add` when auto_provision_worktree is enabled.
35
+ * Creates branch with -b when it does not exist locally.
36
+ */
37
+ export declare function provisionGitWorktree(opts: {
38
+ projectRoot: string;
39
+ worktree: string;
40
+ branch: string;
41
+ }): WorktreeProvisionResult;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * User-aside worktree path resolution, spawn cwd validation, optional git provision.
3
+ */
4
+ import { execFileSync } from 'child_process';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { isUserAsidesEnabled } from './user-aside.js';
8
+ export function normalizeWorktreePath(worktree, projectRoot) {
9
+ const trimmed = worktree.trim().replace(/\/$/, '');
10
+ const resolved = path.isAbsolute(trimmed) ? trimmed : path.resolve(projectRoot, trimmed);
11
+ return resolved.replace(/\/$/, '');
12
+ }
13
+ export function isAutoProvisionWorktreeEnabled(projectRoot = process.cwd()) {
14
+ if (!isUserAsidesEnabled(projectRoot))
15
+ return false;
16
+ const featuresPath = path.join(projectRoot, '.xray', 'features.json');
17
+ if (!fs.existsSync(featuresPath))
18
+ return false;
19
+ try {
20
+ const data = JSON.parse(fs.readFileSync(featuresPath, 'utf8'));
21
+ const raw = data.multi_agent_orchestration?.user_asides;
22
+ if (raw?.enabled === false)
23
+ return false;
24
+ return raw?.auto_provision_worktree === true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /** Extract spawn cwd from host Task / spawn_subagent tool input. */
31
+ export function extractSpawnCwd(toolInput, projectRoot) {
32
+ const isolation = toolInput.isolation ?? toolInput.isolation_mode;
33
+ if (isolation === 'worktree' || isolation === 'git_worktree') {
34
+ const wt = toolInput.worktree_path ??
35
+ toolInput.worktreePath ??
36
+ toolInput.worktree ??
37
+ toolInput.cwd ??
38
+ toolInput.working_directory ??
39
+ toolInput.workingDirectory;
40
+ if (wt != null && String(wt).trim() !== '') {
41
+ return normalizeWorktreePath(String(wt), projectRoot);
42
+ }
43
+ }
44
+ const raw = toolInput.cwd ??
45
+ toolInput.working_directory ??
46
+ toolInput.workingDirectory;
47
+ if (raw == null || String(raw).trim() === '')
48
+ return null;
49
+ return normalizeWorktreePath(String(raw), projectRoot);
50
+ }
51
+ export function validateAsideWorktreeCwd(worktree, toolInput, projectRoot) {
52
+ if (!worktree?.trim())
53
+ return { valid: true };
54
+ const expected = normalizeWorktreePath(worktree, projectRoot);
55
+ const projectNorm = path.resolve(projectRoot).replace(/\/$/, '');
56
+ if (projectNorm === expected || projectNorm.startsWith(`${expected}/`)) {
57
+ return { valid: true };
58
+ }
59
+ const spawnCwd = extractSpawnCwd(toolInput, projectRoot);
60
+ if (!spawnCwd) {
61
+ return {
62
+ valid: false,
63
+ gate: 'aside-worktree-cwd-missing',
64
+ reason: `Aside worktree is ${expected} but project root is ${projectNorm} — ` +
65
+ 'spawn must set cwd / working_directory / isolation:worktree to the aside worktree',
66
+ hint: {
67
+ worktree: expected,
68
+ projectRoot: projectNorm,
69
+ required: ['cwd', 'working_directory', 'isolation:worktree'],
70
+ },
71
+ };
72
+ }
73
+ if (spawnCwd !== expected && !spawnCwd.startsWith(`${expected}/`)) {
74
+ return {
75
+ valid: false,
76
+ gate: 'aside-worktree-cwd-mismatch',
77
+ reason: `Spawn cwd ${spawnCwd} does not match aside worktree ${expected}`,
78
+ hint: { worktree: expected, spawnCwd, projectRoot: projectNorm },
79
+ };
80
+ }
81
+ return { valid: true };
82
+ }
83
+ function gitBranchExists(projectRoot, branch) {
84
+ try {
85
+ execFileSync('git', ['rev-parse', '--verify', branch], {
86
+ cwd: projectRoot,
87
+ stdio: 'pipe',
88
+ });
89
+ return true;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ function resolvePathForCompare(p) {
96
+ try {
97
+ return fs.realpathSync.native(p).replace(/\/$/, '');
98
+ }
99
+ catch {
100
+ return path.resolve(p).replace(/\/$/, '');
101
+ }
102
+ }
103
+ function isGitWorktreePath(wtPath, projectRoot) {
104
+ try {
105
+ const out = execFileSync('git', ['worktree', 'list', '--porcelain'], {
106
+ cwd: projectRoot,
107
+ encoding: 'utf8',
108
+ stdio: ['pipe', 'pipe', 'pipe'],
109
+ });
110
+ const normalized = resolvePathForCompare(wtPath);
111
+ return out.split('\n').some((line) => {
112
+ if (!line.startsWith('worktree '))
113
+ return false;
114
+ const listed = line.slice('worktree '.length).trim();
115
+ return resolvePathForCompare(listed) === normalized;
116
+ });
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
122
+ /**
123
+ * Guarded `git worktree add` when auto_provision_worktree is enabled.
124
+ * Creates branch with -b when it does not exist locally.
125
+ */
126
+ export function provisionGitWorktree(opts) {
127
+ const wtPath = normalizeWorktreePath(opts.worktree, opts.projectRoot);
128
+ const branch = opts.branch.trim();
129
+ if (!branch) {
130
+ return { ok: false, path: wtPath, created: false, message: 'branch is required' };
131
+ }
132
+ if (fs.existsSync(wtPath)) {
133
+ if (isGitWorktreePath(wtPath, opts.projectRoot)) {
134
+ return { ok: true, path: wtPath, created: false, message: 'worktree already exists' };
135
+ }
136
+ return {
137
+ ok: false,
138
+ path: wtPath,
139
+ created: false,
140
+ message: `path exists but is not a git worktree: ${wtPath}`,
141
+ };
142
+ }
143
+ const gitDir = path.join(opts.projectRoot, '.git');
144
+ if (!fs.existsSync(gitDir)) {
145
+ return {
146
+ ok: false,
147
+ path: wtPath,
148
+ created: false,
149
+ message: `not a git repository: ${opts.projectRoot}`,
150
+ };
151
+ }
152
+ const branchExists = gitBranchExists(opts.projectRoot, branch);
153
+ const args = branchExists
154
+ ? ['worktree', 'add', wtPath, branch]
155
+ : ['worktree', 'add', '-b', branch, wtPath];
156
+ try {
157
+ execFileSync('git', args, { cwd: opts.projectRoot, stdio: 'pipe' });
158
+ return { ok: true, path: wtPath, created: true, message: 'worktree provisioned' };
159
+ }
160
+ catch (err) {
161
+ const message = err instanceof Error ? err.message : String(err);
162
+ return { ok: false, path: wtPath, created: false, message };
163
+ }
164
+ }
@@ -0,0 +1,57 @@
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 { type SynthesisConsultReceipt } from './synthesis-consult-receipt.js';
6
+ export declare const CONFER_AGENTS: readonly ["researcher", "architect-tools", "code-review"];
7
+ export interface ConferConfig {
8
+ enabled: boolean;
9
+ on_synthesis: boolean;
10
+ }
11
+ export interface ConferCheckpointState {
12
+ version: 1;
13
+ sessionId: string;
14
+ triggeredAt: string;
15
+ dueReason: string | null;
16
+ status: 'pending' | 'in_progress' | 'completed' | 'failed';
17
+ completedAgents: string[];
18
+ lastError?: string;
19
+ }
20
+ export interface ConferAgentResult {
21
+ todoId: string;
22
+ subagent: string;
23
+ outputText: string;
24
+ receiptRecorded: boolean;
25
+ todoCompleted: boolean;
26
+ verdict: SynthesisConsultReceipt['verdict'] | null;
27
+ error?: string;
28
+ }
29
+ export interface ConferQuorumResult {
30
+ status: 'completed' | 'partial' | 'skipped' | 'failed';
31
+ agents: ConferAgentResult[];
32
+ message: string;
33
+ }
34
+ export declare function conferCheckpointPath(projectRoot?: string): string;
35
+ export declare function defaultConferConfig(): ConferConfig;
36
+ export declare function loadConferConfig(projectRoot?: string): ConferConfig;
37
+ export declare function isConferEnabled(projectRoot?: string): boolean;
38
+ export declare function loadConferCheckpoint(projectRoot?: string): ConferCheckpointState | null;
39
+ export declare function saveConferCheckpoint(state: ConferCheckpointState, projectRoot?: string): string;
40
+ export declare function triggerConferCheckpoint(sessionId: string, dueReason: string | null, projectRoot?: string): ConferCheckpointState;
41
+ export declare function isConferPending(projectRoot?: string, sessionId?: string | null): boolean;
42
+ export declare function buildConferPrompt(subagent: string, todoTask: string, collocatedText: string, dueReason: string | null): string;
43
+ /** Map confer subagent → MCP server + tool. */
44
+ export declare function conferAgentMcpTarget(subagent: string): {
45
+ server: string;
46
+ tool: string;
47
+ args: (prompt: string, projectRoot: string) => Record<string, unknown>;
48
+ };
49
+ export declare function invokeConferAgent(subagent: string, prompt: string, projectRoot?: string): Promise<string>;
50
+ export declare function applyConferConsultResult(todoId: string, subagent: string, sessionId: string, outputText: string, projectRoot?: string): ConferAgentResult;
51
+ export declare function writeFixtureConferReceipt(todoId: string, subagent: string, sessionId: string, projectRoot?: string): ConferAgentResult;
52
+ export declare function runConferQuorum(projectRoot: string | undefined, sessionId: string, options?: {
53
+ collocatedText?: string;
54
+ dueReason?: string | null;
55
+ fixture?: boolean;
56
+ }): Promise<ConferQuorumResult>;
57
+ export declare function formatConferQuorumReport(result: ConferQuorumResult): string;