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.
- package/.gitignore.default +8 -0
- package/.opencode/hooks/post-commit +7 -27
- package/.xray/features.json +6 -0
- package/.xray/features.schema.json +17 -0
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +1 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/.gitignore.default +8 -0
- package/dist/AGENTS.md +1 -1
- package/dist/CHANGELOG.md +25 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/core/bridge.mjs +10 -32
- package/dist/core/features-config.d.ts +16 -0
- package/dist/core/features-config.js +24 -0
- package/dist/integrations/grok/hooks/grok-hook-utils.js +2 -2
- package/dist/integrations/grok/hooks/post-tool-use.js +1 -1
- package/dist/integrations/grok/hooks/pre-tool-use.js +23 -32
- package/dist/integrations/grok/hooks/session-start.js +20 -1
- package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +2 -1
- package/dist/integrations/hermes-agent/bridge.mjs +22 -50
- package/dist/integrations/hooks/synthesis-hook-runtime.mjs +50 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.d.ts +3 -0
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +25 -10
- package/dist/mcps/orchestrator/server.js +124 -4
- package/dist/memory-routing/types.d.ts +7 -0
- package/dist/nucleus/autonomy-kernel.d.ts +3 -0
- package/dist/nucleus/autonomy-kernel.js +40 -0
- package/dist/nucleus/delegation-gate.d.ts +6 -4
- package/dist/nucleus/delegation-gate.js +77 -5
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +3 -0
- package/dist/nucleus/lead-dev-plan-persistence.js +44 -1
- package/dist/nucleus/synthesis-completion.d.ts +6 -0
- package/dist/nucleus/synthesis-completion.js +29 -0
- package/dist/nucleus/synthesis-context.d.ts +31 -0
- package/dist/nucleus/synthesis-context.js +86 -0
- package/dist/nucleus/synthesis.d.ts +54 -0
- package/dist/nucleus/synthesis.js +196 -0
- package/dist/plugin/xray-codex-injection.js +17 -0
- package/package.json +8 -3
- package/scripts/helpers/find-project-root.mjs +92 -0
- package/scripts/hooks/install-hooks.cjs +136 -36
- package/scripts/hooks/run-hook.js +46 -9
- package/scripts/mjs/delegation-gate-fixture.mjs +37 -0
- package/scripts/mjs/verify-grok-synthesis-gate.mjs +121 -0
- package/scripts/node/auto-reflection-generator.mjs +7 -0
- package/scripts/node/consumer-gitignore.cjs +101 -0
- package/scripts/node/load-reflection-config.mjs +77 -0
- package/scripts/node/postinstall.cjs +9 -6
- package/src/integrations/grok/plugin/0xray/hooks/hooks.json +2 -1
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +33 -8
- package/src/mcps/orchestrator/server.ts +181 -5
- package/xray/features.json +6 -0
- 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
|
|
3
|
+
* install-hooks.cjs — Installs xray pre-commit + post-commit hooks into consumer .git/hooks/.
|
|
4
4
|
*
|
|
5
|
-
* Called from
|
|
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
|
-
|
|
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 $
|
|
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 "
|
|
73
|
-
exit
|
|
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 "
|
|
77
|
-
exit
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
console.
|
|
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
|
-
*
|
|
98
|
-
*
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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}`);
|