Parent
Part of #204 (Phase 4: Hardening)
Problem
When using the pr merge strategy, two things can block a PR from merging:
- Review comments — humans leave feedback, nobody addresses it. The PR sits with unresolved threads.
- Failing CI checks — tests fail, lint errors, type errors. The PR sits red until someone manually fixes the branch.
Even after all feedback is resolved and CI is green, the PR still requires a human to click "Merge." For autonomous workflows, users should be able to set a grace period after which the system auto-merges.
Solution
Extend the PR polling to detect unresolved comments AND failing CI checks, dispatch a polecat to address them, and optionally auto-merge the PR after a configurable grace period once everything is green.
User Settings
Opt-in via town settings in the "Refinery" section:
// townConfig
auto_resolve_pr_feedback: boolean; // default: false
auto_merge_delay_minutes: number | null; // default: null (disabled)
Auto-resolve PR feedback toggle:
When enabled, a polecat is automatically dispatched to address unresolved review comments and failing CI checks on open PRs.
Auto-merge delay input (only visible when auto-resolve is enabled):
After all CI checks pass and all review threads are resolved, automatically merge the PR after this delay. Set to 0 for immediate merge. Leave empty to require manual merge.
[ 15 ] minutes
Common presets as quick-select buttons: 0 (immediate), 15 min, 1 hour, 4 hours, disabled.
The delay gives humans a window to review the agent's fixes before auto-merge. If new comments or CI failures appear during the delay, the timer resets.
Auto-Merge Flow
The poll_pr side effect already checks PR status on each tick. Extend it:
if (prStatus === "open" && townConfig.auto_merge_delay_minutes !== null) {
const allGreen = !hasUnresolvedComments && !hasFailingChecks && allChecksPass;
if (allGreen) {
const readySince = getAutoMergeReadySince(sql, mrBeadId);
if (!readySince) {
// First tick where everything is green — start the timer
setAutoMergeReadySince(sql, mrBeadId, now());
} else {
const elapsed = Date.now() - new Date(readySince).getTime();
if (elapsed >= townConfig.auto_merge_delay_minutes * 60_000) {
// Grace period elapsed — emit merge event
ctx.insertEvent("pr_auto_merge", {
bead_id: mrBeadId,
payload: { mr_bead_id: mrBeadId, pr_url, pr_number },
});
}
}
} else {
// Not all green — reset the timer
clearAutoMergeReadySince(sql, mrBeadId);
}
}
The pr_auto_merge event is processed by applyEvent:
case "pr_auto_merge": {
// Merge the PR via GitHub API, then complete the review
// (handled as a side effect — the actual merge is async)
completeReviewMerged(sql, payload.mr_bead_id);
return;
}
The actual gh pr merge call happens in the side effect phase:
case "merge_pr": {
return async () => {
await exec(`gh pr merge ${pr_number} --merge --delete-branch`);
ctx.insertEvent("pr_status_changed", {
bead_id: mrBeadId,
payload: { pr_url, pr_state: "merged" },
});
};
}
Timer Reset Conditions
The auto-merge timer resets (goes back to waiting) if ANY of these occur during the grace period:
- A new review comment is posted
- A CI check starts failing
- A new commit is pushed to the branch (by anyone — human or agent)
- The PR is converted to draft
This prevents merging a PR whose state changed during the grace window.
Reconciler Architecture Integration
Follows the event-driven reconciliation architecture from PR #1336.
New Event Type: pr_feedback_detected
Keep the event lightweight — the polecat has gh CLI access and will fetch the full details itself:
const PrFeedbackDetectedPayload = z.object({
mr_bead_id: z.string(),
pr_url: z.string(),
pr_number: z.number(),
repo: z.string(),
branch: z.string(),
has_unresolved_comments: z.boolean(),
has_failing_checks: z.boolean(),
});
No need to encode the full comment threads or CI logs in the event — that would be redundant. The polecat calls gh pr view, gh pr checks, and gh api to get everything it needs directly from GitHub.
Event Emission (Alarm Side Effect)
The poll_pr side effect is extended. When auto_resolve_pr_feedback is enabled and the poll detects issues:
// In the poll_pr side effect (actions.ts)
if (prStatus === "open" && townConfig.auto_resolve_pr_feedback) {
const hasComments = await hasUnresolvedComments(pr_url, token);
const hasFailingChecks = await hasFailingCIChecks(pr_url, token);
if ((hasComments || hasFailingChecks) && !hasExistingFeedbackBead(mrBeadId)) {
ctx.insertEvent("pr_feedback_detected", {
bead_id: mrBeadId,
payload: {
mr_bead_id: mrBeadId, pr_url, pr_number, repo, branch,
has_unresolved_comments: hasComments,
has_failing_checks: hasFailingChecks,
},
});
}
}
The checks are lightweight API calls:
- Comments:
GET /repos/{owner}/{repo}/pulls/{number}/comments — check if any threads are unresolved
- CI:
GET /repos/{owner}/{repo}/commits/{sha}/check-runs — check if any checks failed
Event Application
case "pr_feedback_detected": {
const mrBead = beadOps.getBead(sql, payload.mr_bead_id);
if (!mrBead || mrBead.status === "closed" || mrBead.status === "failed") return;
// Check for existing non-terminal feedback bead to prevent duplicates
if (hasExistingFeedbackBead(sql, payload.mr_bead_id)) return;
const feedbackBead = beadOps.createBead(sql, {
type: "issue",
title: buildFeedbackBeadTitle(payload),
body: buildFeedbackPrompt(payload),
rig_id: mrBead.rig_id,
labels: ["gt:pr-feedback"],
metadata: {
pr_feedback_for: payload.mr_bead_id,
pr_url: payload.pr_url,
branch: payload.branch,
},
});
// Feedback bead blocks the MR bead (same pattern as rework beads)
insertDependency(sql, payload.mr_bead_id, feedbackBead.bead_id, "blocks");
updateReviewMetadata(sql, payload.mr_bead_id, { last_feedback_check_at: now() });
return;
}
Reconciler Rules (No New Rules Needed)
The feedback bead is a standard issue bead. Existing rules handle it:
- Rule 1 assigns a polecat
- Rework blocker guard keeps the MR from being recovered while the polecat works
- Rule 3 handles polecat failure recovery
Polecat Prompt
The feedback bead body contains the prompt. The agent fetches details itself via gh:
You are addressing feedback on PR #{pr_number} on {repo}, branch {branch}.
{if has_unresolved_comments AND has_failing_checks}
This PR has both unresolved review comments and failing CI checks.
Address the review comments first, then fix the CI failures, as
comment fixes may also resolve some CI issues.
{elif has_unresolved_comments}
This PR has unresolved review comments.
{elif has_failing_checks}
This PR has failing CI checks.
{end}
## Review Comments
Run `gh pr view {pr_number} --comments` to see all review comments.
For each unresolved comment thread:
- If it's a relevant code fix: make the change, push, reply explaining
what you did, and resolve the thread
- If it's not relevant: reply explaining why, and resolve the thread
It's important to resolve the full thread rather than just the base comment.
## CI Checks
Run `gh pr checks {pr_number}` to see the status of all CI checks.
For each failing check:
- Read the failure logs via `gh run view <run_id> --log-failed`
- Fix the underlying issue (test failure, lint error, type error, etc.)
- Push the fix
- Verify the check passes by reviewing the new run
After addressing everything, push all changes in a single commit (or
minimal commits) and call gt_done.
Why a Polecat (Not the Refinery)
Addressing PR feedback is implementation work — fixing code, running tests, pushing changes. The refinery reviews; the polecat implements. The polecat is dispatched to the same branch, makes the fixes, pushes, and the refinery/CI can re-evaluate.
Batching
All feedback on a PR (comments + CI failures) is handled in a single polecat session. One polecat, one branch, one pass. Duplicate dispatch prevented by:
last_feedback_check_at on the MR bead's review metadata
- Check for existing non-terminal
gt:pr-feedback bead blocking the MR in applyEvent
Platform API Calls
Detection (lightweight, in poll_pr side effect):
GET /repos/{owner}/{repo}/pulls/{number}/reviews — check for unresolved reviews
GET /repos/{owner}/{repo}/commits/{sha}/check-runs — check for failed checks
Resolution (by the polecat via gh CLI):
gh pr view --comments — read comment details
gh pr checks — read CI status
gh run view --log-failed — read failure logs
gh api graphql with resolveReviewThread — resolve comment threads
git push — push fixes
Auto-merge (by the system via gh CLI or API):
gh pr merge {number} --merge --delete-branch — merge the PR after grace period
Acceptance Criteria
References
Parent
Part of #204 (Phase 4: Hardening)
Problem
When using the
prmerge strategy, two things can block a PR from merging:Even after all feedback is resolved and CI is green, the PR still requires a human to click "Merge." For autonomous workflows, users should be able to set a grace period after which the system auto-merges.
Solution
Extend the PR polling to detect unresolved comments AND failing CI checks, dispatch a polecat to address them, and optionally auto-merge the PR after a configurable grace period once everything is green.
User Settings
Opt-in via town settings in the "Refinery" section:
Auto-resolve PR feedback toggle:
Auto-merge delay input (only visible when auto-resolve is enabled):
Common presets as quick-select buttons:
0(immediate),15 min,1 hour,4 hours,disabled.The delay gives humans a window to review the agent's fixes before auto-merge. If new comments or CI failures appear during the delay, the timer resets.
Auto-Merge Flow
The
poll_prside effect already checks PR status on each tick. Extend it:The
pr_auto_mergeevent is processed byapplyEvent:The actual
gh pr mergecall happens in the side effect phase:Timer Reset Conditions
The auto-merge timer resets (goes back to waiting) if ANY of these occur during the grace period:
This prevents merging a PR whose state changed during the grace window.
Reconciler Architecture Integration
Follows the event-driven reconciliation architecture from PR #1336.
New Event Type:
pr_feedback_detectedKeep the event lightweight — the polecat has
ghCLI access and will fetch the full details itself:No need to encode the full comment threads or CI logs in the event — that would be redundant. The polecat calls
gh pr view,gh pr checks, andgh apito get everything it needs directly from GitHub.Event Emission (Alarm Side Effect)
The
poll_prside effect is extended. Whenauto_resolve_pr_feedbackis enabled and the poll detects issues:The checks are lightweight API calls:
GET /repos/{owner}/{repo}/pulls/{number}/comments— check if any threads are unresolvedGET /repos/{owner}/{repo}/commits/{sha}/check-runs— check if any checks failedEvent Application
Reconciler Rules (No New Rules Needed)
The feedback bead is a standard issue bead. Existing rules handle it:
Polecat Prompt
The feedback bead body contains the prompt. The agent fetches details itself via
gh:Why a Polecat (Not the Refinery)
Addressing PR feedback is implementation work — fixing code, running tests, pushing changes. The refinery reviews; the polecat implements. The polecat is dispatched to the same branch, makes the fixes, pushes, and the refinery/CI can re-evaluate.
Batching
All feedback on a PR (comments + CI failures) is handled in a single polecat session. One polecat, one branch, one pass. Duplicate dispatch prevented by:
last_feedback_check_aton the MR bead's review metadatagt:pr-feedbackbead blocking the MR inapplyEventPlatform API Calls
Detection (lightweight, in poll_pr side effect):
GET /repos/{owner}/{repo}/pulls/{number}/reviews— check for unresolved reviewsGET /repos/{owner}/{repo}/commits/{sha}/check-runs— check for failed checksResolution (by the polecat via
ghCLI):gh pr view --comments— read comment detailsgh pr checks— read CI statusgh run view --log-failed— read failure logsgh api graphqlwithresolveReviewThread— resolve comment threadsgit push— push fixesAuto-merge (by the system via
ghCLI or API):gh pr merge {number} --merge --delete-branch— merge the PR after grace periodAcceptance Criteria
auto_resolve_pr_feedbacktoggle in town settings (default: off)pr_feedback_detectedevent payload is lightweight — no encoded comment bodies or CI logsReferences
ghCLI)