From 3f8666ee0c0ebaff383d4fe2e2d8a318dfeed47e Mon Sep 17 00:00:00 2001 From: m3taversal Date: Thu, 7 May 2026 11:58:22 -0400 Subject: [PATCH] fix(substantive_fixer): surface silent-skip reasons at INFO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two silent paths in substantive_fix_cycle masked a 13-day stall: 1. Filter strips all candidates → return 0,0 with no log. With LIMIT 3 ordered created_at ASC, if the oldest 3 have no fixer-actionable tags (e.g. eval_issues=[] from leo:skipped+domain:request_changes), the cycle silently picks the same head-of-line every tick. 2. _fix_pr early-returns logged at DEBUG only — invisible without fleet-wide DEBUG. Skip reasons (no_claim_files, no_review_comments, not_open lock, worktree_failed, etc.) never surfaced in journalctl. Patch: log skipped candidate eval_issues when no actionable rows found (path 1); promote DEBUG→INFO for per-PR skip reasons (path 2). Zero behavior change — observability only. Diagnosis context: 98 PRs stuck >3d, last successful substantive_fixer event 2026-04-24. Need journal evidence to choose between (a) one-line fix to the cycle, (b) larger _fix_pr regression. (Ship Step 2 directive.) --- lib/substantive_fixer.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/substantive_fixer.py b/lib/substantive_fixer.py index 9fa33c3..4d6dcd5 100644 --- a/lib/substantive_fixer.py +++ b/lib/substantive_fixer.py @@ -539,6 +539,7 @@ async def substantive_fix_cycle(conn, max_workers=None) -> tuple[int, int]: # Filter to only PRs with substantive issues (not just mechanical) substantive_rows = [] + skipped_no_tags = [] for row in rows: try: issues = json.loads(row["eval_issues"] or "[]") @@ -546,8 +547,20 @@ async def substantive_fix_cycle(conn, max_workers=None) -> tuple[int, int]: continue if set(issues) & (FIXABLE_TAGS | CONVERTIBLE_TAGS | UNFIXABLE_TAGS): substantive_rows.append(row) + else: + skipped_no_tags.append((row["number"], issues)) if not substantive_rows: + # Visibility for the LIMIT-3 head-of-line block: if the oldest + # candidates have no fixer-actionable tags (e.g. eval_issues=[], + # broken_wiki_links only), the cycle silently returns 0 — and the + # next cycle picks the same head-of-line, forever. Log the eval_issues + # of skipped candidates so the journal makes the block visible. + if skipped_no_tags: + logger.info( + "Substantive fix cycle: 0 actionable from %d candidate(s) — head-of-line: %s", + len(rows), skipped_no_tags, + ) return 0, 0 fixed = 0 @@ -559,7 +572,13 @@ async def substantive_fix_cycle(conn, max_workers=None) -> tuple[int, int]: if result.get("action"): fixed += 1 elif result.get("skipped"): - logger.debug("PR #%d: substantive fix skipped: %s", row["number"], result.get("reason")) + # Was DEBUG — promoted to INFO to make stuck-PR root cause + # visible without enabling DEBUG fleet-wide. (Ship Apr 24+ + # silent skip diagnosis.) + logger.info( + "PR #%d: substantive fix skipped: %s", + row["number"], result.get("reason"), + ) except Exception: logger.exception("PR #%d: substantive fix failed", row["number"]) errors += 1