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.
- 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 +32 -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 +32 -385
- 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/__init__.py +50 -3
- package/dist/integrations/hermes-agent/bridge.mjs +71 -30
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +75 -0
- 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 +88 -0
- package/dist/nucleus/delegation-gate.js +347 -0
- package/dist/nucleus/lead-dev-plan-persistence.d.ts +3 -0
- package/dist/nucleus/lead-dev-plan-persistence.js +47 -4
- 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 +50 -0
- package/package.json +11 -2
- 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 +121 -0
- package/scripts/mjs/verify-delegation-gate-core.mjs +99 -0
- package/scripts/mjs/verify-grok-delegation-gate.mjs +9 -69
- package/scripts/mjs/verify-hermes-delegation-gate.mjs +89 -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/scripts/node/universal-version-manager.js +1 -1
- 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) {
|
|
@@ -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,
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
{
|