0xray 3.5.3 → 3.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.xray/features.json +5 -1
  2. package/.xray/features.schema.json +56 -0
  3. package/AGENTS-consumer.md +1 -1
  4. package/AGENTS.md +11 -1
  5. package/README.md +1 -1
  6. package/SKILLS.md +1 -1
  7. package/dist/AGENTS.md +11 -1
  8. package/dist/CHANGELOG.md +54 -0
  9. package/dist/README.md +1 -1
  10. package/dist/SKILLS.md +1 -1
  11. package/dist/cli/commands/hermes-install.js +3 -1
  12. package/dist/cli/index.js +0 -0
  13. package/dist/core/features-config.d.ts +15 -0
  14. package/dist/governance/governance-service.d.ts +1 -0
  15. package/dist/governance/governance-service.js +81 -6
  16. package/dist/integrations/enforcement-gate.js +15 -5
  17. package/dist/integrations/grok/grok-cli.js +1 -10
  18. package/dist/integrations/grok/hooks/grok-hook-utils.js +46 -2
  19. package/dist/integrations/grok/hooks/post-tool-use.js +48 -14
  20. package/dist/integrations/grok/hooks/pre-tool-use.js +20 -0
  21. package/dist/integrations/grok/hooks/session-start.js +28 -0
  22. package/dist/integrations/hermes-agent/__init__.py +11 -1
  23. package/dist/integrations/hermes-agent/__pycache__/__init__.cpython-313.pyc +0 -0
  24. package/dist/integrations/hermes-agent/__pycache__/schemas.cpython-313.pyc +0 -0
  25. package/dist/integrations/hermes-agent/__pycache__/tools.cpython-313.pyc +0 -0
  26. package/dist/integrations/hermes-agent/bridge.mjs +73 -10
  27. package/dist/integrations/hooks/confer-hook-runtime.mjs +49 -0
  28. package/dist/integrations/hooks/delegation-gate-runtime.mjs +6 -0
  29. package/dist/integrations/hooks/pipeline-hook-runtime.mjs +211 -0
  30. package/dist/integrations/hooks/plan-hook-runtime.mjs +68 -0
  31. package/dist/integrations/hooks/user-aside-hook-runtime.mjs +49 -0
  32. package/dist/mcps/architect-tools.server.js +6 -1
  33. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +7 -0
  34. package/dist/mcps/orchestrator/handlers/complexity-handler.js +106 -7
  35. package/dist/mcps/orchestrator/handlers/task-handler.d.ts +6 -0
  36. package/dist/mcps/orchestrator/handlers/task-handler.js +75 -0
  37. package/dist/mcps/orchestrator/server.js +54 -5
  38. package/dist/memory-routing/provider-loader.js +30 -2
  39. package/dist/nucleus/aside-worktree.d.ts +41 -0
  40. package/dist/nucleus/aside-worktree.js +164 -0
  41. package/dist/nucleus/confer.d.ts +57 -0
  42. package/dist/nucleus/confer.js +308 -0
  43. package/dist/nucleus/delegation-gate.d.ts +12 -1
  44. package/dist/nucleus/delegation-gate.js +113 -13
  45. package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
  46. package/dist/nucleus/lead-dev-plan-persistence.js +222 -18
  47. package/dist/nucleus/spawn-plan-resolution.d.ts +20 -0
  48. package/dist/nucleus/spawn-plan-resolution.js +42 -0
  49. package/dist/nucleus/synthesis-consult-receipt.d.ts +32 -0
  50. package/dist/nucleus/synthesis-consult-receipt.js +171 -0
  51. package/dist/nucleus/synthesis-context.js +1 -1
  52. package/dist/nucleus/user-aside.d.ts +70 -0
  53. package/dist/nucleus/user-aside.js +393 -0
  54. package/dist/plugin/xray-codex-injection.js +1 -1
  55. package/dist/postprocessor/PostProcessor.d.ts +1 -0
  56. package/dist/postprocessor/PostProcessor.js +11 -0
  57. package/dist/processors/implementations/session-summary-processor.d.ts +1 -1
  58. package/dist/processors/implementations/session-summary-processor.js +9 -6
  59. package/package.json +14 -5
  60. package/scripts/hooks/install-hooks.cjs +3 -3
  61. package/scripts/hooks/run-hook.js +171 -86
  62. package/scripts/mjs/verify-confer-core.mjs +103 -0
  63. package/scripts/mjs/verify-delegation-gate-core.mjs +48 -2
  64. package/scripts/mjs/verify-hermes-session-start.mjs +137 -0
  65. package/scripts/mjs/verify-pre-push-diff-core.mjs +87 -0
  66. package/scripts/mjs/verify-user-aside-core.mjs +137 -0
  67. package/scripts/node/bridge-mcp-wiring.cjs +10 -0
  68. package/scripts/node/install-bridges.cjs +3 -11
  69. package/scripts/node/load-reflection-config.mjs +2 -2
  70. package/scripts/node/release-gate.mjs +8 -4
  71. package/src/mcps/architect-tools.server.ts +6 -1
  72. package/src/mcps/orchestrator/handlers/complexity-handler.ts +178 -20
  73. package/src/mcps/orchestrator/handlers/task-handler.ts +99 -0
  74. package/src/mcps/orchestrator/server.ts +56 -5
  75. package/xray/features.json +5 -1
  76. package/xray/features.schema.json +56 -0
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Verify pre-push codex validation scopes to commit-range diff hunks.
4
+ */
5
+ import { execSync } from 'node:child_process';
6
+ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const packageRoot = join(__dirname, '../..');
13
+
14
+ const { getRangeAddedLinesByFile, scanAddedLinesForCodex } = await import(
15
+ join(packageRoot, 'scripts/hooks/run-hook.js')
16
+ );
17
+
18
+ let failed = 0;
19
+ function pass(n) {
20
+ console.log(`✅ ${n}`);
21
+ }
22
+ function fail(n, d = '') {
23
+ failed++;
24
+ console.error(`❌ ${n}${d ? ` — ${d}` : ''}`);
25
+ }
26
+
27
+ function initGitRepo(dir) {
28
+ execSync('git init -q', { cwd: dir });
29
+ execSync('git config user.email "verify@0xray.test"', { cwd: dir });
30
+ execSync('git config user.name "verify"', { cwd: dir });
31
+ }
32
+
33
+ const tmp = mkdtempSync(join(tmpdir(), 'xray-prepush-diff-'));
34
+ mkdirSync(join(tmp, 'src'), { recursive: true });
35
+ initGitRepo(tmp);
36
+
37
+ const file = 'src/push-flow.ts';
38
+ const filePath = join(tmp, file);
39
+ writeFileSync(
40
+ filePath,
41
+ `export function flow() {\n console.log('legacy noise');\n return 1;\n}\n`,
42
+ );
43
+ execSync('git add .', { cwd: tmp });
44
+ execSync('git commit -q -m "baseline"', { cwd: tmp });
45
+ const baseSha = execSync('git rev-parse HEAD', { cwd: tmp, encoding: 'utf8' }).trim();
46
+
47
+ writeFileSync(
48
+ filePath,
49
+ `export function flow() {\n console.log('legacy noise');\n return 2;\n}\n`,
50
+ );
51
+ execSync('git add .', { cwd: tmp });
52
+ execSync('git commit -q -m "return tweak"', { cwd: tmp });
53
+ const headSha = execSync('git rev-parse HEAD', { cwd: tmp, encoding: 'utf8' }).trim();
54
+ const range = `${baseSha}..${headSha}`;
55
+
56
+ const added = getRangeAddedLinesByFile([file], range, tmp);
57
+ const scanClean = scanAddedLinesForCodex(file, added.get(file) ?? []);
58
+ if (scanClean.errors.length === 0) {
59
+ pass('ignores pre-existing console.log in commit-range diff');
60
+ } else {
61
+ fail('pre-existing console.log leaked into range scan', scanClean.errors.join('; '));
62
+ }
63
+
64
+ writeFileSync(
65
+ filePath,
66
+ `export function flow() {\n console.log('legacy noise');\n console.log('push bad');\n return 3;\n}\n`,
67
+ );
68
+ execSync('git add .', { cwd: tmp });
69
+ execSync('git commit -q -m "bad push"', { cwd: tmp });
70
+ const badHead = execSync('git rev-parse HEAD', { cwd: tmp, encoding: 'utf8' }).trim();
71
+ const badRange = `${headSha}..${badHead}`;
72
+
73
+ const addedBad = getRangeAddedLinesByFile([file], badRange, tmp);
74
+ const scanBad = scanAddedLinesForCodex(file, addedBad.get(file) ?? []);
75
+ if (scanBad.errors.length > 0) {
76
+ pass('flags console.log introduced in commit-range diff');
77
+ } else {
78
+ fail('new console.log in push range was not flagged');
79
+ }
80
+
81
+ console.log(
82
+ '\n' +
83
+ (failed === 0
84
+ ? '🎉 Pre-push diff-scope verify passed (2/2).'
85
+ : `⚠️ ${failed} pre-push diff check(s) failed.`),
86
+ );
87
+ process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * verify-user-aside-core — user aside SSOT + spawn routing.
4
+ */
5
+ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { getActiveUserAsideBoot } from '../../dist/integrations/hooks/user-aside-hook-runtime.mjs';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const packageRoot = join(__dirname, '../..');
13
+
14
+ function dist(rel) {
15
+ return join(packageRoot, 'dist', rel);
16
+ }
17
+
18
+ const {
19
+ buildUserAsidePlan,
20
+ saveUserAside,
21
+ setActiveAsideId,
22
+ clearActiveAside,
23
+ getActiveAsideId,
24
+ isUserAsideTodoId,
25
+ assertSafeAsideId,
26
+ UserAsideValidationError,
27
+ getUserAsideBootHints,
28
+ } = await import(dist('nucleus/user-aside.js'));
29
+ const { resolveSpawnPlan } = await import(dist('nucleus/spawn-plan-resolution.js'));
30
+ const { validateSpawnMatchesTodo } = await import(dist('nucleus/lead-dev-plan-persistence.js'));
31
+ const { evaluatePreToolGate } = await import(dist('nucleus/delegation-gate.js'));
32
+
33
+ let passed = 0;
34
+ let failed = 0;
35
+ const pass = (label) => {
36
+ passed += 1;
37
+ process.stdout.write(`✅ ${label}\n`);
38
+ };
39
+ const fail = (label, detail) => {
40
+ failed += 1;
41
+ process.stderr.write(`❌ ${label}${detail ? ` — ${detail}` : ''}\n`);
42
+ };
43
+
44
+ const tmp = mkdtempSync(join(tmpdir(), 'xray-user-aside-verify-'));
45
+ mkdirSync(join(tmp, '.xray'), { recursive: true });
46
+ writeFileSync(
47
+ join(tmp, '.xray/features.json'),
48
+ JSON.stringify({
49
+ multi_agent_orchestration: {
50
+ enabled: true,
51
+ lead_dev_mode: true,
52
+ user_asides: { enabled: true },
53
+ },
54
+ }),
55
+ );
56
+
57
+ try {
58
+ const namespaced = 'suit-nft.a.1.1';
59
+ if (!isUserAsideTodoId(namespaced)) fail('step 1: namespaced todo pattern');
60
+ else pass('step 1: namespaced todo pattern');
61
+
62
+ try {
63
+ assertSafeAsideId('../bad');
64
+ fail('step 2: path rejection');
65
+ } catch (err) {
66
+ if (err instanceof UserAsideValidationError) pass('step 2: rejects unsafe aside id');
67
+ else fail('step 2: path rejection', String(err));
68
+ }
69
+
70
+ const aside = buildUserAsidePlan(
71
+ 'suit-nft',
72
+ 'Suit NFT',
73
+ 'Mint on Base',
74
+ [{ description: 'Groover proof', type: 'research' }],
75
+ 35,
76
+ tmp,
77
+ );
78
+ if (aside?.plan.phases[0]?.todos[0]?.id !== 'suit-nft.a.1.1') {
79
+ fail('step 3: build aside plan', aside?.plan.phases[0]?.todos[0]?.id);
80
+ } else pass('step 3: namespaced aside todos');
81
+
82
+ saveUserAside(aside, tmp);
83
+ setActiveAsideId('suit-nft', tmp, 'verify-session');
84
+ if (getActiveAsideId(tmp, 'verify-session') !== 'suit-nft') fail('step 4: active pointer');
85
+ else pass('step 4: active aside pointer with session');
86
+
87
+ const todo = aside.plan.phases[0].todos[0];
88
+ const resolved = resolveSpawnPlan({ planTodoId: todo.id }, tmp, 'verify-session');
89
+ if (resolved.source !== 'aside') fail('step 5: resolve spawn plan', JSON.stringify(resolved));
90
+ else pass('step 5: spawn plan resolves to aside');
91
+
92
+ const validation = validateSpawnMatchesTodo(
93
+ { planTodoId: todo.id, subagent_type: todo.subagent, prompt: todo.id },
94
+ tmp,
95
+ undefined,
96
+ 'verify-session',
97
+ );
98
+ if (!validation.valid) fail('step 6: spawn gate', validation.reason);
99
+ else pass('step 6: spawn gate accepts aside todo');
100
+
101
+ const boot = getUserAsideBootHints(tmp, 'verify-session');
102
+ if (!boot?.activeAside) fail('step 7: boot hints', JSON.stringify(boot));
103
+ else pass('step 7: session-boot hints from getUserAsideBootHints');
104
+
105
+ const hookBoot = getActiveUserAsideBoot(tmp, 'verify-session');
106
+ if (!hookBoot?.activeAside) fail('step 8: hook runtime', JSON.stringify(hookBoot));
107
+ else pass('step 8: user-aside-hook-runtime import');
108
+
109
+ clearActiveAside(tmp);
110
+ if (getActiveAsideId(tmp) !== null) fail('step 9: clear active');
111
+ else pass('step 9: clear active aside');
112
+
113
+ setActiveAsideId('suit-nft', tmp, 'verify-session');
114
+ const wtPath = join(tmp, 'aside-worktree');
115
+ aside.worktree = wtPath;
116
+ saveUserAside(aside, tmp);
117
+ const denySpawn = evaluatePreToolGate(
118
+ 'Task',
119
+ { planTodoId: todo.id, subagent_type: todo.subagent, prompt: todo.id },
120
+ {
121
+ projectRoot: tmp,
122
+ sessionId: 'verify-session',
123
+ features: { lead_dev_mode: true, auto_chain_delegations: true },
124
+ },
125
+ );
126
+ if (denySpawn.allow) fail('step 10: worktree cwd deny', JSON.stringify(denySpawn));
127
+ else if (denySpawn.gate !== 'aside-worktree-cwd-missing') {
128
+ fail('step 10: worktree cwd gate', denySpawn.gate);
129
+ } else pass('step 10: denies aside spawn without worktree cwd');
130
+ } finally {
131
+ rmSync(tmp, { recursive: true, force: true });
132
+ }
133
+
134
+ process.stdout.write(
135
+ `\n${failed === 0 ? `✅ verify-user-aside-core ${passed}/${passed}` : `⚠️ ${failed} user-aside check(s) failed`}\n`,
136
+ );
137
+ process.exit(failed > 0 ? 1 : 0);
@@ -121,6 +121,15 @@ function buildPortableProjectMcpJson() {
121
121
  return { mcpServers };
122
122
  }
123
123
 
124
+ function copyHermesFindProjectRootHelper(packageRoot, targetPluginDir) {
125
+ const helperSrc = path.join(packageRoot, "scripts", "helpers", "find-project-root.mjs");
126
+ if (!fs.existsSync(helperSrc)) return false;
127
+ const helperDst = path.join(targetPluginDir, "scripts", "helpers", "find-project-root.mjs");
128
+ fs.mkdirSync(path.dirname(helperDst), { recursive: true });
129
+ fs.copyFileSync(helperSrc, helperDst);
130
+ return true;
131
+ }
132
+
124
133
  function writeHermesPluginArtifacts(targetDir) {
125
134
  if (!fs.existsSync(HERMES_PLUGIN_DIR)) return false;
126
135
  fs.writeFileSync(path.join(HERMES_PLUGIN_DIR, "xray-consumer-root.txt"), `${targetDir}\n`);
@@ -295,6 +304,7 @@ module.exports = {
295
304
  buildStdioMcpServer,
296
305
  buildPluginMcpJson,
297
306
  buildPortableProjectMcpJson,
307
+ copyHermesFindProjectRootHelper,
298
308
  writeHermesPluginArtifacts,
299
309
  syncHermesMcpRegistry,
300
310
  mergeOpencodeMcpRegistry,
@@ -13,23 +13,14 @@ const {
13
13
  wireOpencodeBridge,
14
14
  wireOpenClawBridge,
15
15
  deployPortableProjectMcpJson,
16
+ copyHermesFindProjectRootHelper,
17
+ XRAY_MCP_SERVERS,
16
18
  } = require("./bridge-mcp-wiring.cjs");
17
19
 
18
20
  const SKIP_DIRS = new Set(["node_modules", "logs"]);
19
21
  const MERGE_FILES = new Set(["enforcer-config.json"]);
20
22
  const KEEP_IF_EXISTS = new Set([".yml", ".yaml", ".md"]);
21
23
 
22
- /** Canonical 7-server MCP surface — matches package .mcp.json and Grok plugin */
23
- const XRAY_MCP_SERVERS = [
24
- { name: "xray-governance", mcpCmd: "governance", env: { XRAY_FORCE_MCP_GOVERNANCE: "true" } },
25
- { name: "xray-skills", mcpCmd: "skills", env: {} },
26
- { name: "xray-orchestrator", mcpCmd: "orchestrator", env: {} },
27
- { name: "xray-enforcer", mcpCmd: "enforcer", env: {} },
28
- { name: "xray-researcher", mcpCmd: "researcher", env: {} },
29
- { name: "xray-code-review", mcpCmd: "code-review", env: {} },
30
- { name: "xray-architect-tools", mcpCmd: "architect-tools", env: {} },
31
- ];
32
-
33
24
  function resolveConsumerTargetDir(packageRoot, fallbackDir) {
34
25
  const resolved = path.resolve(packageRoot);
35
26
  const inNodeModules =
@@ -335,6 +326,7 @@ function installHermesBridge(targetDir, packageRoot, log) {
335
326
  fs.copyFileSync(src, dst);
336
327
  }
337
328
  }
329
+ copyHermesFindProjectRootHelper(packageRoot, targetPluginDir);
338
330
  log("hermes-bridge", "plugin copied", "info", { path: "~/.hermes/plugins/xray-hermes" });
339
331
 
340
332
  writePluginMcpJson(targetPluginDir, targetDir, log, "hermes-bridge");
@@ -16,9 +16,9 @@ const root = process.env.PROJECT_ROOT || process.cwd();
16
16
  const configPath = join(root, '.xray', 'features.json');
17
17
 
18
18
  let mode = 'minimal';
19
- let commitThreshold = 25;
19
+ let commitThreshold = 50;
20
20
  let daysThreshold = 14;
21
- let autoGenerate = true;
21
+ let autoGenerate = false;
22
22
  let promptUser = true;
23
23
 
24
24
  if (existsSync(configPath)) {
@@ -48,14 +48,18 @@ function main() {
48
48
  step("4/5 Consumer install smoke", "node scripts/node/consumer-install-smoke.mjs");
49
49
  }
50
50
  } else {
51
- step("1/5 Build", "npm run build");
52
- step("2/5 Tests", "npm test");
51
+ step("1/6 Build", "npm run build");
52
+ step("2/6 Tests", "npm test");
53
53
  step(
54
- "3/5 Plugin infrastructure",
54
+ "3/6 Consumer hook verifiers",
55
+ "npm run verify:pre-commit-diff && npm run verify:pre-push-diff && node scripts/mjs/verify-delegation-gate-core.mjs --host=grok && node scripts/mjs/verify-delegation-gate-core.mjs --host=hermes && node scripts/mjs/verify-hermes-session-start.mjs && node scripts/mjs/verify-confer-core.mjs && npm run verify:user-aside && npm run build && node scripts/mjs/verify-pipeline-facets.mjs --package-only",
56
+ );
57
+ step(
58
+ "4/6 Plugin infrastructure",
55
59
  "node scripts/test/validate-plugins-e2e.cjs --structural-only",
56
60
  );
57
61
  if (!skipSmoke) {
58
- step("4/5 Consumer install smoke (fresh + upgrade)", "node scripts/node/consumer-install-smoke.mjs");
62
+ step("5/6 Consumer install smoke (fresh + upgrade)", "node scripts/node/consumer-install-smoke.mjs");
59
63
  }
60
64
  }
61
65
 
@@ -235,16 +235,21 @@ class XrayArchitectToolsServer extends XrayKnowledgeSkillBase {
235
235
  private async architectureAssessment(args: Record<string, unknown> | undefined) {
236
236
  const projectRoot = (args?.projectRoot as string) || "";
237
237
  const assessmentType = (args?.assessmentType as "quick" | "comprehensive") || "comprehensive";
238
+ const conferPrompt = typeof args?.conferPrompt === "string" ? args.conferPrompt : "";
238
239
 
239
240
  frameworkLogger.log("mcps/architect-tools", "architecture-assessment", "info", { projectRoot });
240
241
 
241
242
  const result = await architectArchitectureAssessment(projectRoot, assessmentType);
243
+ const assessmentJson = JSON.stringify(result, null, 2);
244
+ const text = conferPrompt.trim()
245
+ ? `${conferPrompt.trim()}\n\n## Architecture assessment\n${assessmentJson}\n\nVerdict: CONDITIONAL\nTop risks: review assessment metrics above\nHardening: address high-complexity or coupling findings before resuming`
246
+ : assessmentJson;
242
247
 
243
248
  return {
244
249
  content: [
245
250
  {
246
251
  type: "text",
247
- text: JSON.stringify(result, null, 2),
252
+ text,
248
253
  },
249
254
  ],
250
255
  };
@@ -11,8 +11,27 @@ import {
11
11
  isLeadDevModeActive,
12
12
  persistLeadDevPlan,
13
13
  } from '../../../nucleus/autonomy-kernel.js';
14
+ import {
15
+ formatConferQuorumReport,
16
+ isConferEnabled,
17
+ runConferQuorum,
18
+ } from '../../../nucleus/confer.js';
14
19
  import { bindPlanToSession } from '../../../nucleus/lead-dev-plan-persistence.js';
15
20
  import { clearPendingDelegations } from '../../../nucleus/pending-delegations.js';
21
+ import {
22
+ isAutoProvisionWorktreeEnabled,
23
+ provisionGitWorktree,
24
+ } from '../../../nucleus/aside-worktree.js';
25
+ import {
26
+ buildUserAsidePlan,
27
+ formatUserAsideSummary,
28
+ isUserAsidesEnabled,
29
+ loadUserAside,
30
+ mergeUserAsideIntake,
31
+ saveUserAside,
32
+ setActiveAsideId,
33
+ UserAsideValidationError,
34
+ } from '../../../nucleus/user-aside.js';
16
35
  import { getExecutionPlanner } from '../execution/execution-planner.js';
17
36
  import { addObservations, extractComplexityObservations } from '../aside-context.js';
18
37
  import type { OrchestrationTask, ComplexityAnalysis } from '../types.js';
@@ -73,6 +92,13 @@ export class ComplexityHandler {
73
92
  sessionId?: string;
74
93
  synthesisCheckpoint?: boolean;
75
94
  synthesisDueReason?: string | null;
95
+ collocatedText?: string | null;
96
+ conferFixture?: boolean;
97
+ userAsideId?: string;
98
+ userAsideTitle?: string;
99
+ worktree?: string;
100
+ branch?: string;
101
+ setActiveAside?: boolean;
76
102
  },
77
103
  asideId?: string,
78
104
  ): Promise<{ content: Array<{ type: string; text: string }>; ok: boolean }> {
@@ -99,20 +125,120 @@ export class ComplexityHandler {
99
125
  .filter((t) => t.description?.trim())
100
126
  .map((t) => ({ description: t.description, type: t.type }));
101
127
  const synthesisCheckpoint = args.synthesisCheckpoint === true;
102
- const leadDevPlan = synthesisCheckpoint
128
+ const projectRoot = process.cwd();
129
+ const userAsideIntake =
130
+ !synthesisCheckpoint &&
131
+ Boolean(args.userAsideId?.trim()) &&
132
+ isUserAsidesEnabled(projectRoot);
133
+
134
+ let userAsideReport = '';
135
+ let leadDevPlan = synthesisCheckpoint
103
136
  ? buildSynthesisCheckpointPlan(args.synthesisDueReason ?? null)
104
- : isLeadDevModeActive() && (primaryDescription || planTaskInputs.length > 0)
105
- ? buildLeadDevPlan(
106
- primaryDescription,
107
- taskTypes.length ? taskTypes : ['implement'],
108
- planTaskInputs,
109
- analysis.overallComplexity,
110
- )
111
- : null;
112
-
113
- let planPersisted = synthesisCheckpoint ? leadDevPlan !== null : !leadDevPlan;
114
-
115
- if (leadDevPlan) {
137
+ : userAsideIntake
138
+ ? null
139
+ : isLeadDevModeActive() && (primaryDescription || planTaskInputs.length > 0)
140
+ ? buildLeadDevPlan(
141
+ primaryDescription,
142
+ taskTypes.length ? taskTypes : ['implement'],
143
+ planTaskInputs,
144
+ analysis.overallComplexity,
145
+ )
146
+ : null;
147
+
148
+ let planPersisted = synthesisCheckpoint
149
+ ? leadDevPlan !== null
150
+ : userAsideIntake
151
+ ? false
152
+ : !leadDevPlan;
153
+
154
+ if (userAsideIntake && args.userAsideId) {
155
+ try {
156
+ const asideId = args.userAsideId.trim();
157
+ const title = args.userAsideTitle?.trim() || asideId;
158
+ const existing = loadUserAside(asideId, projectRoot);
159
+ const userAside = existing
160
+ ? mergeUserAsideIntake(
161
+ existing,
162
+ primaryDescription || existing.description,
163
+ planTaskInputs,
164
+ analysis.overallComplexity,
165
+ projectRoot,
166
+ )
167
+ : buildUserAsidePlan(
168
+ asideId,
169
+ title,
170
+ primaryDescription || title,
171
+ planTaskInputs,
172
+ analysis.overallComplexity,
173
+ projectRoot,
174
+ );
175
+ if (!userAside) {
176
+ planPersisted = false;
177
+ } else {
178
+ if (args.worktree) userAside.worktree = args.worktree;
179
+ if (args.branch) userAside.branch = args.branch;
180
+ if (args.sessionId) userAside.sessionId = args.sessionId;
181
+ if (args.userAsideTitle) userAside.title = title;
182
+
183
+ let worktreeProvisionNote = '';
184
+ if (
185
+ args.worktree?.trim() &&
186
+ args.branch?.trim() &&
187
+ isAutoProvisionWorktreeEnabled(projectRoot)
188
+ ) {
189
+ const provision = provisionGitWorktree({
190
+ projectRoot,
191
+ worktree: args.worktree.trim(),
192
+ branch: args.branch.trim(),
193
+ });
194
+ if (!provision.ok) {
195
+ throw new UserAsideValidationError(
196
+ `Worktree auto-provision failed: ${provision.message}`,
197
+ );
198
+ }
199
+ worktreeProvisionNote = provision.created
200
+ ? `\n\n**Worktree provisioned:** \`${provision.path}\` (branch \`${args.branch.trim()}\`)`
201
+ : `\n\n**Worktree ready:** \`${provision.path}\``;
202
+ }
203
+
204
+ const asidePath = saveUserAside(userAside, projectRoot);
205
+ const activate = args.setActiveAside !== false;
206
+ if (activate) {
207
+ clearPendingDelegations(projectRoot);
208
+ setActiveAsideId(asideId, projectRoot, args.sessionId ?? null);
209
+ }
210
+ planPersisted = true;
211
+ userAsideReport = `\n\n## User aside\n\n${formatUserAsideSummary(userAside)}${worktreeProvisionNote}`;
212
+ if (activate) {
213
+ userAsideReport += `\n\n**Active aside:** \`${asideId}\` — spawns route to \`${asideId}.a.*\` todos.`;
214
+ }
215
+ await frameworkLogger.log(
216
+ 'orchestrator.server',
217
+ 'user-aside-persisted',
218
+ 'info',
219
+ {
220
+ asidePath,
221
+ asideId,
222
+ todoCount: userAside.plan.phases.reduce((n, p) => n + p.todos.length, 0),
223
+ activated: activate,
224
+ },
225
+ );
226
+ }
227
+ } catch (err) {
228
+ planPersisted = false;
229
+ const message = err instanceof Error ? err.message : String(err);
230
+ userAsideReport = `\n\n❌ User aside intake failed: ${message}`;
231
+ await frameworkLogger.log(
232
+ 'orchestrator.server',
233
+ 'user-aside-persist-failed',
234
+ 'warning',
235
+ {
236
+ error: message,
237
+ validation: err instanceof UserAsideValidationError,
238
+ },
239
+ );
240
+ }
241
+ } else if (leadDevPlan) {
116
242
  try {
117
243
  const sessionId = args.sessionId ?? `analyze-${Date.now()}`;
118
244
  if (!synthesisCheckpoint) {
@@ -145,6 +271,37 @@ export class ComplexityHandler {
145
271
  }
146
272
  }
147
273
 
274
+ let conferReport = '';
275
+ if (
276
+ synthesisCheckpoint &&
277
+ leadDevPlan &&
278
+ planPersisted &&
279
+ isConferEnabled() &&
280
+ args.sessionId
281
+ ) {
282
+ const conferOpts: {
283
+ dueReason?: string | null;
284
+ fixture?: boolean;
285
+ collocatedText?: string;
286
+ } = {
287
+ dueReason: args.synthesisDueReason ?? null,
288
+ fixture: args.conferFixture === true,
289
+ };
290
+ if (args.collocatedText) conferOpts.collocatedText = args.collocatedText;
291
+ const conferResult = await runConferQuorum(process.cwd(), args.sessionId, conferOpts);
292
+ conferReport = `\n\n${formatConferQuorumReport(conferResult)}`;
293
+ await frameworkLogger.log(
294
+ 'orchestrator.server',
295
+ 'confer-quorum',
296
+ conferResult.status === 'completed' ? 'info' : 'warning',
297
+ {
298
+ status: conferResult.status,
299
+ agentCount: conferResult.agents.length,
300
+ sessionId: args.sessionId,
301
+ },
302
+ );
303
+ }
304
+
148
305
  const ok = planPersisted;
149
306
 
150
307
  return {
@@ -152,13 +309,14 @@ export class ComplexityHandler {
152
309
  content: [
153
310
  {
154
311
  type: 'text',
155
- text: this.formatComplexityResponse(
156
- analysis,
157
- recommendations,
158
- tasks.length,
159
- leadDevPlan,
160
- synthesisCheckpoint,
161
- ),
312
+ text:
313
+ this.formatComplexityResponse(
314
+ analysis,
315
+ recommendations,
316
+ tasks.length,
317
+ leadDevPlan,
318
+ synthesisCheckpoint,
319
+ ) + conferReport + userAsideReport,
162
320
  },
163
321
  ],
164
322
  };
@@ -47,9 +47,108 @@ export class TaskHandler {
47
47
  sessionId?: string;
48
48
  executionMode?: string;
49
49
  timeout?: number;
50
+ confer?: boolean;
51
+ conferFixture?: boolean;
52
+ collocatedText?: string;
53
+ userAsideId?: string;
54
+ setActiveAside?: boolean;
55
+ clearActiveAside?: boolean;
50
56
  },
51
57
  deps: TaskHandlerDeps
52
58
  ): Promise<{ content: Array<{ type: string; text: string }> }> {
59
+ if (args.clearActiveAside === true) {
60
+ const projectRoot = process.cwd();
61
+ const { clearActiveAside, getActiveAsideId } = await import(
62
+ '../../../nucleus/user-aside.js',
63
+ );
64
+ const prior = getActiveAsideId(projectRoot, args.sessionId ?? null);
65
+ clearActiveAside(projectRoot);
66
+ return {
67
+ content: [
68
+ {
69
+ type: 'text',
70
+ text: prior
71
+ ? `✅ Cleared active aside \`${prior}\` — spawns resume main lead-dev plan.`
72
+ : 'ℹ️ No active aside was set.',
73
+ },
74
+ ],
75
+ };
76
+ }
77
+
78
+ if (args.userAsideId?.trim()) {
79
+ const projectRoot = process.cwd();
80
+ const {
81
+ setActiveAsideId,
82
+ loadUserAside,
83
+ formatUserAsideSummary,
84
+ isUserAsidesEnabled,
85
+ UserAsideValidationError,
86
+ } = await import('../../../nucleus/user-aside.js');
87
+ const { clearPendingDelegations } = await import(
88
+ '../../../nucleus/pending-delegations.js',
89
+ );
90
+ if (!isUserAsidesEnabled(projectRoot)) {
91
+ return {
92
+ content: [{ type: 'text', text: '❌ User asides disabled in features.json' }],
93
+ };
94
+ }
95
+ const asideId = args.userAsideId.trim();
96
+ const aside = loadUserAside(asideId, projectRoot);
97
+ if (!aside) {
98
+ return {
99
+ content: [
100
+ {
101
+ type: 'text',
102
+ text: `❌ User aside \`${asideId}\` not found — run analyze-complexity with userAsideId first.`,
103
+ },
104
+ ],
105
+ };
106
+ }
107
+ if (args.setActiveAside !== false) {
108
+ try {
109
+ clearPendingDelegations(projectRoot);
110
+ setActiveAsideId(asideId, projectRoot, args.sessionId ?? null);
111
+ } catch (err) {
112
+ const message = err instanceof UserAsideValidationError ? err.message : String(err);
113
+ return { content: [{ type: 'text', text: `❌ ${message}` }] };
114
+ }
115
+ }
116
+ return {
117
+ content: [
118
+ {
119
+ type: 'text',
120
+ text:
121
+ `✅ Active aside: \`${asideId}\`\n\n${formatUserAsideSummary(aside)}`,
122
+ },
123
+ ],
124
+ };
125
+ }
126
+
127
+ if (args.confer === true) {
128
+ const { runConferQuorum, formatConferQuorumReport, isConferEnabled } = await import(
129
+ '../../../nucleus/confer.js',
130
+ );
131
+ const sid = args.sessionId ?? `confer_${Date.now()}`;
132
+ if (!isConferEnabled()) {
133
+ return {
134
+ content: [{ type: 'text', text: '❌ Confer disabled in features.json' }],
135
+ };
136
+ }
137
+ const conferOpts: {
138
+ dueReason?: string | null;
139
+ fixture?: boolean;
140
+ collocatedText?: string;
141
+ } = {
142
+ dueReason: args.description,
143
+ fixture: args.conferFixture === true,
144
+ };
145
+ if (args.collocatedText) conferOpts.collocatedText = args.collocatedText;
146
+ const result = await runConferQuorum(process.cwd(), sid, conferOpts);
147
+ return {
148
+ content: [{ type: 'text', text: formatConferQuorumReport(result) }],
149
+ };
150
+ }
151
+
53
152
  const { description, tasks = [], sessionId = `session_${Date.now()}`, executionMode = 'optimized', timeout = 300000 } = args;
54
153
 
55
154
  await frameworkLogger.log(