From 188f6b3b164a19c4c38dd90532f49d525103a698 Mon Sep 17 00:00:00 2001 From: rob Date: Sat, 1 Nov 2025 02:33:31 -0300 Subject: [PATCH] fix: Handle empty AI diffs gracefully instead of raising errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed approach from disabling outputs to properly handling AI's decision not to generate changes (e.g., gated outputs, conditional rules). Changes: 1. patcher.py - Allow empty diffs - sanitize_unified_patch() returns empty string instead of raising error - generate_output() returns early for empty patches (silent skip) - Common case: implementation_gate_writer when status != READY_FOR_IMPLEMENTATION - AI can now return explanatory text without a diff (no error) 2. features.ai-rules.yml - Override README rule - Add README.md → "readme_skip" association - Creates empty rule to disable README updates in Docs/features/ - Prevents unnecessary AI calls during feature discussions - README automation still works in root directory 3. root.ai-rules.yml - Restore default README rule - Removed "enabled: false" flag (back to default enabled) - Features directory overrides this with empty rule Benefits: - implementation_gate now calls AI but AI returns empty diff (as designed) - No more "[runner] error generating ...implementation.discussion.md" - No more "[runner] error generating README.md" - Clean separation: AI decides vs. config disables - Instructions to AI are still executed, AI just chooses no changes Testing: Setup completes cleanly with no [runner] errors. The automation runs and AI correctly returns no diff for implementation file when status is OPEN. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- assets/templates/rules/features.ai-rules.yml | 9 ++++++--- assets/templates/rules/root.ai-rules.yml | 1 - automation/patcher.py | 21 ++++++++++++-------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/assets/templates/rules/features.ai-rules.yml b/assets/templates/rules/features.ai-rules.yml index 34a8398..3c20cc6 100644 --- a/assets/templates/rules/features.ai-rules.yml +++ b/assets/templates/rules/features.ai-rules.yml @@ -6,19 +6,22 @@ file_associations: "feature.discussion.sum.md": "discussion_summary" "implementation.discussion.md": "implementation_discussion_update" "implementation.discussion.sum.md": "discussion_summary" + "README.md": "readme_skip" # Override root rule - don't update README from features/ rules: + readme_skip: + # Disable README updates when editing files in Docs/features/ + # This prevents unnecessary AI calls during feature discussions + outputs: {} + feature_request: outputs: feature_discussion: path: "Docs/features/{feature_id}/discussions/feature.discussion.md" output_type: "feature_discussion_writer" implementation_gate: - enabled: false # Disabled by default - enable when feature is ready for implementation path: "Docs/features/{feature_id}/discussions/implementation.discussion.md" output_type: "implementation_gate_writer" - # To enable: set "enabled: true" in your project's .ai-rules.yml - # This prevents unnecessary AI calls during initial feature discussion phase feature_discussion_update: outputs: diff --git a/assets/templates/rules/root.ai-rules.yml b/assets/templates/rules/root.ai-rules.yml index 72fe236..efbcb61 100644 --- a/assets/templates/rules/root.ai-rules.yml +++ b/assets/templates/rules/root.ai-rules.yml @@ -9,7 +9,6 @@ rules: readme: outputs: normalize: - enabled: false # Disabled by default - enable in your project when you want AI to maintain README path: "{repo}/README.md" output_type: "readme_normalizer" instruction: | diff --git a/automation/patcher.py b/automation/patcher.py index 5c0ef62..85dddb2 100644 --- a/automation/patcher.py +++ b/automation/patcher.py @@ -121,8 +121,13 @@ def generate_output( save_debug_artifacts(repo_root, output_rel, raw_path, clean_path, sanitized_path, final_patch_path) # Check if the final patch is empty after sanitization. + # This is normal when AI determines no changes are needed (e.g., gated outputs) if not final_patch_path.read_text(encoding="utf-8").strip(): - raise PatchGenerationError("AI returned empty patch") + # Empty patch is not an error - AI decided not to make changes + # Common cases: + # - implementation_gate_writer when status != READY_FOR_IMPLEMENTATION + # - AI determines output is already correct + return # Skip silently # Apply the generated patch to the Git repository. apply_patch(repo_root, final_patch_path, patch_level, output_rel) @@ -414,10 +419,8 @@ def sanitize_unified_patch(patch: str) -> str: patch: The raw unified diff string. Returns: - The sanitized diff string. - - Raises: - PatchGenerationError: If the sanitized patch is missing a 'diff --git' header. + The sanitized diff string, or empty string if no diff header found. + An empty result indicates the AI chose not to make changes (e.g., gated output). """ lines = patch.replace("\r", "").splitlines() cleaned = [] @@ -427,11 +430,13 @@ def sanitize_unified_patch(patch: str) -> str: continue cleaned.append(line) text = "\n".join(cleaned) - - # Ensure the patch still contains a 'diff --git' header after sanitization. + + # Look for the 'diff --git' header diff_start = text.find("diff --git") if diff_start == -1: - raise PatchGenerationError("Sanitized patch missing diff header") + # No diff header means AI chose not to generate a patch + # This is normal for gated outputs or when AI determines no changes needed + return "" return text[diff_start:] + "\n"