From b3ab1abc08f97a3ad00e059faceb02f9c7115f77 Mon Sep 17 00:00:00 2001 From: Senna46 <29295263+Senna46@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:43:17 +0900 Subject: [PATCH 1/6] fix: Record no-changes bugs as SKIPPED_NO_CHANGES instead of null When Claude analyzes a bug but makes no code changes, the bug was recorded with fix_commit_sha=null. Since isBugProcessed treats null as "processed successfully", these bugs were never retried, silently preventing detection of new comments on the same PR. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index 1dd1840..95324e3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -217,11 +217,11 @@ class FixoolyDaemon { repo: repoFullName, prNumber: pr.number, })), - null + "SKIPPED_NO_CHANGES" ); logger.info( - `No changes made for PR #${pr.number}. Bugs recorded as processed.`, + `No changes made for PR #${pr.number}. Bugs recorded as skipped.`, { prNumber: pr.number, repo: repoFullName } ); } From 13f9970378d892e083d2547311849a54dc523233 Mon Sep 17 00:00:00 2001 From: Senna46 <29295263+Senna46@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:44:36 +0900 Subject: [PATCH 2/6] feat: Post comment on PR when no code changes were made Fixooly now posts a summary comment listing the analyzed bugs even when Claude determines no changes are needed, so users can confirm the daemon ran and see which bugs were skipped. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/main.ts b/src/main.ts index 95324e3..2282833 100644 --- a/src/main.ts +++ b/src/main.ts @@ -211,6 +211,8 @@ class FixoolyDaemon { } ); } else { + await this.postNoChangesComment(pr, bugs); + this.state.recordProcessedBugs( bugs.map((b) => ({ bugId: b.bugId, @@ -289,6 +291,45 @@ class FixoolyDaemon { } } + // ============================================================ + // Post no-changes comment on the PR + // ============================================================ + + private async postNoChangesComment( + pr: { owner: string; repo: string; number: number }, + bugs: import("./types.js").BugbotBug[] + ): Promise { + const bugList = bugs + .map((b) => `- ⏭️ Skipped: **${b.title}**`) + .join("\n"); + + const body = + `${AUTOFIX_COMMENT_MARKER}\n` + + `[Fixooly](https://github.com/Senna46/fixooly) ` + + `analyzed ${bugs.length} bug(s) but determined no code changes were needed.\n\n` + + bugList; + + try { + await this.github.createIssueComment( + pr.owner, + pr.repo, + pr.number, + body + ); + logger.debug("Posted no-changes comment on PR.", { + prNumber: pr.number, + repo: `${pr.owner}/${pr.repo}`, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.warn("Failed to post no-changes comment on PR.", { + error: message, + prNumber: pr.number, + repo: `${pr.owner}/${pr.repo}`, + }); + } + } + // ============================================================ // Shutdown // ============================================================ From 42b8c8555e9ad5a9c5b87884a40fdcef24de9636 Mon Sep 17 00:00:00 2001 From: "senna-fixooly[bot]" <267219142+senna-fixooly[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:51:30 +0900 Subject: [PATCH 3/6] fix: Make SKIPPED_NO_CHANGES bugs retryable by excluding them from processed state - Change from null to string doesn't alter behavior Applied via Fixooly --- src/bugbotMonitor.ts | 4 ++-- src/state.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bugbotMonitor.ts b/src/bugbotMonitor.ts index 7c6ffe4..de558f8 100644 --- a/src/bugbotMonitor.ts +++ b/src/bugbotMonitor.ts @@ -229,8 +229,8 @@ export class BugbotMonitor { // ============================================================ private computeSinceForRepo(repo: string): string | undefined { - if (this.state.hasFailedBugsForRepo(repo)) { - logger.debug("Skipping since filter to retry failed bugs.", { repo }); + if (this.state.hasRetryableBugsForRepo(repo)) { + logger.debug("Skipping since filter to retry failed/skipped bugs.", { repo }); return undefined; } const lookbackMs = DEFAULT_LOOKBACK_DAYS * 24 * 60 * 60 * 1000; diff --git a/src/state.ts b/src/state.ts index 2fb417b..f734e3d 100644 --- a/src/state.ts +++ b/src/state.ts @@ -50,14 +50,14 @@ export class StateStore { const row = this.db .prepare("SELECT fix_commit_sha FROM processed_bugs WHERE bug_id = ?") .get(bugId) as { fix_commit_sha: string | null } | undefined; - // Bugs marked as FAILED should be retried - return row !== undefined && row.fix_commit_sha !== "FAILED"; + // Bugs marked as FAILED or SKIPPED_NO_CHANGES should be retried + return row !== undefined && row.fix_commit_sha !== "FAILED" && row.fix_commit_sha !== "SKIPPED_NO_CHANGES"; } - hasFailedBugsForRepo(repo: string): boolean { + hasRetryableBugsForRepo(repo: string): boolean { const row = this.db .prepare( - "SELECT 1 FROM processed_bugs WHERE fix_commit_sha = 'FAILED' AND repo = ? LIMIT 1" + "SELECT 1 FROM processed_bugs WHERE fix_commit_sha IN ('FAILED', 'SKIPPED_NO_CHANGES') AND repo = ? LIMIT 1" ) .get(repo); return row !== undefined; From 37d3bc82319435805efebb25372c60ea288002ce Mon Sep 17 00:00:00 2001 From: "senna-fixooly[bot]" <267219142+senna-fixooly[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:57:14 +0900 Subject: [PATCH 4/6] fix: Deduplicate no-changes comments to prevent infinite spam on retryable SKIPPED_NO_CHANGES bugs - Retryable skipped bugs cause infinite comment spam Applied via Fixooly --- src/githubClient.ts | 33 +++++++++++++++++++++++++++++++++ src/main.ts | 33 ++++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/githubClient.ts b/src/githubClient.ts index d0812e3..ef6a284 100644 --- a/src/githubClient.ts +++ b/src/githubClient.ts @@ -393,6 +393,39 @@ export class GitHubClient { return data.id; } + + async hasIssueCommentContaining( + owner: string, + repo: string, + prNumber: number, + marker: string + ): Promise { + logger.debug("Checking for existing issue comment with marker.", { + owner, + repo, + prNumber, + }); + + const octokit = await this.getOctokitForOwner(owner); + + for await (const response of octokit.paginate.iterator( + octokit.rest.issues.listComments, + { + owner, + repo, + issue_number: prNumber, + per_page: 100, + } + )) { + for (const comment of response.data) { + if (comment.body?.includes(marker)) { + return true; + } + } + } + + return false; + } } // ============================================================ diff --git a/src/main.ts b/src/main.ts index 2282833..1cd8d4c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,7 @@ import { StateStore } from "./state.js"; import type { Config, FixResult, PrBugReport } from "./types.js"; const AUTOFIX_COMMENT_MARKER = ""; +const AUTOFIX_NO_CHANGES_MARKER = ""; class FixoolyDaemon { private config: Config; @@ -299,17 +300,31 @@ class FixoolyDaemon { pr: { owner: string; repo: string; number: number }, bugs: import("./types.js").BugbotBug[] ): Promise { - const bugList = bugs - .map((b) => `- ⏭️ Skipped: **${b.title}**`) - .join("\n"); + try { + const alreadyPosted = await this.github.hasIssueCommentContaining( + pr.owner, + pr.repo, + pr.number, + AUTOFIX_NO_CHANGES_MARKER + ); + if (alreadyPosted) { + logger.debug("No-changes comment already exists on PR, skipping.", { + prNumber: pr.number, + repo: `${pr.owner}/${pr.repo}`, + }); + return; + } - const body = - `${AUTOFIX_COMMENT_MARKER}\n` + - `[Fixooly](https://github.com/Senna46/fixooly) ` + - `analyzed ${bugs.length} bug(s) but determined no code changes were needed.\n\n` + - bugList; + const bugList = bugs + .map((b) => `- ⏭️ Skipped: **${b.title}**`) + .join("\n"); + + const body = + `${AUTOFIX_COMMENT_MARKER}\n${AUTOFIX_NO_CHANGES_MARKER}\n` + + `[Fixooly](https://github.com/Senna46/fixooly) ` + + `analyzed ${bugs.length} bug(s) but determined no code changes were needed.\n\n` + + bugList; - try { await this.github.createIssueComment( pr.owner, pr.repo, From a8f0741b56bf1c388e8e15ab66f816ef64cb4389 Mon Sep 17 00:00:00 2001 From: "senna-fixooly[bot]" <267219142+senna-fixooly[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:03:51 +0900 Subject: [PATCH 5/6] fix: Treat SKIPPED_NO_CHANGES as terminal state in isBugProcessed to prevent infinite retries - SKIPPED_NO_CHANGES bugs retried infinitely despite being "terminal" Applied via Fixooly --- src/state.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state.ts b/src/state.ts index f734e3d..ebd9153 100644 --- a/src/state.ts +++ b/src/state.ts @@ -50,8 +50,8 @@ export class StateStore { const row = this.db .prepare("SELECT fix_commit_sha FROM processed_bugs WHERE bug_id = ?") .get(bugId) as { fix_commit_sha: string | null } | undefined; - // Bugs marked as FAILED or SKIPPED_NO_CHANGES should be retried - return row !== undefined && row.fix_commit_sha !== "FAILED" && row.fix_commit_sha !== "SKIPPED_NO_CHANGES"; + // Only FAILED bugs should be retried; SKIPPED_NO_CHANGES is terminal + return row !== undefined && row.fix_commit_sha !== "FAILED"; } hasRetryableBugsForRepo(repo: string): boolean { From 8b6b938bd025a36f453f5673b128bc70fe96ec22 Mon Sep 17 00:00:00 2001 From: "senna-fixooly[bot]" <267219142+senna-fixooly[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:08:02 +0900 Subject: [PATCH 6/6] fix: Exclude SKIPPED_NO_CHANGES from hasRetryableBugsForRepo query to prevent permanent since-filter bypass - `hasRetryableBugsForRepo` incorrectly includes terminal `SKIPPED_NO_CHANGES` state Applied via Fixooly --- src/state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state.ts b/src/state.ts index ebd9153..00b8d7e 100644 --- a/src/state.ts +++ b/src/state.ts @@ -57,7 +57,7 @@ export class StateStore { hasRetryableBugsForRepo(repo: string): boolean { const row = this.db .prepare( - "SELECT 1 FROM processed_bugs WHERE fix_commit_sha IN ('FAILED', 'SKIPPED_NO_CHANGES') AND repo = ? LIMIT 1" + "SELECT 1 FROM processed_bugs WHERE fix_commit_sha = 'FAILED' AND repo = ? LIMIT 1" ) .get(repo); return row !== undefined;