fix(substantive_fixer): surface silent-skip reasons at INFO

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.)
This commit is contained in:
m3taversal 2026-05-07 11:58:22 -04:00
parent 87f97eb4fa
commit 3f8666ee0c

View file

@ -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) # Filter to only PRs with substantive issues (not just mechanical)
substantive_rows = [] substantive_rows = []
skipped_no_tags = []
for row in rows: for row in rows:
try: try:
issues = json.loads(row["eval_issues"] or "[]") issues = json.loads(row["eval_issues"] or "[]")
@ -546,8 +547,20 @@ async def substantive_fix_cycle(conn, max_workers=None) -> tuple[int, int]:
continue continue
if set(issues) & (FIXABLE_TAGS | CONVERTIBLE_TAGS | UNFIXABLE_TAGS): if set(issues) & (FIXABLE_TAGS | CONVERTIBLE_TAGS | UNFIXABLE_TAGS):
substantive_rows.append(row) substantive_rows.append(row)
else:
skipped_no_tags.append((row["number"], issues))
if not substantive_rows: 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 return 0, 0
fixed = 0 fixed = 0
@ -559,7 +572,13 @@ async def substantive_fix_cycle(conn, max_workers=None) -> tuple[int, int]:
if result.get("action"): if result.get("action"):
fixed += 1 fixed += 1
elif result.get("skipped"): 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: except Exception:
logger.exception("PR #%d: substantive fix failed", row["number"]) logger.exception("PR #%d: substantive fix failed", row["number"])
errors += 1 errors += 1