0xray 3.4.7 → 3.4.9

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 (40) hide show
  1. package/AGENTS-consumer.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/README.md +15 -1
  4. package/SKILLS.md +1 -1
  5. package/dist/AGENTS.md +1 -1
  6. package/dist/CHANGELOG.md +28 -0
  7. package/dist/README.md +15 -1
  8. package/dist/SKILLS.md +1 -1
  9. package/dist/governance/governance-service.js +3 -1
  10. package/dist/governance/llm-governance-provider.d.ts +2 -0
  11. package/dist/governance/llm-governance-provider.js +146 -104
  12. package/dist/mcps/governance.server.d.ts +1 -0
  13. package/dist/mcps/governance.server.js +79 -8
  14. package/dist/mcps/knowledge-skills/code-review.server.js +1 -1
  15. package/dist/mcps/knowledge-skills/security-audit.server.js +1 -1
  16. package/dist/mcps/orchestrator/config/memory-routing-bridge.js +2 -1
  17. package/dist/mcps/orchestrator/execution/execution-planner.d.ts +2 -0
  18. package/dist/mcps/orchestrator/execution/execution-planner.js +22 -11
  19. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +13 -1
  20. package/dist/mcps/orchestrator/handlers/complexity-handler.js +40 -4
  21. package/dist/mcps/orchestrator/handlers/task-handler.js +13 -6
  22. package/dist/mcps/orchestrator/server.js +9 -1
  23. package/dist/mcps/orchestrator/types.d.ts +4 -1
  24. package/dist/mcps/researcher.server.js +1 -1
  25. package/dist/nucleus/autonomy-kernel.d.ts +5 -1
  26. package/dist/nucleus/autonomy-kernel.js +20 -16
  27. package/dist/skills/orchestrator/SKILL.md +3 -2
  28. package/package.json +1 -1
  29. package/scripts/node/release-gate.mjs +14 -6
  30. package/src/mcps/governance.server.ts +81 -8
  31. package/src/mcps/knowledge-skills/code-review.server.ts +1 -1
  32. package/src/mcps/knowledge-skills/security-audit.server.ts +1 -1
  33. package/src/mcps/orchestrator/config/memory-routing-bridge.ts +3 -1
  34. package/src/mcps/orchestrator/execution/execution-planner.ts +26 -11
  35. package/src/mcps/orchestrator/handlers/complexity-handler.ts +62 -5
  36. package/src/mcps/orchestrator/handlers/task-handler.ts +22 -6
  37. package/src/mcps/orchestrator/server.ts +9 -1
  38. package/src/mcps/orchestrator/types.ts +4 -1
  39. package/src/mcps/researcher.server.ts +1 -1
  40. package/src/skills/orchestrator/SKILL.md +3 -2
@@ -5,6 +5,20 @@
5
5
  */
6
6
  import { getAgentCapabilitiesManager } from '../config/agent-capabilities.js';
7
7
  import { getProvider, toMemoryTask, fromMemoryTask, } from '../config/memory-routing-bridge.js';
8
+ /** Dependency count from task-ID list or numeric hint from analyze-complexity */
9
+ export function dependencyCount(deps) {
10
+ if (deps == null)
11
+ return 0;
12
+ if (typeof deps === 'number' && Number.isFinite(deps)) {
13
+ return Math.max(0, Math.round(deps));
14
+ }
15
+ if (Array.isArray(deps))
16
+ return deps.length;
17
+ return 0;
18
+ }
19
+ function isDependencyIdList(deps) {
20
+ return Array.isArray(deps) && deps.length > 0;
21
+ }
8
22
  /**
9
23
  * Execution Planner
10
24
  * Plans and optimizes task execution strategies
@@ -33,8 +47,8 @@ export class ExecutionPlanner {
33
47
  if (!task.type) {
34
48
  errors.push(`Task ${i}: Missing required field 'type'`);
35
49
  }
36
- // Check dependencies exist
37
- if (task.dependencies && task.id) {
50
+ // Check task-ID dependencies exist (numeric hints skip graph validation)
51
+ if (isDependencyIdList(task.dependencies) && task.id) {
38
52
  const taskIds = new Set(tasks.map(t => t.id).filter((id) => !!id));
39
53
  for (const depId of task.dependencies) {
40
54
  if (!taskIds.has(depId)) {
@@ -117,11 +131,11 @@ export class ExecutionPlanner {
117
131
  const independentTasks = [];
118
132
  const dependentTasks = [];
119
133
  for (const task of sortedTasks) {
120
- if (!task.dependencies || task.dependencies.length === 0) {
121
- independentTasks.push(task);
134
+ if (isDependencyIdList(task.dependencies)) {
135
+ dependentTasks.push(task);
122
136
  }
123
137
  else {
124
- dependentTasks.push(task);
138
+ independentTasks.push(task);
125
139
  }
126
140
  }
127
141
  // Assign independent tasks in parallel
@@ -140,7 +154,7 @@ export class ExecutionPlanner {
140
154
  if (visited.has(task.id))
141
155
  return;
142
156
  visited.add(task.id);
143
- if (task.dependencies) {
157
+ if (isDependencyIdList(task.dependencies)) {
144
158
  for (const depId of task.dependencies) {
145
159
  const depTask = taskMap.get(depId);
146
160
  if (depTask)
@@ -263,10 +277,7 @@ export class ExecutionPlanner {
263
277
  if (task.estimatedComplexity) {
264
278
  complexity = (complexity + task.estimatedComplexity) / 2;
265
279
  }
266
- // Adjust based on dependencies
267
- if (task.dependencies) {
268
- complexity += task.dependencies.length * 5;
269
- }
280
+ complexity += dependencyCount(task.dependencies) * 5;
270
281
  if (task.metadata?.memoryComplexityBoost !== undefined) {
271
282
  complexity += task.metadata.memoryComplexityBoost;
272
283
  }
@@ -295,7 +306,7 @@ export class ExecutionPlanner {
295
306
  */
296
307
  calculateParallelPotential(tasks) {
297
308
  // Tasks without dependencies can run in parallel
298
- const independent = tasks.filter(t => !t.dependencies || t.dependencies.length === 0);
309
+ const independent = tasks.filter((t) => !isDependencyIdList(t.dependencies));
299
310
  return independent.length / tasks.length;
300
311
  }
301
312
  }
@@ -4,6 +4,17 @@
4
4
  * Handles complexity analysis requests
5
5
  */
6
6
  import type { OrchestrationTask } from '../types.js';
7
+ type AnalyzeComplexityInput = {
8
+ id?: string;
9
+ description?: string;
10
+ type?: string;
11
+ files?: string[];
12
+ dependencyCount?: number;
13
+ dependencies?: string[] | number;
14
+ riskLevel?: string;
15
+ estimatedComplexity?: number;
16
+ priority?: OrchestrationTask['priority'];
17
+ };
7
18
  /**
8
19
  * Complexity Handler
9
20
  * Processes analyze-complexity requests
@@ -14,7 +25,7 @@ export declare class ComplexityHandler {
14
25
  * Handle analyze-complexity request
15
26
  */
16
27
  handleAnalyzeComplexity(args: {
17
- tasks: OrchestrationTask[];
28
+ tasks: AnalyzeComplexityInput[];
18
29
  }, asideId?: string): Promise<{
19
30
  content: Array<{
20
31
  type: string;
@@ -30,3 +41,4 @@ export declare class ComplexityHandler {
30
41
  */
31
42
  private formatComplexityResponse;
32
43
  }
44
+ export {};
@@ -7,6 +7,34 @@ import { frameworkLogger } from '../../../core/framework-logger.js';
7
7
  import { buildLeadDevPlan, isLeadDevModeActive, persistLeadDevPlan, } from '../../../nucleus/autonomy-kernel.js';
8
8
  import { getExecutionPlanner } from '../execution/execution-planner.js';
9
9
  import { addObservations, extractComplexityObservations } from '../aside-context.js';
10
+ function normalizeAnalyzeTasks(raw) {
11
+ return raw.map((task, index) => {
12
+ let deps;
13
+ if (Array.isArray(task.dependencies)) {
14
+ deps = task.dependencies;
15
+ }
16
+ else if (typeof task.dependencies === 'number' && Number.isFinite(task.dependencies)) {
17
+ deps = task.dependencies;
18
+ }
19
+ else if (typeof task.dependencyCount === 'number' &&
20
+ Number.isFinite(task.dependencyCount)) {
21
+ deps = task.dependencyCount;
22
+ }
23
+ const normalized = {
24
+ id: task.id ?? `task-${index + 1}`,
25
+ description: task.description ?? '',
26
+ type: task.type ?? 'implement',
27
+ };
28
+ if (task.priority)
29
+ normalized.priority = task.priority;
30
+ if (deps !== undefined)
31
+ normalized.dependencies = deps;
32
+ if (task.estimatedComplexity !== undefined) {
33
+ normalized.estimatedComplexity = task.estimatedComplexity;
34
+ }
35
+ return normalized;
36
+ });
37
+ }
10
38
  /**
11
39
  * Complexity Handler
12
40
  * Processes analyze-complexity requests
@@ -17,7 +45,7 @@ export class ComplexityHandler {
17
45
  * Handle analyze-complexity request
18
46
  */
19
47
  async handleAnalyzeComplexity(args, asideId) {
20
- const { tasks = [] } = args;
48
+ const tasks = normalizeAnalyzeTasks(args.tasks ?? []);
21
49
  await frameworkLogger.log('orchestrator.server', 'analyze-complexity-start', 'info', { taskCount: tasks.length });
22
50
  try {
23
51
  const analysis = await this.planner.analyzeTaskComplexity(tasks);
@@ -27,12 +55,20 @@ export class ComplexityHandler {
27
55
  }
28
56
  const primaryDescription = tasks.map((t) => t.description).filter(Boolean).join(' ');
29
57
  const taskTypes = tasks.map((t) => t.type).filter(Boolean);
30
- const leadDevPlan = isLeadDevModeActive() && primaryDescription
31
- ? buildLeadDevPlan(primaryDescription, taskTypes.length ? taskTypes : ['implement'])
58
+ const planTaskInputs = tasks
59
+ .filter((t) => t.description?.trim())
60
+ .map((t) => ({ description: t.description, type: t.type }));
61
+ const leadDevPlan = isLeadDevModeActive() && (primaryDescription || planTaskInputs.length > 0)
62
+ ? buildLeadDevPlan(primaryDescription, taskTypes.length ? taskTypes : ['implement'], planTaskInputs)
32
63
  : null;
33
64
  if (leadDevPlan) {
34
65
  try {
35
- persistLeadDevPlan(leadDevPlan);
66
+ const planPath = persistLeadDevPlan(leadDevPlan);
67
+ await frameworkLogger.log('orchestrator.server', 'lead-dev-plan-persisted', 'info', {
68
+ planPath,
69
+ phaseCount: leadDevPlan.phases.length,
70
+ todoCount: leadDevPlan.phases.reduce((n, p) => n + p.todos.length, 0),
71
+ });
36
72
  }
37
73
  catch (err) {
38
74
  await frameworkLogger.log('orchestrator.server', 'lead-dev-plan-persist-failed', 'warning', { error: err instanceof Error ? err.message : String(err) });
@@ -49,7 +49,6 @@ export class TaskHandler {
49
49
  },
50
50
  });
51
51
  }
52
- // Execute orchestration (simulated for MCP server)
53
52
  const results = await this.executePlan(executionPlan, sessionId, timeout);
54
53
  // Feed per-task outcomes back to memory provider (not aggregate session stats)
55
54
  if (memoryProvider.ingestFeedback) {
@@ -74,6 +73,8 @@ export class TaskHandler {
74
73
  orchestrationResult.success = results.success;
75
74
  orchestrationResult.completedTasks = results.completedTasks;
76
75
  orchestrationResult.failedTasks = results.failedTasks;
76
+ orchestrationResult.deferredTasks = results.deferredTasks;
77
+ orchestrationResult.recommendations.push(...results.recommendations);
77
78
  orchestrationResult.agentUtilization = results.agentUtilization;
78
79
  orchestrationResult.bottlenecks = results.bottlenecks;
79
80
  orchestrationResult.recommendations = results.recommendations;
@@ -111,6 +112,7 @@ export class TaskHandler {
111
112
  const agentUtilization = {};
112
113
  let completedTasks = 0;
113
114
  let failedTasks = 0;
115
+ let deferredTasks = 0;
114
116
  const agentOutputs = {};
115
117
  const taskOutcomes = [];
116
118
  const skillServers = new Set([
@@ -163,20 +165,23 @@ export class TaskHandler {
163
165
  }
164
166
  }
165
167
  else {
166
- // For non-MCP agents we still count them as "assigned" (they will be handled by the caller or legacy path)
167
168
  for (const task of tasks) {
168
- completedTasks++;
169
+ deferredTasks++;
169
170
  taskOutcomes.push({
170
171
  taskId: task.id,
171
172
  agent,
172
- success: true,
173
+ success: false,
173
174
  durationMs: 0,
174
175
  });
176
+ await frameworkLogger.log('orchestrator.task-handler', 'delegate-deferred', 'info', { agent, taskId: task.id, sessionId });
175
177
  }
176
178
  }
177
179
  }
178
180
  const recommendations = [];
179
181
  const bottlenecks = [];
182
+ if (deferredTasks > 0) {
183
+ recommendations.push(`${deferredTasks} task(s) require host delegation (Task/spawn_subagent per lead-dev-plan) — orchestrate-task only invokes MCP consult skills directly`);
184
+ }
180
185
  if (plan.agentAssignments.size > 5) {
181
186
  recommendations.push('Consider reducing the number of agents for better coordination');
182
187
  }
@@ -186,9 +191,10 @@ export class TaskHandler {
186
191
  }
187
192
  }
188
193
  return {
189
- success: failedTasks === 0,
194
+ success: failedTasks === 0 && deferredTasks === 0,
190
195
  completedTasks,
191
196
  failedTasks,
197
+ deferredTasks,
192
198
  agentUtilization,
193
199
  bottlenecks,
194
200
  recommendations,
@@ -232,9 +238,10 @@ export class TaskHandler {
232
238
  **Session ID:** ${result.sessionId}
233
239
  **Success:** ${result.success ? '✅ COMPLETED' : '❌ FAILED'}
234
240
  **Duration:** ${result.duration}ms
235
- **Tasks:** ${result.completedTasks + result.failedTasks} total
241
+ **Tasks:** ${result.completedTasks + result.failedTasks + (result.deferredTasks ?? 0)} total
236
242
  - ✅ Completed: ${result.completedTasks}
237
243
  - ❌ Failed: ${result.failedTasks}
244
+ - ⏸ Deferred (host delegation required): ${result.deferredTasks ?? 0}
238
245
 
239
246
  **Agent Utilization:**
240
247
  ${Object.entries(result.agentUtilization)
@@ -117,7 +117,15 @@ export class OrchestratorServer {
117
117
  description: { type: 'string' },
118
118
  type: { type: 'string' },
119
119
  files: { type: 'array', items: { type: 'string' } },
120
- dependencies: { type: 'number' },
120
+ dependencyCount: {
121
+ type: 'number',
122
+ description: 'Upstream dependency count (complexity hint only)',
123
+ },
124
+ dependencies: {
125
+ type: 'array',
126
+ items: { type: 'string' },
127
+ description: 'Task IDs this task depends on',
128
+ },
121
129
  riskLevel: {
122
130
  type: 'string',
123
131
  enum: ['low', 'medium', 'high', 'critical'],
@@ -27,7 +27,8 @@ export interface OrchestrationTask {
27
27
  description: string;
28
28
  type: string;
29
29
  priority?: 'critical' | 'high' | 'medium' | 'low';
30
- dependencies?: string[];
30
+ /** Task IDs (orchestrate-task) or dependency count hint (analyze-complexity) */
31
+ dependencies?: string[] | number;
31
32
  estimatedComplexity?: number;
32
33
  metadata?: OrchestrationTaskMetadata;
33
34
  }
@@ -43,6 +44,8 @@ export interface OrchestrationResult {
43
44
  success: boolean;
44
45
  completedTasks: number;
45
46
  failedTasks: number;
47
+ /** Implementation agents not executed by orchestrate-task — lead must delegate via host Task */
48
+ deferredTasks?: number;
46
49
  duration: number;
47
50
  agentUtilization: Record<string, number>;
48
51
  agentOutputs?: Record<string, string>;
@@ -397,7 +397,7 @@ class XrayLibrarianServer extends XrayKnowledgeSkillBase {
397
397
  if (!vote) {
398
398
  const abstainReason = memoryContext?.confidence.highConfidenceTrapPresent
399
399
  ? "No LLM governance provider configured. Repertoire detected a high-confidence ontological trap — route to the recommended agent before proceeding."
400
- : "No LLM governance provider configured. Set XRAY_GOVERNANCE_LLM_ENABLED=true + XRAY_LLM_ENDPOINT, or install Hermes and run: hermes auth add xai-oauth --no-browser";
400
+ : "No LLM governance provider configured. Install Hermes on PATH (hermes -z) with xai-oauth, or set XRAY_GOVERNANCE_LLM_ENABLED=true + XRAY_LLM_ENDPOINT.";
401
401
  return {
402
402
  content: [
403
403
  {
@@ -18,6 +18,10 @@ export interface LeadDevPhase {
18
18
  definitionOfDone: string;
19
19
  todos: LeadDevTodo[];
20
20
  }
21
+ export interface LeadDevPlanTaskInput {
22
+ description: string;
23
+ type?: string;
24
+ }
21
25
  export interface LeadDevPlan {
22
26
  active: boolean;
23
27
  rules: readonly string[];
@@ -37,7 +41,7 @@ export interface LeadDevPlan {
37
41
  export declare function isLeadDevModeActive(): boolean;
38
42
  export declare function routeSubagent(taskType: string): SubagentRoute;
39
43
  export declare function shouldFlagFullTestSuite(command: string): boolean;
40
- export declare function buildLeadDevPlan(description: string, taskTypes?: string[]): LeadDevPlan | null;
44
+ export declare function buildLeadDevPlan(description: string, taskTypes?: string[], taskInputs?: LeadDevPlanTaskInput[]): LeadDevPlan | null;
41
45
  export declare function buildSessionBootContext(): Record<string, unknown>;
42
46
  /** Persist plan for PreToolUse spawn_subagent gate */
43
47
  export declare function persistLeadDevPlan(plan: LeadDevPlan, projectRoot?: string): string;
@@ -57,14 +57,30 @@ export function shouldFlagFullTestSuite(command) {
57
57
  /\btest:\w+/.test(cmd);
58
58
  return isTest && !hasFocusedTarget;
59
59
  }
60
- export function buildLeadDevPlan(description, taskTypes = ['implement']) {
60
+ function buildImplementationTodos(description, taskTypes, taskInputs, idPrefix) {
61
+ if (taskInputs.length > 0) {
62
+ return taskInputs.map((t, i) => ({
63
+ id: `${idPrefix}.${i + 1}`,
64
+ task: t.description.slice(0, 200),
65
+ subagent: routeSubagent(t.type ?? 'implement'),
66
+ status: 'pending',
67
+ }));
68
+ }
69
+ return taskTypes.map((type, i) => ({
70
+ id: `${idPrefix}.${i + 1}`,
71
+ task: description.slice(0, 200),
72
+ subagent: routeSubagent(type),
73
+ status: 'pending',
74
+ }));
75
+ }
76
+ export function buildLeadDevPlan(description, taskTypes = ['implement'], taskInputs = []) {
61
77
  if (!isLeadDevModeActive())
62
78
  return null;
63
79
  const cfg = orchestrationConfig();
64
80
  const threshold = cfg.phased_plan_threshold ?? 25;
65
81
  const score = scoreComplexity(description, { taskTypes });
66
82
  const complexity = score.score;
67
- const requiresPhasedPlan = complexity > threshold;
83
+ const requiresPhasedPlan = complexity > threshold || taskInputs.length > 1;
68
84
  const mandatoryConsults = cfg.auto_consult_major_work !== false && requiresPhasedPlan
69
85
  ? [...MANDATORY_MAJOR_CONSULTS]
70
86
  : [];
@@ -87,12 +103,7 @@ export function buildLeadDevPlan(description, taskTypes = ['implement']) {
87
103
  name: 'Implementation',
88
104
  goal: 'Execute with best subagent per todo',
89
105
  definitionOfDone: 'Affected per-suite tests green',
90
- todos: taskTypes.map((type, i) => ({
91
- id: `2.${i + 1}`,
92
- task: description.slice(0, 200),
93
- subagent: routeSubagent(type),
94
- status: 'pending',
95
- })),
106
+ todos: buildImplementationTodos(description, taskTypes, taskInputs, '2'),
96
107
  },
97
108
  {
98
109
  id: 'phase-3',
@@ -121,14 +132,7 @@ export function buildLeadDevPlan(description, taskTypes = ['implement']) {
121
132
  name: 'Direct execution',
122
133
  goal: description.slice(0, 120),
123
134
  definitionOfDone: 'Task complete with tests green',
124
- todos: [
125
- {
126
- id: '1.1',
127
- task: description.slice(0, 200),
128
- subagent: routeSubagent(taskTypes[0] ?? 'implement'),
129
- status: 'pending',
130
- },
131
- ],
135
+ todos: buildImplementationTodos(description, taskTypes, taskInputs, '1'),
132
136
  },
133
137
  ];
134
138
  return {
@@ -36,8 +36,9 @@ When `multi_agent_orchestration.lead_dev_mode` is true in `features.json` (defau
36
36
 
37
37
  | Step | MCP tool | Notes |
38
38
  |------|----------|-------|
39
- | **Intake + classify** | `analyze-complexity` | Pass `tasks` array; response includes **lead-dev plan JSON** when mode is on |
40
- | Delegate | `orchestrate-task` | Include agent + sessionId (codex 67) |
39
+ | **Intake + classify** | `analyze-complexity` | Pass `tasks` array; persists `.xray/state/lead-dev-plan.json` when mode is on |
40
+ | Delegate (consult) | `orchestrate-task` | Invokes MCP consult skills (code-review, researcher, security-audit, etc.) |
41
+ | Delegate (implement) | **host `Task` / `spawn_subagent`** | `orchestrate-task` **defers** backend-engineer, frontend-engineer, bug-triage — lead must spawn |
41
42
  | Monitor | `get-orchestration-status` | Lead updates todos |
42
43
  | Major work | researcher + architect-tools + code-review | Auto-listed in plan when complexity > threshold |
43
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "3.4.7",
3
+ "version": "3.4.9",
4
4
  "description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -36,18 +36,26 @@ function main() {
36
36
 
37
37
  try {
38
38
  if (verifyOnly) {
39
- step("1/4 Git + reconcile", "node scripts/node/pre-publish-guard.js --verify-only");
39
+ step("1/5 Git + reconcile", "node scripts/node/pre-publish-guard.js --verify-only");
40
40
  if (!skipDocs) {
41
- step("2/4 Release docs", "node scripts/node/validate-release-docs.mjs");
41
+ step("2/5 Release docs", "node scripts/node/validate-release-docs.mjs");
42
42
  }
43
+ step(
44
+ "3/5 Plugin infrastructure",
45
+ "node scripts/test/validate-plugins-e2e.cjs --structural-only",
46
+ );
43
47
  if (!skipSmoke) {
44
- step("3/4 Consumer install smoke", "node scripts/node/consumer-install-smoke.mjs");
48
+ step("4/5 Consumer install smoke", "node scripts/node/consumer-install-smoke.mjs");
45
49
  }
46
50
  } else {
47
- step("1/4 Build", "npm run build");
48
- step("2/4 Tests", "npm test");
51
+ step("1/5 Build", "npm run build");
52
+ step("2/5 Tests", "npm test");
53
+ step(
54
+ "3/5 Plugin infrastructure",
55
+ "node scripts/test/validate-plugins-e2e.cjs --structural-only",
56
+ );
49
57
  if (!skipSmoke) {
50
- step("3/4 Consumer install smoke (fresh + upgrade)", "node scripts/node/consumer-install-smoke.mjs");
58
+ step("4/5 Consumer install smoke (fresh + upgrade)", "node scripts/node/consumer-install-smoke.mjs");
51
59
  }
52
60
  }
53
61
 
@@ -18,6 +18,7 @@ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js
18
18
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
19
19
  import {
20
20
  CallToolRequestSchema,
21
+ isInitializeRequest,
21
22
  type CallToolResult,
22
23
  } from "@modelcontextprotocol/sdk/types.js";
23
24
  import { XrayKnowledgeSkillBase } from "./shared/knowledge-skill-base.js";
@@ -419,6 +420,7 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
419
420
 
420
421
  /**
421
422
  * Run as HTTP server using Streamable HTTP transport (for Grok CLI compatibility).
423
+ * Uses per-session transports so concurrent clients can each initialize independently.
422
424
  */
423
425
  async runHttp(port: number = parseInt(process.env.MCP_PORT ?? "3100", 10)): Promise<void> {
424
426
  await this.initializeGovernance();
@@ -429,11 +431,7 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
429
431
  host: mcpHost,
430
432
  ...(allowedHosts ? { allowedHosts } : {}),
431
433
  });
432
- const transport = new StreamableHTTPServerTransport({
433
- sessionIdGenerator: () => randomUUID(),
434
- });
435
-
436
- await this.server.connect(transport as any);
434
+ const transports: Record<string, StreamableHTTPServerTransport> = {};
437
435
 
438
436
  const apiKey = process.env.GOVERNANCE_API_KEY;
439
437
  if (apiKey) {
@@ -455,8 +453,45 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
455
453
  next();
456
454
  });
457
455
 
458
- app.post("/mcp", async (req: any, res: any) => {
456
+ const mcpPostHandler = async (req: any, res: any) => {
459
457
  try {
458
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
459
+ let transport: StreamableHTTPServerTransport | undefined;
460
+
461
+ if (sessionId && transports[sessionId]) {
462
+ transport = transports[sessionId];
463
+ } else if (!sessionId && isInitializeRequest(req.body)) {
464
+ transport = new StreamableHTTPServerTransport({
465
+ sessionIdGenerator: () => randomUUID(),
466
+ onsessioninitialized: (sid) => {
467
+ if (transport) {
468
+ transports[sid] = transport;
469
+ frameworkLogger.log("governance-mcp", "session-initialized", "info", {
470
+ sessionId: sid,
471
+ });
472
+ }
473
+ },
474
+ });
475
+ transport.onclose = () => {
476
+ const sid = transport?.sessionId;
477
+ if (sid && transports[sid]) {
478
+ delete transports[sid];
479
+ frameworkLogger.log("governance-mcp", "session-closed", "info", { sessionId: sid });
480
+ }
481
+ };
482
+ const sessionServer = new GovernanceServer();
483
+ await sessionServer.connect(transport as Transport);
484
+ await transport.handleRequest(req, res, req.body);
485
+ return;
486
+ } else {
487
+ res.status(400).json({
488
+ jsonrpc: "2.0",
489
+ error: { code: -32000, message: "Bad Request: No valid session ID provided" },
490
+ id: null,
491
+ });
492
+ return;
493
+ }
494
+
460
495
  await transport.handleRequest(req, res, req.body);
461
496
  } catch (error) {
462
497
  frameworkLogger.log("governance-mcp", "http-handler-error", "error", { error: String(error) });
@@ -464,18 +499,56 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
464
499
  res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal error" }, id: null });
465
500
  }
466
501
  }
502
+ };
503
+
504
+ app.post("/mcp", mcpPostHandler);
505
+
506
+ app.get("/mcp", async (req: any, res: any) => {
507
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
508
+ if (!sessionId || !transports[sessionId]) {
509
+ res.status(400).send("Invalid or missing session ID");
510
+ return;
511
+ }
512
+ try {
513
+ await transports[sessionId].handleRequest(req, res);
514
+ } catch (error) {
515
+ frameworkLogger.log("governance-mcp", "http-sse-error", "error", { error: String(error) });
516
+ if (!res.headersSent) {
517
+ res.status(500).send("Error processing SSE stream");
518
+ }
519
+ }
520
+ });
521
+
522
+ app.delete("/mcp", async (req: any, res: any) => {
523
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
524
+ if (!sessionId || !transports[sessionId]) {
525
+ res.status(400).send("Invalid or missing session ID");
526
+ return;
527
+ }
528
+ try {
529
+ await transports[sessionId].handleRequest(req, res);
530
+ } catch (error) {
531
+ frameworkLogger.log("governance-mcp", "http-delete-error", "error", { error: String(error) });
532
+ if (!res.headersSent) {
533
+ res.status(500).send("Error processing session termination");
534
+ }
535
+ }
467
536
  });
468
537
 
469
538
  app.get("/health", (_req: any, res: any) => {
470
539
  res.json({ status: "ok", server: "governance" });
471
540
  });
472
541
 
542
+ const closeAllTransports = async () => {
543
+ await Promise.all(Object.values(transports).map((t) => t.close()));
544
+ };
545
+
473
546
  app.listen(port, () => {
474
547
  frameworkLogger.log("governance-mcp", "http-listening", "info", { port });
475
548
  });
476
549
 
477
- process.on("SIGINT", () => { transport.close(); process.exit(0); });
478
- process.on("SIGTERM", () => { transport.close(); process.exit(0); });
550
+ process.on("SIGINT", () => { void closeAllTransports().then(() => process.exit(0)); });
551
+ process.on("SIGTERM", () => { void closeAllTransports().then(() => process.exit(0)); });
479
552
  }
480
553
  }
481
554
 
@@ -448,7 +448,7 @@ class XrayCodeReviewServer extends XrayKnowledgeSkillBase {
448
448
  content: [
449
449
  {
450
450
  type: "text",
451
- text: "DECISION: abstain\nCONFIDENCE: 0.50\nREASONING: No LLM governance provider configured. Set XRAY_GOVERNANCE_LLM_ENABLED=true + XRAY_LLM_ENDPOINT, or install Hermes and run: hermes auth add xai-oauth --no-browser",
451
+ text: "DECISION: abstain\nCONFIDENCE: 0.50\nREASONING: No LLM governance provider configured. Install Hermes on PATH (hermes -z) with xai-oauth, or set XRAY_GOVERNANCE_LLM_ENABLED=true + XRAY_LLM_ENDPOINT.",
452
452
  },
453
453
  ],
454
454
  };
@@ -1008,7 +1008,7 @@ class XraySecurityAuditServer extends XrayKnowledgeSkillBase {
1008
1008
  content: [
1009
1009
  {
1010
1010
  type: "text",
1011
- text: "DECISION: abstain\nCONFIDENCE: 0.50\nREASONING: No LLM governance provider configured. Set XRAY_GOVERNANCE_LLM_ENABLED=true + XRAY_LLM_ENDPOINT, or install Hermes and run: hermes auth add xai-oauth --no-browser",
1011
+ text: "DECISION: abstain\nCONFIDENCE: 0.50\nREASONING: No LLM governance provider configured. Install Hermes on PATH (hermes -z) with xai-oauth, or set XRAY_GOVERNANCE_LLM_ENABLED=true + XRAY_LLM_ENDPOINT.",
1012
1012
  },
1013
1013
  ],
1014
1014
  };
@@ -39,7 +39,9 @@ export function toMemoryTask(task: OrchestrationTask): MemoryOrchestrationTask {
39
39
  type: task.type,
40
40
  };
41
41
  if (task.priority) result.priority = task.priority;
42
- if (task.dependencies) result.dependencies = task.dependencies;
42
+ if (Array.isArray(task.dependencies) && task.dependencies.length > 0) {
43
+ result.dependencies = task.dependencies;
44
+ }
43
45
  if (task.estimatedComplexity !== undefined) result.estimatedComplexity = task.estimatedComplexity;
44
46
  if (task.metadata) result.metadata = task.metadata as Record<string, unknown>;
45
47
  return result;