diff --git a/.claude/hooks/auto_fix_diagnostics.py b/.claude/hooks/auto_fix_diagnostics.py index 30d6fd1e..1388e39f 100644 --- a/.claude/hooks/auto_fix_diagnostics.py +++ b/.claude/hooks/auto_fix_diagnostics.py @@ -299,9 +299,30 @@ def is_same_file_as_last(file_path: str) -> bool: return False +def _project_has_own_posttooluse_hooks() -> bool: + """Check if CWD is inside a project with its own PostToolUse hooks.""" + search = Path.cwd() + home = Path.home() + while search != home and search.parent != search: + settings = search / ".claude" / "settings.json" + if settings.exists(): + try: + data = json.loads(settings.read_text(encoding="utf-8")) + ptu = data.get("hooks", {}).get("PostToolUse", []) + if ptu: + return True + except (json.JSONDecodeError, OSError): + pass + search = search.parent + return False + + def main(): """Main hook entry point.""" try: + if _project_has_own_posttooluse_hooks(): + return + input_data = json.load(sys.stdin) tool_name = input_data.get("tool_name", "") tool_input = input_data.get("tool_input", {}) @@ -318,10 +339,8 @@ def main(): # Collect all errors errors = [] - file_type = "" if file_path.endswith(".py"): - file_type = "Python" errors = run_python_checks(file_path) # Seedgo standards checklist @@ -339,7 +358,6 @@ def main(): save_diagnostics_state(file_path, ruff_lint_errors + type_errors) elif file_path.endswith(".json"): - file_type = "JSON" errors = run_json_checks(file_path) else: return diff --git a/src/aipass/drone/apps/handlers/git/pr_handler.py b/src/aipass/drone/apps/handlers/git/pr_handler.py index ed8b75c8..118fb0ab 100644 --- a/src/aipass/drone/apps/handlers/git/pr_handler.py +++ b/src/aipass/drone/apps/handlers/git/pr_handler.py @@ -17,6 +17,7 @@ and cleanup (checkout main + release lock) in a finally block. from __future__ import annotations import json as _json +import re import subprocess from pathlib import Path @@ -73,18 +74,26 @@ To contribute from a fork: """ -def _resolve_git_branch(branch_name: str, branch_dir: Path) -> str: - """Read git_branch from passport if available, else fall back to citizen/{name}.""" +def _slugify(description: str) -> str: + """Convert description to a branch-safe slug.""" + slug = description.lower().strip().replace(" ", "-") + slug = re.sub(r"[^a-z0-9-]", "", slug) + slug = re.sub(r"-{2,}", "-", slug).strip("-") + return slug[:50] + + +def _resolve_git_branch(branch_name: str, branch_dir: Path, slug: str = "") -> str: + """Read git_branch from passport as prefix, append slug for uniqueness.""" passport_path = branch_dir / ".trinity" / "passport.json" if passport_path.is_file(): try: data = _json.loads(passport_path.read_text()) git_branch = data.get("branch_info", {}).get("git_branch", "") if git_branch: - return git_branch + return f"{git_branch}-{slug}" if slug else git_branch except (ValueError, OSError) as exc: logger.warning("Failed to read git_branch from passport %s: %s", passport_path, exc) - return f"citizen/{branch_name}" + return f"citizen/{branch_name}-{slug}" if slug else f"citizen/{branch_name}" def create_pr(branch_name: str, description: str, branch_dir: Path) -> dict: @@ -115,7 +124,8 @@ def create_pr(branch_name: str, description: str, branch_dir: Path) -> dict: Dict with success, pr_url, feature_branch, and message. """ repo_root = find_repo_root() - feature_branch = _resolve_git_branch(branch_name, branch_dir) + slug = _slugify(description) + feature_branch = _resolve_git_branch(branch_name, branch_dir, slug) lock_acquired = False result = { @@ -209,9 +219,9 @@ def create_pr(branch_name: str, description: str, branch_dir: Path) -> dict: logger.error(result["message"]) return result - # Step 7: Push feature branch (force-with-lease for persistent citizen branches) + # Step 7: Push feature branch push = subprocess.run( - ["git", "push", "--force-with-lease", "origin", feature_branch], + ["git", "push", "-u", "origin", feature_branch], capture_output=True, text=True, cwd=str(repo_root), @@ -244,7 +254,7 @@ def create_pr(branch_name: str, description: str, branch_dir: Path) -> dict: cwd=str(repo_root), ) if pr_create.returncode != 0: - # Check if PR already exists — force-push already updated it + # Check if PR already exists for this branch existing = subprocess.run( ["gh", "pr", "list", "--head", feature_branch, "--json", "url", "--limit", "1"], capture_output=True, @@ -258,7 +268,7 @@ def create_pr(branch_name: str, description: str, branch_dir: Path) -> dict: existing_prs = [] if existing_prs: pr_url = existing_prs[0]["url"] - logger.info("Existing PR updated via force-push: %s", pr_url) + logger.info("Existing PR found: %s", pr_url) else: result["message"] = f"PR creation failed: {pr_create.stderr.strip()}" logger.error(result["message"]) diff --git a/src/aipass/drone/apps/plugins/devpulse_ops/pr_plugin.py b/src/aipass/drone/apps/plugins/devpulse_ops/pr_plugin.py index 60d87824..0b2f0b6e 100644 --- a/src/aipass/drone/apps/plugins/devpulse_ops/pr_plugin.py +++ b/src/aipass/drone/apps/plugins/devpulse_ops/pr_plugin.py @@ -92,8 +92,8 @@ def create_system_pr(description: str, caller: str) -> dict: """ repo_root = find_repo_root() slug = slugify(description) - passport_branch = _resolve_system_git_branch(caller, repo_root) - feature_branch = passport_branch if passport_branch else f"system/{caller}-{slug}" + passport_prefix = _resolve_system_git_branch(caller, repo_root) + feature_branch = f"{passport_prefix}-{slug}" if passport_prefix else f"system/{caller}-{slug}" lock_acquired = False result: dict = { @@ -210,7 +210,7 @@ def create_system_pr(description: str, caller: str) -> dict: # Step 7: Push feature branch push = subprocess.run( - ["git", "push", "--force-with-lease", "origin", feature_branch], + ["git", "push", "-u", "origin", feature_branch], capture_output=True, text=True, cwd=str(repo_root),