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.
- package/.xray/features.json +5 -1
- package/.xray/features.schema.json +56 -0
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +11 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +11 -1
- package/dist/CHANGELOG.md +54 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/cli/commands/hermes-install.js +3 -1
- package/dist/cli/index.js +0 -0
- package/dist/core/features-config.d.ts +15 -0
- package/dist/governance/governance-service.d.ts +1 -0
- package/dist/governance/governance-service.js +81 -6
- package/dist/integrations/enforcement-gate.js +15 -5
- package/dist/integrations/grok/grok-cli.js +1 -10
- package/dist/integrations/grok/hooks/grok-hook-utils.js +46 -2
- package/dist/integrations/grok/hooks/post-tool-use.js +48 -14
- package/dist/integrations/grok/hooks/pre-tool-use.js +20 -0
- package/dist/integrations/grok/hooks/session-start.js +28 -0
- package/dist/integrations/hermes-agent/__init__.py +11 -1
- package/dist/integrations/hermes-agent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/__pycache__/schemas.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/__pycache__/tools.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/bridge.mjs +73 -10
- package/dist/integrations/hooks/confer-hook-runtime.mjs +49 -0
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +6 -0
- package/dist/integrations/hooks/pipeline-hook-runtime.mjs +211 -0
- package/dist/integrations/hooks/plan-hook-runtime.mjs +68 -0
- package/dist/integrations/hooks/user-aside-hook-runtime.mjs +49 -0
- package/dist/mcps/architect-tools.server.js +6 -1
- package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +7 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +106 -7
- package/dist/mcps/orchestrator/handlers/task-handler.d.ts +6 -0
- package/dist/mcps/orchestrator/handlers/task-handler.js +75 -0
- package/dist/mcps/orchestrator/server.js +54 -5
- package/dist/memory-routing/provider-loader.js +30 -2
- package/dist/nucleus/aside-worktree.d.ts +41 -0
- package/dist/nucleus/aside-worktree.js +164 -0
- package/dist/nucleus/confer.d.ts +57 -0
- package/dist/nucleus/confer.js +308 -0
- package/dist/nucleus/delegation-gate.d.ts +12 -1
- package/dist/nucleus/delegation-gate.js +113 -13
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +27 -3
- package/dist/nucleus/lead-dev-plan-persistence.js +222 -18
- package/dist/nucleus/spawn-plan-resolution.d.ts +20 -0
- package/dist/nucleus/spawn-plan-resolution.js +42 -0
- package/dist/nucleus/synthesis-consult-receipt.d.ts +32 -0
- package/dist/nucleus/synthesis-consult-receipt.js +171 -0
- package/dist/nucleus/synthesis-context.js +1 -1
- package/dist/nucleus/user-aside.d.ts +70 -0
- package/dist/nucleus/user-aside.js +393 -0
- package/dist/plugin/xray-codex-injection.js +1 -1
- package/dist/postprocessor/PostProcessor.d.ts +1 -0
- package/dist/postprocessor/PostProcessor.js +11 -0
- package/dist/processors/implementations/session-summary-processor.d.ts +1 -1
- package/dist/processors/implementations/session-summary-processor.js +9 -6
- package/package.json +14 -5
- package/scripts/hooks/install-hooks.cjs +3 -3
- package/scripts/hooks/run-hook.js +171 -86
- package/scripts/mjs/verify-confer-core.mjs +103 -0
- package/scripts/mjs/verify-delegation-gate-core.mjs +48 -2
- package/scripts/mjs/verify-hermes-session-start.mjs +137 -0
- package/scripts/mjs/verify-pre-push-diff-core.mjs +87 -0
- package/scripts/mjs/verify-user-aside-core.mjs +137 -0
- package/scripts/node/bridge-mcp-wiring.cjs +10 -0
- package/scripts/node/install-bridges.cjs +3 -11
- package/scripts/node/load-reflection-config.mjs +2 -2
- package/scripts/node/release-gate.mjs +8 -4
- package/src/mcps/architect-tools.server.ts +6 -1
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +178 -20
- package/src/mcps/orchestrator/handlers/task-handler.ts +99 -0
- package/src/mcps/orchestrator/server.ts +56 -5
- package/xray/features.json +5 -1
- 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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
216
|
-
|
|
260
|
+
return addedByFile;
|
|
261
|
+
}
|
|
217
262
|
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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(
|
|
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
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
597
|
-
const
|
|
598
|
-
handler
|
|
599
|
-
|
|
600
|
-
|
|
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 (
|
|
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);
|