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.
Invoke it
Section titled “Invoke it”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:
keelson workflow run resolve-pr \ --inputs ARGUMENTS="resolve PR 42" \ --inputs resolve_wontfix=trueThe 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.
What it needs
Section titled “What it needs”ghorglabauthenticated with write scope on the repo. The bundledforgeshim resolves whichever is present and reads unresolved threads the same way on either, through review-thread GraphQL on GitHub and MR discussions on GitLab.jqonPATH, 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 loop
Section titled “The loop”The workflow uses a top-level converge: block:
converge: gate: converge-check max_rounds: 8 on_exhaust: approvalKeelson 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; andpost-ci-retry.jsonis empty, meaning no fixed thread is still awaiting a successfulresolveReviewThread.
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: yesReview threads clear: no, 2 deliberately-open threads: src/a.ts, src/b.tsA 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.
Node by node
Section titled “Node by node”| Node | Kind | What it does |
|---|---|---|
extract-pr | script (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-state | bash | Resolves 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-fork | cancel | Stops the run cleanly when fetch-state reports a fork PR. |
triage | prompt | Runs 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-gate | bash | Fails 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. |
approve | approval | Pauses 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. |
fix | prompt | Applies 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. |
validate | bash | Soft-runs the discovered verify.sh, reporting VALIDATION_STATUS for the fixer. |
fix-validation | prompt | Repairs broken validation, if any, and commits the repair. |
capture-fix-diff | bash | Writes 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-fix | prompt | model: 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-fix | prompt | model: 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. |
revalidate | bash | Hard 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. |
push | bash | Pushes the detached HEAD back to the recorded PR head ref on origin, then advances the review base to the pushed SHA. |
reply-gate | bash | Fail-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-resolve | bash | Replies 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-audit | bash | Reads current-round reply-failures.json entries after both reply nodes finish and emits whether operator attention is required. |
reply-failure-approval | approval | Pauses the run when the reply audit found a failed gate, reply, or resolution. await-ci cannot advance until this gate is approved. |
resolve-retry | bash | Runs every round. It retries authorized unresolved threads without posting a second reply and flips their ledger entries to resolved: true on success. |
await-ci | bash | Runs 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-state | bash | Runs 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-check | bash | Gate 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-metrics | bash | Reduces 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. |
report | prompt | Writes the operator summary from those computed metrics, with a first-class review-thread line and an attention-needed note when reply failures exist. |
Related
Section titled “Related”- 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, andworktree.