0xray 3.5.1 → 3.5.3

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 (55) 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 +25 -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 +2 -2
  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/bridge.mjs +22 -50
  23. package/dist/integrations/hooks/synthesis-hook-runtime.mjs +50 -0
  24. package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +3 -0
  25. package/dist/mcps/orchestrator/handlers/complexity-handler.js +25 -10
  26. package/dist/mcps/orchestrator/server.js +124 -4
  27. package/dist/memory-routing/types.d.ts +7 -0
  28. package/dist/nucleus/autonomy-kernel.d.ts +3 -0
  29. package/dist/nucleus/autonomy-kernel.js +40 -0
  30. package/dist/nucleus/delegation-gate.d.ts +6 -4
  31. package/dist/nucleus/delegation-gate.js +77 -5
  32. package/dist/nucleus/lead-dev-plan-persistence.d.ts +3 -0
  33. package/dist/nucleus/lead-dev-plan-persistence.js +44 -1
  34. package/dist/nucleus/synthesis-completion.d.ts +6 -0
  35. package/dist/nucleus/synthesis-completion.js +29 -0
  36. package/dist/nucleus/synthesis-context.d.ts +31 -0
  37. package/dist/nucleus/synthesis-context.js +86 -0
  38. package/dist/nucleus/synthesis.d.ts +54 -0
  39. package/dist/nucleus/synthesis.js +196 -0
  40. package/dist/plugin/xray-codex-injection.js +17 -0
  41. package/package.json +8 -3
  42. package/scripts/helpers/find-project-root.mjs +92 -0
  43. package/scripts/hooks/install-hooks.cjs +136 -36
  44. package/scripts/hooks/run-hook.js +46 -9
  45. package/scripts/mjs/delegation-gate-fixture.mjs +37 -0
  46. package/scripts/mjs/verify-grok-synthesis-gate.mjs +121 -0
  47. package/scripts/node/auto-reflection-generator.mjs +7 -0
  48. package/scripts/node/consumer-gitignore.cjs +101 -0
  49. package/scripts/node/load-reflection-config.mjs +77 -0
  50. package/scripts/node/postinstall.cjs +9 -6
  51. package/src/integrations/grok/plugin/0xray/hooks/hooks.json +2 -1
  52. package/src/mcps/orchestrator/handlers/complexity-handler.ts +33 -8
  53. package/src/mcps/orchestrator/server.ts +181 -5
  54. package/xray/features.json +6 -0
  55. 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) {
@@ -60,6 +60,43 @@ export function seedDelegationGateFixture(tmp) {
60
60
  );
61
61
  }
62
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
+
63
100
  export function seedSpawnTodoPlan(tmp) {
64
101
  writeFileSync(
65
102
  join(tmp, '.xray', 'state', 'lead-dev-plan.json'),
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hook fixture verify — PreToolUse synthesis gate via live Grok hook path.
4
+ * Run: node scripts/mjs/verify-grok-synthesis-gate.mjs
5
+ */
6
+ import { execSync } from 'node:child_process';
7
+ import { mkdtempSync, rmSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import {
12
+ SYNTHESIS_FIXTURE_SESSION_ID,
13
+ seedSynthesisDueFixture,
14
+ } from './delegation-gate-fixture.mjs';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const packageRoot = join(__dirname, '../..');
18
+ const preToolHook = join(packageRoot, 'dist/integrations/grok/hooks/pre-tool-use.js');
19
+
20
+ let failed = 0;
21
+ function pass(n) {
22
+ console.log(`✅ ${n}`);
23
+ }
24
+ function fail(n, d = '') {
25
+ failed++;
26
+ console.error(`❌ ${n}${d ? ` — ${d}` : ''}`);
27
+ }
28
+
29
+ function runHook(fixture, env = {}) {
30
+ const payload = JSON.stringify(fixture);
31
+ const out = execSync(`printf '%s' '${payload.replace(/'/g, "'\\''")}' | node "${preToolHook}"`, {
32
+ encoding: 'utf8',
33
+ env: { ...process.env, ...env },
34
+ });
35
+ return JSON.parse(out.trim());
36
+ }
37
+
38
+ const tmp = mkdtempSync(join(tmpdir(), 'xray-synth-grok-'));
39
+ seedSynthesisDueFixture(tmp);
40
+
41
+ const baseEnv = {
42
+ GROK_WORKSPACE_ROOT: tmp,
43
+ GROK_SESSION_ID: SYNTHESIS_FIXTURE_SESSION_ID,
44
+ };
45
+
46
+ try {
47
+ const denyWrite = runHook(
48
+ {
49
+ toolName: 'search_replace',
50
+ workspaceRoot: tmp,
51
+ sessionId: SYNTHESIS_FIXTURE_SESSION_ID,
52
+ toolInput: { path: 'src/a.ts', new_string: 'x' },
53
+ },
54
+ baseEnv,
55
+ );
56
+ if (denyWrite.decision === 'deny' && denyWrite.gate === 'synthesis-checkpoint') {
57
+ pass('PreToolUse denies write when synthesis due');
58
+ } else {
59
+ fail('synthesis write deny', JSON.stringify(denyWrite));
60
+ }
61
+
62
+ const allowRead = runHook(
63
+ {
64
+ toolName: 'read_file',
65
+ workspaceRoot: tmp,
66
+ sessionId: SYNTHESIS_FIXTURE_SESSION_ID,
67
+ toolInput: { path: 'src/a.ts' },
68
+ },
69
+ baseEnv,
70
+ );
71
+ if (allowRead.decision === 'allow') pass('PreToolUse allows read when synthesis due');
72
+ else fail('synthesis read allow', JSON.stringify(allowRead));
73
+
74
+ const allowConsult = runHook(
75
+ {
76
+ toolName: 'CallMcpTool',
77
+ workspaceRoot: tmp,
78
+ sessionId: SYNTHESIS_FIXTURE_SESSION_ID,
79
+ toolInput: {
80
+ server: 'xray-orchestrator',
81
+ toolName: 'analyze-complexity',
82
+ arguments: { tasks: [{ description: 'synthesis reflect', type: 'plan' }] },
83
+ },
84
+ },
85
+ baseEnv,
86
+ );
87
+ if (allowConsult.decision === 'allow') {
88
+ pass('PreToolUse allows analyze-complexity consult when synthesis due');
89
+ } else {
90
+ fail('synthesis consult allow', JSON.stringify(allowConsult));
91
+ }
92
+
93
+ const denyGovern = runHook(
94
+ {
95
+ toolName: 'CallMcpTool',
96
+ workspaceRoot: tmp,
97
+ sessionId: SYNTHESIS_FIXTURE_SESSION_ID,
98
+ toolInput: {
99
+ server: 'xray-orchestrator',
100
+ toolName: 'govern-and-apply',
101
+ arguments: { proposal: 'test' },
102
+ },
103
+ },
104
+ baseEnv,
105
+ );
106
+ if (denyGovern.decision === 'deny' && denyGovern.gate === 'synthesis-checkpoint') {
107
+ pass('PreToolUse denies govern-and-apply when synthesis due');
108
+ } else {
109
+ fail('synthesis govern deny', JSON.stringify(denyGovern));
110
+ }
111
+ } finally {
112
+ rmSync(tmp, { recursive: true, force: true });
113
+ }
114
+
115
+ console.log(
116
+ '\n' +
117
+ (failed === 0
118
+ ? '🎉 Grok synthesis gate verify passed (4/4).'
119
+ : `⚠️ ${failed} synthesis gate check(s) failed.`),
120
+ );
121
+ process.exit(failed === 0 ? 0 : 1);
@@ -113,6 +113,13 @@ function loadConfig() {
113
113
  try {
114
114
  const content = readFileSync(configPath, "utf-8");
115
115
  const parsed = JSON.parse(content);
116
+ if (parsed.synthesis?.reflection) {
117
+ return {
118
+ mode: parsed.synthesis.reflection.mode ?? "minimal",
119
+ triggers: parsed.synthesis.reflection.triggers,
120
+ thresholds: parsed.synthesis.reflection.thresholds,
121
+ };
122
+ }
116
123
  if (parsed.auto_reflection) {
117
124
  return parsed.auto_reflection;
118
125
  }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Consumer .gitignore — template + idempotent merge into existing .gitignore.
3
+ */
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const MARKER_START = "# --- 0xray suit (postinstall) ---";
8
+ const MARKER_END = "# --- end 0xray suit ---";
9
+
10
+ /** Suit artifacts dropped by postinstall / install-bridges — keep out of product repos */
11
+ const SUIT_IGNORE_LINES = [
12
+ ".grok/",
13
+ ".mcp.json",
14
+ "opencode.json",
15
+ "AGENTS.md",
16
+ "SKILLS.md",
17
+ ".xray/",
18
+ ".strray/",
19
+ "strray/",
20
+ ".hermes/",
21
+ ".opencode/agents/",
22
+ ".opencode/commands/",
23
+ ".opencode/workflows/",
24
+ ".opencode/codex.codex",
25
+ ".opencode/enforcer-config.json",
26
+ ".opencode/skills/",
27
+ ".opencode/scripts/",
28
+ ".opencode/logs/",
29
+ ".opencode/validation/",
30
+ ".opencode/triage/",
31
+ "logs/framework/",
32
+ ];
33
+
34
+ function buildMarkerBlock(lines = SUIT_IGNORE_LINES) {
35
+ return [MARKER_START, ...lines, MARKER_END].join("\n");
36
+ }
37
+
38
+ function stripMarkerBlock(content) {
39
+ const start = content.indexOf(MARKER_START);
40
+ if (start === -1) return content;
41
+ const end = content.indexOf(MARKER_END);
42
+ if (end === -1) {
43
+ // Incomplete marker — preserve content before START; orphan tail handled in merge
44
+ return content.slice(0, start).trimEnd();
45
+ }
46
+ const tail = content.slice(end + MARKER_END.length);
47
+ return `${content.slice(0, start).trimEnd()}${tail}`.trimEnd();
48
+ }
49
+
50
+ function markerBlockIsCurrent(raw) {
51
+ if (!raw.includes(MARKER_START) || !raw.includes(MARKER_END)) {
52
+ return false;
53
+ }
54
+ const start = raw.indexOf(MARKER_START);
55
+ const end = raw.indexOf(MARKER_END) + MARKER_END.length;
56
+ const blockSection = raw.slice(start, end);
57
+ return SUIT_IGNORE_LINES.every((line) => blockSection.includes(line));
58
+ }
59
+
60
+ /**
61
+ * @returns {'created' | 'merged' | 'unchanged'}
62
+ */
63
+ function applyConsumerGitignore(targetDir, packageRoot) {
64
+ const gitignoreSource = path.join(packageRoot, ".gitignore.default");
65
+ const gitignoreDest = path.join(targetDir, ".gitignore");
66
+
67
+ if (!fs.existsSync(gitignoreSource)) {
68
+ return "unchanged";
69
+ }
70
+
71
+ if (!fs.existsSync(gitignoreDest)) {
72
+ fs.copyFileSync(gitignoreSource, gitignoreDest);
73
+ return "created";
74
+ }
75
+
76
+ const raw = fs.readFileSync(gitignoreDest, "utf8");
77
+ if (markerBlockIsCurrent(raw)) {
78
+ return "unchanged";
79
+ }
80
+
81
+ const hasIncompleteMarker =
82
+ raw.includes(MARKER_START) && !raw.includes(MARKER_END);
83
+ const orphanTail = hasIncompleteMarker
84
+ ? raw.slice(raw.indexOf(MARKER_START)).trimEnd()
85
+ : "";
86
+ const content = stripMarkerBlock(raw);
87
+ const next = orphanTail
88
+ ? `${content.trimEnd()}\n\n${buildMarkerBlock()}\n\n${orphanTail}\n`
89
+ : `${content.trimEnd()}\n\n${buildMarkerBlock()}\n`;
90
+ fs.writeFileSync(gitignoreDest, next);
91
+ return "merged";
92
+ }
93
+
94
+ module.exports = {
95
+ MARKER_START,
96
+ MARKER_END,
97
+ SUIT_IGNORE_LINES,
98
+ buildMarkerBlock,
99
+ stripMarkerBlock,
100
+ applyConsumerGitignore,
101
+ };
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Emit reflection config for post-commit hook.
4
+ * SSOT: synthesis.reflection, fallback auto_reflection.
5
+ *
6
+ * Usage:
7
+ * node load-reflection-config.mjs # shell-safe export lines
8
+ * node load-reflection-config.mjs --json # JSON for hook parsers
9
+ */
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ const ALLOWED_MODES = new Set(['minimal', 'full', 'off']);
14
+
15
+ const root = process.env.PROJECT_ROOT || process.cwd();
16
+ const configPath = join(root, '.xray', 'features.json');
17
+
18
+ let mode = 'minimal';
19
+ let commitThreshold = 25;
20
+ let daysThreshold = 14;
21
+ let autoGenerate = true;
22
+ let promptUser = true;
23
+
24
+ if (existsSync(configPath)) {
25
+ try {
26
+ const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
27
+ const reflection = parsed.synthesis?.reflection ?? parsed.auto_reflection;
28
+ if (reflection) {
29
+ const rawMode = reflection.mode ?? 'minimal';
30
+ mode = ALLOWED_MODES.has(rawMode) ? rawMode : 'minimal';
31
+ const thresholds = reflection.thresholds?.[mode];
32
+ if (thresholds) {
33
+ if (typeof thresholds.commit_threshold === 'number') {
34
+ commitThreshold = thresholds.commit_threshold;
35
+ }
36
+ if (typeof thresholds.days_threshold === 'number') {
37
+ daysThreshold = thresholds.days_threshold;
38
+ }
39
+ if (typeof thresholds.auto_generate === 'boolean') {
40
+ autoGenerate = thresholds.auto_generate;
41
+ }
42
+ if (typeof thresholds.prompt_user === 'boolean') {
43
+ promptUser = thresholds.prompt_user;
44
+ }
45
+ } else if (mode === 'full') {
46
+ commitThreshold = 10;
47
+ daysThreshold = 5;
48
+ }
49
+ }
50
+ } catch {
51
+ /* keep defaults */
52
+ }
53
+ }
54
+
55
+ if (mode === 'off') {
56
+ autoGenerate = false;
57
+ promptUser = false;
58
+ }
59
+
60
+ const config = {
61
+ mode,
62
+ commitThreshold,
63
+ daysThreshold,
64
+ autoGenerate,
65
+ promptUser,
66
+ };
67
+
68
+ if (process.argv.includes('--json')) {
69
+ console.log(JSON.stringify(config));
70
+ process.exit(0);
71
+ }
72
+
73
+ console.log(`MODE=${mode}`);
74
+ console.log(`COMMIT_THRESHOLD=${commitThreshold}`);
75
+ console.log(`DAYS_THRESHOLD=${daysThreshold}`);
76
+ console.log(`AUTO_GENERATE=${autoGenerate}`);
77
+ console.log(`PROMPT_USER=${promptUser}`);