From 759db4db358efa7a1fdbadb8372d271cab4fa959 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:10:04 +0000 Subject: [PATCH 1/3] Initial plan From ab3e7345710b379beec1b0fdd9b1cd22baaed6b1 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:13:15 +0000 Subject: [PATCH 2/3] feat(ci): add auto-remediation to repository reconciliation workflow - Comment on non-draft PRs missing canonical issue references - Auto-close superseded draft PRs when the newer PR declares supersession - Auto-close the drift report issue when all findings are resolved - Upgrade pull-requests permission from read to write - Add workflow to README catalog Agent-Logs-Url: https://github.com/groupthinking/EventRelay/sessions/36224c07-1067-42d5-a6f8-0c386ec50ed6 Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com> --- .github/workflows/README.md | 1 + .../workflows/repository-reconciliation.yml | 53 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 37aaa7354..52cbc2fd0 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -30,6 +30,7 @@ workflow; this README is the index. | API-cost PostgreSQL | `api-cost-postgres.yml` | push / PR when substrate changes; manual | Exercise fresh, upgrade-from-002, and round-trip migrations plus runtime-role integration tests on PostgreSQL 16 | | Deploy to Google Cloud Run | `deploy-cloud-run.yml` | manual | Run migrations, deploy the bounded delivery-disabled worker, then promote a tested API candidate | | Emergency Stop | `emergency-stop.yml` | manual (typed confirmation) | Operational kill-switch announcement for running automation | +| Repository Reconciliation | `repository-reconciliation.yml` | daily (13:17 UTC); manual | Drift report: PRs without canonical issues, competing PRs, stale branches; auto-comments on untracked PRs and auto-closes superseded drafts | ## Key Workflows diff --git a/.github/workflows/repository-reconciliation.yml b/.github/workflows/repository-reconciliation.yml index 60fb04a93..c5ecce727 100644 --- a/.github/workflows/repository-reconciliation.yml +++ b/.github/workflows/repository-reconciliation.yml @@ -8,7 +8,7 @@ on: permissions: contents: read issues: write - pull-requests: read + pull-requests: write concurrency: group: repository-reconciliation @@ -145,3 +145,54 @@ jobs: } else { await github.rest.issues.create({ owner, repo, title, body }); } + + // --- Remediation: comment on non-draft PRs missing a canonical issue --- + const botTag = ""; + for (const pr of untracked) { + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: pr.number, per_page: 100 + }); + if (comments.some(c => (c.body || "").includes(botTag))) continue; + await github.rest.issues.createComment({ + owner, repo, issue_number: pr.number, + body: `${botTag}\nāš ļø **Repository reconciliation**: this PR does not reference exactly one canonical issue.\n\nPlease add a \`Closes #\` reference in the PR description so it can be tracked against the delivery plan.\n\nSee governance: #898` + }); + } + + // --- Remediation: auto-close superseded competing draft PRs --- + const supersedesPattern = /supersedes\s+(?:stalled\s+)?(?:draft\s+)?#(\d+)/gi; + for (const [issue, numbers] of duplicates) { + // For each competing group, check if any PR explicitly supersedes another. + const competingPRs = pulls.filter(p => numbers.includes(p.number)); + for (const pr of competingPRs) { + const superseded = [...(pr.body || "").matchAll(supersedesPattern)] + .map(m => Number(m[1])); + for (const targetNum of superseded) { + const target = competingPRs.find(p => p.number === targetNum); + if (!target || !target.draft) continue; + // Close the superseded draft PR with explanation. + const closedComments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: targetNum, per_page: 100 + }); + const closeTag = ""; + if (closedComments.some(c => (c.body || "").includes(closeTag))) continue; + await github.rest.issues.createComment({ + owner, repo, issue_number: targetNum, + body: `${closeTag}\nšŸ”„ **Auto-closed by repository reconciliation**: this draft PR has been superseded by #${pr.number}, which implements the same issue #${issue}.\n\nSee governance: #898` + }); + await github.rest.pulls.update({ + owner, repo, pull_number: targetNum, state: "closed" + }); + core.info(`Closed superseded draft PR #${targetNum} (superseded by #${pr.number} for issue #${issue})`); + } + } + } + + // --- Close the drift report issue when all findings are zero --- + if (report && untracked.length === 0 && duplicates.length === 0 && staleBranches.length === 0) { + await github.rest.issues.update({ + owner, repo, issue_number: report.number, + state: "closed", state_reason: "completed" + }); + core.info("All drift findings resolved — closed the report issue."); + } From 4d97a9b85f3f5a20f06eade7a0dd953575490a69 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:47:21 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Fix:=20Auto-close=20block=20uses=20`report`?= =?UTF-8?q?=20(only=20set=20for=20pre-existing=20open=20issues),=20so=20fr?= =?UTF-8?q?eshly-created=20clean=20drift=20reports=20are=20left=20open,=20?= =?UTF-8?q?causing=20the=20workflow=20to=20oscillate=20=E2=80=94=20spammin?= =?UTF-8?q?g=20a=20new=20"Repository=20drift=20report"=20issue=20every=20c?= =?UTF-8?q?lean=20run.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the issue reported at .github/workflows/repository-reconciliation.yml:191 ## Bug In `.github/workflows/repository-reconciliation.yml`, the report issue is located via a search restricted to open issues: ```js const query = `repo: ``` * When an open report already exists, `report` is defined. * Otherwise the code **creates** a new issue but discards the return value: ```js await github.rest.issues.create({ owner, repo, title, body }); // not captured ``` The auto-close block was then guarded by `report`: ```js if (report && untracked.length === 0 && ...) { await github.rest.issues.update({ ..., state: "closed" }); } ``` ### Failure mode (concrete trigger) Consider the exact state right after a prior clean run closed the report: 1. Run N (clean): no *open* report exists → search returns nothing → `report` is `undefined` → a **brand new** report issue is created → close block is skipped (`report` falsy) → the fresh issue is left **OPEN**. 2. Run N+1 (still clean): the search now finds that open issue → `report` defined → close block runs → issue closed. 3. Run N+2 (still clean): no open report again → creates a new one, leaves it open... This oscillation spams a new open "Repository drift report" issue on every other run even when the repository is perfectly clean, defeating the auto-close feature. ## Fix Capture the created issue's number and use a single `reportNumber` variable in the close condition. In `@actions/github-script`, `github.rest.issues.create()` is an Octokit request that resolves to a response object whose payload is under `.data`, so `created.data.number` is the correct path. ```js let reportNumber; if (report) { reportNumber = report.number; await github.rest.issues.update({ owner, repo, issue_number: report.number, body }); } else { const created = await github.rest.issues.create({ owner, repo, title, body }); reportNumber = created.data.number; } ... if (reportNumber && untracked.length === 0 && duplicates.length === 0 && staleBranches.length === 0) { await github.rest.issues.update({ owner, repo, issue_number: reportNumber, state: "closed", state_reason: "completed" }); } ``` Now a freshly-created clean report is closed in the same run, eliminating the oscillation. Co-authored-by: Vercel Co-authored-by: groupthinking --- .github/workflows/repository-reconciliation.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/repository-reconciliation.yml b/.github/workflows/repository-reconciliation.yml index c5ecce727..ba9297f11 100644 --- a/.github/workflows/repository-reconciliation.yml +++ b/.github/workflows/repository-reconciliation.yml @@ -138,12 +138,15 @@ jobs: const report = existing.data.items.find(item => item.title === title); const body = lines.join("\n"); + let reportNumber; if (report) { + reportNumber = report.number; await github.rest.issues.update({ owner, repo, issue_number: report.number, body }); } else { - await github.rest.issues.create({ owner, repo, title, body }); + const created = await github.rest.issues.create({ owner, repo, title, body }); + reportNumber = created.data.number; } // --- Remediation: comment on non-draft PRs missing a canonical issue --- @@ -189,9 +192,9 @@ jobs: } // --- Close the drift report issue when all findings are zero --- - if (report && untracked.length === 0 && duplicates.length === 0 && staleBranches.length === 0) { + if (reportNumber && untracked.length === 0 && duplicates.length === 0 && staleBranches.length === 0) { await github.rest.issues.update({ - owner, repo, issue_number: report.number, + owner, repo, issue_number: reportNumber, state: "closed", state_reason: "completed" }); core.info("All drift findings resolved — closed the report issue.");