diff --git a/.github/agents/learn-from-pr.md b/.github/agents/learn-from-pr.md index ce0a506c3af3..759d1db88328 100644 --- a/.github/agents/learn-from-pr.md +++ b/.github/agents/learn-from-pr.md @@ -89,7 +89,6 @@ Present a summary: | Situation | Action | |-----------|--------| | PR not found | Ask user to verify PR number | -| No session markdown | Proceed with PR diff analysis only | | Target file doesn't exist | Create if instruction/architecture doc, skip if code | | Duplicate content exists | Skip, note in report | | Unclear where to add | Ask user for guidance | diff --git a/.github/agents/pr.md b/.github/agents/pr.md index d5c55dbc80de..e4fdc4163c73 100644 --- a/.github/agents/pr.md +++ b/.github/agents/pr.md @@ -1,6 +1,6 @@ --- name: pr -description: Sequential 4-phase workflow for GitHub issues - Pre-Flight, Gate, Fix, Report. Phases MUST complete in order. State tracked in CustomAgentLogsTmp/PRState/ +description: Sequential 4-phase workflow for GitHub issues - Pre-Flight, Gate, Fix, Report. Phases MUST complete in order. --- # .NET MAUI Pull Request Agent @@ -48,13 +48,13 @@ After Gate passes, read `.github/agents/pr/post-gate.md` for **Phases 3-4**. - Follow Templates EXACTLY (no `open` attributes, no "improvements") - No Direct Git Commands (use `gh pr diff/view`, let scripts handle files) - Use Skills' Scripts (don't bypass with manual commands) -- Stop on Environment Blockers (strict retry limits, report and ask user) +- Stop on Environment Blockers (retry once, then skip and continue autonomously) - Multi-Model Configuration (5 models for Phase 4) - Platform Selection (must be affected AND available on host) **Key points:** - ❌ Never run `git checkout`, `git switch`, `git stash`, `git reset` - agent is always on correct branch -- ❌ Never continue after environment blocker - STOP and ask user +- ❌ Never stop and ask user - use best judgment to skip blocked phases and continue - ❌ Never mark phase ✅ with [PENDING] fields remaining Phase 3 uses a 5-model exploration workflow. See `post-gate.md` for detailed instructions after Gate passes. @@ -65,8 +65,6 @@ Phase 3 uses a 5-model exploration workflow. See `post-gate.md` for detailed ins > **⚠️ SCOPE**: Document only. No code analysis. No fix opinions. No running tests. -**🚨 CRITICAL: Create the state file BEFORE doing anything else.** - ### ❌ Pre-Flight Boundaries (What NOT To Do) | ❌ Do NOT | Why | When to do it | @@ -79,141 +77,11 @@ Phase 3 uses a 5-model exploration workflow. See `post-gate.md` for detailed ins ### ✅ What TO Do in Pre-Flight -- Create/check state file - Read issue description and comments - Note platforms affected (from labels) - Identify files changed (if PR exists) - Document disagreements and edge cases from comments -### Step 0: Check for Existing State File or Create New One - -**State file location**: `CustomAgentLogsTmp/PRState/pr-XXXXX.md` - -**Naming convention:** -- If starting from **PR #12345** → Name file `pr-12345.md` (use PR number) -- If starting from **Issue #33356** (no PR yet) → Name file `pr-33356.md` (use issue number as placeholder) -- When PR is created later → Rename to use actual PR number - -```bash -# Check if state file exists -mkdir -p CustomAgentLogsTmp/PRState -if [ -f "CustomAgentLogsTmp/PRState/pr-XXXXX.md" ]; then - echo "State file exists - resuming session" - cat CustomAgentLogsTmp/PRState/pr-XXXXX.md -else - echo "Creating new state file" -fi -``` - -**If the file EXISTS**: Read it to determine your current phase and resume from there. Look for: -- Which phase has `▶️ IN PROGRESS` status - that's where you left off -- Which phases have `✅ PASSED` status - those are complete -- Which phases have `⏳ PENDING` status - those haven't started - -**If the file does NOT exist**: Create it with the template structure: - -```markdown -# PR Review: #XXXXX - [Issue Title TBD] - -**Date:** [TODAY] | **Issue:** [#XXXXX](https://github.com/dotnet/maui/issues/XXXXX) | **PR:** [#YYYYY](https://github.com/dotnet/maui/pull/YYYYY) or None - -## ⏳ Status: IN PROGRESS - -| Phase | Status | -|-------|--------| -| Pre-Flight | ▶️ IN PROGRESS | -| 🚦 Gate | ⏳ PENDING | -| 🔧 Fix | ⏳ PENDING | -| 📋 Report | ⏳ PENDING | - ---- - -
-📋 Issue Summary - -[From issue body] - -**Steps to Reproduce:** -1. [Step 1] -2. [Step 2] - -**Platforms Affected:** -- [ ] iOS -- [ ] Android -- [ ] Windows -- [ ] MacCatalyst - -
- -
-📁 Files Changed - -| File | Type | Changes | -|------|------|---------| -| `path/to/fix.cs` | Fix | +X lines | -| `path/to/test.cs` | Test | +Y lines | - -
- -
-💬 PR Discussion Summary - -**Key Comments:** -- [Notable comments from issue/PR discussion] - -**Reviewer Feedback:** -- [Key points from review comments] - -**Disagreements to Investigate:** -| File:Line | Reviewer Says | Author Says | Status | -|-----------|---------------|-------------|--------| - -**Author Uncertainty:** -- [Areas where author expressed doubt] - -
- -
-🚦 Gate - Test Verification - -**Status**: ⏳ PENDING - -- [ ] Tests FAIL (bug reproduced) - -**Result:** [PENDING] - -
- -
-🔧 Fix Candidates - -**Status**: ⏳ PENDING - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| PR | PR #XXXXX | [PR's approach - from Pre-Flight] | ⏳ PENDING (Gate) | [files] | Original PR - validated by Gate | - -**Note:** try-fix candidates (1, 2, 3...) are added during Phase 3. PR's fix is reference only. - -**Exhausted:** No -**Selected Fix:** [PENDING] - -
- ---- - -**Next Step:** After Gate passes, read `.github/agents/pr/post-gate.md` and continue with phases 3-4. -``` - -This file: -- Serves as your TODO list for all phases -- Tracks progress if interrupted -- Must exist before you start gathering context -- **Always include when saving changes** (to `CustomAgentLogsTmp/PRState/`) -- **Phases 3-4 sections are added AFTER Gate passes** (see `pr/post-gate.md`) - -**Then gather context and update the file as you go.** - ### Step 1: Gather Context (depends on starting point) **If starting from a PR:** @@ -258,11 +126,9 @@ gh pr view XXXXX --json comments --jq '.comments[] | select(.body | contains("Fi - Contains structured analysis (Root Cause, Platform Comparison, etc.) **If prior agent review found:** -1. **Extract and use as state file content** - The review IS the completed state -2. Parse the phase statuses to determine what's already done -3. Import all findings (fix candidates, test results) -4. Update your local state file with this content -5. Resume from whichever phase is not yet complete (or report as done) +1. Parse the phase statuses to determine what's already done +2. Import all findings (fix candidates, test results) +3. Resume from whichever phase is not yet complete (or report as done) **Do NOT:** - Start from scratch if a complete review already exists @@ -271,8 +137,6 @@ gh pr view XXXXX --json comments --jq '.comments[] | select(.body | contains("Fi ### Step 3: Document Key Findings -Update the state file `CustomAgentLogsTmp/PRState/pr-XXXXX.md`: - **If PR exists** - Document disagreements and reviewer feedback: | File:Line | Reviewer Says | Author Says | Status | |-----------|---------------|-------------|--------| @@ -308,21 +172,12 @@ The test result will be updated to `✅ PASS (Gate)` after Gate passes. ### Step 5: Complete Pre-Flight -**🚨 MANDATORY: Update state file** - -**Update state file** - Change Pre-Flight status and populate with gathered context: -1. Change Pre-Flight status from `▶️ IN PROGRESS` to `✅ COMPLETE` -2. Fill in issue summary, platforms affected, regression info -3. Add edge cases and any disagreements (if PR exists) -4. Change 🚦 Gate status to `▶️ IN PROGRESS` - -**Before marking ✅ COMPLETE, verify state file contains:** -- [ ] Issue summary filled (not [PENDING]) -- [ ] Platform checkboxes marked -- [ ] Files Changed table populated (if PR exists) -- [ ] PR Discussion Summary documented (if PR exists) -- [ ] All [PENDING] placeholders replaced -- [ ] State file saved +Verify the following before proceeding: +- [ ] Issue summary captured +- [ ] Platform information noted +- [ ] Files changed identified (if PR exists) +- [ ] PR discussion summarized (if PR exists) +- [ ] **Write phase output to `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/pre-flight/content.md`** (see SHARED-RULES.md "Phase Output Artifacts") --- @@ -356,7 +211,7 @@ find src/Controls/tests -name "*XXXXX*" -type f 2>/dev/null **🚨 CRITICAL: Choose a platform that is BOTH affected by the bug AND available on the current host.** **Identify affected platforms** from Pre-Flight: -- Check the "Platforms Affected" checkboxes in the state file +- Check the platforms affected from Pre-Flight context - Check issue labels (e.g., `platform/iOS`, `platform/Android`) - Check which platform-specific files the PR modifies @@ -416,18 +271,11 @@ See `.github/skills/verify-tests-fail-without-fix/SKILL.md` for full skill docum ### Complete 🚦 Gate -**🚨 MANDATORY: Update state file** - -**Update state file**: -1. Fill in **Result**: `PASSED ✅` -2. Change 🚦 Gate status to `✅ PASSED` -3. Proceed to Phase 3 - -**Before marking ✅ PASSED, verify state file contains:** -- [ ] Result shows PASSED ✅ or FAILED ❌ +Verify the following before proceeding: +- [ ] Test result documented (PASSED ✅ or FAILED ❌) - [ ] Test behavior documented - [ ] Platform tested noted -- [ ] State file saved +- [ ] **Write phase output to `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/gate/content.md`** (see SHARED-RULES.md "Phase Output Artifacts") --- @@ -445,7 +293,6 @@ See `.github/skills/verify-tests-fail-without-fix/SKILL.md` for full skill docum - ❌ **Looking at implementation code during Pre-Flight** - Just gather issue/PR context - ❌ **Forming opinions on the fix during Pre-Flight** - That's Phase 3 - ❌ **Running tests during Pre-Flight** - That's Phase 2 (Gate) -- ❌ **Not creating state file first** - ALWAYS create state file before gathering context - ❌ **Skipping to Phase 3** - Gate MUST pass first ## Common Gate Mistakes diff --git a/.github/agents/pr/PLAN-TEMPLATE.md b/.github/agents/pr/PLAN-TEMPLATE.md index 6e9836d7d9fd..2cdeafd2ebd7 100644 --- a/.github/agents/pr/PLAN-TEMPLATE.md +++ b/.github/agents/pr/PLAN-TEMPLATE.md @@ -12,7 +12,7 @@ ## 🚨 Critical Rules (Summary) See `SHARED-RULES.md` for complete details. Key points: -- **Environment Blockers**: STOP immediately, report, ask user (strict retry limits) +- **Environment Blockers**: Skip blocked phase and continue autonomously (no human operator) - **No Git Commands**: Never checkout/switch branches - agent is always on correct branch - **Gate via Task Agent**: Never run inline (prevents fabrication) - **Multi-Model try-fix**: 5 models, SEQUENTIAL only @@ -23,7 +23,6 @@ See `SHARED-RULES.md` for complete details. Key points: ## Work Plan ### Phase 1: Pre-Flight -- [ ] Create state file: `CustomAgentLogsTmp/PRState/pr-XXXXX.md` - [ ] Gather PR metadata (title, body, labels, author) - [ ] Fetch and read linked issue - [ ] Fetch PR comments and review feedback @@ -31,8 +30,7 @@ See `SHARED-RULES.md` for complete details. Key points: - [ ] Document platforms affected - [ ] Classify changed files (fix vs test) - [ ] Document PR's fix approach in Fix Candidates table -- [ ] Update state file: Pre-Flight → ✅ COMPLETE -- [ ] Save state file +- [ ] **Write `PRAgent/pre-flight/content.md`** **Boundaries:** No code analysis, no fix opinions, no test running @@ -46,11 +44,10 @@ See `SHARED-RULES.md` for complete details. Key points: "Run verify-tests-fail-without-fix skill Platform: [X], TestFilter: 'IssueXXXXX', RequireFullVerification: true" ``` -- [ ] ⛔ If environment blocker: STOP, report, ask user +- [ ] ⛔ If environment blocker: retry once, then skip and document - [ ] Verify: Tests FAIL without fix, PASS with fix - [ ] If Gate fails: STOP, request test fixes -- [ ] Update state file: Gate → ✅ PASSED -- [ ] Save state file +- [ ] **Write `PRAgent/gate/content.md`** ### Phase 3: Fix 🔧 *(Only if Gate ✅ PASSED)* @@ -61,7 +58,7 @@ See `SHARED-RULES.md` for complete details. Key points: - [ ] gpt-5.2 - [ ] gpt-5.3-codex - [ ] gemini-3-pro-preview -- [ ] ⛔ If blocker: STOP, report, ask user +- [ ] ⛔ If blocker: retry once, skip remaining models, proceed to Report - [ ] Record: approach, result, files, failure analysis **Round 2+: Cross-Pollination (MANDATORY)** @@ -75,25 +72,23 @@ See `SHARED-RULES.md` for complete details. Key points: - [ ] Mark Exhausted: Yes - [ ] Compare passing candidates with PR's fix - [ ] Select best fix (results → simplicity → robustness) -- [ ] Update state file: Fix → ✅ COMPLETE -- [ ] Save state file +- [ ] **Write `PRAgent/try-fix/content.md`** ### Phase 4: Report 📋 *(Only if Phases 1-3 complete)* - [ ] Run `pr-finalize` skill - [ ] Generate review: root cause, candidates, recommendation -- [ ] Post AI Summary comment (PR phases + try-fix): +- [ ] **Write `PRAgent/report/content.md`** +- [ ] Post AI Summary comment (auto-loads from PRAgent/*/content.md): ```bash - pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber XXXXX -SkipValidation + pwsh .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 -PRNumber XXXXX pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 -IssueNumber XXXXX ``` - [ ] Post PR Finalization comment (separate): ```bash - pwsh .github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 -PRNumber XXXXX -SummaryFile CustomAgentLogsTmp/PRState/pr-XXXXX.md + pwsh .github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 -PRNumber XXXXX -SummaryFile CustomAgentLogsTmp/PRState/XXXXX/PRAgent/pr-finalize/pr-finalize-summary.md ``` -- [ ] Update state file: Report → ✅ COMPLETE -- [ ] Save final state file --- @@ -101,11 +96,9 @@ See `SHARED-RULES.md` for complete details. Key points: | Phase | Key Action | Blocker Response | |-------|------------|------------------| -| Pre-Flight | Create state file | N/A | -| Gate | Task agent → verify script | ⛔ STOP, report, ask | -| Fix | Multi-model try-fix | ⛔ STOP, report, ask | -| Report | Post via skill | ⛔ STOP, report, ask | +| Pre-Flight | Gather context | N/A | +| Gate | Task agent → verify script | Skip, report incomplete | +| Fix | Multi-model try-fix | Skip remaining, proceed to Report | +| Report | Post via skill | Document what completed | -**State file:** `CustomAgentLogsTmp/PRState/pr-XXXXX.md` - -**Never:** Mark BLOCKED and continue, claim success without tests, bypass scripts +**Never:** Claim success without tests, bypass scripts, stop and ask user diff --git a/.github/agents/pr/SHARED-RULES.md b/.github/agents/pr/SHARED-RULES.md index 1347d92e659a..6f9b1a0827c2 100644 --- a/.github/agents/pr/SHARED-RULES.md +++ b/.github/agents/pr/SHARED-RULES.md @@ -8,24 +8,114 @@ This file contains critical rules that apply across all PR agent phases. Referen **Before changing ANY phase status to ✅ COMPLETE:** -1. **Read the state file section** for the phase you're completing -2. **Find ALL ⏳ PENDING and [PENDING] fields** in that section -3. **Fill in every field** with actual content -4. **Verify no pending markers remain** in your section -5. **Save the state file** (it's in gitignored `CustomAgentLogsTmp/`) -6. **Then change status** to ✅ COMPLETE +1. **Review the phase checklist** for the phase you're completing +2. **Verify all required items** are addressed +3. **Write the phase output to `content.md`** (see Phase Output Artifacts below) +4. **Then mark the phase** as ✅ COMPLETE -**Rule:** Status ✅ means "documentation complete", not "I finished thinking about it" +**Rule:** Status ✅ means "work complete and verified", not "I finished thinking about it" --- -## Follow Templates EXACTLY +## Phase Output Artifacts -When creating state files, use the EXACT format from the documentation: -- **Do NOT add attributes** like `open` to `
` tags -- **Do NOT "improve"** the template format -- **Do NOT deviate** from documented structure -- Downstream scripts depend on exact formatting (regex patterns expect specific structure) +**After completing EACH phase, write a `content.md` file to the phase's output directory.** + +This is MANDATORY. The comment scripts (`post-ai-summary-comment.ps1`) read from these files to build the PR comment. + +### Output Directory Structure + +``` +CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/ +├── pre-flight/ +│ └── content.md # Written after Phase 1 +├── gate/ +│ ├── content.md # Written after Phase 2 +│ └── verify-tests-fail/ # Script output from verify-tests-fail.ps1 +├── try-fix/ +│ ├── content.md # Written after Phase 3 (summary of all attempts) +│ └── attempt-{N}/ # Individual attempt outputs from try-fix skill +└── report/ + └── content.md # Written after Phase 4 +``` + +### What Goes in Each content.md + +Each `content.md` should contain the **formatted phase content** — the same content that would appear inside the collapsible `
` section in the PR comment. + +**Pre-Flight example:** +```markdown +**Issue:** #XXXXX - [Title] +**Platforms Affected:** iOS, Android +**Files Changed:** 2 implementation files, 1 test file + +### Key Findings +- [Finding 1] +- [Finding 2] + +### Fix Candidates +| # | Source | Approach | Test Result | Files Changed | Notes | +|---|--------|----------|-------------|---------------|-------| +| PR | PR #XXXXX | [approach] | ⏳ PENDING (Gate) | `file.cs` | Original PR | +``` + +**Gate example:** +```markdown +**Result:** ✅ PASSED +**Platform:** android +**Mode:** Full Verification + +- Tests FAIL without fix ✅ +- Tests PASS with fix ✅ +``` + +**Fix (try-fix) example:** +```markdown +### Fix Candidates +| # | Source | Approach | Test Result | Files Changed | Notes | +|---|--------|----------|-------------|---------------|-------| +| 1 | try-fix | [approach] | ❌ FAIL | 1 file | Why: [reason] | +| 2 | try-fix | [approach] | ✅ PASS | 2 files | Works! | +| PR | PR #XXXXX | [approach] | ✅ PASS (Gate) | 2 files | Original PR | + +**Exhausted:** Yes +**Selected Fix:** PR's fix - [Reason] +``` + +**Report example:** +```markdown +## ✅ Final Recommendation: APPROVE + +### Summary +[Brief summary of the review] + +### Root Cause +[Root cause analysis] + +### Fix Quality +[Assessment of the fix] +``` + +### How to Write the File + +```bash +# Create the directory (idempotent) +mkdir -p "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pre-flight" + +# Write content (use create tool or bash) +cat > "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pre-flight/content.md" << 'EOF' +[phase content here] +EOF +``` + +### Timing + +| Phase | When to Write | +|-------|---------------| +| Pre-Flight | After all context gathered and documented | +| Gate | After verification result received from task agent | +| Fix (try-fix) | After all try-fix models explored and best fix selected | +| Report | After final recommendation determined | --- @@ -65,63 +155,58 @@ When a skill provides a PowerShell script: If you encounter an environment or system setup blocker that prevents completing a phase: -**STOP IMMEDIATELY. Do NOT continue to the next phase.** +### 🚨 Autonomous Execution (Default) -### Common Blockers +When running via `Review-PR.ps1`, there is **NO human operator** to respond to questions. -- Missing Appium drivers (Windows, iOS, Android) -- WinAppDriver not installed or returning errors -- Xcode/iOS simulators not available (on Windows) -- Android emulator not running or not configured -- Developer Mode not enabled -- Port conflicts (e.g., 4723 in use) -- Missing SDKs or tools -- Server errors (500, timeout, "unknown error occurred") +**NEVER stop and ask the user. NEVER present options and wait for a choice. Nobody will respond.** -### Retry Limits (STRICT ENFORCEMENT) - -| Blocker Type | Max Retries | Then Do | -|--------------|-------------|---------| -| Missing tool/driver | 1 install attempt | STOP and ask user | -| Server errors (500, timeout) | 0 | STOP immediately and report | -| Port conflicts | 1 (kill process) | STOP and ask user | -| Configuration issues | 1 fix attempt | STOP and ask user | +Instead, use your best judgment to continue autonomously: -### When Blocked +1. **Try ONE retry** (install missing tool, kill conflicting process, etc.) +2. **If still blocked after one retry**, SKIP the blocked phase and continue to the next phase +3. **Document what was skipped and why** in your report +4. **Always prefer continuing with partial results** over stopping completely -1. **Stop all work** - Do not proceed to the next phase -2. **Do NOT keep troubleshooting** - After the retry limit, STOP -3. **Report the blocker** clearly (use template below) -4. **Ask the user** how to proceed -5. **Wait for user response** - Do not assume or work around +**Autonomous decision guide:** -### Blocker Report Template +| Blocker Type | Max Retries | Then Do | +|--------------|-------------|---------| +| Missing tool/driver | 1 install attempt | Skip phase, continue | +| Server errors (500, timeout) | 1 retry | Skip phase, continue | +| Port conflicts | 1 (kill process) | Skip phase, continue | +| Build failures in try-fix | 2 attempts | Skip remaining try-fix models, proceed to Report | +| Configuration issues | 1 fix attempt | Skip phase, continue | -``` -⛔ BLOCKED: Cannot complete [Phase Name] +**Common autonomous decisions:** +- Gate passes but Fix phase is blocked → **Skip Fix, proceed to Report** with Gate results only +- try-fix builds fail for multiple models → **Stop try-fix exploration, proceed to Report** +- A specific platform fails → **Try alternative platform ONCE**, then skip if still blocked +- Gate fails due to environment → **Report as incomplete**, proceed to Report -**What failed:** [Step/skill that failed] -**Blocker:** [Tool/driver/error type] -**Error:** "[Exact error message]" +### Interactive Mode -**What I tried:** [List retry attempts, max 1-2] +When running with `-Interactive` flag, you MAY ask the user for guidance on blockers. -**I am STOPPING here. Options:** -1. [Option for user - e.g., investigate setup manually] -2. [Alternative platform] -3. [Skip with documented limitation] +### Common Blockers -Which would you like me to do? -``` +- Missing Appium drivers (Windows, iOS, Android) +- WinAppDriver not installed or returning errors +- Xcode/iOS simulators not available (on Windows) +- Android emulator not running or not configured +- Developer Mode not enabled +- Port conflicts (e.g., 4723 in use) +- Missing SDKs or tools +- Server errors (500, timeout, "unknown error occurred") ### Never Do - ❌ Keep trying different fixes after retry limit exceeded -- ❌ Mark a phase as ⚠️ BLOCKED and continue to the next phase - ❌ Claim "verification passed" when tests couldn't actually run -- ❌ Skip device/emulator testing and proceed with code review only - ❌ Install multiple tools/drivers without asking between each - ❌ Spend more than 2-3 tool calls troubleshooting the same blocker +- ❌ **Stop and present options to the user** - choose the best option yourself +- ❌ **Wait for user response** - nobody will respond --- @@ -148,7 +233,6 @@ Phase 4 uses these 5 AI models for try-fix exploration (run SEQUENTIALLY): **Choose a platform that is BOTH affected by the bug AND available on the current host.** ### Step 1: Identify affected platforms from Pre-Flight -- Check the "Platforms Affected" checkboxes in the state file - Check issue labels (e.g., `platform/iOS`, `platform/Android`) - Check which platform-specific files the PR modifies diff --git a/.github/agents/pr/post-gate.md b/.github/agents/pr/post-gate.md index e5a149d070fb..55e4e876aa7b 100644 --- a/.github/agents/pr/post-gate.md +++ b/.github/agents/pr/post-gate.md @@ -1,6 +1,6 @@ # PR Agent: Post-Gate Phases (3-4) -**⚠️ PREREQUISITE: Only read this file after 🚦 Gate shows `✅ PASSED` in your state file.** +**⚠️ PREREQUISITE: Only read this file after 🚦 Gate shows `✅ PASSED`.** If Gate is not passed, go back to `.github/agents/pr.md` and complete phases 1-2 first. @@ -19,20 +19,27 @@ If Gate is not passed, go back to `.github/agents/pr.md` and complete phases 1-2 **All rules from `.github/agents/pr/SHARED-RULES.md` apply here**, including: - Phase Completion Protocol (fill ALL pending fields before marking complete) -- Stop on Environment Blockers (STOP and ask user, don't continue) +- Stop on Environment Blockers (retry once, then skip and continue autonomously) - Multi-Model Configuration (5 models, SEQUENTIAL only) -If try-fix cannot run due to environment issues, **STOP and ask the user**. Do NOT mark attempts as "BLOCKED" and continue. +If try-fix cannot run due to environment issues after one retry, **skip the remaining try-fix models and proceed to Report**. Do NOT stop and ask the user. -### 🚨 CRITICAL: Stop on Environment Blockers (Applies to Phase 3) +### 🚨 CRITICAL: Environment Blockers in Phase 3 -The same "Stop on Environment Blockers" rule from `pr.md` applies here. If try-fix cannot run due to: +The default mode is **non-interactive** — no human can respond to questions. + +If try-fix cannot run due to: - Missing Appium drivers - Device/emulator not available - WinAppDriver not installed - Platform tools missing +- Build failures unrelated to the fix -**STOP and ask the user** before continuing. Do NOT mark try-fix attempts as "BLOCKED" and continue. Either fix the environment issue or get explicit user permission to skip. +**Use your best judgment to continue autonomously:** +1. Try ONE alternative (e.g., different platform, rebuild) +2. If still blocked, **skip remaining try-fix models and proceed to Report** +3. Document what was skipped and why in the Report phase +4. The PR's fix was already validated by Gate - that's sufficient for a recommendation --- @@ -40,7 +47,7 @@ The same "Stop on Environment Blockers" rule from `pr.md` applies here. If try-f > **SCOPE**: Explore independent fix alternatives using `try-fix` skill, compare with PR's fix, select the best approach. -**⚠️ Gate Check:** Verify 🚦 Gate is `✅ PASSED` in your state file before proceeding. +**⚠️ Gate Check:** Verify 🚦 Gate is `✅ PASSED` before proceeding. ### 🚨 CRITICAL: try-fix is Independent of PR's Fix @@ -73,7 +80,6 @@ Invoke the try-fix skill for PR #XXXXX: - target_files: - src/[area]/[likely-affected-file-1].cs - src/[area]/[likely-affected-file-2].cs -- state_file: CustomAgentLogsTmp/PRState/pr-XXXXX.md Generate ONE independent fix idea. Review the PR's fix first to ensure your approach is DIFFERENT. ``` @@ -118,7 +124,7 @@ After Round 1, invoke EACH of the 5 models to ask for new ideas. **No shortcuts Do you have any NEW fix ideas? Reply: 'NEW IDEA: [desc]' or 'NO NEW IDEAS'" ``` -3. **Record each model's response** in state file Cross-Pollination table +3. **Record each model's response** in Cross-Pollination table 4. **For each new idea**: Run try-fix with that model (SEQUENTIAL, wait for completion) @@ -127,13 +133,12 @@ After Round 1, invoke EACH of the 5 models to ask for new ideas. **No shortcuts #### try-fix Behavior Each `try-fix` invocation (run via task agent with specific model): -1. Reads state file to learn from prior failed attempts +1. Learns from prior failed attempts 2. Reverts PR's fix to get a broken baseline 3. Proposes ONE new independent fix idea 4. Implements and tests it 5. Records result (with failure analysis if it failed) -6. **Updates state file** (appends row to Fix Candidates table) -7. Reverts all changes (restores PR's fix) +6. Reverts all changes (restores PR's fix) See `.github/skills/try-fix/SKILL.md` for full details. @@ -162,7 +167,7 @@ After the loop, review the **Fix Candidates** table: 3. **Most robust** - Handles edge cases, less likely to regress 4. **Matches codebase style** - Consistent with existing patterns -Update the state file: +Update the selected fix: ```markdown **Exhausted:** Yes (or No if stopped early) @@ -187,36 +192,17 @@ Update the state file: ### Complete 🔧 Fix -**🚨 MANDATORY: Update state file** - -**Update state file**: -1. Verify Fix Candidates table is complete with all attempts -2. Verify failure analyses are documented for failed attempts -3. Verify Selected Fix is documented with reasoning -4. Change 🔧 Fix status to `✅ COMPLETE` -5. Change 📋 Report status to `▶️ IN PROGRESS` - -**Before marking ✅ COMPLETE, verify state file contains:** +Verify the following before proceeding: - [ ] Round 1 completed: All 5 models ran try-fix -- [ ] **Cross-pollination table exists** with responses from ALL 5 models: - ``` - | Model | Round 2 Response | - |-------|------------------| - | claude-sonnet-4.6 | NO NEW IDEAS | - | claude-opus-4.6 | NO NEW IDEAS | - | gpt-5.2 | NO NEW IDEAS | - | gpt-5.3-codex | NO NEW IDEAS | - | gemini-3-pro-preview | NO NEW IDEAS | - ``` +- [ ] Cross-pollination completed with responses from ALL 5 models - [ ] Fix Candidates table has numbered rows for each try-fix attempt - [ ] Each row has: approach, test result, files changed, notes - [ ] "Exhausted" field set to Yes (all models confirmed no new ideas) - [ ] "Selected Fix" populated with reasoning -- [ ] Root cause analysis documented for the selected fix (to be surfaced in 📋 Report phase "### Root Cause" section) -- [ ] No ⏳ PENDING markers remain in Fix section -- [ ] State file saved +- [ ] Root cause analysis documented for the selected fix +- [ ] **Write phase output to `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/content.md`** (see SHARED-RULES.md "Phase Output Artifacts") -**🚨 If cross-pollination table is missing, you skipped Round 2. Go back and invoke each model.** +**🚨 If cross-pollination is missing, you skipped Round 2. Go back and invoke each model.** --- @@ -224,7 +210,7 @@ Update the state file: > **SCOPE**: Deliver the final result - either a PR review or a new PR. -**⚠️ Gate Check:** Verify ALL phases 1-3 are `✅ COMPLETE` or `✅ PASSED` before proceeding. +**⚠️ Gate Check:** Verify ALL phases 1-3 are complete before proceeding. ### Finalize Title and Description @@ -256,8 +242,6 @@ If reviewing an existing PR, check if title/description need updates and include **Do NOT run git commands. User handles commit/push/PR creation.** -2. **Update state file** with PR link once user provides it - ### If Starting from PR - Write Review Determine your recommendation based on the Fix phase: @@ -279,35 +263,14 @@ Determine your recommendation based on the Fix phase: - Run the `pr-finalize` skill to verify title and description match implementation - If discrepancies found, include suggested updates in review comments -### Final State File Format - -Update the state file header: - -```markdown -## ✅ Final Recommendation: APPROVE -``` -or -```markdown -## ⚠️ Final Recommendation: REQUEST CHANGES -``` - -Update all phase statuses to complete. - ### Complete 📋 Report -**🚨 MANDATORY: Update state file** - -**Update state file**: -1. Change header status to final recommendation -2. Update all phases to `✅ COMPLETE` or `✅ PASSED` -3. Present final result to user - -**Before marking ✅ COMPLETE, verify state file contains:** -- [ ] Final recommendation (APPROVE/REQUEST_CHANGES/COMMENT) -- [ ] Summary of findings +Verify the following before finishing: +- [ ] Final recommendation determined (APPROVE/REQUEST_CHANGES/COMMENT) +- [ ] Summary of findings prepared - [ ] Key technical insights documented -- [ ] Overall status changed to final recommendation -- [ ] State file saved +- [ ] **Write phase output to `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/report/content.md`** (see SHARED-RULES.md "Phase Output Artifacts") +- [ ] Result presented to user --- @@ -319,7 +282,7 @@ Update all phase statuses to complete. - ❌ **Running try-fix in parallel** - SEQUENTIAL ONLY - they modify same files and use same device - ❌ **Using explore/glob instead of invoking models** - Cross-pollination requires ACTUAL task agent invocations with each model, not code searches - ❌ **Assuming "comprehensive coverage" = exhausted** - Only exhausted when all 5 models explicitly say "NO NEW IDEAS" -- ❌ **Not recording cross-pollination responses** - State file must have table showing each model's Round 2 response +- ❌ **Not recording cross-pollination responses** - Must track each model's Round 2 response - ❌ **Not analyzing why fixes failed** - Record the flawed reasoning to help future attempts - ❌ **Selecting a failing fix** - Only select from passing candidates - ❌ **Forgetting to revert between attempts** - Each try-fix must start from broken baseline, end with PR restored diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index aaa22df4882e..b8f6e9a890a0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -260,7 +260,7 @@ Skills are modular capabilities that can be invoked directly or used by agents. - **Purpose**: Verifies PR title and description match actual implementation, AND performs code review for best practices before merge. - **Trigger phrases**: "finalize PR #XXXXX", "check PR description for #XXXXX", "review commit message" - **Used by**: Before merging any PR, when description may be stale - - **Note**: Does NOT require agent involvement or session markdown - works on any PR + - **Note**: Works on any PR - **🚨 CRITICAL**: NEVER use `--approve` or `--request-changes` - only post comments. Approval is a human decision. 4. **learn-from-pr** (`.github/skills/learn-from-pr/SKILL.md`) @@ -302,7 +302,7 @@ Skills are modular capabilities that can be invoked directly or used by agents. - **Purpose**: Proposes ONE independent fix approach, applies it, tests, records result with failure analysis, then reverts - **Used by**: pr agent Phase 3 (Fix phase) - rarely invoked directly by users - **Behavior**: Reads prior attempts to learn from failures. Max 5 attempts per session. - - **Output**: Updates session markdown with attempt results and failure analysis + - **Output**: Reports attempt results and failure analysis ### Using Custom Agents diff --git a/.github/scripts/BuildAndRunHostApp.ps1 b/.github/scripts/BuildAndRunHostApp.ps1 index 60d15963eb66..0702c73b015a 100644 --- a/.github/scripts/BuildAndRunHostApp.ps1 +++ b/.github/scripts/BuildAndRunHostApp.ps1 @@ -279,6 +279,11 @@ Write-Host "" $env:DEVICE_UDID = $DeviceUdid Write-Info "Set DEVICE_UDID environment variable: $DeviceUdid" +# Set APPIUM_LOG_FILE so UITestBase saves screenshots/page-source to our log directory +$appiumLogFile = Join-Path $HostAppLogsDir "appium.log" +$env:APPIUM_LOG_FILE = $appiumLogFile +Write-Info "Set APPIUM_LOG_FILE: $appiumLogFile (screenshots will be saved here)" + try { # Run dotnet test and capture output $testOutput = & dotnet test $TestProject --filter $effectiveFilter --logger "console;verbosity=detailed" 2>&1 @@ -313,6 +318,37 @@ try { #endregion +#region Collect Test Artifacts (screenshots, page source) + +Write-Step "Collecting test artifacts (screenshots, page source)..." + +# Collect any screenshots/page source from the test assembly output directory +# UITestBase saves these via TestContext.AddTestAttachment to the assembly dir +$testAssemblyDirs = @( + (Join-Path $RepoRoot "artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0"), + (Join-Path $RepoRoot "artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0"), + (Join-Path $RepoRoot "artifacts/bin/Controls.TestCases.Mac.Tests/Debug/net10.0") +) + +$copiedCount = 0 +foreach ($dir in $testAssemblyDirs) { + if (Test-Path $dir) { + $artifacts = Get-ChildItem -Path $dir -File -Include "*.png","*.txt" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match "ScreenShot|PageSource" } + foreach ($artifact in $artifacts) { + Copy-Item -Path $artifact.FullName -Destination $HostAppLogsDir -Force + $copiedCount++ + } + } +} + +# Also check the HostAppLogsDir itself for screenshots saved via APPIUM_LOG_FILE +$screenshotCount = (Get-ChildItem -Path $HostAppLogsDir -Filter "*.png" -ErrorAction SilentlyContinue).Count +$pageSourceCount = (Get-ChildItem -Path $HostAppLogsDir -Filter "*PageSource*" -ErrorAction SilentlyContinue).Count +Write-Info "Test artifacts collected: $screenshotCount screenshot(s), $pageSourceCount page source(s) (copied $copiedCount from assembly dir)" + +#endregion + #region Capture Device Logs Write-Step "Capturing device logs..." diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 1d8749d98aa6..0844fafb5893 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -236,6 +236,25 @@ if (-not (Test-Path $stateDir)) { Write-Host " 📁 Created state directory: $stateDir" -ForegroundColor Gray } +# Create PRAgent phase directories for structured output +$PRAgentDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" +$phaseSubdirs = @("pre-flight", "gate", "try-fix", "report") +foreach ($subdir in $phaseSubdirs) { + $dirPath = Join-Path $PRAgentDir $subdir + if (-not (Test-Path $dirPath)) { + New-Item -ItemType Directory -Path $dirPath -Force | Out-Null + } +} +Write-Host " 📁 Created PRAgent phase directories: $PRAgentDir" -ForegroundColor Gray + +# Save the branch name and commit SHA BEFORE launching the agent. +# The Copilot agent may change HEAD (e.g., via git checkout, gh pr checkout, git reset) +# despite prompt instructions forbidding it. Using a pinned SHA for restoration +# ensures we always recover to the correct state regardless of what the agent did. +$savedBranch = git branch --show-current +$savedHead = git rev-parse HEAD +Write-Host " 📌 Pinned restore point: $savedBranch @ $($savedHead.Substring(0, 10))" -ForegroundColor Gray + # Step 4: Build the prompt for Copilot CLI $planTemplatePath = ".github/agents/pr/PLAN-TEMPLATE.md" @@ -255,19 +274,37 @@ $platformInstruction - NEVER run ``git checkout``, ``git switch``, ``git fetch``, ``git stash``, or ``git reset`` - NEVER run ``git push`` - you do NOT have permission to push anything - You are ALWAYS on the correct branch already - the script handles this -- If the state file says "wrong branch", that's stale state - delete it and start fresh - If you think you need to switch branches or push changes, you are WRONG - ask the user instead +🚨 **CRITICAL - AUTONOMOUS EXECUTION:** +- There is NO human operator to respond to questions +- NEVER stop and ask the user for input - nobody will respond +- NEVER present a list of options and wait for a choice +- When you encounter an environment blocker, use your best judgment to choose the best path forward: + - If a phase is blocked (e.g., try-fix builds fail), SKIP that phase and proceed to the next one + - If Gate passes but Fix phase is blocked, proceed directly to Report phase with Gate results only + - If a specific platform fails, try an alternative platform ONCE, then skip if still blocked + - Always prefer CONTINUING with partial results over STOPPING completely +- Document what was skipped and why in your report, but keep moving forward + +📁 **PHASE OUTPUT ARTIFACTS (MANDATORY):** +- After completing EACH phase, write a ``content.md`` file to the phase output directory +- Pre-Flight: ``CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pre-flight/content.md`` +- Gate: ``CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/gate/content.md`` +- Fix: ``CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/try-fix/content.md`` +- Report: ``CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/report/content.md`` +- These files are read by the comment scripts to build the PR comment +- See SHARED-RULES.md "Phase Output Artifacts" section for content format + **Instructions:** 1. Read the plan template at ``$planTemplatePath`` for the 4-phase workflow 2. Read ``.github/agents/pr.md`` for Phases 1-2 instructions 3. Follow ALL critical rules, especially: - - STOP on environment blockers and ask before continuing + - On environment blockers: skip the blocked phase and continue autonomously (see AUTONOMOUS EXECUTION above) - Use task agent for Gate verification - Run multi-model try-fix in Phase 3 **Start with Phase 1: Pre-Flight** -- Create state file: CustomAgentLogsTmp/PRState/pr-$PRNumber.md - Gather context from PR #$PRNumber - Proceed through all 4 phases @@ -298,7 +335,6 @@ if ($DryRun) { Write-Host "PR Review Context:" -ForegroundColor Cyan Write-Host " PR_NUMBER: $PRNumber" -ForegroundColor White Write-Host " PLATFORM: $(if ($Platform) { $Platform } else { '(agent will determine)' })" -ForegroundColor White - Write-Host " STATE_FILE: CustomAgentLogsTmp/PRState/pr-$PRNumber.md" -ForegroundColor White Write-Host " PLAN_TEMPLATE: $planTemplatePath" -ForegroundColor White Write-Host " CURRENT_BRANCH: $(git branch --show-current)" -ForegroundColor White Write-Host " PR_TITLE: $($prInfo.title)" -ForegroundColor White @@ -330,7 +366,7 @@ if ($DryRun) { # Branch switching prevention relies on agent instructions in pr.md only # Create log directory for this PR - $prLogDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/copilot-logs" + $prLogDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/copilot-logs" if (-not (Test-Path $prLogDir)) { New-Item -ItemType Directory -Path $prLogDir -Force | Out-Null } @@ -369,14 +405,25 @@ if ($DryRun) { # Restore tracked files to clean state before running post-completion skills. # Phase 1 (PR Agent) may have left the working tree dirty from try-fix attempts, - # which can cause skill files to be missing or modified in subsequent phases. - # NOTE: State files in CustomAgentLogsTmp/ are .gitignore'd and untracked, - # so this won't touch them. Using HEAD to also restore deleted files. + # or even switched branches despite instructions. We use the pinned SHA ($savedHead) + # instead of HEAD to guarantee restoration to the correct commit. + # NOTE: CustomAgentLogsTmp/ is .gitignore'd and untracked, so this won't touch them. Write-Host "" Write-Host "🧹 Restoring working tree to clean state between phases..." -ForegroundColor Yellow git status --porcelain 2>$null | Set-Content "CustomAgentLogsTmp/PRState/phase1-exit-git-status.log" -ErrorAction SilentlyContinue - git checkout HEAD -- . 2>&1 | Out-Null - Write-Host " ✅ Working tree restored" -ForegroundColor Green + + # Check if the agent moved HEAD or switched branches + $postAgentBranch = git branch --show-current + $postAgentHead = git rev-parse HEAD + if ($postAgentBranch -ne $savedBranch -or $postAgentHead -ne $savedHead) { + Write-Host " ⚠️ Agent changed git state! Branch: $postAgentBranch (expected: $savedBranch), HEAD: $($postAgentHead.Substring(0, 10)) (expected: $($savedHead.Substring(0, 10)))" -ForegroundColor Red + Write-Host " 🔄 Recovering to pinned restore point..." -ForegroundColor Yellow + git checkout $savedBranch 2>&1 | Out-Null + git reset --hard $savedHead 2>&1 | Out-Null + } else { + git checkout $savedHead -- . 2>&1 | Out-Null + } + Write-Host " ✅ Working tree restored (pinned SHA: $($savedHead.Substring(0, 10)))" -ForegroundColor Green # Phase 2: Run pr-finalize skill if requested if ($RunFinalize) { @@ -387,12 +434,12 @@ if ($DryRun) { Write-Host "" # Ensure output directory exists for finalize results - $finalizeDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize" + $finalizeDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize" if (-not (Test-Path $finalizeDir)) { New-Item -ItemType Directory -Path $finalizeDir -Force | Out-Null } - $finalizePrompt = "Run the pr-finalize skill for PR #$PRNumber. Verify the PR title and description match the actual implementation. Do NOT post a comment. Write your findings to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/pr-finalize-summary.md (NOT the main state file pr-$PRNumber.md which contains phase data that must not be overwritten). If you recommend a new description, also write it to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/recommended-description.md. If you have code review findings, also write them to CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/code-review.md." + $finalizePrompt = "Run the pr-finalize skill for PR #$PRNumber. Verify the PR title and description match the actual implementation. Do NOT post a comment. Write your findings to CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize/pr-finalize-summary.md. If you recommend a new description, also write it to CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize/recommended-description.md. If you have code review findings, also write them to CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize/code-review.md." $finalizeArgs = @( "-p", $finalizePrompt, @@ -422,17 +469,27 @@ if ($DryRun) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Magenta Write-Host "" - # Restore tracked files (including deleted ones) to clean state. + # Restore tracked files (including deleted ones) to clean state using pinned SHA. Write-Host "🧹 Restoring working tree to clean state..." -ForegroundColor Yellow git status --porcelain 2>$null | Set-Content "CustomAgentLogsTmp/PRState/phase2-exit-git-status.log" -ErrorAction SilentlyContinue - git checkout HEAD -- . 2>&1 | Out-Null - Write-Host " ✅ Working tree restored" -ForegroundColor Green - # 3a: Post PR agent summary comment (from Phase 1 state file) + # Check if Phase 2 (pr-finalize) moved HEAD or switched branches + $postPhase2Branch = git branch --show-current + $postPhase2Head = git rev-parse HEAD + if ($postPhase2Branch -ne $savedBranch -or $postPhase2Head -ne $savedHead) { + Write-Host " ⚠️ Phase 2 changed git state! Recovering to pinned restore point..." -ForegroundColor Red + git checkout $savedBranch 2>&1 | Out-Null + git reset --hard $savedHead 2>&1 | Out-Null + } else { + git checkout $savedHead -- . 2>&1 | Out-Null + } + Write-Host " ✅ Working tree restored (pinned SHA: $($savedHead.Substring(0, 10)))" -ForegroundColor Green + + # 3a: Post PR agent summary comment $scriptPath = ".github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1" if (-not (Test-Path $scriptPath)) { - Write-Host "⚠️ Script missing after checkout, attempting targeted recovery..." -ForegroundColor Yellow - git checkout HEAD -- $scriptPath 2>&1 | Out-Null + Write-Host "⚠️ Script missing after restore, attempting targeted recovery..." -ForegroundColor Yellow + git checkout $savedHead -- $scriptPath 2>&1 | Out-Null } if (Test-Path $scriptPath) { Write-Host "💬 Running post-ai-summary-comment.ps1 directly..." -ForegroundColor Yellow @@ -455,11 +512,11 @@ if ($DryRun) { $finalizeScriptPath = ".github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1" if (-not (Test-Path $finalizeScriptPath)) { Write-Host "⚠️ Finalize script missing, attempting targeted recovery..." -ForegroundColor Yellow - git checkout HEAD -- $finalizeScriptPath 2>&1 | Out-Null + git checkout $savedHead -- $finalizeScriptPath 2>&1 | Out-Null } if (Test-Path $finalizeScriptPath) { - Write-Host "💬 Running post-pr-finalize-comment.ps1 directly..." -ForegroundColor Yellow - & $finalizeScriptPath -PRNumber $PRNumber + Write-Host "💬 Running post-pr-finalize-comment.ps1 directly (unified mode)..." -ForegroundColor Yellow + & $finalizeScriptPath -PRNumber $PRNumber -Unified $finalizeCommentExit = $LASTEXITCODE if ($finalizeCommentExit -eq 0) { @@ -473,14 +530,14 @@ if ($DryRun) { } } } + } } Write-Host "" -Write-Host "📝 State file: CustomAgentLogsTmp/PRState/pr-$PRNumber.md" -ForegroundColor Gray Write-Host "📋 Plan template: $planTemplatePath" -ForegroundColor Gray if (-not $DryRun) { - Write-Host "📁 Copilot logs: CustomAgentLogsTmp/PRState/$PRNumber/copilot-logs/" -ForegroundColor Gray + Write-Host "📁 Copilot logs: CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/copilot-logs/" -ForegroundColor Gray if (-not $Interactive) { Write-Host "📄 Session markdown: $sessionFile" -ForegroundColor Gray } diff --git a/.github/scripts/shared/Build-AndDeploy.ps1 b/.github/scripts/shared/Build-AndDeploy.ps1 index 8abdafe7b21c..f57d5c088094 100644 --- a/.github/scripts/shared/Build-AndDeploy.ps1 +++ b/.github/scripts/shared/Build-AndDeploy.ps1 @@ -136,6 +136,20 @@ if ($Platform -eq "android") { # Deploy to iOS simulator Write-Step "Deploying to iOS simulator..." + + # Shutdown any OTHER booted simulators to avoid Appium connecting to the wrong device + $bootedSims = xcrun simctl list devices --json | ConvertFrom-Json + $otherBooted = $bootedSims.devices.PSObject.Properties.Value | + ForEach-Object { $_ } | + Where-Object { $_.state -eq "Booted" -and $_.udid -ne $DeviceUdid } + + if ($otherBooted) { + foreach ($sim in $otherBooted) { + Write-Info "Shutting down other booted simulator: $($sim.name) ($($sim.udid))" + xcrun simctl shutdown $sim.udid 2>$null + } + } + Write-Info "Booting simulator (if not already running)..." xcrun simctl boot $DeviceUdid 2>$null diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index b842d840a6cc..a5d90f192530 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -339,7 +339,12 @@ if ($Platform -eq "android") { exit 1 } - # Get device UDID if not provided + # Get device UDID if not provided - check env var first + if (-not $DeviceUdid -and $env:DEVICE_UDID) { + Write-Info "Using DEVICE_UDID from environment: $($env:DEVICE_UDID)" + $DeviceUdid = $env:DEVICE_UDID + } + if (-not $DeviceUdid) { Write-Info "Auto-detecting iOS simulator..." $simList = xcrun simctl list devices available --json | ConvertFrom-Json @@ -454,6 +459,19 @@ if ($Platform -eq "android") { Write-Success "iOS simulator: $deviceName ($DeviceUdid)" + # Shutdown any OTHER booted simulators to avoid Appium connecting to the wrong device + $bootedSims = xcrun simctl list devices --json | ConvertFrom-Json + $otherBooted = $bootedSims.devices.PSObject.Properties.Value | + ForEach-Object { $_ } | + Where-Object { $_.state -eq "Booted" -and $_.udid -ne $DeviceUdid } + + if ($otherBooted) { + foreach ($sim in $otherBooted) { + Write-Info "Shutting down other booted simulator: $($sim.name) ($($sim.udid))" + xcrun simctl shutdown $sim.udid 2>$null + } + } + # Boot simulator if not already booted Write-Info "Booting simulator (if not already running)..." xcrun simctl boot $DeviceUdid 2>$null @@ -470,7 +488,7 @@ if ($Platform -eq "android") { exit 1 } - Write-Success "Simulator is booted and ready" + Write-Success "Simulator is booted and ready: $deviceName" #endregion } diff --git a/.github/skills/ai-summary-comment/IMPROVEMENTS.md b/.github/skills/ai-summary-comment/IMPROVEMENTS.md index 2de8dc008fe4..4c50133edaf9 100644 --- a/.github/skills/ai-summary-comment/IMPROVEMENTS.md +++ b/.github/skills/ai-summary-comment/IMPROVEMENTS.md @@ -10,7 +10,7 @@ The `post-ai-summary-comment.ps1` script has been significantly improved to make **Before:** Script used hardcoded pattern matching with predefined title variations -**After:** Script **automatically discovers ALL sections** from your state file and extracts them dynamically +**After:** Script **automatically discovers ALL sections** from the provided content and extracts them dynamically ```powershell # Extracts ALL
TITLE sections @@ -25,13 +25,13 @@ $preFlightContent = Get-SectionByPattern -Sections $allSections -Patterns @( **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 +- ✅ Automatically adapts - add new sections without modifying the script +- ✅ Better debugging - shows exactly which sections were found in the content +- ✅ More maintainable - less code, more flexible **Example debug output:** ``` -[DEBUG] Found 6 section(s) in state file +[DEBUG] Found 6 section(s) in content [DEBUG] Section: '📋 Issue Summary' (803 chars) [DEBUG] Section: '🚦 Gate - Test Verification' (488 chars) [DEBUG] Section: '🔧 Fix Candidates' (868 chars) @@ -91,7 +91,7 @@ $DebugPreference = 'Continue' ``` **Shows:** -- Which sections were found in the state file +- Which sections were found in the content - How many characters each section contains - Which patterns matched which sections - Why validation passed or failed @@ -104,7 +104,7 @@ $DebugPreference = 'Continue' ``` ⛔ VALIDATION FAILED -💡 Fix these issues in the state file before posting. +💡 Fix these issues in the content before posting. Or use -SkipValidation to bypass these checks. 🐛 Debug tip: Run with $DebugPreference = 'Continue' for details @@ -126,7 +126,7 @@ function Extract-AllSections { } ``` -**Result:** Hashtable with ALL sections from your state file +**Result:** Hashtable with ALL sections from your content ### Step 2: Map to Phases @@ -208,12 +208,12 @@ Any title matching `'📋.*Report'` or `'Final Report'`: **No changes needed!** The script is backward compatible. -**Old state files** with exact headers like: +**Old content** with exact headers like: ```markdown 📋 Phase 4: Report — Final Recommendation ``` -**New state files** with simpler headers like: +**New content** with simpler headers like: ```markdown 📋 Final Report ``` @@ -236,7 +236,7 @@ Any title matching `'📋.*Report'` or `'Final Report'`: ## Common Issues & Solutions -### Issue: "Phase X has NO content in state file" +### Issue: "Phase X has NO content" **Step 1:** Enable debug mode to see what was found ```powershell @@ -245,7 +245,7 @@ pwsh -Command '$DebugPreference = "Continue"; ./post-ai-summary-comment.ps1 -PRN **Look for:** ``` -[DEBUG] Found 7 section(s) in state file +[DEBUG] Found 7 section(s) in content [DEBUG] Section: 'Your Section Title' (XXX chars) ``` @@ -263,7 +263,7 @@ If your title is `"📋 Final Analysis"`, it won't match! ### Issue: Section extracted but content is empty -**Cause:** State file structure issue (missing content between tags) +**Cause:** Content structure issue (missing content between tags) **Check your markdown:** ```markdown @@ -336,7 +336,7 @@ Tested with: **Debug output example:** ``` -[DEBUG] Found 6 section(s) in state file +[DEBUG] Found 6 section(s) in content [DEBUG] Section: '📋 Issue Summary' (803 chars) [DEBUG] Section: '📁 Files Changed' (0 chars) [DEBUG] Section: '💬 PR Discussion Summary' (0 chars) @@ -425,16 +425,16 @@ PR #27340 provides a **correct and well-tested fix**... ``` ⛔ VALIDATION FAILED Found 1 validation error(s): - - Report: Phase Report is marked as '✅ COMPLETE' but has NO content in state file + - Report: Phase Report is marked as '✅ COMPLETE' but has NO content ``` **After:** ``` ⛔ VALIDATION FAILED Found 1 validation error(s): - - Report: Phase Report is marked as '✅ COMPLETE' but has NO content in state file + - Report: Phase Report is marked as '✅ COMPLETE' but has NO content -💡 Fix these issues in the state file before posting the review comment. +💡 Fix these issues in the content before posting the review comment. Or use -SkipValidation to bypass these checks (not recommended). 🐛 Debug tip: Run with $DebugPreference = 'Continue' for detailed extraction info @@ -517,7 +517,7 @@ Any of these variations will be recognized: ## 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. +**No changes needed!** The script is backward compatible. If you have existing content with the old header format, it will continue to work. If you want to use the new flexibility: - Just use simpler headers like `📋 Final Report` instead of `📋 Phase 4: Report — Final Recommendation` @@ -527,11 +527,11 @@ If you want to use the new flexibility: ## Common Issues & Solutions -### Issue: "Phase Report has NO content in state file" +### Issue: "Phase Report has NO content" -**Solution 1:** Check your state file structure +**Solution 1:** Check your content structure ```bash -grep -A 5 "📋.*Report" CustomAgentLogsTmp/PRState/pr-XXXXX.md +grep -A 5 "📋.*Report" your-content-file.md ``` Make sure you have: @@ -612,7 +612,7 @@ $reportContent = Extract-PhaseContent -StateContent $Content -PhaseTitles @( ## Future Improvements Potential enhancements: -- [ ] Auto-detect phase titles from state file (no hardcoded patterns) +- [ ] Auto-detect phase titles from content (no hardcoded patterns) - [ ] Support markdown headings (`##` / `###`) in addition to `
` - [ ] Validate links and references work - [ ] Check that commit SHAs are valid @@ -624,7 +624,7 @@ Potential enhancements: The improvements have been tested with: - ✅ PR #27340 (Entry/Editor keyboard issue) -- ✅ State files with various header formats +- ✅ Content with various header formats - ✅ Dry run mode - ✅ Debug mode - ✅ Skip validation mode @@ -636,5 +636,5 @@ The improvements have been tested with: If you encounter issues or have suggestions, please: 1. Try debug mode first: `$DebugPreference = 'Continue'` -2. Check the state file structure +2. Check the content 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 index 84545c9fa546..70223059df16 100644 --- a/.github/skills/ai-summary-comment/NO-EXTERNAL-REFERENCES-RULE.md +++ b/.github/skills/ai-summary-comment/NO-EXTERNAL-REFERENCES-RULE.md @@ -87,17 +87,12 @@ Add an **Implementation** subsection: ### pr-finalize Skill -When running `pr-finalize` skill, you create TWO outputs: +When running `pr-finalize` skill, you create output: 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 4 (Report) @@ -184,9 +179,8 @@ When completing Phase 4, verify: | 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! +**Remember:** Anything posted to GitHub must be self-contained. Never reference local files. -- diff --git a/.github/skills/ai-summary-comment/SKILL.md b/.github/skills/ai-summary-comment/SKILL.md index 9073d3bef37c..073fea7448fb 100644 --- a/.github/skills/ai-summary-comment/SKILL.md +++ b/.github/skills/ai-summary-comment/SKILL.md @@ -18,7 +18,6 @@ This skill posts automated progress comments to GitHub Pull Requests during the - **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 @@ -97,42 +96,22 @@ If an existing finalize comment exists, it will be replaced with the updated sec ## Usage -### Simplest: Just provide PR number +### Simplest: Just provide PR number (auto-loads from phase files) ```bash -# Auto-loads CustomAgentLogsTmp/PRState/pr-27246.md +# Auto-loads from CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/*/content.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 | +| `PRNumber` | Yes | Pull request number | `12345` | | `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. @@ -224,7 +203,7 @@ When the same PR is reviewed multiple times (e.g., after new commits), the scrip 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/`. +**✨ Auto-Loading from `CustomAgentLogsTmp`**: The script automatically discovers and aggregates ALL attempt directories from `CustomAgentLogsTmp/PRState/{IssueNumber}/PRAgent/try-fix/`. ### Usage @@ -233,17 +212,17 @@ The `post-try-fix-comment.ps1` script updates the `` sec ```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 + -TryFixDir CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/attempt-1 ``` #### Or just provide issue number ```powershell -# Auto-discovers and posts latest attempt from CustomAgentLogsTmp/PRState/27246/try-fix/ +# Auto-discovers and posts latest attempt from CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/ pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 -IssueNumber 27246 ``` -#### Legacy: Manual parameters +#### Manual parameters ```powershell pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 ` @@ -277,7 +256,7 @@ pwsh .github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 ` ### Expected Directory Structure ``` -CustomAgentLogsTmp/PRState/{IssueNumber}/try-fix/ +CustomAgentLogsTmp/PRState/{IssueNumber}/PRAgent/try-fix/ ├── attempt-1/ │ ├── approach.md # Brief description of the approach (required) │ ├── result.txt # "Pass", "Fail", or "Compiles" (required) @@ -344,7 +323,7 @@ pwsh .github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 -PR ```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 + -ReportFile CustomAgentLogsTmp/PRState/32891/PRAgent/gate/verify-tests-fail/verification-report.md ``` ### Parameters @@ -362,7 +341,7 @@ pwsh .github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 ` ### Expected Directory Structure ``` -CustomAgentLogsTmp/PRState/{PRNumber}/verify-tests-fail/ +CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/gate/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) @@ -394,7 +373,7 @@ pwsh .github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 ` pwsh .github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 -IssueNumber 27246 ``` -#### Legacy: Manual parameters +#### Manual parameters ```powershell pwsh .github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 ` @@ -539,15 +518,52 @@ pwsh .github/skills/ai-summary-comment/scripts/post-pr-finalize-comment.ps1 ` 4. **Auto-parsing from summary file getting confused** - When in doubt, provide explicit parameters instead of relying on auto-parsing -### Expected Directory Structure for Auto-Loading +### Expected Directory Structure + +The PR agent writes phase output files that comment scripts auto-load: ``` -CustomAgentLogsTmp/PRState/{PRNumber}/pr-finalize/ -├── pr-finalize-summary.md # Main summary (auto-parsed) -├── recommended-description.md # Full recommended description (optional) -└── code-review.md # Code review findings (optional) +CustomAgentLogsTmp/PRState/{PRNumber}/ +├── PRAgent/ +│ ├── pre-flight/ +│ │ └── content.md # Phase 1 output (auto-loaded by post-ai-summary-comment.ps1) +│ ├── gate/ +│ │ ├── content.md # Phase 2 output (auto-loaded by post-ai-summary-comment.ps1) +│ │ └── verify-tests-fail/ # Script output from verify-tests-fail.ps1 +│ │ ├── verification-report.md +│ │ ├── verification-log.txt +│ │ ├── test-without-fix.log +│ │ └── test-with-fix.log +│ ├── try-fix/ +│ │ ├── content.md # Phase 3 summary (auto-loaded by post-ai-summary-comment.ps1) +│ │ ├── attempt-1/ # Individual attempt outputs +│ │ │ ├── approach.md +│ │ │ ├── result.txt +│ │ │ ├── fix.diff +│ │ │ └── analysis.md +│ │ └── attempt-2/ +│ │ └── ... +│ └── report/ +│ └── content.md # Phase 4 output (auto-loaded by post-ai-summary-comment.ps1) +├── pr-finalize/ +│ ├── pr-finalize-summary.md +│ ├── recommended-description.md +│ └── code-review.md +└── copilot-logs/ + ├── process-*.log + └── session-*.md ``` +### Auto-Loading Behavior + +When `post-ai-summary-comment.ps1` is called, it auto-discovers phase files: +1. Checks `CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/*/content.md` +2. Loads all available phase content files +3. Builds the comment structure from the loaded files +4. Posts/updates the unified AI Summary comment + +This eliminates the need to pass large content strings as parameters. + --- ## Technical Details 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 index 46388f0a320f..9c1353794cbd 100644 --- a/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.ps1 @@ -7,8 +7,10 @@ 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** + Content is always auto-loaded from PRAgent phase files + (CustomAgentLogsTmp/PRState//PRAgent/*/content.md). + + **Validates that phases marked as COMPLETE actually have content.** Format: ## 🤖 AI Summary — ✅ APPROVE @@ -17,14 +19,7 @@
.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) + The pull request number (required) .PARAMETER DryRun Print comment instead of posting @@ -33,28 +28,16 @@ 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)" + ./post-ai-summary-comment.ps1 -PRNumber 12345 -DryRun #> param( [Parameter(Mandatory=$false)] [int]$PRNumber, - [Parameter(Mandatory=$false)] - [string]$StateFile, - - [Parameter(Mandatory=$false)] - [string]$Content, - [Parameter(Mandatory=$false)] [switch]$DryRun, @@ -68,77 +51,141 @@ param( $ErrorActionPreference = "Stop" # ============================================================================ -# STATE FILE RESOLUTION +# INPUT VALIDATION # ============================================================================ -# 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 ($PRNumber -eq 0) { + throw "PRNumber is required." +} + +# Auto-load from PRAgent phase files +Write-Host "ℹ️ Auto-loading from PRAgent phase files..." -ForegroundColor Cyan + +$PRAgentDir = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" +if (-not (Test-Path $PRAgentDir)) { + $repoRoot = git rev-parse --show-toplevel 2>$null + if ($repoRoot) { + $PRAgentDir = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" } - - if (Test-Path $StateFile) { - $Content = Get-Content $StateFile -Raw -Encoding UTF8 - Write-Host "ℹ️ Loaded state file: $StateFile" -ForegroundColor Cyan +} + +if (-not (Test-Path $PRAgentDir)) { + Write-Host "" + Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Red + Write-Host "║ ⛔ No PRAgent directory found ║" -ForegroundColor Red + Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red + Write-Host "" + Write-Host "Expected directory: $PRAgentDir" -ForegroundColor Yellow + Write-Host "Ensure PRAgent phase files exist." -ForegroundColor Yellow + throw "No PRAgent directory found. Ensure PRAgent/*/content.md files exist." +} + +# Load each phase content file +$phaseFiles = @{ + "pre-flight" = Join-Path $PRAgentDir "pre-flight/content.md" + "gate" = Join-Path $PRAgentDir "gate/content.md" + "try-fix" = Join-Path $PRAgentDir "try-fix/content.md" + "report" = Join-Path $PRAgentDir "report/content.md" +} + +$loadedPhases = @() +$phaseContentMap = @{} + +foreach ($phase in $phaseFiles.GetEnumerator()) { + if (Test-Path $phase.Value) { + $phaseContentMap[$phase.Key] = Get-Content $phase.Value -Raw -Encoding UTF8 + $loadedPhases += $phase.Key + Write-Host " ✅ Loaded: $($phase.Key) ($((Get-Item $phase.Value).Length) bytes)" -ForegroundColor Green } else { - throw "State file not found: $StateFile" + Write-Host " ⏭️ Skipped: $($phase.Key) (no content.md)" -ForegroundColor Gray } } -# 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 +if ($loadedPhases.Count -eq 0) { + throw "No phase content files found in $PRAgentDir. Ensure at least one phase has a content.md file." +} + +Write-Host " 📦 Loaded $($loadedPhases.Count) phase(s): $($loadedPhases -join ', ')" -ForegroundColor Cyan + +# Build synthetic Content from phase files in the expected
format +$syntheticParts = @() + +# Determine phase statuses based on which files exist and content +$phaseStatusMap = @{} +foreach ($phase in @("pre-flight", "gate", "try-fix", "report")) { + if ($phaseContentMap.ContainsKey($phase)) { + $phaseStatusMap[$phase] = "✅ COMPLETE" } 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 - } - } + $phaseStatusMap[$phase] = "⏳ PENDING" } } -# If Content still not provided, try stdin (legacy support) -if ([string]::IsNullOrWhiteSpace($Content)) { - $Content = $input | Out-String +# Build status table +$statusTable = @" +| Phase | Status | +|-------|--------| +| Pre-Flight | $($phaseStatusMap['pre-flight']) | +| Gate | $($phaseStatusMap['gate']) | +| Fix | $($phaseStatusMap['try-fix']) | +| Report | $($phaseStatusMap['report']) | +"@ +$syntheticParts += $statusTable + +# Build phase sections +if ($phaseContentMap.ContainsKey('pre-flight')) { + $syntheticParts += @" +
📋 Pre-Flight — Issue Summary + +$($phaseContentMap['pre-flight']) + +
+"@ +} + +if ($phaseContentMap.ContainsKey('gate')) { + $syntheticParts += @" +
🚦 Gate — Test Verification + +$($phaseContentMap['gate']) + +
+"@ +} + +if ($phaseContentMap.ContainsKey('try-fix')) { + $syntheticParts += @" +
🔧 Fix — Analysis & Comparison + +$($phaseContentMap['try-fix']) + +
+"@ +} + +if ($phaseContentMap.ContainsKey('report')) { + $syntheticParts += @" +
📋 Report — Final Recommendation + +$($phaseContentMap['report']) + +
+"@ } +$Content = $syntheticParts -join "`n`n---`n`n" +Write-Host " ✅ Built synthetic content ($($Content.Length) chars)" -ForegroundColor Green + # Final validation if ([string]::IsNullOrWhiteSpace($Content)) { Write-Host "" Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Red - Write-Host "║ ⛔ No state file content found ║" -ForegroundColor Red + Write-Host "║ ⛔ No content loaded from phase files ║" -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 "Usage:" -ForegroundColor Yellow + Write-Host " ./post-ai-summary-comment.ps1 -PRNumber 12345 # auto-loads from PRAgent/*/content.md" -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" + throw "No content loaded from PRAgent phase files." } Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan @@ -167,7 +214,7 @@ function Test-PhaseContentComplete { # Check if content exists if ([string]::IsNullOrWhiteSpace($PhaseContent)) { - $validationErrors += "Phase $PhaseName is marked as '$PhaseStatus' but has NO content in state file" + $validationErrors += "Phase $PhaseName is marked as '$PhaseStatus' but has NO content" if ($Debug) { Write-Host " [DEBUG] Content is null or whitespace for phase: $PhaseName" -ForegroundColor DarkGray } @@ -179,8 +226,9 @@ function Test-PhaseContentComplete { 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') + # Check for PENDING markers - only match [PENDING] placeholder markers. + # ⏳ PENDING is a status indicator (redundant with phase table), not an unfilled placeholder. + $pendingMatches = [regex]::Matches($PhaseContent, '\[PENDING\]') if ($pendingMatches.Count -gt 0) { $validationErrors += "Phase $PhaseName is marked as '$PhaseStatus' but contains $($pendingMatches.Count) PENDING markers" } @@ -235,7 +283,7 @@ function Test-PhaseContentComplete { # EXTRACTION FUNCTIONS # ============================================================================ -# Extract recommendation from state file +# Extract recommendation from content $recommendation = "IN PROGRESS" if ($Content -match '##\s+✅\s+Final Recommendation:\s+APPROVE') { $recommendation = "✅ APPROVE" @@ -247,7 +295,7 @@ if ($Content -match '##\s+✅\s+Final Recommendation:\s+APPROVE') { $recommendation = "⚠️ REQUEST CHANGES" } -# Extract phase statuses from state file +# Extract phase statuses from content $phaseStatuses = @{ "Pre-Flight" = "⏳ PENDING" "Gate" = "⏳ PENDING" @@ -273,7 +321,7 @@ if ($Content -match '(?s)\|\s*Phase\s*\|\s*Status\s*\|.*?\n\|[\s-]+\|[\s-]+\|(.* # DYNAMIC SECTION EXTRACTION # ============================================================================ -# Extract ALL sections from state file dynamically +# Extract ALL sections from content dynamically function Extract-AllSections { param( [string]$StateContent, @@ -288,7 +336,7 @@ function Extract-AllSections { $matches = [regex]::Matches($StateContent, $pattern) if ($Debug) { - Write-Host " [DEBUG] Found $($matches.Count) section(s) in state file" -ForegroundColor Cyan + Write-Host " [DEBUG] Found $($matches.Count) section(s) in content" -ForegroundColor Cyan } foreach ($match in $matches) { @@ -361,6 +409,27 @@ $reportContent = Get-SectionByPattern -Sections $allSections -Patterns @( 'Final Report' ) -Debug:$debugMode +# Fallback: If Report content not found in
blocks, look for +# "## Final Recommendation" section directly in the markdown (agent sometimes +# writes Report as a top-level heading instead of a
block) +if ([string]::IsNullOrWhiteSpace($reportContent)) { + # Look for "## Final Recommendation" heading - capture up to the first --- separator + # or
block to avoid including content from other phases + if ($Content -match '(?s)##\s+[✅⚠️❌\uFE0F]*\s*Final Recommendation[:\s].+?(?=\n---|\n$null if ($repoRoot) { - $summaryPath = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/pr-finalize/pr-finalize-summary.md" + $summaryPath = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize/pr-finalize-summary.md" } } @@ -410,7 +413,14 @@ if ([string]::IsNullOrWhiteSpace($DescriptionStatus)) { } if ([string]::IsNullOrWhiteSpace($DescriptionAssessment)) { - throw "DescriptionAssessment is required. Provide via -DescriptionAssessment or use -SummaryFile" + if (-not [string]::IsNullOrWhiteSpace($SummaryFile)) { + # We have a summary file but couldn't extract a specific assessment section. + # Use the full summary content as the assessment (the whole file IS the assessment). + $DescriptionAssessment = $content + Write-Host "ℹ️ Using full summary file content as DescriptionAssessment" -ForegroundColor Cyan + } else { + throw "DescriptionAssessment is required. Provide via -DescriptionAssessment or use -SummaryFile" + } } # Warn if description needs work but no recommended description is provided @@ -573,10 +583,153 @@ if ($CodeReviewStatus -ne "Skipped" -or -not [string]::IsNullOrWhiteSpace($CodeR } # ============================================================================ -# STANDALONE COMMENT HANDLING -# Posts as separate PR Finalization comment with marker +# COMMENT POSTING +# Two modes: -Unified (inject into AI Summary comment) or standalone (default) # ============================================================================ +if ($Unified) { + # ======================================================================== + # UNIFIED MODE: Inject into the comment + # ======================================================================== + + $MAIN_MARKER = "" + $SECTION_START = "" + $SECTION_END = "" + $LEGACY_MARKER = "" + + # Build the finalize section wrapped in expandable details + $finalizeSection = @" +$SECTION_START +
+📋 Expand PR Finalization Review + +--- + +$titleSection + +$descSection +$codeReviewSection + +--- + +
+$SECTION_END +"@ + + Write-Host "`nUnified mode: injecting into AI Summary comment on #$PRNumber..." -ForegroundColor Yellow + + $existingUnifiedComment = $null + $existingLegacyComment = $null + + try { + $commentsJson = gh api "repos/dotnet/maui/issues/$PRNumber/comments?per_page=100" 2>$null + $comments = $commentsJson | ConvertFrom-Json + + foreach ($comment in $comments) { + if ($comment.body -match [regex]::Escape($MAIN_MARKER)) { + $existingUnifiedComment = $comment + Write-Host "✓ Found unified AI Summary comment (ID: $($comment.id))" -ForegroundColor Green + } + if ($comment.body -match [regex]::Escape($LEGACY_MARKER)) { + $existingLegacyComment = $comment + } + } + } catch { + Write-Host "⚠️ Could not fetch comments: $_" -ForegroundColor Yellow + } + + if ($DryRun) { + if ([string]::IsNullOrWhiteSpace($PreviewFile)) { + $PreviewFile = "CustomAgentLogsTmp/PRState/$PRNumber/ai-summary-comment-preview.md" + } + + $previewDir = Split-Path $PreviewFile -Parent + if (-not (Test-Path $previewDir)) { + New-Item -ItemType Directory -Path $previewDir -Force | Out-Null + } + + $existingPreview = "" + if (Test-Path $PreviewFile) { + $existingPreview = Get-Content $PreviewFile -Raw -Encoding UTF8 + } + + if ($existingPreview -match [regex]::Escape($SECTION_START)) { + $pattern = [regex]::Escape($SECTION_START) + "[\s\S]*?" + [regex]::Escape($SECTION_END) + $finalComment = $existingPreview -replace $pattern, $finalizeSection + } elseif (-not [string]::IsNullOrWhiteSpace($existingPreview)) { + $finalComment = $existingPreview.TrimEnd() + "`n`n" + $finalizeSection + } else { + $finalComment = @" +$MAIN_MARKER + +## 🤖 AI Summary + +$finalizeSection +"@ + } + + 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 + exit 0 + } + + if ($existingUnifiedComment) { + $body = $existingUnifiedComment.body + + if ($body -match [regex]::Escape($SECTION_START)) { + $pattern = [regex]::Escape($SECTION_START) + "[\s\S]*?" + [regex]::Escape($SECTION_END) + $newBody = $body -replace $pattern, $finalizeSection + } else { + $newBody = $body.TrimEnd() + "`n`n" + $finalizeSection + } + + $newBody = $newBody -replace "`n{4,}", "`n`n`n" + + Write-Host "Updating unified comment ID $($existingUnifiedComment.id) with PR finalize section..." -ForegroundColor Yellow + $tempFile = [System.IO.Path]::GetTempFileName() + @{ body = $newBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + + $result = gh api --method PATCH "repos/dotnet/maui/issues/comments/$($existingUnifiedComment.id)" --input $tempFile --jq '.html_url' + Remove-Item $tempFile + Write-Host "✅ PR finalize section added to unified comment: $result" -ForegroundColor Green + } else { + $commentBody = @" +$MAIN_MARKER + +## 🤖 AI Summary + +$finalizeSection +"@ + + Write-Host "Creating new unified comment with PR finalize section on PR #$PRNumber..." -ForegroundColor Yellow + $tempFile = [System.IO.Path]::GetTempFileName() + @{ body = $commentBody } | ConvertTo-Json -Depth 10 | Set-Content -Path $tempFile -Encoding UTF8 + + $result = gh api --method POST "repos/dotnet/maui/issues/$PRNumber/comments" --input $tempFile --jq '.html_url' + Remove-Item $tempFile + Write-Host "✅ Unified comment posted: $result" -ForegroundColor Green + } + + # Clean up legacy standalone finalize comment if it exists + if ($existingLegacyComment) { + Write-Host "🧹 Removing legacy standalone PR Finalization comment (ID: $($existingLegacyComment.id))..." -ForegroundColor Yellow + gh api --method DELETE "repos/dotnet/maui/issues/comments/$($existingLegacyComment.id)" 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host " ✅ Legacy comment removed" -ForegroundColor Green + } else { + Write-Host " ⚠️ Could not remove legacy comment (non-fatal)" -ForegroundColor Yellow + } + } + +} else { + # ======================================================================== + # STANDALONE MODE (default): Post as separate PR Finalization comment + # ======================================================================== + $FINALIZE_MARKER = "" Write-Host "`nChecking for existing PR Finalization comment on #$PRNumber..." -ForegroundColor Yellow @@ -615,7 +768,7 @@ $codeReviewSection 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" + $PreviewFile = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/pr-finalize-preview.md" } # Ensure directory exists @@ -651,3 +804,5 @@ if ($existingComment) { } Remove-Item $tempFile + +} # end standalone mode 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 index f5bcf49540c5..27bd4566bcb0 100644 --- a/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.ps1 @@ -10,7 +10,7 @@ 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/** + **NEW: Auto-loads from CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/** Format: ## 🔧 Try-Fix Analysis for Issue #XXXXX @@ -35,7 +35,7 @@ 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) + Path to try-fix attempt directory (e.g., CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/attempt-1) If provided, all parameters are auto-loaded from files in this directory .PARAMETER Approach @@ -61,14 +61,14 @@ .EXAMPLE # Simplest: Just provide attempt directory (all info auto-loaded) - ./post-try-fix-comment.ps1 -TryFixDir CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1 + ./post-try-fix-comment.ps1 -TryFixDir CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/attempt-1 .EXAMPLE # Post all attempts for an issue ./post-try-fix-comment.ps1 -IssueNumber 27246 .EXAMPLE - # Manual parameters (legacy) + # Manual parameters ./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" ` @@ -128,7 +128,7 @@ if (-not [string]::IsNullOrWhiteSpace($TryFixDir)) { throw "Try-fix directory not found: $TryFixDir" } - # Extract IssueNumber from path (e.g., CustomAgentLogsTmp/PRState/27246/try-fix/attempt-1) + # Extract IssueNumber from path (e.g., CustomAgentLogsTmp/PRState/27246/PRAgent/try-fix/attempt-1) if ($TryFixDir -match '[/\\](\d+)[/\\]try-fix') { if ($IssueNumber -eq 0) { $IssueNumber = [int]$Matches[1] @@ -207,11 +207,11 @@ if (-not [string]::IsNullOrWhiteSpace($TryFixDir)) { # 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" + $tryFixBase = "CustomAgentLogsTmp/PRState/$IssueNumber/PRAgent/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" + $tryFixBase = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$IssueNumber/PRAgent/try-fix" } } 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 index 5cace91ca955..cc78274e74a2 100644 --- a/.github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-verify-tests-comment.ps1 @@ -85,11 +85,11 @@ Write-Host "╚═════════════════════ # 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" + $reportPath = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/gate/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" + $reportPath = Join-Path $repoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/gate/verify-tests-fail/verification-report.md" } } 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 index fb3125529c99..da3972cd92e4 100644 --- a/.github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 +++ b/.github/skills/ai-summary-comment/scripts/post-write-tests-comment.ps1 @@ -74,7 +74,7 @@ ./post-write-tests-comment.ps1 -IssueNumber 27246 .EXAMPLE - # Manual parameters (legacy) + # Manual parameters ./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" ` diff --git a/.github/skills/learn-from-pr/SKILL.md b/.github/skills/learn-from-pr/SKILL.md index 18e3a0d3f700..2a21ef5dc0c1 100644 --- a/.github/skills/learn-from-pr/SKILL.md +++ b/.github/skills/learn-from-pr/SKILL.md @@ -16,7 +16,6 @@ Extracts lessons learned from a completed PR to improve repository documentation | Input | Required | Source | |-------|----------|--------| | PR number or Issue number | Yes | User provides (e.g., "PR #33352" or "issue 33352") | -| Session markdown | Optional | `CustomAgentLogsTmp/PRState/issue-XXXXX.md` or `pr-XXXXX.md` | ## Outputs @@ -61,19 +60,10 @@ The skill is complete when you have: # Required: Get PR info gh pr view XXXXX --json title,body,files gh pr diff XXXXX - -# Check for session markdown -ls CustomAgentLogsTmp/PRState/issue-XXXXX.md CustomAgentLogsTmp/PRState/pr-XXXXX.md 2>/dev/null ``` -**If session markdown exists, extract:** -- Fix Candidates table (what was tried) -- Files each attempt targeted -- Why attempts failed - -**Analyzing without session markdown:** +**Analyze the PR to extract learning:** -When no session file exists, you can still learn from: 1. **PR discussion** - Comments reveal what was tried 2. **Commit history** - Multiple commits may show iteration 3. **Code complexity** - Non-obvious fixes suggest learning opportunities @@ -88,8 +78,6 @@ Focus on: "What would have helped an agent find this fix faster?" ```bash # Where did final fix go? gh pr view XXXXX --json files --jq '.files[].path' | grep -v test - -# If session markdown exists, compare to attempted files ``` | Scenario | Implication | @@ -209,7 +197,6 @@ Present your analysis covering: | Situation | Action | |-----------|--------| | PR not found | Ask user to verify PR number | -| No session markdown | Analyze PR diff only, note limited context | | No agent involvement evident | Ask user if they still want analysis | | Can't determine failure mode | State "insufficient data" and what's missing | diff --git a/.github/skills/try-fix/SKILL.md b/.github/skills/try-fix/SKILL.md index 291283981e3c..d13690296403 100644 --- a/.github/skills/try-fix/SKILL.md +++ b/.github/skills/try-fix/SKILL.md @@ -47,7 +47,6 @@ All inputs are provided by the invoker (CI, agent, or user). | Platform | Yes | Target platform (`android`, `ios`, `windows`, `maccatalyst`) | | Hints | Optional | Suggested approaches, prior attempts, or areas to focus on | | Baseline | Optional | Git ref or instructions for establishing broken state (default: current state) | -| state_file | Optional | Path to PR agent state file (e.g., `CustomAgentLogsTmp/PRState/pr-12345.md`). If provided, try-fix will append its results to the Fix Candidates table. | ## Outputs @@ -70,7 +69,7 @@ Results reported back to the invoker: $IssueNumber = "" # Replace with actual number # Find next attempt number -$tryFixDir = "CustomAgentLogsTmp/PRState/$IssueNumber/try-fix" +$tryFixDir = "CustomAgentLogsTmp/PRState/$IssueNumber/PRAgent/try-fix" $existingAttempts = (Get-ChildItem "$tryFixDir/attempt-*" -Directory -ErrorAction SilentlyContinue).Count $attemptNum = $existingAttempts + 1 @@ -163,9 +162,8 @@ The skill is complete when: - Review what files were changed - Read the actual code changes to understand the current fix approach -2. **If state_file provided, review prior attempts:** - - Read the Fix Candidates table - - Note which approaches failed and WHY (the Notes column) +2. **Review prior attempts if any are known:** + - Note which approaches failed and WHY - Note which approaches partially succeeded 3. **Identify what makes your approach DIFFERENT:** @@ -195,7 +193,7 @@ The skill is complete when: pwsh .github/scripts/EstablishBrokenBaseline.ps1 *>&1 | Tee-Object -FilePath "$OUTPUT_DIR/baseline.log" ``` -The script auto-detects and reverts fix files to merge-base state while preserving test files. **Will fail fast if no fix files detected** - you must be on the actual PR branch. Optional flags: `-BaseBranch main`, `-DryRun`. +The script auto-detects and reverts fix files to merge-base state while preserving test files. **Will fail fast if no fix files detected** - the PR's changes must be present in the current branch (the `Review-PR.ps1` script handles this by merging the PR before the agent runs). Optional flags: `-BaseBranch main`, `-DryRun`. **Verify baseline was established:** ```powershell @@ -203,7 +201,7 @@ The script auto-detects and reverts fix files to merge-base state while preservi Select-String -Path "$OUTPUT_DIR/baseline.log" -Pattern "Baseline established" ``` -**If the script fails with "No fix files detected":** You're likely on the wrong branch. Checkout the actual PR branch with `gh pr checkout ` and try again. +**If the script fails with "No fix files detected":** The PR changes are not present on the current branch. Report this as `Blocked` — do NOT switch branches. **If something fails mid-attempt:** `pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore` @@ -349,29 +347,6 @@ Provide structured output to the invoker: **Determining Status:** Set `Done` when you've completed testing this approach (whether it passed or failed). Set `NeedsRetry` only if you hit a transient error (network timeout, flaky test) and want to retry the same approach. -### Step 10: Update State File (if provided) - -If `state_file` input was provided and file exists: - -1. **Read current Fix Candidates table** from state file -2. **Determine next attempt number** (count existing try-fix rows + 1) -3. **Append new row** with this attempt's results: - -| # | Source | Approach | Test Result | Files Changed | Notes | -|---|--------|----------|-------------|---------------|-------| -| N | try-fix #N | [approach] | ✅ PASS / ❌ FAIL | [files] | [analysis] | - -**If no state file provided:** Skip this step (results returned to invoker only). - -**⚠️ Do NOT `git add` or `git commit` the state file.** It lives in `CustomAgentLogsTmp/` which is `.gitignore`d. Committing it with `git add -f` would cause `git checkout HEAD -- .` (used between phases) to revert it, losing data. - -**⚠️ IMPORTANT: Do NOT set any "Exhausted" field.** Cross-pollination exhaustion is determined by the pr agent after invoking ALL 6 models and confirming none have new ideas. try-fix only reports its own attempt result. - -**Ownership rule:** try-fix updates its own row ONLY. Never modify: -- Phase status fields -- "Selected Fix" field -- Other try-fix rows - ## Error Handling | Situation | Action | diff --git a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 index de3bad6fee15..5eacfed3135a 100644 --- a/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 +++ b/.github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 @@ -126,14 +126,14 @@ if (-not $PRNumber) { } if (-not $foundPR) { - Write-Host "⚠️ Could not auto-detect PR number - using 'unknown' folder" -ForegroundColor Yellow - $PRNumber = "unknown" + Write-Error "Could not auto-detect PR number. Please provide -PRNumber parameter." + exit 1 } } } # Set output directory based on PR number -$OutputDir = "CustomAgentLogsTmp/PRState/$PRNumber/verify-tests-fail" +$OutputDir = "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent/gate/verify-tests-fail" Write-Host "📁 Output directory: $OutputDir" -ForegroundColor Cyan # ============================================================ @@ -144,59 +144,6 @@ $BaselineScript = Join-Path $RepoRoot ".github/scripts/EstablishBrokenBaseline.p # Import Test-IsTestFile and Find-MergeBase from shared script . $BaselineScript -# ============================================================ -# Label management for verification results -# ============================================================ -$LabelConfirmed = "s/ai-reproduction-confirmed" -$LabelFailed = "s/ai-reproduction-failed" - -function Update-VerificationLabels { - param( - [Parameter(Mandatory = $true)] - [bool]$ReproductionConfirmed, - - [Parameter(Mandatory = $false)] - [string]$PR = $PRNumber - ) - - if ($PR -eq "unknown" -or -not $PR) { - Write-Host "⚠️ Cannot update labels: PR number not available" -ForegroundColor Yellow - return - } - - $labelToAdd = if ($ReproductionConfirmed) { $LabelConfirmed } else { $LabelFailed } - $labelToRemove = if ($ReproductionConfirmed) { $LabelFailed } else { $LabelConfirmed } - - Write-Host "" - Write-Host "🏷️ Updating verification labels on PR #$PR..." -ForegroundColor Cyan - - # Track success for both operations - $removeSuccess = $true - - # Remove the opposite label if it exists (using REST API to avoid GraphQL deprecation issues) - $existingLabels = gh pr view $PR --json labels --jq '.labels[].name' 2>$null - if ($existingLabels -contains $labelToRemove) { - Write-Host " Removing: $labelToRemove" -ForegroundColor Yellow - gh api "repos/dotnet/maui/issues/$PR/labels/$labelToRemove" --method DELETE 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { - $removeSuccess = $false - Write-Host " ⚠️ Failed to remove label: $labelToRemove" -ForegroundColor Yellow - } - } - - # Add the appropriate label (using REST API to avoid GraphQL deprecation issues) - Write-Host " Adding: $labelToAdd" -ForegroundColor Green - $result = gh api "repos/dotnet/maui/issues/$PR/labels" --method POST -f "labels[]=$labelToAdd" 2>&1 - $addSuccess = $LASTEXITCODE -eq 0 - - if ($addSuccess -and $removeSuccess) { - Write-Host "✅ Labels updated successfully" -ForegroundColor Green - } elseif ($addSuccess) { - Write-Host "⚠️ Label added but failed to remove old label" -ForegroundColor Yellow - } else { - Write-Host "⚠️ Failed to update labels: $result" -ForegroundColor Yellow - } -} # ============================================================ # Auto-detect test filter from changed files @@ -466,7 +413,6 @@ if ($DetectedFixFiles.Count -eq 0) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Green Write-Host "" Write-Host "Failed tests: $($testResult.FailCount)" -ForegroundColor Yellow - Update-VerificationLabels -ReproductionConfirmed $true exit 0 } else { # Tests PASSED - this is bad! @@ -487,7 +433,6 @@ if ($DetectedFixFiles.Count -eq 0) { Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red Write-Host "" Write-Host "Passed tests: $($testResult.PassCount)" -ForegroundColor Yellow - Update-VerificationLabels -ReproductionConfirmed $false exit 1 } } @@ -882,9 +827,7 @@ if ($verificationPassed) { Write-Host "╠═══════════════════════════════════════════════════════════╣" -ForegroundColor Green Write-Host "║ Tests correctly detect the issue: ║" -ForegroundColor Green Write-Host "║ - FAIL without fix (as expected) ║" -ForegroundColor Green - Write-Host "║ - PASS with fix (as expected) ║" -ForegroundColor Green Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Green - Update-VerificationLabels -ReproductionConfirmed $true exit 0 } else { Write-Host "" @@ -904,8 +847,6 @@ if ($verificationPassed) { Write-Host "║ 1. Wrong fix files specified ║" -ForegroundColor Red Write-Host "║ 2. Tests don't actually test the fixed behavior ║" -ForegroundColor Red Write-Host "║ 3. The issue was already fixed in base branch ║" -ForegroundColor Red - Write-Host "║ 4. Build caching - try clean rebuild ║" -ForegroundColor Red Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Red - Update-VerificationLabels -ReproductionConfirmed $false exit 1 } diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml new file mode 100644 index 000000000000..05e4d11a2796 --- /dev/null +++ b/eng/pipelines/ci-copilot.yml @@ -0,0 +1,736 @@ +# Pipeline for running GitHub Copilot PR Reviewer Agent +# This pipeline installs the Copilot CLI and invokes the PR reviewer agent +# to conduct automated code reviews on pull requests. +# +# For more information, see: +# https://github.com/dotnet/maui/wiki/PR-Reviewer-Agent + +trigger: none # Manual trigger only + +pr: none # Not triggered by PRs + +parameters: + - name: PRNumber + displayName: 'Pull Request Number' + type: string + default: '' + + - name: Platform + displayName: 'Target Platform' + type: string + default: 'android' + values: + - android + - ios + + - name: pool + type: object + default: + name: AcesShared + +variables: + - template: /eng/pipelines/common/variables.yml@self + - name: Codeql.Enabled + value: false + - name: Codeql.SkipTaskAutoInjection + value: true + +stages: + - stage: ReviewPR + displayName: 'Review Pull Request' + jobs: + - job: CopilotReview + displayName: 'Run Copilot PR Reviewer Agent' + pool: ${{ parameters.pool }} + timeoutInMinutes: 180 + steps: + - checkout: self + fetchDepth: 0 + persistCredentials: true + + - script: | + echo "Validating PR Number parameter..." + if [ -z "${{ parameters.PRNumber }}" ]; then + echo "##vso[task.logissue type=error]PRNumber parameter is required" + exit 1 + fi + echo "PR Number: ${{ parameters.PRNumber }}" + displayName: 'Validate Parameters' + + - script: | + echo "##vso[build.updatebuildnumber]PR ${{ parameters.PRNumber }} ${{ parameters.Platform }}" + displayName: 'Set Pipeline Run Title' + + # Provision environment (Xcode, .NET SDK, Android SDK, etc.) + - template: common/provision.yml + parameters: + skipXcode: false + skipProvisionator: false + skipAndroidCommonSdks: ${{ eq(parameters.Platform, 'ios') }} + skipAndroidPlatformApis: ${{ eq(parameters.Platform, 'ios') }} + skipJdk: ${{ eq(parameters.Platform, 'ios') }} + skipSimulatorSetup: false + skipCertificates: true + # Android emulator setup (skip for iOS) + skipAndroidEmulatorImages: ${{ eq(parameters.Platform, 'ios') }} + skipAndroidCreateAvds: ${{ eq(parameters.Platform, 'ios') }} + androidEmulatorApiLevel: '30' + + # Configure AVD for hardware acceleration (AcesShared ARM64 macOS agents have HVF) + # Match maui-pr Cake script: no GPU override on macOS (uses default hw GPU) + - script: | + echo "=== Configuring AVD for hardware-accelerated emulation ===" + AVD_CONFIG="$HOME/.android/avd/Emulator_30.avd/config.ini" + if [ -f "$AVD_CONFIG" ]; then + echo "Found AVD config: $AVD_CONFIG" + cat "$AVD_CONFIG" + else + echo "##vso[task.logissue type=warning]AVD config not found at: $AVD_CONFIG" + ls -la "$HOME/.android/avd/" 2>/dev/null || echo "AVD directory not found" + fi + displayName: 'Configure AVD' + condition: eq('${{ parameters.Platform }}', 'android') + + # Set up Android SDK PATH (required on self-hosted agents) + - pwsh: | + if ($env:ANDROID_SDK_ROOT) { + $platformTools = Join-Path $env:ANDROID_SDK_ROOT "platform-tools" + $emulatorPath = Join-Path $env:ANDROID_SDK_ROOT "emulator" + $cmdlineTools = Join-Path $env:ANDROID_SDK_ROOT "cmdline-tools/latest/bin" + + # Use Azure Pipelines' prependpath command to persist across steps + Write-Host "##vso[task.prependpath]$platformTools" + Write-Host "##vso[task.prependpath]$emulatorPath" + Write-Host "##vso[task.prependpath]$cmdlineTools" + + Write-Host "Added to PATH (will apply to subsequent steps):" + Write-Host " platform-tools: $platformTools" + Write-Host " emulator: $emulatorPath" + Write-Host " cmdline-tools: $cmdlineTools" + + # Verify the tools exist + if (Test-Path (Join-Path $platformTools "adb")) { + Write-Host "✅ adb found at: $platformTools/adb" + } else { + Write-Host "⚠️ adb NOT found at expected location" + } + if (Test-Path (Join-Path $emulatorPath "emulator")) { + Write-Host "✅ emulator found at: $emulatorPath/emulator" + } else { + Write-Host "⚠️ emulator NOT found at expected location" + } + } else { + Write-Host "##vso[task.logissue type=warning]ANDROID_SDK_ROOT not set, skipping PATH setup" + } + + # Auto-detect and set JAVA_HOME if not already set + if (-not $env:JAVA_HOME) { + $jvmDir = "/Library/Java/JavaVirtualMachines" + $msJdk = Get-ChildItem "$jvmDir/microsoft-*.jdk" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($msJdk) { + $javaHome = "$($msJdk.FullName)/Contents/Home" + Write-Host "##vso[task.setvariable variable=JAVA_HOME]$javaHome" + Write-Host "Set JAVA_HOME to: $javaHome" + } + } + displayName: 'Configure Android SDK PATH' + condition: eq('${{ parameters.Platform }}', 'android') + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + + # Start Android Emulator EARLY (using Start-Emulator.ps1 script) + - pwsh: | + Write-Host "=== Starting Android Emulator ===" + Write-Host "Working directory: $(Get-Location)" + Write-Host "ANDROID_SDK_ROOT: $env:ANDROID_SDK_ROOT" + Write-Host "JAVA_HOME: $env:JAVA_HOME" + Write-Host "PATH (first 500 chars): $($env:PATH.Substring(0, [Math]::Min(500, $env:PATH.Length)))" + + # Verify adb is in path + $adbPath = Get-Command adb -ErrorAction SilentlyContinue + if ($adbPath) { + Write-Host "adb found at: $($adbPath.Source)" + } else { + Write-Host "adb NOT in PATH, checking manually..." + $manualAdb = Join-Path $env:ANDROID_SDK_ROOT "platform-tools/adb" + if (Test-Path $manualAdb) { + Write-Host "adb exists at $manualAdb but not in PATH" + } + } + + $scriptPath = ".github/scripts/shared/Start-Emulator.ps1" + Write-Host "Script path: $scriptPath" + + if (-not (Test-Path $scriptPath)) { + Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" + exit 1 + } + Write-Host "Script exists: OK" + + $ErrorActionPreference = "Continue" + try { + Write-Host "Invoking Start-Emulator.ps1 -Platform android -DeviceUdid Emulator_30..." + & "./$scriptPath" -Platform android -DeviceUdid Emulator_30 2>&1 | ForEach-Object { Write-Host $_ } + $exitCode = $LASTEXITCODE + Write-Host "Script returned exit code: $exitCode" + } catch { + Write-Host "##vso[task.logissue type=error]Exception: $_" + Write-Host $_.ScriptStackTrace + $exitCode = 1 + } + + if ($exitCode -ne 0) { + Write-Host "##vso[task.logissue type=error]Start-Emulator failed with code $exitCode" + exit $exitCode + } + + Write-Host "=== Android Emulator Started Successfully ===" + displayName: 'Start Android Emulator' + condition: eq('${{ parameters.Platform }}', 'android') + timeoutInMinutes: 35 + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + + # Install .NET and workloads via build.ps1 + - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic + displayName: 'Install .NET and workloads' + retryCountOnTaskFailure: 2 + env: + DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) + PRIVATE_BUILD: $(PrivateBuild) + + - pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)" + displayName: 'Add .NET to PATH' + + # Build MSBuild tasks (required for MAUI builds) + - pwsh: ./build.ps1 --target=dotnet-buildtasks --configuration="Release" --verbosity=diagnostic + displayName: 'Build MSBuild Tasks' + retryCountOnTaskFailure: 1 + env: + DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) + PRIVATE_BUILD: $(PrivateBuild) + + # Verify environment is ready + - script: | + echo "=== Verifying Build Environment ===" + ERRORS="" + + # Check .NET SDK + echo "Checking .NET SDK..." + if ! dotnet --version; then + ERRORS="${ERRORS}\n- .NET SDK not available" + else + echo "✓ .NET SDK: $(dotnet --version)" + fi + + # Check Android SDK (only for Android platform) + if [ "${{ parameters.Platform }}" = "android" ]; then + echo "Checking Android SDK..." + if [ -z "$ANDROID_HOME" ] && [ -z "$ANDROID_SDK_ROOT" ]; then + ERRORS="${ERRORS}\n- ANDROID_HOME/ANDROID_SDK_ROOT not set" + else + SDK_PATH="${ANDROID_HOME:-$ANDROID_SDK_ROOT}" + if [ ! -d "$SDK_PATH" ]; then + ERRORS="${ERRORS}\n- Android SDK directory not found: $SDK_PATH" + else + echo "✓ Android SDK: $SDK_PATH" + fi + fi + fi + + # Check Xcode (macOS only) + if [ "$(uname)" = "Darwin" ]; then + echo "Checking Xcode..." + if ! xcodebuild -version; then + ERRORS="${ERRORS}\n- Xcode not available" + else + echo "✓ Xcode available" + fi + + # Check iOS simulators + echo "Checking iOS simulators..." + if ! xcrun simctl list devices available | grep -q "iPhone"; then + ERRORS="${ERRORS}\n- No iOS simulators available" + else + echo "✓ iOS simulators available" + fi + fi + + # Check Java/JDK (only for Android platform) + if [ "${{ parameters.Platform }}" = "android" ]; then + echo "Checking JDK..." + if ! java -version 2>&1; then + ERRORS="${ERRORS}\n- JDK not available" + else + echo "✓ JDK available" + fi + fi + + # Report errors + if [ -n "$ERRORS" ]; then + echo "" + echo "##vso[task.logissue type=error]=== Environment Verification FAILED ===" + echo -e "Missing dependencies:$ERRORS" + echo "##vso[task.logissue type=error]Build environment is not properly configured. See above for details." + exit 1 + fi + + echo "" + echo "=== Environment Verification PASSED ===" + displayName: 'Verify Build Environment' + + + # Restore .NET tools (includes xharness) + - script: | + echo "Restoring .NET tools..." + dotnet tool restore + echo "Tools restored successfully" + displayName: 'Restore .NET Tools' + + # Boot Android emulator for UI tests + - script: | + echo "=== Booting Android Emulator ===" + + # Match maui-pr Cake script: on macOS use default GPU (hardware-accelerated), + # only use swiftshader on Linux. AcesShared agents are ARM64 macOS with HVF. + # Start emulator in background with logging for debugging + EMULATOR_LOG="/tmp/emulator-boot.log" + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim > "$EMULATOR_LOG" 2>&1 & + EMULATOR_PID=$! + echo "Emulator started with PID: $EMULATOR_PID" + + # Give emulator a moment to start and verify process is running + sleep 3 + if ! pgrep -f 'qemu-system' > /dev/null; then + echo "##vso[task.logissue type=error]Emulator process did not start" + echo "=== Emulator Log ===" + cat "$EMULATOR_LOG" 2>/dev/null || echo "No log available" + exit 1 + fi + echo "Emulator process verified running" + + # Wait for device to appear with timeout (don't use adb wait-for-device - can hang forever) + echo "Waiting for emulator device..." + device_timeout=60 + device_waited=0 + while ! adb devices | grep -q "emulator.*device"; do + sleep 2 + device_waited=$((device_waited + 2)) + if [ $device_waited -ge $device_timeout ]; then + echo "##vso[task.logissue type=error]Emulator device did not appear in time" + echo "=== Emulator Log (last 50 lines) ===" + tail -50 "$EMULATOR_LOG" 2>/dev/null || echo "No log available" + adb devices -l + exit 1 + fi + done + echo "Emulator device detected" + + # Wait for boot_completed + echo "Waiting for emulator to finish booting..." + timeout=600 + waited=0 + while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do + sleep 2 + waited=$((waited + 2)) + echo "Waiting for boot... ($waited/$timeout seconds)" + if [ $waited -ge $timeout ]; then + echo "##vso[task.logissue type=error]Emulator did not boot in time" + echo "=== Emulator Log (last 50 lines) ===" + tail -50 "$EMULATOR_LOG" 2>/dev/null || echo "No log available" + adb devices -l + exit 1 + fi + done + echo "Boot completed flag set" + + # Wait for package manager service to be available (critical for app installation) + echo "Waiting for package manager service..." + pm_timeout=120 + pm_waited=0 + while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do + sleep 3 + pm_waited=$((pm_waited + 3)) + echo "Waiting for package manager... ($pm_waited/$pm_timeout seconds)" + if [ $pm_waited -ge $pm_timeout ]; then + echo "##vso[task.logissue type=error]Package manager service did not start" + echo "=== Checking services ===" + adb shell service list 2>/dev/null | head -20 || echo "Cannot list services" + exit 1 + fi + done + echo "Package manager service is ready" + + echo "=== Emulator booted successfully! ===" + adb devices -l + displayName: 'Boot Android Emulator' + condition: eq('${{ parameters.Platform }}', 'android') + continueOnError: true + timeoutInMinutes: 15 + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + JAVA_HOME: $(JAVA_HOME_17_X64) + + - script: | + echo "Installing Node.js 22..." + brew install node@22 + brew link --overwrite node@22 + if ! node --version; then + echo "##vso[task.logissue type=error]Failed to install Node.js" + exit 1 + fi + npm --version + echo "Node.js installed successfully" + displayName: 'Install Node.js' + + - script: | + echo "Installing Appium and platform driver..." + + # Get npm global bin directory and add to PATH + NPM_BIN=$(npm config get prefix)/bin + echo "NPM global bin directory: $NPM_BIN" + export PATH="$NPM_BIN:$PATH" + + # Install Appium globally + npm install -g appium + + # Install both drivers — the agent may switch platforms if the bug + # only affects one platform (e.g., iOS bug triggered via Android run) + echo "Installing UiAutomator2 driver for Android..." + appium driver install uiautomator2 + echo "Installing XCUITest driver for iOS..." + appium driver install xcuitest + + # Verify installation + if ! which appium; then + echo "##vso[task.logissue type=error]Failed to install Appium" + exit 1 + fi + + APPIUM_PATH=$(which appium) + echo "Appium path: $APPIUM_PATH" + echo "Appium version: $(appium --version)" + echo "Appium drivers installed:" + appium driver list --installed + + # Export PATH for subsequent steps (Azure DevOps specific) + echo "##vso[task.prependpath]$NPM_BIN" + + echo "Appium installed successfully" + displayName: 'Install Appium' + + - script: | + echo "Installing GitHub CLI..." + brew install gh + if ! gh --version; then + echo "##vso[task.logissue type=error]Failed to install GitHub CLI" + exit 1 + fi + echo "GitHub CLI installed successfully" + displayName: 'Install GitHub CLI' + + - script: | + echo "Authenticating with GitHub CLI..." + if [ -z "$(GH_CLI_TOKEN)" ]; then + echo "##vso[task.logissue type=error]GH_CLI_TOKEN is not set. Please configure the pipeline variable." + exit 1 + fi + echo "$(GH_CLI_TOKEN)" | gh auth login --with-token + if ! gh auth status; then + echo "##vso[task.logissue type=error]GitHub CLI authentication failed" + exit 1 + fi + displayName: 'Authenticate GitHub CLI' + env: + GH_CLI_TOKEN: $(GH_CLI_TOKEN) + + - script: | + echo "Installing GitHub Copilot CLI..." + npm install -g @github/copilot + if ! which copilot; then + echo "##vso[task.logissue type=error]Failed to install GitHub Copilot CLI" + exit 1 + fi + echo "Copilot CLI installed successfully" + displayName: 'Install GitHub Copilot CLI' + + # Restart Android emulator to ensure it's fresh before PR Reviewer runs + # The emulator can become unstable after running for a long time + - script: | + echo "=== Restarting Android Emulator for PR Reviewer ===" + + # Kill any existing emulator + echo "Killing existing emulator..." + pkill -f 'qemu-system' 2>/dev/null || true + sleep 5 + + # Restart ADB server + echo "Restarting ADB server..." + adb kill-server + sleep 2 + adb start-server + sleep 2 + + # Start fresh emulator + EMULATOR_LOG="/tmp/emulator-fresh.log" + echo "Starting fresh emulator..." + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim > "$EMULATOR_LOG" 2>&1 & + EMULATOR_PID=$! + echo "Emulator started with PID: $EMULATOR_PID" + + # Wait for device to appear + echo "Waiting for emulator device..." + device_timeout=300 + device_waited=0 + while ! adb devices | grep -q "emulator.*device"; do + sleep 5 + device_waited=$((device_waited + 5)) + if [ $device_waited -ge $device_timeout ]; then + echo "##vso[task.logissue type=error]Emulator device did not appear in time" + cat "$EMULATOR_LOG" 2>/dev/null | tail -30 + exit 1 + fi + echo "Waiting for device... ($device_waited/$device_timeout seconds)" + done + echo "Emulator device detected" + + # Wait for boot_completed + echo "Waiting for boot_completed..." + boot_timeout=600 + boot_waited=0 + while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do + sleep 5 + boot_waited=$((boot_waited + 5)) + if [ $boot_waited -ge $boot_timeout ]; then + echo "##vso[task.logissue type=error]Emulator did not boot in time" + exit 1 + fi + done + echo "Boot completed" + + # Wait for package manager + echo "Waiting for package manager service..." + pm_timeout=120 + pm_waited=0 + while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do + sleep 5 + pm_waited=$((pm_waited + 5)) + if [ $pm_waited -ge $pm_timeout ]; then + echo "##vso[task.logissue type=error]Package manager service did not start" + adb shell service list 2>/dev/null | head -20 + exit 1 + fi + echo "Waiting for package manager... ($pm_waited/$pm_timeout seconds)" + done + + echo "=== Fresh Android Emulator Ready! ===" + adb devices -l + displayName: 'Restart Android Emulator (Fresh)' + condition: eq('${{ parameters.Platform }}', 'android') + timeoutInMinutes: 15 + env: + ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + + # Boot iOS Simulator (only for iOS platform) + # UI test baseline screenshots are captured on iPhone Xs - must use same device + - script: | + echo "=== Booting iOS Simulator ===" + + # Find the latest stable iOS runtime (prefer 18.x, fallback to 17.x) + RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' + [.runtimes[] | select(.name | test("iOS 18"))] | sort_by(.version) | last | .identifier // empty + ') + if [ -z "$RUNTIME" ]; then + RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' + [.runtimes[] | select(.name | test("iOS 17"))] | sort_by(.version) | last | .identifier // empty + ') + fi + echo "Selected iOS runtime: $RUNTIME" + + # Look for iPhone Xs (matches UI test baselines - required for snapshot tests) + UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' + .devices[$rt] // [] | + map(select(.name == "iPhone Xs")) | + .[0].udid // empty + ') + + # If iPhone Xs doesn't exist, try iPhone 11 Pro (same 1125×2436 resolution) + if [ -z "$UDID" ]; then + UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' + .devices[$rt] // [] | + map(select(.name == "iPhone 11 Pro")) | + .[0].udid // empty + ') + if [ -n "$UDID" ]; then + echo "Found existing iPhone 11 Pro (same resolution as iPhone Xs): $UDID" + fi + else + echo "Found existing iPhone Xs: $UDID" + fi + + # If neither exists, try to create them + if [ -z "$UDID" ]; then + echo "No matching device found - attempting to create one for runtime $RUNTIME..." + + # Try iPhone Xs first + UDID=$(xcrun simctl create "iPhone Xs" com.apple.CoreSimulator.SimDeviceType.iPhone-Xs "$RUNTIME" 2>&1) + if [ $? -ne 0 ]; then + echo "iPhone Xs device type unavailable: $UDID" + # Try iPhone 11 Pro (same 1125×2436 resolution) + UDID=$(xcrun simctl create "iPhone 11 Pro" com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro "$RUNTIME" 2>&1) + if [ $? -ne 0 ]; then + echo "##vso[task.logissue type=warning]Failed to create iPhone 11 Pro: $UDID" + # Last resort: first available iPhone + UDID=$(xcrun simctl list devices available --json | jq -r ' + .devices | to_entries | + map(.value) | flatten | + map(select(.name | test("iPhone"))) | + .[0].udid + ') + else + echo "Created iPhone 11 Pro simulator: $UDID" + fi + else + echo "Created iPhone Xs simulator: $UDID" + fi + fi + + if [ -z "$UDID" ]; then + echo "##vso[task.logissue type=error]No iOS simulator found" + exit 1 + fi + + # Shutdown any other booted simulators to avoid Appium connecting to wrong device + xcrun simctl list devices booted --json | jq -r ' + .devices | to_entries | map(.value) | flatten | + map(select(.state == "Booted" and .udid != "'"$UDID"'")) | + .[].udid + ' | while read OTHER_UDID; do + echo "Shutting down other simulator: $OTHER_UDID" + xcrun simctl shutdown "$OTHER_UDID" 2>/dev/null || true + done + + echo "Booting simulator: $UDID" + xcrun simctl boot "$UDID" 2>/dev/null || echo "Simulator may already be booted" + sleep 10 + + echo "Booted simulators:" + xcrun simctl list devices booted + + echo "##vso[task.setvariable variable=DEVICE_UDID]$UDID" + echo "iOS Simulator UDID: $UDID" + displayName: 'Boot iOS Simulator' + condition: eq('${{ parameters.Platform }}', 'ios') + timeoutInMinutes: 5 + + - script: | + echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." + echo "Reviewing PR #${{ parameters.PRNumber }}..." + + # Configure git identity (required for merge operations on self-hosted agents) + git config user.email "copilot-ci@microsoft.com" + git config user.name "Copilot CI" + echo "Git identity configured" + + # Create Directory.Build.Override.props to skip Xcode version check + # AcesShared agents may have a newer Xcode than the .NET iOS SDK expects + cp Directory.Build.Override.props.in Directory.Build.Override.props + # Insert ValidateXcodeVersion before closing tag + sed -i '' 's|| false\n|' Directory.Build.Override.props + + # Create artifacts directory for Copilot outputs + mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs + + # Invoke the PR reviewer using our PowerShell script + # The script will merge the PR into the current branch + # -PostSummaryComment and -RunFinalize handle posting comments + set +e + pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform ${{ parameters.Platform }} -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" + COPILOT_EXIT_CODE=$? + set -e + + echo "Review-PR.ps1 exit code: $COPILOT_EXIT_CODE" + + # Terminate any orphaned copilot CLI processes that could hold this step's + # stdout fd open and prevent the bash step from exiting. + # Only target processes whose command line includes the copilot CLI path. + echo "Cleaning up orphaned copilot processes..." + SELF_PID=$$ + for proc in $(pgrep -f "[c]opilot" 2>/dev/null || true); do + if [ -n "$proc" ] && [ "$proc" != "$SELF_PID" ]; then + PROC_CMD=$(ps -p "$proc" -o args= 2>/dev/null || true) + if echo "$PROC_CMD" | grep -q "copilot"; then + echo " Stopping copilot process $proc: $PROC_CMD" + kill "$proc" 2>/dev/null || true + fi + fi + done + + # Copy any Copilot session files + if [ -d "$HOME/.copilot" ]; then + echo "Copying Copilot session state..." + cp -r "$HOME/.copilot" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot-session-state || true + fi + + # Copy CustomAgentLogsTmp if it exists + if [ -d "CustomAgentLogsTmp" ]; then + echo "Copying CustomAgentLogsTmp..." + cp -r CustomAgentLogsTmp $(Build.ArtifactStagingDirectory)/copilot-logs/ || true + fi + + # Copy any Review_Feedback files + find . -name "Review_Feedback_*.md" -type f -exec cp {} $(Build.ArtifactStagingDirectory)/copilot-logs/ \; 2>/dev/null || true + + # Copy any .github/agent-pr-session files + if [ -d ".github/agent-pr-session" ]; then + echo "Copying agent-pr-session..." + cp -r .github/agent-pr-session $(Build.ArtifactStagingDirectory)/copilot-logs/ || true + fi + + # Check for failure indicators in output + if [ $COPILOT_EXIT_CODE -ne 0 ]; then + echo "##vso[task.logissue type=error]Review-PR.ps1 exited with code $COPILOT_EXIT_CODE" + # Don't exit yet - let artifacts be published first + echo "##vso[task.setvariable variable=CopilotFailed]true" + fi + + # Check output for common failure patterns + if grep -qi "error\|failed\|exception" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md 2>/dev/null; then + if grep -qi "simulator.*not\|emulator.*not\|workload.*not\|sdk.*not found" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md 2>/dev/null; then + echo "##vso[task.logissue type=warning]Copilot encountered environment issues. Check artifacts for details." + fi + fi + + echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot-logs/" + displayName: 'Run PR Reviewer Agent' + env: + COPILOT_GITHUB_TOKEN: $(COPILOT_TOKEN) + GH_TOKEN: $(GH_COMMENT_TOKEN) + DEVICE_UDID: $(DEVICE_UDID) + + # Publish Copilot logs and session artifacts + - task: PublishPipelineArtifact@1 + displayName: 'Publish Copilot Logs' + inputs: + targetPath: '$(Build.ArtifactStagingDirectory)/copilot-logs' + artifact: 'CopilotLogs' + publishLocation: 'pipeline' + condition: succeededOrFailed() + + # Publish build logs if they exist + - task: PublishPipelineArtifact@1 + displayName: 'Publish Build Logs' + inputs: + targetPath: '$(LogDirectory)' + artifact: 'BuildLogs' + publishLocation: 'pipeline' + condition: and(succeededOrFailed(), ne(variables['LogDirectory'], '')) + + # Fail the pipeline if Copilot failed + - script: | + if [ "$(CopilotFailed)" = "true" ]; then + echo "##vso[task.logissue type=error]Copilot PR review failed. Check CopilotLogs artifact for details." + exit 1 + fi + displayName: 'Check Copilot Result' + condition: succeededOrFailed() diff --git a/eng/pipelines/common/provision.yml b/eng/pipelines/common/provision.yml index 527cd9885306..1f00c37d748e 100644 --- a/eng/pipelines/common/provision.yml +++ b/eng/pipelines/common/provision.yml @@ -32,6 +32,7 @@ parameters: expiryInHours: 1 base64Encode: false skipInternalFeeds: true + skipCertificates: false steps: diff --git a/eng/pipelines/common/variables.yml b/eng/pipelines/common/variables.yml index ed84d69ca591..8dee742a2525 100644 --- a/eng/pipelines/common/variables.yml +++ b/eng/pipelines/common/variables.yml @@ -56,14 +56,7 @@ variables: - group: MAUI # This is the main MAUI variable group that contains secrets for the apple certificate -# Variable groups required for all builds -- ${{ if and(ne(variables['Build.DefinitionName'], 'maui-pr'), ne(variables['Build.DefinitionName'], 'dotnet-maui'), ne(variables['Build.DefinitionName'], 'maui-pr-devicetests'), ne(variables['Build.DefinitionName'], 'maui-pr-uitests')) }}: - - group: maui-provisionator # This is just needed for the provisionator - - -- ${{ if or(eq(variables['System.TeamProject'], 'DevDiv'), eq(variables['Build.DefinitionName'], 'dotnet-maui'), eq(variables['Build.DefinitionName'], 'dotnet-maui-build')) }}: - - name: internalProvisioning - value: true +- ${{ if or(eq(variables['Build.DefinitionName'], 'dotnet-maui'), eq(variables['Build.DefinitionName'], 'dotnet-maui-build')) }}: - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: - name: PrivateBuild value: false @@ -73,13 +66,11 @@ variables: value: true - name: _SignType value: real - - - group: AzureDevOps-Artifact-Feeds-Pats - -- ${{ if eq(variables['Build.DefinitionName'], 'dotnet-maui') }}: - - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: - # Publish-Build-Assets provides: MaestroAccessToken, BotAccount-dotnet-maestro-bot-PAT - # DotNet-HelixApi-Access provides: HelixApiAccessToken - - group: Publish-Build-Assets - group: DotNet-HelixApi-Access - group: SDL_Settings + - group: AzureDevOps-Artifact-Feeds-Pats + - ${{ if eq(variables['Build.DefinitionName'], 'dotnet-maui') }}: + - group: Publish-Build-Assets # This variable group contains secrets to publis to BAR + + +