Skip to content

fix(triage-bot): stop a cleanup run that exits early from wedging the daemon - #5244

Merged
springfall2008 merged 1 commit into
mainfrom
fix/triage-bot-cleanup-leaves-dirty-clone
Sep 26, 2026
Merged

springfall2008 merged 1 commit into
mainfrom
fix/triage-bot-cleanup-leaves-dirty-clone

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

What happened

On 2026-09-26 the bot stopped working partway through a poll cycle:

  1. The /pr-cleanup run for fix(givtcp): fill per-inverter arg slots by REST endpoint index, not discovery order (#5209) #5216 edited givtcp.py, its tests and the debug journal, and merged origin/main locally. It then started its pre-commit run in the background and ended with "The pre-commit quick suite is still running in the background. I'll push and reply once it finishes." In claude -p mode nothing comes back to finish the job. The session exited 0, the background job died with it, and nothing was committed, pushed or posted.
  2. process_bot_cleanup_pr() treated exit 0 as success and removed BOT_CLEANUP. The PR was left with no push, no reply, and nothing marking it for a retry.
  3. On the next flow, sync_repo() ran git checkout main, and git refused because the uncommitted edits would be overwritten. Every flow starts with sync_repo(), so every flow failed at that step from then on.

Changes

  • claude_env() sets CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 for every claude invocation. This removes run_in_background from the Bash tool, so the setting is enforced by the tool itself. It does not depend on a prompt instruction. I checked the effect against Claude Code 2.1.283 by asking a -p session whether its Bash schema has the parameter: YES without the variable, NO with it. The value is assigned rather than set with setdefault, so an exported value cannot turn backgrounding back on. claude_env() therefore now always returns a dict; on the default route it is the daemon's environment plus this one key.
  • sync_repo() stashes leftovers before the checkout (new set_aside_leftovers()). The stash message is labelled triage-daemon: … and git records the branch the changes came from. If a merge is in progress it is aborted first, because both stash and checkout refuse a tree mid-merge. Leftovers are stashed rather than force-discarded, because the daemon cannot tell a half-finished fix from noise.
  • A cleanup that exits 0 without finishing is marked failed (new unfinished_cleanup_reason()). Uncommitted tracked changes, unpushed commits, or no upstream to compare against all lead to mark_pr_cleanup_failed(pr, reason), which now accepts a reason in the same way mark_pr_review_failed does. This follows the same principle as the review flow's comment count: exit status is not evidence that the work was done.
  • pr-cleanup/SKILL.md: run the quality gate in the foreground and never end a turn waiting on it. The skill also stated that an unpushed local commit "is discarded automatically the next time this runs". That was false, because sync_repo() only resets main. It now describes the new check.

Consequence to be aware of

A cleanup that stops at a failed quality gate, posts its "didn't push" summary and leaves the local merge commit behind is now reported BOT_FAILED. Before this change it was silently counted as success. This is intended, because the cleanup did not complete.

Testing

  • python3 tools/test_triage_daemon.py: 284 tests pass (270 before). The new tests cover the stash before the checkout, the merge abort, silence on a clean tree, each unfinished-run reason, the orchestrator marking failure with a reason, and the environment variable on the Claude, --ollama and --ollama_review routes. Existing tests that asserted claude_env() returns None now assert that no Ollama overrides are present.
  • I reproduced the wedge against a throwaway real git repo: a plain git checkout main refuses; the new sync_repo() stashes (On fix/x: triage-daemon: …) and lands on main; unfinished_cleanup_reason() reports both the uncommitted file and the unpushed commits.
  • Pre-commit hooks pass on the changed files.

The daemon runs from its own clone of main, so it picks up this change only after merge and a daemon restart.

🤖 Generated with Claude Code

… daemon

On 2026-09-26 the PR #5216 cleanup backgrounded its pre-commit run, ended on
"I'll push and reply once it finishes", and exited 0 with its fixes
uncommitted. The daemon took exit 0 as success and cleared BOT_CLEANUP; the
next sync_repo() then failed at `git checkout main` because the leftover edits
would be overwritten - and since every flow starts with sync_repo(), every
flow after it failed the same way.

- claude_env() now sets CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 for every
  claude invocation, which removes run_in_background from the Bash tool
  (checked against Claude Code 2.1.283). A -p session cannot come back for a
  background result.
- sync_repo() stashes anything left uncommitted (aborting an unfinished merge
  first) before checking out main, so leftovers are kept but can no longer
  block the checkout.
- process_bot_cleanup_pr() checks the clone after a clean exit and marks the
  PR BOT_FAILED, with the reason, when work was left uncommitted or unpushed.
- pr-cleanup SKILL.md: run the quality gate in the foreground, and drop the
  false claim that an unpushed commit is discarded on the next run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit e9cee18 into main Sep 26, 2026
2 checks passed
@springfall2008
springfall2008 deleted the fix/triage-bot-cleanup-leaves-dirty-clone branch September 26, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant