From c749f71eb7532b0ce28280921d37106cb70da6c4 Mon Sep 17 00:00:00 2001 From: Kush Agrawal Date: Sun, 21 Jun 2026 14:20:11 -0400 Subject: [PATCH 1/3] Fix sync-ai-rules not triggering on rule file deletions pre-commit gathers staged files with --diff-filter=ACMRTUXB, which excludes D (deleted). When a .cursor/rules/*.mdc file is deleted, it never matches the hook's files pattern, so the hook is silently skipped and the corresponding .claude/rules/generated/*.md output is left orphaned. Switch to always_run: true so the hook runs on every commit. The hook's internal wipe-and-regenerate logic already handles the case where no source rules have changed (it regenerates from whatever .cursor/rules/ files currently exist, deleting outputs for any that are gone). Co-Authored-By: Claude Opus 4.6 (1M context) --- .pre-commit-hooks.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 81ca3e7..fc69a17 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -14,7 +14,11 @@ name: Sync AI Rules entry: duolingo/pre-commit-hooks:1.16.1 sh -c "PYTHONPATH=/ python3 -m sync_ai_rules" language: docker_image - files: &sync_ai_rules_files (^\.cursor/rules/.*\.mdc$|^\.code_review/.*\.md$) + # always_run is needed because pre-commit uses --diff-filter=ACMRTUXB + # (everything except D), so deleting a rule file never matches the files + # pattern and the hook is silently skipped. The wipe-and-regenerate logic + # inside the hook handles no-op detection on its own. + always_run: true pass_filenames: false # Nobody should ever use these hooks in production. They're just for testing PRs in @@ -32,5 +36,5 @@ name: Sync AI Rules (dev) entry: sh -c "PYTHONPATH=/ python3 -m sync_ai_rules" language: docker - files: *sync_ai_rules_files + always_run: true pass_filenames: false From ad32a92dcfca92ab5c3f01032cc9271d7aad3a24 Mon Sep 17 00:00:00 2001 From: Kush Agrawal Date: Sun, 21 Jun 2026 14:22:48 -0400 Subject: [PATCH 2/3] Remove verbose comment Co-Authored-By: Claude Opus 4.6 (1M context) --- .pre-commit-hooks.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index fc69a17..50c8bf4 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -14,10 +14,6 @@ name: Sync AI Rules entry: duolingo/pre-commit-hooks:1.16.1 sh -c "PYTHONPATH=/ python3 -m sync_ai_rules" language: docker_image - # always_run is needed because pre-commit uses --diff-filter=ACMRTUXB - # (everything except D), so deleting a rule file never matches the files - # pattern and the hook is silently skipped. The wipe-and-regenerate logic - # inside the hook handles no-op detection on its own. always_run: true pass_filenames: false From 7e34bf5138a126ce418d0efe6df9b5a6b30d60d7 Mon Sep 17 00:00:00 2001 From: Kush Agrawal Date: Thu, 25 Jun 2026 17:00:57 -0400 Subject: [PATCH 3/3] Add early-exit git diff check to skip hook on unrelated commits Co-Authored-By: Claude Opus 4.6 --- sync_ai_rules/__main__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/sync_ai_rules/__main__.py b/sync_ai_rules/__main__.py index 6961c89..53e5d0c 100755 --- a/sync_ai_rules/__main__.py +++ b/sync_ai_rules/__main__.py @@ -5,6 +5,7 @@ import logging import os +import subprocess from pathlib import Path from typing import Dict, List @@ -96,8 +97,29 @@ def _ensure_agents_skills_symlinks(project_root: str) -> None: logger.warning("Failed to create agents skills symlink at %s: %s", rel, e) +_SOURCE_PREFIXES = (".cursor/", ".code_review/", ".agents/") + + +def _has_relevant_staged_changes() -> bool: + """Check if any staged files (including deletions) touch source directories.""" + try: + result = subprocess.run( + ["git", "diff", "--cached", "--name-only"], + capture_output=True, + text=True, + check=True, + ) + except (subprocess.CalledProcessError, FileNotFoundError): + return True + + return any(line.startswith(_SOURCE_PREFIXES) for line in result.stdout.splitlines()) + + def main(): """Main orchestration: load pipelines → parse → generate → update files.""" + if not _has_relevant_staged_changes(): + return + # Setup project_root = str(Path.cwd()) script_dir = os.path.dirname(os.path.abspath(__file__))