From 56d5519dc1966304a2ae9c1cdc6442166cc798fa Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 11:10:14 -0600 Subject: [PATCH 01/11] Improve vmr-codeflow-status skill: detect force pushes, empty diffs, and post-action state Add three improvements based on investigating a stale codeflow PR: 1. Force push detection - Query PR timeline for head_ref_force_pushed events, showing who force-pushed and when. 2. Empty diff detection - Check changedFiles/additions/deletions from the PR API to flag 0-change PRs that are effectively no-ops. 3. Post-action staleness analysis - Cross-reference force push timestamps against conflict/staleness warnings. When a force push post-dates these warnings and produces an empty diff, the PR is identified as a no-op with tailored recommendations (merge empty, close, or force-trigger). --- .github/skills/vmr-codeflow-status/SKILL.md | 27 +++++ .../scripts/Get-CodeflowStatus.ps1 | 99 +++++++++++++++++-- 2 files changed, 119 insertions(+), 7 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index 7da69a157c9c96..6b5f0694653b38 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -105,6 +105,10 @@ Use this skill when: - **âš ī¸ Staleness warning**: A forward flow merged while this backflow PR was open. Maestro blocked further updates. - **🔴 Conflict detected**: Maestro found merge conflicts. Shows conflicting files and `darc vmr resolve-conflict` command. +### Force Push & Empty Diff Detection +- **🔄 Force push detected**: Shows who force-pushed and when. Cross-references against conflict/staleness warnings to determine if someone already acted (e.g., ran `darc vmr resolve-conflict`). +- **📭 Empty diff**: PR has 0 changed files. If this follows a force push after warnings, the PR is likely a no-op — its changes already landed in the target branch via other paths. Recommendations shift to merge-empty/close/force-trigger. + ### Manual Commits Manual commits on the PR branch are at risk if the PR is closed or force-triggered. The script lists them so you can decide whether to preserve them. @@ -137,6 +141,29 @@ darc vmr resolve-conflict --subscription Install darc via `eng\common\darc-init.ps1` in any arcade-enabled repository. +### When the script reports "Maestro may be stuck" + +When the script shows a missing backflow PR with "Maestro may be stuck" (builds are fresh but no PR was created), follow these diagnostic steps: + +1. **Check the subscription** to find when it last consumed a build: + ```bash + darc get-subscriptions --target-repo --source-repo dotnet/dotnet + ``` + Look at the `Last Build` field — if it's weeks old while the channel has newer builds, the subscription is stuck. + +2. **Compare against the latest channel build** to confirm the gap: + ```bash + darc get-latest-build --repo dotnet/dotnet --channel "" + ``` + Channel names follow patterns like `.NET 11.0.1xx SDK`, `.NET 10.0.1xx SDK`, `.NET 11.0.1xx SDK Preview 1`. + +3. **Trigger the subscription manually** to unstick it: + ```bash + darc trigger-subscriptions --id + ``` + +4. **If triggering doesn't produce a PR within a few minutes**, the issue may be deeper — check Maestro health or open an issue on `dotnet/arcade`. + ## References - **VMR codeflow concepts**: See [references/vmr-codeflow-reference.md](references/vmr-codeflow-reference.md) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index a6ea0536e9ab06..1a5e2b040857c5 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -626,7 +626,7 @@ if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { return } -$prJson = gh pr view $PRNumber -R $Repository --json body,title,state,author,headRefName,baseRefName,createdAt,updatedAt,url,comments,commits +$prJson = gh pr view $PRNumber -R $Repository --json body,title,state,author,headRefName,baseRefName,createdAt,updatedAt,url,comments,commits,additions,deletions,changedFiles if ($LASTEXITCODE -ne 0) { Write-Error "Could not fetch PR #$PRNumber from $Repository. Ensure you are authenticated (gh auth login)." return @@ -657,6 +657,36 @@ elseif ($isBackflow) { Write-Status "Flow" "Backflow (dotnet/dotnet → $Repository)" "Cyan" } +# Check for empty diff (0 changed files) +$isEmptyDiff = ($pr.changedFiles -eq 0 -and $pr.additions -eq 0 -and $pr.deletions -eq 0) +if ($isEmptyDiff) { + Write-Host "" + Write-Host " 📭 Empty diff: 0 changed files, 0 additions, 0 deletions" -ForegroundColor Yellow +} + +# Check PR timeline for force pushes +$forcePushEvents = @() +$owner, $repo = $Repository -split '/' +$timelineJson = gh api "repos/$owner/$repo/issues/$PRNumber/timeline" --paginate --jq '[.[] | select(.event == "head_ref_force_pushed")]' 2>$null +if ($LASTEXITCODE -eq 0 -and $timelineJson) { + $forcePushEvents = @(($timelineJson -join "`n") | ConvertFrom-Json) +} + +if ($forcePushEvents.Count -gt 0) { + Write-Host "" + foreach ($fp in $forcePushEvents) { + $fpActor = if ($fp.actor) { $fp.actor.login } else { "unknown" } + $fpTime = $fp.created_at + $fpSha = if ($fp.commit_id) { Get-ShortSha $fp.commit_id } else { "unknown" } + Write-Host " 🔄 Force push by @$fpActor at $fpTime (→ $fpSha)" -ForegroundColor Cyan + } + $lastForcePush = $forcePushEvents[-1] + $lastForcePushTime = if ($lastForcePush.created_at) { + [DateTimeOffset]::Parse($lastForcePush.created_at).UtcDateTime + } else { $null } + $lastForcePushActor = if ($lastForcePush.actor) { $lastForcePush.actor.login } else { "unknown" } +} + # --- Step 2: Parse PR body metadata --- Write-Section "Codeflow Metadata" @@ -1081,6 +1111,33 @@ else { Write-Host " ✅ No staleness or conflict warnings found" -ForegroundColor Green } +# Cross-reference force push against conflict/staleness warnings +$conflictMayBeResolved = $false +$stalenessMayBeResolved = $false +if ($lastForcePushTime) { + if ($conflictWarnings.Count -gt 0 -and $lastConflictComment) { + $lastConflictTime = [DateTimeOffset]::Parse($lastConflictComment.createdAt).UtcDateTime + if ($lastForcePushTime -gt $lastConflictTime) { + $conflictMayBeResolved = $true + Write-Host "" + Write-Host " â„šī¸ Force push by @$lastForcePushActor at $($lastForcePush.created_at) is AFTER the last conflict warning" -ForegroundColor Cyan + Write-Host " Conflict may have been resolved via darc vmr resolve-conflict" -ForegroundColor DarkGray + } + } + if ($stalenessWarnings.Count -gt 0 -and $lastStalenessComment) { + $lastWarnTime2 = [DateTimeOffset]::Parse($lastStalenessComment.createdAt).UtcDateTime + if ($lastForcePushTime -gt $lastWarnTime2) { + $stalenessMayBeResolved = $true + Write-Host " â„šī¸ Force push is AFTER the staleness warning — someone may have acted on it" -ForegroundColor Cyan + } + } + if ($isEmptyDiff -and ($conflictMayBeResolved -or $stalenessMayBeResolved)) { + Write-Host "" + Write-Host " 📭 PR has empty diff after force push — codeflow changes may already be in target branch" -ForegroundColor Yellow + Write-Host " This PR is likely a no-op. Consider merging to clear state or closing it." -ForegroundColor DarkGray + } +} + # --- Step 5: Analyze PR branch commits (using commits from gh pr view) --- Write-Section "PR Branch Analysis" @@ -1288,13 +1345,33 @@ Write-Section "Recommendations" $issues = @() # Summarize issues -if ($conflictWarnings.Count -gt 0) { - $fileHint = if ($conflictFiles -and $conflictFiles.Count -gt 0) { " in $($conflictFiles -join ', ')" } else { "" } - $issues += "Conflict detected$fileHint — manual resolution required" +if ($isEmptyDiff -and $lastForcePushTime -and ($conflictMayBeResolved -or $stalenessMayBeResolved)) { + # Special case: empty diff after force push that post-dates warnings + $issues += "PR is a no-op (empty diff after force push by @$lastForcePushActor)" } +else { + if ($conflictWarnings.Count -gt 0) { + $fileHint = if ($conflictFiles -and $conflictFiles.Count -gt 0) { " in $($conflictFiles -join ', ')" } else { "" } + if ($conflictMayBeResolved) { + $issues += "Conflict$fileHint was reported but may be resolved (force push after warning)" + } + else { + $issues += "Conflict detected$fileHint — manual resolution required" + } + } + + if ($stalenessWarnings.Count -gt 0) { + if ($stalenessMayBeResolved) { + $issues += "Staleness warning was active but may be addressed (force push after warning)" + } + else { + $issues += "Staleness warning active — codeflow is blocked" + } + } -if ($stalenessWarnings.Count -gt 0) { - $issues += "Staleness warning active — codeflow is blocked" + if ($isEmptyDiff) { + $issues += "PR has empty diff (0 changed files) — may be a no-op" + } } if ($vmrCommit -and $sourceHeadSha -and $vmrCommit -ne $sourceHeadSha -and $compareStatus -ne 'identical') { @@ -1325,7 +1402,15 @@ else { Write-Host "" Write-Host " Options:" -ForegroundColor White - if ($conflictWarnings.Count -gt 0) { + if ($isEmptyDiff -and $lastForcePushTime -and ($conflictMayBeResolved -or $stalenessMayBeResolved)) { + Write-Host " 1. Merge empty PR — clears codeflow state so Maestro creates a fresh PR" -ForegroundColor White + Write-Host " 2. Close PR — Maestro will create a new one with current VMR content" -ForegroundColor White + if ($subscriptionId) { + Write-Host " 3. Force trigger — push fresh codeflow content into this PR" -ForegroundColor White + Write-Host " darc trigger-subscriptions --id $subscriptionId --force" -ForegroundColor DarkGray + } + } + elseif ($conflictWarnings.Count -gt 0 -and -not $conflictMayBeResolved) { Write-Host " 1. Resolve conflicts — follow the darc vmr resolve-conflict instructions above" -ForegroundColor White if ($subscriptionId) { Write-Host " darc vmr resolve-conflict --subscription $subscriptionId" -ForegroundColor DarkGray From dd57b4c2809f01d0a108b9e35fedccd28d77fc13 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 11:26:29 -0600 Subject: [PATCH 02/11] Restructure analysis: assess current state before reading comments Inspired by the code-review skill pattern (PR #124229), restructure the codeflow analysis to form an independent assessment from primary signals before consulting Maestro comments: - New 'Current State' section (Step 2) synthesizes empty diff, force push events, and activity recency into an immediate verdict: NO-OP / IN PROGRESS / STALE / ACTIVE - PR Branch Analysis now appears before comments, providing commit categorization as primary data - Renamed 'Staleness & Conflict Check' to 'Codeflow History' to clearly signal these are past events, not current state. Framing line directs reader to Current State for present status. - Recommendations driven by current state assessment, with comment history as supporting context The principle: comments tell you the history, not the present. Determine the PR's actual state from primary signals (diff, branch, timeline) before consulting Maestro comments for context. --- .github/skills/vmr-codeflow-status/SKILL.md | 33 +-- .../scripts/Get-CodeflowStatus.ps1 | 192 +++++++++++------- 2 files changed, 137 insertions(+), 88 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index 6b5f0694653b38..65cb6ffce69a34 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -69,14 +69,18 @@ Use this skill when: ## What the Script Does ### PR Analysis Mode (default) -1. **Parses PR metadata** — Extracts VMR commit, subscription ID, build info from PR body -2. **Validates snapshot** — Cross-references PR body commit against branch commit messages to detect stale metadata -3. **Checks VMR freshness** — Compares PR's VMR snapshot against current VMR branch HEAD -4. **Shows pending forward flow** — For behind backflow PRs, finds open forward flow PRs that would close part of the gap -5. **Detects staleness & conflicts** — Finds Maestro "codeflow cannot continue" warnings and "Conflict detected" messages with file lists and resolve commands -6. **Analyzes PR commits** — Categorizes as auto-updates vs manual commits -7. **Traces fixes** (with `-TraceFix`) — Checks if a specific fix has flowed through VMR → codeflow PR -8. **Recommends actions** — Suggests force trigger, close/reopen, merge as-is, resolve conflicts, or wait + +> **Design principle**: Assess current state from primary signals first, then use Maestro comments as historical context — not the other way around. Comments tell you the history, not the present. + +1. **PR Overview** — Basic PR info, flow direction (backflow vs forward flow) +2. **Current State** — Independent assessment from primary signals: empty diff, force pushes, merge status. Produces a one-line verdict (NO-OP / IN PROGRESS / STALE / ACTIVE) before reading any comments +3. **Codeflow Metadata** — Extracts VMR commit, subscription ID, build info from PR body +4. **Snapshot Validation** — Cross-references PR body commit against Version.Details.xml and branch commits to detect stale metadata +5. **Source Freshness** — Compares PR's VMR snapshot against current VMR branch HEAD; shows pending forward flow PRs +6. **PR Branch Analysis** — Categorizes commits as auto-updates vs manual; detects codeflow-like manual commits +7. **Codeflow History** — Maestro comments as historical context (conflict/staleness warnings), cross-referenced against force push timestamps to determine if issues were already addressed +8. **Traces fixes** (with `-TraceFix`) — Checks if a specific fix has flowed through VMR → codeflow PR +9. **Recommends actions** — Driven by current state assessment, informed by history ### Flow Health Mode (`-CheckMissing`) 1. **Checks official build freshness** — Queries `aka.ms` shortlinks for latest published VMR build dates per channel @@ -90,6 +94,12 @@ Use this skill when: ## Interpreting Results +### Current State (assessed first, from primary signals) +- **📭 NO-OP**: Empty diff with force push — PR likely already resolved, changes landed via other paths +- **🔄 IN PROGRESS**: Recent force push within 24h — someone is actively working on it +- **âŗ STALE**: No activity for >3 days — may need attention +- **✅ ACTIVE**: PR has content and recent activity + ### Freshness - **✅ Up to date**: PR has the latest VMR snapshot - **âš ī¸ VMR is N commits ahead**: The PR is missing updates. Check if the missing commits contain the fix you need. @@ -100,14 +110,11 @@ Use this skill when: - **âš ī¸ Mismatch**: PR body is stale — the script automatically uses the branch-derived commit for freshness checks - **â„šī¸ Initial commit only**: PR body can't be verified yet (no "Backflow from" commit exists) -### Staleness & Conflicts +### Codeflow History (Maestro comments as context) - **✅ No warnings**: Maestro can freely update the PR - **âš ī¸ Staleness warning**: A forward flow merged while this backflow PR was open. Maestro blocked further updates. - **🔴 Conflict detected**: Maestro found merge conflicts. Shows conflicting files and `darc vmr resolve-conflict` command. - -### Force Push & Empty Diff Detection -- **🔄 Force push detected**: Shows who force-pushed and when. Cross-references against conflict/staleness warnings to determine if someone already acted (e.g., ran `darc vmr resolve-conflict`). -- **📭 Empty diff**: PR has 0 changed files. If this follows a force push after warnings, the PR is likely a no-op — its changes already landed in the target branch via other paths. Recommendations shift to merge-empty/close/force-trigger. +- **â„šī¸ Force push after warning**: When a force push post-dates a conflict/staleness warning, the issue may already be resolved. The script cross-references timestamps automatically. ### Manual Commits Manual commits on the PR branch are at risk if the PR is closed or force-triggered. The script lists them so you can decide whether to preserve them. diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 1a5e2b040857c5..3eba9c1174669c 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -618,7 +618,7 @@ if (-not $PRNumber) { return } -# --- Step 1: Get PR details (single call for PR + comments + commits) --- +# --- Step 1: PR Overview --- Write-Section "Codeflow PR #$PRNumber in $Repository" if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { @@ -657,10 +657,12 @@ elseif ($isBackflow) { Write-Status "Flow" "Backflow (dotnet/dotnet → $Repository)" "Cyan" } +# --- Step 2: Current State (independent assessment from primary signals) --- +Write-Section "Current State" + # Check for empty diff (0 changed files) $isEmptyDiff = ($pr.changedFiles -eq 0 -and $pr.additions -eq 0 -and $pr.deletions -eq 0) if ($isEmptyDiff) { - Write-Host "" Write-Host " 📭 Empty diff: 0 changed files, 0 additions, 0 deletions" -ForegroundColor Yellow } @@ -673,7 +675,6 @@ if ($LASTEXITCODE -eq 0 -and $timelineJson) { } if ($forcePushEvents.Count -gt 0) { - Write-Host "" foreach ($fp in $forcePushEvents) { $fpActor = if ($fp.actor) { $fp.actor.login } else { "unknown" } $fpTime = $fp.created_at @@ -687,7 +688,28 @@ if ($forcePushEvents.Count -gt 0) { $lastForcePushActor = if ($lastForcePush.actor) { $lastForcePush.actor.login } else { "unknown" } } -# --- Step 2: Parse PR body metadata --- +# Synthesize current state assessment +$prUpdatedTime = if ($pr.updatedAt) { [DateTimeOffset]::Parse($pr.updatedAt).UtcDateTime } else { $null } +$prAgeDays = if ($prUpdatedTime) { ([DateTime]::UtcNow - $prUpdatedTime).TotalDays } else { 0 } +$currentState = if ($isEmptyDiff -and $forcePushEvents.Count -gt 0) { + "NO-OP" +} elseif ($forcePushEvents.Count -gt 0 -and $lastForcePushTime -and ([DateTime]::UtcNow - $lastForcePushTime).TotalHours -lt 24) { + "IN_PROGRESS" +} elseif ($prAgeDays -gt 3) { + "STALE" +} else { + "ACTIVE" +} + +Write-Host "" +switch ($currentState) { + "NO-OP" { Write-Host " 📭 NO-OP — empty diff, likely already resolved" -ForegroundColor Yellow } + "IN_PROGRESS" { Write-Host " 🔄 IN PROGRESS — recent force push, awaiting update" -ForegroundColor Cyan } + "STALE" { Write-Host " âŗ STALE — no recent activity" -ForegroundColor Yellow } + "ACTIVE" { Write-Host " ✅ ACTIVE — PR has content" -ForegroundColor Green } +} + +# --- Step 3: Codeflow Metadata --- Write-Section "Codeflow Metadata" $body = $pr.body @@ -772,7 +794,7 @@ $freshnessRepoLabel = if ($isForwardFlow) { $sourceRepo } else { "VMR" } # Pre-load PR commits for use in validation and later analysis $prCommits = $pr.commits -# --- Step 2b: Determine actual VMR snapshot on the PR branch --- +# --- Step 4: Determine actual VMR snapshot on the PR branch --- # Priority: 1) Version.Details.xml (ground truth), 2) commit messages, 3) PR body $branchVmrCommit = $null $commitMsgVmrCommit = $null @@ -871,7 +893,7 @@ if ($branchVmrCommit -or $vmrCommit) { } } -# --- Step 3: Check source freshness --- +# --- Step 5: Check source freshness --- $freshnessLabel = if ($isForwardFlow) { "Source Freshness" } else { "VMR Freshness" } Write-Section $freshnessLabel @@ -1022,9 +1044,7 @@ else { Write-Warning "Cannot check freshness without source commit and branch info" } -# --- Step 4: Check staleness and conflict warnings (using comments from gh pr view) --- -Write-Section "Staleness & Conflict Check" - +# Collect Maestro comment data (needed by PR Branch Analysis and Codeflow History) $stalenessWarnings = @() $lastStalenessComment = $null @@ -1055,63 +1075,16 @@ if ($pr.comments) { } } -if ($stalenessWarnings.Count -gt 0 -or $conflictWarnings.Count -gt 0) { - if ($conflictWarnings.Count -gt 0) { - Write-Host " 🔴 Conflict detected ($($conflictWarnings.Count) conflict warning(s))" -ForegroundColor Red - Write-Status "Latest conflict" $lastConflictComment.createdAt - - # Extract conflicting files - $conflictFiles = @() - $fileMatches = [regex]::Matches($lastConflictComment.body, '-\s+`([^`]+)`\s*\r?\n') - foreach ($fm in $fileMatches) { - $conflictFiles += $fm.Groups[1].Value - } - if ($conflictFiles.Count -gt 0) { - Write-Host " Conflicting files:" -ForegroundColor Yellow - foreach ($f in $conflictFiles) { - Write-Host " - $f" -ForegroundColor Yellow - } - } - - # Extract VMR commit from the conflict comment - if ($lastConflictComment.body -match 'sources from \[`([a-fA-F0-9]+)`\]') { - Write-Host " Conflicting VMR commit: $($Matches[1])" -ForegroundColor DarkGray - } - - # Extract resolve command - if ($lastConflictComment.body -match '(darc vmr resolve-conflict --subscription [a-fA-F0-9-]+(?:\s+--build [a-fA-F0-9-]+)?)') { - Write-Host "" - Write-Host " Resolve command:" -ForegroundColor White - Write-Host " $($Matches[1])" -ForegroundColor DarkGray - } - } - - if ($stalenessWarnings.Count -gt 0) { - if ($conflictWarnings.Count -gt 0) { Write-Host "" } - Write-Host " âš ī¸ Staleness warning detected ($($stalenessWarnings.Count) warning(s))" -ForegroundColor Yellow - Write-Status "Latest warning" $lastStalenessComment.createdAt - $oppositeFlow = if ($isForwardFlow) { "backflow from VMR merged into $sourceRepo" } else { "forward flow merged into VMR" } - Write-Host " Opposite codeflow ($oppositeFlow) while this PR was open." -ForegroundColor Yellow - Write-Host " Maestro has blocked further codeflow updates to this PR." -ForegroundColor Yellow - - # Extract darc commands from the warning - if ($lastStalenessComment.body -match 'darc trigger-subscriptions --id ([a-fA-F0-9-]+)(?:\s+--force)?') { - Write-Host "" - Write-Host " Suggested commands from Maestro:" -ForegroundColor White - if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-fA-F0-9-]+)\s*\r?\n') { - Write-Host " Normal trigger: $($Matches[1])" - } - if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-fA-F0-9-]+ --force)') { - Write-Host " Force trigger: $($Matches[1])" - } - } +# Extract conflicting files (used in History and Recommendations) +$conflictFiles = @() +if ($lastConflictComment) { + $fileMatches = [regex]::Matches($lastConflictComment.body, '-\s+`([^`]+)`\s*\r?\n') + foreach ($fm in $fileMatches) { + $conflictFiles += $fm.Groups[1].Value } } -else { - Write-Host " ✅ No staleness or conflict warnings found" -ForegroundColor Green -} -# Cross-reference force push against conflict/staleness warnings +# Cross-reference force push against conflict/staleness warnings (data only) $conflictMayBeResolved = $false $stalenessMayBeResolved = $false if ($lastForcePushTime) { @@ -1119,26 +1092,17 @@ if ($lastForcePushTime) { $lastConflictTime = [DateTimeOffset]::Parse($lastConflictComment.createdAt).UtcDateTime if ($lastForcePushTime -gt $lastConflictTime) { $conflictMayBeResolved = $true - Write-Host "" - Write-Host " â„šī¸ Force push by @$lastForcePushActor at $($lastForcePush.created_at) is AFTER the last conflict warning" -ForegroundColor Cyan - Write-Host " Conflict may have been resolved via darc vmr resolve-conflict" -ForegroundColor DarkGray } } if ($stalenessWarnings.Count -gt 0 -and $lastStalenessComment) { $lastWarnTime2 = [DateTimeOffset]::Parse($lastStalenessComment.createdAt).UtcDateTime if ($lastForcePushTime -gt $lastWarnTime2) { $stalenessMayBeResolved = $true - Write-Host " â„šī¸ Force push is AFTER the staleness warning — someone may have acted on it" -ForegroundColor Cyan } } - if ($isEmptyDiff -and ($conflictMayBeResolved -or $stalenessMayBeResolved)) { - Write-Host "" - Write-Host " 📭 PR has empty diff after force push — codeflow changes may already be in target branch" -ForegroundColor Yellow - Write-Host " This PR is likely a no-op. Consider merging to clear state or closing it." -ForegroundColor DarkGray - } } -# --- Step 5: Analyze PR branch commits (using commits from gh pr view) --- +# --- Step 6: PR Branch Analysis --- Write-Section "PR Branch Analysis" if ($prCommits) { @@ -1203,7 +1167,78 @@ if ($prCommits) { } } -# --- Step 6: Trace a specific fix (optional) --- +# --- Step 7: Codeflow History (Maestro comments as historical context) --- +Write-Section "Codeflow History" +Write-Host " Maestro warnings (historical — see Current State for present status):" -ForegroundColor DarkGray + +if ($stalenessWarnings.Count -gt 0 -or $conflictWarnings.Count -gt 0) { + if ($conflictWarnings.Count -gt 0) { + Write-Host " 🔴 Conflict detected ($($conflictWarnings.Count) conflict warning(s))" -ForegroundColor Red + Write-Status "Latest conflict" $lastConflictComment.createdAt + + if ($conflictFiles.Count -gt 0) { + Write-Host " Conflicting files:" -ForegroundColor Yellow + foreach ($f in $conflictFiles) { + Write-Host " - $f" -ForegroundColor Yellow + } + } + + # Extract VMR commit from the conflict comment + if ($lastConflictComment.body -match 'sources from \[`([a-fA-F0-9]+)`\]') { + Write-Host " Conflicting VMR commit: $($Matches[1])" -ForegroundColor DarkGray + } + + # Extract resolve command + if ($lastConflictComment.body -match '(darc vmr resolve-conflict --subscription [a-fA-F0-9-]+(?:\s+--build [a-fA-F0-9-]+)?)') { + Write-Host "" + Write-Host " Resolve command:" -ForegroundColor White + Write-Host " $($Matches[1])" -ForegroundColor DarkGray + } + } + + if ($stalenessWarnings.Count -gt 0) { + if ($conflictWarnings.Count -gt 0) { Write-Host "" } + Write-Host " âš ī¸ Staleness warning detected ($($stalenessWarnings.Count) warning(s))" -ForegroundColor Yellow + Write-Status "Latest warning" $lastStalenessComment.createdAt + $oppositeFlow = if ($isForwardFlow) { "backflow from VMR merged into $sourceRepo" } else { "forward flow merged into VMR" } + Write-Host " Opposite codeflow ($oppositeFlow) while this PR was open." -ForegroundColor Yellow + Write-Host " Maestro has blocked further codeflow updates to this PR." -ForegroundColor Yellow + + # Extract darc commands from the warning + if ($lastStalenessComment.body -match 'darc trigger-subscriptions --id ([a-fA-F0-9-]+)(?:\s+--force)?') { + Write-Host "" + Write-Host " Suggested commands from Maestro:" -ForegroundColor White + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-fA-F0-9-]+)\s*\r?\n') { + Write-Host " Normal trigger: $($Matches[1])" + } + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-fA-F0-9-]+ --force)') { + Write-Host " Force trigger: $($Matches[1])" + } + } + } +} +else { + Write-Host " ✅ No staleness or conflict warnings found" -ForegroundColor Green +} + +# Cross-reference force push against conflict/staleness warnings (historical context) +if ($lastForcePushTime) { + if ($conflictMayBeResolved) { + Write-Host "" + Write-Host " â„šī¸ Force push by @$lastForcePushActor at $($lastForcePush.created_at) is AFTER the last conflict warning" -ForegroundColor Cyan + Write-Host " Conflict may have been resolved via darc vmr resolve-conflict" -ForegroundColor DarkGray + } + if ($stalenessMayBeResolved) { + Write-Host " â„šī¸ Force push is AFTER the staleness warning — someone may have acted on it" -ForegroundColor Cyan + } + if ($isEmptyDiff -and ($conflictMayBeResolved -or $stalenessMayBeResolved)) { + Write-Host "" + Write-Host " 📭 PR has empty diff after force push — codeflow changes may already be in target branch" -ForegroundColor Yellow + Write-Host " This PR is likely a no-op. Consider merging to clear state or closing it." -ForegroundColor DarkGray + } +} + +# --- Step 8: Trace a specific fix (optional) --- if ($TraceFix) { Write-Section "Tracing Fix: $TraceFix" @@ -1339,9 +1374,16 @@ if ($TraceFix) { } } -# --- Step 7: Recommendations --- +# --- Step 9: Recommendations --- Write-Section "Recommendations" +# Lead with current state assessment if NO-OP +if ($currentState -eq "NO-OP") { + Write-Host " 📭 Current state: NO-OP — empty diff with force push activity" -ForegroundColor Yellow + Write-Host " This PR likely has no meaningful changes left to merge." -ForegroundColor DarkGray + Write-Host "" +} + $issues = @() # Summarize issues From a21d3d291409e20d7d1197a18740ebd72fa2ab10 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 11:39:11 -0600 Subject: [PATCH 03/11] Handle merged/closed PR states and empty-diff-only NO-OP - Current State now checks PR state first: MERGED and CLOSED override all other heuristics (force push, staleness, etc.) - Empty diff alone is now sufficient for NO-OP verdict (previously required both empty diff AND force push) - Recommendations are state-aware: skip merge/close advice for already-terminal PRs - Updated SKILL.md with MERGED/CLOSED state documentation Fixes consensus findings from multi-model review (Claude Sonnet 4, GPT-5). --- .github/skills/vmr-codeflow-status/SKILL.md | 4 ++- .../scripts/Get-CodeflowStatus.ps1 | 30 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index 65cb6ffce69a34..58a860e207cad0 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -95,7 +95,9 @@ Use this skill when: ## Interpreting Results ### Current State (assessed first, from primary signals) -- **📭 NO-OP**: Empty diff with force push — PR likely already resolved, changes landed via other paths +- **✅ MERGED**: PR has been merged — no action needed +- **âœ–ī¸ CLOSED**: PR was closed without merging — Maestro should create a replacement +- **📭 NO-OP**: Empty diff — PR likely already resolved, changes landed via other paths - **🔄 IN PROGRESS**: Recent force push within 24h — someone is actively working on it - **âŗ STALE**: No activity for >3 days — may need attention - **✅ ACTIVE**: PR has content and recent activity diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 3eba9c1174669c..937278be522c95 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -691,7 +691,15 @@ if ($forcePushEvents.Count -gt 0) { # Synthesize current state assessment $prUpdatedTime = if ($pr.updatedAt) { [DateTimeOffset]::Parse($pr.updatedAt).UtcDateTime } else { $null } $prAgeDays = if ($prUpdatedTime) { ([DateTime]::UtcNow - $prUpdatedTime).TotalDays } else { 0 } -$currentState = if ($isEmptyDiff -and $forcePushEvents.Count -gt 0) { +$isClosed = $pr.state -eq "CLOSED" +$isMerged = $pr.state -eq "MERGED" +$currentState = if ($isMerged) { + "MERGED" +} elseif ($isClosed) { + "CLOSED" +} elseif ($isEmptyDiff -and $forcePushEvents.Count -gt 0) { + "NO-OP" +} elseif ($isEmptyDiff) { "NO-OP" } elseif ($forcePushEvents.Count -gt 0 -and $lastForcePushTime -and ([DateTime]::UtcNow - $lastForcePushTime).TotalHours -lt 24) { "IN_PROGRESS" @@ -703,6 +711,8 @@ $currentState = if ($isEmptyDiff -and $forcePushEvents.Count -gt 0) { Write-Host "" switch ($currentState) { + "MERGED" { Write-Host " ✅ MERGED — PR has been merged" -ForegroundColor Green } + "CLOSED" { Write-Host " âœ–ī¸ CLOSED — PR was closed without merging" -ForegroundColor DarkGray } "NO-OP" { Write-Host " 📭 NO-OP — empty diff, likely already resolved" -ForegroundColor Yellow } "IN_PROGRESS" { Write-Host " 🔄 IN PROGRESS — recent force push, awaiting update" -ForegroundColor Cyan } "STALE" { Write-Host " âŗ STALE — no recent activity" -ForegroundColor Yellow } @@ -1377,9 +1387,23 @@ if ($TraceFix) { # --- Step 9: Recommendations --- Write-Section "Recommendations" +# Handle terminal states first +if ($currentState -eq "MERGED") { + Write-Host " ✅ PR is merged — no action needed" -ForegroundColor Green + Write-Host " Maestro will create a new codeflow PR if the VMR has newer content." +} +elseif ($currentState -eq "CLOSED") { + Write-Host " âœ–ī¸ PR was closed without merging" -ForegroundColor DarkGray + Write-Host " Maestro should create a new codeflow PR automatically." + if ($subscriptionId) { + Write-Host " To force a new PR: darc trigger-subscriptions --id $subscriptionId" -ForegroundColor DarkGray + } +} +else { + # Lead with current state assessment if NO-OP if ($currentState -eq "NO-OP") { - Write-Host " 📭 Current state: NO-OP — empty diff with force push activity" -ForegroundColor Yellow + Write-Host " 📭 Current state: NO-OP — empty diff, likely already resolved" -ForegroundColor Yellow Write-Host " This PR likely has no meaningful changes left to merge." -ForegroundColor DarkGray Write-Host "" } @@ -1494,4 +1518,6 @@ else { Write-Host " darc trigger-subscriptions --id $subscriptionId" -ForegroundColor DarkGray } } + +} # end of else (non-terminal state) } From 2c68eef8e617139402fd060e38f4b1c4ab67ac21 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 11:40:15 -0600 Subject: [PATCH 04/11] Fix paginated timeline API parsing with --slurp Use --slurp to merge paginated results into a single JSON array before filtering, avoiding invalid JSON when timeline spans multiple pages. Add try/catch for parse resilience. Addresses Copilot review comment. --- .../scripts/Get-CodeflowStatus.ps1 | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 937278be522c95..0a91e1fb3fed3e 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -669,9 +669,15 @@ if ($isEmptyDiff) { # Check PR timeline for force pushes $forcePushEvents = @() $owner, $repo = $Repository -split '/' -$timelineJson = gh api "repos/$owner/$repo/issues/$PRNumber/timeline" --paginate --jq '[.[] | select(.event == "head_ref_force_pushed")]' 2>$null -if ($LASTEXITCODE -eq 0 -and $timelineJson) { - $forcePushEvents = @(($timelineJson -join "`n") | ConvertFrom-Json) +try { + $timelineJson = gh api "repos/$owner/$repo/issues/$PRNumber/timeline" --paginate --slurp --jq 'map(.[] | select(.event == "head_ref_force_pushed"))' 2>$null + if ($LASTEXITCODE -eq 0 -and $timelineJson) { + $forcePushEvents = @($timelineJson | ConvertFrom-Json) + } +} +catch { + Write-Verbose "Failed to parse timeline JSON for force push events: $($_.Exception.Message)" + $forcePushEvents = @() } if ($forcePushEvents.Count -gt 0) { From 31eac1409d39442128f8875207c3998267aba069 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 12:10:13 -0600 Subject: [PATCH 05/11] Replace scripted recommendations with JSON summary + agent guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The script's Step 9 (Recommendations) was 130+ lines of hardcoded if/elseif branching logic reasoning about state combinations. This is exactly what LLMs do better — given structured facts, produce contextual advice. Changes: - Script now emits a [CODEFLOW_SUMMARY] JSON block with all key facts (currentState, freshness, warnings, commits, etc.) - Removed hardcoded recommendations from script - Added 'Generating Recommendations' section to SKILL.md that teaches the agent how to reason over the JSON summary - Agent synthesizes contextual, nuanced recommendations instead of canned text from a decision tree The script still produces full human-readable output for Steps 1-8. The JSON summary is the structured handoff point where the agent takes over for the reasoning-heavy final step. --- .github/skills/vmr-codeflow-status/SKILL.md | 45 ++++- .../scripts/Get-CodeflowStatus.ps1 | 177 +++++------------- 2 files changed, 96 insertions(+), 126 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index 58a860e207cad0..b318bcee6a82ca 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -11,6 +11,8 @@ Analyze the health of VMR codeflow PRs in both directions: > 🚨 **NEVER** use `gh pr review --approve` or `--request-changes`. Only `--comment` is allowed. Approval and blocking are human-only actions. +**Workflow**: Run the script → read the human-readable output + `[CODEFLOW_SUMMARY]` JSON → synthesize recommendations yourself. The script collects data; you generate the advice. + ## Prerequisites - **GitHub CLI (`gh`)** — must be installed and authenticated (`gh auth login`) @@ -80,7 +82,9 @@ Use this skill when: 6. **PR Branch Analysis** — Categorizes commits as auto-updates vs manual; detects codeflow-like manual commits 7. **Codeflow History** — Maestro comments as historical context (conflict/staleness warnings), cross-referenced against force push timestamps to determine if issues were already addressed 8. **Traces fixes** (with `-TraceFix`) — Checks if a specific fix has flowed through VMR → codeflow PR -9. **Recommends actions** — Driven by current state assessment, informed by history +9. **Emits structured summary** — `[CODEFLOW_SUMMARY]` JSON block with all key facts for the agent to reason over + +> **After the script runs**, you (the agent) generate recommendations. The script collects data; you synthesize the advice. See [Generating Recommendations](#generating-recommendations) below. ### Flow Health Mode (`-CheckMissing`) 1. **Checks official build freshness** — Queries `aka.ms` shortlinks for latest published VMR build dates per channel @@ -127,6 +131,45 @@ When using `-TraceFix`: - **✅ Fix is in PR snapshot**: The codeflow PR already includes this fix - **❌ Fix is NOT in PR snapshot**: The PR needs a codeflow update to get this fix +## Generating Recommendations + +After the script outputs the `[CODEFLOW_SUMMARY]` JSON block, **you** synthesize recommendations. Do not parrot the JSON — reason over it. + +### Decision logic + +Read `currentState` first: + +| State | Action | +|-------|--------| +| `MERGED` | No action needed. Mention Maestro will create a new PR if VMR has newer content. | +| `CLOSED` | Suggest triggering a new PR if `subscriptionId` is available. | +| `NO-OP` | PR has no meaningful changes. Recommend closing or merging to clear state. If `subscriptionId` is available, offer force-trigger as a third option. | +| `IN_PROGRESS` | Someone is actively working. Recommend waiting, then checking back. | +| `STALE` | Needs attention — see warnings below for what's blocking. | +| `ACTIVE` | PR is healthy — check freshness and warnings for nuance. | + +Then layer in context from `warnings`, `freshness`, and `commits`: + +- **Unresolved conflict** (`warnings.conflictCount > 0`, `conflictMayBeResolved = false`): Lead with "resolve conflicts" using `darc vmr resolve-conflict --subscription `. Offer "close & reopen" as alternative. +- **Conflict may be resolved** (`conflictMayBeResolved = true`): Note the force push post-dates the conflict warning. Suggest verifying, then merging. +- **Staleness warning active** (`stalenessCount > 0`, `stalenessMayBeResolved = false`): Codeflow is blocked. Options: merge as-is, force trigger, or close & reopen. +- **Manual commits present** (`commits.manual > 0`): Warn that force-trigger or close will lose them. If `commits.codeflowLikeManual > 0`, note the freshness gap may be partially covered. +- **Behind on freshness** (`freshness.aheadBy > 0`): Mention the PR is missing updates. If staleness is blocking, a force trigger is needed. Otherwise, Maestro should auto-update. + +### Darc commands to include + +When recommending actions, include the relevant `darc` command with the actual `subscriptionId` from the summary: + +``` +darc trigger-subscriptions --id # normal trigger +darc trigger-subscriptions --id --force # force trigger (overwrites PR) +darc vmr resolve-conflict --subscription # resolve conflicts locally +``` + +### Tone + +Be direct. Lead with the most important action. Use 2-4 bullet points, not long paragraphs. Include the darc command inline so the user can copy-paste. + ## Darc Commands for Remediation After analyzing the codeflow status, common next steps involve `darc` commands: diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 0a91e1fb3fed3e..cb52950bfc4859 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -1390,140 +1390,67 @@ if ($TraceFix) { } } -# --- Step 9: Recommendations --- -Write-Section "Recommendations" - -# Handle terminal states first -if ($currentState -eq "MERGED") { - Write-Host " ✅ PR is merged — no action needed" -ForegroundColor Green - Write-Host " Maestro will create a new codeflow PR if the VMR has newer content." -} -elseif ($currentState -eq "CLOSED") { - Write-Host " âœ–ī¸ PR was closed without merging" -ForegroundColor DarkGray - Write-Host " Maestro should create a new codeflow PR automatically." - if ($subscriptionId) { - Write-Host " To force a new PR: darc trigger-subscriptions --id $subscriptionId" -ForegroundColor DarkGray - } +# --- Step 9: Structured Summary --- +# Emit a JSON summary for the agent to reason over when generating recommendations. +# The agent should use SKILL.md guidance to synthesize contextual recommendations. + +$summary = [ordered]@{ + prNumber = $PRNumber + repository = $Repository + prState = $pr.state + currentState = $currentState + flowDirection = if ($isForwardFlow) { "forward" } elseif ($isBackflow) { "backflow" } else { "unknown" } + isEmptyDiff = $isEmptyDiff + changedFiles = [int]$pr.changedFiles + additions = [int]$pr.additions + deletions = [int]$pr.deletions + subscriptionId = $subscriptionId + vmrCommit = if ($vmrCommit) { Get-ShortSha $vmrCommit } else { $null } + vmrBranch = $vmrBranch } -else { -# Lead with current state assessment if NO-OP -if ($currentState -eq "NO-OP") { - Write-Host " 📭 Current state: NO-OP — empty diff, likely already resolved" -ForegroundColor Yellow - Write-Host " This PR likely has no meaningful changes left to merge." -ForegroundColor DarkGray - Write-Host "" +# Freshness +$summary.freshness = [ordered]@{ + sourceHeadSha = if ($sourceHeadSha) { Get-ShortSha $sourceHeadSha } else { $null } + compareStatus = $compareStatus + aheadBy = $aheadBy + behindBy = $behindBy + isUpToDate = ($vmrCommit -and $sourceHeadSha -and ($vmrCommit -eq $sourceHeadSha -or $compareStatus -eq 'identical')) } -$issues = @() - -# Summarize issues -if ($isEmptyDiff -and $lastForcePushTime -and ($conflictMayBeResolved -or $stalenessMayBeResolved)) { - # Special case: empty diff after force push that post-dates warnings - $issues += "PR is a no-op (empty diff after force push by @$lastForcePushActor)" -} -else { - if ($conflictWarnings.Count -gt 0) { - $fileHint = if ($conflictFiles -and $conflictFiles.Count -gt 0) { " in $($conflictFiles -join ', ')" } else { "" } - if ($conflictMayBeResolved) { - $issues += "Conflict$fileHint was reported but may be resolved (force push after warning)" - } - else { - $issues += "Conflict detected$fileHint — manual resolution required" - } - } - - if ($stalenessWarnings.Count -gt 0) { - if ($stalenessMayBeResolved) { - $issues += "Staleness warning was active but may be addressed (force push after warning)" - } - else { - $issues += "Staleness warning active — codeflow is blocked" - } - } - - if ($isEmptyDiff) { - $issues += "PR has empty diff (0 changed files) — may be a no-op" - } +# Force pushes +$summary.forcePushes = [ordered]@{ + count = $forcePushEvents.Count + lastActor = if ($lastForcePushActor) { $lastForcePushActor } else { $null } + lastTime = if ($lastForcePushTime) { $lastForcePushTime.ToString("o") } else { $null } } -if ($vmrCommit -and $sourceHeadSha -and $vmrCommit -ne $sourceHeadSha -and $compareStatus -ne 'identical') { - switch ($compareStatus) { - 'ahead' { $issues += "$freshnessRepoLabel is $aheadBy commit(s) ahead of PR snapshot" } - 'behind' { $issues += "$freshnessRepoLabel is $behindBy commit(s) behind PR snapshot" } - 'diverged' { $issues += "$freshnessRepoLabel and PR snapshot diverged ($aheadBy ahead, $behindBy behind)" } - default { $issues += "$freshnessRepoLabel and PR snapshot differ" } - } +# Warnings +$summary.warnings = [ordered]@{ + conflictCount = $conflictWarnings.Count + conflictFiles = $conflictFiles + conflictMayBeResolved = $conflictMayBeResolved + stalenessCount = $stalenessWarnings.Count + stalenessMayBeResolved = $stalenessMayBeResolved } -if ($manualCommits -and $manualCommits.Count -gt 0) { - $issues += "$($manualCommits.Count) manual commit(s) on PR branch" +# Commits +$manualCommitCount = if ($manualCommits) { $manualCommits.Count } else { 0 } +$codeflowLikeCount = if ($codeflowLikeManualCommits) { $codeflowLikeManualCommits.Count } else { 0 } +$summary.commits = [ordered]@{ + total = if ($prCommits) { $prCommits.Count } else { 0 } + manual = $manualCommitCount + codeflowLikeManual = $codeflowLikeCount } -if ($issues.Count -eq 0) { - Write-Host " ✅ CODEFLOW HEALTHY" -ForegroundColor Green - Write-Host " The PR appears to be up to date with no issues detected." +# PR age +$summary.age = [ordered]@{ + daysSinceUpdate = [math]::Round($prAgeDays, 1) + createdAt = $pr.createdAt + updatedAt = $pr.updatedAt } -else { - Write-Host " âš ī¸ CODEFLOW NEEDS ATTENTION" -ForegroundColor Yellow - Write-Host "" - Write-Host " Issues:" -ForegroundColor White - foreach ($issue in $issues) { - Write-Host " â€ĸ $issue" -ForegroundColor Yellow - } - - Write-Host "" - Write-Host " Options:" -ForegroundColor White - - if ($isEmptyDiff -and $lastForcePushTime -and ($conflictMayBeResolved -or $stalenessMayBeResolved)) { - Write-Host " 1. Merge empty PR — clears codeflow state so Maestro creates a fresh PR" -ForegroundColor White - Write-Host " 2. Close PR — Maestro will create a new one with current VMR content" -ForegroundColor White - if ($subscriptionId) { - Write-Host " 3. Force trigger — push fresh codeflow content into this PR" -ForegroundColor White - Write-Host " darc trigger-subscriptions --id $subscriptionId --force" -ForegroundColor DarkGray - } - } - elseif ($conflictWarnings.Count -gt 0 -and -not $conflictMayBeResolved) { - Write-Host " 1. Resolve conflicts — follow the darc vmr resolve-conflict instructions above" -ForegroundColor White - if ($subscriptionId) { - Write-Host " darc vmr resolve-conflict --subscription $subscriptionId" -ForegroundColor DarkGray - } - Write-Host " 2. Close & reopen — abandon this PR and let Maestro create a fresh one" -ForegroundColor White - } - elseif ($stalenessWarnings.Count -gt 0 -and $manualCommits.Count -gt 0) { - if ($codeflowLikeManualCommits -and $codeflowLikeManualCommits.Count -gt 0) { - Write-Host " â„šī¸ Note: Some manual commits appear to contain codeflow-like changes —" -ForegroundColor DarkGray - Write-Host " the reported freshness gap may already be partially addressed" -ForegroundColor DarkGray - Write-Host "" - } - Write-Host " 1. Merge as-is — keep manual commits, get remaining changes in next codeflow PR" -ForegroundColor White - Write-Host " 2. Force trigger — updates codeflow but may revert manual commits" -ForegroundColor White - if ($subscriptionId) { - Write-Host " darc trigger-subscriptions --id $subscriptionId --force" -ForegroundColor DarkGray - } - Write-Host " 3. Close & reopen — loses manual commits, gets fresh codeflow" -ForegroundColor White - } - elseif ($stalenessWarnings.Count -gt 0) { - Write-Host " 1. Merge as-is — get remaining changes in next codeflow PR" -ForegroundColor White - Write-Host " 2. Close & reopen — gets fresh codeflow with all updates" -ForegroundColor White - Write-Host " 3. Force trigger — forces codeflow update into this PR" -ForegroundColor White - if ($subscriptionId) { - Write-Host " darc trigger-subscriptions --id $subscriptionId --force" -ForegroundColor DarkGray - } - } - elseif ($manualCommits.Count -gt 0) { - Write-Host " 1. Wait — Maestro should auto-update (if not stale)" -ForegroundColor White - Write-Host " 2. Trigger manually — if auto-updates seem delayed" -ForegroundColor White - if ($subscriptionId) { - Write-Host " darc trigger-subscriptions --id $subscriptionId" -ForegroundColor DarkGray - } - } - else { - Write-Host " 1. Wait — Maestro should auto-update the PR" -ForegroundColor White - Write-Host " 2. Trigger manually — if auto-updates seem delayed" -ForegroundColor White - if ($subscriptionId) { - Write-Host " darc trigger-subscriptions --id $subscriptionId" -ForegroundColor DarkGray - } - } -} # end of else (non-terminal state) -} +Write-Host "" +Write-Host "[CODEFLOW_SUMMARY]" +Write-Host ($summary | ConvertTo-Json -Depth 4 -Compress) +Write-Host "[/CODEFLOW_SUMMARY]" From c04b163edca4baa6063894d8315ef534ef17df0d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 12:28:07 -0600 Subject: [PATCH 06/11] Fix test findings: isCodeflowPR field, null freshness, negative age, exit code Multi-model testing (Claude Sonnet 4, GPT-5) identified 5 issues: - Add isCodeflowPR boolean to JSON summary for explicit classification - isUpToDate is now null (not false) when freshness data unavailable - daysSinceUpdate clamped to 0 minimum (was negative due to clock skew) - Script exits 0 on completion (gh api failures leaked LASTEXITCODE=1) - SKILL.md: non-codeflow PRs skip all darc command suggestions --- .github/skills/vmr-codeflow-status/SKILL.md | 5 ++++- .../vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index b318bcee6a82ca..4717572532fe37 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -137,7 +137,10 @@ After the script outputs the `[CODEFLOW_SUMMARY]` JSON block, **you** synthesize ### Decision logic -Read `currentState` first: +Check `isCodeflowPR` first — if `false`, skip all codeflow-specific advice: +- **Not a codeflow PR** (`isCodeflowPR = false` or `flowDirection = "unknown"`): State this clearly. No darc commands, no codeflow recommendations. Treat as a normal PR. + +Then read `currentState`: | State | Action | |-------|--------| diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index cb52950bfc4859..9402618d0c120a 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -1399,6 +1399,7 @@ $summary = [ordered]@{ repository = $Repository prState = $pr.state currentState = $currentState + isCodeflowPR = ($isMaestroPR -or $isBackflow -or $isForwardFlow) flowDirection = if ($isForwardFlow) { "forward" } elseif ($isBackflow) { "backflow" } else { "unknown" } isEmptyDiff = $isEmptyDiff changedFiles = [int]$pr.changedFiles @@ -1410,12 +1411,13 @@ $summary = [ordered]@{ } # Freshness +$hasFresnessData = ($null -ne $vmrCommit -and $null -ne $sourceHeadSha) $summary.freshness = [ordered]@{ sourceHeadSha = if ($sourceHeadSha) { Get-ShortSha $sourceHeadSha } else { $null } compareStatus = $compareStatus aheadBy = $aheadBy behindBy = $behindBy - isUpToDate = ($vmrCommit -and $sourceHeadSha -and ($vmrCommit -eq $sourceHeadSha -or $compareStatus -eq 'identical')) + isUpToDate = if ($hasFresnessData) { ($vmrCommit -eq $sourceHeadSha -or $compareStatus -eq 'identical') } else { $null } } # Force pushes @@ -1445,7 +1447,7 @@ $summary.commits = [ordered]@{ # PR age $summary.age = [ordered]@{ - daysSinceUpdate = [math]::Round($prAgeDays, 1) + daysSinceUpdate = [math]::Max(0, [math]::Round($prAgeDays, 1)) createdAt = $pr.createdAt updatedAt = $pr.updatedAt } @@ -1454,3 +1456,6 @@ Write-Host "" Write-Host "[CODEFLOW_SUMMARY]" Write-Host ($summary | ConvertTo-Json -Depth 4 -Compress) Write-Host "[/CODEFLOW_SUMMARY]" + +# Ensure clean exit code (gh api failures may leave $LASTEXITCODE = 1) +exit 0 From cde8a6c4c87edfc0a982362263c98fc9e5ee1e9e Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 12:40:53 -0600 Subject: [PATCH 07/11] Remove redundant NO-OP branch, rename lastWarnTime2 - Collapse duplicate isEmptyDiff check (with/without force push both returned NO-OP) - Rename lastWarnTime2 to lastStalenessTime for consistency --- .../vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 9402618d0c120a..64cc05c71f363e 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -703,8 +703,6 @@ $currentState = if ($isMerged) { "MERGED" } elseif ($isClosed) { "CLOSED" -} elseif ($isEmptyDiff -and $forcePushEvents.Count -gt 0) { - "NO-OP" } elseif ($isEmptyDiff) { "NO-OP" } elseif ($forcePushEvents.Count -gt 0 -and $lastForcePushTime -and ([DateTime]::UtcNow - $lastForcePushTime).TotalHours -lt 24) { @@ -1111,8 +1109,8 @@ if ($lastForcePushTime) { } } if ($stalenessWarnings.Count -gt 0 -and $lastStalenessComment) { - $lastWarnTime2 = [DateTimeOffset]::Parse($lastStalenessComment.createdAt).UtcDateTime - if ($lastForcePushTime -gt $lastWarnTime2) { + $lastStalenessTime = [DateTimeOffset]::Parse($lastStalenessComment.createdAt).UtcDateTime + if ($lastForcePushTime -gt $lastStalenessTime) { $stalenessMayBeResolved = $true } } From 53d6b311c0cc8102aa8a2a42c0aeaf815eb2888c Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 12:51:26 -0600 Subject: [PATCH 08/11] Clarify force-trigger vs close+trigger in recommendation guidance Force-trigger overwrites the existing PR branch; it does not create a new PR. A normal trigger after closing does create a new PR. Added a table and warning to prevent the common mistake of recommending 'close then force-trigger'. --- .github/skills/vmr-codeflow-status/SKILL.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index 4717572532fe37..d7e8f716ab70f7 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -161,13 +161,15 @@ Then layer in context from `warnings`, `freshness`, and `commits`: ### Darc commands to include -When recommending actions, include the relevant `darc` command with the actual `subscriptionId` from the summary: +When recommending actions, include the relevant `darc` command with the actual `subscriptionId` from the summary. Be precise about what each command does: -``` -darc trigger-subscriptions --id # normal trigger -darc trigger-subscriptions --id --force # force trigger (overwrites PR) -darc vmr resolve-conflict --subscription # resolve conflicts locally -``` +| Command | What it does | When to use | +|---------|-------------|-------------| +| `darc trigger-subscriptions --id ` | Normal trigger — only works if subscription isn't stale. Creates a new PR if none exists. | PR was closed, or no PR exists | +| `darc trigger-subscriptions --id --force` | Force trigger — **overwrites the existing PR branch** with fresh VMR content. Does not create a new PR. | PR exists but is stale/no-op and you want to reuse it | +| `darc vmr resolve-conflict --subscription ` | Resolve conflicts locally and push to the PR branch | PR has merge conflicts | + +> âš ī¸ **Common mistake**: Don't say "close then force-trigger" — force-trigger pushes to the *existing* PR. If you close first, use a normal trigger instead (which creates a new PR). The two paths are: (A) force-trigger to refresh the existing PR, or (B) close + normal-trigger to get a new PR. ### Tone From 37db72391fd6d857d4323bcc2e8c50dab979b2f0 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 14:25:36 -0600 Subject: [PATCH 09/11] Address review: fix isCodeflowPR scope, hasFreshnessData typo, add MERGED/CLOSED to verdict list --- .github/skills/vmr-codeflow-status/SKILL.md | 2 +- .../vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index d7e8f716ab70f7..01e3ce99930138 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -75,7 +75,7 @@ Use this skill when: > **Design principle**: Assess current state from primary signals first, then use Maestro comments as historical context — not the other way around. Comments tell you the history, not the present. 1. **PR Overview** — Basic PR info, flow direction (backflow vs forward flow) -2. **Current State** — Independent assessment from primary signals: empty diff, force pushes, merge status. Produces a one-line verdict (NO-OP / IN PROGRESS / STALE / ACTIVE) before reading any comments +2. **Current State** — Independent assessment from primary signals: empty diff, force pushes, merge status. Produces a one-line verdict (NO-OP / IN PROGRESS / STALE / ACTIVE / MERGED / CLOSED) before reading any comments 3. **Codeflow Metadata** — Extracts VMR commit, subscription ID, build info from PR body 4. **Snapshot Validation** — Cross-references PR body commit against Version.Details.xml and branch commits to detect stale metadata 5. **Source Freshness** — Compares PR's VMR snapshot against current VMR branch HEAD; shows pending forward flow PRs diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 64cc05c71f363e..82f04133367c49 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -1397,7 +1397,8 @@ $summary = [ordered]@{ repository = $Repository prState = $pr.state currentState = $currentState - isCodeflowPR = ($isMaestroPR -or $isBackflow -or $isForwardFlow) + isCodeflowPR = ($isBackflow -or $isForwardFlow) + isMaestroAuthored = $isMaestroPR flowDirection = if ($isForwardFlow) { "forward" } elseif ($isBackflow) { "backflow" } else { "unknown" } isEmptyDiff = $isEmptyDiff changedFiles = [int]$pr.changedFiles @@ -1409,13 +1410,13 @@ $summary = [ordered]@{ } # Freshness -$hasFresnessData = ($null -ne $vmrCommit -and $null -ne $sourceHeadSha) +$hasFreshnessData = ($null -ne $vmrCommit -and $null -ne $sourceHeadSha) $summary.freshness = [ordered]@{ sourceHeadSha = if ($sourceHeadSha) { Get-ShortSha $sourceHeadSha } else { $null } compareStatus = $compareStatus aheadBy = $aheadBy behindBy = $behindBy - isUpToDate = if ($hasFresnessData) { ($vmrCommit -eq $sourceHeadSha -or $compareStatus -eq 'identical') } else { $null } + isUpToDate = if ($hasFreshnessData) { ($vmrCommit -eq $sourceHeadSha -or $compareStatus -eq 'identical') } else { $null } } # Force pushes From aa956689a80f26cef0e5bb7b45ed026b330a044c Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 10 Feb 2026 17:42:17 -0600 Subject: [PATCH 10/11] Use mergeable status to resolve false conflict reports --- .../scripts/Get-CodeflowStatus.ps1 | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 82f04133367c49..99b1fd28936f66 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -111,7 +111,7 @@ function Get-CodeflowPRHealth { $result = @{ Status = "âš ī¸ Unknown"; Color = "Yellow"; HasConflict = $false; HasStaleness = $false; WasResolved = $false; Details = @() } - $prJson = gh pr view $PRNumber -R $Repo --json body,comments,updatedAt 2>$null + $prJson = gh pr view $PRNumber -R $Repo --json body,comments,updatedAt,mergeable 2>$null if ($LASTEXITCODE -ne 0 -or -not $prJson) { return $result } try { $prDetail = ($prJson -join "`n") | ConvertFrom-Json } catch { return $result } @@ -134,17 +134,26 @@ function Get-CodeflowPRHealth { $wasConflict = $hasConflict $wasStaleness = $hasStaleness - # If issues detected, check if they were resolved via Codeflow verification - # Codeflow verification SUCCESS reliably indicates conflict resolution, - # but staleness (code moved on) needs a newer commit after the warning + # If issues detected, check if they were resolved + # Two signals: (1) PR is mergeable (no git conflict), (2) Codeflow verification SUCCESS + # Either one clears the conflict flag. Staleness needs a newer commit after the warning. if ($hasConflict -or $hasStaleness) { + # Check mergeable status — if PR has no git conflicts, clear the conflict flag + $isMergeable = $false + if ($prDetail.PSObject.Properties.Name -contains 'mergeable' -and $prDetail.mergeable -eq 'MERGEABLE') { + $isMergeable = $true + } + if ($isMergeable -and $hasConflict) { + $hasConflict = $false + } + $checksJson = gh pr checks $PRNumber -R $Repo --json name,state 2>$null if ($LASTEXITCODE -eq 0 -and $checksJson) { try { $checks = ($checksJson -join "`n") | ConvertFrom-Json $codeflowCheck = @($checks | Where-Object { $_.name -match 'Codeflow verification' }) | Select-Object -First 1 - if ($codeflowCheck -and $codeflowCheck.state -eq 'SUCCESS') { - # Codeflow verification passing means no merge conflict + if (($codeflowCheck -and $codeflowCheck.state -eq 'SUCCESS') -or $isMergeable) { + # No merge conflict — either Codeflow verification passes or PR is mergeable $hasConflict = $false # For staleness, check if there are commits after the last staleness warning if ($hasStaleness) { From c9e96de7f895750165f5b61d59f6f29bc210d1b8 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 11 Feb 2026 09:14:48 -0600 Subject: [PATCH 11/11] force-push: emit warning and fetchSucceeded on timeline API failure Addresses review comment: when gh api .../timeline fails, emit Write-Warning so the user knows force push data is missing, and add forcePushes.fetchSucceeded boolean to the JSON summary so recommendation logic can account for incomplete timeline data. --- .../vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 99b1fd28936f66..9c968ec8e9931d 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -678,14 +678,20 @@ if ($isEmptyDiff) { # Check PR timeline for force pushes $forcePushEvents = @() $owner, $repo = $Repository -split '/' +$forcePushFetchSucceeded = $false try { $timelineJson = gh api "repos/$owner/$repo/issues/$PRNumber/timeline" --paginate --slurp --jq 'map(.[] | select(.event == "head_ref_force_pushed"))' 2>$null if ($LASTEXITCODE -eq 0 -and $timelineJson) { $forcePushEvents = @($timelineJson | ConvertFrom-Json) + $forcePushFetchSucceeded = $true + } elseif ($LASTEXITCODE -ne 0) { + Write-Warning "Could not fetch PR timeline for force push detection (gh api exit code $LASTEXITCODE). Current state assessment may be incomplete." + } else { + $forcePushFetchSucceeded = $true } } catch { - Write-Verbose "Failed to parse timeline JSON for force push events: $($_.Exception.Message)" + Write-Warning "Failed to parse timeline JSON for force push events: $($_.Exception.Message)" $forcePushEvents = @() } @@ -1431,6 +1437,7 @@ $summary.freshness = [ordered]@{ # Force pushes $summary.forcePushes = [ordered]@{ count = $forcePushEvents.Count + fetchSucceeded = $forcePushFetchSucceeded lastActor = if ($lastForcePushActor) { $lastForcePushActor } else { $null } lastTime = if ($lastForcePushTime) { $lastForcePushTime.ToString("o") } else { $null } }