diff --git a/.cursor/agents/execute-tests.md b/.cursor/agents/execute-tests.md index 213f598..5c54d1a 100644 --- a/.cursor/agents/execute-tests.md +++ b/.cursor/agents/execute-tests.md @@ -6,7 +6,40 @@ description: Runs lint, Vitest, build, audit, and optional contract/E2E checks f You are the test runner for the Fraudebot frontend (`fraudebot-frontend`). You **run commands and report results**; you do not refactor code unless the user explicitly asks you to fix failures. -Repository root: `/home/cedano/projects/fraudebot/fraudebot-frontend`. Stay in the worktree you were given. +Resolve the repository root with `git rev-parse --show-toplevel` on every run. Stay in the worktree you were given (do not assume a fixed path like the main checkout). + +## ESLint on changed `src/` TypeScript (replaces former Cursor hooks) + +Previously, `.cursor/hooks.json` ran ESLint **only on agent-edited** `src/**/*.ts` and `src/**/*.tsx` files (`npx eslint --max-warnings 0 `), using the worktree Docker container when present. You must do the same **before** the default `npm run quality` step whenever you are verifying work after edits (the usual reason this subagent runs). + +1. From the repo root (`git rev-parse --show-toplevel`), collect paths that are **changed in the working tree** (staged and unstaged) and are under `src/` with extension `.ts` or `.tsx`: + + ```bash + { git diff --name-only; git diff --cached --name-only; } | sort -u + ``` + + Keep only existing files (skip deletes) matching `src/**/*.ts` and `src/**/*.tsx`. + +2. If that list is **non-empty**, run ESLint with **zero warnings** on exactly those paths, using the same execution context as other npm commands (see **Where to run npm**): + + - **Inside** the worktree container: `docker exec -w /app fraudebot-frontend- npx eslint --max-warnings 0 -- ` + - **Host**: `npx eslint --max-warnings 0 -- ` + + Paths must be relative to the repository root (as reported by `git diff --name-only`). + +3. If the working tree is clean but the branch has commits to verify, fall back to files changed against the merge base with the default branch (try `origin/main`, then `main`): + + ```bash + git fetch origin main 2>/dev/null || true + base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null || echo HEAD~1)" + git diff --name-only "$base"...HEAD | sort -u + ``` + + Apply the same `src/**/*.ts(x)` filter and run the same ESLint command when the list is non-empty. + +4. If there are still no matching files, **skip** this targeted ESLint step (nothing replaces “no edited TS files”). + +Stop on first ESLint failure unless the user asked for a full report of all failures. Mention this step in the report under **Results** as `npx eslint --max-warnings 0 (changed src files)`. ## Where to run npm @@ -28,13 +61,18 @@ Never assume green without running the commands. Run sequentially from the repo root (via host or `docker exec` as above). Stop on first failure unless the user asked for a full report of all failures. -When the user asks to run tests, verify changes, hand off, or match CI, run: +When the user asks to run tests, verify changes, hand off, or match CI: + +1. Run **ESLint on changed `src/` TypeScript** as described above (when applicable). +2. Then run: ```bash npm run quality npm run typecheck ``` +`npm run quality` already includes a full-repo `npm run lint`; the targeted ESLint step above preserves the faster, path-scoped check the old hooks performed on touched files first. + For a **narrow** scope only (user names lint, unit tests, or build explicitly), run the minimal subset they asked for, for example: ```bash @@ -60,8 +98,8 @@ For E2E that must hit the **running worktree dev server**, follow `architecture- ## Subagent workflow -1. Skim `git status` / `git diff --stat` to choose optional checks (contract, E2E, coverage). -2. Run the default suite, then any optional commands that apply. +1. Skim `git status` / `git diff --stat` to choose optional checks (contract, E2E, coverage) and to list changed `src/**/*.ts(x)` files for the **ESLint on changed `src/` TypeScript** step. +2. Run targeted ESLint when applicable, then the default suite, then any optional commands that apply. 3. Capture failing command output (last ~40 lines per failure is enough). 4. Return a structured report to the main agent or user. diff --git a/.cursor/hooks.json b/.cursor/hooks.json deleted file mode 100644 index adcca1d..0000000 --- a/.cursor/hooks.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": 1, - "hooks": { - "afterFileEdit": [ - { - "command": ".cursor/hooks/eslint-after-agent-edits.sh", - "timeout": 15 - } - ], - "stop": [ - { - "command": ".cursor/hooks/eslint-after-agent-edits.sh", - "timeout": 120, - "loop_limit": 2 - } - ] - } -} diff --git a/.cursor/hooks/eslint-after-agent-edits.sh b/.cursor/hooks/eslint-after-agent-edits.sh deleted file mode 100755 index 604f05e..0000000 --- a/.cursor/hooks/eslint-after-agent-edits.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash -# Track agent-edited src TS/TSX files and run ESLint on stop (worktree container or host). -set -uo pipefail - -root="$(cd "$(dirname "$0")/../.." && pwd -P)" -root_real="$(realpath -m "$root")" -input="$(cat)" - -finish() { - printf '%s\n' '{}' - exit 0 -} - -if ! command -v python3 >/dev/null 2>&1; then - echo "ESLint hook: python3 is not on PATH." >&2 - finish -fi - -parsed="$(printf '%s' "$input" | python3 -c ' -import json, sys -data = json.load(sys.stdin) -event = str(data.get("hook_event_name") or "") -conv = str(data.get("conversation_id") or "") -gen = str(data.get("generation_id") or "") -loop_count = int(data.get("loop_count") or 0) -paths = [] -file_path = data.get("file_path") -if isinstance(file_path, str): - paths.append(file_path) -modified = data.get("modified_files") -if isinstance(modified, list): - paths.extend(str(path) for path in modified) - -def is_src_ts(path: str) -> bool: - p = path.replace("\\\\", "/") - if "/src/" not in p: - return False - lower = p.lower() - return lower.endswith(".ts") or lower.endswith(".tsx") - -tracked = [p for p in paths if is_src_ts(p)] -print(event) -print(conv) -print(gen) -print(loop_count) -print("1" if tracked else "0") -for p in tracked: - print(p) -')" - -event="$(sed -n '1p' <<<"$parsed")" -conversation_id="$(sed -n '2p' <<<"$parsed")" -generation_id="$(sed -n '3p' <<<"$parsed")" -loop_count="$(sed -n '4p' <<<"$parsed")" -has_src="$(sed -n '5p' <<<"$parsed")" -mapfile -t edited_paths < <(sed -n '6,$p' <<<"$parsed") - -safe() { - printf '%s' "$1" | tr -c 'A-Za-z0-9._-' '_' -} - -state_dir="/tmp/fraudebot-eslint-edits/$(safe "$root_real")/$(safe "$conversation_id")/$(safe "$generation_id")" -list_file="${state_dir}/files" - -if [[ "$event" == "afterFileEdit" ]]; then - if [[ "$has_src" == "1" && -n "$conversation_id" && -n "$generation_id" ]]; then - mkdir -p "$state_dir" - for path in "${edited_paths[@]}"; do - [[ -n "$path" ]] && printf '%s\n' "$path" >>"$list_file" - done - if [[ -f "$list_file" ]]; then - sort -u "$list_file" -o "$list_file" - fi - fi - finish -fi - -if [[ "$event" != "stop" ]]; then - finish -fi - -if [[ ! -f "$list_file" ]]; then - finish -fi - -mapfile -t files < <(grep -v '^[[:space:]]*$' "$list_file" || true) -rm -rf "$state_dir" - -if [[ "${#files[@]}" -eq 0 ]]; then - finish -fi - -rel_files=() -for abs in "${files[@]}"; do - case "$abs" in - "$root_real"/*) - rel_files+=("${abs#"$root_real"/}") - ;; - *) - rel_files+=("$abs") - ;; - esac -done - -container="" -workdir="/app" - -if command -v docker >/dev/null 2>&1; then - while IFS= read -r name; do - [[ "$name" == fraudebot-frontend-* ]] || continue - while IFS=$'\t' read -r source destination; do - [[ -n "$source" && -n "$destination" ]] || continue - source_real="$(realpath -m "$source")" - if [[ "$source_real" == "$root_real" ]]; then - container="$name" - workdir="$destination" - break 2 - fi - done < <(docker inspect --format '{{range .Mounts}}{{.Source}}{{"\t"}}{{.Destination}}{{"\n"}}{{end}}' "$name" 2>/dev/null || true) - done < <(docker ps --format '{{.Names}}') -fi - -lint_ok=0 -if [[ -n "$container" ]]; then - echo "ESLint hook: docker exec -w ${workdir} ${container} npx eslint --max-warnings 0 (${#rel_files[@]} files)" >&2 - if docker exec -w "$workdir" "$container" npx eslint --max-warnings 0 "${rel_files[@]}" >&2; then - lint_ok=1 - fi -else - echo "ESLint hook: npx eslint --max-warnings 0 (${#rel_files[@]} files)" >&2 - if (cd "$root" && npx eslint --max-warnings 0 "${rel_files[@]}") >&2; then - lint_ok=1 - fi -fi - -if [[ "$lint_ok" != "1" && "${loop_count:-0}" -lt 1 ]]; then - printf '%s\n' '{"followup_message":"ESLint reported issues on agent-edited files. Fix them, then run the execute-tests subagent or npm run quality before opening a PR."}' - exit 0 -fi - -finish diff --git a/README.md b/README.md index 8b6de9a..e9ccad4 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ npm run build # Production build in dist/ npm run test:e2e # Chromium end-to-end tests ``` -Agent sessions may run ESLint on edited `src/` files when the agent stops; see `.cursor/hooks.json`. +After agent edits, run the **execute-tests** subagent (`.cursor/agents/execute-tests.md`): it runs ESLint with `--max-warnings 0` on changed `src/**/*.ts(x)` files when applicable, then `npm run quality` and `npm run typecheck`. Playwright starts a production preview on port `4173`. Install its browser once with `npx playwright install chromium` if it is not already available. Docker-based end-to-end