From 38282bed46b1282ce7836f4cdd0db68548b70638 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:01:13 +0000 Subject: [PATCH 1/2] Initial plan From 33ec4a7b1d47bf9a0a22522518a1454769d54660 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:18:08 +0000 Subject: [PATCH 2/2] Block auto re-dispatch of retry-blocked Copilot topics in issue-monster Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/issue-monster.lock.yml | 155 ++++++++++++++++++++++- .github/workflows/issue-monster.md | 95 +++++++++++++- 2 files changed, 244 insertions(+), 6 deletions(-) diff --git a/.github/workflows/issue-monster.lock.yml b/.github/workflows/issue-monster.lock.yml index c3307bd43ce..c9f87b1a86f 100644 --- a/.github/workflows/issue-monster.lock.yml +++ b/.github/workflows/issue-monster.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e502e4f5cc7f799cfdf7d7260bbfaf212ae72b0a2917b0125c9cb8a9aa7adb62","body_hash":"131c693fc1634aec500e89ddb3c13ecb739943046e23bbd07420368998c62724","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.83.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"17310f2dac6469863de8f6af528982246b18b3f6f4a404fc463968170d44d8d3","body_hash":"640c4bdbb7e81ce552cd2e1106b1bf2cc175c6eb756fdf28930dc4b089af61f2","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.83.0"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43","digest":"sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43@sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -159,6 +159,7 @@ on: # core.setOutput('issue_numbers', ''); # core.setOutput('issue_list', ''); # core.setOutput('issue_context', ''); + # core.setOutput('retry_blocked_list', ''); # core.setOutput('has_issues', 'false'); # return; # } @@ -178,7 +179,10 @@ on: # 'waiting-for-feedback', # 'needs-more-info', # 'no-bot', - # 'no-campaign' + # 'no-campaign', + # // Topics where repeated Copilot attempts were closed without merging. + # // A maintainer must remove this label before a new attempt is dispatched. + # 'copilot-retry-blocked' # ]; # # // Labels that indicate an issue is a GOOD candidate for auto-assignment @@ -308,7 +312,56 @@ on: # core.warning(`🛡️ Integrity filter diagnostic: ${integrityFilteredIssues.length} issue(s) were skipped due to integrity policy: #${integrityFilteredIssues.join(', #')}. These issues will be excluded from this run.`); # } # + # // Pre-flight retry-blocked check: build a map of topics that Copilot already + # // attempted and had closed without merging. Repeated attempts on the same topic + # // burn a full agent session each time and need a human checkpoint first. + # const RETRY_BLOCK_THRESHOLD = 2; + # const MIN_TOPIC_LENGTH = 20; + # // Normalize a title into a comparable topic key: drop bracketed prefixes + # // (e.g. "[WIP] ", "[copilot-opt] "), lowercase, and strip punctuation. + # const normalizeTopic = (title) => (title || '') + # .replace(/^\s*(\[[^\]]*\]\s*)+/, '') + # .toLowerCase() + # .replace(/[^a-z0-9]+/g, ' ') + # .trim(); + # const closedTopicCounts = new Map(); + # try { + # const closedPRQuery = `is:pr is:closed is:unmerged author:app/copilot-swe-agent repo:${owner}/${repo}`; + # const closedPRResponse = await github.rest.search.issuesAndPullRequests({ + # q: closedPRQuery, + # per_page: 100, + # sort: 'created', + # order: 'desc' + # }); + # for (const pr of closedPRResponse.data.items) { + # const topic = normalizeTopic(pr.title); + # if (topic.length < MIN_TOPIC_LENGTH) continue; + # const entry = closedTopicCounts.get(topic) || { count: 0, prs: [] }; + # entry.count += 1; + # entry.prs.push(pr.number); + # closedTopicCounts.set(topic, entry); + # } + # const blockedTopics = [...closedTopicCounts.entries()].filter(([, v]) => v.count >= RETRY_BLOCK_THRESHOLD); + # core.info(`Retry pre-flight: ${closedTopicCounts.size} closed-unmerged Copilot topics, ${blockedTopics.length} at or above the retry-block threshold (${RETRY_BLOCK_THRESHOLD})`); + # } catch (error) { + # // A failed pre-flight check must not block the run; fall back to no topic data. + # core.warning(`Could not build retry-blocked topic map: ${error.message}`); + # } + # // Returns the blocked topic entry for an issue title, or null when not blocked. + # const findRetryBlock = (title) => { + # const topic = normalizeTopic(title); + # if (topic.length < MIN_TOPIC_LENGTH) return null; + # for (const [closedTopic, entry] of closedTopicCounts) { + # if (entry.count < RETRY_BLOCK_THRESHOLD) continue; + # if (topic === closedTopic || topic.includes(closedTopic) || closedTopic.includes(topic)) { + # return entry; + # } + # } + # return null; + # }; + # # // Filter and score issues + # const retryBlockedIssues = []; # const scoredIssues = issuesWithDetails # .filter(issue => { # // Exclude issues that already have assignees @@ -354,6 +407,15 @@ on: # return false; # } # + # // Block topics that Copilot already attempted twice or more without a merge. + # // These require human review before another agent session is spent. + # const retryBlock = findRetryBlock(issue.title); + # if (retryBlock) { + # core.warning(`🛑 Skipping #${issue.number}: retry-blocked topic — ${retryBlock.count} prior Copilot PR(s) closed without merging (#${retryBlock.prs.join(', #')}). Human review required.`); + # retryBlockedIssues.push({ number: issue.number, count: retryBlock.count, prs: retryBlock.prs }); + # return false; + # } + # # return true; # }) # .map(issue => { @@ -430,10 +492,18 @@ on: # core.info(`Top candidates:\n${issueList.split('\n').slice(0, 10).join('\n')}`); # } # + # const retryBlockedList = retryBlockedIssues + # .map(i => `#${i.number} | prior closed Copilot PRs: ${i.count} (#${i.prs.join(', #')})`) + # .join('\n'); + # if (retryBlockedIssues.length > 0) { + # core.warning(`🛑 ${retryBlockedIssues.length} issue(s) were retry-blocked and excluded from assignment:\n${retryBlockedList}`); + # } + # # core.setOutput('issue_count', scoredIssues.length); # core.setOutput('issue_numbers', issueNumbers); # core.setOutput('issue_list', issueList); # core.setOutput('issue_context', issueContext); + # core.setOutput('retry_blocked_list', retryBlockedList); # # if (scoredIssues.length === 0) { # core.info('🍽️ No suitable candidate issues - the plate is empty!'); @@ -447,6 +517,7 @@ on: # core.setOutput('issue_numbers', ''); # core.setOutput('issue_list', ''); # core.setOutput('issue_context', ''); + # core.setOutput('retry_blocked_list', ''); # core.setOutput('has_issues', 'false'); # } workflow_dispatch: @@ -655,6 +726,7 @@ jobs: GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_COUNT: ${{ needs.pre_activation.outputs.issue_count }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_LIST: ${{ needs.pre_activation.outputs.issue_list }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_NUMBERS: ${{ needs.pre_activation.outputs.issue_numbers }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_RETRY_BLOCKED_LIST: ${{ needs.pre_activation.outputs.retry_blocked_list }} # poutine:ignore untrusted_checkout_exec run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" @@ -722,6 +794,7 @@ jobs: GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_COUNT: ${{ needs.pre_activation.outputs.issue_count }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_LIST: ${{ needs.pre_activation.outputs.issue_list }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_NUMBERS: ${{ needs.pre_activation.outputs.issue_numbers }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_RETRY_BLOCKED_LIST: ${{ needs.pre_activation.outputs.retry_blocked_list }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -746,6 +819,7 @@ jobs: GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_COUNT: ${{ needs.pre_activation.outputs.issue_count }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_LIST: ${{ needs.pre_activation.outputs.issue_list }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_NUMBERS: ${{ needs.pre_activation.outputs.issue_numbers }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_RETRY_BLOCKED_LIST: ${{ needs.pre_activation.outputs.retry_blocked_list }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -770,7 +844,8 @@ jobs: GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_CONTEXT: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_CONTEXT, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_COUNT: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_COUNT, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_LIST: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_LIST, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_NUMBERS: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_NUMBERS + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_NUMBERS: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ISSUE_NUMBERS, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_RETRY_BLOCKED_LIST: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_RETRY_BLOCKED_LIST } }); - name: Validate prompt placeholders @@ -2148,6 +2223,7 @@ jobs: issue_list: ${{ steps.search.outputs.issue_list }} issue_numbers: ${{ steps.search.outputs.issue_numbers }} matched_command: '' + retry_blocked_list: ${{ steps.search.outputs.retry_blocked_list }} search_result: ${{ steps.search.outcome }} setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} @@ -2301,6 +2377,7 @@ jobs: core.setOutput('issue_numbers', ''); core.setOutput('issue_list', ''); core.setOutput('issue_context', ''); + core.setOutput('retry_blocked_list', ''); core.setOutput('has_issues', 'false'); return; } @@ -2320,7 +2397,10 @@ jobs: 'waiting-for-feedback', 'needs-more-info', 'no-bot', - 'no-campaign' + 'no-campaign', + // Topics where repeated Copilot attempts were closed without merging. + // A maintainer must remove this label before a new attempt is dispatched. + 'copilot-retry-blocked' ]; // Labels that indicate an issue is a GOOD candidate for auto-assignment @@ -2450,7 +2530,56 @@ jobs: core.warning(`🛡️ Integrity filter diagnostic: ${integrityFilteredIssues.length} issue(s) were skipped due to integrity policy: #${integrityFilteredIssues.join(', #')}. These issues will be excluded from this run.`); } + // Pre-flight retry-blocked check: build a map of topics that Copilot already + // attempted and had closed without merging. Repeated attempts on the same topic + // burn a full agent session each time and need a human checkpoint first. + const RETRY_BLOCK_THRESHOLD = 2; + const MIN_TOPIC_LENGTH = 20; + // Normalize a title into a comparable topic key: drop bracketed prefixes + // (e.g. "[WIP] ", "[copilot-opt] "), lowercase, and strip punctuation. + const normalizeTopic = (title) => (title || '') + .replace(/^\s*(\[[^\]]*\]\s*)+/, '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, ' ') + .trim(); + const closedTopicCounts = new Map(); + try { + const closedPRQuery = `is:pr is:closed is:unmerged author:app/copilot-swe-agent repo:${owner}/${repo}`; + const closedPRResponse = await github.rest.search.issuesAndPullRequests({ + q: closedPRQuery, + per_page: 100, + sort: 'created', + order: 'desc' + }); + for (const pr of closedPRResponse.data.items) { + const topic = normalizeTopic(pr.title); + if (topic.length < MIN_TOPIC_LENGTH) continue; + const entry = closedTopicCounts.get(topic) || { count: 0, prs: [] }; + entry.count += 1; + entry.prs.push(pr.number); + closedTopicCounts.set(topic, entry); + } + const blockedTopics = [...closedTopicCounts.entries()].filter(([, v]) => v.count >= RETRY_BLOCK_THRESHOLD); + core.info(`Retry pre-flight: ${closedTopicCounts.size} closed-unmerged Copilot topics, ${blockedTopics.length} at or above the retry-block threshold (${RETRY_BLOCK_THRESHOLD})`); + } catch (error) { + // A failed pre-flight check must not block the run; fall back to no topic data. + core.warning(`Could not build retry-blocked topic map: ${error.message}`); + } + // Returns the blocked topic entry for an issue title, or null when not blocked. + const findRetryBlock = (title) => { + const topic = normalizeTopic(title); + if (topic.length < MIN_TOPIC_LENGTH) return null; + for (const [closedTopic, entry] of closedTopicCounts) { + if (entry.count < RETRY_BLOCK_THRESHOLD) continue; + if (topic === closedTopic || topic.includes(closedTopic) || closedTopic.includes(topic)) { + return entry; + } + } + return null; + }; + // Filter and score issues + const retryBlockedIssues = []; const scoredIssues = issuesWithDetails .filter(issue => { // Exclude issues that already have assignees @@ -2496,6 +2625,15 @@ jobs: return false; } + // Block topics that Copilot already attempted twice or more without a merge. + // These require human review before another agent session is spent. + const retryBlock = findRetryBlock(issue.title); + if (retryBlock) { + core.warning(`🛑 Skipping #${issue.number}: retry-blocked topic — ${retryBlock.count} prior Copilot PR(s) closed without merging (#${retryBlock.prs.join(', #')}). Human review required.`); + retryBlockedIssues.push({ number: issue.number, count: retryBlock.count, prs: retryBlock.prs }); + return false; + } + return true; }) .map(issue => { @@ -2572,10 +2710,18 @@ jobs: core.info(`Top candidates:\n${issueList.split('\n').slice(0, 10).join('\n')}`); } + const retryBlockedList = retryBlockedIssues + .map(i => `#${i.number} | prior closed Copilot PRs: ${i.count} (#${i.prs.join(', #')})`) + .join('\n'); + if (retryBlockedIssues.length > 0) { + core.warning(`🛑 ${retryBlockedIssues.length} issue(s) were retry-blocked and excluded from assignment:\n${retryBlockedList}`); + } + core.setOutput('issue_count', scoredIssues.length); core.setOutput('issue_numbers', issueNumbers); core.setOutput('issue_list', issueList); core.setOutput('issue_context', issueContext); + core.setOutput('retry_blocked_list', retryBlockedList); if (scoredIssues.length === 0) { core.info('🍽️ No suitable candidate issues - the plate is empty!'); @@ -2589,6 +2735,7 @@ jobs: core.setOutput('issue_numbers', ''); core.setOutput('issue_list', ''); core.setOutput('issue_context', ''); + core.setOutput('retry_blocked_list', ''); core.setOutput('has_issues', 'false'); } diff --git a/.github/workflows/issue-monster.md b/.github/workflows/issue-monster.md index f258655c55c..8470a23addd 100644 --- a/.github/workflows/issue-monster.md +++ b/.github/workflows/issue-monster.md @@ -98,6 +98,7 @@ on: core.setOutput('issue_numbers', ''); core.setOutput('issue_list', ''); core.setOutput('issue_context', ''); + core.setOutput('retry_blocked_list', ''); core.setOutput('has_issues', 'false'); return; } @@ -117,7 +118,10 @@ on: 'waiting-for-feedback', 'needs-more-info', 'no-bot', - 'no-campaign' + 'no-campaign', + // Topics where repeated Copilot attempts were closed without merging. + // A maintainer must remove this label before a new attempt is dispatched. + 'copilot-retry-blocked' ]; // Labels that indicate an issue is a GOOD candidate for auto-assignment @@ -247,7 +251,56 @@ on: core.warning(`🛡️ Integrity filter diagnostic: ${integrityFilteredIssues.length} issue(s) were skipped due to integrity policy: #${integrityFilteredIssues.join(', #')}. These issues will be excluded from this run.`); } + // Pre-flight retry-blocked check: build a map of topics that Copilot already + // attempted and had closed without merging. Repeated attempts on the same topic + // burn a full agent session each time and need a human checkpoint first. + const RETRY_BLOCK_THRESHOLD = 2; + const MIN_TOPIC_LENGTH = 20; + // Normalize a title into a comparable topic key: drop bracketed prefixes + // (e.g. "[WIP] ", "[copilot-opt] "), lowercase, and strip punctuation. + const normalizeTopic = (title) => (title || '') + .replace(/^\s*(\[[^\]]*\]\s*)+/, '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, ' ') + .trim(); + const closedTopicCounts = new Map(); + try { + const closedPRQuery = `is:pr is:closed is:unmerged author:app/copilot-swe-agent repo:${owner}/${repo}`; + const closedPRResponse = await github.rest.search.issuesAndPullRequests({ + q: closedPRQuery, + per_page: 100, + sort: 'created', + order: 'desc' + }); + for (const pr of closedPRResponse.data.items) { + const topic = normalizeTopic(pr.title); + if (topic.length < MIN_TOPIC_LENGTH) continue; + const entry = closedTopicCounts.get(topic) || { count: 0, prs: [] }; + entry.count += 1; + entry.prs.push(pr.number); + closedTopicCounts.set(topic, entry); + } + const blockedTopics = [...closedTopicCounts.entries()].filter(([, v]) => v.count >= RETRY_BLOCK_THRESHOLD); + core.info(`Retry pre-flight: ${closedTopicCounts.size} closed-unmerged Copilot topics, ${blockedTopics.length} at or above the retry-block threshold (${RETRY_BLOCK_THRESHOLD})`); + } catch (error) { + // A failed pre-flight check must not block the run; fall back to no topic data. + core.warning(`Could not build retry-blocked topic map: ${error.message}`); + } + // Returns the blocked topic entry for an issue title, or null when not blocked. + const findRetryBlock = (title) => { + const topic = normalizeTopic(title); + if (topic.length < MIN_TOPIC_LENGTH) return null; + for (const [closedTopic, entry] of closedTopicCounts) { + if (entry.count < RETRY_BLOCK_THRESHOLD) continue; + if (topic === closedTopic || topic.includes(closedTopic) || closedTopic.includes(topic)) { + return entry; + } + } + return null; + }; + // Filter and score issues + const retryBlockedIssues = []; const scoredIssues = issuesWithDetails .filter(issue => { // Exclude issues that already have assignees @@ -293,6 +346,15 @@ on: return false; } + // Block topics that Copilot already attempted twice or more without a merge. + // These require human review before another agent session is spent. + const retryBlock = findRetryBlock(issue.title); + if (retryBlock) { + core.warning(`🛑 Skipping #${issue.number}: retry-blocked topic — ${retryBlock.count} prior Copilot PR(s) closed without merging (#${retryBlock.prs.join(', #')}). Human review required.`); + retryBlockedIssues.push({ number: issue.number, count: retryBlock.count, prs: retryBlock.prs }); + return false; + } + return true; }) .map(issue => { @@ -369,10 +431,18 @@ on: core.info(`Top candidates:\n${issueList.split('\n').slice(0, 10).join('\n')}`); } + const retryBlockedList = retryBlockedIssues + .map(i => `#${i.number} | prior closed Copilot PRs: ${i.count} (#${i.prs.join(', #')})`) + .join('\n'); + if (retryBlockedIssues.length > 0) { + core.warning(`🛑 ${retryBlockedIssues.length} issue(s) were retry-blocked and excluded from assignment:\n${retryBlockedList}`); + } + core.setOutput('issue_count', scoredIssues.length); core.setOutput('issue_numbers', issueNumbers); core.setOutput('issue_list', issueList); core.setOutput('issue_context', issueContext); + core.setOutput('retry_blocked_list', retryBlockedList); if (scoredIssues.length === 0) { core.info('🍽️ No suitable candidate issues - the plate is empty!'); @@ -386,6 +456,7 @@ on: core.setOutput('issue_numbers', ''); core.setOutput('issue_list', ''); core.setOutput('issue_context', ''); + core.setOutput('retry_blocked_list', ''); core.setOutput('has_issues', 'false'); } @@ -429,6 +500,7 @@ jobs: issue_numbers: ${{ steps.search.outputs.issue_numbers }} issue_list: ${{ steps.search.outputs.issue_list }} issue_context: ${{ steps.search.outputs.issue_context }} + retry_blocked_list: ${{ steps.search.outputs.retry_blocked_list }} has_issues: ${{ steps.search.outputs.has_issues }} safe-outputs: @@ -481,12 +553,13 @@ The issue search has already been performed in the pre-activation job with smart **Filtering Applied:** - ✅ Only open issues **with "cookie" label** (indicating approved work queue items from automated workflows) -- ✅ Excluded issues with labels: wontfix, duplicate, invalid, question, discussion, needs-discussion, blocked, on-hold, waiting-for-feedback, needs-more-info, no-bot, no-campaign +- ✅ Excluded issues with labels: wontfix, duplicate, invalid, question, discussion, needs-discussion, blocked, on-hold, waiting-for-feedback, needs-more-info, no-bot, no-campaign, copilot-retry-blocked - ✅ Excluded issues with campaign labels (campaign:*) - these are managed by campaign orchestrators - ✅ Excluded issues that already have assignees - ✅ Excluded issues that have sub-issues (parent/organizing issues) - ✅ Excluded issues with closed or merged PRs (treating those as complete) - ✅ Excluded issues with open PRs from Copilot coding agent (already being worked on) +- 🛑 Excluded **retry-blocked topics**: issues whose normalized title matches two or more Copilot PRs that were closed without merging - ✅ Prioritized issues with labels: good-first-issue, bug, security, documentation, enhancement, feature, performance, tech-debt, refactoring **Scoring System:** @@ -505,6 +578,11 @@ Issues are scored and sorted by priority: **Issue Count**: ${{ needs.pre_activation.outputs.issue_count }} **Issue Numbers**: ${{ needs.pre_activation.outputs.issue_numbers }} +**Retry-Blocked Issues (excluded — human review required):** +``` +${{ needs.pre_activation.outputs.retry_blocked_list }} +``` + **Available Issues (sorted by priority score):** ``` ${{ needs.pre_activation.outputs.issue_list }} @@ -617,6 +695,18 @@ The Copilot coding agent will: 3. Create a pull request with the fix 4. Follow the repository's AGENTS.md guidelines +### 4a. Handle Retry-Blocked Issues + +The pre-activation job lists issues whose topic already has **two or more Copilot PRs closed without merging**. Never assign these to Copilot. + +For each issue in the retry-blocked list (up to the `add_comment` limit, and only if you have not already commented on it in a prior run), post a checkpoint comment asking for human review: + +``` +safeoutputs/add_comment(item_number=, body="🛑 **Retry blocked — human review required**\n\nThis topic already has prior Copilot pull requests that were closed without merging. Automatic re-dispatch is disabled to avoid spending another agent session on a blocked topic.\n\nA maintainer should review the prior closed PRs, clarify the requirements, and then add a 'retry approved' comment (or remove the `copilot-retry-blocked` label) before this issue is reassigned.") +``` + +Also state in your final summary how many issues were retry-blocked. If the retry-blocked list is empty, skip this step entirely. + ### 5. Add Comment to Each Assigned Issue For each issue you assign, use the `add_comment` tool from the `safeoutputs` MCP server to add a comment: @@ -655,6 +745,7 @@ Issue Monster runs frequently (every 30 minutes), so keeping each run lean is cr - ✅ **Skip integrity-blocked issues**: If `issue_read` is blocked by integrity policy, skip that issue and continue — never call `missing_data` for integrity errors - ❌ **Don't force batching**: If only 1-2 clearly separate issues exist, assign only those - ❌ **Never assign pull requests**: `assign_to_agent` is for issues only — never pass a PR number +- 🛑 **Never re-dispatch retry-blocked topics**: if an issue appears in the retry-blocked list, comment for human review instead of assigning it ## skill: `issue-monster-report-formatting` ---