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"
"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:

View File

@ -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: |

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)
# 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 = []
@ -428,10 +431,12 @@ def sanitize_unified_patch(patch: str) -> str:
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"