0xray 3.5.0 → 3.5.2

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 (60) hide show
  1. package/.gitignore.default +8 -0
  2. package/.opencode/hooks/post-commit +7 -27
  3. package/.xray/features.json +6 -0
  4. package/.xray/features.schema.json +17 -0
  5. package/AGENTS-consumer.md +1 -1
  6. package/AGENTS.md +1 -1
  7. package/README.md +1 -1
  8. package/SKILLS.md +1 -1
  9. package/dist/.gitignore.default +8 -0
  10. package/dist/AGENTS.md +1 -1
  11. package/dist/CHANGELOG.md +32 -0
  12. package/dist/README.md +1 -1
  13. package/dist/SKILLS.md +1 -1
  14. package/dist/core/bridge.mjs +10 -32
  15. package/dist/core/features-config.d.ts +16 -0
  16. package/dist/core/features-config.js +24 -0
  17. package/dist/integrations/grok/hooks/grok-hook-utils.js +32 -385
  18. package/dist/integrations/grok/hooks/post-tool-use.js +1 -1
  19. package/dist/integrations/grok/hooks/pre-tool-use.js +23 -32
  20. package/dist/integrations/grok/hooks/session-start.js +20 -1
  21. package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +2 -1
  22. package/dist/integrations/hermes-agent/__init__.py +50 -3
  23. package/dist/integrations/hermes-agent/bridge.mjs +71 -30
  24. package/dist/integrations/hooks/delegation-gate-runtime.mjs +75 -0
  25. package/dist/integrations/hooks/synthesis-hook-runtime.mjs +50 -0
  26. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +3 -0
  27. package/dist/mcps/orchestrator/handlers/complexity-handler.js +25 -10
  28. package/dist/mcps/orchestrator/server.js +124 -4
  29. package/dist/memory-routing/types.d.ts +7 -0
  30. package/dist/nucleus/autonomy-kernel.d.ts +3 -0
  31. package/dist/nucleus/autonomy-kernel.js +40 -0
  32. package/dist/nucleus/delegation-gate.d.ts +88 -0
  33. package/dist/nucleus/delegation-gate.js +347 -0
  34. package/dist/nucleus/lead-dev-plan-persistence.d.ts +3 -0
  35. package/dist/nucleus/lead-dev-plan-persistence.js +47 -4
  36. package/dist/nucleus/synthesis-completion.d.ts +6 -0
  37. package/dist/nucleus/synthesis-completion.js +29 -0
  38. package/dist/nucleus/synthesis-context.d.ts +31 -0
  39. package/dist/nucleus/synthesis-context.js +86 -0
  40. package/dist/nucleus/synthesis.d.ts +54 -0
  41. package/dist/nucleus/synthesis.js +196 -0
  42. package/dist/plugin/xray-codex-injection.js +50 -0
  43. package/package.json +11 -2
  44. package/scripts/helpers/find-project-root.mjs +92 -0
  45. package/scripts/hooks/install-hooks.cjs +136 -36
  46. package/scripts/hooks/run-hook.js +46 -9
  47. package/scripts/mjs/delegation-gate-fixture.mjs +121 -0
  48. package/scripts/mjs/verify-delegation-gate-core.mjs +99 -0
  49. package/scripts/mjs/verify-grok-delegation-gate.mjs +9 -69
  50. package/scripts/mjs/verify-hermes-delegation-gate.mjs +89 -0
  51. package/scripts/node/auto-reflection-generator.mjs +7 -0
  52. package/scripts/node/consumer-gitignore.cjs +101 -0
  53. package/scripts/node/load-reflection-config.mjs +77 -0
  54. package/scripts/node/postinstall.cjs +9 -6
  55. package/scripts/node/universal-version-manager.js +1 -1
  56. package/src/integrations/grok/plugin/0xray/hooks/hooks.json +2 -1
  57. package/src/mcps/orchestrator/handlers/complexity-handler.ts +33 -8
  58. package/src/mcps/orchestrator/server.ts +181 -5
  59. package/xray/features.json +6 -0
  60. package/xray/features.schema.json +17 -0
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hermes bridge delegation gate fixture (4/4) — uses bridge delegation-gate command.
4
+ */
5
+ import { execSync } from 'node:child_process';
6
+ import { mkdtempSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import {
11
+ FIXTURE_SESSION_ID,
12
+ seedDelegationGateFixture,
13
+ seedSpawnTodoPlan,
14
+ } from './delegation-gate-fixture.mjs';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const packageRoot = join(__dirname, '../..');
18
+ const bridge = join(packageRoot, 'dist/integrations/hermes-agent/bridge.mjs');
19
+
20
+ let failed = 0;
21
+ function pass(n) {
22
+ console.log(`✅ ${n}`);
23
+ }
24
+ function fail(n, d = '') {
25
+ failed++;
26
+ console.error(`❌ ${n}${d ? ` — ${d}` : ''}`);
27
+ }
28
+
29
+ function runBridge(payload, cwd) {
30
+ const json = JSON.stringify(payload).replace(/'/g, "'\\''");
31
+ const out = execSync(`node "${bridge}" delegation-gate --cwd "${cwd}" '${json}'`, {
32
+ cwd,
33
+ encoding: 'utf8',
34
+ env: { ...process.env, XRAY_ROOT: cwd },
35
+ });
36
+ return JSON.parse(out.trim());
37
+ }
38
+
39
+ const tmp = mkdtempSync(join(tmpdir(), 'xray-hermes-gate-'));
40
+ seedDelegationGateFixture(tmp);
41
+
42
+ const base = {
43
+ command: 'delegation-gate',
44
+ phase: 'pre',
45
+ sessionId: FIXTURE_SESSION_ID,
46
+ host: 'hermes',
47
+ };
48
+
49
+ try {
50
+ const deny = runBridge({ ...base, tool: 'write_file', args: { path: 'src/a.ts', content: 'x' } }, tmp);
51
+ if (deny.allow === false && deny.gate === 'auto-chain-pending') pass('bridge denies write while pending');
52
+ else fail('write deny', JSON.stringify(deny));
53
+
54
+ const allowRead = runBridge({ ...base, tool: 'read_file', args: { path: 'src/a.ts' } }, tmp);
55
+ if (allowRead.allow === true) pass('bridge allows read while pending');
56
+ else fail('read allow', JSON.stringify(allowRead));
57
+
58
+ const allowSpawn = runBridge(
59
+ {
60
+ ...base,
61
+ tool: 'delegate_task',
62
+ args: { prompt: 'plan todo 2.1 swap deps', subagent_type: 'backend-engineer' },
63
+ },
64
+ tmp,
65
+ );
66
+ if (allowSpawn.allow === true) pass('bridge allows delegate_task while pending');
67
+ else fail('spawn allow', JSON.stringify(allowSpawn));
68
+
69
+ seedSpawnTodoPlan(tmp);
70
+ const denyTodo = runBridge(
71
+ { ...base, tool: 'delegate_task', args: { prompt: 'explore only', subagent_type: 'explore' } },
72
+ tmp,
73
+ );
74
+ if (denyTodo.allow === false && denyTodo.gate === 'spawn-todo-persistence') {
75
+ pass('bridge denies spawn skipping required plan todo');
76
+ } else {
77
+ fail('spawn todo persistence', JSON.stringify(denyTodo));
78
+ }
79
+ } finally {
80
+ rmSync(tmp, { recursive: true, force: true });
81
+ }
82
+
83
+ console.log(
84
+ '\n' +
85
+ (failed === 0
86
+ ? '🎉 Hermes delegation gate verify passed (4/4).'
87
+ : `⚠️ ${failed} Hermes gate check(s) failed.`),
88
+ );
89
+ process.exit(failed === 0 ? 0 : 1);
@@ -113,6 +113,13 @@ function loadConfig() {
113
113
  try {
114
114
  const content = readFileSync(configPath, "utf-8");
115
115
  const parsed = JSON.parse(content);
116
+ if (parsed.synthesis?.reflection) {
117
+ return {
118
+ mode: parsed.synthesis.reflection.mode ?? "minimal",
119
+ triggers: parsed.synthesis.reflection.triggers,
120
+ thresholds: parsed.synthesis.reflection.thresholds,
121
+ };
122
+ }
116
123
  if (parsed.auto_reflection) {
117
124
  return parsed.auto_reflection;
118
125
  }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Consumer .gitignore — template + idempotent merge into existing .gitignore.
3
+ */
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const MARKER_START = "# --- 0xray suit (postinstall) ---";
8
+ const MARKER_END = "# --- end 0xray suit ---";
9
+
10
+ /** Suit artifacts dropped by postinstall / install-bridges — keep out of product repos */
11
+ const SUIT_IGNORE_LINES = [
12
+ ".grok/",
13
+ ".mcp.json",
14
+ "opencode.json",
15
+ "AGENTS.md",
16
+ "SKILLS.md",
17
+ ".xray/",
18
+ ".strray/",
19
+ "strray/",
20
+ ".hermes/",
21
+ ".opencode/agents/",
22
+ ".opencode/commands/",
23
+ ".opencode/workflows/",
24
+ ".opencode/codex.codex",
25
+ ".opencode/enforcer-config.json",
26
+ ".opencode/skills/",
27
+ ".opencode/scripts/",
28
+ ".opencode/logs/",
29
+ ".opencode/validation/",
30
+ ".opencode/triage/",
31
+ "logs/framework/",
32
+ ];
33
+
34
+ function buildMarkerBlock(lines = SUIT_IGNORE_LINES) {
35
+ return [MARKER_START, ...lines, MARKER_END].join("\n");
36
+ }
37
+
38
+ function stripMarkerBlock(content) {
39
+ const start = content.indexOf(MARKER_START);
40
+ if (start === -1) return content;
41
+ const end = content.indexOf(MARKER_END);
42
+ if (end === -1) {
43
+ // Incomplete marker — preserve content before START; orphan tail handled in merge
44
+ return content.slice(0, start).trimEnd();
45
+ }
46
+ const tail = content.slice(end + MARKER_END.length);
47
+ return `${content.slice(0, start).trimEnd()}${tail}`.trimEnd();
48
+ }
49
+
50
+ function markerBlockIsCurrent(raw) {
51
+ if (!raw.includes(MARKER_START) || !raw.includes(MARKER_END)) {
52
+ return false;
53
+ }
54
+ const start = raw.indexOf(MARKER_START);
55
+ const end = raw.indexOf(MARKER_END) + MARKER_END.length;
56
+ const blockSection = raw.slice(start, end);
57
+ return SUIT_IGNORE_LINES.every((line) => blockSection.includes(line));
58
+ }
59
+
60
+ /**
61
+ * @returns {'created' | 'merged' | 'unchanged'}
62
+ */
63
+ function applyConsumerGitignore(targetDir, packageRoot) {
64
+ const gitignoreSource = path.join(packageRoot, ".gitignore.default");
65
+ const gitignoreDest = path.join(targetDir, ".gitignore");
66
+
67
+ if (!fs.existsSync(gitignoreSource)) {
68
+ return "unchanged";
69
+ }
70
+
71
+ if (!fs.existsSync(gitignoreDest)) {
72
+ fs.copyFileSync(gitignoreSource, gitignoreDest);
73
+ return "created";
74
+ }
75
+
76
+ const raw = fs.readFileSync(gitignoreDest, "utf8");
77
+ if (markerBlockIsCurrent(raw)) {
78
+ return "unchanged";
79
+ }
80
+
81
+ const hasIncompleteMarker =
82
+ raw.includes(MARKER_START) && !raw.includes(MARKER_END);
83
+ const orphanTail = hasIncompleteMarker
84
+ ? raw.slice(raw.indexOf(MARKER_START)).trimEnd()
85
+ : "";
86
+ const content = stripMarkerBlock(raw);
87
+ const next = orphanTail
88
+ ? `${content.trimEnd()}\n\n${buildMarkerBlock()}\n\n${orphanTail}\n`
89
+ : `${content.trimEnd()}\n\n${buildMarkerBlock()}\n`;
90
+ fs.writeFileSync(gitignoreDest, next);
91
+ return "merged";
92
+ }
93
+
94
+ module.exports = {
95
+ MARKER_START,
96
+ MARKER_END,
97
+ SUIT_IGNORE_LINES,
98
+ buildMarkerBlock,
99
+ stripMarkerBlock,
100
+ applyConsumerGitignore,
101
+ };
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Emit reflection config for post-commit hook.
4
+ * SSOT: synthesis.reflection, fallback auto_reflection.
5
+ *
6
+ * Usage:
7
+ * node load-reflection-config.mjs # shell-safe export lines
8
+ * node load-reflection-config.mjs --json # JSON for hook parsers
9
+ */
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ const ALLOWED_MODES = new Set(['minimal', 'full', 'off']);
14
+
15
+ const root = process.env.PROJECT_ROOT || process.cwd();
16
+ const configPath = join(root, '.xray', 'features.json');
17
+
18
+ let mode = 'minimal';
19
+ let commitThreshold = 25;
20
+ let daysThreshold = 14;
21
+ let autoGenerate = true;
22
+ let promptUser = true;
23
+
24
+ if (existsSync(configPath)) {
25
+ try {
26
+ const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
27
+ const reflection = parsed.synthesis?.reflection ?? parsed.auto_reflection;
28
+ if (reflection) {
29
+ const rawMode = reflection.mode ?? 'minimal';
30
+ mode = ALLOWED_MODES.has(rawMode) ? rawMode : 'minimal';
31
+ const thresholds = reflection.thresholds?.[mode];
32
+ if (thresholds) {
33
+ if (typeof thresholds.commit_threshold === 'number') {
34
+ commitThreshold = thresholds.commit_threshold;
35
+ }
36
+ if (typeof thresholds.days_threshold === 'number') {
37
+ daysThreshold = thresholds.days_threshold;
38
+ }
39
+ if (typeof thresholds.auto_generate === 'boolean') {
40
+ autoGenerate = thresholds.auto_generate;
41
+ }
42
+ if (typeof thresholds.prompt_user === 'boolean') {
43
+ promptUser = thresholds.prompt_user;
44
+ }
45
+ } else if (mode === 'full') {
46
+ commitThreshold = 10;
47
+ daysThreshold = 5;
48
+ }
49
+ }
50
+ } catch {
51
+ /* keep defaults */
52
+ }
53
+ }
54
+
55
+ if (mode === 'off') {
56
+ autoGenerate = false;
57
+ promptUser = false;
58
+ }
59
+
60
+ const config = {
61
+ mode,
62
+ commitThreshold,
63
+ daysThreshold,
64
+ autoGenerate,
65
+ promptUser,
66
+ };
67
+
68
+ if (process.argv.includes('--json')) {
69
+ console.log(JSON.stringify(config));
70
+ process.exit(0);
71
+ }
72
+
73
+ console.log(`MODE=${mode}`);
74
+ console.log(`COMMIT_THRESHOLD=${commitThreshold}`);
75
+ console.log(`DAYS_THRESHOLD=${daysThreshold}`);
76
+ console.log(`AUTO_GENERATE=${autoGenerate}`);
77
+ console.log(`PROMPT_USER=${promptUser}`);
@@ -7,6 +7,7 @@ const {
7
7
  resolveConsumerTargetDir,
8
8
  isConsumerInstall,
9
9
  } = require("./install-bridges.cjs");
10
+ const { applyConsumerGitignore } = require("./consumer-gitignore.cjs");
10
11
 
11
12
  function structuredLog(component, action, status, details) {
12
13
  const ts = new Date().toISOString();
@@ -39,12 +40,14 @@ if (fs.existsSync(agentsConsumer) && isConsumerInstall(resolvedPackage, resolved
39
40
  }
40
41
  }
41
42
 
42
- // Copy .gitignore.default → .gitignore (never overwrite existing)
43
- const gitignoreSource = path.join(packageRoot, ".gitignore.default");
44
- const gitignoreDest = path.join(targetDir, ".gitignore");
45
- if (!fs.existsSync(gitignoreDest) && isConsumerInstall(resolvedPackage, resolvedTarget)) {
46
- fs.copyFileSync(gitignoreSource, gitignoreDest);
47
- structuredLog("postinstall", "Created .gitignore from template", "info");
43
+ // .gitignore: full template when absent; merge suit block when existing
44
+ if (isConsumerInstall(resolvedPackage, resolvedTarget)) {
45
+ const gitignoreResult = applyConsumerGitignore(targetDir, packageRoot);
46
+ if (gitignoreResult === "created") {
47
+ structuredLog("postinstall", "Created .gitignore from template", "info");
48
+ } else if (gitignoreResult === "merged") {
49
+ structuredLog("postinstall", "Merged 0xray suit entries into .gitignore", "info");
50
+ }
48
51
  }
49
52
 
50
53
  // Unified 4-platform bridge install (OpenCode, Grok, Hermes, OpenClaw)
@@ -152,7 +152,7 @@ const OFFICIAL_VERSIONS = {
152
152
  framework: {
153
153
  version: PACKAGE_VERSION,
154
154
  displayName: "xray: Self-Healing AI Governance OS",
155
- lastUpdated: "2026-06-17",
155
+ lastUpdated: "2026-06-19",
156
156
  // Counts (auto-calculated, but can be overridden)
157
157
  ...CALCULATED_COUNTS,
158
158
  },
@@ -36,7 +36,8 @@
36
36
  "type": "command",
37
37
  "command": "node",
38
38
  "args": [
39
- "${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/grok/hooks/session-start.js"
39
+ "${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/grok/hooks/session-start.js",
40
+ "--hook-event=user_prompt_submit"
40
41
  ],
41
42
  "timeout": 15000
42
43
  }
@@ -7,6 +7,7 @@
7
7
  import { frameworkLogger } from '../../../core/framework-logger.js';
8
8
  import {
9
9
  buildLeadDevPlan,
10
+ buildSynthesisCheckpointPlan,
10
11
  isLeadDevModeActive,
11
12
  persistLeadDevPlan,
12
13
  } from '../../../nucleus/autonomy-kernel.js';
@@ -67,9 +68,14 @@ export class ComplexityHandler {
67
68
  * Handle analyze-complexity request
68
69
  */
69
70
  async handleAnalyzeComplexity(
70
- args: { tasks: AnalyzeComplexityInput[]; sessionId?: string },
71
+ args: {
72
+ tasks: AnalyzeComplexityInput[];
73
+ sessionId?: string;
74
+ synthesisCheckpoint?: boolean;
75
+ synthesisDueReason?: string | null;
76
+ },
71
77
  asideId?: string,
72
- ): Promise<{ content: Array<{ type: string; text: string }> }> {
78
+ ): Promise<{ content: Array<{ type: string; text: string }>; ok: boolean }> {
73
79
  const tasks = normalizeAnalyzeTasks(args.tasks ?? []);
74
80
 
75
81
  await frameworkLogger.log(
@@ -92,8 +98,10 @@ export class ComplexityHandler {
92
98
  const planTaskInputs = tasks
93
99
  .filter((t) => t.description?.trim())
94
100
  .map((t) => ({ description: t.description, type: t.type }));
95
- const leadDevPlan =
96
- isLeadDevModeActive() && (primaryDescription || planTaskInputs.length > 0)
101
+ const synthesisCheckpoint = args.synthesisCheckpoint === true;
102
+ const leadDevPlan = synthesisCheckpoint
103
+ ? buildSynthesisCheckpointPlan(args.synthesisDueReason ?? null)
104
+ : isLeadDevModeActive() && (primaryDescription || planTaskInputs.length > 0)
97
105
  ? buildLeadDevPlan(
98
106
  primaryDescription,
99
107
  taskTypes.length ? taskTypes : ['implement'],
@@ -102,15 +110,20 @@ export class ComplexityHandler {
102
110
  )
103
111
  : null;
104
112
 
113
+ let planPersisted = synthesisCheckpoint ? leadDevPlan !== null : !leadDevPlan;
114
+
105
115
  if (leadDevPlan) {
106
116
  try {
107
117
  const sessionId = args.sessionId ?? `analyze-${Date.now()}`;
108
- clearPendingDelegations();
118
+ if (!synthesisCheckpoint) {
119
+ clearPendingDelegations();
120
+ }
109
121
  const planPath = persistLeadDevPlan(leadDevPlan);
110
122
  const bound = bindPlanToSession(sessionId);
123
+ planPersisted = true;
111
124
  await frameworkLogger.log(
112
125
  'orchestrator.server',
113
- 'lead-dev-plan-persisted',
126
+ synthesisCheckpoint ? 'synthesis-plan-persisted' : 'lead-dev-plan-persisted',
114
127
  'info',
115
128
  {
116
129
  planPath,
@@ -118,19 +131,24 @@ export class ComplexityHandler {
118
131
  planGeneration: bound?.planGeneration,
119
132
  phaseCount: leadDevPlan.phases.length,
120
133
  todoCount: leadDevPlan.phases.reduce((n, p) => n + p.todos.length, 0),
134
+ synthesisCheckpoint,
121
135
  },
122
136
  );
123
137
  } catch (err) {
138
+ planPersisted = false;
124
139
  await frameworkLogger.log(
125
140
  'orchestrator.server',
126
- 'lead-dev-plan-persist-failed',
141
+ synthesisCheckpoint ? 'synthesis-plan-persist-failed' : 'lead-dev-plan-persist-failed',
127
142
  'warning',
128
143
  { error: err instanceof Error ? err.message : String(err) },
129
144
  );
130
145
  }
131
146
  }
132
147
 
148
+ const ok = planPersisted;
149
+
133
150
  return {
151
+ ok,
134
152
  content: [
135
153
  {
136
154
  type: 'text',
@@ -139,12 +157,14 @@ export class ComplexityHandler {
139
157
  recommendations,
140
158
  tasks.length,
141
159
  leadDevPlan,
160
+ synthesisCheckpoint,
142
161
  ),
143
162
  },
144
163
  ],
145
164
  };
146
165
  } catch (error) {
147
166
  return {
167
+ ok: false,
148
168
  content: [
149
169
  {
150
170
  type: 'text',
@@ -195,6 +215,7 @@ export class ComplexityHandler {
195
215
  recommendations: string[],
196
216
  taskCount: number,
197
217
  leadDevPlan: ReturnType<typeof buildLeadDevPlan> = null,
218
+ synthesisCheckpoint = false,
198
219
  ): string {
199
220
  const leadDevSection = leadDevPlan
200
221
  ? `
@@ -216,7 +237,11 @@ ${JSON.stringify(leadDevPlan, null, 2)}
216
237
  \`\`\``
217
238
  : '';
218
239
 
219
- return `🔍 Complexity Analysis Results
240
+ const header = synthesisCheckpoint
241
+ ? '🔄 Synthesis Checkpoint — Reflect & Realign'
242
+ : '🔍 Complexity Analysis Results';
243
+
244
+ return `${header}
220
245
 
221
246
  **Tasks Analyzed:** ${taskCount}
222
247
  **Overall Complexity:** ${analysis.overallComplexity}/100
@@ -140,10 +140,15 @@ export class OrchestratorServer {
140
140
  {
141
141
  name: 'analyze-complexity',
142
142
  description:
143
- 'Analyze task complexity and recommend orchestration strategy. When lead_dev_mode is on (features.json), includes phased plan + todos + subagent routes.',
143
+ '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.',
144
144
  inputSchema: {
145
145
  type: 'object',
146
146
  properties: {
147
+ sessionId: {
148
+ type: 'string',
149
+ description:
150
+ 'Active session id (required to clear synthesis checkpoint; inferred from checkpoint state when omitted)',
151
+ },
147
152
  tasks: {
148
153
  type: 'array',
149
154
  items: {
@@ -276,12 +281,140 @@ export class OrchestratorServer {
276
281
  }
277
282
  }
278
283
  case 'analyze-complexity': {
279
- const complexityArgs = args as { tasks: OrchestrationTask[] };
284
+ const complexityArgs = args as {
285
+ tasks: OrchestrationTask[];
286
+ sessionId?: string;
287
+ };
288
+ const projectRoot = process.cwd();
289
+ const {
290
+ isSynthesisCheckpointDue,
291
+ getSynthesisDueReason,
292
+ getSynthesisCheckpointSessionId,
293
+ } = await import('../../nucleus/synthesis.js');
294
+ const { appendSynthesisContextToResponse, prepareSynthesisCollocatedContext } =
295
+ await import('../../nucleus/synthesis-context.js');
296
+ const {
297
+ loadPersistedLeadDevPlan,
298
+ isSynthesisRealignmentPlan,
299
+ areSynthesisConsultTodosComplete,
300
+ getSynthesisConsultTodos,
301
+ getNextRequiredTodo,
302
+ } = await import('../../nucleus/lead-dev-plan-persistence.js');
303
+
304
+ const checkpointSession = getSynthesisCheckpointSessionId(projectRoot);
305
+ if (
306
+ checkpointSession &&
307
+ complexityArgs.sessionId &&
308
+ complexityArgs.sessionId !== checkpointSession
309
+ ) {
310
+ return {
311
+ content: [
312
+ {
313
+ type: 'text' as const,
314
+ text:
315
+ `❌ Synthesis sessionId mismatch — expected \`${checkpointSession}\` ` +
316
+ `(from synthesis-checkpoint.json), got \`${complexityArgs.sessionId}\`.`,
317
+ },
318
+ ],
319
+ };
320
+ }
321
+
322
+ const sessionId = checkpointSession ?? complexityArgs.sessionId ?? null;
323
+ const synthesisDue = checkpointSession
324
+ ? isSynthesisCheckpointDue(projectRoot, checkpointSession)
325
+ : false;
326
+ const dueReason = synthesisDue
327
+ ? getSynthesisDueReason(projectRoot, sessionId)
328
+ : null;
329
+
330
+ if (synthesisDue && !sessionId) {
331
+ return {
332
+ content: [
333
+ {
334
+ type: 'text' as const,
335
+ text:
336
+ '❌ Synthesis checkpoint due but sessionId could not be resolved. ' +
337
+ 'Pass sessionId matching .xray/state/synthesis-checkpoint.json.',
338
+ },
339
+ ],
340
+ };
341
+ }
342
+
343
+ const existingPlan = loadPersistedLeadDevPlan(projectRoot);
344
+ const realignmentInProgress =
345
+ synthesisDue &&
346
+ existingPlan &&
347
+ isSynthesisRealignmentPlan(existingPlan) &&
348
+ !areSynthesisConsultTodosComplete(existingPlan);
349
+
350
+ if (realignmentInProgress) {
351
+ const collocated = await prepareSynthesisCollocatedContext(projectRoot, dueReason);
352
+ const nextTodo = getNextRequiredTodo(existingPlan);
353
+ const consultTodos = getSynthesisConsultTodos(existingPlan);
354
+ const todoList = consultTodos
355
+ .map(
356
+ (t) =>
357
+ `- [${t.status === 'completed' ? 'x' : ' '}] ${t.id} (${t.subagent}): ${t.task}`,
358
+ )
359
+ .join('\n');
360
+ let text =
361
+ '🔄 Synthesis realignment in progress — consult todos preserved (analyze-complexity no-op).\n\n' +
362
+ `**Next required todo:** ${nextTodo?.id ?? 'none'} (${nextTodo?.subagent ?? ''})\n\n` +
363
+ `${todoList}\n\n` +
364
+ 'Complete mandatory consult todos via Task spawns before other gated work.';
365
+ if (collocated.collatedText) {
366
+ text = appendSynthesisContextToResponse(
367
+ [{ type: 'text', text }],
368
+ collocated.collatedText,
369
+ )[0]!.text;
370
+ }
371
+ return { content: [{ type: 'text' as const, text }] };
372
+ }
373
+
374
+ const collocated = synthesisDue
375
+ ? await prepareSynthesisCollocatedContext(projectRoot, dueReason)
376
+ : null;
377
+ const inheritedContext = synthesisDue && collocated
378
+ ? { synthesis: true, ...collocated }
379
+ : { taskCount: complexityArgs.tasks.length };
380
+
280
381
  const aside = await spawnAside({
281
- description: `Complexity analysis: ${complexityArgs.tasks.length} tasks`,
382
+ description: synthesisDue
383
+ ? 'Synthesis checkpoint: reflect & realign'
384
+ : `Complexity analysis: ${complexityArgs.tasks.length} tasks`,
385
+ ...(sessionId ? { sessionId } : {}),
386
+ inheritedContext,
282
387
  });
283
388
  try {
284
- return await this.complexityHandler.handleAnalyzeComplexity(complexityArgs, aside.asideId);
389
+ const result = await this.complexityHandler.handleAnalyzeComplexity(
390
+ {
391
+ ...complexityArgs,
392
+ ...(sessionId ? { sessionId } : {}),
393
+ ...(synthesisDue
394
+ ? { synthesisCheckpoint: true, synthesisDueReason: dueReason }
395
+ : {}),
396
+ },
397
+ aside.asideId,
398
+ );
399
+
400
+ let content = result.content;
401
+ if (synthesisDue && collocated?.collatedText) {
402
+ content = appendSynthesisContextToResponse(content, collocated.collatedText);
403
+ }
404
+
405
+ if (synthesisDue && result.ok) {
406
+ content = content.map((block, index) =>
407
+ index === 0
408
+ ? {
409
+ ...block,
410
+ text:
411
+ `${block.text}\n\n**Synthesis checkpoint remains active** until mandatory consult todos (s.1–s.3) are completed via Task spawns.`,
412
+ }
413
+ : block,
414
+ );
415
+ }
416
+
417
+ return { content };
285
418
  } finally {
286
419
  closeAside(aside.asideId);
287
420
  }
@@ -356,13 +489,51 @@ export class OrchestratorServer {
356
489
  skipApply?: boolean;
357
490
  }): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
358
491
  try {
492
+ const projectRoot = process.cwd();
493
+ const {
494
+ isSynthesisCheckpointDue,
495
+ getSynthesisDueReason,
496
+ getSynthesisCheckpointSessionId,
497
+ recordExecutionSlice,
498
+ } = await import('../../nucleus/synthesis.js');
499
+ const {
500
+ loadPersistedLeadDevPlan,
501
+ isSynthesisRealignmentPlan,
502
+ areSynthesisConsultTodosComplete,
503
+ } = await import('../../nucleus/lead-dev-plan-persistence.js');
504
+
505
+ const checkpointSession = getSynthesisCheckpointSessionId(projectRoot);
506
+ const sessionId = checkpointSession ?? null;
507
+ if (sessionId && isSynthesisCheckpointDue(projectRoot, sessionId)) {
508
+ const dueReason = getSynthesisDueReason(projectRoot, sessionId);
509
+ const plan = loadPersistedLeadDevPlan(projectRoot);
510
+ const realignmentPending =
511
+ plan && isSynthesisRealignmentPlan(plan) && !areSynthesisConsultTodosComplete(plan);
512
+ return {
513
+ content: [{
514
+ type: 'text' as const,
515
+ text: JSON.stringify({
516
+ blocked: true,
517
+ gate: 'synthesis-checkpoint',
518
+ dueReason,
519
+ primitive: 'synthesis',
520
+ realignmentPending,
521
+ hint: realignmentPending
522
+ ? 'Complete mandatory consult todos (s.1–s.3) before govern-and-apply'
523
+ : 'Run analyze-complexity to reflect and realign before govern-and-apply',
524
+ }, null, 2),
525
+ }],
526
+ };
527
+ }
528
+
359
529
  const { InferenceCycle } = await import('../../inference/inference-cycle.js');
360
- const cycle = InferenceCycle.getInstance(process.cwd(), {
530
+ const cycle = InferenceCycle.getInstance(projectRoot, {
361
531
  skipApply: args.skipApply ?? false,
362
532
  });
363
533
 
364
534
  const aside = await spawnAside({
365
535
  description: `Governance cycle: ${args.proposals.length} proposals`,
536
+ ...(sessionId ? { sessionId } : {}),
366
537
  priorVerdictContext: { proposalCount: args.proposals.length },
367
538
  });
368
539
 
@@ -377,12 +548,17 @@ export class OrchestratorServer {
377
548
  const approved = result!.votes.filter((v) => v.decision === 'approve').length;
378
549
  const rejected = result!.votes.length - approved;
379
550
 
551
+ if (sessionId) {
552
+ recordExecutionSlice('gate', { projectRoot, sessionId });
553
+ }
554
+
380
555
  return {
381
556
  content: [{
382
557
  type: 'text' as const,
383
558
  text: JSON.stringify({
384
559
  cycleId: result!.cycleId,
385
560
  asideId: aside.asideId,
561
+ synthesisBridged: Boolean(sessionId),
386
562
  approved,
387
563
  rejected,
388
564
  votes: result!.votes,