From 4209aef4f2609e84085c1cf95226bdd79c3c9406 Mon Sep 17 00:00:00 2001 From: Miyamura80 Date: Thu, 23 Apr 2026 22:44:55 +0100 Subject: [PATCH 1/3] Add source-size guardrails (folder count + file line count) CI workflows and shared bash scripts that warn at 20 files/500 lines and error at 35 files/800 lines for .rs sources. Existing violations are grandfathered. Prek hooks mirror CI locally. Includes a folder-refactor-advisor subagent for when the check fires. Co-Authored-By: Claude Opus 4.6 --- .claude/agents/folder-refactor-advisor.md | 43 +++++++ .codex/agents/folder-refactor-advisor.toml | 44 +++++++ .github/workflows/folder-size.yaml | 30 +++++ .github/workflows/large-files.yaml | 30 +++++ .gitignore | 1 + CLAUDE.md | 6 + prek.toml | 17 +++ scripts/check_folder_sizes.sh | 129 ++++++++++++++++++++ scripts/check_large_files.sh | 130 +++++++++++++++++++++ 9 files changed, 430 insertions(+) create mode 100644 .claude/agents/folder-refactor-advisor.md create mode 100644 .codex/agents/folder-refactor-advisor.toml create mode 100644 .github/workflows/folder-size.yaml create mode 100644 .github/workflows/large-files.yaml create mode 100644 prek.toml create mode 100755 scripts/check_folder_sizes.sh create mode 100755 scripts/check_large_files.sh diff --git a/.claude/agents/folder-refactor-advisor.md b/.claude/agents/folder-refactor-advisor.md new file mode 100644 index 00000000..a7c8fef1 --- /dev/null +++ b/.claude/agents/folder-refactor-advisor.md @@ -0,0 +1,43 @@ +--- +name: folder-refactor-advisor +description: Use when the folder-size CI check (`.github/workflows/folder-size.yaml`) errors or warns, or when the user asks for help splitting a folder that has grown too many flat sibling files. Investigates the folder, interviews the user about how the area is expected to evolve, and proposes three concrete subfolder restructurings with pros/cons and a recommendation. Advisory only - does not move files. +tools: Read, Glob, Grep, Bash, AskUserQuestion +model: sonnet +color: yellow +--- + +# Folder Refactor Advisor + +Advisory-only. You diagnose one flat folder and recommend how to split it. +Never move, rename, create, or delete files. + +## Steps + +1. **Inventory** with `Glob`/`Read`/`Grep`: every immediate `.rs` + child, its line count, one-line purpose, import graph (internal + external + callers via `use`/`mod`), naming patterns, matching test layout. Skim, don't deep-read. +2. **Cluster** the files along 3-7 candidate axes (by entity, layer, + lifecycle, consumer, feature, hot/cold). Expect 2-3 axes to overlap - the + interview disambiguates. +3. **Interview** via `AskUserQuestion`, in batches of 2-3. Ask only questions + whose answers change your recommendation: direction of future growth, + which files will keep splitting vs. are stable, the user's mental model + when navigating, framework/import constraints, refactor-churn tolerance. +4. **Propose exactly three options.** Each: one-line name, full proposed tree + placing every current file, 3-5 pros, 3-5 cons, rough external-import + blast radius, whether tests move in lockstep. Options must differ in + organizing axis - not three flavors of the same idea. +5. **Recommend one** in a sentence, citing the deciding user answer verbatim. + Acknowledge the strongest counter-argument. +6. **Hand off** the concrete file moves, `mod.rs` updates, and `use` path + changes, in order. Do not perform them. + +## Guardrails + +- One folder per invocation. +- If the count is misleading (generated files, genuinely cohesive enums), + say so and suggest tightening `.github/workflows/folder-size.yaml` + exclusions instead of inventing a refactor. +- Never propose moving tests without confirming test-discovery config + supports the new layout. +- Tables and trees beat paragraphs. diff --git a/.codex/agents/folder-refactor-advisor.toml b/.codex/agents/folder-refactor-advisor.toml new file mode 100644 index 00000000..0e8caee2 --- /dev/null +++ b/.codex/agents/folder-refactor-advisor.toml @@ -0,0 +1,44 @@ +name = "folder-refactor-advisor" +description = "Use when the folder-size CI check (`.github/workflows/folder-size.yaml`) errors or warns, or when the user asks for help splitting a folder that has grown too many flat sibling files. Investigates the folder, interviews the user about how the area is expected to evolve, and proposes three concrete subfolder restructurings with pros/cons and a recommendation. Advisory only - does not move files." +developer_instructions = """ +# Folder Refactor Advisor + +Advisory-only. You diagnose one flat folder and recommend how to split it. +Never move, rename, create, or delete files. + +## Steps + +1. **Inventory** with `Glob`/`Read`/`Grep`: every immediate `.rs` + child, its line count, one-line purpose, import graph (internal + external + callers via `use`/`mod`), naming patterns, matching test layout. Skim, don't deep-read. +2. **Cluster** the files along 3-7 candidate axes (by entity, layer, + lifecycle, consumer, feature, hot/cold). Expect 2-3 axes to overlap - the + interview disambiguates. +3. **Interview** via `AskUserQuestion`, in batches of 2-3. Ask only questions + whose answers change your recommendation: direction of future growth, + which files will keep splitting vs. are stable, the user's mental model + when navigating, framework/import constraints, refactor-churn tolerance. +4. **Propose exactly three options.** Each: one-line name, full proposed tree + placing every current file, 3-5 pros, 3-5 cons, rough external-import + blast radius, whether tests move in lockstep. Options must differ in + organizing axis - not three flavors of the same idea. +5. **Recommend one** in a sentence, citing the deciding user answer verbatim. + Acknowledge the strongest counter-argument. +6. **Hand off** the concrete file moves, `mod.rs` updates, and `use` path + changes, in order. Do not perform them. + +## Guardrails + +- One folder per invocation. +- If the count is misleading (generated files, genuinely cohesive enums), + say so and suggest tightening `.github/workflows/folder-size.yaml` + exclusions instead of inventing a refactor. +- Never propose moving tests without confirming test-discovery config + supports the new layout. +- Tables and trees beat paragraphs. +""" + +# Claude-only frontmatter (preserved for reference, not used by Codex): +# tools = 'Read, Glob, Grep, Bash, AskUserQuestion' +# model = 'sonnet' +# color = 'yellow' diff --git a/.github/workflows/folder-size.yaml b/.github/workflows/folder-size.yaml new file mode 100644 index 00000000..cff24ec3 --- /dev/null +++ b/.github/workflows/folder-size.yaml @@ -0,0 +1,30 @@ +name: Folder Size Check + +on: + workflow_dispatch: + pull_request: + paths: + - '**.rs' + +jobs: + check-folder-sizes: + name: Folder File Count Limit + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check for oversized folders + run: | + if [ -n "${{ github.event.pull_request.base.sha }}" ]; then + mapfile -t files < <(git diff --name-only --diff-filter=d "${{ github.event.pull_request.base.sha }}...HEAD" -- '*.rs') + if [ "${#files[@]}" -eq 0 ]; then + echo "No source files changed." + exit 0 + fi + scripts/check_folder_sizes.sh "${files[@]}" + else + scripts/check_folder_sizes.sh --all + fi diff --git a/.github/workflows/large-files.yaml b/.github/workflows/large-files.yaml new file mode 100644 index 00000000..b7650d50 --- /dev/null +++ b/.github/workflows/large-files.yaml @@ -0,0 +1,30 @@ +name: Large File Check + +on: + workflow_dispatch: + pull_request: + paths: + - '**.rs' + +jobs: + check-file-sizes: + name: Source File Line Limit + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check for large source files + run: | + if [ -n "${{ github.event.pull_request.base.sha }}" ]; then + mapfile -t files < <(git diff --name-only --diff-filter=d "${{ github.event.pull_request.base.sha }}...HEAD" -- '*.rs') + if [ "${#files[@]}" -eq 0 ]; then + echo "No source files changed." + exit 0 + fi + scripts/check_large_files.sh "${files[@]}" + else + scripts/check_large_files.sh --all + fi diff --git a/.gitignore b/.gitignore index 17c68374..97bbec14 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .env .DS_Store .claude/* +!.claude/agents/ !.claude/skills/ !.claude/settings.json /test-results diff --git a/CLAUDE.md b/CLAUDE.md index f38be811..eee66f50 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,7 @@ # CLAUDE.md +**Before any other work in this repo, enable prek:** `uv tool install prek && prek install`. Hooks are defined in `prek.toml`. + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview @@ -83,3 +85,7 @@ Host-side modules: - `src/windows/deploy.rs` — App deployment into Windows VM - `src/windows/readiness.rs` — Desktop/app readiness detection for Windows - `src/init_windows.rs` — `desktest init-windows` golden image provisioning + +## Subagents + +- Folder-size CI failure → spawn subagent `.claude/agents/folder-refactor-advisor.md`. diff --git a/prek.toml b/prek.toml new file mode 100644 index 00000000..ee3e03b4 --- /dev/null +++ b/prek.toml @@ -0,0 +1,17 @@ +# ── Source-size guardrails (mirror GitHub Actions) ──────────────── +[[repos]] +repo = "local" + +[[repos.hooks]] +id = "check-large-files" +name = "fail if any source file exceeds the line-count error threshold" +language = "system" +entry = "scripts/check_large_files.sh" +files = "\\.(rs)$" + +[[repos.hooks]] +id = "check-folder-sizes" +name = "fail if any source folder exceeds the file-count error threshold" +language = "system" +entry = "scripts/check_folder_sizes.sh" +files = "\\.(rs)$" diff --git a/scripts/check_folder_sizes.sh b/scripts/check_folder_sizes.sh new file mode 100755 index 00000000..800638fa --- /dev/null +++ b/scripts/check_folder_sizes.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# +# Enforce a file-count limit on source folders (non-recursive). Shared by +# .github/workflows/folder-size.yaml and prek.toml. +# +# Usage: +# check_folder_sizes.sh [file ...] # check folders containing the given files +# check_folder_sizes.sh --all # scan every folder in the tree +# +# Thresholds: warn at WARN files-per-folder, error at ERROR. Override via +# FOLDER_WARN_THRESHOLD / FOLDER_ERROR_THRESHOLD env vars (namespaced so +# they don't collide with check_large_files.sh). +# Exit 1 on non-grandfathered errors, 0 on warnings-only or clean. +# +# If $GITHUB_STEP_SUMMARY is set, a markdown summary is appended to it. + +set -euo pipefail + +WARN_THRESHOLD="${FOLDER_WARN_THRESHOLD:-20}" +ERROR_THRESHOLD="${FOLDER_ERROR_THRESHOLD:-35}" + +GRANDFATHERED=( + "src" +) + +EXCLUDE_PATH_RE='(^|/)(target|node_modules|\.venv|venv|\.git|dist|build)(/|$)' + +is_grandfathered() { + local target="$1" + for g in "${GRANDFATHERED[@]}"; do + [ "$target" = "$g" ] && return 0 + done + return 1 +} + +should_skip() { + local f="$1" + [ -z "$f" ] && return 0 + [ "$f" = "." ] && return 0 + echo "$f" | grep -qE "$EXCLUDE_PATH_RE" && return 0 + return 1 +} + +count_folder() { + find "$1" -mindepth 1 -maxdepth 1 -type f \ + -name '*.rs' \ + | wc -l +} + +collect_all_folders() { + find . -type d \ + -not -path './.git/*' \ + -not -path '*/target/*' \ + | sed 's|^\./||' +} + +folder_list=$(mktemp) +trap 'rm -f "$folder_list"' EXIT + +if [ "${1:-}" = "--all" ]; then + collect_all_folders > "$folder_list" +else + for f in "$@"; do + [ -z "$f" ] && continue + dirname "$f" + done | sort -u > "$folder_list" +fi + +warnings=0 +errors=0 +warn_list="" +error_list="" + +while IFS= read -r folder; do + folder="${folder#./}" + should_skip "$folder" && continue + [ ! -d "$folder" ] && continue + + count=$(count_folder "$folder") + + if [ "$count" -gt "$ERROR_THRESHOLD" ]; then + if is_grandfathered "$folder"; then + warnings=$((warnings + 1)) + warn_list="${warn_list}| \`${folder}/\` | ${count} | :warning: exceeds ${ERROR_THRESHOLD} (grandfathered) |\n" + else + errors=$((errors + 1)) + error_list="${error_list}| \`${folder}/\` | ${count} | :x: exceeds ${ERROR_THRESHOLD} |\n" + fi + elif [ "$count" -gt "$WARN_THRESHOLD" ]; then + warnings=$((warnings + 1)) + warn_list="${warn_list}| \`${folder}/\` | ${count} | :warning: exceeds ${WARN_THRESHOLD} |\n" + fi +done < "$folder_list" + +if [ -n "${GITHUB_STEP_SUMMARY:-}" ] && { [ "$errors" -gt 0 ] || [ "$warnings" -gt 0 ]; }; then + { + echo "## Folder Size Report" + echo "" + echo "| Folder | Files | Status |" + echo "|--------|-------|--------|" + [ "$errors" -gt 0 ] && printf '%b' "$error_list" + [ "$warnings" -gt 0 ] && printf '%b' "$warn_list" + echo "" + echo "**Thresholds:** warn at ${WARN_THRESHOLD} files, error at ${ERROR_THRESHOLD} files. Counts immediate \`.rs\` children only - subfolders are the fix, not the problem." + } >> "$GITHUB_STEP_SUMMARY" +fi + +format_list() { + if command -v column >/dev/null 2>&1; then + printf '%b' "$1" | column -t -s '|' + else + printf '%b' "$1" + fi +} + +if [ "$errors" -gt 0 ]; then + echo "::error::${errors} folder(s) exceed the ${ERROR_THRESHOLD}-file error threshold" >&2 + format_list "$error_list" >&2 +fi +if [ "$warnings" -gt 0 ]; then + echo "::warning::${warnings} folder(s) exceed the ${WARN_THRESHOLD}-file warning threshold" >&2 + format_list "$warn_list" >&2 +fi +if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then + echo "All folders are within the ${WARN_THRESHOLD}-file limit." +fi + +[ "$errors" -gt 0 ] && exit 1 +exit 0 diff --git a/scripts/check_large_files.sh b/scripts/check_large_files.sh new file mode 100755 index 00000000..e7fd6472 --- /dev/null +++ b/scripts/check_large_files.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# +# Enforce a line-count limit on source files. Shared by +# .github/workflows/large-files.yaml and prek.toml. +# +# Usage: +# check_large_files.sh [file ...] # check the given files +# check_large_files.sh --all # scan the whole tree +# +# Thresholds: warn at WARN lines, error at ERROR lines. Override via +# LARGE_FILE_WARN_THRESHOLD / LARGE_FILE_ERROR_THRESHOLD env vars +# (namespaced so they don't collide with check_folder_sizes.sh). +# Exit 1 on errors, 0 on warnings-only or clean. +# +# If $GITHUB_STEP_SUMMARY is set, a markdown summary is appended to it. + +set -euo pipefail + +WARN_THRESHOLD="${LARGE_FILE_WARN_THRESHOLD:-500}" +ERROR_THRESHOLD="${LARGE_FILE_ERROR_THRESHOLD:-800}" + +EXCLUDE_PATH_RE='(^|/)(target|node_modules|\.venv|venv|\.git)(/|$)' + +GRANDFATHERED=( + "src/config.rs" + "src/agent/pyautogui.rs" + "src/agent/loop_v2.rs" + "src/agent/context.rs" + "src/task.rs" + "src/main.rs" + "src/orchestration.rs" + "src/logs.rs" +) + +is_grandfathered() { + local target="$1" + for g in "${GRANDFATHERED[@]}"; do + [ "$target" = "$g" ] && return 0 + done + return 1 +} + +is_source_file() { + case "$1" in + *.rs) return 0 ;; + *) return 1 ;; + esac +} + +is_excluded() { + local f="$1" + echo "$f" | grep -qE "$EXCLUDE_PATH_RE" && return 0 + return 1 +} + +collect_all() { + find . -type f -name '*.rs' \ + -not -path './.git/*' \ + -not -path '*/target/*' \ + | sed 's|^\./||' +} + +files=() +if [ "${1:-}" = "--all" ]; then + mapfile -t files < <(collect_all) +else + files=("$@") +fi + +warnings=0 +errors=0 +warn_list="" +error_list="" + +for file in "${files[@]}"; do + [ -z "$file" ] && continue + [ ! -f "$file" ] && continue + is_source_file "$file" || continue + is_excluded "$file" && continue + + lines=$(wc -l < "$file") + if [ "$lines" -gt "$ERROR_THRESHOLD" ]; then + if is_grandfathered "$file"; then + warnings=$((warnings + 1)) + warn_list="${warn_list}| \`${file}\` | ${lines} | :warning: exceeds ${ERROR_THRESHOLD} (grandfathered) |\n" + else + errors=$((errors + 1)) + error_list="${error_list}| \`${file}\` | ${lines} | :x: exceeds ${ERROR_THRESHOLD} |\n" + fi + elif [ "$lines" -gt "$WARN_THRESHOLD" ]; then + warnings=$((warnings + 1)) + warn_list="${warn_list}| \`${file}\` | ${lines} | :warning: exceeds ${WARN_THRESHOLD} |\n" + fi +done + +if [ -n "${GITHUB_STEP_SUMMARY:-}" ] && { [ "$errors" -gt 0 ] || [ "$warnings" -gt 0 ]; }; then + { + echo "## Large File Report" + echo "" + echo "| File | Lines | Status |" + echo "|------|-------|--------|" + [ "$errors" -gt 0 ] && printf '%b' "$error_list" + [ "$warnings" -gt 0 ] && printf '%b' "$warn_list" + echo "" + echo "**Thresholds:** warn at ${WARN_THRESHOLD} lines, error at ${ERROR_THRESHOLD} lines" + } >> "$GITHUB_STEP_SUMMARY" +fi + +format_list() { + if command -v column >/dev/null 2>&1; then + printf '%b' "$1" | column -t -s '|' + else + printf '%b' "$1" + fi +} + +if [ "$errors" -gt 0 ]; then + echo "::error::${errors} file(s) exceed the ${ERROR_THRESHOLD}-line error threshold" >&2 + format_list "$error_list" >&2 +fi +if [ "$warnings" -gt 0 ]; then + echo "::warning::${warnings} file(s) exceed the ${WARN_THRESHOLD}-line warning threshold" >&2 + format_list "$warn_list" >&2 +fi +if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then + echo "All source files are within the ${WARN_THRESHOLD}-line limit." +fi + +[ "$errors" -gt 0 ] && exit 1 +exit 0 From f43e8566aa10d738951c0d187bba7c76bd23966a Mon Sep 17 00:00:00 2001 From: Miyamura80 Date: Thu, 23 Apr 2026 22:50:36 +0100 Subject: [PATCH 2/3] fix: use brew instead of uv for prek install instruction Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index eee66f50..1f467bb9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md -**Before any other work in this repo, enable prek:** `uv tool install prek && prek install`. Hooks are defined in `prek.toml`. +**Before any other work in this repo, enable prek:** `brew install prek && prek install`. Hooks are defined in `prek.toml`. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. From 1a75e61632ec488cca3b07661ae573beddc74559 Mon Sep 17 00:00:00 2001 From: Miyamura80 Date: Thu, 23 Apr 2026 22:51:31 +0100 Subject: [PATCH 3/3] fix: use cargo binstall for prek install instruction Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1f467bb9..77cdb460 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md -**Before any other work in this repo, enable prek:** `brew install prek && prek install`. Hooks are defined in `prek.toml`. +**Before any other work in this repo, enable prek:** `cargo binstall prek && prek install`. Hooks are defined in `prek.toml`. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.