fix: Handle empty AI diffs gracefully instead of raising errors

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 <noreply@anthropic.com>
This commit is contained in:
rob 2025-11-01 02:33:31 -03:00
parent 19d6119542
commit 188f6b3b16
3 changed files with 19 additions and 12 deletions

View File

@ -6,19 +6,22 @@ file_associations:
"feature.discussion.sum.md": "discussion_summary" "feature.discussion.sum.md": "discussion_summary"
"implementation.discussion.md": "implementation_discussion_update" "implementation.discussion.md": "implementation_discussion_update"
"implementation.discussion.sum.md": "discussion_summary" "implementation.discussion.sum.md": "discussion_summary"
"README.md": "readme_skip" # Override root rule - don't update README from features/
rules: rules:
readme_skip:
# Disable README updates when editing files in Docs/features/
# This prevents unnecessary AI calls during feature discussions
outputs: {}
feature_request: feature_request:
outputs: outputs:
feature_discussion: feature_discussion:
path: "Docs/features/{feature_id}/discussions/feature.discussion.md" path: "Docs/features/{feature_id}/discussions/feature.discussion.md"
output_type: "feature_discussion_writer" output_type: "feature_discussion_writer"
implementation_gate: implementation_gate:
enabled: false # Disabled by default - enable when feature is ready for implementation
path: "Docs/features/{feature_id}/discussions/implementation.discussion.md" path: "Docs/features/{feature_id}/discussions/implementation.discussion.md"
output_type: "implementation_gate_writer" 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: feature_discussion_update:
outputs: outputs:

View File

@ -9,7 +9,6 @@ rules:
readme: readme:
outputs: outputs:
normalize: normalize:
enabled: false # Disabled by default - enable in your project when you want AI to maintain README
path: "{repo}/README.md" path: "{repo}/README.md"
output_type: "readme_normalizer" output_type: "readme_normalizer"
instruction: | instruction: |

View File

@ -121,8 +121,13 @@ def generate_output(
save_debug_artifacts(repo_root, output_rel, raw_path, clean_path, sanitized_path, final_patch_path) 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. # 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(): 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 the generated patch to the Git repository.
apply_patch(repo_root, final_patch_path, patch_level, output_rel) 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. patch: The raw unified diff string.
Returns: Returns:
The sanitized diff string. 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).
Raises:
PatchGenerationError: If the sanitized patch is missing a 'diff --git' header.
""" """
lines = patch.replace("\r", "").splitlines() lines = patch.replace("\r", "").splitlines()
cleaned = [] cleaned = []
@ -427,11 +430,13 @@ def sanitize_unified_patch(patch: str) -> str:
continue continue
cleaned.append(line) cleaned.append(line)
text = "\n".join(cleaned) 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") diff_start = text.find("diff --git")
if diff_start == -1: 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" return text[diff_start:] + "\n"