Skip to content

resolve-pr

resolve-pr drives an existing same-repo pull request toward a clean finish: it fetches unresolved review threads, triages only threads this run has not handled yet, fixes actionable ones, replies to every handled thread, resolves only what the honesty gate authorizes, waits for CI, then repeats until the next round sees CI green and no new threads. Its final report distinguishes workflow convergence from forge mergeability.

Use it when a PR is already open and reviewers or bots may keep adding comments after each push. fix-issue starts from an issue and opens a PR; resolve-pr picks up where it leaves off: it assumes the PR exists, then keeps re-fetching PR state until the review and CI surface stops changing.

Terminal window
keelson workflow run resolve-pr --inputs ARGUMENTS="resolve PR 42"

Pass exactly one explicit PR or MR number: a bare number (42), a reference (PR #42 or MR !42), or a GitHub PR or GitLab MR URL. The target is parsed without an agent call, so every convergence round uses the same number. Missing or conflicting targets fail extraction. resolve-pr runs in an isolated worktree on a generated branch, so it cannot infer the target from the current branch.

Approved invalid and already-addressed verdicts resolve in either mode. Repos that also require approved wontfix threads to close can opt in:

Terminal window
keelson workflow run resolve-pr \
--inputs ARGUMENTS="resolve PR 42" \
--inputs resolve_wontfix=true

The mode is off by default. When enabled, it authorizes resolution of approved wontfix verdicts. It also makes the closing reply for wontfix, invalid, and already-addressed restate the rationale and say it was “reviewed and accepted on the maintainer side.” Questions always stay open.

  • gh or glab authenticated with write scope on the repo. The bundled forge shim resolves whichever is present and reads unresolved threads the same way on either, through review-thread GraphQL on GitHub and MR discussions on GitLab.
  • jq on PATH, since several bash nodes use it (Git-for-Windows Bash does not ship it).
  • A GitHub Copilot subscription (provider: copilot, model: auto).
  • A running Keelson server (keelson start) for approval pauses.

The workflow uses a top-level converge: block:

converge:
gate: converge-check
max_rounds: 8
on_exhaust: approval

Keelson reruns the converge-check dependency subgraph until the gate succeeds. fetch-state’s threads.json is computed at the start of the round, before the CI watch, so the gate does not read it. Instead, after the watch, a post-ci-state node re-fetches GitHub’s live unresolved threads and re-diffs them against the ledger. The gate exits 0 only when all three hold:

  • the latest CI watch reports CI_STATUS: PASS;
  • post-ci-threads.json (the post-CI re-fetch) contains zero new, unhandled threads, so a thread a bot adds during the CI watch blocks the round; and
  • post-ci-retry.json is empty, meaning no fixed thread is still awaiting a successful resolveReviewThread.

If any of those fails, the gate fails into another round.

Convergence does not imply that the forge will allow a merge. On every attempt, converge-check writes mergeability.json with the gate status and the fresh post-ci-open-threads.json set. The report describes only that thread state:

Review threads clear: yes
Review threads clear: no, 2 deliberately-open threads: src/a.ts, src/b.ts

A default-mode wontfix or question can therefore coexist with convergence while still appearing as deliberately open. Draft state, merge conflicts, and required approvals remain forge-owned merge checks.

The key state is $ARTIFACTS_DIR/handled.json. fetch-state diffs GitHub’s current unresolved threads against that ledger and splits them: threads with no ledger entry are new (written to threads.json for full triage/reply/resolve), and a thread whose latest ledger entry is replied: true, resolved: false, resolve_authorized: true is a resolve-retry (written to resolve-retry.json for a resolve-only retry). reply-resolve records a thread into the ledger as soon as its reply is posted, so an authorized thread whose resolve failed is never re-replied, only re-resolved. The per-entry authorization keeps default-mode wontfix replies out of the retry lane.

Each round reads its own fixes before the reviewer does. A review fix is narrow by construction: it repairs the path the reviewer cited. With review on push enabled, anything the fix breaks elsewhere comes back as the next round’s thread, one defect per round. So between fix-validation and the push gate, review-fix reads only the commits the forge has not seen, on a different model from the fixer and with read-only tools, and apply-review-fix repairs what survives a refutation attempt. revalidate then gates the whole result. A failed review node fails the round instead of pushing unreviewed code.

Public mutation failures also persist in $ARTIFACTS_DIR/reply-failures.json. reply-audit checks only the current round. If reply-gate, a reply call, or a resolve call failed, the workflow pauses at reply-failure-approval before await-ci can advance the round.

max_rounds: 8 caps runaway bot churn. With on_exhaust: approval, reaching the cap pauses for a human override instead of silently declaring success or failing without context.

NodeKindWhat it does
extract-prscript (Bun)Deterministically parses one explicit PR or MR number from $ARGUMENTS, including PR/MR references and URLs. Rejects missing or conflicting targets instead of guessing from the isolated worktree’s branch.
fetch-statebashResolves the PR, refuses forks by emitting is_fork, checks out the head commit detached on round 1, pages the reviewThreads GraphQL query, and splits unresolved threads against handled.json into new threads and ledger-authorized resolve retries.
refuse-forkcancelStops the run cleanly when fetch-state reports a fork PR.
triagepromptRuns only when there are new threads. Reads the cited code and classifies each new thread, including separate actionable-code-change and actionable-metadata-change decisions.
triage-gatebashFails closed if triage ids do not exactly match the fetched set, writes triage.md, normalizes the runtime resolve_wontfix input into resolve-mode.json, and emits the approval state.
approveapprovalPauses on reply-only verdicts. Approval authorizes resolution of invalid and already-addressed; opt-in mode also authorizes wontfix and requires owner-side closing wording for all three. Questions remain reviewer-owned.
fixpromptApplies code fixes or PR body, title, and label fixes. Results carry fix_kind: "code" with a commit or fix_kind: "metadata" with commit: null, plus the per-entry resolve_authorized decision.
validatebashSoft-runs the discovered verify.sh, reporting VALIDATION_STATUS for the fixer.
fix-validationpromptRepairs broken validation, if any, and commits the repair.
capture-fix-diffbashWrites every commit the forge does not have yet to fix-diff.patch and emits has_fix. The base is the PR head fetch-state checked out, and only a successful push advances it, so a fix that a failed attempt left unpushed is still inside the next attempt’s review. It fails when no base was recorded, so a missing base never reads as a clean round.
review-fixpromptmodel: gpt-6-astra, effort: xhigh, allowed_tools: [Read, Glob, Grep], typed JSON. Runs only when the round committed code. Reads the round’s delta as the last reader before the push: every entry path that reaches the changed code, what the fix newly assumes, and the docs, version constants, and exported types that must move with it. Each finding carries a repro, or none plus the reason.
apply-review-fixpromptmodel: gpt-6-sol, effort: high. Tries to refute each CRITICAL or HIGH finding at confidence 80 or above by running its repro first, fixes the ones that hold, commits without pushing, and logs the outcome per finding in pre-push-review.md.
revalidatebashHard gate: refuses to push if verify.sh is missing or failing. It runs after the pre-push review, so it covers the review’s fixes too, and it still runs when that review was skipped because the round committed no code.
pushbashPushes the detached HEAD back to the recorded PR head ref on origin, then advances the review base to the pushed SHA.
reply-gatebashFail-closed honesty check. It validates full result coverage, code versus metadata fix evidence, the approved decision, per-entry resolution authorization, comment targets, and required owner-side closing language before review replies or thread resolutions. Failures enter reply-failures.json.
reply-resolvebashReplies on every result entry and resolves exactly those with resolve_authorized: true. It records reply or resolve call failures and persists the authorization in handled.json for safe retries.
reply-auditbashReads current-round reply-failures.json entries after both reply nodes finish and emits whether operator attention is required.
reply-failure-approvalapprovalPauses the run when the reply audit found a failed gate, reply, or resolution. await-ci cannot advance until this gate is approved.
resolve-retrybashRuns every round. It retries authorized unresolved threads without posting a second reply and flips their ledger entries to resolved: true on success.
await-cibashRuns every round, even when there were no new threads. Gates on the required checks discovered by forge pr required-checks; non-required checks are only logged for the audit trail. Reruns failed jobs at most once per head SHA (a per-SHA marker in $ARTIFACTS_DIR survives rounds, so an unchanged commit is not rerun on every round), and emits CI_STATUS. With no required checks configured it falls back to waiting for all checks to settle. Discovery failure gates on every check strictly (nothing rides the advisory carve-outs), so the loop can still converge on a genuinely green PR; a PR with no checks at all emits UNKNOWN, never a pass.
post-ci-statebashRuns after the CI watch. Re-fetches the live unresolved set into post-ci-open-threads.json, plus new-thread and pending-retry subsets, so the gate and report use fresh state.
converge-checkbashGate node. Succeeds only when CI passed with no new threads or pending retries. Every attempt writes mergeability.json with the gate status and a thread-clear verdict from the fresh open set.
report-metricsbashReduces handled.json to each thread’s latest entry and computes the counts the report prints: replied, resolved (from the resolved boolean), awaiting the reviewer, resolvable-but-unresolved ids, plus CI status, open paths, and reply failures.
reportpromptWrites the operator summary from those computed metrics, with a first-class review-thread line and an attention-needed note when reply failures exist.
  • fix-issue: starts from a GitHub issue and opens a draft PR.
  • pr-review: reviews a PR without fixing it.
  • Workflow nodes: converge, approval, cancel, trigger_rule, and worktree.