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
@@ -1,12 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * install-hooks.cjs — Installs xray pre-commit hook into the consumer project's .git/hooks/.
3
+ * install-hooks.cjs — Installs xray pre-commit + post-commit hooks into consumer .git/hooks/.
4
4
  *
5
- * Called from setup.cjs or standalone via:
5
+ * Called from postinstall.cjs or standalone via:
6
6
  * node scripts/hooks/install-hooks.cjs
7
- *
8
- * Creates a consumer-aware pre-commit hook that references the installed
9
- * package's run-hook.js and LightweightValidator via node_modules/ paths.
10
7
  */
11
8
 
12
9
  const fs = require("fs");
@@ -15,7 +12,6 @@ const { execSync } = require("child_process");
15
12
 
16
13
  const packageRoot = path.resolve(path.join(__dirname, "..", ".."));
17
14
 
18
- // Determine the target project root (where .git/ lives)
19
15
  let targetDir;
20
16
  try {
21
17
  targetDir = execSync("git rev-parse --show-toplevel 2>/dev/null", {
@@ -33,34 +29,49 @@ if (!fs.existsSync(gitHooksDir)) {
33
29
  process.exit(0);
34
30
  }
35
31
 
36
- // Check if we're in a consumer install (via node_modules) or dev
37
32
  const isConsumer = packageRoot.includes("node_modules");
38
-
39
- // Resolve run-hook.js path
40
- // In dev: scripts/hooks/run-hook.js (relative to project root)
41
- // In consumer: node_modules/0xray/scripts/hooks/run-hook.js (relative to project root)
42
33
  const runHookRelativePath = isConsumer
43
34
  ? "node_modules/0xray/scripts/hooks/run-hook.js"
44
35
  : "scripts/hooks/run-hook.js";
36
+ const loadReflectionRelativePath = isConsumer
37
+ ? "node_modules/0xray/scripts/node/load-reflection-config.mjs"
38
+ : "scripts/node/load-reflection-config.mjs";
39
+ const autoReflectionRelativePath = isConsumer
40
+ ? "node_modules/0xray/scripts/node/auto-reflection-generator.mjs"
41
+ : "scripts/node/auto-reflection-generator.mjs";
45
42
 
46
- const hookContent = `#!/bin/bash
43
+ function hookRunnerExists(projectRoot) {
44
+ return (
45
+ fs.existsSync(path.join(projectRoot, "node_modules/0xray/scripts/hooks/run-hook.js")) ||
46
+ fs.existsSync(path.join(projectRoot, "scripts/hooks/run-hook.js"))
47
+ );
48
+ }
49
+
50
+ function isXrayHookContent(content) {
51
+ return content.includes("0xRay") || content.includes("xray");
52
+ }
53
+
54
+ function backupExistingHook(hookPath) {
55
+ const backupPath = `${hookPath}.backup-${Date.now()}`;
56
+ fs.copyFileSync(hookPath, backupPath);
57
+ console.log(`ℹ️ Backed up existing hook to ${path.basename(backupPath)}`);
58
+ }
59
+
60
+ const preCommitContent = `#!/bin/bash
47
61
  # 0xRay Pre-Commit Hook — Consumer Install
48
- # Installed by xray setup on $(new Date().toISOString().split('T')[0])
49
- # Runs TypeScript check, linting, and Codex validation before commit
62
+ # Installed by xray setup on ${new Date().toISOString().split("T")[0]}
50
63
  # BLOCKS the commit if validation fails
51
64
 
52
65
  set -e
53
66
 
54
67
  PROJECT_ROOT=$(git rev-parse --show-toplevel)
55
68
 
56
- # Get staged files (only source files)
57
69
  STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(ts|tsx|js|jsx|mjs)$' || true)
58
70
 
59
71
  if [ -z "$STAGED_FILES" ]; then
60
72
  exit 0
61
73
  fi
62
74
 
63
- # Run the Node.js hook runner
64
75
  if command -v node >/dev/null 2>&1; then
65
76
  export HOOK_TYPE="pre-commit"
66
77
  export STAGED_FILES="$STAGED_FILES"
@@ -69,32 +80,121 @@ if command -v node >/dev/null 2>&1; then
69
80
  node "$PROJECT_ROOT/${runHookRelativePath}" pre-commit
70
81
  exit $?
71
82
  else
72
- echo "Warning: xray hook runner not found — install may be incomplete"
73
- exit 0
83
+ echo "Error: xray hook runner not found at ${runHookRelativePath} — run npm install 0xray"
84
+ exit 1
74
85
  fi
75
86
  else
76
- echo "Warning: Node.js not found, skipping xray pre-commit validation"
77
- exit 0
87
+ echo "Error: Node.js not found — xray pre-commit validation requires node"
88
+ exit 1
78
89
  fi
79
90
  `;
80
91
 
81
- const hookPath = path.join(gitHooksDir, "pre-commit");
82
- const existing = fs.existsSync(hookPath);
83
- if (existing) {
84
- const existingContent = fs.readFileSync(hookPath, "utf-8");
85
- if (existingContent.includes("0xRay") || existingContent.includes("xray")) {
86
- console.log("ℹ️ Pre-commit hook already installed by xray — skipping");
87
- process.exit(0);
88
- }
89
- }
92
+ const postCommitContent = `#!/bin/bash
93
+ # 0xRay Post-Commit Hook — Consumer Install
94
+ # Runs activity logging and auto-reflection after commit (non-blocking)
90
95
 
91
- try {
92
- fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
96
+ PROJECT_ROOT=$(git rev-parse --show-toplevel)
97
+ COMMIT_SHA=$(git rev-parse HEAD)
98
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
99
+
100
+ (
101
+ if command -v node >/dev/null 2>&1 && [ -f "$PROJECT_ROOT/${runHookRelativePath}" ]; then
102
+ export HOOK_TYPE="post-commit"
103
+ export COMMIT_SHA="$COMMIT_SHA"
104
+ export BRANCH="$BRANCH"
105
+ export PROJECT_ROOT="$PROJECT_ROOT"
106
+ node "$PROJECT_ROOT/${runHookRelativePath}" post-commit 2>/dev/null || true
107
+ fi
108
+ ) &
109
+
110
+ MODE="minimal"
111
+ COMMIT_THRESHOLD=25
112
+ DAYS_THRESHOLD=14
113
+ AUTO_GENERATE=true
114
+ PROMPT_USER=true
115
+
116
+ if command -v node >/dev/null 2>&1; then
117
+ LOAD_SCRIPT="$PROJECT_ROOT/${loadReflectionRelativePath}"
118
+ if [ -f "$LOAD_SCRIPT" ]; then
119
+ export PROJECT_ROOT="$PROJECT_ROOT"
120
+ REFLECTION_JSON=$(node "$LOAD_SCRIPT" --json 2>/dev/null || echo '{}')
121
+ MODE=$(node -e 'const c=JSON.parse(process.argv[1]);process.stdout.write(String(c.mode||"minimal"))' "$REFLECTION_JSON")
122
+ COMMIT_THRESHOLD=$(node -e 'const c=JSON.parse(process.argv[1]);process.stdout.write(String(c.commitThreshold??25))' "$REFLECTION_JSON")
123
+ DAYS_THRESHOLD=$(node -e 'const c=JSON.parse(process.argv[1]);process.stdout.write(String(c.daysThreshold??14))' "$REFLECTION_JSON")
124
+ AUTO_GENERATE=$(node -e 'const c=JSON.parse(process.argv[1]);process.stdout.write(c.autoGenerate===false?"false":"true")' "$REFLECTION_JSON")
125
+ PROMPT_USER=$(node -e 'const c=JSON.parse(process.argv[1]);process.stdout.write(c.promptUser===false?"false":"true")' "$REFLECTION_JSON")
126
+ fi
127
+ fi
128
+
129
+ COMMIT_MSG=$(git log -1 --pretty=%B)
130
+ SIGNIFICANT_KEYWORDS="fix|bug|debug|deploy|release|feature|refactor|kernel|learning|pattern|wire|connect|integrate"
131
+
132
+ if echo "$COMMIT_MSG" | grep -qiE "$SIGNIFICANT_KEYWORDS" && [ "$PROMPT_USER" = true ]; then
133
+ echo ""
134
+ echo "📝 REFLECTION SUGGESTION: This commit appears to involve significant changes."
135
+ echo " Consider writing a reflection using the v3.0 template:"
136
+ echo " → cat docs/reflections/TEMPLATE_v3.md"
137
+ echo ""
138
+ fi
139
+
140
+ if [ "$AUTO_GENERATE" = true ] && [ -d "$PROJECT_ROOT/docs/reflections" ]; then
141
+ if stat --version >/dev/null 2>&1; then
142
+ LAST_REFLECTION=$(find "$PROJECT_ROOT/docs/reflections" -name "*.md" -type f -exec stat --format="%Y" {} \\; 2>/dev/null | sort -rn | head -1)
143
+ else
144
+ LAST_REFLECTION=$(find "$PROJECT_ROOT/docs/reflections" -name "*.md" -type f -exec stat -f "%m" {} \\; 2>/dev/null | sort -rn | head -1)
145
+ fi
146
+
147
+ if [ -n "$LAST_REFLECTION" ]; then
148
+ REFLECTION_EPOCH=$(date -r "$LAST_REFLECTION" +%s 2>/dev/null || echo "0")
149
+ NOW_EPOCH=$(date +%s)
150
+ DAYS_SINCE=$(( (NOW_EPOCH - REFLECTION_EPOCH) / 86400 ))
151
+ COMMITS_SINCE=$(git log --since="$LAST_REFLECTION" --oneline 2>/dev/null | wc -l | tr -d ' ')
152
+ else
153
+ DAYS_SINCE=999
154
+ COMMITS_SINCE=$(git rev-list --count HEAD 2>/dev/null || echo "0")
155
+ fi
156
+
157
+ if [ "$COMMITS_SINCE" -gt "$COMMIT_THRESHOLD" ] || [ "$DAYS_SINCE" -gt "$DAYS_THRESHOLD" ]; then
158
+ echo ""
159
+ echo "⚠️ AUTO-REFLECTION TRIGGERED (mode: $MODE)"
160
+ echo " $COMMITS_SINCE commits since last reflection ($DAYS_SINCE days)"
161
+ if [ "$MODE" != "off" ]; then
162
+ cd "$PROJECT_ROOT"
163
+ if [ -f "$PROJECT_ROOT/${autoReflectionRelativePath}" ]; then
164
+ node "$PROJECT_ROOT/${autoReflectionRelativePath}" --trigger commit-threshold \\
165
+ --title "Multiple commits since last reflection" 2>/dev/null || true
166
+ fi
167
+ fi
168
+ fi
169
+ fi
170
+
171
+ exit 0
172
+ `;
173
+
174
+ function installHook(name, content, { forceReinstall = false } = {}) {
175
+ const hookPath = path.join(gitHooksDir, name);
176
+ const existing = fs.existsSync(hookPath);
93
177
  if (existing) {
94
- console.log("✅ Pre-commit hook updated for xray validation");
95
- } else {
96
- console.log("✅ Pre-commit hook installed — runs Codex validation on every commit");
178
+ const existingContent = fs.readFileSync(hookPath, "utf-8");
179
+ const isXray = isXrayHookContent(existingContent);
180
+ if (isXray && hookRunnerExists(targetDir) && !forceReinstall) {
181
+ console.log(`ℹ️ ${name} hook already installed by xray — skipping`);
182
+ return false;
183
+ }
184
+ if (isXray && !hookRunnerExists(targetDir)) {
185
+ console.log(`⚠️ Stale xray ${name} hook detected — reinstalling`);
186
+ } else if (!isXray) {
187
+ backupExistingHook(hookPath);
188
+ }
97
189
  }
98
- } catch (e) {
99
- console.warn(`⚠️ Failed to install pre-commit hook: ${e.message}`);
190
+ fs.writeFileSync(hookPath, content, { mode: 0o755 });
191
+ console.log(
192
+ existing
193
+ ? `✅ ${name} hook updated for xray`
194
+ : `✅ ${name} hook installed`,
195
+ );
196
+ return true;
100
197
  }
198
+
199
+ installHook("pre-commit", preCommitContent);
200
+ installHook("post-commit", postCommitContent);
@@ -21,7 +21,7 @@
21
21
 
22
22
  import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
23
23
  import { join, dirname } from "path";
24
- import { execSync, exec } from "child_process";
24
+ import { execSync, exec, execFileSync } from "child_process";
25
25
 
26
26
  // ── Parse arguments ──────────────────────────────────────────
27
27
 
@@ -92,14 +92,47 @@ function recordMetrics(duration, exitCode) {
92
92
 
93
93
  // ── TypeScript checking ──────────────────────────────────────
94
94
 
95
+ function packageHasTypecheckScript() {
96
+ try {
97
+ const pkgPath = join(projectRoot, "package.json");
98
+ if (!existsSync(pkgPath)) return false;
99
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
100
+ return typeof pkg.scripts?.typecheck === "string" && pkg.scripts.typecheck.length > 0;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
95
106
  function runTypeScriptCheck(files) {
96
107
  /**
97
- * Run tsc --noEmit on a subset of files.
98
- * Falls back to full project check if per-file fails.
108
+ * Prefer npm run typecheck when defined (matches CI).
109
+ * Otherwise tsc --noEmit on staged files or full project.
99
110
  */
100
111
  log("Running TypeScript validation...");
101
112
 
102
113
  try {
114
+ if (packageHasTypecheckScript()) {
115
+ try {
116
+ execSync("npm run typecheck", {
117
+ cwd: projectRoot,
118
+ encoding: "utf-8",
119
+ timeout: 120000,
120
+ stdio: ["pipe", "pipe", "pipe"],
121
+ });
122
+ log("npm run typecheck passed");
123
+ return { passed: true, errors: [] };
124
+ } catch (err) {
125
+ const stderr = (err.stderr || err.stdout || "").toString();
126
+ const errorLines = stderr
127
+ .split("\n")
128
+ .filter((l) => /error TS\d+/.test(l))
129
+ .map((l) => l.trim())
130
+ .slice(0, 20);
131
+ log(`npm run typecheck failed: ${errorLines.length} errors`);
132
+ return { passed: false, errors: errorLines };
133
+ }
134
+ }
135
+
103
136
  // Check if tsconfig.json exists
104
137
  const tsconfigPath = join(projectRoot, "tsconfig.json");
105
138
  if (!existsSync(tsconfigPath)) {
@@ -115,12 +148,16 @@ function runTypeScriptCheck(files) {
115
148
  const tsFiles = files.filter((f) => /\.(ts|tsx)$/.test(f));
116
149
  if (tsFiles.length > 0 && tsFiles.length <= 20) {
117
150
  try {
118
- execSync(`${tscCmd} --noEmit ${tsFiles.map((f) => `"${f}"`).join(" ")}`, {
119
- cwd: projectRoot,
120
- encoding: "utf-8",
121
- timeout: 30000,
122
- stdio: ["pipe", "pipe", "pipe"],
123
- });
151
+ execFileSync(
152
+ tscCmd,
153
+ ["--noEmit", ...tsFiles],
154
+ {
155
+ cwd: projectRoot,
156
+ encoding: "utf-8",
157
+ timeout: 30000,
158
+ stdio: ["pipe", "pipe", "pipe"],
159
+ },
160
+ );
124
161
  log(`TypeScript check passed: ${tsFiles.length} files`);
125
162
  return { passed: true, errors: [] };
126
163
  } catch (err) {
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Shared 4/4 delegation gate fixture — seeds consumer tmp workspace.
3
+ */
4
+ import { mkdirSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ export const FIXTURE_SESSION_ID = 'verify-gate';
8
+
9
+ export function seedDelegationGateFixture(tmp) {
10
+ mkdirSync(join(tmp, '.xray', 'state'), { recursive: true });
11
+ writeFileSync(
12
+ join(tmp, '.xray', 'features.json'),
13
+ JSON.stringify({
14
+ multi_agent_orchestration: {
15
+ enabled: true,
16
+ lead_dev_mode: true,
17
+ auto_chain_delegations: true,
18
+ },
19
+ }),
20
+ );
21
+ writeFileSync(
22
+ join(tmp, '.xray', 'state', 'lead-dev-plan.json'),
23
+ JSON.stringify({
24
+ active: true,
25
+ persistedAt: new Date().toISOString(),
26
+ phases: [],
27
+ }),
28
+ );
29
+ writeFileSync(
30
+ join(tmp, '.xray', 'state', 'pending-delegations.json'),
31
+ JSON.stringify({
32
+ sessionId: FIXTURE_SESSION_ID,
33
+ createdAt: new Date().toISOString(),
34
+ ttlMs: 14400000,
35
+ delegations: [
36
+ {
37
+ id: 'del-verify',
38
+ taskId: 'impl-1',
39
+ agent: 'backend-engineer',
40
+ taskDescription: 'implement',
41
+ taskType: 'implement',
42
+ sessionId: FIXTURE_SESSION_ID,
43
+ planTodoId: '2.1',
44
+ planTodoTask: 'swap deps',
45
+ matchMethod: 'agent-only',
46
+ matchConfidence: 0.5,
47
+ status: 'pending',
48
+ createdAt: new Date().toISOString(),
49
+ satisfiedAt: null,
50
+ spawnHint: {
51
+ tool: 'Task',
52
+ subagent_type: 'backend-engineer',
53
+ description: 'Lead-dev delegation 2.1',
54
+ planTodoId: '2.1',
55
+ delegationId: 'del-verify',
56
+ },
57
+ },
58
+ ],
59
+ }),
60
+ );
61
+ }
62
+
63
+ export const SYNTHESIS_FIXTURE_SESSION_ID = 'verify-synthesis';
64
+
65
+ export function seedSynthesisDueFixture(tmp, sessionId = SYNTHESIS_FIXTURE_SESSION_ID) {
66
+ mkdirSync(join(tmp, '.xray', 'state'), { recursive: true });
67
+ const now = new Date().toISOString();
68
+ writeFileSync(
69
+ join(tmp, '.xray', 'features.json'),
70
+ JSON.stringify({
71
+ synthesis: {
72
+ enabled: true,
73
+ every_n_gates: 12,
74
+ every_n_turns: 0,
75
+ every_n_todos_completed: 0,
76
+ },
77
+ multi_agent_orchestration: {
78
+ enabled: true,
79
+ lead_dev_mode: true,
80
+ auto_chain_delegations: true,
81
+ },
82
+ }),
83
+ );
84
+ writeFileSync(
85
+ join(tmp, '.xray', 'state', 'synthesis-checkpoint.json'),
86
+ JSON.stringify({
87
+ version: 1,
88
+ sessionId,
89
+ slicesSinceLastSynthesis: { gates: 12, turns: 0, todosCompleted: 0 },
90
+ lifetimeSlices: { gates: 12, turns: 0, todosCompleted: 0 },
91
+ synthesisDue: true,
92
+ dueReason: 'gate_threshold',
93
+ lastSynthesisAt: null,
94
+ lastSliceAt: now,
95
+ synthesisCount: 0,
96
+ }),
97
+ );
98
+ }
99
+
100
+ export function seedSpawnTodoPlan(tmp) {
101
+ writeFileSync(
102
+ join(tmp, '.xray', 'state', 'lead-dev-plan.json'),
103
+ JSON.stringify({
104
+ active: true,
105
+ persistedAt: new Date().toISOString(),
106
+ phases: [
107
+ {
108
+ id: 'phase-2',
109
+ todos: [
110
+ {
111
+ id: '2.1',
112
+ task: 'swap deps',
113
+ subagent: 'backend-engineer',
114
+ status: 'pending',
115
+ },
116
+ ],
117
+ },
118
+ ],
119
+ }),
120
+ );
121
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared 4/4 delegation gate fixture against nucleus SSOT (any host adapter).
4
+ * Usage: node scripts/mjs/verify-delegation-gate-core.mjs [--host=grok|hermes|opencode]
5
+ */
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
+
19
+ const hostArg = process.argv.find((a) => a.startsWith('--host='));
20
+ const host = hostArg?.split('=')[1] || 'generic';
21
+
22
+ const { evaluatePreToolGate, loadDelegationGateFeatures } = await import(
23
+ join(packageRoot, 'dist/integrations/hooks/delegation-gate-runtime.mjs')
24
+ );
25
+
26
+ const WRITE_TOOL =
27
+ host === 'hermes' ? 'write_file' : host === 'opencode' ? 'write' : 'search_replace';
28
+ const READ_TOOL = host === 'hermes' ? 'read_file' : 'read_file';
29
+ const SPAWN_TOOL =
30
+ host === 'hermes' ? 'delegate_task' : host === 'opencode' ? 'task' : 'Task';
31
+
32
+ let failed = 0;
33
+ function pass(n) {
34
+ console.log(`✅ ${n}`);
35
+ }
36
+ function fail(n, d = '') {
37
+ failed++;
38
+ console.error(`❌ ${n}${d ? ` — ${d}` : ''}`);
39
+ }
40
+
41
+ const tmp = mkdtempSync(join(tmpdir(), 'xray-gate-core-'));
42
+ seedDelegationGateFixture(tmp);
43
+
44
+ const ctx = () => ({
45
+ projectRoot: tmp,
46
+ sessionId: FIXTURE_SESSION_ID,
47
+ features: loadDelegationGateFeatures(tmp),
48
+ host,
49
+ });
50
+
51
+ try {
52
+ const deny = evaluatePreToolGate(
53
+ WRITE_TOOL,
54
+ host === 'opencode'
55
+ ? { filePath: 'src/a.ts', content: 'x' }
56
+ : { path: 'src/a.ts', new_string: 'x', content: 'x' },
57
+ ctx(),
58
+ );
59
+ if (!deny.allow && deny.gate === 'auto-chain-pending') {
60
+ pass('denies write while pending');
61
+ } else {
62
+ fail('write deny', JSON.stringify(deny));
63
+ }
64
+
65
+ const allowRead = evaluatePreToolGate(READ_TOOL, { path: 'src/a.ts' }, ctx());
66
+ if (allowRead.allow) pass('allows read while pending');
67
+ else fail('read allow', JSON.stringify(allowRead));
68
+
69
+ const allowSpawn = evaluatePreToolGate(
70
+ SPAWN_TOOL,
71
+ { prompt: 'plan todo 2.1 swap deps', subagent_type: 'backend-engineer' },
72
+ ctx(),
73
+ );
74
+ if (allowSpawn.allow) pass('allows spawn while pending');
75
+ else fail('spawn allow', JSON.stringify(allowSpawn));
76
+
77
+ seedSpawnTodoPlan(tmp);
78
+ const denyWrongTodo = evaluatePreToolGate(
79
+ SPAWN_TOOL,
80
+ { prompt: 'explore only', subagent_type: 'explore' },
81
+ ctx(),
82
+ );
83
+ if (!denyWrongTodo.allow && denyWrongTodo.gate === 'spawn-todo-persistence') {
84
+ pass('denies spawn skipping required plan todo');
85
+ } else {
86
+ fail('spawn todo persistence', JSON.stringify(denyWrongTodo));
87
+ }
88
+ } finally {
89
+ rmSync(tmp, { recursive: true, force: true });
90
+ }
91
+
92
+ const label = host === 'generic' ? 'core' : host;
93
+ console.log(
94
+ '\n' +
95
+ (failed === 0
96
+ ? `🎉 Delegation gate verify passed (4/4) [${label}].`
97
+ : `⚠️ ${failed} gate check(s) failed [${label}].`),
98
+ );
99
+ process.exit(failed === 0 ? 0 : 1);
@@ -4,10 +4,15 @@
4
4
  * Run: node scripts/mjs/verify-grok-delegation-gate.mjs
5
5
  */
6
6
  import { execSync } from 'node:child_process';
7
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { mkdtempSync, rmSync } from 'node:fs';
8
8
  import { tmpdir } from 'node:os';
9
9
  import { join, dirname } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import {
12
+ FIXTURE_SESSION_ID,
13
+ seedDelegationGateFixture,
14
+ seedSpawnTodoPlan,
15
+ } from './delegation-gate-fixture.mjs';
11
16
 
12
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
18
  const packageRoot = join(__dirname, '../..');
@@ -32,57 +37,11 @@ function runHook(fixture, env = {}) {
32
37
  }
33
38
 
34
39
  const tmp = mkdtempSync(join(tmpdir(), 'xray-gate-verify-'));
35
- mkdirSync(join(tmp, '.xray', 'state'), { recursive: true });
36
- writeFileSync(
37
- join(tmp, '.xray', 'features.json'),
38
- JSON.stringify({
39
- multi_agent_orchestration: {
40
- enabled: true,
41
- lead_dev_mode: true,
42
- auto_chain_delegations: true,
43
- },
44
- }),
45
- );
46
- writeFileSync(
47
- join(tmp, '.xray', 'state', 'lead-dev-plan.json'),
48
- JSON.stringify({ active: true }),
49
- );
50
- writeFileSync(
51
- join(tmp, '.xray', 'state', 'pending-delegations.json'),
52
- JSON.stringify({
53
- sessionId: 'verify-gate',
54
- createdAt: new Date().toISOString(),
55
- ttlMs: 14400000,
56
- delegations: [
57
- {
58
- id: 'del-verify',
59
- taskId: 'impl-1',
60
- agent: 'backend-engineer',
61
- taskDescription: 'implement',
62
- taskType: 'implement',
63
- sessionId: 'verify-gate',
64
- planTodoId: '2.1',
65
- planTodoTask: 'swap deps',
66
- matchMethod: 'agent-only',
67
- matchConfidence: 0.5,
68
- status: 'pending',
69
- createdAt: new Date().toISOString(),
70
- satisfiedAt: null,
71
- spawnHint: {
72
- tool: 'Task',
73
- subagent_type: 'backend-engineer',
74
- description: 'Lead-dev delegation 2.1',
75
- planTodoId: '2.1',
76
- delegationId: 'del-verify',
77
- },
78
- },
79
- ],
80
- }),
81
- );
40
+ seedDelegationGateFixture(tmp);
82
41
 
83
42
  const baseEnv = {
84
43
  GROK_WORKSPACE_ROOT: tmp,
85
- GROK_SESSION_ID: 'verify-gate',
44
+ GROK_SESSION_ID: FIXTURE_SESSION_ID,
86
45
  };
87
46
 
88
47
  try {
@@ -125,26 +84,7 @@ try {
125
84
  if (allowTask.decision === 'allow') pass('PreToolUse allows Task while pending');
126
85
  else fail('Task allow', JSON.stringify(allowTask));
127
86
 
128
- writeFileSync(
129
- join(tmp, '.xray', 'state', 'lead-dev-plan.json'),
130
- JSON.stringify({
131
- active: true,
132
- persistedAt: new Date().toISOString(),
133
- phases: [
134
- {
135
- id: 'phase-2',
136
- todos: [
137
- {
138
- id: '2.1',
139
- task: 'swap deps',
140
- subagent: 'backend-engineer',
141
- status: 'pending',
142
- },
143
- ],
144
- },
145
- ],
146
- }),
147
- );
87
+ seedSpawnTodoPlan(tmp);
148
88
 
149
89
  const denyWrongTodo = runHook(
150
90
  {