mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(agent hooks): wire hooks into real harness entry points and fix payload parsing
Claude Code never reads a standalone .claude/hooks.json, so the PostToolUse and Stop pipeline is moved into .claude/settings.json and hooks.json is removed; .cursor/hooks.json is rewritten in Cursor's version+afterFileEdit/stop schema; .codex/hooks.json is already Codex-valid and stays. The shared scripts now parse both the Cursor file_path and Claude/Codex tool_input.file_path stdin shapes and normalize absolute paths, so the format, yarn-install, and react-pattern-review hooks stop being silent no-ops. verify.sh blocks with exit 2 plus a stderr reason, guards stop_hook_active, and skips clean trees; react-pattern-review surfaces its reminder via hookSpecificOutput on PostToolUse; sync-git-branches no longer misreports open PRs as merged. The validator now checks that the three harness-specific entry points wire the same hook scripts instead of requiring byte-identical hooks.json copies.
This commit is contained in:
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"hooks": {
|
|
||||||
"PostToolUse": [
|
|
||||||
{
|
|
||||||
"matcher": "Edit|Write|apply_patch",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.claude/hooks/format.sh\"",
|
|
||||||
"timeout": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.claude/hooks/yarn-install.sh\"",
|
|
||||||
"timeout": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.claude/hooks/react-pattern-review.sh\"",
|
|
||||||
"timeout": 10
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Stop": [
|
|
||||||
{
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.claude/hooks/sync-git-branches.sh\"",
|
|
||||||
"timeout": 60
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.claude/hooks/react-pattern-review.sh\"",
|
|
||||||
"timeout": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.claude/hooks/code-quality-review-reminder.sh\"",
|
|
||||||
"timeout": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.claude/hooks/verify.sh\"",
|
|
||||||
"timeout": 60
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,54 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh",
|
||||||
|
"timeout": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/yarn-install.sh",
|
||||||
|
"timeout": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/react-pattern-review.sh",
|
||||||
|
"timeout": 15
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Stop": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/sync-git-branches.sh",
|
||||||
|
"timeout": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/react-pattern-review.sh",
|
||||||
|
"timeout": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/code-quality-review-reminder.sh",
|
||||||
|
"timeout": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify.sh",
|
||||||
|
"timeout": 600
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-46
@@ -1,52 +1,16 @@
|
|||||||
{
|
{
|
||||||
|
"version": 1,
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"PostToolUse": [
|
"afterFileEdit": [
|
||||||
{
|
{ "command": "./.cursor/hooks/format.sh" },
|
||||||
"matcher": "Edit|Write|apply_patch",
|
{ "command": "./.cursor/hooks/yarn-install.sh" },
|
||||||
"hooks": [
|
{ "command": "./.cursor/hooks/react-pattern-review.sh" }
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.cursor/hooks/format.sh\"",
|
|
||||||
"timeout": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.cursor/hooks/yarn-install.sh\"",
|
|
||||||
"timeout": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.cursor/hooks/react-pattern-review.sh\"",
|
|
||||||
"timeout": 10
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"Stop": [
|
"stop": [
|
||||||
{
|
{ "command": "./.cursor/hooks/sync-git-branches.sh" },
|
||||||
"hooks": [
|
{ "command": "./.cursor/hooks/react-pattern-review.sh" },
|
||||||
{
|
{ "command": "./.cursor/hooks/code-quality-review-reminder.sh" },
|
||||||
"type": "command",
|
{ "command": "./.cursor/hooks/verify.sh" }
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.cursor/hooks/sync-git-branches.sh\"",
|
|
||||||
"timeout": 60
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.cursor/hooks/react-pattern-review.sh\"",
|
|
||||||
"timeout": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.cursor/hooks/code-quality-review-reminder.sh\"",
|
|
||||||
"timeout": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"$(git rev-parse --show-toplevel)/.cursor/hooks/verify.sh\"",
|
|
||||||
"timeout": 60
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# afterFileEdit hook: Auto-format files after AI edits them
|
# afterFileEdit/PostToolUse hook: Auto-format files after AI edits them
|
||||||
# Receives JSON via stdin: {"file_path": "...", "edits": [...]}
|
# Stdin JSON differs per harness:
|
||||||
|
# Cursor afterFileEdit: {"file_path": "...", "edits": [...]}
|
||||||
|
# Claude/Codex PostToolUse: {"tool_input": {"file_path": "..."}, ...}
|
||||||
|
|
||||||
input=$(cat)
|
input=$(cat)
|
||||||
if ! command -v jq >/dev/null 2>&1; then
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
file_path=$(printf '%s' "$input" | jq -r '.file_path // empty' 2>/dev/null)
|
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .file_path // empty' 2>/dev/null)
|
||||||
|
|
||||||
if [ -z "$file_path" ]; then
|
if [ -z "$file_path" ]; then
|
||||||
exit 0
|
exit 0
|
||||||
@@ -17,8 +19,15 @@ fi
|
|||||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
cd "$repo_root" || exit 0
|
cd "$repo_root" || exit 0
|
||||||
|
|
||||||
|
# Claude Code and Codex deliver absolute paths; make them repo-relative and
|
||||||
|
# skip anything outside the repo.
|
||||||
case "$file_path" in
|
case "$file_path" in
|
||||||
*.js|*.ts|*.tsx|*.mjs)
|
"$repo_root"/*) file_path="${file_path#"$repo_root"/}" ;;
|
||||||
|
/*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$file_path" in
|
||||||
|
*.js|*.jsx|*.cjs|*.mjs|*.ts|*.tsx)
|
||||||
dir_part="${file_path%/*}"
|
dir_part="${file_path%/*}"
|
||||||
base_name="${file_path##*/}"
|
base_name="${file_path##*/}"
|
||||||
if [ "$dir_part" = "$file_path" ]; then
|
if [ "$dir_part" = "$file_path" ]; then
|
||||||
|
|||||||
@@ -28,13 +28,26 @@ done
|
|||||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
cd "$repo_root" || exit 0
|
cd "$repo_root" || exit 0
|
||||||
|
|
||||||
|
# Cursor afterFileEdit sends {"file_path": ...}; Claude/Codex PostToolUse send
|
||||||
|
# {"tool_input": {"file_path": ...}} with an absolute path.
|
||||||
extract_file_path() {
|
extract_file_path() {
|
||||||
if command -v jq >/dev/null 2>&1; then
|
if command -v jq >/dev/null 2>&1; then
|
||||||
printf '%s' "$input" | jq -r '.file_path // empty' 2>/dev/null
|
printf '%s' "$input" | jq -r '.tool_input.file_path // .file_path // empty' 2>/dev/null
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:.*"\([^"]*\)"/\1/'
|
echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\([^"]*\)"/\1/'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make absolute paths repo-relative so scope prefixes and src/ checks match.
|
||||||
|
normalize_file_path() {
|
||||||
|
local candidate="$1"
|
||||||
|
|
||||||
|
case "$candidate" in
|
||||||
|
"$repo_root"/*) printf '%s' "${candidate#"$repo_root"/}" ;;
|
||||||
|
/*) printf '' ;;
|
||||||
|
*) printf '%s' "$candidate" ;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
is_source_file() {
|
is_source_file() {
|
||||||
@@ -130,7 +143,13 @@ append_file_if_react_ui_source() {
|
|||||||
|
|
||||||
results=""
|
results=""
|
||||||
react_source_files=""
|
react_source_files=""
|
||||||
file_path="$(extract_file_path)"
|
raw_file_path="$(extract_file_path)"
|
||||||
|
file_path="$(normalize_file_path "$raw_file_path")"
|
||||||
|
|
||||||
|
# A per-file event for a file outside the repo is not ours to review.
|
||||||
|
if [ -n "$raw_file_path" ] && [ -z "$file_path" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$file_path" ]; then
|
if [ -n "$file_path" ]; then
|
||||||
if is_source_file "$file_path" && matches_scope "$file_path"; then
|
if is_source_file "$file_path" && matches_scope "$file_path"; then
|
||||||
@@ -178,6 +197,7 @@ if [ -n "$skill_dir" ] && [ -f "$repo_root/$skill_dir/vercel-react-best-practice
|
|||||||
vercel_skill="$repo_root/$skill_dir/vercel-react-best-practices/SKILL.md"
|
vercel_skill="$repo_root/$skill_dir/vercel-react-best-practices/SKILL.md"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
build_report() {
|
||||||
echo "=== React Best Practices Review Reminder ==="
|
echo "=== React Best Practices Review Reminder ==="
|
||||||
|
|
||||||
if [ -n "$react_source_files" ]; then
|
if [ -n "$react_source_files" ]; then
|
||||||
@@ -226,5 +246,23 @@ echo "- Does the TSX avoid inline object/array prop churn and unnecessary compon
|
|||||||
echo "- Can this be derived during render instead of synchronized with an effect?"
|
echo "- Can this be derived during render instead of synchronized with an effect?"
|
||||||
echo "- Can interaction logic move to an event handler or a key-based reset?"
|
echo "- Can interaction logic move to an event handler or a key-based reset?"
|
||||||
echo "- Is the memoization actually needed, or is simpler render-time code better?"
|
echo "- Is the memoization actually needed, or is simpler render-time code better?"
|
||||||
|
}
|
||||||
|
|
||||||
|
report="$(build_report)"
|
||||||
|
|
||||||
|
# On Claude Code and Codex PostToolUse events, plain stdout with exit 0 is
|
||||||
|
# transcript-only and never reaches the model; hookSpecificOutput JSON does.
|
||||||
|
# Cursor's afterFileEdit/stop events send no hook_event_name, so they keep
|
||||||
|
# getting the plain-text report.
|
||||||
|
hook_event_name=""
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
hook_event_name="$(printf '%s' "$input" | jq -r '.hook_event_name // empty' 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$hook_event_name" = "PostToolUse" ] && command -v jq >/dev/null 2>&1; then
|
||||||
|
jq -cn --arg ctx "$report" '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $ctx}}'
|
||||||
|
else
|
||||||
|
printf '%s\n' "$report"
|
||||||
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ branch_has_live_upstream() {
|
|||||||
merged_pr_number_for_branch() {
|
merged_pr_number_for_branch() {
|
||||||
local branch="$1"
|
local branch="$1"
|
||||||
local pr_number=""
|
local pr_number=""
|
||||||
|
local merged_at=""
|
||||||
|
|
||||||
if ! command -v gh >/dev/null 2>&1; then
|
if ! command -v gh >/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
@@ -60,7 +61,10 @@ merged_pr_number_for_branch() {
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
if [ -n "$pr_number" ]; then
|
if [ -n "$pr_number" ]; then
|
||||||
gh pr view "$pr_number" --repo bitsocialnet/5chan --json mergedAt --jq 'select(.mergedAt != null) | .mergedAt' >/dev/null 2>&1 || return 0
|
# gh exits 0 even when the PR exists but is unmerged (the jq select just
|
||||||
|
# produces no output), so test the output instead of the exit code.
|
||||||
|
merged_at="$(gh pr view "$pr_number" --repo bitsocialnet/5chan --json mergedAt --jq 'select(.mergedAt != null) | .mergedAt' 2>/dev/null || true)"
|
||||||
|
[ -n "$merged_at" ] || return 0
|
||||||
echo "$pr_number"
|
echo "$pr_number"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -11,10 +11,24 @@ if [ "${1:-}" = "--advisory" ]; then
|
|||||||
shift
|
shift
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > /dev/null
|
input="$(cat)"
|
||||||
|
|
||||||
|
# Avoid infinite stop loops: when a previous blocking verify already forced the
|
||||||
|
# agent to continue, Claude Code/Codex set stop_hook_active on the next Stop.
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
if [ "$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)" = "true" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" || exit 0
|
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" || exit 0
|
||||||
|
|
||||||
|
# Read-only sessions change nothing; skip the expensive full verification.
|
||||||
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && [ -z "$(git status --porcelain 2>/dev/null)" ]; then
|
||||||
|
echo "Working tree clean; skipping verification."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
cleanup_generated_dir() {
|
cleanup_generated_dir() {
|
||||||
local path="$1"
|
local path="$1"
|
||||||
|
|
||||||
@@ -76,8 +90,11 @@ if [ "$failures" -ne 0 ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Verification failed."
|
# Exit 2 is the only exit code that blocks the stop and feeds the reason back
|
||||||
exit 1
|
# to the agent in Claude Code and Codex; exit 1 would be a silent, non-blocking
|
||||||
|
# error. The full logs are on stdout above; keep the stderr reason short.
|
||||||
|
echo "Verification failed: build, lint, or type-check reported errors (see hook output). Fix them before finishing, or rerun with AGENT_VERIFY_MODE=advisory to intentionally stop on a broken tree." >&2
|
||||||
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Verification complete."
|
echo "Verification complete."
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# afterFileEdit hook: Run Corepack-managed Yarn install when package.json is changed
|
# afterFileEdit/PostToolUse hook: Run Corepack-managed Yarn install when package.json is changed
|
||||||
# Receives JSON via stdin: {"file_path": "...", "edits": [...]}
|
# Stdin JSON differs per harness:
|
||||||
|
# Cursor afterFileEdit: {"file_path": "...", "edits": [...]}
|
||||||
|
# Claude/Codex PostToolUse: {"tool_input": {"file_path": "..."}, ...}
|
||||||
|
|
||||||
input=$(cat)
|
input=$(cat)
|
||||||
file_path=$(echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:.*"\([^"]*\)"/\1/')
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .file_path // empty' 2>/dev/null)
|
||||||
|
else
|
||||||
|
file_path=$(echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\([^"]*\)"/\1/')
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$file_path" ]; then
|
if [ -z "$file_path" ]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$file_path" = "package.json" ]; then
|
|
||||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
|
|
||||||
|
# Claude Code and Codex deliver absolute paths; compare repo-relative so the
|
||||||
|
# root package.json matches in every harness.
|
||||||
|
case "$file_path" in
|
||||||
|
"$repo_root"/*) file_path="${file_path#"$repo_root"/}" ;;
|
||||||
|
/*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$file_path" = "package.json" ]; then
|
||||||
cd "$repo_root" || exit 0
|
cd "$repo_root" || exit 0
|
||||||
echo "package.json changed - running corepack yarn install to update yarn.lock..."
|
echo "package.json changed - running corepack yarn install to update yarn.lock..."
|
||||||
corepack yarn install
|
corepack yarn install
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
* every toolchain
|
* every toolchain
|
||||||
* - mirrored files are identical after normalizing toolchain-specific
|
* - mirrored files are identical after normalizing toolchain-specific
|
||||||
* tokens (.claude/.codex/.cursor path prefixes, agent model lines)
|
* tokens (.claude/.codex/.cursor path prefixes, agent model lines)
|
||||||
|
* - hook entry points (harness-specific formats: .claude/settings.json,
|
||||||
|
* .cursor/hooks.json, .codex/hooks.json) wire the same hook scripts
|
||||||
* - SKILL.md frontmatter has a name matching its directory and a
|
* - SKILL.md frontmatter has a name matching its directory and a
|
||||||
* non-empty description
|
* non-empty description
|
||||||
* - agent model rules: no composer-* models in .claude agents, no
|
* - agent model rules: no composer-* models in .claude agents, no
|
||||||
@@ -234,18 +236,41 @@ for (const hook of allHooks) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hooks.json entry points must mirror each other.
|
// Hook entry points are harness-specific formats and are NOT byte-mirrored:
|
||||||
|
// .claude/settings.json — Claude Code only reads hooks from settings files,
|
||||||
|
// never from a standalone hooks.json
|
||||||
|
// .cursor/hooks.json — Cursor schema ({"version": 1, "hooks": {...}} with
|
||||||
|
// afterFileEdit/stop event names)
|
||||||
|
// .codex/hooks.json — Codex schema (intentionally Claude-compatible:
|
||||||
|
// PostToolUse/Stop, matcher, type "command")
|
||||||
|
// Instead of mirroring content, require every entry point to wire the same set
|
||||||
|
// of hooks/<name>.sh scripts (modulo SINGLE_TOOLCHAIN_HOOKS exemptions).
|
||||||
{
|
{
|
||||||
const holders = TOOLCHAINS.filter((tc) => exists(path.join(repoRoot, tc, 'hooks.json')));
|
const entryPoints = new Map([
|
||||||
for (const tc of TOOLCHAINS) {
|
['.claude', 'settings.json'],
|
||||||
if (!holders.includes(tc)) errors.push(`missing file: ${tc}/hooks.json`);
|
['.codex', 'hooks.json'],
|
||||||
|
['.cursor', 'hooks.json'],
|
||||||
|
]);
|
||||||
|
const wired = new Map();
|
||||||
|
for (const [tc, file] of entryPoints) {
|
||||||
|
const p = path.join(repoRoot, tc, file);
|
||||||
|
if (!exists(p)) {
|
||||||
|
errors.push(`missing hook entry point: ${tc}/${file}`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
const reference = holders[0];
|
const names = [...read(p).matchAll(/hooks\/([A-Za-z0-9._-]+\.sh)/g)].map((m) => m[1]);
|
||||||
for (const tc of holders.slice(1)) {
|
wired.set(tc, new Set(names));
|
||||||
const a = normalize(read(path.join(repoRoot, reference, 'hooks.json')));
|
}
|
||||||
const b = normalize(read(path.join(repoRoot, tc, 'hooks.json')));
|
const allWired = [...new Set([...wired.values()].flatMap((s) => [...s]))].sort();
|
||||||
if (a !== b) {
|
for (const hook of allWired) {
|
||||||
errors.push(`content drift: ${tc}/hooks.json differs from ${reference}/hooks.json`);
|
const holders = [...wired].filter(([, names]) => names.has(hook)).map(([tc]) => tc);
|
||||||
|
for (const tc of wired.keys()) {
|
||||||
|
if (holders.includes(tc)) continue;
|
||||||
|
const onlyCopy = holders.length === 1 ? `${holders[0]}/hooks/${hook}` : null;
|
||||||
|
if (onlyCopy && SINGLE_TOOLCHAIN_HOOKS.has(onlyCopy)) continue;
|
||||||
|
errors.push(
|
||||||
|
`hook not wired: ${tc}/${entryPoints.get(tc)} does not reference hooks/${hook} (wired in ${holders.join(', ')})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user