diff --git a/.github/skills/ai-summary-comment/IMPROVEMENTS.md b/.github/skills/ai-summary-comment/IMPROVEMENTS.md new file mode 100644 index 000000000000..86094bdd752c --- /dev/null +++ b/.github/skills/ai-summary-comment/IMPROVEMENTS.md @@ -0,0 +1,653 @@ +# PR Comment Script Improvements + +## Summary of Changes + +The `post-ai-summary-comment.ps1` script has been significantly improved to make posting PR review comments easier and more flexible using **dynamic section extraction**. + +## Key Improvements + +### 1. **Dynamic Section Extraction** (NEW!) + +**Before:** Script used hardcoded pattern matching with predefined title variations + +**After:** Script **automatically discovers ALL sections** from your state file and extracts them dynamically + +```powershell +# Extracts ALL
TITLE sections +$allSections = Extract-AllSections -StateContent $Content + +# Then maps them to phases using flexible regex patterns +$preFlightContent = Get-SectionByPattern -Sections $allSections -Patterns @( + '๐Ÿ“‹.*Issue Summary', + '๐Ÿ“‹.*Pre-Flight' +) +``` + +**Benefits:** +- โœ… **No hardcoded titles** - works with ANY section header you use +- โœ… **Automatically adapts** - add new sections without modifying the script +- โœ… **Better debugging** - shows exactly which sections were found +- โœ… **More maintainable** - less code, more flexible + +**Example debug output:** +``` +[DEBUG] Found 7 section(s) in state file +[DEBUG] Section: '๐Ÿ“‹ Issue Summary' (803 chars) +[DEBUG] Section: '๐Ÿงช Tests' (539 chars) +[DEBUG] Section: '๐Ÿšฆ Gate - Test Verification' (488 chars) +[DEBUG] Section: '๐Ÿ”ง Fix Candidates' (868 chars) +[DEBUG] Section: '๐Ÿ“‹ Final Report' (2351 chars) +[DEBUG] Matched '๐Ÿ“‹ Final Report' with pattern '๐Ÿ“‹.*Report' +``` + +--- + +### 2. **Flexible Pattern Matching** + +**Before:** Exact string matching required + +**After:** Uses **regex patterns** to match section titles flexibly + +```powershell +# Matches any of these (and more!): +- "๐Ÿ“‹ Final Report" โœ… +- "๐Ÿ“‹ Phase 5: Final Report" โœ… +- "๐Ÿ“‹ Report - Final Recommendation" โœ… +- Any title containing "๐Ÿ“‹" and "Report" โœ… +``` + +**Pattern examples:** +- `'๐Ÿ“‹.*Issue Summary'` matches "๐Ÿ“‹ Issue Summary", "๐Ÿ“‹ Pre-Flight Issue Summary", etc. +- `'๐Ÿงช.*Tests'` matches "๐Ÿงช Tests", "๐Ÿงช Phase 2: Tests", etc. +- `'๐Ÿ“‹.*Report'` matches any title with ๐Ÿ“‹ and Report in it + +--- + +### 3. **Errors vs Warnings** + +**Validation levels:** +- **Errors** (โŒ) - Block posting (missing content, PENDING markers) +- **Warnings** (โš ๏ธ) - Suggestions only (missing optional sections) + +**Example:** +``` +โœ… All validation checks passed! + +โš ๏ธ VALIDATION WARNINGS +Found 2 warning(s) (non-critical): + - Fix: Fix phase missing 'Exhausted' field (non-critical) + +๐Ÿ’ก These are suggestions but won't block posting. +``` + +--- + +### 4. **Debug Mode** + +Enable detailed extraction information: + +```powershell +$DebugPreference = 'Continue' +./post-ai-summary-comment.ps1 -PRNumber 12345 +``` + +**Shows:** +- Which sections were found in the state file +- How many characters each section contains +- Which patterns matched which sections +- Why validation passed or failed + +--- + +### 5. **Better Error Messages** + +**Comprehensive guidance when validation fails:** +``` +โ›” VALIDATION FAILED + +๐Ÿ’ก Fix these issues in the state file before posting. + Or use -SkipValidation to bypass these checks. + +๐Ÿ› Debug tip: Run with $DebugPreference = 'Continue' for details +``` + +--- + +## How Dynamic Extraction Works + +### Step 1: Extract ALL Sections + +```powershell +function Extract-AllSections { + # Pattern matches:
TITLE...content...
+ $pattern = '(?s)
\s*([^<]+)(.*?)
' + $matches = [regex]::Matches($StateContent, $pattern) + + # Returns hashtable: @{ "Title" = "content", ... } +} +``` + +**Result:** Hashtable with ALL sections from your state file + +### Step 2: Map to Phases + +```powershell +function Get-SectionByPattern { + # Try each pattern until one matches + foreach ($pattern in $Patterns) { + foreach ($key in $Sections.Keys) { + if ($key -match $pattern) { + return $Sections[$key] # Found it! + } + } + } +} +``` + +**Result:** Phase content matched by flexible regex patterns + +--- + +## Usage Examples + +### Basic Usage (unchanged) +```powershell +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27340 +``` + +### With Debug Mode (recommended when troubleshooting) +```powershell +pwsh -Command '$DebugPreference = "Continue"; ./.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27340' +``` + +### Skip Validation +```powershell +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27340 -SkipValidation +``` + +### Dry Run +```powershell +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27340 -DryRun +``` + +--- + +## What Section Headers Work Now? + +**The script uses regex patterns, so it's VERY flexible:** + +### Pre-Flight Phase +Any title matching `'๐Ÿ“‹.*Issue Summary'` or `'๐Ÿ“‹.*Pre-Flight'`: +- โœ… "๐Ÿ“‹ Issue Summary" (preferred) +- โœ… "๐Ÿ“‹ Pre-Flight Analysis" +- โœ… "๐Ÿ“‹ Context and Issue Summary" + +### Tests Phase +Any title matching `'๐Ÿงช.*Tests'`: +- โœ… "๐Ÿงช Tests" +- โœ… "๐Ÿงช Phase 2: Tests" +- โœ… "๐Ÿงช Test Verification" + +### Gate Phase +Any title matching `'๐Ÿšฆ.*Gate'`: +- โœ… "๐Ÿšฆ Gate - Test Verification" +- โœ… "๐Ÿšฆ Gate" +- โœ… "๐Ÿšฆ Phase 3: Gate" + +### Fix Phase +Any title matching `'๐Ÿ”ง.*Fix'`: +- โœ… "๐Ÿ”ง Fix Candidates" +- โœ… "๐Ÿ”ง Fix Analysis" +- โœ… "๐Ÿ”ง Fix" + +### Report Phase +Any title matching `'๐Ÿ“‹.*Report'` or `'Final Report'`: +- โœ… "๐Ÿ“‹ Final Report" +- โœ… "๐Ÿ“‹ Phase 5: Report" +- โœ… "๐Ÿ“‹ Report - Final Recommendation" +- โœ… "Final Report" + +**The beauty:** You don't need to remember exact titles anymore! + +--- + +## Migration Guide + +**No changes needed!** The script is backward compatible. + +**Old state files** with exact headers like: +```markdown +๐Ÿ“‹ Phase 5: Report โ€” Final Recommendation +``` + +**New state files** with simpler headers like: +```markdown +๐Ÿ“‹ Final Report +``` + +**Both work!** The dynamic extraction finds them automatically. + +--- + +## Advantages Over Old Approach + +| Aspect | Old (Pattern Matching) | New (Dynamic Extraction) | +|--------|------------------------|--------------------------| +| **Flexibility** | โŒ Hardcoded titles | โœ… Any title works | +| **Maintenance** | โŒ Update code for new headers | โœ… No code changes needed | +| **Debugging** | โš ๏ธ Limited visibility | โœ… Full extraction visibility | +| **Speed** | โš ๏ธ Tries multiple patterns | โœ… Single pass extraction | +| **Reliability** | โš ๏ธ Can miss variations | โœ… Finds everything | + +--- + +## Common Issues & Solutions + +### Issue: "Phase X has NO content in state file" + +**Step 1:** Enable debug mode to see what was found +```powershell +pwsh -Command '$DebugPreference = "Continue"; ./post-ai-summary-comment.ps1 -PRNumber XXXXX' +``` + +**Look for:** +``` +[DEBUG] Found 7 section(s) in state file +[DEBUG] Section: 'Your Section Title' (XXX chars) +``` + +**Step 2:** Check if your section title matches the patterns + +Report phase patterns: `'๐Ÿ“‹.*Report'`, `'Final Report'` + +If your title is `"๐Ÿ“‹ Final Analysis"`, it won't match! + +**Solution:** Either: +- Rename section to include "Report": `"๐Ÿ“‹ Final Report"` โœ… +- Or use `-SkipValidation` if content is there + +--- + +### Issue: Section extracted but content is empty + +**Cause:** State file structure issue (missing content between tags) + +**Check your markdown:** +```markdown +
+๐Ÿ“‹ Final Report + + +Your report content... + +
+``` + +**Not this:** +```markdown +
+๐Ÿ“‹ Final Report +
โŒ No content! +``` + +--- + +## Developer Notes + +### How to Add Support for New Phase Patterns + +Just add a regex pattern to the mapping: + +```powershell +$reportContent = Get-SectionByPattern -Sections $allSections -Patterns @( + '๐Ÿ“‹.*Report', + 'Final Report', + 'Your New Pattern Here' # Add here +) -Debug:$debugMode +``` + +**Example:** Support "Summary" as alias for "Report": +```powershell +$reportContent = Get-SectionByPattern -Sections $allSections -Patterns @( + '๐Ÿ“‹.*Report', + '๐Ÿ“‹.*Summary', # New pattern + 'Final Report' +) +``` + +--- + +### Regex Pattern Tips + +- `.*` matches any characters +- `^` matches start of string +- `$` matches end of string +- Use `[regex]::Escape()` if you need literal special chars + +**Examples:** +- `'๐Ÿงช.*Tests'` - Title must contain both ๐Ÿงช and Tests +- `'^๐Ÿ“‹ Report'` - Title must START with "๐Ÿ“‹ Report" +- `'Report$'` - Title must END with "Report" + +--- + +## Testing + +Tested with: +- โœ… PR #27340 (7 sections extracted successfully) +- โœ… Debug mode showing section discovery +- โœ… Various header formats +- โœ… Dry run mode +- โœ… Skip validation mode +- โœ… Empty sections (proper error handling) + +**Debug output example:** +``` +[DEBUG] Found 7 section(s) in state file +[DEBUG] Section: '๐Ÿ“‹ Issue Summary' (803 chars) +[DEBUG] Section: '๐Ÿ“ Files Changed' (0 chars) +[DEBUG] Section: '๐Ÿ’ฌ PR Discussion Summary' (0 chars) +[DEBUG] Section: '๐Ÿงช Tests' (539 chars) +[DEBUG] Section: '๐Ÿšฆ Gate - Test Verification' (488 chars) +[DEBUG] Section: '๐Ÿ”ง Fix Candidates' (868 chars) +[DEBUG] Section: '๐Ÿ“‹ Final Report' (2351 chars) +[DEBUG] Matched '๐Ÿ“‹ Issue Summary' with pattern '๐Ÿ“‹.*Issue Summary' +[DEBUG] Matched '๐Ÿงช Tests' with pattern '๐Ÿงช.*Tests' +[DEBUG] Matched '๐Ÿšฆ Gate - Test Verification' with pattern '๐Ÿšฆ.*Gate' +[DEBUG] Matched '๐Ÿ”ง Fix Candidates' with pattern '๐Ÿ”ง.*Fix' +[DEBUG] Matched '๐Ÿ“‹ Final Report' with pattern '๐Ÿ“‹.*Report' +``` + +--- + +## Future Improvements + +Potential enhancements: +- [ ] Auto-generate comment structure from discovered sections +- [ ] Support markdown headings (`##`/`###`) as alternative to `
` +- [ ] Validate section content structure (required fields) +- [ ] Suggest section renaming for better patterns +- [ ] Export sections as separate files for versioning + +--- + +## Feedback + +The dynamic extraction makes the script much more maintainable and flexible! + +If you find sections that aren't being extracted: +1. Run with `$DebugPreference = 'Continue'` to see what was found +2. Check which patterns are being used +3. Add a new pattern if needed (or rename your section) + +--- + +### 2. **Errors vs Warnings** + +**Before:** Everything was treated as a blocking error + +**After:** Two levels of feedback: +- **Errors** (โŒ) - Block posting (e.g., missing content, PENDING markers) +- **Warnings** (โš ๏ธ) - Suggestions only (e.g., missing optional sections) + +**Example output:** +``` +โœ… All validation checks passed! + +โš ๏ธ VALIDATION WARNINGS +Found 2 warning(s) (non-critical): + - Report: Report phase missing root cause analysis (non-critical) + - Fix: Fix phase missing 'Exhausted' field (non-critical) + +๐Ÿ’ก These are suggestions for improvement but won't block posting. +``` + +--- + +### 3. **Debug Mode** + +**New feature:** Set `$DebugPreference = 'Continue'` to see detailed extraction information + +```powershell +$DebugPreference = 'Continue' +./post-ai-summary-comment.ps1 -PRNumber 12345 +``` + +**Debug output shows:** +``` +[DEBUG] Matched pattern for: ๐Ÿ“‹ Final Report +[DEBUG] Content length: 2355 chars +[DEBUG] First 100 chars: --- + +### Summary + +PR #27340 provides a **correct and well-tested fix**... +``` + +**Benefit:** Easy troubleshooting when validation fails + +--- + +### 4. **Better Error Messages** + +**Before:** +``` +โ›” VALIDATION FAILED +Found 1 validation error(s): + - Report: Phase Report is marked as 'โœ… COMPLETE' but has NO content in state file +``` + +**After:** +``` +โ›” VALIDATION FAILED +Found 1 validation error(s): + - Report: Phase Report is marked as 'โœ… COMPLETE' but has NO content in state file + +๐Ÿ’ก Fix these issues in the state file before posting the review comment. + Or use -SkipValidation to bypass these checks (not recommended). + +๐Ÿ› Debug tip: Run with $DebugPreference = 'Continue' for detailed extraction info +``` + +--- + +### 5. **Relaxed Phase 5 Validation** + +**Before:** Report phase required: +- Exact "Final Recommendation" text +- "Root Cause" section +- "Key Findings" section +- "Solution Analysis" section +- Minimum 500 characters + +**After:** Report phase only requires: +- Any form of recommendation (APPROVE, REQUEST CHANGES, etc.) +- Any form of analysis (Summary, Fix Quality, etc.) +- Minimum 200 characters (reduced from 500) + +**Benefit:** More flexibility in how you structure the final report + +--- + +## Usage Examples + +### Basic Usage (unchanged) +```powershell +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27340 +``` + +### With Debug Mode +```powershell +$DebugPreference = 'Continue' +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27340 +``` + +### Skip Validation (when needed) +```powershell +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27340 -SkipValidation +``` + +### Dry Run (preview only) +```powershell +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27340 -DryRun +``` + +--- + +## What You Need to Know + +### Headers That Work Now + +Any of these variations will be recognized: + +**Pre-Flight:** +- `๐Ÿ“‹ Issue Summary` โœ… (preferred) +- `๐Ÿ“‹ Pre-Flight` โœ… +- `๐Ÿ” Pre-Flight` โœ… + +**Tests:** +- `๐Ÿงช Tests` โœ… (preferred) +- `๐Ÿ“‹ Tests` โœ… + +**Gate:** +- `๐Ÿšฆ Gate - Test Verification` โœ… (preferred) +- `๐Ÿšฆ Gate` โœ… +- `๐Ÿ“‹ Gate` โœ… + +**Fix:** +- `๐Ÿ”ง Fix Candidates` โœ… (preferred) +- `๐Ÿ”ง Fix` โœ… +- `๐Ÿ“‹ Fix` โœ… + +**Report:** +- `๐Ÿ“‹ Final Report` โœ… +- `๐Ÿ“‹ Phase 5: Final Report` โœ… +- `๐Ÿ“‹ Report` โœ… +- `Phase 5: Report` โœ… +- `Final Report` โœ… + +--- + +## Migration Guide + +**No changes needed!** The script is backward compatible. If you have existing state files with the old header format, they'll continue to work. + +If you want to use the new flexibility: +- Just use simpler headers like `๐Ÿ“‹ Final Report` instead of `๐Ÿ“‹ Phase 5: Report โ€” Final Recommendation` +- The script will find it either way + +--- + +## Common Issues & Solutions + +### Issue: "Phase Report has NO content in state file" + +**Solution 1:** Check your state file structure +```bash +grep -A 5 "๐Ÿ“‹.*Report" CustomAgentLogsTmp/PRState/pr-XXXXX.md +``` + +Make sure you have: +```markdown +
+๐Ÿ“‹ Final Report + +Your report content here... + +
+``` + +**Solution 2:** Use debug mode to see what's happening +```powershell +$DebugPreference = 'Continue' +./post-ai-summary-comment.ps1 -PRNumber XXXXX +``` + +**Solution 3:** Use `-SkipValidation` if content is definitely there +```powershell +./post-ai-summary-comment.ps1 -PRNumber XXXXX -SkipValidation +``` + +--- + +### Issue: Validation warnings about missing sections + +**These are just suggestions!** Warnings won't block posting. You can: +- Ignore them (the comment will post anyway) +- Address them if you want a more complete review +- Use `-SkipValidation` to hide all validation output + +--- + +## Developer Notes + +### How Pattern Matching Works + +```powershell +function Extract-PhaseContent { + param( + [string]$StateContent, + [string[]]$PhaseTitles, # Array of possible titles + [switch]$Debug + ) + + foreach ($title in $PhaseTitles) { + $pattern = "(?s)
\s*$([regex]::Escape($title))(.*?)
" + if ($StateContent -match $pattern) { + return $Matches[1].Trim() + } + } + return $null # No match found +} +``` + +The function tries each pattern in order until one matches. + +### Adding New Pattern Variations + +To support a new header variation, just add it to the array: + +```powershell +$reportContent = Extract-PhaseContent -StateContent $Content -PhaseTitles @( + "๐Ÿ“‹ Phase 5: Report โ€” Final Recommendation", + "๐Ÿ“‹ Phase 5: Final Report", + "๐Ÿ“‹ Phase 5: Report", + "๐Ÿ“‹ Final Report", + "๐Ÿ“‹ Report", + "Phase 5: Report", + "Final Report", + "Your New Pattern Here" # <-- Add here +) -Debug:$debugMode +``` + +--- + +## Future Improvements + +Potential enhancements: +- [ ] Auto-detect phase titles from state file (no hardcoded patterns) +- [ ] Support markdown headings (`##` / `###`) in addition to `
` +- [ ] Validate links and references work +- [ ] Check that commit SHAs are valid +- [ ] Suggest fixes for common issues (auto-fix mode) + +--- + +## Testing + +The improvements have been tested with: +- โœ… PR #27340 (Entry/Editor keyboard issue) +- โœ… State files with various header formats +- โœ… Dry run mode +- โœ… Debug mode +- โœ… Skip validation mode +- โœ… Multiple phase title variations + +--- + +## Feedback + +If you encounter issues or have suggestions, please: +1. Try debug mode first: `$DebugPreference = 'Continue'` +2. Check the state file structure +3. Report the issue with debug output included diff --git a/.github/skills/ai-summary-comment/NO-EXTERNAL-REFERENCES-RULE.md b/.github/skills/ai-summary-comment/NO-EXTERNAL-REFERENCES-RULE.md new file mode 100644 index 000000000000..a4b8a4dd5c03 --- /dev/null +++ b/.github/skills/ai-summary-comment/NO-EXTERNAL-REFERENCES-RULE.md @@ -0,0 +1,192 @@ +# Critical Rule: No External File References in PR Comments + +## The Problem + +When the PR agent posts review comments to GitHub, those comments are viewed by: +- PR authors +- Other reviewers +- Future contributors searching issue history +- Community members watching the PR + +**None of these people have access to your local files!** + +## The Rule + +### โŒ NEVER Do This + +```markdown +### Title & Description: โš ๏ธ Minor Updates Needed + +**Issues to fix:** +1. Missing required NOTE block +2. Technical inaccuracy in description + +**See:** `CustomAgentLogsTmp/PRState/27340/pr-finalize/pr-finalize-summary.md` for recommended updates +``` + +**Why this is bad:** +- GitHub users can't access `CustomAgentLogsTmp/` +- Makes the review useless - "see file I can't access" +- Author can't act on recommendations +- Future agents can't learn from the review + +--- + +### โœ… ALWAYS Do This + +```markdown +### Title & Description: โš ๏ธ Minor Updates Needed + +**Current description is HIGH QUALITY:** +- โœ… Clear root cause for both platforms +- โœ… Before/after videos +- โœ… Well-structured sections + +**Issues to fix:** + +**Issue 1: Missing required NOTE block** + +Add this at the top of the description: +```markdown +> [!NOTE] +> Are you waiting for the changes in this PR to be merged? +> It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! +``` + +**Issue 2: Technical inaccuracy in "Description of Change"** + +Current text says: +> "Added platform-specific handling to dismiss the soft keyboard **and remove focus**..." + +Should say: +> "Added platform-specific handling to dismiss the soft keyboard when the Entry or Editor visibility is set to False." + +**Reason:** The code only calls `HideSoftInputAsync()` to dismiss the keyboard. It does NOT call `Unfocus()` to remove focus. Focus state remains unchanged. + +**Recommended additions:** + +Add an **Implementation** subsection: +```markdown +**Implementation:** +- Added `MapIsVisible` handler in `InputView.Platform.cs` (iOS/Android only) +- Calls `HideSoftInputAsync()` when control becomes invisible and keyboard is showing +- Registered handler in `Entry.Mapper.cs` and `Editor.Mapper.cs` +``` +``` + +**Why this is good:** +- โœ… Self-contained - everything needed is in the comment +- โœ… Actionable - author can copy/paste the fixes +- โœ… Clear - shows exact before/after text +- โœ… Educational - explains the reasoning +- โœ… Accessible - works on GitHub + +--- + +## Where This Applies + +### pr-finalize Skill + +When running `pr-finalize` skill, you create TWO outputs: + +1. **Summary file** (local reference) + - Location: `CustomAgentLogsTmp/PRState/XXXXX/pr-finalize/pr-finalize-summary.md` + - Purpose: Your detailed analysis and working notes + - Audience: You and local CLI users + +2. **State file Report section** (GitHub audience) + - Location: `CustomAgentLogsTmp/PRState/pr-XXXXX.md` (Report phase) + - Purpose: Final recommendations that get posted to GitHub + - Audience: PR authors, reviewers, community + - **MUST be self-contained** - no external references + +### PR Agent Phase 5 (Report) + +When completing Phase 5: +- Include ALL pr-finalize findings inline +- Show exact code blocks for NOTE block +- Show exact before/after text for corrections +- Explain reasoning for each recommendation +- Never reference local files + +--- + +## Examples from Real Usage + +### โŒ Bad Example (PR #27340 - first attempt) + +```markdown +**Issues to fix:** +1. **Missing required NOTE block** (mandatory for all PRs) +2. **Minor technical inaccuracy:** Description says "remove focus" but code only dismisses keyboard + +**See:** `CustomAgentLogsTmp/PRState/27340/pr-finalize/pr-finalize-summary.md` for recommended updates +``` + +**Result:** PR author sees the issues but has no idea how to fix them without accessing local files. + +--- + +### โœ… Good Example (PR #27340 - corrected) + +```markdown +**Issues to fix:** + +**Issue 1: Missing required NOTE block** + +Add this at the top of the description: +```markdown +> [!NOTE] +> Are you waiting for the changes in this PR to be merged? +> It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! +``` + +**Issue 2: Technical inaccuracy in "Description of Change"** + +Current text says: +> "Added platform-specific handling to dismiss the soft keyboard **and remove focus**..." + +Should say: +> "Added platform-specific handling to dismiss the soft keyboard when the Entry or Editor visibility is set to False." + +**Reason:** The code only calls `HideSoftInputAsync()` to dismiss the keyboard. It does NOT call `Unfocus()` to remove focus. + +**Recommended additions to description:** + +Add an **Implementation** subsection after "Description of Change": +```markdown +**Implementation:** +- Added `MapIsVisible` handler in `InputView.Platform.cs` (iOS/Android only) +- Calls `HideSoftInputAsync()` when control becomes invisible and keyboard is showing +- Registered handler in `Entry.Mapper.cs` and `Editor.Mapper.cs` +``` +``` + +**Result:** PR author can immediately act on every recommendation with clear guidance. + +--- + +## Checklist for Report Phase + +When completing Phase 5, verify: + +- [ ] All recommendations are inline (no file references) +- [ ] Code blocks show exact text to add +- [ ] Before/after comparisons for corrections +- [ ] Reasoning explained for each issue +- [ ] Examples are copy/paste ready +- [ ] No references to `CustomAgentLogsTmp/` + +--- + +## Quick Reference + +| What | Where | Audience | Self-Contained? | +|------|-------|----------|-----------------| +| Summary file | `CustomAgentLogsTmp/.../summary.md` | Local CLI | N/A (local only) | +| State file | `CustomAgentLogsTmp/PRState/pr-XXXXX.md` | GitHub users | โœ… YES - REQUIRED | +| PR comment | GitHub PR page | Public | โœ… YES - REQUIRED | + +**Remember:** Anything that goes in the state file's `
` sections will be posted to GitHub. Make it self-contained! + +-- diff --git a/.github/skills/ai-summary-comment/SKILL.md b/.github/skills/ai-summary-comment/SKILL.md new file mode 100644 index 000000000000..185e7af83af5 --- /dev/null +++ b/.github/skills/ai-summary-comment/SKILL.md @@ -0,0 +1,456 @@ +--- +name: ai-summary-comment +description: Posts or updates automated progress comments on GitHub PRs. Use after completing any PR agent phase (pre-flight, tests, gate, fix, report). Triggers on 'post comment to PR', 'update PR progress', 'comment on PR with results', 'post pre-flight comment'. Creates single aggregated review comment with collapsible sections per commit. +metadata: + author: dotnet-maui + version: "5.0" +compatibility: Requires GitHub CLI (gh) authenticated with access to dotnet/maui repository. +--- + +# PR Comment Skill + +This skill posts automated progress comments to GitHub Pull Requests during the PR review workflow. Comments are **self-contained** with collapsible Review Session details, providing rich context to maintainers and contributors. + +**โš ๏ธ Self-Contained Rule**: All content in PR comments must be self-contained. Never reference local files like `CustomAgentLogsTmp/` - GitHub users cannot access your local filesystem. + +**โœจ Key Features**: +- **Single Unified Comment**: ONE comment per PR/Issue containing ALL sections (PR Review, Try-Fix, Write-Tests, Verify-Tests) +- **Section-Based Updates**: Each script updates only its section, preserving others +- **Duplicate Prevention**: Finds existing `` comment and updates it +- **File-Based DryRun Preview**: Use `-DryRun` to preview changes in a local file before posting +- **Auto-Loading State Files**: Automatically finds and loads state files from `CustomAgentLogsTmp/PRState/` +- **Simple Interface**: Just provide PR number - script handles everything else + +## Comment Architecture + +### Unified AI Summary Comment + +Most scripts post to the **same single comment** identified by ``. Each script updates its own section: + +```markdown + + +## ๐Ÿค– AI Summary + + +... PR review phases ... + + + +... try-fix attempts ... + + + +... write-tests attempts ... + + + +... test verification results ... + +``` + +**Behavior:** +- First script to run creates the comment +- Subsequent scripts find the existing comment and update/add their section +- Sections are independent - updating one preserves others + +### Separate PR Finalization Comment + +The `post-pr-finalize-comment.ps1` script posts a **separate comment** identified by ``. This is intentional because: +- Finalization reviews can happen multiple times (after each commit) +- Each review is numbered (Review 1, Review 2, etc.) +- Keeps finalization reviews distinct from automated analysis + +## Section Scripts + +### AI Summary Sections (Unified Comment) + +| Section | Script | Location | +|---------|--------|----------| +| `PR-REVIEW` | `post-ai-summary-comment.ps1` | `.github/skills/ai-summary-comment/scripts/` | +| `TRY-FIX` | `post-try-fix-comment.ps1` | `.github/skills/ai-summary-comment/scripts/` | +| `WRITE-TESTS` | `post-write-tests-comment.ps1` | `.github/skills/ai-summary-comment/scripts/` | +| `VERIFY-TESTS` | `post-verify-tests-comment.ps1` | `.github/skills/ai-summary-comment/scripts/` | + +### Separate Comments + +| Comment | Script | Marker | +|---------|--------|--------| +| PR Finalization | `post-pr-finalize-comment.ps1` | `` | + +## Supported Phases + +| Phase | Description | When to Post | What This Enables Next | +|-------|-------------|--------------|------------------------| +| `pre-flight` | Context gathering complete | After documenting issue, files, and discussion | **Tests Phase**: Agent can now verify/create test files that reproduce the bug | +| `tests` | Test analysis complete | After identifying test files and coverage | **Gate Phase**: Agent can run tests to verify they catch the bug | +| `gate` | Test validation complete | After running tests and verifying bug reproduction | **Fix Phase**: Agent can explore alternative fixes (tests proven to catch bug) | +| `fix` | Solution comparison complete | After comparing PR fix with alternatives | **Report Phase**: Agent can finalize recommendation based on fix comparison | +| `report` | Final analysis complete | After generating comprehensive review | **PR Decision**: Maintainers can approve/merge or request changes based on full analysis | + +## Usage + +### Simplest: Just provide PR number + +```bash +# Auto-loads CustomAgentLogsTmp/PRState/pr-27246.md +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 27246 +``` + +### With explicit state file path + +```bash +# PRNumber auto-extracted from filename (pr-27246.md โ†’ 27246) +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -StateFile CustomAgentLogsTmp/PRState/pr-27246.md +``` + +### Legacy: Provide content directly + +```bash +pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber 12345 -Content "$(cat CustomAgentLogsTmp/PRState/pr-12345.md)" +``` + +### Parameters + +| Parameter | Required | Description | Example | +|-----------|----------|-------------|---------| +| `PRNumber` | No* | Pull request number | `12345` | +| `StateFile` | No* | Path to state file (PRNumber auto-extracted from `pr-XXXXX.md` naming) | `CustomAgentLogsTmp/PRState/pr-27246.md` | +| `Content` | No* | Full state file content (legacy, can be piped via stdin) | Content from state file | +| `DryRun` | No | Preview changes in local file instead of posting to GitHub | `-DryRun` | +| `PreviewFile` | No | Path to local preview file for DryRun mode (default: `CustomAgentLogsTmp/PRState/{PRNumber}/ai-summary-comment-preview.md`) | `-PreviewFile ./preview.md` | +| `SkipValidation` | No | Skip validation checks (not recommended) | `-SkipValidation` | + +*At least one of PRNumber, StateFile, or Content is required. The script will: +- If `-PRNumber` provided: Auto-load `CustomAgentLogsTmp/PRState/pr-{PRNumber}.md` +- If `-StateFile` provided: Load the file and extract PRNumber from `pr-XXXXX.md` filename +- If `-Content` provided: Use content directly (legacy, requires `-PRNumber`) + +## DryRun Preview Workflow + +Use `-DryRun` to preview the combined comment before posting to GitHub. Each script updates the same preview file, mirroring how the actual GitHub comment is updated. + +```bash +# Step 1: Run verify-tests script (creates preview file) +pwsh .github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 -PRNumber 32891 -DryRun + +# Step 2: Run try-fix script (updates same preview file) +pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 -IssueNumber 32891 -DryRun + +# Step 3: Review the combined preview +open CustomAgentLogsTmp/PRState/32891/ai-summary-comment-preview.md + +# Step 4: Post for real (remove -DryRun) +pwsh .github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 -PRNumber 32891 +pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 -IssueNumber 32891 +``` + +**Key behavior:** The preview file exactly matches what will be posted to GitHub. Multiple scripts accumulate their sections in the same file. + +### Section Ordering + +Sections appear in the unified comment in this order (based on which scripts run first): +1. **VERIFY-TESTS** - Test verification results +2. **TRY-FIX** - Alternative fix exploration attempts +3. **WRITE-TESTS** - Test writing attempts +4. **PR-REVIEW** - PR review phases + +Each section is wrapped with markers like `` and ``. + +### Cleanup + +To reset the preview file for a fresh start: +```bash +rm CustomAgentLogsTmp/PRState/{PRNumber}/ai-summary-comment-preview.md +``` + +### Prerequisites + +Scripts require GitHub CLI authentication: +```bash +gh auth status # Verify authentication before running +``` + +## Comment Format + +Comments are formatted with: +- **Phase badge** (๐Ÿ” Pre-Flight, ๐Ÿงช Tests, ๐Ÿšฆ Gate, ๐Ÿ”ง Fix, ๐Ÿ“‹ Report) +- **Status indicator** (โœ… Completed, โš ๏ธ Issues Found) +- **Expandable review sessions** (each session is a collapsible section) +- **What's Next** (what phase happens next) + +### Review Session Tracking + +When the same PR is reviewed multiple times (e.g., after new commits), the script **updates the single aggregated review comment** and adds a new expandable section for each commit-based review session. + +### Example Output + +```markdown +## ๐Ÿ” Pre-Flight: Context Gathering Complete + +โœ… **Status**: Phase completed successfully + +### Summary +- **Issue**: #33356 - CollectionView crash on iOS +- **Platforms Affected**: iOS, MacCatalyst +- **Files Changed**: 2 implementation files, 1 test file +- **Discussion**: 3 key reviewer comments identified + +### Key Findings +- Crash occurs when scrolling rapidly with large datasets +- Existing PR adds null check in ItemsViewController +- Test coverage includes iOS device test + +### Next Steps +โ†’ **Phase 2: Tests** - Analyzing test files and coverage + +--- +*Posted by PR Agent @ 2026-01-17 14:23:45 UTC* +``` + +## Script Files + +- [`post-ai-summary-comment.ps1`](scripts/post-ai-summary-comment.ps1) - Posts or updates the aggregated PR agent review comment +- [`post-try-fix-comment.ps1`](scripts/post-try-fix-comment.ps1) - Posts or updates try-fix attempts comment + +## Try-Fix Comment Script + +The `post-try-fix-comment.ps1` script updates the `` section of the unified AI Summary comment. It aggregates all try-fix attempts into collapsible sections. Works for both issues and PRs (GitHub treats PR comments as issue comments). + +**โœจ Auto-Loading from `CustomAgentLogsTmp`**: The script automatically discovers and aggregates ALL attempt directories from `CustomAgentLogsTmp/PRState/{IssueNumber}/try-fix/`. + +### Usage + +#### Simplest: Provide attempt directory + +```powershell +# All parameters auto-loaded from directory structure +pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 ` + -TryFixDir CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1 +``` + +#### Or just provide issue number + +```powershell +# Auto-discovers and posts latest attempt from CustomAgentLogsTmp/PRState/27246/try-fix/ +pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 -IssueNumber 27246 +``` + +#### Legacy: Manual parameters + +```powershell +pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 ` + -IssueNumber 19806 ` + -AttemptNumber 1 ` + -Approach "LayoutExtensions Width Constraint" ` + -RootCause "ComputeFrame only constrains width for Fill alignment" ` + -FilesChanged "| File | Changes |`n|------|---------|`n| LayoutExtensions.cs | +17/-3 |" ` + -Status "Compiles" ` + -CodeSnippet "else if (!hasExplicitWidth) { ... }" ` + -Analysis "Core project compiles successfully" +``` + +### Parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `TryFixDir` | No* | Path to try-fix attempt directory (auto-loads all parameters) | +| `IssueNumber` | No* | Issue or PR number to post comment on | +| `AttemptNumber` | No* | Attempt number (1, 2, 3, etc.) - auto-detected from TryFixDir | +| `Approach` | No* | Brief description of fix approach | +| `RootCause` | No | Description of root cause identified | +| `FilesChanged` | No* | Markdown table of files changed - auto-generated from diff | +| `Status` | No* | "Compiles", "Pass", or "Fail" - loaded from result.txt | +| `CodeSnippet` | No | Code snippet showing the fix - loaded from fix.diff | +| `Analysis` | No | Analysis of why it worked/failed - loaded from analysis.md | +| `DryRun` | No | Print comment instead of posting | + +*When using `-TryFixDir`, all marked parameters are auto-loaded from files in the directory. + +### Expected Directory Structure + +``` +CustomAgentLogsTmp/PRState/{IssueNumber}/try-fix/ +โ”œโ”€โ”€ attempt-1/ +โ”‚ โ”œโ”€โ”€ approach.md # Brief description of the approach (required) +โ”‚ โ”œโ”€โ”€ result.txt # "Pass", "Fail", or "Compiles" (required) +โ”‚ โ”œโ”€โ”€ fix.diff # Git diff of the fix (optional) +โ”‚ โ””โ”€โ”€ analysis.md # Detailed analysis (optional) +โ”œโ”€โ”€ attempt-2/ +โ”‚ โ””โ”€โ”€ ... +โ””โ”€โ”€ attempt-3/ + โ””โ”€โ”€ ... +``` + +### Comment Format + +```markdown +## ๐Ÿ”ง Try-Fix Attempts for Issue #XXXXX + + + +
+๐Ÿ“Š Expand Full Details + +**Issue:** [#XXXXX](link) + +--- + +
+๐Ÿ”ง Attempt #1: Approach Name โœ… Status +... attempt details ... +
+ +--- + +*This fix was developed independently.* + +
+``` + +### Key Behaviors + +- First attempt creates new comment with `` marker +- Subsequent attempts **edit the same comment** (no new comments) +- Outer wrapper shows "๐Ÿ“Š Expand Full Details" - keeps PR page clean +- Each attempt is a nested collapsible section inside the wrapper + +--- + +## Verify-Tests Comment Script + +The `post-verify-tests-comment.ps1` script updates the `` section of the unified AI Summary comment. It documents test verification results (whether tests fail without fix and pass with fix). + +**โœจ Auto-Loading from `CustomAgentLogsTmp`**: The script automatically loads verification results from `CustomAgentLogsTmp/PRState/{PRNumber}/verify-tests-fail/verification-report.md`. + +### Usage + +#### Simplest: Provide PR number + +```powershell +# Auto-loads from CustomAgentLogsTmp/PRState/{PRNumber}/verify-tests-fail/ +pwsh .github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 -PRNumber 32891 +``` + +#### With explicit report file + +```powershell +pwsh .github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 ` + -PRNumber 32891 ` + -ReportFile CustomAgentLogsTmp/PRState/32891/verify-tests-fail/verification-report.md +``` + +### Parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `PRNumber` | Yes | Pull request number | +| `ReportFile` | No | Path to verification report (auto-discovered if not provided) | +| `Status` | No | "Passed" or "Failed" - auto-detected from report | +| `Platform` | No | Platform tested (ios, android, etc.) - auto-detected from report | +| `Mode` | No | "FailureOnly" or "FullVerification" - auto-detected from report | +| `DryRun` | No | Preview changes in local file instead of posting | +| `PreviewFile` | No | Path to local preview file for DryRun mode | + +### Expected Directory Structure + +``` +CustomAgentLogsTmp/PRState/{PRNumber}/verify-tests-fail/ +โ”œโ”€โ”€ verification-report.md # Full verification report (required) +โ”œโ”€โ”€ verification-log.txt # Detailed log (optional) +โ”œโ”€โ”€ test-without-fix.log # Test output without fix (optional) +โ””โ”€โ”€ test-with-fix.log # Test output with fix (optional) +``` + +--- + +## Write-Tests Comment Script + +The `post-write-tests-comment.ps1` script updates the `` section of the unified AI Summary comment. It documents test writing attempts for an issue. + +**โœจ Auto-Loading from `CustomAgentLogsTmp`**: The script can automatically load test details from the write-tests output directory structure. + +### Usage + +#### Simplest: Provide test directory + +```powershell +# All parameters auto-loaded from directory structure +pwsh .github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 ` + -TestDir CustomAgentLogsTmp/PRState/27246/write-tests/attempt-1 +``` + +#### Or just provide issue number + +```powershell +# Auto-discovers and posts latest attempt from CustomAgentLogsTmp/PRState/27246/write-tests/ +pwsh .github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 -IssueNumber 27246 +``` + +#### Legacy: Manual parameters + +```powershell +pwsh .github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 ` + -IssueNumber 33331 ` + -AttemptNumber 1 ` + -TestDescription "Verifies Picker.IsOpen property changes correctly" ` + -HostAppFile "src/Controls/tests/TestCases.HostApp/Issues/Issue33331.cs" ` + -TestFile "src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33331.cs" ` + -TestMethod "PickerIsOpenPropertyChanges" ` + -Category "Picker" ` + -VerificationStatus "Verified" +``` + +### Parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `TestDir` | No* | Path to write-tests attempt directory (auto-loads all parameters) | +| `IssueNumber` | No* | Issue or PR number to post comment on | +| `AttemptNumber` | No* | Attempt number (1, 2, 3, etc.) - auto-detected from TestDir | +| `TestDescription` | No* | Brief description of what the test verifies | +| `HostAppFile` | No* | Path to the HostApp test page file | +| `TestFile` | No* | Path to the NUnit test file | +| `TestMethod` | No* | Name of the test method | +| `Category` | No* | UITestCategories category used | +| `VerificationStatus` | No* | "Verified", "Failed", or "Unverified" - loaded from result.txt | +| `Platforms` | No | Platforms the test runs on (default: "All") | +| `Notes` | No | Additional notes - loaded from notes.md | +| `DryRun` | No | Print comment instead of posting | + +*When using `-TestDir`, all marked parameters are auto-loaded from files in the directory. + +### Expected Directory Structure + +``` +CustomAgentLogsTmp/PRState/{IssueNumber}/write-tests/ +โ”œโ”€โ”€ attempt-1/ +โ”‚ โ”œโ”€โ”€ description.md # Brief test description (required) +โ”‚ โ”œโ”€โ”€ test-info.json # {HostAppFile, TestFile, TestMethod, Category} (required) +โ”‚ โ”œโ”€โ”€ result.txt # "Verified", "Pass", "Failed", or "Unverified" (required) +โ”‚ โ””โ”€โ”€ notes.md # Additional notes (optional) +โ”œโ”€โ”€ attempt-2/ +โ”‚ โ””โ”€โ”€ ... +โ””โ”€โ”€ attempt-3/ + โ””โ”€โ”€ ... +``` + +### test-info.json Format + +```json +{ + "HostAppFile": "src/Controls/tests/TestCases.HostApp/Issues/Issue27246.cs", + "TestFile": "src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue27246.cs", + "TestMethod": "ScrollToFirstItemWithHeader", + "Category": "CollectionView" +} +``` + +--- + +## Technical Details + +- Comments identified by HTML marker `` +- Existing comments are updated (not duplicated) when posting again +- Review sessions grouped by commit SHA +- Uses `gh api` for create/update operations diff --git a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 new file mode 100644 index 000000000000..d0748a5a245f --- /dev/null +++ b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 @@ -0,0 +1,791 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Posts or updates the PR agent review comment on a GitHub Pull Request with validation. + +.DESCRIPTION + Creates ONE comment for the entire PR review with all phases wrapped in an expandable section. + Uses HTML marker for identification. + + **NEW: Validates that phases marked as COMPLETE actually have content.** + **NEW: Auto-loads state file from CustomAgentLogsTmp/PRState/pr-XXXXX.md** + + Format: + ## ๐Ÿค– AI Summary โ€” โœ… APPROVE +
๐Ÿ“Š Expand Full Review + Status table + all 5 phases as nested details +
+ +.PARAMETER PRNumber + The pull request number (required unless -StateFile is provided with pr-XXXXX.md naming) + +.PARAMETER StateFile + Path to state file (defaults to CustomAgentLogsTmp/PRState/pr-{PRNumber}.md) + If provided with pr-XXXXX.md naming, PRNumber is auto-extracted + +.PARAMETER Content + The full state file content (alternative to -StateFile) + +.PARAMETER DryRun + Print comment instead of posting + +.PARAMETER SkipValidation + Skip validation checks (not recommended) + +.EXAMPLE + # Simplest: just provide PR number, state file auto-loaded + ./post-ai-summary-comment.ps1 -PRNumber 12345 + +.EXAMPLE + # Provide state file directly (PR number auto-extracted from filename) + ./post-ai-summary-comment.ps1 -StateFile CustomAgentLogsTmp/PRState/pr-27246.md + +.EXAMPLE + # Legacy: provide content directly + ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content "$(cat CustomAgentLogsTmp/PRState/pr-12345.md)" +#> + +param( + [Parameter(Mandatory=$false)] + [int]$PRNumber, + + [Parameter(Mandatory=$false)] + [string]$StateFile, + + [Parameter(Mandatory=$false)] + [string]$Content, + + [Parameter(Mandatory=$false)] + [switch]$DryRun, + + [Parameter(Mandatory=$false)] + [switch]$SkipValidation, + + [Parameter(Mandatory=$false)] + [string]$PreviewFile +) + +$ErrorActionPreference = "Stop" + +# ============================================================================ +# STATE FILE RESOLUTION +# ============================================================================ + +# Priority: 1) -Content, 2) -StateFile, 3) Auto-detect from PRNumber + +# If StateFile provided, extract PRNumber from filename if not already set +if (-not [string]::IsNullOrWhiteSpace($StateFile)) { + if ($StateFile -match 'pr-(\d+)\.md$') { + $extractedPR = [int]$Matches[1] + if ($PRNumber -eq 0) { + $PRNumber = $extractedPR + Write-Host "โ„น๏ธ Auto-detected PRNumber: $PRNumber from state file name" -ForegroundColor Cyan + } elseif ($PRNumber -ne $extractedPR) { + Write-Host "โš ๏ธ Warning: PRNumber ($PRNumber) differs from state file name (pr-$extractedPR.md)" -ForegroundColor Yellow + } + } + + if (Test-Path $StateFile) { + $Content = Get-Content $StateFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Loaded state file: $StateFile" -ForegroundColor Cyan + } else { + throw "State file not found: $StateFile" + } +} + +# If no Content and no StateFile, try auto-detect from PRNumber +if ([string]::IsNullOrWhiteSpace($Content) -and $PRNumber -gt 0) { + $autoStateFile = "CustomAgentLogsTmp/PRState/pr-$PRNumber.md" + if (Test-Path $autoStateFile) { + $Content = Get-Content $autoStateFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Auto-loaded state file: $autoStateFile" -ForegroundColor Cyan + } else { + # Try relative to repo root + $repoRoot = git rev-parse --show-toplevel 2>$null + if ($repoRoot) { + $autoStateFile = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/pr-$PRNumber.md" + if (Test-Path $autoStateFile) { + $Content = Get-Content $autoStateFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Auto-loaded state file: $autoStateFile" -ForegroundColor Cyan + } + } + } +} + +# If Content still not provided, try stdin (legacy support) +if ([string]::IsNullOrWhiteSpace($Content)) { + $Content = $input | Out-String +} + +# Final validation +if ([string]::IsNullOrWhiteSpace($Content)) { + Write-Host "" + Write-Host "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" -ForegroundColor Red + Write-Host "โ•‘ โ›” No state file content found โ•‘" -ForegroundColor Red + Write-Host "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" -ForegroundColor Red + Write-Host "" + Write-Host "Usage options:" -ForegroundColor Yellow + Write-Host " 1. ./post-ai-summary-comment.ps1 -PRNumber 12345" -ForegroundColor Gray + Write-Host " (auto-loads CustomAgentLogsTmp/PRState/pr-12345.md)" -ForegroundColor Gray + Write-Host "" + Write-Host " 2. ./post-ai-summary-comment.ps1 -StateFile path/to/pr-12345.md" -ForegroundColor Gray + Write-Host " (loads specified file, extracts PRNumber from name)" -ForegroundColor Gray + Write-Host "" + Write-Host " 3. ./post-ai-summary-comment.ps1 -PRNumber 12345 -Content `"...`"" -ForegroundColor Gray + Write-Host " (legacy: provide content directly)" -ForegroundColor Gray + Write-Host "" + throw "Content is required. See usage options above." +} + +if ($PRNumber -eq 0) { + throw "PRNumber is required. Provide via -PRNumber or use a state file named pr-XXXXX.md" +} + +Write-Host "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" -ForegroundColor Cyan +Write-Host "โ•‘ AI Summary Comment (with Validation) โ•‘" -ForegroundColor Cyan +Write-Host "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" -ForegroundColor Cyan + +# ============================================================================ +# VALIDATION FUNCTIONS +# ============================================================================ + +function Test-PhaseContentComplete { + param( + [string]$PhaseContent, + [string]$PhaseName, + [string]$PhaseStatus, + [switch]$Debug + ) + + # Skip validation if phase is not marked COMPLETE or PASSED + if ($PhaseStatus -notmatch 'โœ…\s*(COMPLETE|PASSED)') { + return @{ IsValid = $true; Errors = @(); Warnings = @() } + } + + $validationErrors = @() + $validationWarnings = @() + + # Check if content exists + if ([string]::IsNullOrWhiteSpace($PhaseContent)) { + $validationErrors += "Phase $PhaseName is marked as '$PhaseStatus' but has NO content in state file" + if ($Debug) { + Write-Host " [DEBUG] Content is null or whitespace for phase: $PhaseName" -ForegroundColor DarkGray + } + return @{ IsValid = $false; Errors = $validationErrors; Warnings = @() } + } + + if ($Debug) { + Write-Host " [DEBUG] $PhaseName content length: $($PhaseContent.Length) chars" -ForegroundColor DarkGray + Write-Host " [DEBUG] First 100 chars: $($PhaseContent.Substring(0, [Math]::Min(100, $PhaseContent.Length)))" -ForegroundColor DarkGray + } + + # Check for PENDING markers + $pendingMatches = [regex]::Matches($PhaseContent, '\[PENDING\]|โณ\s*PENDING') + if ($pendingMatches.Count -gt 0) { + $validationErrors += "Phase $PhaseName is marked as '$PhaseStatus' but contains $($pendingMatches.Count) PENDING markers" + } + + # Phase-specific validation (relaxed for better UX) + switch ($PhaseName) { + "Pre-Flight" { + if ($PhaseContent -notmatch 'Platforms Affected:') { + $validationWarnings += "Pre-Flight missing 'Platforms Affected' section (non-critical)" + } + } + "Tests" { + if ($PhaseContent -notmatch '(HostApp:|Test Files:)') { + $validationWarnings += "Tests phase missing test file paths (non-critical)" + } + } + "Gate" { + if ($PhaseContent -notmatch 'Result:') { + $validationWarnings += "Gate phase missing 'Result' field (non-critical)" + } + } + "Fix" { + if ($PhaseContent -notmatch 'Selected Fix:') { + $validationErrors += "Fix phase missing 'Selected Fix' field" + } + if ($PhaseContent -notmatch 'Exhausted:') { + $validationWarnings += "Fix phase missing 'Exhausted' field (non-critical)" + } + } + "Report" { + # Relaxed validation - only check for substantive content + $hasRecommendation = $PhaseContent -match '(Final Recommendation|Verdict|Recommendation:|APPROVE|REQUEST CHANGES)' + $hasAnalysis = $PhaseContent -match '(Summary|Fix Quality|Test Quality|Why|Analysis)' + + if (-not $hasRecommendation) { + $validationWarnings += "Report phase missing clear recommendation (non-critical)" + } + if (-not $hasAnalysis) { + $validationWarnings += "Report phase missing analysis sections (non-critical)" + } + + # Only error if content is extremely short + if ($PhaseContent.Length -lt 200) { + $validationErrors += "Report phase content is too short ($($PhaseContent.Length) chars) - expected comprehensive final report" + } + } + } + + return @{ + IsValid = ($validationErrors.Count -eq 0) + Errors = $validationErrors + Warnings = $validationWarnings + } +} + +# ============================================================================ +# EXTRACTION FUNCTIONS +# ============================================================================ + +# Extract recommendation from state file +$recommendation = "IN PROGRESS" +if ($Content -match '##\s+โœ…\s+Final Recommendation:\s+APPROVE') { + $recommendation = "โœ… APPROVE" +} elseif ($Content -match '##\s+โš ๏ธ\s+Final Recommendation:\s+REQUEST CHANGES') { + $recommendation = "โš ๏ธ REQUEST CHANGES" +} elseif ($Content -match 'Final Recommendation:\s+APPROVE') { + $recommendation = "โœ… APPROVE" +} elseif ($Content -match 'Final Recommendation:\s+REQUEST CHANGES') { + $recommendation = "โš ๏ธ REQUEST CHANGES" +} + +# Extract phase statuses from state file +$phaseStatuses = @{ + "Pre-Flight" = "โณ PENDING" + "Tests" = "โณ PENDING" + "Gate" = "โณ PENDING" + "Fix" = "โณ PENDING" + "Report" = "โณ PENDING" +} + +# Parse phase status table - match any status format +if ($Content -match '(?s)\|\s*Phase\s*\|\s*Status\s*\|.*?\n\|[\s-]+\|[\s-]+\|(.*?)(?=\n\n|---|\z)') { + $tableContent = $Matches[1] + $tableContent -split '\n' | ForEach-Object { + if ($_ -match '\|\s*(.+?)\s*\|\s*(.+?)\s*\|') { + $phaseName = $Matches[1].Trim() -replace '^๐Ÿ”\s*', '' -replace '^๐Ÿงช\s*', '' -replace '^๐Ÿšฆ\s*', '' -replace '^๐Ÿ”ง\s*', '' -replace '^๐Ÿ“‹\s*', '' + $status = $Matches[2].Trim() + if ($phaseStatuses.ContainsKey($phaseName)) { + $phaseStatuses[$phaseName] = $status + } + } + } +} + +# ============================================================================ +# DYNAMIC SECTION EXTRACTION +# ============================================================================ + +# Extract ALL sections from state file dynamically +function Extract-AllSections { + param( + [string]$StateContent, + [switch]$Debug + ) + + $sections = @{} + + # Pattern to find all
TITLE...content...
blocks + $pattern = '(?s)
\s*([^<]+)(.*?)
' + $matches = [regex]::Matches($StateContent, $pattern) + + if ($Debug) { + Write-Host " [DEBUG] Found $($matches.Count) section(s) in state file" -ForegroundColor Cyan + } + + foreach ($match in $matches) { + $title = $match.Groups[1].Value.Trim() + $content = $match.Groups[2].Value.Trim() + + $sections[$title] = $content + + if ($Debug) { + Write-Host " [DEBUG] Section: '$title' (${content.Length} chars)" -ForegroundColor DarkGray + } + } + + return $sections +} + +# Extract all sections dynamically +$debugMode = $false # Set to $true for debugging +if ($DebugPreference -eq 'Continue') { $debugMode = $true } + +$allSections = Extract-AllSections -StateContent $Content -Debug:$debugMode + +# Map sections to phase content using flexible matching +function Get-SectionByPattern { + param( + [hashtable]$Sections, + [string[]]$Patterns, + [switch]$Debug + ) + + foreach ($pattern in $Patterns) { + foreach ($key in $Sections.Keys) { + if ($key -match $pattern) { + if ($Debug) { + Write-Host " [DEBUG] Matched '$key' with pattern '$pattern'" -ForegroundColor Green + } + return $Sections[$key] + } + } + } + + if ($Debug) { + Write-Host " [DEBUG] No match for patterns: $($Patterns -join ', ')" -ForegroundColor Yellow + Write-Host " [DEBUG] Available sections: $($Sections.Keys -join ', ')" -ForegroundColor Yellow + } + + return $null +} + +# Map to phase content with flexible patterns (regex) +$preFlightContent = Get-SectionByPattern -Sections $allSections -Patterns @( + '๐Ÿ“‹.*Issue Summary', + '๐Ÿ“‹.*Pre-Flight', + '๐Ÿ”.*Pre-Flight' +) -Debug:$debugMode + +$testsContent = Get-SectionByPattern -Sections $allSections -Patterns @( + '๐Ÿงช.*Tests', + '๐Ÿ“‹.*Tests' +) -Debug:$debugMode + +$gateContent = Get-SectionByPattern -Sections $allSections -Patterns @( + '๐Ÿšฆ.*Gate', + '๐Ÿ“‹.*Gate' +) -Debug:$debugMode + +$fixContent = Get-SectionByPattern -Sections $allSections -Patterns @( + '๐Ÿ”ง.*Fix', + '๐Ÿ“‹.*Fix' +) -Debug:$debugMode + +$reportContent = Get-SectionByPattern -Sections $allSections -Patterns @( + '๐Ÿ“‹.*Report', + 'Phase 5.*Report', + 'Final Report' +) -Debug:$debugMode + +# ============================================================================ +# VALIDATION +# ============================================================================ + +if (-not $SkipValidation) { + Write-Host "`nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" -ForegroundColor Yellow + Write-Host "โ•‘ Phase Content Validation โ•‘" -ForegroundColor Yellow + Write-Host "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" -ForegroundColor Yellow + + $allValidationErrors = @() + $allValidationWarnings = @() + + # Validate each phase + $phases = @( + @{ Name = "Pre-Flight"; Content = $preFlightContent; Status = $phaseStatuses["Pre-Flight"] }, + @{ Name = "Tests"; Content = $testsContent; Status = $phaseStatuses["Tests"] }, + @{ Name = "Gate"; Content = $gateContent; Status = $phaseStatuses["Gate"] }, + @{ Name = "Fix"; Content = $fixContent; Status = $phaseStatuses["Fix"] }, + @{ Name = "Report"; Content = $reportContent; Status = $phaseStatuses["Report"] } + ) + + foreach ($phase in $phases) { + $result = Test-PhaseContentComplete -PhaseContent $phase.Content -PhaseName $phase.Name -PhaseStatus $phase.Status -Debug:$debugMode + + if ($result.IsValid) { + Write-Host " โœ… $($phase.Name): Valid" -ForegroundColor Green + } else { + Write-Host " โŒ $($phase.Name): INVALID" -ForegroundColor Red + foreach ($error in $result.Errors) { + Write-Host " - $error" -ForegroundColor Red + $allValidationErrors += "$($phase.Name): $error" + } + } + + # Show warnings + if ($result.Warnings -and $result.Warnings.Count -gt 0) { + foreach ($warning in $result.Warnings) { + Write-Host " โš ๏ธ $warning" -ForegroundColor Yellow + $allValidationWarnings += "$($phase.Name): $warning" + } + } + } + + # Show warnings summary if any + if ($allValidationWarnings.Count -gt 0) { + Write-Host "`nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" -ForegroundColor Yellow + Write-Host "โ•‘ โš ๏ธ VALIDATION WARNINGS โ•‘" -ForegroundColor Yellow + Write-Host "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" -ForegroundColor Yellow + Write-Host "" + Write-Host "Found $($allValidationWarnings.Count) warning(s) (non-critical):" -ForegroundColor Yellow + foreach ($warning in $allValidationWarnings) { + Write-Host " - $warning" -ForegroundColor Yellow + } + Write-Host "" + Write-Host "๐Ÿ’ก These are suggestions for improvement but won't block posting." -ForegroundColor Cyan + } + + # Only fail on errors + if ($allValidationErrors.Count -gt 0) { + Write-Host "`nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" -ForegroundColor Red + Write-Host "โ•‘ โ›” VALIDATION FAILED โ•‘" -ForegroundColor Red + Write-Host "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" -ForegroundColor Red + Write-Host "" + Write-Host "Found $($allValidationErrors.Count) validation error(s):" -ForegroundColor Red + foreach ($error in $allValidationErrors) { + Write-Host " - $error" -ForegroundColor Red + } + Write-Host "" + Write-Host "๐Ÿ’ก Fix these issues in the state file before posting the review comment." -ForegroundColor Cyan + Write-Host " Or use -SkipValidation to bypass these checks (not recommended)." -ForegroundColor Cyan + Write-Host "" + Write-Host "๐Ÿ› Debug tip: Run with `$DebugPreference = 'Continue' for detailed extraction info" -ForegroundColor DarkGray + exit 1 + } + + if ($allValidationWarnings.Count -eq 0) { + Write-Host "" + Write-Host "โœ… All validation checks passed!" -ForegroundColor Green + } +} + +# ============================================================================ +# BUILD COMMENT (Rest of the original script logic) +# ============================================================================ + +# Get latest commit for NEW Review Session header +Write-Host "`nFetching latest commit info..." -ForegroundColor Yellow +$commitJson = gh api "repos/dotnet/maui/pulls/$PRNumber/commits" --jq '.[-1] | {message: .commit.message, sha: .sha}' | ConvertFrom-Json +$latestCommitTitle = ($commitJson.message -split "`n")[0] +$latestCommitSha = $commitJson.sha.Substring(0, 7) +$latestCommitUrl = "https://github.com/dotnet/maui/commit/$($commitJson.sha)" + +# Helper function to create a NEW review session +function New-ReviewSession { + param([string]$PhaseContent, [string]$CommitTitle, [string]$CommitSha, [string]$CommitUrl) + + if ([string]::IsNullOrWhiteSpace($PhaseContent)) { + return "" + } + + return @" +
+๐Ÿ“ Review Session โ€” $CommitTitle ยท $CommitSha + +--- + +$PhaseContent + +
+"@ +} + +# Helper function to extract existing review sessions from a phase +function Get-ExistingReviewSessions { + param([string]$PhaseContent) + + if ([string]::IsNullOrWhiteSpace($PhaseContent)) { + return @() + } + + $sessions = @() + $pattern = '(?s)
\s*๐Ÿ“.*?.*?
' + $matches = [regex]::Matches($PhaseContent, $pattern) + + foreach ($match in $matches) { + $sessions += $match.Value + } + + return $sessions +} + +# Helper function to combine existing sessions with new session +function Merge-ReviewSessions { + param( + [string[]]$ExistingSessions, + [string]$NewSession, + [string]$NewCommitSha + ) + + if ([string]::IsNullOrWhiteSpace($NewSession)) { + return "" + } + + # Check if any existing session is for the same commit + $allSessions = @() + $replaced = $false + + foreach ($existingSession in $ExistingSessions) { + # Check if this session contains the new commit SHA + if ($existingSession -match "$NewCommitSha") { + # Replace this session with the new one (only once) + if (-not $replaced) { + $allSessions += $NewSession + $replaced = $true + } + # Skip the old session with same commit SHA + } else { + # Keep the existing session + $allSessions += $existingSession + } + } + + # If we didn't replace any session, add the new one + if (-not $replaced) { + $allSessions += $NewSession + } + + return ($allSessions -join "`n`n---`n`n") +} + +# Fetch existing comment to preserve old review sessions +Write-Host "Checking for existing review comment..." -ForegroundColor Yellow +$existingComment = gh api "repos/dotnet/maui/issues/$PRNumber/comments" --jq '.[] | select(.body | contains("")) | {id: .id, body: .body}' | ConvertFrom-Json + +$existingPreFlightSessions = @() +$existingTestsSessions = @() +$existingGateSessions = @() +$existingFixSessions = @() +$existingReportSessions = @() + +if ($existingComment) { + Write-Host "โœ“ Found existing review comment (ID: $($existingComment.id)) - extracting review sessions..." -ForegroundColor Green + + # Helper function to extract phase content with fallback patterns + function Extract-PhaseFromComment { + param( + [string]$CommentBody, + [string]$Emoji, + [string]$PhaseName + ) + + # Try patterns in order of specificity (most specific first) + $patterns = @( + # Pattern 1: Phase name anywhere in the header + "(?s).*?$PhaseName.*?(.*?)
" + # Pattern 2: Just emoji (most lenient fallback) + "(?s)$Emoji[^<]*(.*?)
" + ) + + foreach ($pattern in $patterns) { + if ($CommentBody -match $pattern) { + return $Matches[1] + } + } + + return $null + } + + # Extract existing sessions from each phase with fallback + $preFlightMatch = Extract-PhaseFromComment -CommentBody $existingComment.body -Emoji "๐Ÿ”" -PhaseName "Pre-Flight" + if ($preFlightMatch) { $existingPreFlightSessions = Get-ExistingReviewSessions -PhaseContent $preFlightMatch } + + $testsMatch = Extract-PhaseFromComment -CommentBody $existingComment.body -Emoji "๐Ÿงช" -PhaseName "Tests" + if ($testsMatch) { $existingTestsSessions = Get-ExistingReviewSessions -PhaseContent $testsMatch } + + $gateMatch = Extract-PhaseFromComment -CommentBody $existingComment.body -Emoji "๐Ÿšฆ" -PhaseName "Gate" + if ($gateMatch) { $existingGateSessions = Get-ExistingReviewSessions -PhaseContent $gateMatch } + + $fixMatch = Extract-PhaseFromComment -CommentBody $existingComment.body -Emoji "๐Ÿ”ง" -PhaseName "Fix" + if ($fixMatch) { $existingFixSessions = Get-ExistingReviewSessions -PhaseContent $fixMatch } + + $reportMatch = Extract-PhaseFromComment -CommentBody $existingComment.body -Emoji "๐Ÿ“‹" -PhaseName "Report" + if ($reportMatch) { $existingReportSessions = Get-ExistingReviewSessions -PhaseContent $reportMatch } +} else { + Write-Host "โœ“ No existing comment found - creating new..." -ForegroundColor Yellow +} + +# Create NEW review sessions from current state file +$newPreFlightSession = New-ReviewSession -PhaseContent $preFlightContent -CommitTitle $latestCommitTitle -CommitSha $latestCommitSha -CommitUrl $latestCommitUrl +$newTestsSession = New-ReviewSession -PhaseContent $testsContent -CommitTitle $latestCommitTitle -CommitSha $latestCommitSha -CommitUrl $latestCommitUrl +$newGateSession = New-ReviewSession -PhaseContent $gateContent -CommitTitle $latestCommitTitle -CommitSha $latestCommitSha -CommitUrl $latestCommitUrl +$newFixSession = New-ReviewSession -PhaseContent $fixContent -CommitTitle $latestCommitTitle -CommitSha $latestCommitSha -CommitUrl $latestCommitUrl +$newReportSession = New-ReviewSession -PhaseContent $reportContent -CommitTitle $latestCommitTitle -CommitSha $latestCommitSha -CommitUrl $latestCommitUrl + +# Merge existing sessions with new session (if new content exists) +$allPreFlightSessions = if ($newPreFlightSession) { Merge-ReviewSessions -ExistingSessions $existingPreFlightSessions -NewSession $newPreFlightSession -NewCommitSha $latestCommitSha } else { $existingPreFlightSessions -join "`n`n---`n`n" } +$allTestsSessions = if ($newTestsSession) { Merge-ReviewSessions -ExistingSessions $existingTestsSessions -NewSession $newTestsSession -NewCommitSha $latestCommitSha } else { $existingTestsSessions -join "`n`n---`n`n" } +$allGateSessions = if ($newGateSession) { Merge-ReviewSessions -ExistingSessions $existingGateSessions -NewSession $newGateSession -NewCommitSha $latestCommitSha } else { $existingGateSessions -join "`n`n---`n`n" } +$allFixSessions = if ($newFixSession) { Merge-ReviewSessions -ExistingSessions $existingFixSessions -NewSession $newFixSession -NewCommitSha $latestCommitSha } else { $existingFixSessions -join "`n`n---`n`n" } +$allReportSessions = if ($newReportSession) { Merge-ReviewSessions -ExistingSessions $existingReportSessions -NewSession $newReportSession -NewCommitSha $latestCommitSha } else { $existingReportSessions -join "`n`n---`n`n" } + +# Build phase sections dynamically - only include phases with content +$phaseSections = @() + +# Helper to create phase section +function New-PhaseSection { + param( + [string]$Icon, + [string]$PhaseName, + [string]$Subtitle, + [string]$Content, + [string]$Status + ) + + # Skip phases with no content + if ([string]::IsNullOrWhiteSpace($Content) -or $Content -eq "_No review sessions yet_") { + return $null + } + + return @" +
+$Icon $PhaseName โ€” $Subtitle + +--- + +$Content + +
+"@ +} + +# Build phase sections (only non-empty ones) +$preFlightSection = New-PhaseSection -Icon "๐Ÿ”" -PhaseName "Pre-Flight" -Subtitle "Context & Validation" -Content $allPreFlightSessions -Status $phaseStatuses['Pre-Flight'] +$testsSection = New-PhaseSection -Icon "๐Ÿงช" -PhaseName "Tests" -Subtitle "Verification" -Content $allTestsSessions -Status $phaseStatuses['Tests'] +$gateSection = New-PhaseSection -Icon "๐Ÿšฆ" -PhaseName "Gate" -Subtitle "Test Verification" -Content $allGateSessions -Status $phaseStatuses['Gate'] +$fixSection = New-PhaseSection -Icon "๐Ÿ”ง" -PhaseName "Fix" -Subtitle "Analysis & Comparison" -Content $allFixSessions -Status $phaseStatuses['Fix'] +$reportSection = New-PhaseSection -Icon "๐Ÿ“‹" -PhaseName "Report" -Subtitle "Final Recommendation" -Content $allReportSessions -Status $phaseStatuses['Report'] + +# Collect non-null sections +if ($preFlightSection) { $phaseSections += $preFlightSection } +if ($testsSection) { $phaseSections += $testsSection } +if ($gateSection) { $phaseSections += $gateSection } +if ($fixSection) { $phaseSections += $fixSection } +if ($reportSection) { $phaseSections += $reportSection } + +# Join sections with separators +$phaseContent = if ($phaseSections.Count -gt 0) { + $phaseSections -join "`n`n---`n`n" +} else { + "_No phases completed yet_" +} + +# ============================================================================ +# UNIFIED COMMENT HANDLING +# Uses single comment with section markers +# ============================================================================ + +$MAIN_MARKER = "" +$SECTION_START = "" +$SECTION_END = "" + +# Build the PR review section with markers +$prReviewSection = @" +$SECTION_START +
+๐Ÿ“Š Expand Full Review + +--- + +$phaseContent + +--- + +
+$SECTION_END +"@ + +# Check if there are other sections in the existing comment that we need to preserve +$existingOtherSections = "" +if ($existingComment) { + $body = $existingComment.body + + # Extract all non-PR-REVIEW sections + $sectionTypes = @("TRY-FIX", "WRITE-TESTS", "PR-FINALIZE") + foreach ($sectionType in $sectionTypes) { + $sStart = [regex]::Escape("") + $sEnd = [regex]::Escape("") + if ($body -match "(?s)($sStart.*?$sEnd)") { + $existingOtherSections += "`n`n" + $Matches[1] + } + } +} + +# Build aggregated comment body +$commentBody = @" +$MAIN_MARKER + +## ๐Ÿค– AI Summary + +$prReviewSection +$existingOtherSections +"@ + +# Clean up any double newlines +$commentBody = $commentBody -replace "`n{4,}", "`n`n`n" + +if ($DryRun) { + # File-based DryRun: mirrors GitHub comment behavior using a local file + if ([string]::IsNullOrWhiteSpace($PreviewFile)) { + $PreviewFile = "CustomAgentLogsTmp/PRState/$PRNumber/ai-summary-comment-preview.md" + } + + # Ensure directory exists + $previewDir = Split-Path $PreviewFile -Parent + if (-not (Test-Path $previewDir)) { + New-Item -ItemType Directory -Path $previewDir -Force | Out-Null + } + + # Read existing preview file + $existingPreview = "" + if (Test-Path $PreviewFile) { + $existingPreview = Get-Content $PreviewFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Updating existing preview file: $PreviewFile" -ForegroundColor Cyan + } else { + Write-Host "โ„น๏ธ Creating new preview file: $PreviewFile" -ForegroundColor Cyan + } + + # Update or insert the PR-REVIEW section + $PR_REVIEW_MARKER = "" + $PR_REVIEW_END_MARKER = "" + + if ($existingPreview -match [regex]::Escape($PR_REVIEW_MARKER)) { + # Replace existing PR-REVIEW section + $pattern = [regex]::Escape($PR_REVIEW_MARKER) + "[\s\S]*?" + [regex]::Escape($PR_REVIEW_END_MARKER) + $finalComment = $existingPreview -replace $pattern, $prReviewSection + } elseif (-not [string]::IsNullOrWhiteSpace($existingPreview)) { + # Append PR-REVIEW section to existing content + $finalComment = $existingPreview.TrimEnd() + "`n`n" + $prReviewSection + } else { + # New file - use full comment body + $finalComment = $commentBody + } + + # Write to preview file + Set-Content -Path $PreviewFile -Value "$($finalComment.TrimEnd())`n" -Encoding UTF8 -NoNewline + + Write-Host "`n=== COMMENT PREVIEW ===" -ForegroundColor Yellow + Write-Host $finalComment + Write-Host "`n=== END PREVIEW ===" -ForegroundColor Yellow + Write-Host "`nโœ… Preview saved to: $PreviewFile" -ForegroundColor Green + Write-Host " Run 'open $PreviewFile' to view in editor" -ForegroundColor Gray + exit 0 +} + +# Post or update comment (reuse $existingComment from earlier check) +if ($existingComment) { + Write-Host "โœ“ Updating existing review comment (ID: $($existingComment.id))..." -ForegroundColor Green + + # Create temp file for update + $tempFile = [System.IO.Path]::GetTempFileName() + @{ body = $commentBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + + gh api --method PATCH "repos/dotnet/maui/issues/comments/$($existingComment.id)" --input $tempFile | Out-Null + Remove-Item $tempFile + + Write-Host "โœ… Review comment updated successfully" -ForegroundColor Green +} else { + Write-Host "Creating new review comment..." -ForegroundColor Yellow + + # Create temp file for new comment + $tempFile = [System.IO.Path]::GetTempFileName() + @{ body = $commentBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + + gh api --method POST "repos/dotnet/maui/issues/$PRNumber/comments" --input $tempFile | Out-Null + Remove-Item $tempFile + + Write-Host "โœ… Review comment posted successfully" -ForegroundColor Green +} diff --git a/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 new file mode 100644 index 000000000000..f1aaade8f45d --- /dev/null +++ b/.github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 @@ -0,0 +1,574 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Posts or updates a PR finalize comment on a GitHub Pull Request. + +.DESCRIPTION + Creates ONE comment for PR finalization reviews with each review in a collapsible section. + Uses HTML marker for identification. + + **NEW: Auto-loads from CustomAgentLogsTmp/PRState/{PRNumber}/pr-finalize/** + + If an existing finalize comment exists, it will be EDITED with the new review added. + Otherwise, a new comment will be created. + + Format: + ## ๐Ÿ“‹ PR Finalization Review + + +
+ Review 1: Title and description check โœ… Ready + + ... review details ... +
+ +
+ Review 2: Updated after implementation change โš ๏ธ Needs Update + + ... review details ... +
+ +.PARAMETER PRNumber + The PR number to post comment on (required unless SummaryFile provided) + +.PARAMETER SummaryFile + Path to pr-finalize-summary.md file. If provided, auto-loads review data from this file. + +.PARAMETER ReviewNumber + The review number (1, 2, 3, etc.) - auto-detected from SummaryFile if not specified + +.PARAMETER ReviewDescription + Brief description of what was reviewed (required unless loading from SummaryFile) + +.PARAMETER TitleStatus + Title assessment: "Good", "NeedsUpdate" (required unless loading from SummaryFile) + +.PARAMETER CurrentTitle + Current PR title (required unless loading from SummaryFile) + +.PARAMETER RecommendedTitle + Recommended PR title (optional - only if TitleStatus is NeedsUpdate) + +.PARAMETER DescriptionStatus + Description assessment: "Excellent", "Good", "NeedsUpdate", "NeedsRewrite" (required unless loading from SummaryFile) + +.PARAMETER DescriptionAssessment + Quality assessment of the description (required unless loading from SummaryFile) + +.PARAMETER MissingElements + List of missing elements (optional) + +.PARAMETER RecommendedDescription + Full recommended description (optional - only if needs rewrite) + +.PARAMETER DryRun + Print comment instead of posting + +.EXAMPLE + # Simplest: Just provide PR number (auto-loads from CustomAgentLogsTmp) + ./post-pr-finalize-comment.ps1 -PRNumber 27246 + +.EXAMPLE + # Or provide summary file path + ./post-pr-finalize-comment.ps1 -SummaryFile CustomAgentLogsTmp/PRState/27246/pr-finalize/pr-finalize-summary.md + +.EXAMPLE + # Manual parameters (legacy) + ./post-pr-finalize-comment.ps1 -PRNumber 25748 -ReviewNumber 1 ` + -ReviewDescription "Initial finalization check" ` + -TitleStatus "Good" ` + -CurrentTitle "[iOS] Fix SafeArea padding calculation" ` + -DescriptionStatus "Good" ` + -DescriptionAssessment "Clear structure, accurate technical details, matches implementation" +#> + +param( + [Parameter(Mandatory=$false)] + [int]$PRNumber, + + [Parameter(Mandatory=$false)] + [string]$SummaryFile, + + [Parameter(Mandatory=$false)] + [int]$ReviewNumber, + + [Parameter(Mandatory=$false)] + [string]$ReviewDescription, + + [Parameter(Mandatory=$false)] + [ValidateSet("Good", "NeedsUpdate", "")] + [string]$TitleStatus, + + [Parameter(Mandatory=$false)] + [string]$CurrentTitle, + + [Parameter(Mandatory=$false)] + [string]$RecommendedTitle, + + [Parameter(Mandatory=$false)] + [ValidateSet("Excellent", "Good", "NeedsUpdate", "NeedsRewrite", "")] + [string]$DescriptionStatus, + + [Parameter(Mandatory=$false)] + [string]$DescriptionAssessment, + + [Parameter(Mandatory=$false)] + [string]$MissingElements, + + [Parameter(Mandatory=$false)] + [string]$RecommendedDescription, + + [Parameter(Mandatory=$false)] + [switch]$DryRun, + + [Parameter(Mandatory=$false)] + [string]$PreviewFile +) + +$ErrorActionPreference = "Stop" + +Write-Host "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" -ForegroundColor Cyan +Write-Host "โ•‘ PR Finalize Comment (Post/Update) โ•‘" -ForegroundColor Cyan +Write-Host "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" -ForegroundColor Cyan + +# ============================================================================ +# AUTO-DISCOVERY FROM SUMMARY FILE +# ============================================================================ + +# If PRNumber provided but no SummaryFile, try to find it +if ($PRNumber -gt 0 -and [string]::IsNullOrWhiteSpace($SummaryFile) -and [string]::IsNullOrWhiteSpace($ReviewDescription)) { + $summaryPath = "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/pr-finalize-summary.md" + if (-not (Test-Path $summaryPath)) { + $repoRoot = git rev-parse --show-toplevel 2>$null + if ($repoRoot) { + $summaryPath = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/pr-finalize-summary.md" + } + } + + if (Test-Path $summaryPath) { + $SummaryFile = $summaryPath + Write-Host "โ„น๏ธ Auto-discovered summary file: $SummaryFile" -ForegroundColor Cyan + } +} + +# If SummaryFile provided, parse it +if (-not [string]::IsNullOrWhiteSpace($SummaryFile)) { + if (-not (Test-Path $SummaryFile)) { + throw "Summary file not found: $SummaryFile" + } + + $content = Get-Content $SummaryFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Loading from summary file: $SummaryFile" -ForegroundColor Cyan + + # Extract PRNumber from path if not provided + if ($PRNumber -eq 0 -and $SummaryFile -match '[/\\](\d+)[/\\]pr-finalize') { + $PRNumber = [int]$Matches[1] + Write-Host "โ„น๏ธ Auto-detected PRNumber: $PRNumber from path" -ForegroundColor Cyan + } + + # Extract ReviewNumber (default to 1) + if ($ReviewNumber -eq 0) { + # Check if there's a review number in the content + if ($content -match 'Review (\d+)') { + $ReviewNumber = [int]$Matches[1] + } else { + $ReviewNumber = 1 + } + Write-Host "โ„น๏ธ Using ReviewNumber: $ReviewNumber" -ForegroundColor Cyan + } + + # Extract verdict for ReviewDescription + if ([string]::IsNullOrWhiteSpace($ReviewDescription)) { + if ($content -match 'โœ…\s*No Changes Needed') { + $ReviewDescription = "Finalization check - Ready" + } elseif ($content -match 'โš ๏ธ\s*Needs Updates') { + $ReviewDescription = "Finalization check - Needs Updates" + } else { + $ReviewDescription = "Finalization review" + } + } + + # Extract Title assessment + if ([string]::IsNullOrWhiteSpace($TitleStatus)) { + # Check if there are title issues or a recommended title + $hasRecommendedTitle = $content -match '\*\*Recommended.*?Title' + $hasTitleIssues = $content -match '(?s)### ๐Ÿ“‹ Title Assessment.+?\*\*Issues:\*\*' + + if ($hasRecommendedTitle -or $hasTitleIssues) { + $TitleStatus = "NeedsUpdate" + } elseif ($content -match '### Title.*?โœ…') { + $TitleStatus = "Good" + } else { + $TitleStatus = "Good" + } + Write-Host "โ„น๏ธ Detected TitleStatus: $TitleStatus" -ForegroundColor Cyan + } + + # Extract Current Title + if ([string]::IsNullOrWhiteSpace($CurrentTitle)) { + if ($content -match '\*\*Current.*?Title.*?\*\*:?\s*[`"]?([^`"\n]+)[`"]?') { + $CurrentTitle = $Matches[1].Trim() + } elseif ($content -match 'Current:\s*`([^`]+)`') { + $CurrentTitle = $Matches[1].Trim() + } + if ($CurrentTitle) { + Write-Host "โ„น๏ธ Extracted CurrentTitle: $CurrentTitle" -ForegroundColor Cyan + } + } + + # Extract Recommended Title + if ([string]::IsNullOrWhiteSpace($RecommendedTitle)) { + # Try different patterns + if ($content -match '\*\*Recommended.*?Title.*?\*\*:?\s*[`"]?([^`"\n]+)[`"]?') { + $RecommendedTitle = $Matches[1].Trim() + } elseif ($content -match 'Recommended:\s*`([^`]+)`') { + $RecommendedTitle = $Matches[1].Trim() + } elseif ($content -match '(?s)\*\*Recommended:\*\*\s*```\s*([^\n]+)') { + # Code fence format + $RecommendedTitle = $Matches[1].Trim() + } elseif ($content -match '(?s)### ๐Ÿ“‹ Title Assessment.+?\*\*Recommended:\*\*\s*```\s*([^\n]+)') { + $RecommendedTitle = $Matches[1].Trim() + } + if ($RecommendedTitle) { + Write-Host "โ„น๏ธ Extracted RecommendedTitle: $RecommendedTitle" -ForegroundColor Cyan + } + } + + # Extract Description assessment + if ([string]::IsNullOrWhiteSpace($DescriptionStatus)) { + if ($content -match 'Description.*?Excellent|Excellent.*?description') { + $DescriptionStatus = "Excellent" + } elseif ($content -match 'Description.*?Good|Good.*?description') { + $DescriptionStatus = "Good" + } elseif ($content -match 'Needs\s*Rewrite|NeedsRewrite') { + $DescriptionStatus = "NeedsRewrite" + } elseif ($content -match 'Needs\s*Update|NeedsUpdate') { + $DescriptionStatus = "NeedsUpdate" + } else { + $DescriptionStatus = "Good" + } + Write-Host "โ„น๏ธ Detected DescriptionStatus: $DescriptionStatus" -ForegroundColor Cyan + } + + # Extract Title Issues/Reasoning + $titleIssues = "" + if ([string]::IsNullOrWhiteSpace($RecommendedTitle)) { + # No recommended title means title is good + $titleIssues = "Title is clear and follows conventions." + } else { + # Extract the issues list + if ($content -match '(?s)\*\*Issues:\*\*(.+?)(?=\*\*Recommended|\*\*Reasoning|###|$)') { + $titleIssues = $Matches[1].Trim() + } elseif ($content -match '(?s)### ๐Ÿ“‹ Title Assessment.+?\*\*Issues:\*\*(.+?)(?=\*\*Recommended|###|$)') { + $titleIssues = $Matches[1].Trim() + } + } + + # Extract Description Assessment text + if ([string]::IsNullOrWhiteSpace($DescriptionAssessment)) { + # Try to extract detailed issues from the summary + $issuesSection = "" + if ($content -match '(?s)### ๐Ÿ“ Description Quality Assessment(.+?)(?=###|---|\z)') { + $issuesSection = $Matches[1].Trim() + # Remove the Status line since we already show it in the header + $issuesSection = $issuesSection -replace '(?m)^\*\*Status:\*\*.*$\n?', '' + $issuesSection = $issuesSection.Trim() + } elseif ($content -match '(?s)\*\*Issues:\*\*(.+?)(?=\*\*Recommended|\*\*Action|###|---|\z)') { + $issuesSection = $Matches[1].Trim() + } + + # Try to extract what works + $worksSection = "" + if ($content -match '(?s)### โŒ Issues Found(.+?)(?=###|---|\z)') { + $worksSection = $Matches[1].Trim() + } elseif ($content -match '(?s)\| Issue \| Severity \| Details \|(.+?)(?=###|---|\z)') { + # Extract table content + $worksSection = "**Issues:**`n" + $Matches[1].Trim() + } + + # Combine into assessment + if (-not [string]::IsNullOrWhiteSpace($issuesSection) -or -not [string]::IsNullOrWhiteSpace($worksSection)) { + $DescriptionAssessment = "" + if ($worksSection) { $DescriptionAssessment += $worksSection + "`n`n" } + if ($issuesSection) { $DescriptionAssessment += $issuesSection } + } else { + # Fallback - try to get the verdict section + if ($content -match '(?s)## โš ๏ธ Verdict:(.+?)(?=###|$)') { + $DescriptionAssessment = $Matches[1].Trim() + } else { + $DescriptionAssessment = "Description needs updates. See details below." + } + } + } + + # Extract Missing Elements + if ([string]::IsNullOrWhiteSpace($MissingElements)) { + if ($content -match '(?s)Missing.*?elements?:(.+?)(?=###|$)') { + $MissingElements = $Matches[1].Trim() + } elseif ($content -match '(?s)Only addition needed:(.+?)(?=###|\*\*Action|$)') { + $MissingElements = $Matches[1].Trim() + } + } + + # Extract Recommended Description + if ([string]::IsNullOrWhiteSpace($RecommendedDescription)) { + # First, try to find a separate recommended-description.md file in same directory + $summaryDir = Split-Path $SummaryFile -Parent + $recommendedDescFile = Join-Path $summaryDir "recommended-description.md" + + if (Test-Path $recommendedDescFile) { + Write-Host "โ„น๏ธ Found recommended-description.md file, loading full content..." -ForegroundColor Cyan + $RecommendedDescription = Get-Content $recommendedDescFile -Raw -Encoding UTF8 + $RecommendedDescription = $RecommendedDescription.Trim() + } + # Try to extract from
section in summary file + elseif ($content -match '(?s)
\s*Click to see proposed description\s*```markdown\s*(.+?)```\s*
') { + Write-Host "โ„น๏ธ Extracted recommended description from expandable section..." -ForegroundColor Cyan + $RecommendedDescription = $Matches[1].Trim() + } + # Otherwise, try to extract from header in summary file + elseif ($content -match '(?s)### Recommended Description(.+?)(?=### [A-Z]|---|\z)') { + $RecommendedDescription = $Matches[1].Trim() + } + # If still empty, check for recommended description block in summary + elseif ($content -match '(?s)```markdown\s*" + +Write-Host "`nChecking for existing PR Finalization comment on #$PRNumber..." -ForegroundColor Yellow +$existingComment = $null + +try { + $commentsJson = gh api "repos/dotnet/maui/issues/$PRNumber/comments" 2>$null + $comments = $commentsJson | ConvertFrom-Json + + foreach ($comment in $comments) { + if ($comment.body -match [regex]::Escape($FINALIZE_MARKER)) { + $existingComment = $comment + Write-Host "โœ“ Found existing PR Finalization comment (ID: $($comment.id))" -ForegroundColor Green + break + } + } + + if (-not $existingComment) { + Write-Host "โœ“ No existing PR Finalization comment found - will create new" -ForegroundColor Yellow + } +} catch { + Write-Host "โœ“ No existing PR Finalization comment found - will create new" -ForegroundColor Yellow +} + +# Build the full comment body +$commentBody = @" +## ๐Ÿ“‹ PR Finalization Review +$FINALIZE_MARKER + +"@ + +if ($existingComment) { + # Parse existing reviews + $existingReviews = @() + $pattern = '(?s)
\s*Review (\d+):.+?
' + $matches = [regex]::Matches($existingComment.body, $pattern) + + foreach ($match in $matches) { + $reviewNum = [int]$match.Groups[1].Value + if ($reviewNum -ne $ReviewNumber) { + $existingReviews += $match.Value + } + } + + # Add existing reviews first, then new review + if ($existingReviews.Count -gt 0) { + $commentBody += ($existingReviews -join "`n`n") + $commentBody += "`n`n" + } +} + +$commentBody += $reviewSection + +if ($DryRun) { + # File-based DryRun: uses separate preview file for finalize (separate comment from unified) + if ([string]::IsNullOrWhiteSpace($PreviewFile)) { + $PreviewFile = "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize-preview.md" + } + + # Ensure directory exists + $previewDir = Split-Path $PreviewFile -Parent + if (-not (Test-Path $previewDir)) { + New-Item -ItemType Directory -Path $previewDir -Force | Out-Null + } + + # For finalize, we replace the entire file (it's a separate comment) + Write-Host "โ„น๏ธ Saving finalize preview to: $PreviewFile" -ForegroundColor Cyan + Set-Content -Path $PreviewFile -Value "$($commentBody.TrimEnd())`n" -Encoding UTF8 -NoNewline + + Write-Host "`n=== COMMENT PREVIEW ===" -ForegroundColor Yellow + Write-Host $commentBody + Write-Host "`n=== END PREVIEW ===" -ForegroundColor Yellow + Write-Host "`nโœ… Preview saved to: $PreviewFile" -ForegroundColor Green + Write-Host " Run 'open $PreviewFile' to view in editor" -ForegroundColor Gray + exit 0 +} + +# Write to temp file to avoid shell escaping issues +$tempFile = [System.IO.Path]::GetTempFileName() +@{ body = $commentBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + +if ($existingComment) { + Write-Host "Updating comment ID $($existingComment.id)..." -ForegroundColor Yellow + $result = gh api --method PATCH "repos/dotnet/maui/issues/comments/$($existingComment.id)" --input $tempFile --jq '.html_url' + Write-Host "โœ… Comment updated: $result" -ForegroundColor Green +} else { + Write-Host "Posting new comment to PR #$PRNumber..." -ForegroundColor Yellow + $result = gh api --method POST "repos/dotnet/maui/issues/$PRNumber/comments" --input $tempFile --jq '.html_url' + Write-Host "โœ… Comment posted: $result" -ForegroundColor Green +} + +Remove-Item $tempFile diff --git a/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 new file mode 100644 index 000000000000..5c3d5634b1dc --- /dev/null +++ b/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 @@ -0,0 +1,449 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Posts or updates a try-fix attempts comment on a GitHub Issue or Pull Request. + +.DESCRIPTION + Creates ONE comment for all try-fix attempts with each attempt in a collapsible section. + Uses HTML marker for identification. + + If an existing try-fix comment exists, it will be EDITED with the new attempt added. + Otherwise, a new comment will be created. + + **NEW: Auto-loads from CustomAgentLogsTmp/PRState/{PRNumber}/try-fix/** + + Format: + ## ๐Ÿ”ง Try-Fix Analysis for Issue #XXXXX + + +
+ Attempt 1: Approach Name โœ… PASS + + ... attempt details ... +
+ +
+ Attempt 2: Different Approach โŒ FAIL + + ... attempt details ... +
+ +.PARAMETER IssueNumber + The issue number to post comment on (required unless -TryFixDir provided) + +.PARAMETER AttemptNumber + The attempt number (1, 2, 3, etc.) - auto-detected from TryFixDir if not specified + +.PARAMETER TryFixDir + Path to try-fix attempt directory (e.g., CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1) + If provided, all parameters are auto-loaded from files in this directory + +.PARAMETER Approach + Brief description of the fix approach (required unless loading from TryFixDir) + +.PARAMETER RootCause + Description of the root cause identified (optional for failed attempts) + +.PARAMETER FilesChanged + Markdown table or list of files changed (required unless loading from TryFixDir) + +.PARAMETER Status + Status of the attempt: "Compiles", "Pass", "Fail" (required unless loading from TryFixDir) + +.PARAMETER Analysis + Analysis of why it worked or failed (optional) + +.PARAMETER CodeSnippet + Code snippet showing the fix (optional) + +.PARAMETER DryRun + Print comment instead of posting + +.EXAMPLE + # Simplest: Just provide attempt directory (all info auto-loaded) + ./post-try-fix-comment.ps1 -TryFixDir CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1 + +.EXAMPLE + # Post all attempts for an issue + ./post-try-fix-comment.ps1 -IssueNumber 27246 + +.EXAMPLE + # Manual parameters (legacy) + ./post-try-fix-comment.ps1 -IssueNumber 19560 -AttemptNumber 1 ` + -Approach "Change Shadow base class to StyleableElement" ` + -RootCause "Shadow inherits from Element which lacks styling support" ` + -FilesChanged "| File | Changes |`n|------|---------|`n| Shadow.cs | +1/-1 |" ` + -Status "Pass" +#> + +param( + [Parameter(Mandatory=$false)] + [int]$IssueNumber, + + [Parameter(Mandatory=$false)] + [int]$AttemptNumber, + + [Parameter(Mandatory=$false)] + [string]$TryFixDir, + + [Parameter(Mandatory=$false)] + [string]$Approach, + + [Parameter(Mandatory=$false)] + [string]$RootCause, + + [Parameter(Mandatory=$false)] + [string]$FilesChanged, + + [Parameter(Mandatory=$false)] + [ValidateSet("Compiles", "Pass", "Fail", "")] + [string]$Status, + + [Parameter(Mandatory=$false)] + [string]$Analysis, + + [Parameter(Mandatory=$false)] + [string]$CodeSnippet, + + [Parameter(Mandatory=$false)] + [switch]$DryRun, + + [Parameter(Mandatory=$false)] + [string]$PreviewFile +) + +$ErrorActionPreference = "Stop" + +Write-Host "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" -ForegroundColor Cyan +Write-Host "โ•‘ Try-Fix Comment (Post/Update) โ•‘" -ForegroundColor Cyan +Write-Host "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" -ForegroundColor Cyan + +# ============================================================================ +# AUTO-DISCOVERY FROM DIRECTORIES +# ============================================================================ + +# If TryFixDir provided, load everything from there +if (-not [string]::IsNullOrWhiteSpace($TryFixDir)) { + if (-not (Test-Path $TryFixDir)) { + throw "Try-fix directory not found: $TryFixDir" + } + + # Extract IssueNumber from path (e.g., CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1) + if ($TryFixDir -match '[/\\](\d+)[/\\]try-fix') { + if ($IssueNumber -eq 0) { + $IssueNumber = [int]$Matches[1] + Write-Host "โ„น๏ธ Auto-detected IssueNumber: $IssueNumber from path" -ForegroundColor Cyan + } + } + + # Extract AttemptNumber from path (e.g., attempt-1) + if ($TryFixDir -match 'attempt-(\d+)$') { + if ($AttemptNumber -eq 0) { + $AttemptNumber = [int]$Matches[1] + Write-Host "โ„น๏ธ Auto-detected AttemptNumber: $AttemptNumber from path" -ForegroundColor Cyan + } + } + + # Load approach from approach.md or approach.txt + if ([string]::IsNullOrWhiteSpace($Approach)) { + $approachFile = Join-Path $TryFixDir "approach.md" + if (-not (Test-Path $approachFile)) { + $approachFile = Join-Path $TryFixDir "approach.txt" + } + if (Test-Path $approachFile) { + $Approach = Get-Content $approachFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Loaded approach from: $approachFile" -ForegroundColor Cyan + } + } + + # Load result from result.txt + if ([string]::IsNullOrWhiteSpace($Status)) { + $resultFile = Join-Path $TryFixDir "result.txt" + if (Test-Path $resultFile) { + $resultContent = (Get-Content $resultFile -Raw -Encoding UTF8).Trim().ToUpper() + $Status = switch -Regex ($resultContent) { + 'PASS' { "Pass" } + 'FAIL' { "Fail" } + 'COMPILES' { "Compiles" } + default { "Fail" } + } + Write-Host "โ„น๏ธ Loaded status: $Status from result.txt" -ForegroundColor Cyan + } + } + + # Load analysis from analysis.md + if ([string]::IsNullOrWhiteSpace($Analysis)) { + $analysisFile = Join-Path $TryFixDir "analysis.md" + if (Test-Path $analysisFile) { + $Analysis = Get-Content $analysisFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Loaded analysis from: $analysisFile" -ForegroundColor Cyan + } + } + + # Load diff from fix.diff + if ([string]::IsNullOrWhiteSpace($CodeSnippet)) { + $diffFile = Join-Path $TryFixDir "fix.diff" + if (Test-Path $diffFile) { + $CodeSnippet = Get-Content $diffFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Loaded code diff from: $diffFile" -ForegroundColor Cyan + } + } + + # Generate FilesChanged from diff if not provided + if ([string]::IsNullOrWhiteSpace($FilesChanged) -and -not [string]::IsNullOrWhiteSpace($CodeSnippet)) { + $files = $CodeSnippet | Select-String -Pattern "^\+\+\+ b/(.+)$" -AllMatches | + ForEach-Object { $_.Matches.Groups[1].Value } | + Sort-Object -Unique + if ($files) { + $FilesChanged = "| File | Type |`n|------|------|`n" + foreach ($file in $files) { + $FilesChanged += "| ``$file`` | Modified |`n" + } + } else { + $FilesChanged = "_See diff above_" + } + } +} + +# If IssueNumber provided but no TryFixDir, try to find all attempts +if ($IssueNumber -gt 0 -and [string]::IsNullOrWhiteSpace($TryFixDir) -and [string]::IsNullOrWhiteSpace($Approach)) { + $tryFixBase = "CustomAgentLogsTmp/PRState/$IssueNumber/try-fix" + if (-not (Test-Path $tryFixBase)) { + $repoRoot = git rev-parse --show-toplevel 2>$null + if ($repoRoot) { + $tryFixBase = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$IssueNumber/try-fix" + } + } + + if (Test-Path $tryFixBase) { + $attemptDirs = Get-ChildItem -Path $tryFixBase -Directory | Where-Object { $_.Name -match '^attempt-\d+$' } | Sort-Object { [int]($_.Name -replace 'attempt-', '') } + if ($attemptDirs.Count -gt 0) { + Write-Host "โ„น๏ธ Found $($attemptDirs.Count) attempt(s) in $tryFixBase" -ForegroundColor Cyan + Write-Host " Posting ALL attempts..." -ForegroundColor Cyan + + # Loop through ALL attempts and recurse for each + foreach ($attemptDir in $attemptDirs) { + Write-Host " Processing: $($attemptDir.Name)" -ForegroundColor Gray + & $PSCommandPath -TryFixDir $attemptDir.FullName -DryRun:$DryRun -PreviewFile:$PreviewFile + } + exit 0 + } + } +} + +# Validate required parameters +if ($IssueNumber -eq 0) { + throw "IssueNumber is required. Provide via -IssueNumber or use -TryFixDir with path containing issue number" +} + +if ($AttemptNumber -eq 0) { + throw "AttemptNumber is required. Provide via -AttemptNumber or use -TryFixDir with path like attempt-N" +} + +if ([string]::IsNullOrWhiteSpace($Approach)) { + throw "Approach is required. Provide via -Approach or create approach.md in TryFixDir" +} + +if ([string]::IsNullOrWhiteSpace($Status)) { + throw "Status is required. Provide via -Status or create result.txt in TryFixDir" +} + +if ([string]::IsNullOrWhiteSpace($FilesChanged)) { + $FilesChanged = "_No files changed information available_" +} + +# Status emoji mapping +$statusEmoji = switch ($Status) { + "Pass" { "โœ…" } + "Fail" { "โŒ" } + "Compiles" { "๐Ÿ”จ" } + default { "โšช" } +} + +# Build the new attempt section - compact format +# Note: blank line after is required for proper markdown rendering +$attemptSection = @" +
+$statusEmoji Fix $AttemptNumber + +"@ + +# Show brief approach description +if (-not [string]::IsNullOrWhiteSpace($Approach)) { + $attemptSection += "$Approach`n`n" +} + +# Only show diff if available +if (-not [string]::IsNullOrWhiteSpace($CodeSnippet)) { + $attemptSection += @" +``````diff +$CodeSnippet +`````` + +"@ +} + +# Show analysis if available (explains why it passed/failed) +if (-not [string]::IsNullOrWhiteSpace($Analysis)) { + $attemptSection += "$Analysis`n" +} + +$attemptSection += @" +
+"@ + +# ============================================================================ +# UNIFIED COMMENT HANDLING +# Uses single comment with section markers +# ============================================================================ + +$MAIN_MARKER = "" +$SECTION_START = "" +$SECTION_END = "" + +Write-Host "`nChecking for existing AI Summary comment on #$IssueNumber..." -ForegroundColor Yellow +$existingComment = $null +$existingBody = "" + +try { + $commentsJson = gh api "repos/dotnet/maui/issues/$IssueNumber/comments" 2>$null + $comments = $commentsJson | ConvertFrom-Json + + foreach ($comment in $comments) { + if ($comment.body -match [regex]::Escape($MAIN_MARKER)) { + $existingComment = $comment + $existingBody = $comment.body + Write-Host "โœ“ Found existing AI Summary comment (ID: $($comment.id))" -ForegroundColor Green + break + } + } + + if (-not $existingComment) { + Write-Host "โœ“ No existing AI Summary comment found - will create new" -ForegroundColor Yellow + } +} catch { + Write-Host "โœ“ No existing AI Summary comment found - will create new" -ForegroundColor Yellow +} + +# Build the try-fix section content +$tryFixHeader = "### ๐Ÿ”ง Try-Fix Analysis`n`n" + +# Extract existing try-fix section to preserve previous attempts +$existingTryFixContent = "" +$startPattern = [regex]::Escape($SECTION_START) +$endPattern = [regex]::Escape($SECTION_END) +if ($existingBody -match "(?s)$startPattern(.*?)$endPattern") { + $existingTryFixContent = $Matches[1].Trim() +} + +# Check if this attempt number already exists and replace it, or add new +# Match both old format (Attempt N:) and new format (Fix N) +$attemptPattern = "(?s)
\s*.*?(Attempt $AttemptNumber`:|Fix $AttemptNumber).*?
" +if ($existingTryFixContent -match $attemptPattern) { + Write-Host "Replacing existing Fix $AttemptNumber..." -ForegroundColor Yellow + $tryFixContent = $existingTryFixContent -replace $attemptPattern, $attemptSection +} elseif (-not [string]::IsNullOrWhiteSpace($existingTryFixContent)) { + Write-Host "Adding new Fix $AttemptNumber..." -ForegroundColor Yellow + # Remove header if present to avoid duplication + $existingTryFixContent = $existingTryFixContent -replace "^### ๐Ÿ”ง (Try-Fix Analysis|Fix Attempts)\s*`n*", "" + $tryFixContent = $tryFixHeader + $existingTryFixContent.TrimEnd() + "`n`n" + $attemptSection +} else { + Write-Host "Creating first fix..." -ForegroundColor Yellow + $tryFixContent = $tryFixHeader + $attemptSection +} + +# Build the section with markers +$tryFixSection = @" +$SECTION_START +$tryFixContent +$SECTION_END +"@ + +if ($existingComment) { + # Update existing comment - replace or add try-fix section + if ($existingBody -match "(?s)$startPattern.*?$endPattern") { + # Replace existing try-fix section + $commentBody = $existingBody -replace "(?s)$startPattern.*?$endPattern", $tryFixSection + } else { + # Add try-fix section before footer + $footerPattern = "(?s)(---\s*\n+.*?\s*)$" + if ($existingBody -match $footerPattern) { + $commentBody = $existingBody -replace $footerPattern, "`n$tryFixSection`n`n`$1" + } else { + $commentBody = $existingBody.TrimEnd() + "`n`n$tryFixSection" + } + } +} else { + # Create new unified comment + $commentBody = @" +$MAIN_MARKER + +## ๐Ÿค– AI Summary + +$tryFixSection +"@ +} + +if ($DryRun) { + # File-based DryRun: mirrors GitHub comment behavior using a local file + if ([string]::IsNullOrWhiteSpace($PreviewFile)) { + $PreviewFile = "CustomAgentLogsTmp/PRState/$IssueNumber/ai-summary-comment-preview.md" + } + + # Ensure directory exists + $previewDir = Split-Path $PreviewFile -Parent + if (-not (Test-Path $previewDir)) { + New-Item -ItemType Directory -Path $previewDir -Force | Out-Null + } + + # Read existing preview file (mimics reading existing GitHub comment) + $existingPreview = "" + if (Test-Path $PreviewFile) { + $existingPreview = Get-Content $PreviewFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Updating existing preview file: $PreviewFile" -ForegroundColor Cyan + } else { + Write-Host "โ„น๏ธ Creating new preview file: $PreviewFile" -ForegroundColor Cyan + } + + # Update or insert the TRY-FIX section + $TRY_FIX_MARKER = "" + $TRY_FIX_END_MARKER = "" + + if ($existingPreview -match [regex]::Escape($TRY_FIX_MARKER)) { + # Replace existing TRY-FIX section + $pattern = [regex]::Escape($TRY_FIX_MARKER) + "[\s\S]*?" + [regex]::Escape($TRY_FIX_END_MARKER) + $finalComment = $existingPreview -replace $pattern, $tryFixSection + } elseif (-not [string]::IsNullOrWhiteSpace($existingPreview)) { + # Append TRY-FIX section to existing content + $finalComment = $existingPreview.TrimEnd() + "`n`n" + $tryFixSection + } else { + # New file - use full comment body + $finalComment = $commentBody + } + + # Write to preview file + Set-Content -Path $PreviewFile -Value "$($finalComment.TrimEnd())`n" -Encoding UTF8 -NoNewline + + Write-Host "`n=== COMMENT PREVIEW ===" -ForegroundColor Yellow + Write-Host $finalComment + Write-Host "`n=== END PREVIEW ===" -ForegroundColor Yellow + Write-Host "`nโœ… Preview saved to: $PreviewFile" -ForegroundColor Green + Write-Host " Run 'open $PreviewFile' to view in editor" -ForegroundColor Gray + exit 0 +} + +# Write to temp file to avoid shell escaping issues +$tempFile = [System.IO.Path]::GetTempFileName() +@{ body = $commentBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + +if ($existingComment) { + Write-Host "Updating comment ID $($existingComment.id)..." -ForegroundColor Yellow + $result = gh api --method PATCH "repos/dotnet/maui/issues/comments/$($existingComment.id)" --input $tempFile --jq '.html_url' + Write-Host "โœ… Comment updated: $result" -ForegroundColor Green +} else { + Write-Host "Posting new comment to issue #$IssueNumber..." -ForegroundColor Yellow + $result = gh api --method POST "repos/dotnet/maui/issues/$IssueNumber/comments" --input $tempFile --jq '.html_url' + Write-Host "โœ… Comment posted: $result" -ForegroundColor Green +} + +Remove-Item $tempFile diff --git a/.github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 new file mode 100644 index 000000000000..b2803aa143a2 --- /dev/null +++ b/.github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 @@ -0,0 +1,333 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Posts or updates the verification test results in the unified AI Summary comment. + +.DESCRIPTION + Reads verification report from CustomAgentLogsTmp and posts/updates + the VERIFY-TESTS section in the unified AI Summary comment. + + Uses the same marker and + section markers as other comment scripts. + +.PARAMETER PRNumber + The PR number to post comment on (required unless ReportFile provided) + +.PARAMETER ReportFile + Path to verification-report.md file. If not provided, auto-discovers from + CustomAgentLogsTmp/PRState/{PRNumber}/verify-tests-fail/verification-report.md + +.PARAMETER Status + Overall verification status: "Passed", "Failed" (auto-detected from report if not provided) + +.PARAMETER Platform + Platform tested (auto-detected from report if not provided) + +.PARAMETER Mode + Verification mode: "FailureOnly", "FullVerification" (auto-detected from report if not provided) + +.PARAMETER Summary + Brief summary of results (auto-generated if not provided) + +.PARAMETER DryRun + Print comment instead of posting + +.EXAMPLE + # Simplest: Just provide PR number (auto-loads from CustomAgentLogsTmp) + ./post-verify-tests-comment.ps1 -PRNumber 27246 + +.EXAMPLE + # Or provide report file path + ./post-verify-tests-comment.ps1 -ReportFile CustomAgentLogsTmp/PRState/27246/verify-tests-fail/verification-report.md + +.EXAMPLE + # Manual parameters + ./post-verify-tests-comment.ps1 -PRNumber 27246 -Status "Passed" -Platform "android" -Mode "FullVerification" +#> + +param( + [Parameter(Mandatory=$false)] + [int]$PRNumber, + + [Parameter(Mandatory=$false)] + [string]$ReportFile, + + [Parameter(Mandatory=$false)] + [ValidateSet("Passed", "Failed", "")] + [string]$Status, + + [Parameter(Mandatory=$false)] + [string]$Platform, + + [Parameter(Mandatory=$false)] + [ValidateSet("FailureOnly", "FullVerification", "")] + [string]$Mode, + + [Parameter(Mandatory=$false)] + [string]$Summary, + + [Parameter(Mandatory=$false)] + [switch]$DryRun, + + [Parameter(Mandatory=$false)] + [string]$PreviewFile +) + +$ErrorActionPreference = "Stop" + +Write-Host "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" -ForegroundColor Cyan +Write-Host "โ•‘ Verify-Tests Comment (Post/Update) โ•‘" -ForegroundColor Cyan +Write-Host "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" -ForegroundColor Cyan + +# ============================================================================ +# AUTO-DISCOVERY FROM REPORT FILE +# ============================================================================ + +# If PRNumber provided but no ReportFile, try to find it +if ($PRNumber -gt 0 -and [string]::IsNullOrWhiteSpace($ReportFile)) { + $reportPath = "CustomAgentLogsTmp/PRState/$PRNumber/verify-tests-fail/verification-report.md" + if (-not (Test-Path $reportPath)) { + $repoRoot = git rev-parse --show-toplevel 2>$null + if ($repoRoot) { + $reportPath = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/verify-tests-fail/verification-report.md" + } + } + + if (Test-Path $reportPath) { + $ReportFile = $reportPath + Write-Host "โ„น๏ธ Auto-discovered report file: $ReportFile" -ForegroundColor Cyan + } +} + +# If ReportFile provided, parse it +if (-not [string]::IsNullOrWhiteSpace($ReportFile)) { + if (-not (Test-Path $ReportFile)) { + throw "Report file not found: $ReportFile" + } + + $reportContent = Get-Content $ReportFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Loading from report file: $ReportFile" -ForegroundColor Cyan + + # Extract PRNumber from path if not provided + if ($PRNumber -eq 0 -and $ReportFile -match '[/\\](\d+)[/\\]verify-tests-fail') { + $PRNumber = [int]$Matches[1] + Write-Host "โ„น๏ธ Auto-detected PRNumber: $PRNumber from path" -ForegroundColor Cyan + } + + # Extract Status from report + if ([string]::IsNullOrWhiteSpace($Status)) { + if ($reportContent -match 'VERIFICATION PASSED|โœ…\s*PASSED') { + $Status = "Passed" + } elseif ($reportContent -match 'VERIFICATION FAILED|โŒ\s*FAILED') { + $Status = "Failed" + } else { + $Status = "Unknown" + } + Write-Host "โ„น๏ธ Detected Status: $Status" -ForegroundColor Cyan + } + + # Extract Platform from report + if ([string]::IsNullOrWhiteSpace($Platform)) { + if ($reportContent -match 'Platform[:\s]+(\w+)') { + $Platform = $Matches[1] + } elseif ($reportContent -match '(android|ios|catalyst|windows)' ) { + $Platform = $Matches[1] + } + if ($Platform) { + Write-Host "โ„น๏ธ Detected Platform: $Platform" -ForegroundColor Cyan + } + } + + # Extract Mode from report + if ([string]::IsNullOrWhiteSpace($Mode)) { + if ($reportContent -match 'Full Verification|FAIL without fix.*PASS with fix') { + $Mode = "FullVerification" + } elseif ($reportContent -match 'Verify Failure Only|tests.*FAILED as expected') { + $Mode = "FailureOnly" + } + if ($Mode) { + Write-Host "โ„น๏ธ Detected Mode: $Mode" -ForegroundColor Cyan + } + } + + # Use report content as summary if not provided + if ([string]::IsNullOrWhiteSpace($Summary)) { + # Extract key results from report + $Summary = $reportContent + } +} + +# Validate required parameters +if ($PRNumber -eq 0) { + throw "PRNumber is required. Provide via -PRNumber or use -ReportFile with path containing PR number" +} + +if ([string]::IsNullOrWhiteSpace($Status)) { + throw "Status is required. Provide via -Status or use -ReportFile with verification results" +} + +# Generate summary if not provided +if ([string]::IsNullOrWhiteSpace($Summary)) { + $statusEmoji = if ($Status -eq "Passed") { "โœ…" } else { "โŒ" } + $modeDesc = if ($Mode -eq "FullVerification") { "Full verification (FAIL without fix, PASS with fix)" } else { "Failure only (tests FAIL as expected)" } + $Summary = @" +**Status**: $statusEmoji $Status + +**Mode**: $modeDesc +**Platform**: $Platform + +_Run `verify-tests-fail.ps1` for full details._ +"@ +} + +# Status emoji +$statusEmoji = if ($Status -eq "Passed") { "โœ… PASSED" } else { "โŒ FAILED" } +$modeDesc = if ($Mode -eq "FullVerification") { "Full Verification" } else { "Failure Only" } + +# Build verification section content +$verifyContent = @" +### ๐Ÿšฆ Test Verification + +**Result**: $statusEmoji +**Mode**: $modeDesc +**Platform**: $Platform + +
+Expand Details + +$Summary + +
+"@ + +# ============================================================================ +# UNIFIED COMMENT HANDLING +# Uses single comment with section markers +# ============================================================================ + +$MAIN_MARKER = "" +$SECTION_START = "" +$SECTION_END = "" + +Write-Host "`nChecking for existing AI Summary comment on #$PRNumber..." -ForegroundColor Yellow +$existingComment = $null +$existingBody = "" + +try { + $commentsJson = gh api "repos/dotnet/maui/issues/$PRNumber/comments" 2>$null + $comments = $commentsJson | ConvertFrom-Json + + foreach ($comment in $comments) { + if ($comment.body -match [regex]::Escape($MAIN_MARKER)) { + $existingComment = $comment + $existingBody = $comment.body + Write-Host "โœ“ Found existing AI Summary comment (ID: $($comment.id))" -ForegroundColor Green + break + } + } + + if (-not $existingComment) { + Write-Host "โœ“ No existing AI Summary comment found - will create new" -ForegroundColor Yellow + } +} catch { + Write-Host "โœ“ No existing AI Summary comment found - will create new" -ForegroundColor Yellow +} + +# Build the section with markers +$verifySection = @" +$SECTION_START +$verifyContent +$SECTION_END +"@ + +$startPattern = [regex]::Escape($SECTION_START) +$endPattern = [regex]::Escape($SECTION_END) + +if ($existingComment) { + # Update existing comment - replace or add verify-tests section + if ($existingBody -match "(?s)$startPattern.*?$endPattern") { + # Replace existing verify-tests section + $commentBody = $existingBody -replace "(?s)$startPattern.*?$endPattern", $verifySection + } else { + # Add verify-tests section before footer + $footerPattern = "(?s)(---\s*\n+.*?\s*)$" + if ($existingBody -match $footerPattern) { + $commentBody = $existingBody -replace $footerPattern, "`n$verifySection`n`n`$1" + } else { + $commentBody = $existingBody.TrimEnd() + "`n`n$verifySection" + } + } +} else { + # Create new unified comment + $commentBody = @" +$MAIN_MARKER + +## ๐Ÿค– AI Summary + +$verifySection +"@ +} + +if ($DryRun) { + # File-based DryRun: mirrors GitHub comment behavior using a local file + if ([string]::IsNullOrWhiteSpace($PreviewFile)) { + $PreviewFile = "CustomAgentLogsTmp/PRState/$PRNumber/ai-summary-comment-preview.md" + } + + # Ensure directory exists + $previewDir = Split-Path $PreviewFile -Parent + if (-not (Test-Path $previewDir)) { + New-Item -ItemType Directory -Path $previewDir -Force | Out-Null + } + + # Read existing preview file (mimics reading existing GitHub comment) + $existingPreview = "" + if (Test-Path $PreviewFile) { + $existingPreview = Get-Content $PreviewFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Updating existing preview file: $PreviewFile" -ForegroundColor Cyan + } else { + Write-Host "โ„น๏ธ Creating new preview file: $PreviewFile" -ForegroundColor Cyan + } + + # Update or insert the VERIFY-TESTS section + $VERIFY_MARKER = "" + $VERIFY_END_MARKER = "" + + if ($existingPreview -match [regex]::Escape($VERIFY_MARKER)) { + # Replace existing VERIFY-TESTS section + $pattern = [regex]::Escape($VERIFY_MARKER) + "[\s\S]*?" + [regex]::Escape($VERIFY_END_MARKER) + $finalComment = $existingPreview -replace $pattern, $verifySection + } elseif (-not [string]::IsNullOrWhiteSpace($existingPreview)) { + # Append VERIFY-TESTS section to existing content + $finalComment = $existingPreview.TrimEnd() + "`n`n" + $verifySection + } else { + # New file - use full comment body + $finalComment = $commentBody + } + + # Write to preview file + Set-Content -Path $PreviewFile -Value "$($finalComment.TrimEnd())`n" -Encoding UTF8 -NoNewline + + Write-Host "`n=== COMMENT PREVIEW ===" -ForegroundColor Yellow + Write-Host $finalComment + Write-Host "`n=== END PREVIEW ===" -ForegroundColor Yellow + Write-Host "`nโœ… Preview saved to: $PreviewFile" -ForegroundColor Green + Write-Host " Run 'open $PreviewFile' to view in editor" -ForegroundColor Gray + exit 0 +} + +# Write to temp file to avoid shell escaping issues +$tempFile = [System.IO.Path]::GetTempFileName() +@{ body = $commentBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + +if ($existingComment) { + Write-Host "Updating comment ID $($existingComment.id)..." -ForegroundColor Yellow + $result = gh api --method PATCH "repos/dotnet/maui/issues/comments/$($existingComment.id)" --input $tempFile --jq '.html_url' + Write-Host "โœ… Comment updated: $result" -ForegroundColor Green +} else { + Write-Host "Posting new comment to PR #$PRNumber..." -ForegroundColor Yellow + $result = gh api --method POST "repos/dotnet/maui/issues/$PRNumber/comments" --input $tempFile --jq '.html_url' + Write-Host "โœ… Comment posted: $result" -ForegroundColor Green +} + +Remove-Item $tempFile diff --git a/.github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 b/.github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 new file mode 100644 index 000000000000..fb3125529c99 --- /dev/null +++ b/.github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 @@ -0,0 +1,487 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Posts or updates a write-tests comment on a GitHub Issue or Pull Request. + +.DESCRIPTION + Creates ONE comment for all test-writing attempts with each attempt in a collapsible section. + Uses HTML marker for identification. + + **NEW: Auto-loads from CustomAgentLogsTmp/PRState/{IssueNumber}/write-tests/** + + If an existing write-tests comment exists, it will be EDITED with the new attempt added. + Otherwise, a new comment will be created. + + Format: + ## ๐Ÿงช Test Writing for Issue #XXXXX + + +
+ Attempt 1: Test description โœ… Verified + + ... test details ... +
+ +
+ Attempt 2: Different approach โŒ Failed + + ... test details ... +
+ +.PARAMETER IssueNumber + The issue number to post comment on (required unless TestDir provided) + +.PARAMETER AttemptNumber + The attempt number (1, 2, 3, etc.) - auto-detected from TestDir if not specified + +.PARAMETER TestDir + Path to test attempt directory (e.g., CustomAgentLogsTmp/PRState/27246/write-tests/attempt-1) + If provided, all parameters are auto-loaded from files in this directory + +.PARAMETER TestDescription + Brief description of what the test verifies (required unless loading from TestDir) + +.PARAMETER HostAppFile + Path to the HostApp test page file (required unless loading from TestDir) + +.PARAMETER TestFile + Path to the NUnit test file (required unless loading from TestDir) + +.PARAMETER TestMethod + Name of the test method (required unless loading from TestDir) + +.PARAMETER Category + UITestCategories category used (required unless loading from TestDir) + +.PARAMETER VerificationStatus + Status: "Verified" (tests fail without fix), "Failed" (tests don't catch bug), "Unverified" (not yet run) (required unless loading from TestDir) + +.PARAMETER Platforms + Platforms the test runs on (e.g., "All", "iOS, Android") (optional) + +.PARAMETER Notes + Additional notes about the test (optional) + +.PARAMETER DryRun + Print comment instead of posting + +.EXAMPLE + # Simplest: Just provide test directory (all info auto-loaded) + ./post-write-tests-comment.ps1 -TestDir CustomAgentLogsTmp/PRState/27246/write-tests/attempt-1 + +.EXAMPLE + # Or just provide issue number (auto-discovers latest attempt) + ./post-write-tests-comment.ps1 -IssueNumber 27246 + +.EXAMPLE + # Manual parameters (legacy) + ./post-write-tests-comment.ps1 -IssueNumber 33331 -AttemptNumber 1 ` + -TestDescription "Verifies Picker.IsOpen property changes correctly" ` + -HostAppFile "src/Controls/tests/TestCases.HostApp/Issues/Issue33331.cs" ` + -TestFile "src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33331.cs" ` + -TestMethod "PickerIsOpenPropertyChanges" ` + -Category "Picker" ` + -VerificationStatus "Verified" +#> + +param( + [Parameter(Mandatory=$false)] + [int]$IssueNumber, + + [Parameter(Mandatory=$false)] + [int]$AttemptNumber, + + [Parameter(Mandatory=$false)] + [string]$TestDir, + + [Parameter(Mandatory=$false)] + [string]$TestDescription, + + [Parameter(Mandatory=$false)] + [string]$HostAppFile, + + [Parameter(Mandatory=$false)] + [string]$TestFile, + + [Parameter(Mandatory=$false)] + [string]$TestMethod, + + [Parameter(Mandatory=$false)] + [string]$Category, + + [Parameter(Mandatory=$false)] + [ValidateSet("Verified", "Failed", "Unverified", "")] + [string]$VerificationStatus, + + [Parameter(Mandatory=$false)] + [string]$Platforms = "All", + + [Parameter(Mandatory=$false)] + [string]$Notes, + + [Parameter(Mandatory=$false)] + [switch]$DryRun, + + [Parameter(Mandatory=$false)] + [string]$PreviewFile +) + +$ErrorActionPreference = "Stop" + +Write-Host "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" -ForegroundColor Cyan +Write-Host "โ•‘ Write-Tests Comment (Post/Update) โ•‘" -ForegroundColor Cyan +Write-Host "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" -ForegroundColor Cyan + +# ============================================================================ +# AUTO-DISCOVERY FROM DIRECTORIES +# ============================================================================ + +# If TestDir provided, load everything from there +if (-not [string]::IsNullOrWhiteSpace($TestDir)) { + if (-not (Test-Path $TestDir)) { + throw "Test directory not found: $TestDir" + } + + # Extract IssueNumber from path (e.g., CustomAgentLogsTmp/PRState/27246/write-tests/attempt-1) + if ($TestDir -match '[/\\](\d+)[/\\]write-tests') { + if ($IssueNumber -eq 0) { + $IssueNumber = [int]$Matches[1] + Write-Host "โ„น๏ธ Auto-detected IssueNumber: $IssueNumber from path" -ForegroundColor Cyan + } + } + + # Extract AttemptNumber from path (e.g., attempt-1) + if ($TestDir -match 'attempt-(\d+)$') { + if ($AttemptNumber -eq 0) { + $AttemptNumber = [int]$Matches[1] + Write-Host "โ„น๏ธ Auto-detected AttemptNumber: $AttemptNumber from path" -ForegroundColor Cyan + } + } + + # Load test description from description.md or description.txt + if ([string]::IsNullOrWhiteSpace($TestDescription)) { + $descFile = Join-Path $TestDir "description.md" + if (-not (Test-Path $descFile)) { + $descFile = Join-Path $TestDir "description.txt" + } + if (Test-Path $descFile) { + $TestDescription = (Get-Content $descFile -Raw -Encoding UTF8).Trim() + Write-Host "โ„น๏ธ Loaded description from: $descFile" -ForegroundColor Cyan + } + } + + # Load test info from test-info.json or test-info.txt + $infoFile = Join-Path $TestDir "test-info.json" + if (Test-Path $infoFile) { + $info = Get-Content $infoFile -Raw | ConvertFrom-Json + if ([string]::IsNullOrWhiteSpace($HostAppFile) -and $info.HostAppFile) { $HostAppFile = $info.HostAppFile } + if ([string]::IsNullOrWhiteSpace($TestFile) -and $info.TestFile) { $TestFile = $info.TestFile } + if ([string]::IsNullOrWhiteSpace($TestMethod) -and $info.TestMethod) { $TestMethod = $info.TestMethod } + if ([string]::IsNullOrWhiteSpace($Category) -and $info.Category) { $Category = $info.Category } + Write-Host "โ„น๏ธ Loaded test info from: $infoFile" -ForegroundColor Cyan + } + + # Load verification status from result.txt + if ([string]::IsNullOrWhiteSpace($VerificationStatus)) { + $resultFile = Join-Path $TestDir "result.txt" + if (Test-Path $resultFile) { + $resultContent = (Get-Content $resultFile -Raw -Encoding UTF8).Trim().ToUpper() + $VerificationStatus = switch -Regex ($resultContent) { + 'VERIFIED' { "Verified" } + 'PASS' { "Verified" } + 'FAILED' { "Failed" } + 'FAIL' { "Failed" } + default { "Unverified" } + } + Write-Host "โ„น๏ธ Loaded status: $VerificationStatus from result.txt" -ForegroundColor Cyan + } + } + + # Load notes from notes.md + if ([string]::IsNullOrWhiteSpace($Notes)) { + $notesFile = Join-Path $TestDir "notes.md" + if (Test-Path $notesFile) { + $Notes = Get-Content $notesFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Loaded notes from: $notesFile" -ForegroundColor Cyan + } + } +} + +# If IssueNumber provided but no TestDir, try to find all attempts +if ($IssueNumber -gt 0 -and [string]::IsNullOrWhiteSpace($TestDir) -and [string]::IsNullOrWhiteSpace($TestDescription)) { + $testBase = "CustomAgentLogsTmp/PRState/$IssueNumber/write-tests" + if (-not (Test-Path $testBase)) { + $repoRoot = git rev-parse --show-toplevel 2>$null + if ($repoRoot) { + $testBase = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$IssueNumber/write-tests" + } + } + + if (Test-Path $testBase) { + $attemptDirs = Get-ChildItem -Path $testBase -Directory | Where-Object { $_.Name -match '^attempt-\d+$' } | Sort-Object Name + if ($attemptDirs.Count -gt 0) { + Write-Host "โ„น๏ธ Found $($attemptDirs.Count) attempt(s) in $testBase" -ForegroundColor Cyan + Write-Host " Use -TestDir to post a specific attempt, or posting latest..." -ForegroundColor Cyan + + # Post the latest attempt + $latestAttempt = $attemptDirs | Sort-Object { [int]($_.Name -replace 'attempt-', '') } | Select-Object -Last 1 + $TestDir = $latestAttempt.FullName + + # Recurse with the discovered directory + & $PSCommandPath -TestDir $TestDir -DryRun:$DryRun + exit 0 + } + } +} + +# Validate required parameters +if ($IssueNumber -eq 0) { + throw "IssueNumber is required. Provide via -IssueNumber or use -TestDir with path containing issue number" +} + +if ($AttemptNumber -eq 0) { + throw "AttemptNumber is required. Provide via -AttemptNumber or use -TestDir with path like attempt-N" +} + +if ([string]::IsNullOrWhiteSpace($TestDescription)) { + throw "TestDescription is required. Provide via -TestDescription or create description.md in TestDir" +} + +if ([string]::IsNullOrWhiteSpace($HostAppFile)) { + throw "HostAppFile is required. Provide via -HostAppFile or add to test-info.json in TestDir" +} + +if ([string]::IsNullOrWhiteSpace($TestFile)) { + throw "TestFile is required. Provide via -TestFile or add to test-info.json in TestDir" +} + +if ([string]::IsNullOrWhiteSpace($TestMethod)) { + throw "TestMethod is required. Provide via -TestMethod or add to test-info.json in TestDir" +} + +if ([string]::IsNullOrWhiteSpace($Category)) { + throw "Category is required. Provide via -Category or add to test-info.json in TestDir" +} + +if ([string]::IsNullOrWhiteSpace($VerificationStatus)) { + $VerificationStatus = "Unverified" +} + +# Status emoji mapping +$statusEmoji = switch ($VerificationStatus) { + "Verified" { "โœ… Verified" } + "Failed" { "โŒ Failed" } + "Unverified" { "โณ Unverified" } + default { $VerificationStatus } +} + +# Build the new attempt section (collapsible) +$attemptSection = @" +
+Attempt $AttemptNumber`: $TestDescription $statusEmoji + + +### Test Details + +| Property | Value | +|----------|-------| +| **Test Method** | ``$TestMethod`` | +| **Category** | ``UITestCategories.$Category`` | +| **Platforms** | $Platforms | +| **Status** | $statusEmoji | + +### Files Created + +
+๐Ÿ“„ HostApp Test Page - Click to expand code + +**File:** ``$HostAppFile`` + +``````csharp +$(if (Test-Path $HostAppFile) { Get-Content $HostAppFile -Raw } else { "File not found: $HostAppFile" }) +`````` + +
+ +
+๐Ÿงช NUnit Test - Click to expand code + +**File:** ``$TestFile`` + +``````csharp +$(if (Test-Path $TestFile) { Get-Content $TestFile -Raw } else { "File not found: $TestFile" }) +`````` + +
+ +"@ + +if (-not [string]::IsNullOrWhiteSpace($Notes)) { + $attemptSection += @" + +### Notes + +$Notes + +"@ +} + +$attemptSection += @" +
+"@ + +# ============================================================================ +# UNIFIED COMMENT HANDLING +# Uses single comment with section markers +# ============================================================================ + +$MAIN_MARKER = "" +$SECTION_START = "" +$SECTION_END = "" + +Write-Host "`nChecking for existing AI Summary comment on #$IssueNumber..." -ForegroundColor Yellow +$existingComment = $null +$existingBody = "" + +try { + $commentsJson = gh api "repos/dotnet/maui/issues/$IssueNumber/comments" 2>$null + $comments = $commentsJson | ConvertFrom-Json + + foreach ($comment in $comments) { + if ($comment.body -match [regex]::Escape($MAIN_MARKER)) { + $existingComment = $comment + $existingBody = $comment.body + Write-Host "โœ“ Found existing AI Summary comment (ID: $($comment.id))" -ForegroundColor Green + break + } + } + + if (-not $existingComment) { + Write-Host "โœ“ No existing AI Summary comment found - will create new" -ForegroundColor Yellow + } +} catch { + Write-Host "โœ“ No existing AI Summary comment found - will create new" -ForegroundColor Yellow +} + +# Build the write-tests section content +$writeTestsHeader = "### ๐Ÿงช Test Writing`n`n" + +# Extract existing write-tests section to preserve previous attempts +$existingWriteTestsContent = "" +$startPattern = [regex]::Escape($SECTION_START) +$endPattern = [regex]::Escape($SECTION_END) +if ($existingBody -match "(?s)$startPattern(.*?)$endPattern") { + $existingWriteTestsContent = $Matches[1].Trim() +} + +# Check if this attempt number already exists and replace it, or add new +$attemptPattern = "(?s)
\s*Attempt $AttemptNumber`:.*?
" +if ($existingWriteTestsContent -match $attemptPattern) { + Write-Host "Replacing existing Attempt $AttemptNumber..." -ForegroundColor Yellow + $tryFixContent = $existingWriteTestsContent -replace $attemptPattern, $attemptSection +} elseif (-not [string]::IsNullOrWhiteSpace($existingWriteTestsContent)) { + Write-Host "Adding new Attempt $AttemptNumber..." -ForegroundColor Yellow + # Remove header if present to avoid duplication + $existingWriteTestsContent = $existingWriteTestsContent -replace "^### ๐Ÿงช Test Writing\s*`n*", "" + $writeTestsContent = $writeTestsHeader + $existingWriteTestsContent.TrimEnd() + "`n`n" + $attemptSection +} else { + Write-Host "Creating first attempt..." -ForegroundColor Yellow + $writeTestsContent = $writeTestsHeader + $attemptSection +} + +# Build the section with markers +$writeTestsSection = @" +$SECTION_START +$writeTestsContent +$SECTION_END +"@ + +if ($existingComment) { + # Update existing comment - replace or add write-tests section + if ($existingBody -match "(?s)$startPattern.*?$endPattern") { + # Replace existing write-tests section + $commentBody = $existingBody -replace "(?s)$startPattern.*?$endPattern", $writeTestsSection + } else { + # Add write-tests section before footer + $footerPattern = "(?s)(---\s*\n+.*?\s*)$" + if ($existingBody -match $footerPattern) { + $commentBody = $existingBody -replace $footerPattern, "`n$writeTestsSection`n`n`$1" + } else { + $commentBody = $existingBody.TrimEnd() + "`n`n$writeTestsSection" + } + } +} else { + # Create new unified comment + $commentBody = @" +$MAIN_MARKER + +## ๐Ÿค– AI Summary + +$writeTestsSection +"@ +} + +if ($DryRun) { + # File-based DryRun: mirrors GitHub comment behavior using a local file + if ([string]::IsNullOrWhiteSpace($PreviewFile)) { + $PreviewFile = "CustomAgentLogsTmp/PRState/$IssueNumber/ai-summary-comment-preview.md" + } + + # Ensure directory exists + $previewDir = Split-Path $PreviewFile -Parent + if (-not (Test-Path $previewDir)) { + New-Item -ItemType Directory -Path $previewDir -Force | Out-Null + } + + # Read existing preview file + $existingPreview = "" + if (Test-Path $PreviewFile) { + $existingPreview = Get-Content $PreviewFile -Raw -Encoding UTF8 + Write-Host "โ„น๏ธ Updating existing preview file: $PreviewFile" -ForegroundColor Cyan + } else { + Write-Host "โ„น๏ธ Creating new preview file: $PreviewFile" -ForegroundColor Cyan + } + + # Update or insert the WRITE-TESTS section + $sectionMarker = "" + $sectionEndMarker = "" + $wrappedSection = "$sectionMarker`n$writeTestsSection`n$sectionEndMarker" + + if ($existingPreview -match [regex]::Escape($sectionMarker)) { + # Replace existing WRITE-TESTS section + $pattern = [regex]::Escape($sectionMarker) + "[\s\S]*?" + [regex]::Escape($sectionEndMarker) + $finalComment = $existingPreview -replace $pattern, $wrappedSection + } elseif (-not [string]::IsNullOrWhiteSpace($existingPreview)) { + # Append WRITE-TESTS section to existing content + $finalComment = $existingPreview.TrimEnd() + "`n`n" + $wrappedSection + } else { + # New file - use full comment body with section markers + $finalComment = $commentBody.Replace($writeTestsSection, $wrappedSection) + } + + # Write to preview file + Set-Content -Path $PreviewFile -Value "$($finalComment.TrimEnd())`n" -Encoding UTF8 -NoNewline + + Write-Host "`n=== COMMENT PREVIEW ===" -ForegroundColor Yellow + Write-Host $finalComment + Write-Host "`n=== END PREVIEW ===" -ForegroundColor Yellow + Write-Host "`nโœ… Preview saved to: $PreviewFile" -ForegroundColor Green + Write-Host " Run 'open $PreviewFile' to view in editor" -ForegroundColor Gray + exit 0 +} + +# Write to temp file to avoid shell escaping issues +$tempFile = [System.IO.Path]::GetTempFileName() +@{ body = $commentBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + +if ($existingComment) { + Write-Host "Updating comment ID $($existingComment.id)..." -ForegroundColor Yellow + $result = gh api --method PATCH "repos/dotnet/maui/issues/comments/$($existingComment.id)" --input $tempFile --jq '.html_url' + Write-Host "โœ… Comment updated: $result" -ForegroundColor Green +} else { + Write-Host "Posting new comment to issue #$IssueNumber..." -ForegroundColor Yellow + $result = gh api --method POST "repos/dotnet/maui/issues/$IssueNumber/comments" --input $tempFile --jq '.html_url' + Write-Host "โœ… Comment posted: $result" -ForegroundColor Green +} + +Remove-Item $tempFile diff --git a/.github/skills/pr-finalize/SKILL.md b/.github/skills/pr-finalize/SKILL.md index 7310f356d465..d99df5da8e72 100644 --- a/.github/skills/pr-finalize/SKILL.md +++ b/.github/skills/pr-finalize/SKILL.md @@ -9,38 +9,6 @@ Ensures PR title and description accurately reflect the implementation for a goo **Standalone skill** - Can be used on any PR, not just PRs created by the pr agent. ---- - -## ๐Ÿšจ MANDATORY: Save Summary File - -**This is NOT optional. Complete this step EVERY time you run this skill.** - -Before presenting results to the user, you MUST save the summary: - -```bash -# Create directory -mkdir -p CustomAgentLogsTmp/PRState//pr-finalize - -# Save summary to: CustomAgentLogsTmp/PRState//pr-finalize/pr-finalize-summary.md -``` - -**The summary file must include:** -- PR number, title, author, date reviewed -- Verdict (โœ… No Changes Needed / โš ๏ธ Needs Updates) -- Title assessment (current vs recommended) -- Description quality assessment with table -- Issues found table -- Changed files coverage -- Recommended description (if changes needed) -- Action items checklist - -**Why this matters:** Skills are complete workflows, not just reference material. Saving the summary ensures: -1. Traceability of what was reviewed -2. User can reference findings later -3. Consistent output format across runs - ---- - ## Core Principle: Preserve Quality **Review existing description BEFORE suggesting changes.** Many PR authors write excellent, detailed descriptions. Your job is to: