chore(agent hooks): remind on new React effects and memos

This commit is contained in:
Tommaso Casaburi
2026-04-15 13:31:13 +07:00
parent 431f84b871
commit 3fc9f0fa19
9 changed files with 217 additions and 0 deletions
+8
View File
@@ -9,6 +9,10 @@
{
"command": ".claude/hooks/yarn-install.sh",
"timeout": 120
},
{
"command": ".claude/hooks/react-pattern-review.sh",
"timeout": 10
}
],
"stop": [
@@ -16,6 +20,10 @@
"command": ".claude/hooks/sync-git-branches.sh",
"timeout": 60
},
{
"command": ".claude/hooks/react-pattern-review.sh",
"timeout": 10
},
{
"command": ".claude/hooks/verify.sh",
"timeout": 60
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
exec "$repo_root/scripts/agent-hooks/react-pattern-review.sh" --skill-dir .claude/skills --scope-prefix src/ "$@"
+8
View File
@@ -9,6 +9,10 @@
{
"command": ".codex/hooks/yarn-install.sh",
"timeout": 120
},
{
"command": ".codex/hooks/react-pattern-review.sh",
"timeout": 10
}
],
"stop": [
@@ -16,6 +20,10 @@
"command": ".codex/hooks/sync-git-branches.sh",
"timeout": 60
},
{
"command": ".codex/hooks/react-pattern-review.sh",
"timeout": 10
},
{
"command": ".codex/hooks/verify.sh",
"timeout": 60
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
exec "$repo_root/scripts/agent-hooks/react-pattern-review.sh" --skill-dir .codex/skills --scope-prefix src/ "$@"
+8
View File
@@ -9,6 +9,10 @@
{
"command": ".cursor/hooks/yarn-install.sh",
"timeout": 120
},
{
"command": ".cursor/hooks/react-pattern-review.sh",
"timeout": 10
}
],
"stop": [
@@ -16,6 +20,10 @@
"command": ".cursor/hooks/sync-git-branches.sh",
"timeout": 60
},
{
"command": ".cursor/hooks/react-pattern-review.sh",
"timeout": 10
},
{
"command": ".cursor/hooks/verify.sh",
"timeout": 60
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
exec "$repo_root/scripts/agent-hooks/react-pattern-review.sh" --skill-dir .cursor/skills --scope-prefix src/ "$@"
+1
View File
@@ -144,6 +144,7 @@ src/
- When changing shared agent behavior, update the relevant files in `.codex/skills/`, `.cursor/skills/`, `.claude/skills/`, `.codex/agents/`, `.cursor/agents/`, `.claude/agents/`, `.codex/hooks/`, `.cursor/hooks/`, `.claude/hooks/`, and their `hooks.json` or config entry points as needed.
- If `AGENTS.md` references a skill, agent, or hook, prefer a tracked file under `.codex/`, `.cursor/`, or `.claude/` rather than an untracked local-only instruction.
- Review `.codex/config.toml`, `.cursor/hooks.json`, and `.claude/hooks.json` before changing agent orchestration or hook behavior, because they are the entry points contributors will actually load.
- When a diff adds new `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useMemo`, `useCallback`, or `memo(...)` usage under `src/`, treat the repo hook reminder as mandatory and reconsider the change with `you-might-not-need-an-effect` and `vercel-react-best-practices` before finishing.
- Directory-specific auto-loaded rules live under `src/AGENTS.md` and `scripts/AGENTS.md`; read them before editing files in those trees.
- For work expected to span multiple sessions, keep explicit task state in a `feature-list.json` plus `progress.md` pair using `docs/agent-playbooks/long-running-agent-workflow.md`.
- If more than one human or toolchain needs the same task state, keep it in a tracked location such as `docs/agent-runs/<slug>/` instead of burying it in a tool-specific hidden directory.
+3
View File
@@ -8,13 +8,16 @@ If your AI coding assistant supports lifecycle hooks, configure these for this r
|---|---|---|
| `afterFileEdit` | `scripts/agent-hooks/format.sh` | Auto-format files after AI edits |
| `afterFileEdit` | `scripts/agent-hooks/yarn-install.sh` | Run `corepack yarn install` when `package.json` changes |
| `afterFileEdit` | `scripts/agent-hooks/react-pattern-review.sh` | When a diff adds `useEffect`/memo primitives in `src/`, remind the agent to reconsider with the React review skills |
| `stop` | `scripts/agent-hooks/sync-git-branches.sh` | Prune stale refs and delete integrated temporary task branches |
| `stop` | `scripts/agent-hooks/react-pattern-review.sh` | Re-scan the current diff for new React effects/memos before the final verify gate |
| `stop` | `scripts/agent-hooks/verify.sh` | Hard-gate build, lint, and type-check; keep `yarn audit` informational |
## Why
- Consistent formatting
- Lockfile stays in sync
- New `useEffect`/memo additions get an explicit second look before the agent finishes
- Build/lint/type issues caught early
- Security visibility via `corepack yarn npm audit`
- One shared hook implementation for both Codex and Cursor
+177
View File
@@ -0,0 +1,177 @@
#!/bin/bash
# afterFileEdit/stop hook: remind agents to review new React effects and memoization
set -u
input="$(cat)"
skill_dir=""
scope_prefixes=()
while [ "$#" -gt 0 ]; do
case "$1" in
--skill-dir)
skill_dir="${2:-}"
shift 2
;;
--scope-prefix)
scope_prefixes+=("${2:-}")
shift 2
;;
*)
shift
;;
esac
done
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$repo_root" || exit 0
extract_file_path() {
if command -v jq >/dev/null 2>&1; then
printf '%s' "$input" | jq -r '.file_path // empty' 2>/dev/null
return
fi
echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:.*"\([^"]*\)"/\1/'
}
is_source_file() {
case "$1" in
*.js|*.jsx|*.ts|*.tsx|*.mjs|*.cjs) return 0 ;;
*) return 1 ;;
esac
}
matches_scope() {
local candidate="$1"
if [ "${#scope_prefixes[@]}" -eq 0 ]; then
return 0
fi
local prefix
for prefix in "${scope_prefixes[@]}"; do
case "$candidate" in
"$prefix"*) return 0 ;;
esac
done
return 1
}
parse_matches_from_diff() {
awk '
/^\+\+\+ b\// {
file = substr($0, 7)
next
}
/^\+[^+]/ {
line = substr($0, 2)
if (line ~ /(^|[^[:alnum:]_])(useEffect|useLayoutEffect|useInsertionEffect|useMemo|useCallback)[[:space:]]*[(<]/ || line ~ /(^|[^[:alnum:]_])React\.(useEffect|useLayoutEffect|useInsertionEffect|useMemo|useCallback|memo)[[:space:]]*[(<]/ || line ~ /(^|[^[:alnum:]_])memo[[:space:]]*[(<]/) {
print file ": " line
}
}
'
}
scan_untracked_file() {
local file_path="$1"
[ -f "$file_path" ] || return 0
awk -v file="$file_path" '
{
line = $0
if (line ~ /(^|[^[:alnum:]_])(useEffect|useLayoutEffect|useInsertionEffect|useMemo|useCallback)[[:space:]]*[(<]/ || line ~ /(^|[^[:alnum:]_])React\.(useEffect|useLayoutEffect|useInsertionEffect|useMemo|useCallback|memo)[[:space:]]*[(<]/ || line ~ /(^|[^[:alnum:]_])memo[[:space:]]*[(<]/) {
print file ": " line
}
}
' "$file_path"
}
append_results() {
local existing="$1"
local incoming="$2"
if [ -z "$incoming" ]; then
printf '%s' "$existing"
return
fi
if [ -z "$existing" ]; then
printf '%s' "$incoming"
return
fi
printf '%s\n%s' "$existing" "$incoming"
}
results=""
file_path="$(extract_file_path)"
if [ -n "$file_path" ]; then
if is_source_file "$file_path" && matches_scope "$file_path"; then
if git ls-files --others --exclude-standard -- "$file_path" | grep -q '.'; then
results="$(scan_untracked_file "$file_path")"
else
diff_output="$(git diff --no-ext-diff --unified=0 --no-color HEAD -- "$file_path" 2>/dev/null || true)"
results="$(printf '%s\n' "$diff_output" | parse_matches_from_diff)"
fi
fi
else
diff_output="$(git diff --no-ext-diff --unified=0 --no-color HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.mjs' '*.cjs' 2>/dev/null || true)"
results="$(printf '%s\n' "$diff_output" | parse_matches_from_diff)"
while IFS= read -r untracked_file; do
[ -z "$untracked_file" ] && continue
is_source_file "$untracked_file" || continue
matches_scope "$untracked_file" || continue
file_results="$(scan_untracked_file "$untracked_file")"
results="$(append_results "$results" "$file_results")"
done < <(git ls-files --others --exclude-standard -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.mjs' '*.cjs')
fi
results="$(printf '%s\n' "$results" | sed '/^$/d' | awk '!seen[$0]++')"
if [ -z "$results" ]; then
exit 0
fi
effect_skill="you-might-not-need-an-effect"
if [ -n "$skill_dir" ] && [ -f "$repo_root/$skill_dir/you-might-not-need-an-effect/SKILL.md" ]; then
effect_skill="$repo_root/$skill_dir/you-might-not-need-an-effect/SKILL.md"
fi
vercel_skill="vercel-react-best-practices"
if [ -n "$skill_dir" ] && [ -f "$repo_root/$skill_dir/vercel-react-best-practices/SKILL.md" ]; then
vercel_skill="$repo_root/$skill_dir/vercel-react-best-practices/SKILL.md"
fi
echo "=== React Hook Review Reminder ==="
echo "New React effect or memo primitives were added in the current diff:"
match_count=0
while IFS= read -r match_line; do
[ -z "$match_line" ] && continue
match_count=$((match_count + 1))
if [ "$match_count" -le 10 ]; then
echo "- $match_line"
fi
done <<< "$results"
if [ "$match_count" -gt 10 ]; then
echo "- ... and $((match_count - 10)) more"
fi
echo "Reconsider this change with:"
echo "- $effect_skill"
echo "- $vercel_skill"
echo "Questions to resolve before finishing:"
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 "- Is the memoization actually needed, or is simpler render-time code better?"
exit 0