0xray 3.5.4 → 3.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.xray/features.json +5 -1
  2. package/.xray/features.schema.json +56 -0
  3. package/AGENTS-consumer.md +1 -1
  4. package/AGENTS.md +11 -1
  5. package/README.md +1 -1
  6. package/SKILLS.md +1 -1
  7. package/dist/AGENTS.md +11 -1
  8. package/dist/CHANGELOG.md +44 -0
  9. package/dist/README.md +1 -1
  10. package/dist/SKILLS.md +1 -1
  11. package/dist/cli/commands/hermes-install.js +3 -1
  12. package/dist/cli/index.js +0 -0
  13. package/dist/core/features-config.d.ts +15 -0
  14. package/dist/governance/governance-service.d.ts +1 -0
  15. package/dist/governance/governance-service.js +81 -6
  16. package/dist/integrations/enforcement-gate.js +15 -5
  17. package/dist/integrations/grok/grok-cli.js +1 -10
  18. package/dist/integrations/grok/hooks/grok-hook-utils.js +46 -2
  19. package/dist/integrations/grok/hooks/post-tool-use.js +21 -0
  20. package/dist/integrations/grok/hooks/pre-tool-use.js +20 -0
  21. package/dist/integrations/grok/hooks/session-start.js +28 -0
  22. package/dist/integrations/hermes-agent/__init__.py +11 -1
  23. package/dist/integrations/hermes-agent/__pycache__/__init__.cpython-313.pyc +0 -0
  24. package/dist/integrations/hermes-agent/__pycache__/schemas.cpython-313.pyc +0 -0
  25. package/dist/integrations/hermes-agent/__pycache__/tools.cpython-313.pyc +0 -0
  26. package/dist/integrations/hermes-agent/bridge.mjs +73 -10
  27. package/dist/integrations/hooks/confer-hook-runtime.mjs +49 -0
  28. package/dist/integrations/hooks/pipeline-hook-runtime.mjs +211 -0
  29. package/dist/integrations/hooks/plan-hook-runtime.mjs +68 -0
  30. package/dist/integrations/hooks/user-aside-hook-runtime.mjs +49 -0
  31. package/dist/mcps/architect-tools.server.js +6 -1
  32. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +7 -0
  33. package/dist/mcps/orchestrator/handlers/complexity-handler.js +106 -7
  34. package/dist/mcps/orchestrator/handlers/task-handler.d.ts +6 -0
  35. package/dist/mcps/orchestrator/handlers/task-handler.js +75 -0
  36. package/dist/mcps/orchestrator/server.js +54 -5
  37. package/dist/memory-routing/provider-loader.js +30 -2
  38. package/dist/nucleus/aside-worktree.d.ts +41 -0
  39. package/dist/nucleus/aside-worktree.js +164 -0
  40. package/dist/nucleus/confer.d.ts +57 -0
  41. package/dist/nucleus/confer.js +308 -0
  42. package/dist/nucleus/delegation-gate.d.ts +4 -0
  43. package/dist/nucleus/delegation-gate.js +90 -11
  44. package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
  45. package/dist/nucleus/lead-dev-plan-persistence.js +201 -19
  46. package/dist/nucleus/spawn-plan-resolution.d.ts +20 -0
  47. package/dist/nucleus/spawn-plan-resolution.js +42 -0
  48. package/dist/nucleus/synthesis-consult-receipt.js +18 -2
  49. package/dist/nucleus/synthesis-context.js +1 -1
  50. package/dist/nucleus/user-aside.d.ts +70 -0
  51. package/dist/nucleus/user-aside.js +393 -0
  52. package/dist/postprocessor/PostProcessor.d.ts +1 -0
  53. package/dist/postprocessor/PostProcessor.js +11 -0
  54. package/dist/processors/implementations/session-summary-processor.d.ts +1 -1
  55. package/dist/processors/implementations/session-summary-processor.js +9 -6
  56. package/package.json +14 -5
  57. package/scripts/hooks/install-hooks.cjs +3 -3
  58. package/scripts/hooks/run-hook.js +171 -86
  59. package/scripts/mjs/verify-confer-core.mjs +103 -0
  60. package/scripts/mjs/verify-delegation-gate-core.mjs +48 -2
  61. package/scripts/mjs/verify-hermes-session-start.mjs +137 -0
  62. package/scripts/mjs/verify-pre-push-diff-core.mjs +87 -0
  63. package/scripts/mjs/verify-user-aside-core.mjs +137 -0
  64. package/scripts/node/bridge-mcp-wiring.cjs +10 -0
  65. package/scripts/node/install-bridges.cjs +3 -11
  66. package/scripts/node/load-reflection-config.mjs +2 -2
  67. package/scripts/node/release-gate.mjs +8 -4
  68. package/src/mcps/architect-tools.server.ts +6 -1
  69. package/src/mcps/orchestrator/handlers/complexity-handler.ts +178 -20
  70. package/src/mcps/orchestrator/handlers/task-handler.ts +99 -0
  71. package/src/mcps/orchestrator/server.ts +56 -5
  72. package/xray/features.json +5 -1
  73. package/xray/features.schema.json +56 -0
@@ -20,8 +20,9 @@
20
20
  */
21
21
 
22
22
  import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
23
- import { join, dirname } from "path";
23
+ import { join, dirname, resolve } from "path";
24
24
  import { execSync, exec, execFileSync } from "child_process";
25
+ import { fileURLToPath } from "url";
25
26
 
26
27
  // ── Parse arguments ──────────────────────────────────────────
27
28
 
@@ -203,82 +204,152 @@ function runTypeScriptCheck(files) {
203
204
  }
204
205
  }
205
206
 
206
- // ── Codex validation ─────────────────────────────────────────
207
+ // ── Codex validation (diff-hunk scoped) ──────────────────────
208
+
209
+ /** Parse + lines from a unified diff (staged or commit-range). */
210
+ export function parseAddedLinesFromDiff(diff) {
211
+ const addedLines = [];
212
+ for (const line of diff.split("\n")) {
213
+ if (
214
+ line.startsWith("+++") ||
215
+ line.startsWith("---") ||
216
+ line.startsWith("@@") ||
217
+ line.startsWith("diff ") ||
218
+ line.startsWith("index ")
219
+ ) {
220
+ continue;
221
+ }
222
+ if (line.startsWith("+")) {
223
+ addedLines.push(line.slice(1));
224
+ }
225
+ }
226
+ return addedLines;
227
+ }
207
228
 
208
- async function runCodexValidation(files) {
209
- /**
210
- * Run lightweight codex validation on files.
211
- * Uses dynamic import of framework modules when available.
212
- */
213
- log("Running Codex validation...");
229
+ /**
230
+ * Collect added lines per file from git diff.
231
+ * scope `staged` → --cached (pre-commit); scope `range` → COMMIT_RANGE (pre-push).
232
+ */
233
+ export function getAddedLinesByFile(
234
+ files,
235
+ { scope = "staged", range = null, root = projectRoot } = {},
236
+ ) {
237
+ const addedByFile = new Map();
238
+
239
+ for (const file of files) {
240
+ if (!/\.(ts|tsx|js|jsx|mjs)$/.test(file)) continue;
241
+ try {
242
+ const gitArgs =
243
+ scope === "range" && range
244
+ ? ["diff", range, "-U0", "--", file]
245
+ : ["diff", "--cached", "-U0", "--", file];
246
+ const diff = execFileSync("git", gitArgs, {
247
+ cwd: root,
248
+ encoding: "utf-8",
249
+ stdio: ["pipe", "pipe", "pipe"],
250
+ });
251
+ const addedLines = parseAddedLinesFromDiff(diff);
252
+ if (addedLines.length > 0) {
253
+ addedByFile.set(file, addedLines);
254
+ }
255
+ } catch {
256
+ // Deletions-only or unreadable diff — skip codex for this file
257
+ }
258
+ }
214
259
 
215
- const tsFiles = files.filter((f) => /\.(ts|tsx)$/.test(f));
216
- const jsFiles = files.filter((f) => /\.(js|jsx|mjs)$/.test(f));
260
+ return addedByFile;
261
+ }
217
262
 
218
- if (tsFiles.length === 0 && jsFiles.length === 0) {
219
- return { passed: true, warnings: [], errors: [] };
220
- }
263
+ /** Pre-commit: staged diff hunks only. */
264
+ export function getStagedAddedLinesByFile(files, root = projectRoot) {
265
+ return getAddedLinesByFile(files, { scope: "staged", root });
266
+ }
267
+
268
+ /** Pre-push: added lines in commit range (e.g. remote..local). */
269
+ export function getRangeAddedLinesByFile(files, range, root = projectRoot) {
270
+ return getAddedLinesByFile(files, { scope: "range", range, root });
271
+ }
221
272
 
222
- // Quick static checks without framework dependency
273
+ export function scanAddedLinesForCodex(file, addedLines) {
223
274
  const errors = [];
224
275
  const warnings = [];
276
+ const content = addedLines.join("\n");
277
+ const lines = addedLines;
225
278
 
226
- for (const file of [...tsFiles, ...jsFiles]) {
227
- const filePath = join(projectRoot, file);
228
- if (!existsSync(filePath)) continue;
279
+ const consoleLogLines = lines.reduce((acc, line, i) => {
280
+ if (/\bconsole\.(log|warn|error)\b/.test(line) && !line.includes("NOSONAR")) {
281
+ acc.push(i + 1);
282
+ }
283
+ return acc;
284
+ }, []);
285
+ if (consoleLogLines.length > 0) {
286
+ errors.push(
287
+ `${file}: console.log/warn/error in staged diff at hunk line(s) ${consoleLogLines.slice(0, 3).join(", ")}`,
288
+ );
289
+ }
229
290
 
230
- try {
231
- const content = readFileSync(filePath, "utf-8");
232
- const lines = content.split("\n");
291
+ const todoLines = lines.reduce((acc, line, i) => {
292
+ if (/\/\/\s*(TODO|FIXME|HACK|XXX)\b/i.test(line)) {
293
+ acc.push(i + 1);
294
+ }
295
+ return acc;
296
+ }, []);
297
+ if (todoLines.length > 0) {
298
+ warnings.push(`${file}: ${todoLines.length} TODO/FIXME in staged diff`);
299
+ }
233
300
 
234
- // Check for console.log (codex violation)
235
- const consoleLogLines = lines.reduce((acc, line, i) => {
236
- if (/\bconsole\.(log|warn|error)\b/.test(line) && !line.includes("NOSONAR")) {
237
- acc.push(i + 1);
238
- }
239
- return acc;
240
- }, []);
241
- if (consoleLogLines.length > 0) {
242
- errors.push(`${file}: console.log/warn/error found at lines ${consoleLogLines.slice(0, 3).join(", ")}`);
243
- }
301
+ const tsIgnoreLines = lines.reduce((acc, line, i) => {
302
+ if (/@ts-ignore|@ts-nocheck|@ts-expect-error/.test(line)) {
303
+ acc.push(i + 1);
304
+ }
305
+ return acc;
306
+ }, []);
307
+ if (tsIgnoreLines.length > 0) {
308
+ warnings.push(`${file}: ${tsIgnoreLines.length} @ts-ignore/@ts-nocheck in staged diff`);
309
+ }
244
310
 
245
- // Check for TODO/FIXME
246
- const todoLines = lines.reduce((acc, line, i) => {
247
- if (/\/\/\s*(TODO|FIXME|HACK|XXX)\b/i.test(line)) {
248
- acc.push(i + 1);
249
- }
250
- return acc;
251
- }, []);
252
- if (todoLines.length > 0) {
253
- warnings.push(`${file}: ${todoLines.length} TODO/FIXME comment(s) found`);
311
+ if (/\bany\b/.test(content)) {
312
+ const anyLines = lines.reduce((acc, line, i) => {
313
+ if (/\bany\b/.test(line) && !line.includes("//") && !line.includes("*")) {
314
+ acc.push(i + 1);
254
315
  }
316
+ return acc;
317
+ }, []);
318
+ if (anyLines.length > 0) {
319
+ warnings.push(`${file}: ${anyLines.length} use(s) of 'any' in staged diff`);
320
+ }
321
+ }
255
322
 
256
- // Check for @ts-ignore
257
- const tsIgnoreLines = lines.reduce((acc, line, i) => {
258
- if (/@ts-ignore|@ts-nocheck|@ts-expect-error/.test(line)) {
259
- acc.push(i + 1);
260
- }
261
- return acc;
262
- }, []);
263
- if (tsIgnoreLines.length > 0) {
264
- warnings.push(`${file}: ${tsIgnoreLines.length} @ts-ignore/@ts-nocheck found`);
265
- }
323
+ return { errors, warnings };
324
+ }
266
325
 
267
- // Check for any type
268
- if (/\bany\b/.test(content)) {
269
- const anyLines = lines.reduce((acc, line, i) => {
270
- if (/\bany\b/.test(line) && !line.includes("//") && !line.includes("*")) {
271
- acc.push(i + 1);
272
- }
273
- return acc;
274
- }, []);
275
- if (anyLines.length > 3) {
276
- warnings.push(`${file}: ${anyLines.length} uses of 'any' type`);
277
- }
278
- }
279
- } catch {
280
- // Skip unreadable files
281
- }
326
+ async function runCodexValidation(files, options = {}) {
327
+ /**
328
+ * Run lightweight codex validation on diff hunks only (staged or commit range).
329
+ */
330
+ const scope = options.scope || "staged";
331
+ const range = options.range || null;
332
+ const scopeLabel = scope === "range" && range ? `range ${range}` : "staged";
333
+ log(`Running Codex validation (diff-hunk scope: ${scopeLabel})...`);
334
+
335
+ const codexFiles = files.filter((f) => /\.(ts|tsx|js|jsx|mjs)$/.test(f));
336
+ if (codexFiles.length === 0) {
337
+ return { passed: true, warnings: [], errors: [] };
338
+ }
339
+
340
+ const addedByFile =
341
+ scope === "range" && range
342
+ ? getRangeAddedLinesByFile(codexFiles, range)
343
+ : getStagedAddedLinesByFile(codexFiles);
344
+ const errors = [];
345
+ const warnings = [];
346
+
347
+ for (const file of codexFiles) {
348
+ const addedLines = addedByFile.get(file);
349
+ if (!addedLines || addedLines.length === 0) continue;
350
+ const scan = scanAddedLinesForCodex(file, addedLines);
351
+ errors.push(...scan.errors);
352
+ warnings.push(...scan.warnings);
282
353
  }
283
354
 
284
355
  const passed = errors.length === 0;
@@ -302,6 +373,7 @@ async function runLogMaintenance() {
302
373
  try {
303
374
  const distDirs = [
304
375
  join(projectRoot, "dist"),
376
+ join(projectRoot, "node_modules", "0xray", "dist"),
305
377
  join(projectRoot, "node_modules", "xray", "dist"),
306
378
  ];
307
379
 
@@ -375,7 +447,7 @@ async function runLogMaintenance() {
375
447
 
376
448
  // ── Comprehensive validation (pre-push) ──────────────────────
377
449
 
378
- async function runFullValidation(files) {
450
+ async function runFullValidation(files, commitRange = null) {
379
451
  /**
380
452
  * Full validation suite for pre-push.
381
453
  * Includes TypeScript check, codex validation, and test hints.
@@ -385,8 +457,11 @@ async function runFullValidation(files) {
385
457
  // TypeScript check
386
458
  const tsResult = runTypeScriptCheck(files);
387
459
 
388
- // Codex validation
389
- const codexResult = await runCodexValidation(files);
460
+ // Codex validation — commit range diff (not --cached)
461
+ const codexResult = await runCodexValidation(files, {
462
+ scope: commitRange ? "range" : "staged",
463
+ range: commitRange,
464
+ });
390
465
 
391
466
  // Check for test files
392
467
  const sourceFiles = files.filter((f) =>
@@ -487,15 +562,20 @@ async function handlePrePush() {
487
562
  .split("\n")
488
563
  .map((f) => f.trim())
489
564
  .filter(Boolean);
565
+ const commitRange = (process.env.COMMIT_RANGE || "").trim() || null;
490
566
 
491
567
  if (changedFiles.length === 0) {
492
568
  log("No changed files to validate");
493
569
  process.exit(0);
494
570
  }
495
571
 
496
- log(`Validating ${changedFiles.length} changed file(s) for push...`);
572
+ log(
573
+ `Validating ${changedFiles.length} changed file(s) for push` +
574
+ (commitRange ? ` (range ${commitRange})` : "") +
575
+ "...",
576
+ );
497
577
 
498
- const result = await runFullValidation(changedFiles);
578
+ const result = await runFullValidation(changedFiles, commitRange);
499
579
 
500
580
  if (!result.passed) {
501
581
  console.error("\n❌ Pre-push validation failed:\n");
@@ -586,22 +666,27 @@ const handlers = {
586
666
  "pre-command": handlePreCommand,
587
667
  };
588
668
 
589
- const handler = handlers[hookType];
590
- if (!handler) {
591
- console.error(`Unknown hook type: ${hookType}`);
592
- console.error(`Valid types: ${Object.keys(handlers).join(", ")}`);
593
- process.exit(1);
594
- }
669
+ const isDirectRun =
670
+ process.argv[1] &&
671
+ resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
595
672
 
596
- // Run with timing
597
- const startTime = Date.now();
598
- handler()
599
- .then(() => {
600
- const duration = Date.now() - startTime;
601
- log(`Hook completed in ${duration}ms`);
602
- })
603
- .catch((err) => {
604
- logError(`Hook crashed: ${err.message}`);
605
- console.error(`0xRay hook error: ${err.message}`);
673
+ if (isDirectRun) {
674
+ const handler = handlers[hookType];
675
+ if (!handler) {
676
+ console.error(`Unknown hook type: ${hookType}`);
677
+ console.error(`Valid types: ${Object.keys(handlers).join(", ")}`);
606
678
  process.exit(1);
607
- });
679
+ }
680
+
681
+ const startTime = Date.now();
682
+ handler()
683
+ .then(() => {
684
+ const duration = Date.now() - startTime;
685
+ log(`Hook completed in ${duration}ms`);
686
+ })
687
+ .catch((err) => {
688
+ logError(`Hook crashed: ${err.message}`);
689
+ console.error(`0xRay hook error: ${err.message}`);
690
+ process.exit(1);
691
+ });
692
+ }
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Confer quorum fixture — 3-agent synthesis consult completes via fixture mode.
4
+ */
5
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const packageRoot = join(__dirname, '../..');
12
+
13
+ const { buildSynthesisCheckpointPlan } = await import(
14
+ join(packageRoot, 'dist/nucleus/autonomy-kernel.js')
15
+ );
16
+ const { savePersistedLeadDevPlan, areSynthesisConsultTodosComplete } = await import(
17
+ join(packageRoot, 'dist/nucleus/lead-dev-plan-persistence.js')
18
+ );
19
+ const { recordExecutionSlice, isSynthesisCheckpointDue } = await import(
20
+ join(packageRoot, 'dist/nucleus/synthesis.js')
21
+ );
22
+ const { runConferQuorum, loadConferCheckpoint, CONFER_AGENTS } = await import(
23
+ join(packageRoot, 'dist/nucleus/confer.js')
24
+ );
25
+
26
+ const sessionId = 'verify-confer-core';
27
+ let failed = 0;
28
+ function pass(n) {
29
+ console.log(`✅ ${n}`);
30
+ }
31
+ function fail(n, d = '') {
32
+ failed++;
33
+ console.error(`❌ ${n}${d ? ` — ${d}` : ''}`);
34
+ }
35
+
36
+ const tmp = mkdtempSync(join(tmpdir(), 'xray-confer-verify-'));
37
+ mkdirSync(join(tmp, '.xray', 'state'), { recursive: true });
38
+ writeFileSync(
39
+ join(tmp, '.xray', 'features.json'),
40
+ JSON.stringify({
41
+ multi_agent_orchestration: { lead_dev_mode: true, confer_on_synthesis: true },
42
+ synthesis: { enabled: true, every_n_gates: 1, every_n_turns: 0, every_n_todos_completed: 0 },
43
+ }),
44
+ );
45
+
46
+ try {
47
+ recordExecutionSlice('gate', { projectRoot: tmp, sessionId });
48
+ if (!isSynthesisCheckpointDue(tmp, sessionId)) fail('step 1: synthesis due');
49
+ else pass('step 1: synthesis checkpoint due');
50
+
51
+ const plan = buildSynthesisCheckpointPlan('gate threshold (1/1)');
52
+ if (!plan) fail('step 2: synthesis plan');
53
+ else {
54
+ savePersistedLeadDevPlan(
55
+ { ...plan, persistedAt: new Date().toISOString(), sessionId },
56
+ tmp,
57
+ );
58
+ pass('step 2: synthesis plan persisted');
59
+ }
60
+
61
+ if (CONFER_AGENTS.length !== 3) fail('step 3: confer agent count', String(CONFER_AGENTS.length));
62
+ else pass('step 3: three confer agents defined');
63
+
64
+ const result = await runConferQuorum(tmp, sessionId, {
65
+ collocatedText: 'verify confer fixture context',
66
+ dueReason: 'gate threshold',
67
+ fixture: true,
68
+ });
69
+
70
+ if (result.status === 'completed' && result.agents.length === 3) {
71
+ pass('step 4: confer quorum completed all agents');
72
+ } else {
73
+ fail('step 4: confer quorum', JSON.stringify(result));
74
+ }
75
+
76
+ const state = loadConferCheckpoint(tmp);
77
+ if (state?.status === 'completed' && state.completedAgents.length === 3) {
78
+ pass('step 5: confer checkpoint state completed');
79
+ } else {
80
+ fail('step 5: confer state', JSON.stringify(state));
81
+ }
82
+
83
+ if (areSynthesisConsultTodosComplete(
84
+ (await import(join(packageRoot, 'dist/nucleus/lead-dev-plan-persistence.js'))).loadPersistedLeadDevPlan(tmp),
85
+ )) {
86
+ pass('step 6: synthesis consult todos complete after confer');
87
+ } else {
88
+ fail('step 6: consult todos incomplete');
89
+ }
90
+
91
+ if (!isSynthesisCheckpointDue(tmp, sessionId)) pass('step 7: synthesis checkpoint cleared');
92
+ else fail('step 7: checkpoint still due');
93
+ } finally {
94
+ rmSync(tmp, { recursive: true, force: true });
95
+ }
96
+
97
+ console.log(
98
+ '\n' +
99
+ (failed === 0
100
+ ? '🎉 Confer verify passed (7/7).'
101
+ : `⚠️ ${failed} confer check(s) failed.`),
102
+ );
103
+ process.exit(failed === 0 ? 0 : 1);
@@ -3,7 +3,7 @@
3
3
  * Shared 4/4 delegation gate fixture against nucleus SSOT (any host adapter).
4
4
  * Usage: node scripts/mjs/verify-delegation-gate-core.mjs [--host=grok|hermes|opencode]
5
5
  */
6
- import { mkdtempSync, rmSync } from 'node:fs';
6
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { dirname, join } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
@@ -85,6 +85,52 @@ try {
85
85
  } else {
86
86
  fail('spawn todo persistence', JSON.stringify(denyWrongTodo));
87
87
  }
88
+
89
+ const staleTmp = mkdtempSync(join(tmpdir(), 'xray-gate-stale-'));
90
+ const staleAt = new Date(Date.now() - 9 * 60 * 60 * 1000).toISOString();
91
+ mkdirSync(join(staleTmp, '.xray', 'state'), { recursive: true });
92
+ writeFileSync(
93
+ join(staleTmp, '.xray', 'features.json'),
94
+ JSON.stringify({
95
+ multi_agent_orchestration: { enabled: true, lead_dev_mode: true },
96
+ }),
97
+ );
98
+ writeFileSync(
99
+ join(staleTmp, '.xray', 'state', 'lead-dev-plan.json'),
100
+ JSON.stringify({
101
+ active: true,
102
+ persistedAt: staleAt,
103
+ phases: [
104
+ {
105
+ id: 'phase-1',
106
+ todos: [
107
+ {
108
+ id: '1.1',
109
+ task: 'stale unstarted todo',
110
+ subagent: 'researcher',
111
+ status: 'pending',
112
+ },
113
+ ],
114
+ },
115
+ ],
116
+ }),
117
+ );
118
+ const denyStale = evaluatePreToolGate(
119
+ SPAWN_TOOL,
120
+ { prompt: 'plan todo 1.1 consult', subagent_type: 'researcher' },
121
+ {
122
+ projectRoot: staleTmp,
123
+ sessionId: FIXTURE_SESSION_ID,
124
+ features: loadDelegationGateFeatures(staleTmp),
125
+ host,
126
+ },
127
+ );
128
+ if (!denyStale.allow && denyStale.gate === 'spawn-plan-stale') {
129
+ pass('denies spawn when lead-dev plan is stale');
130
+ } else {
131
+ fail('spawn plan stale', JSON.stringify(denyStale));
132
+ }
133
+ rmSync(staleTmp, { recursive: true, force: true });
88
134
  } finally {
89
135
  rmSync(tmp, { recursive: true, force: true });
90
136
  }
@@ -93,7 +139,7 @@ const label = host === 'generic' ? 'core' : host;
93
139
  console.log(
94
140
  '\n' +
95
141
  (failed === 0
96
- ? `🎉 Delegation gate verify passed (4/4) [${label}].`
142
+ ? `🎉 Delegation gate verify passed (5/5) [${label}].`
97
143
  : `⚠️ ${failed} gate check(s) failed [${label}].`),
98
144
  );
99
145
  process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hermes session-start bridge — stale plan archival with synthesis exemption.
4
+ */
5
+ import { execSync } from 'node:child_process';
6
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, 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
+ const bridge = join(packageRoot, 'dist/integrations/hermes-agent/bridge.mjs');
14
+
15
+ let failed = 0;
16
+ function pass(n) {
17
+ console.log(`✅ ${n}`);
18
+ }
19
+ function fail(n, d = '') {
20
+ failed++;
21
+ console.error(`❌ ${n}${d ? ` — ${d}` : ''}`);
22
+ }
23
+
24
+ function runBridge(payload, cwd) {
25
+ const json = JSON.stringify(payload).replace(/'/g, "'\\''");
26
+ const out = execSync(`node "${bridge}" session-start --cwd "${cwd}" '${json}'`, {
27
+ cwd,
28
+ encoding: 'utf8',
29
+ env: { ...process.env, XRAY_ROOT: cwd },
30
+ });
31
+ return JSON.parse(out.trim());
32
+ }
33
+
34
+ const tmp = mkdtempSync(join(tmpdir(), 'xray-hermes-session-'));
35
+ const planPath = join(tmp, '.xray', 'state', 'lead-dev-plan.json');
36
+ mkdirSync(join(tmp, '.xray', 'state'), { recursive: true });
37
+ writeFileSync(
38
+ join(tmp, '.xray', 'features.json'),
39
+ JSON.stringify({
40
+ multi_agent_orchestration: {
41
+ enabled: true,
42
+ lead_dev_mode: true,
43
+ auto_consult_major_work: true,
44
+ },
45
+ synthesis: { enabled: true, every_n_gates: 1, every_n_turns: 0, every_n_todos_completed: 0 },
46
+ }),
47
+ );
48
+
49
+ const staleAt = new Date(Date.now() - 10 * 60 * 60 * 1000).toISOString();
50
+ const synthesisPlan = {
51
+ active: true,
52
+ persistedAt: staleAt,
53
+ sessionId: 'hermes-session-start-fixture',
54
+ phases: [
55
+ {
56
+ id: 'phase-synthesis',
57
+ name: 'Reflect & realign',
58
+ goal: 'consult',
59
+ definitionOfDone: 'done',
60
+ todos: [
61
+ { id: 's.1', task: 'researcher consult', subagent: 'researcher', status: 'pending' },
62
+ { id: 's.2', task: 'architect consult', subagent: 'architect-tools', status: 'pending' },
63
+ { id: 's.3', task: 'review consult', subagent: 'code-review', status: 'pending' },
64
+ ],
65
+ },
66
+ ],
67
+ };
68
+ writeFileSync(planPath, JSON.stringify(synthesisPlan, null, 2));
69
+
70
+ try {
71
+ const result = runBridge({ command: 'session-start', sessionId: 'hermes-session-start-fixture' }, tmp);
72
+ if (result.ok === true) pass('bridge session-start returns ok');
73
+ else fail('session-start ok', JSON.stringify(result));
74
+
75
+ if (existsSync(planPath)) pass('aged synthesis plan survives session-start archival');
76
+ else fail('synthesis plan preserved', 'lead-dev-plan.json missing');
77
+
78
+ const archived = result.planArchive?.archived === true;
79
+ if (!archived) pass('session-start did not archive protected synthesis plan');
80
+ else fail('synthesis archive skip', JSON.stringify(result.planArchive));
81
+
82
+ const staleTmp = mkdtempSync(join(tmpdir(), 'xray-hermes-stale-'));
83
+ const stalePlanPath = join(staleTmp, '.xray', 'state', 'lead-dev-plan.json');
84
+ mkdirSync(join(staleTmp, '.xray', 'state'), { recursive: true });
85
+ writeFileSync(
86
+ join(staleTmp, '.xray', 'features.json'),
87
+ JSON.stringify({
88
+ multi_agent_orchestration: { enabled: true, lead_dev_mode: true },
89
+ }),
90
+ );
91
+ const staleAt = new Date(Date.now() - 10 * 60 * 60 * 1000).toISOString();
92
+ writeFileSync(
93
+ stalePlanPath,
94
+ JSON.stringify(
95
+ {
96
+ active: true,
97
+ persistedAt: staleAt,
98
+ phases: [
99
+ {
100
+ id: 'phase-1',
101
+ todos: [
102
+ {
103
+ id: '1.1',
104
+ task: 'stale hygiene',
105
+ subagent: 'researcher',
106
+ status: 'pending',
107
+ },
108
+ ],
109
+ },
110
+ ],
111
+ },
112
+ null,
113
+ 2,
114
+ ),
115
+ );
116
+
117
+ const staleResult = runBridge(
118
+ { command: 'session-start', sessionId: 'hermes-stale-archive-fixture' },
119
+ staleTmp,
120
+ );
121
+ if (staleResult.planArchive?.archived === true && !existsSync(stalePlanPath)) {
122
+ pass('session-start archives ordinary stale lead-dev plan');
123
+ } else {
124
+ fail('stale plan archival', JSON.stringify(staleResult.planArchive));
125
+ }
126
+ rmSync(staleTmp, { recursive: true, force: true });
127
+ } finally {
128
+ rmSync(tmp, { recursive: true, force: true });
129
+ }
130
+
131
+ console.log(
132
+ '\n' +
133
+ (failed === 0
134
+ ? '🎉 Hermes session-start verify passed (4/4).'
135
+ : `⚠️ ${failed} Hermes session-start check(s) failed.`),
136
+ );
137
+ process.exit(failed === 0 ? 0 : 1);