Files
actual/scripts/agent-hooks/check-on-stop.sh
Matiss Janis Aboltins 328417f8de [AI] Consolidate agent tooling and simplify developer guidance (#8089)
* [AI] Replace mechanical agent rules with shared agent hooks + nano-staged

Move the mechanical "remember to run X" rules out of AGENTS.md and the
agent guidance docs into deterministic, cross-agent hooks, and migrate
the pre-commit runner from lint-staged to nano-staged.

- Add shared hook scripts in scripts/agent-hooks/ (git-guard,
  format-edited-file, no-strict-ignore-new-file, prefer-one-component,
  check-on-stop, common helpers) wired for Claude (.claude/settings.json),
  Codex (.codex/config.toml), and Cursor (.cursor/hooks.json + adapters).
- Enforce "avoid enum" via a new actual/no-enum lint rule, grandfathering
  the two existing declarations in .oxlintrc.json overrides.
- Migrate lint-staged -> nano-staged (.nano-staged.json, .husky/pre-commit,
  package.json).
- Trim AGENTS.md, .github/agents/pr-and-commit-rules.md, the Cursor rule,
  and the committing skill down to the rules that aren't auto-enforced.
- Add release note 8089.md.

* [AI] Align PR-title wording in AGENTS.md with canonical rules

* [AI] Fix check-on-stop hook on bash 3.2 and TSV column collapse

The Stop hook silently no-op'd in two ways:

- `declare -A` isn't supported on stock macOS bash 3.2, and the
  slash/hyphen path subscript aborted under `set -u`. Dedupe via a
  space-padded string match instead.
- jq emitted an empty middle TSV field for a package with test but no
  typecheck; tab is IFS-whitespace so `read` collapsed it and shifted the
  columns (eslint-plugin-actual then ran neither). Emit non-empty
  sentinels (-, yes/no) so no field can collapse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-08 20:54:54 +00:00

101 lines
3.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Agent hook (Stop), shared across Claude and Codex.
#
# When the agent finishes its turn, run typecheck + test for each workspace this
# branch has touched (scoped — far cheaper than the whole-monorepo check).
# Failures are surfaced back to the agent via exit code 2 so it can fix them.
# Adapted from a suggestion by @StephenBrown2 on PR #8089.
set -uo pipefail
# Read the hook payload (Claude provides stop_hook_active to prevent loops).
payload=$(cat 2>/dev/null || true)
if printf '%s' "$payload" | jq -e '.stop_hook_active == true' >/dev/null 2>&1; then
exit 0
fi
# Resolve the repo root regardless of which agent invoked us.
. "$(dirname "$0")/common.sh"
ROOT=$(resolve_repo_root)
cd "$ROOT" 2>/dev/null || exit 0
# Skip if dependencies aren't installed.
[ -f "$ROOT/node_modules/.yarn-state.yml" ] || exit 0
# Collect changed files, NUL-delimited so paths with spaces survive: this
# branch's commits vs master, plus tracked + untracked working-tree changes.
base=$(git merge-base HEAD origin/master 2>/dev/null || true)
files=()
while IFS= read -r -d '' f; do
[ -n "$f" ] && files+=("$f")
done < <(
{
[ -n "$base" ] && git diff -z --name-only "$base"...HEAD
git diff -z --name-only HEAD
git ls-files -z --others --exclude-standard
} 2>/dev/null
)
[ "${#files[@]}" -gt 0 ] || exit 0
# Map changed source files to the workspace dir that owns them (deduped).
# Stock macOS ships bash 3.2, which has no associative arrays (`declare -A`) and
# evaluates `arr[$d]` subscripts as arithmetic (slashes/hyphens in a path then
# blow up under `set -u`). So dedupe via a space-padded string match instead —
# `ws_dirs` stays a plain indexed array, which bash 3.2 handles fine.
seen=" "
ws_dirs=()
for f in "${files[@]}"; do
case "$f" in
*.js | *.mjs | *.jsx | *.ts | *.tsx) ;;
*) continue ;;
esac
d=$(dirname "$f")
while [ "$d" != "." ] && [ "$d" != "/" ]; do
if [ -f "$ROOT/$d/package.json" ]; then
case "$seen" in
*" $d "*) ;;
*)
seen="$seen$d "
ws_dirs+=("$d")
;;
esac
break
fi
d=$(dirname "$d")
done
done
[ "${#ws_dirs[@]}" -gt 0 ] || exit 0
fail=0
report=""
for d in "${ws_dirs[@]}"; do
# One jq read per package.json: name + which scripts exist. Every field is a
# non-empty token (the name falls back to "-", the flags to yes/no) so that
# `read` can't collapse an empty middle field — tab is IFS-whitespace, so a
# package with test-but-no-typecheck would otherwise shift the columns.
IFS=$'\t' read -r name tc te < <(
jq -r '[.name // "-",
(if .scripts.typecheck then "yes" else "no" end),
(if .scripts.test then "yes" else "no" end)] | @tsv' "$ROOT/$d/package.json"
)
[ "$name" != "-" ] || continue
if [ "$tc" = "yes" ]; then
if ! out=$(yarn workspace "$name" run typecheck 2>&1); then
report="$report"$'\n'"[typecheck failed: $name]"$'\n'"$(printf '%s' "$out" | tail -25)"
fail=1
fi
fi
if [ "$te" = "yes" ]; then
if ! out=$(yarn workspace "$name" run test 2>&1); then
report="$report"$'\n'"[tests failed: $name]"$'\n'"$(printf '%s' "$out" | tail -25)"
fail=1
fi
fi
done
if [ "$fail" -eq 1 ]; then
printf 'Stop check found failures in touched workspaces:%s\n' "$report" >&2
exit 2
fi
exit 0