From 7496ab76900f6f1910f7e4f02746895f1410fed8 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 16:17:55 -0500 Subject: [PATCH 01/15] Add CI lint gate and local precheck command - Add lint-gate job to test.yml that runs fast checks (formatting, spelling, linting) before expensive test matrix and HPC jobs start - Add concurrency groups to test.yml, coverage.yml, cleanliness.yml, and bench.yml to cancel superseded runs on new pushes - Add ./mfc.sh precheck command for local CI validation before pushing Co-Authored-By: Claude Opus 4.5 --- .github/workflows/bench.yml | 4 + .github/workflows/cleanliness.yml | 4 + .github/workflows/coverage.yml | 4 + .github/workflows/test.yml | 48 +++++++++++- mfc.sh | 4 + toolchain/bootstrap/precheck.sh | 118 ++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 3 deletions(-) create mode 100755 toolchain/bootstrap/precheck.sh diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 1427f9d693..0b6f069f0a 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -6,6 +6,10 @@ on: types: [submitted] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: file-changes: name: Detect File Changes diff --git a/.github/workflows/cleanliness.yml b/.github/workflows/cleanliness.yml index b02df12898..bcbe35caaa 100644 --- a/.github/workflows/cleanliness.yml +++ b/.github/workflows/cleanliness.yml @@ -2,6 +2,10 @@ name: Cleanliness on: [push, pull_request, workflow_dispatch] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: file-changes: name: Detect File Changes diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ad0ea7a220..469048f8a6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,6 +2,10 @@ name: Coverage Check on: [push, pull_request, workflow_dispatch] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: file-changes: name: Detect File Changes diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0dc72783d..fb16384045 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,50 @@ name: 'Test Suite' on: [push, pull_request, workflow_dispatch] - + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: + lint-gate: + name: Lint Gate + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Initialize MFC + run: ./mfc.sh init + + - name: Check Formatting + run: | + ./mfc.sh format -j $(nproc) + git diff --exit-code || (echo "::error::Code is not formatted. Run './mfc.sh format' locally." && exit 1) + + - name: Spell Check + run: ./mfc.sh spelling + + - name: Lint Toolchain + run: ./mfc.sh lint + + - name: Lint Source - No Raw Directives + run: | + ! grep -iR '!\$acc\|!\$omp' --exclude="parallel_macros.fpp" --exclude="acc_macros.fpp" --exclude="omp_macros.fpp" --exclude="shared_parallel_macros.fpp" --exclude="syscheck.fpp" ./src/* + + - name: Lint Source - No Double Precision Intrinsics + run: | + ! grep -iR 'double_precision\|dsqrt\|dexp\|dlog\|dble\|dabs\|double\ precision\|real(8)\|real(4)\|dprod\|dmin\|dmax\|dfloat\|dreal\|dcos\|dsin\|dtan\|dsign\|dtanh\|dsinh\|dcosh\|d0' --exclude-dir=syscheck --exclude="*nvtx*" --exclude="*precision_select*" ./src/* + + - name: Lint Source - No Junk Code + run: | + ! grep -iR -e '\.\.\.' -e '\-\-\-' -e '===' ./src/* + file-changes: name: Detect File Changes runs-on: 'ubuntu-latest' @@ -21,7 +63,7 @@ jobs: github: name: Github if: needs.file-changes.outputs.checkall == 'true' - needs: file-changes + needs: [lint-gate, file-changes] strategy: matrix: os: ['ubuntu', 'macos'] @@ -95,7 +137,7 @@ jobs: self: name: "${{ matrix.cluster_name }} (${{ matrix.device }}${{ matrix.interface != 'none' && format('-{0}', matrix.interface) || '' }})" if: github.repository == 'MFlowCode/MFC' && needs.file-changes.outputs.checkall == 'true' - needs: file-changes + needs: [lint-gate, file-changes] continue-on-error: false timeout-minutes: 480 strategy: diff --git a/mfc.sh b/mfc.sh index 9fb95ae61a..3eb015e1e9 100755 --- a/mfc.sh +++ b/mfc.sh @@ -40,6 +40,10 @@ elif [ "$1" '==' "spelling" ]; then . "$(pwd)/toolchain/bootstrap/python.sh" shift; . "$(pwd)/toolchain/bootstrap/spelling.sh" $@; exit 0 +elif [ "$1" '==' "precheck" ]; then + . "$(pwd)/toolchain/bootstrap/python.sh" + + shift; . "$(pwd)/toolchain/bootstrap/precheck.sh" $@; exit 0 fi mkdir -p "$(pwd)/build" diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh new file mode 100755 index 0000000000..b5d98258c7 --- /dev/null +++ b/toolchain/bootstrap/precheck.sh @@ -0,0 +1,118 @@ +#!/bin/bash +set -e +set -o pipefail + +# Function to display help message +show_help() { + echo "Usage: ./mfc.sh precheck [OPTIONS]" + echo "Run the same fast checks that CI runs before expensive tests start." + echo "Use this locally before pushing to catch issues early." + echo "" + echo "Options:" + echo " -h, --help Display this help message and exit." + echo " -j, --jobs JOBS Runs JOBS number of parallel jobs for formatting." + echo "" + exit 0 +} + +JOBS=1 + +while [[ $# -gt 0 ]]; do + case "$1" in + -j|--jobs) + JOBS="$2" + shift + ;; + -h | --help) + show_help + ;; + *) + echo "Precheck: unknown argument: $1." + exit 1 + ;; + esac + + shift +done + +FAILED=0 + +log "Running$MAGENTA precheck$COLOR_RESET (same checks as CI lint-gate)..." +echo "" + +# 1. Check formatting +log "[$CYAN 1/4$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." +# Capture state before formatting +BEFORE_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | md5sum | cut -d' ' -f1) +if ! ./mfc.sh format -j "$JOBS" > /dev/null 2>&1; then + error "Formatting check failed to run." + FAILED=1 +else + # Check if formatting changed any Fortran/Python files + AFTER_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | md5sum | cut -d' ' -f1) + if [ "$BEFORE_HASH" != "$AFTER_HASH" ]; then + error "Code is not formatted. Run$MAGENTA ./mfc.sh format$COLOR_RESET to fix." + echo "" + git diff --stat -- '*.f90' '*.fpp' '*.py' 2>/dev/null || true + echo "" + FAILED=1 + else + ok "Formatting check passed." + fi +fi + +# 2. Spell check +log "[$CYAN 2/4$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." +if ./mfc.sh spelling > /dev/null 2>&1; then + ok "Spell check passed." +else + error "Spell check failed." + FAILED=1 +fi + +# 3. Lint toolchain (Python) +log "[$CYAN 3/4$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." +if ./mfc.sh lint > /dev/null 2>&1; then + ok "Toolchain lint passed." +else + error "Toolchain lint failed. Run$MAGENTA ./mfc.sh lint$COLOR_RESET for details." + FAILED=1 +fi + +# 4. Source code lint checks +log "[$CYAN 4/4$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET checks..." +SOURCE_FAILED=0 + +# Check for raw OpenACC/OpenMP directives +if grep -qiR '!\$acc\|!\$omp' --exclude="parallel_macros.fpp" --exclude="acc_macros.fpp" --exclude="omp_macros.fpp" --exclude="shared_parallel_macros.fpp" --exclude="syscheck.fpp" ./src/* 2>/dev/null; then + error "Found raw OpenACC/OpenMP directives. Use macros instead." + SOURCE_FAILED=1 +fi + +# Check for double precision intrinsics +if grep -qiR 'double_precision\|dsqrt\|dexp\|dlog\|dble\|dabs\|double\ precision\|real(8)\|real(4)\|dprod\|dmin\|dmax\|dfloat\|dreal\|dcos\|dsin\|dtan\|dsign\|dtanh\|dsinh\|dcosh\|d0' --exclude-dir=syscheck --exclude="*nvtx*" --exclude="*precision_select*" ./src/* 2>/dev/null; then + error "Found double precision intrinsics. Use generic intrinsics." + SOURCE_FAILED=1 +fi + +# Check for junk code patterns +if grep -qiR -e '\.\.\.' -e '\-\-\-' -e '===' ./src/* 2>/dev/null; then + error "Found junk code patterns (..., ---, ===) in source." + SOURCE_FAILED=1 +fi + +if [ $SOURCE_FAILED -eq 0 ]; then + ok "Source lint passed." +else + FAILED=1 +fi + +echo "" + +if [ $FAILED -eq 0 ]; then + ok "All precheck tests passed! Safe to push." + exit 0 +else + error "Some precheck tests failed. Fix issues before pushing." + exit 1 +fi From 26f7e17295c08d9a84cb412aa5df3101c3d9bf56 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 16:33:27 -0500 Subject: [PATCH 02/15] Fix precheck.sh portability and usability issues - Add cross-platform hash function (macOS uses md5, Linux uses md5sum) - Validate -j/--jobs argument (require value, must be numeric) - Improve error messages with actionable guidance - Clarify that formatting has been auto-applied when check fails Co-Authored-By: Claude Opus 4.5 --- toolchain/bootstrap/precheck.sh | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index b5d98258c7..e215b5945e 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -15,11 +15,31 @@ show_help() { exit 0 } +# Cross-platform hash function (macOS uses md5, Linux uses md5sum) +compute_hash() { + if command -v md5sum > /dev/null 2>&1; then + md5sum | cut -d' ' -f1 + elif command -v md5 > /dev/null 2>&1; then + md5 -q + else + # Fallback: use cksum if neither available + cksum | cut -d' ' -f1 + fi +} + JOBS=1 while [[ $# -gt 0 ]]; do case "$1" in -j|--jobs) + if [[ -z "$2" || "$2" == -* ]]; then + echo "Precheck: -j/--jobs requires a value." + exit 1 + fi + if ! [[ "$2" =~ ^[0-9]+$ ]]; then + echo "Precheck: jobs value '$2' is not a valid number." + exit 1 + fi JOBS="$2" shift ;; @@ -43,15 +63,15 @@ echo "" # 1. Check formatting log "[$CYAN 1/4$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." # Capture state before formatting -BEFORE_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | md5sum | cut -d' ' -f1) +BEFORE_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | compute_hash) if ! ./mfc.sh format -j "$JOBS" > /dev/null 2>&1; then error "Formatting check failed to run." FAILED=1 else # Check if formatting changed any Fortran/Python files - AFTER_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | md5sum | cut -d' ' -f1) + AFTER_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | compute_hash) if [ "$BEFORE_HASH" != "$AFTER_HASH" ]; then - error "Code is not formatted. Run$MAGENTA ./mfc.sh format$COLOR_RESET to fix." + error "Code was not formatted. Files have been auto-formatted; review and stage the changes." echo "" git diff --stat -- '*.f90' '*.fpp' '*.py' 2>/dev/null || true echo "" @@ -66,7 +86,7 @@ log "[$CYAN 2/4$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." if ./mfc.sh spelling > /dev/null 2>&1; then ok "Spell check passed." else - error "Spell check failed." + error "Spell check failed. Run$MAGENTA ./mfc.sh spelling$COLOR_RESET for details." FAILED=1 fi From 4b17fa245b34f36cda98423f17ef8bf67753a93f Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 18:00:32 -0500 Subject: [PATCH 03/15] Gate benchmarks on Test Suite completion Add wait-for-tests job that polls GitHub API to ensure: - Lint Gate passes first (fast fail) - All Github test jobs complete successfully - Only then do benchmark jobs start This prevents wasting HPC resources on benchmarking code that fails tests, while preserving the existing maintainer approval gate. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/bench.yml | 58 +++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 0b6f069f0a..f75631b2dd 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -14,7 +14,7 @@ jobs: file-changes: name: Detect File Changes runs-on: 'ubuntu-latest' - outputs: + outputs: checkall: ${{ steps.changes.outputs.checkall }} steps: - name: Clone @@ -23,13 +23,65 @@ jobs: - name: Detect Changes uses: dorny/paths-filter@v3 id: changes - with: + with: filters: ".github/file-filter.yml" + wait-for-tests: + name: Wait for Test Suite + runs-on: ubuntu-latest + steps: + - name: Wait for Test Suite to Pass + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "Waiting for Test Suite workflow to complete..." + SHA="${{ github.event.pull_request.head.sha || github.sha }}" + + # Poll every 60 seconds for up to 3 hours + for i in $(seq 1 180); do + # Get the Test Suite workflow runs for this commit + STATUS=$(gh api repos/${{ github.repository }}/commits/$SHA/check-runs \ + --jq '.check_runs[] | select(.name == "Lint Gate") | .conclusion' | head -1) + + if [ "$STATUS" = "success" ]; then + echo "Lint Gate passed. Checking test jobs..." + + # Check if any Github test jobs failed + FAILED=$(gh api repos/${{ github.repository }}/commits/$SHA/check-runs \ + --jq '[.check_runs[] | select(.name | startswith("Github")) | select(.conclusion == "failure")] | length') + + if [ "$FAILED" != "0" ]; then + echo "::error::Test Suite has failing jobs. Benchmarks will not run." + exit 1 + fi + + # Check if Github tests are still running + PENDING=$(gh api repos/${{ github.repository }}/commits/$SHA/check-runs \ + --jq '[.check_runs[] | select(.name | startswith("Github")) | select(.conclusion == null)] | length') + + if [ "$PENDING" = "0" ]; then + echo "All Test Suite jobs completed successfully!" + exit 0 + fi + + echo "Tests still running ($PENDING pending)..." + elif [ "$STATUS" = "failure" ]; then + echo "::error::Lint Gate failed. Benchmarks will not run." + exit 1 + else + echo "Lint Gate status: ${STATUS:-pending}..." + fi + + sleep 60 + done + + echo "::error::Timeout waiting for Test Suite to complete." + exit 1 + self: name: "${{ matrix.name }} (${{ matrix.device }}${{ matrix.interface != 'none' && format('-{0}', matrix.interface) || '' }})" if: ${{ github.repository=='MFlowCode/MFC' && needs.file-changes.outputs.checkall=='true' && ((github.event_name=='pull_request_review' && github.event.review.state=='approved') || (github.event_name=='pull_request' && (github.event.pull_request.user.login=='sbryngelson' || github.event.pull_request.user.login=='wilfonba'))) }} - needs: file-changes + needs: [file-changes, wait-for-tests] strategy: fail-fast: false matrix: From 1b211a53e29b8e7fdd0264ca668f742b4b9e0c08 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 18:15:18 -0500 Subject: [PATCH 04/15] Auto-install git pre-commit hook for precheck - Add .githooks/pre-commit that runs ./mfc.sh precheck before commits - Auto-install hook on first ./mfc.sh invocation (symlinks to .git/hooks/) - Hook only installs once; subsequent runs skip if already present - Developers can bypass with: git commit --no-verify Co-Authored-By: Claude Opus 4.5 --- .githooks/pre-commit | 27 +++++++++++++++++++++++++++ mfc.sh | 6 ++++++ 2 files changed, 33 insertions(+) create mode 100755 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000000..2bae9c8653 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,27 @@ +#!/bin/bash + +# MFC pre-commit hook +# Runs ./mfc.sh precheck before allowing commits +# Bypass with: git commit --no-verify + +# Only run if we're in the MFC repo root (where mfc.sh exists) +if [ ! -f "$(git rev-parse --show-toplevel)/mfc.sh" ]; then + exit 0 +fi + +cd "$(git rev-parse --show-toplevel)" + +echo "" +echo "mfc: Running precheck before commit..." +echo "" + +# Run precheck with parallel jobs +if ./mfc.sh precheck -j 4; then + echo "" + exit 0 +else + echo "" + echo "mfc: Commit blocked. Fix issues above or use 'git commit --no-verify' to bypass." + echo "" + exit 1 +fi diff --git a/mfc.sh b/mfc.sh index 3eb015e1e9..5a1308ca98 100755 --- a/mfc.sh +++ b/mfc.sh @@ -10,6 +10,12 @@ fi # Load utility script . "$(pwd)/toolchain/util.sh" +# Auto-install git pre-commit hook (once, silently) +if [ -d "$(pwd)/.git" ] && [ ! -e "$(pwd)/.git/hooks/pre-commit" ] && [ -f "$(pwd)/.githooks/pre-commit" ]; then + ln -sf "$(pwd)/.githooks/pre-commit" "$(pwd)/.git/hooks/pre-commit" + log "Installed git pre-commit hook (runs$MAGENTA ./mfc.sh precheck$COLOR_RESET before commits)." +fi + # Handle upgrading from older MFC build systems if [ -d "$(pwd)/bootstrap" ] || [ -d "$(pwd)/dependencies" ] || [ -f "$(pwd)/build/mfc.lock.yaml" ]; then error "Please remove, if applicable, the following directories:" From 9304137c8c3af1c16a4a87e36da6ac9b036f904d Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 18:41:50 -0500 Subject: [PATCH 05/15] Use dynamic CPU count in pre-commit hook Auto-detect available CPUs for parallel formatting: - Linux: nproc - macOS: sysctl -n hw.ncpu - Fallback: 4 Co-Authored-By: Claude Opus 4.5 --- .githooks/pre-commit | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 2bae9c8653..84d0110dab 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -15,8 +15,9 @@ echo "" echo "mfc: Running precheck before commit..." echo "" -# Run precheck with parallel jobs -if ./mfc.sh precheck -j 4; then +# Run precheck with parallel jobs (auto-detect CPU count) +JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) +if ./mfc.sh precheck -j "$JOBS"; then echo "" exit 0 else From 77ac8cc391b75854383ec7226340acd566ebcb2c Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 18:45:38 -0500 Subject: [PATCH 06/15] Show CPU count in pre-commit hook output Co-Authored-By: Claude Opus 4.5 --- .githooks/pre-commit | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 84d0110dab..01e6f3c257 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -11,12 +11,13 @@ fi cd "$(git rev-parse --show-toplevel)" +# Auto-detect CPU count +JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + echo "" -echo "mfc: Running precheck before commit..." +echo "mfc: Running precheck before commit (-j $JOBS)..." echo "" -# Run precheck with parallel jobs (auto-detect CPU count) -JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) if ./mfc.sh precheck -j "$JOBS"; then echo "" exit 0 From 4b55c30102f70a81a87b6d28b9c53ad05e4eaa4a Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 20:27:52 -0500 Subject: [PATCH 07/15] Add precheck command to CLI and autocomplete Register precheck in commands.py so it appears in: - Shell tab completion - CLI documentation - ./mfc.sh precheck --help Co-Authored-By: Claude Opus 4.5 --- toolchain/mfc/cli/commands.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/toolchain/mfc/cli/commands.py b/toolchain/mfc/cli/commands.py index 0f827a4b74..9697078aa2 100644 --- a/toolchain/mfc/cli/commands.py +++ b/toolchain/mfc/cli/commands.py @@ -751,6 +751,27 @@ ], ) +PRECHECK_COMMAND = Command( + name="precheck", + help="Run CI lint checks locally before committing.", + description="Run the same fast checks that CI runs before expensive tests start. Use this locally before pushing to catch issues early.", + arguments=[ + Argument( + name="jobs", + short="j", + help="Number of parallel jobs for formatting.", + metavar="N", + ), + ], + examples=[ + Example("./mfc.sh precheck", "Run all lint checks"), + Example("./mfc.sh precheck -j 8", "Run with 8 parallel jobs"), + ], + key_options=[ + ("-j, --jobs N", "Number of parallel jobs for formatting"), + ], +) + INTERACTIVE_COMMAND = Command( name="interactive", help="Launch interactive menu-driven interface.", @@ -990,6 +1011,7 @@ LINT_COMMAND, FORMAT_COMMAND, SPELLING_COMMAND, + PRECHECK_COMMAND, INTERACTIVE_COMMAND, BENCH_COMMAND, BENCH_DIFF_COMMAND, From e017cf0993906f428342dfe74361ce3432347480 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 20:34:18 -0500 Subject: [PATCH 08/15] Auto-update installed shell completions on regeneration When completion scripts are auto-regenerated, also update the installed completions at ~/.local/share/mfc/completions/ if they exist. Co-Authored-By: Claude Opus 4.5 --- toolchain/main.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/toolchain/main.py b/toolchain/main.py index 10e6e44ab0..3c0d1c2d5d 100644 --- a/toolchain/main.py +++ b/toolchain/main.py @@ -12,6 +12,7 @@ def __do_regenerate(toolchain: str): """Perform the actual regeneration of completion scripts and schema.""" import json # pylint: disable=import-outside-toplevel + import shutil # pylint: disable=import-outside-toplevel from pathlib import Path # pylint: disable=import-outside-toplevel from mfc.cli.commands import MFC_CLI_SCHEMA # pylint: disable=import-outside-toplevel from mfc.cli.completion_gen import generate_bash_completion, generate_zsh_completion # pylint: disable=import-outside-toplevel @@ -23,14 +24,24 @@ def __do_regenerate(toolchain: str): completions_dir.mkdir(exist_ok=True) # Generate completion files - (completions_dir / "mfc.bash").write_text(generate_bash_completion(MFC_CLI_SCHEMA)) - (completions_dir / "_mfc").write_text(generate_zsh_completion(MFC_CLI_SCHEMA)) + bash_content = generate_bash_completion(MFC_CLI_SCHEMA) + zsh_content = generate_zsh_completion(MFC_CLI_SCHEMA) + (completions_dir / "mfc.bash").write_text(bash_content) + (completions_dir / "_mfc").write_text(zsh_content) # Generate JSON schema schema = generate_json_schema(include_descriptions=True) with open(Path(toolchain) / "mfc-case-schema.json", 'w', encoding='utf-8') as f: json.dump(schema, f, indent=2) + # Also update installed completions if they exist + install_dir = Path.home() / ".local" / "share" / "mfc" / "completions" + if install_dir.exists(): + if (install_dir / "mfc.bash").exists(): + shutil.copy2(completions_dir / "mfc.bash", install_dir / "mfc.bash") + if (install_dir / "_mfc").exists(): + shutil.copy2(completions_dir / "_mfc", install_dir / "_mfc") + def __ensure_generated_files(): """Auto-regenerate completion scripts and docs if source files have changed.""" From 780138ba03366bc7941640f8fd71e80a40de2754 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 20:39:53 -0500 Subject: [PATCH 09/15] Show source command when completions auto-update When installed shell completions are auto-updated, print a message with the appropriate source command for the user's detected shell. Co-Authored-By: Claude Opus 4.5 --- toolchain/main.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/toolchain/main.py b/toolchain/main.py index 3c0d1c2d5d..b2b43385a6 100644 --- a/toolchain/main.py +++ b/toolchain/main.py @@ -23,24 +23,29 @@ def __do_regenerate(toolchain: str): completions_dir = Path(toolchain) / "completions" completions_dir.mkdir(exist_ok=True) - # Generate completion files - bash_content = generate_bash_completion(MFC_CLI_SCHEMA) - zsh_content = generate_zsh_completion(MFC_CLI_SCHEMA) - (completions_dir / "mfc.bash").write_text(bash_content) - (completions_dir / "_mfc").write_text(zsh_content) + # Generate and write completion files directly + (completions_dir / "mfc.bash").write_text(generate_bash_completion(MFC_CLI_SCHEMA)) + (completions_dir / "_mfc").write_text(generate_zsh_completion(MFC_CLI_SCHEMA)) # Generate JSON schema - schema = generate_json_schema(include_descriptions=True) with open(Path(toolchain) / "mfc-case-schema.json", 'w', encoding='utf-8') as f: - json.dump(schema, f, indent=2) + json.dump(generate_json_schema(include_descriptions=True), f, indent=2) # Also update installed completions if they exist install_dir = Path.home() / ".local" / "share" / "mfc" / "completions" if install_dir.exists(): - if (install_dir / "mfc.bash").exists(): + bash_updated = (install_dir / "mfc.bash").exists() + zsh_updated = (install_dir / "_mfc").exists() + if bash_updated: shutil.copy2(completions_dir / "mfc.bash", install_dir / "mfc.bash") - if (install_dir / "_mfc").exists(): + if zsh_updated: shutil.copy2(completions_dir / "_mfc", install_dir / "_mfc") + if bash_updated or zsh_updated: + # Detect user's shell and show appropriate source command + if "zsh" in os.environ.get("SHELL", "") and zsh_updated: + cons.print(f"[dim]Tab completions updated. Run: source {install_dir}/_mfc[/dim]") + elif bash_updated: + cons.print(f"[dim]Tab completions updated. Run: source {install_dir}/mfc.bash[/dim]") def __ensure_generated_files(): From 06f042500c82adec8bb0b0806663a47f145478ed Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 21:05:51 -0500 Subject: [PATCH 10/15] Always check installed completions on every run Previously, installed completions only updated when source files changed and regeneration occurred. Now we also check if the installed completions are older than the generated ones (e.g., after git pull brings new pre-generated completions). Co-Authored-By: Claude Opus 4.5 --- toolchain/main.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/toolchain/main.py b/toolchain/main.py index b2b43385a6..e025d3da8d 100644 --- a/toolchain/main.py +++ b/toolchain/main.py @@ -48,6 +48,31 @@ def __do_regenerate(toolchain: str): cons.print(f"[dim]Tab completions updated. Run: source {install_dir}/mfc.bash[/dim]") +def __update_installed_completions(toolchain: str): + """Update installed completions if they're older than the generated ones.""" + import shutil # pylint: disable=import-outside-toplevel + from pathlib import Path # pylint: disable=import-outside-toplevel + + completions_dir = Path(toolchain) / "completions" + install_dir = Path.home() / ".local" / "share" / "mfc" / "completions" + + if not install_dir.exists(): + return + + updated = [] + for src_name, dst_name in [("mfc.bash", "mfc.bash"), ("_mfc", "_mfc")]: + src, dst = completions_dir / src_name, install_dir / dst_name + if src.exists() and dst.exists() and os.path.getmtime(src) > os.path.getmtime(dst): + shutil.copy2(src, dst) + updated.append(("bash" if src_name == "mfc.bash" else "zsh", dst)) + + if updated: + if "zsh" in os.environ.get("SHELL", "") and any(s[0] == "zsh" for s in updated): + cons.print(f"[dim]Tab completions updated. Run: source {install_dir}/_mfc[/dim]") + elif any(s[0] == "bash" for s in updated): + cons.print(f"[dim]Tab completions updated. Run: source {install_dir}/mfc.bash[/dim]") + + def __ensure_generated_files(): """Auto-regenerate completion scripts and docs if source files have changed.""" toolchain = os.path.join(MFC_ROOT_DIR, "toolchain") @@ -79,6 +104,10 @@ def __ensure_generated_files(): if needs_regen: __do_regenerate(toolchain) + else: + # Even if we didn't regenerate, check if installed completions need updating + # (e.g., after git pull with new generated files) + __update_installed_completions(toolchain) def __print_greeting(): MFC_LOGO_LINES = MFC_LOGO.splitlines() From f65ed30e2adbc10d9537144a0cb6f7ad59f452d6 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 21:11:53 -0500 Subject: [PATCH 11/15] Prevent directory completion fallback in shell completions - Remove -o bashdefault from bash complete command to prevent falling back to directory completion when no matches found - Add explicit : (no-op) for zsh commands without arguments to prevent default file/directory completion Co-Authored-By: Claude Opus 4.5 --- toolchain/mfc/cli/completion_gen.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/toolchain/mfc/cli/completion_gen.py b/toolchain/mfc/cli/completion_gen.py index d34d7d9cdc..a12f55e686 100644 --- a/toolchain/mfc/cli/completion_gen.py +++ b/toolchain/mfc/cli/completion_gen.py @@ -245,9 +245,10 @@ def generate_bash_completion(schema: CLISchema) -> str: ' return 0', '}', '', - 'complete -o filenames -o bashdefault -F _mfc_completions ./mfc.sh', - 'complete -o filenames -o bashdefault -F _mfc_completions mfc.sh', - 'complete -o filenames -o bashdefault -F _mfc_completions mfc', + '# Note: -o nospace prevents trailing space; we only use -o filenames when actually completing files', + 'complete -F _mfc_completions ./mfc.sh', + 'complete -F _mfc_completions mfc.sh', + 'complete -F _mfc_completions mfc', ]) return '\n'.join(lines) @@ -401,6 +402,9 @@ def generate_zsh_completion(schema: CLISchema) -> str: if arg_lines: lines.append(' _arguments \\') lines.append(' ' + ' \\\n '.join(arg_lines)) + else: + # Explicitly disable default completion for commands with no args + lines.append(' :') lines.append(' ;;') lines.extend([ From d23addc7ea98fa1c3518ce88f57adaaa59251216 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 21:19:45 -0500 Subject: [PATCH 12/15] Auto-install completions and fix bash completion options - Auto-install completions on first mfc.sh run (via main.py) - Add -o filenames back to bash complete (needed for file completion) - Keep -o bashdefault removed to prevent directory fallback - Simplify code by having __update_installed_completions handle both install and update cases Co-Authored-By: Claude Opus 4.5 --- toolchain/main.py | 36 +++++++++++------------------ toolchain/mfc/cli/completion_gen.py | 9 ++++---- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/toolchain/main.py b/toolchain/main.py index e025d3da8d..bc684024c6 100644 --- a/toolchain/main.py +++ b/toolchain/main.py @@ -12,7 +12,6 @@ def __do_regenerate(toolchain: str): """Perform the actual regeneration of completion scripts and schema.""" import json # pylint: disable=import-outside-toplevel - import shutil # pylint: disable=import-outside-toplevel from pathlib import Path # pylint: disable=import-outside-toplevel from mfc.cli.commands import MFC_CLI_SCHEMA # pylint: disable=import-outside-toplevel from mfc.cli.completion_gen import generate_bash_completion, generate_zsh_completion # pylint: disable=import-outside-toplevel @@ -31,34 +30,28 @@ def __do_regenerate(toolchain: str): with open(Path(toolchain) / "mfc-case-schema.json", 'w', encoding='utf-8') as f: json.dump(generate_json_schema(include_descriptions=True), f, indent=2) - # Also update installed completions if they exist - install_dir = Path.home() / ".local" / "share" / "mfc" / "completions" - if install_dir.exists(): - bash_updated = (install_dir / "mfc.bash").exists() - zsh_updated = (install_dir / "_mfc").exists() - if bash_updated: - shutil.copy2(completions_dir / "mfc.bash", install_dir / "mfc.bash") - if zsh_updated: - shutil.copy2(completions_dir / "_mfc", install_dir / "_mfc") - if bash_updated or zsh_updated: - # Detect user's shell and show appropriate source command - if "zsh" in os.environ.get("SHELL", "") and zsh_updated: - cons.print(f"[dim]Tab completions updated. Run: source {install_dir}/_mfc[/dim]") - elif bash_updated: - cons.print(f"[dim]Tab completions updated. Run: source {install_dir}/mfc.bash[/dim]") - def __update_installed_completions(toolchain: str): - """Update installed completions if they're older than the generated ones.""" + """Install or update shell completions automatically.""" import shutil # pylint: disable=import-outside-toplevel from pathlib import Path # pylint: disable=import-outside-toplevel completions_dir = Path(toolchain) / "completions" install_dir = Path.home() / ".local" / "share" / "mfc" / "completions" + # Auto-install if not installed yet if not install_dir.exists(): + install_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(completions_dir / "mfc.bash", install_dir / "mfc.bash") + shutil.copy2(completions_dir / "_mfc", install_dir / "_mfc") + user_shell = os.environ.get("SHELL", "") + if "zsh" in user_shell: + cons.print(f"[dim]Tab completions installed. Run: source {install_dir}/_mfc[/dim]") + else: + cons.print(f"[dim]Tab completions installed. Run: source {install_dir}/mfc.bash[/dim]") return + # Update if installed but older than generated updated = [] for src_name, dst_name in [("mfc.bash", "mfc.bash"), ("_mfc", "_mfc")]: src, dst = completions_dir / src_name, install_dir / dst_name @@ -104,10 +97,9 @@ def __ensure_generated_files(): if needs_regen: __do_regenerate(toolchain) - else: - # Even if we didn't regenerate, check if installed completions need updating - # (e.g., after git pull with new generated files) - __update_installed_completions(toolchain) + + # Always check if completions need to be installed or updated + __update_installed_completions(toolchain) def __print_greeting(): MFC_LOGO_LINES = MFC_LOGO.splitlines() diff --git a/toolchain/mfc/cli/completion_gen.py b/toolchain/mfc/cli/completion_gen.py index a12f55e686..45ab1e94a2 100644 --- a/toolchain/mfc/cli/completion_gen.py +++ b/toolchain/mfc/cli/completion_gen.py @@ -245,10 +245,11 @@ def generate_bash_completion(schema: CLISchema) -> str: ' return 0', '}', '', - '# Note: -o nospace prevents trailing space; we only use -o filenames when actually completing files', - 'complete -F _mfc_completions ./mfc.sh', - 'complete -F _mfc_completions mfc.sh', - 'complete -F _mfc_completions mfc', + '# -o filenames: handle escaping/slashes for file completions', + '# Removed -o bashdefault to prevent unwanted directory fallback', + 'complete -o filenames -F _mfc_completions ./mfc.sh', + 'complete -o filenames -F _mfc_completions mfc.sh', + 'complete -o filenames -F _mfc_completions mfc', ]) return '\n'.join(lines) From 33cbd0e182d62492e2006a63ee2731580bd419d7 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 21:27:49 -0500 Subject: [PATCH 13/15] Auto-install completions from mfc.sh with shell rc setup Move completion auto-install to mfc.sh so it runs for ALL commands including help, precheck, etc. This ensures completions are always set up on first run. - Install completion files to ~/.local/share/mfc/completions/ - Add source line to .bashrc or fpath to .zshrc - Tell user to restart shell or source the file - main.py now only handles updates when generated files change Co-Authored-By: Claude Opus 4.5 --- mfc.sh | 27 +++++++++++++++++++++++++++ toolchain/main.py | 39 +++++++++++++++------------------------ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/mfc.sh b/mfc.sh index 5ad491d602..fa3d420df9 100755 --- a/mfc.sh +++ b/mfc.sh @@ -16,6 +16,33 @@ if [ -d "$(pwd)/.git" ] && [ ! -e "$(pwd)/.git/hooks/pre-commit" ] && [ -f "$(pw log "Installed git pre-commit hook (runs$MAGENTA ./mfc.sh precheck$COLOR_RESET before commits)." fi +# Auto-install shell completions (once) +COMPLETION_DIR="$HOME/.local/share/mfc/completions" +if [ ! -d "$COMPLETION_DIR" ]; then + mkdir -p "$COMPLETION_DIR" + cp "$(pwd)/toolchain/completions/mfc.bash" "$COMPLETION_DIR/" + cp "$(pwd)/toolchain/completions/_mfc" "$COMPLETION_DIR/" + + # Add to shell rc file based on current shell + if [[ "$SHELL" == *"zsh"* ]]; then + RC_FILE="$HOME/.zshrc" + RC_LINE="fpath=(\"$COMPLETION_DIR\" \$fpath)" + SOURCE_CMD="source $COMPLETION_DIR/_mfc" + else + RC_FILE="$HOME/.bashrc" + RC_LINE="[ -f \"$COMPLETION_DIR/mfc.bash\" ] && source \"$COMPLETION_DIR/mfc.bash\"" + SOURCE_CMD="source $COMPLETION_DIR/mfc.bash" + fi + + if [ -f "$RC_FILE" ] && ! grep -q "$COMPLETION_DIR" "$RC_FILE" 2>/dev/null; then + echo "" >> "$RC_FILE" + echo "# MFC shell completion" >> "$RC_FILE" + echo "$RC_LINE" >> "$RC_FILE" + fi + + log "Installed tab completions. Restart shell or run:$MAGENTA $SOURCE_CMD$COLOR_RESET" +fi + # Print startup message immediately for user feedback log "Starting..." diff --git a/toolchain/main.py b/toolchain/main.py index bc684024c6..568db1db67 100644 --- a/toolchain/main.py +++ b/toolchain/main.py @@ -32,38 +32,29 @@ def __do_regenerate(toolchain: str): def __update_installed_completions(toolchain: str): - """Install or update shell completions automatically.""" + """Update installed shell completions if they're older than generated ones.""" import shutil # pylint: disable=import-outside-toplevel from pathlib import Path # pylint: disable=import-outside-toplevel - completions_dir = Path(toolchain) / "completions" - install_dir = Path.home() / ".local" / "share" / "mfc" / "completions" - - # Auto-install if not installed yet - if not install_dir.exists(): - install_dir.mkdir(parents=True, exist_ok=True) - shutil.copy2(completions_dir / "mfc.bash", install_dir / "mfc.bash") - shutil.copy2(completions_dir / "_mfc", install_dir / "_mfc") - user_shell = os.environ.get("SHELL", "") - if "zsh" in user_shell: - cons.print(f"[dim]Tab completions installed. Run: source {install_dir}/_mfc[/dim]") - else: - cons.print(f"[dim]Tab completions installed. Run: source {install_dir}/mfc.bash[/dim]") + src_dir = Path(toolchain) / "completions" + dst_dir = Path.home() / ".local" / "share" / "mfc" / "completions" + + # Only update if already installed (mfc.sh handles initial install) + if not dst_dir.exists(): return # Update if installed but older than generated - updated = [] - for src_name, dst_name in [("mfc.bash", "mfc.bash"), ("_mfc", "_mfc")]: - src, dst = completions_dir / src_name, install_dir / dst_name - if src.exists() and dst.exists() and os.path.getmtime(src) > os.path.getmtime(dst): - shutil.copy2(src, dst) - updated.append(("bash" if src_name == "mfc.bash" else "zsh", dst)) + updated = False + for name in ["mfc.bash", "_mfc"]: + if (src_dir / name).exists() and (dst_dir / name).exists(): + if os.path.getmtime(src_dir / name) > os.path.getmtime(dst_dir / name): + shutil.copy2(src_dir / name, dst_dir / name) + updated = True if updated: - if "zsh" in os.environ.get("SHELL", "") and any(s[0] == "zsh" for s in updated): - cons.print(f"[dim]Tab completions updated. Run: source {install_dir}/_mfc[/dim]") - elif any(s[0] == "bash" for s in updated): - cons.print(f"[dim]Tab completions updated. Run: source {install_dir}/mfc.bash[/dim]") + is_zsh = "zsh" in os.environ.get("SHELL", "") + src_cmd = f"source {dst_dir}/_mfc" if is_zsh else f"source {dst_dir}/mfc.bash" + cons.print(f"[dim]Tab completions updated. Run: {src_cmd}[/dim]") def __ensure_generated_files(): From 1ba466614d8969b755c4f169ceef3992c6d0de0f Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Wed, 4 Feb 2026 22:43:34 -0500 Subject: [PATCH 14/15] Clarify verbose, debug, and debug-log flag documentation - -v/-vv/-vvv: output verbosity levels - --debug: build with debug compiler flags (for MFC Fortran code) - --debug-log/-d: Python toolchain debug logging (not MFC code) Co-Authored-By: Claude Opus 4.5 --- toolchain/mfc/cli/commands.py | 8 ++++---- toolchain/mfc/cli/completion_gen.py | 4 ++-- toolchain/mfc/cli/docs_gen.py | 13 ++++++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/toolchain/mfc/cli/commands.py b/toolchain/mfc/cli/commands.py index 9697078aa2..8ad8c4bd07 100644 --- a/toolchain/mfc/cli/commands.py +++ b/toolchain/mfc/cli/commands.py @@ -85,7 +85,7 @@ Argument( name="verbose", short="v", - help="Increase verbosity level. Use -v, -vv, or -vvv for more detail.", + help="Increase output verbosity (-v, -vv, -vvv for more detail).", action=ArgAction.COUNT, default=0, ), @@ -98,7 +98,7 @@ Argument( name="debug-log", short="d", - help="Enable debug logging for troubleshooting.", + help="Enable Python toolchain debug logging (not MFC code).", action=ArgAction.STORE_TRUE, dest="debug_log", ), @@ -519,10 +519,10 @@ ], examples=[ Example("./mfc.sh validate case.py", "Check syntax and constraints"), - Example("./mfc.sh validate case.py -d", "Validate with debug output"), + Example("./mfc.sh validate case.py -d", "Validate with toolchain debug output"), ], key_options=[ - ("-d, --debug-log", "Enable debug logging"), + ("-d, --debug-log", "Enable toolchain debug logging"), ], ) diff --git a/toolchain/mfc/cli/completion_gen.py b/toolchain/mfc/cli/completion_gen.py index 45ab1e94a2..7a286bc981 100644 --- a/toolchain/mfc/cli/completion_gen.py +++ b/toolchain/mfc/cli/completion_gen.py @@ -319,8 +319,8 @@ def _generate_zsh_command_args(cmd: Command, schema: CLISchema) -> List[str]: "'--no-mpi[Disable MPI]'", "'--gpu[Enable GPU]:mode:(acc mp)'", "'--no-gpu[Disable GPU]'", - "'--debug[Enable debug mode]'", - "'--no-debug[Disable debug mode]'", + "'--debug[Build with debug compiler flags (for MFC code)]'", + "'--no-debug[Build without debug flags]'", "'--gcov[Enable gcov coverage]'", "'--no-gcov[Disable gcov coverage]'", "'--unified[Enable unified memory]'", diff --git a/toolchain/mfc/cli/docs_gen.py b/toolchain/mfc/cli/docs_gen.py index 0c4b8b69a7..cf120fc07f 100644 --- a/toolchain/mfc/cli/docs_gen.py +++ b/toolchain/mfc/cli/docs_gen.py @@ -83,7 +83,7 @@ def _generate_options_table(cmd: Command, schema: CLISchema) -> List[str]: if "mfc_config" in cmd.include_common: lines.append("| `--mpi`, `--no-mpi` | Enable/disable MPI | `true` |") lines.append("| `--gpu [acc/mp]`, `--no-gpu` | Enable GPU (OpenACC/OpenMP) | `no` |") - lines.append("| `--debug`, `--no-debug` | Enable debug mode | `false` |") + lines.append("| `--debug`, `--no-debug` | Build with debug compiler flags | `false` |") lines.append("") @@ -270,16 +270,23 @@ def generate_cli_reference(schema: CLISchema) -> str: "|------|-------------|", "| `--mpi` / `--no-mpi` | Enable/disable MPI support |", "| `--gpu [acc/mp]` / `--no-gpu` | Enable GPU with OpenACC or OpenMP |", - "| `--debug` / `--no-debug` | Enable debug build |", + "| `--debug` / `--no-debug` | Build with debug compiler flags |", "| `--gcov` / `--no-gcov` | Enable code coverage |", "| `--single` / `--no-single` | Single precision |", "| `--mixed` / `--no-mixed` | Mixed precision |", "", "### Verbosity (`-v, --verbose`)", "", + "Controls output verbosity level:", + "", "- `-v` - Basic verbose output", "- `-vv` - Show build commands", - "- `-vvv` - Full debug output including CMake debug", + "- `-vvv` - Full verbose output including CMake details", + "", + "### Debug Logging (`-d, --debug-log`)", + "", + "Enables debug logging for the Python toolchain (mfc.sh internals).", + "This is for troubleshooting the build system, not the MFC simulation code.", "", ]) From adfcb7f961be00f05d57db1bbfe2888a1a78b5fc Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Thu, 5 Feb 2026 09:10:17 -0500 Subject: [PATCH 15/15] Cap pre-commit hook parallelism at 12 jobs Avoid hogging resources on machines with many cores. Co-Authored-By: Claude Opus 4.5 --- .githooks/pre-commit | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 01e6f3c257..b13ccf0cfb 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -11,8 +11,9 @@ fi cd "$(git rev-parse --show-toplevel)" -# Auto-detect CPU count +# Auto-detect CPU count (capped at 12 to avoid hogging resources) JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) +[ "$JOBS" -gt 12 ] && JOBS=12 echo "" echo "mfc: Running precheck before commit (-j $JOBS)..."