fix(reviews): stop writing external PR URLs to pr_review task rows#481
Merged
Conversation
Complements #480. The root cause of the external-PR auto-merge bug was that `launchPrReview` persisted the external PR's URL into `tasks.pr_url` — a column whose only valid meaning is "the PR this task opened." Once the reconciler (or anyone else) saw `pr_url` set, the PR-reactive path kicked in and auto-merged the external PR as soon as CI went green. This change makes the data layer match the invariant #480 documented: - `pr-review-service.launchPrReview` and the chat-turn update no longer write `pr_url` / `pr_number` to the task row. The PR being reviewed is tracked on `review_drafts` (root review tasks) and task `metadata` (chat-turn tasks) — both existed already; we just stop duplicating into the column that had the wrong meaning. - New `hydratePrReviewPrUrls` helper denormalizes `pr_url` / `pr_number` onto `pr_review` rows at the HTTP boundary so the shared task chrome (task list, task card, task detail header) keeps rendering PR links. Resolution order: legacy column value → matching review_drafts row → metadata fallback for chat-turn tasks. Applied in list, detail, and search routes. - Migration clears `pr_url` / `pr_number` on in-flight `pr_review` rows (state NOT IN completed/cancelled/failed). Terminal rows keep their data for audit — the non-coding reconciler guard from #480 prevents any re-action on them. After this lands, the column's semantic is strictly enforced: `tasks.pr_url` is only populated for `taskType="coding"` rows, so it becomes structurally impossible for the reconciler to mistake an external PR for its own output.
jonwiggins
added a commit
that referenced
this pull request
Apr 23, 2026
External PR reviews were bolted onto the `tasks` table via `taskType='pr_review'`, which tangled them into the coding-task reconciler — the PR's CI status and merge conflicts got interpreted as the review task's problem, causing auto-resume loops on every external PR review. Reviews are now a sibling of Repo Tasks and Standalone Tasks: - New schema: `pr_reviews` (canonical record), `pr_review_runs` (each agent execution: initial / rereview / chat), `pr_review_events` (transition log), `pr_review_chat_messages`. `task_logs.task_id` made nullable with a new `pr_review_run_id` column. - New pure decider (`reconcilePrReview`) with its own state machine (queued → waiting_ci → reviewing → ready ⇄ stale → submitted) and action kinds (launchReviewRun, submitReview, markStale). The coding reconciler is now blind to reviews. - New `pr-review-worker` runs agents on the repo-pod infrastructure; removes all `taskType==='pr_review'` branches from `task-worker`. - External PR poller upserts into `pr_reviews`; pr-watcher stale detection points at the new table; workflow_triggers gains `target_type='pr_review'` for scheduled rereviews. - New HTTP surface at `/api/pr-reviews/*`. `/api/tasks` unified layer learns `type: 'pr-review'`. - New UI at `/reviews` and `/reviews/[id]` replaces the ReviewDraftPanel embedded in the task detail page. Merge button now surfaces the reason when disabled and offers "Merge anyway" with a CI-failing warning instead of silently hard-disabling on failing checks. - Old `review_drafts` / `review_chat_messages` tables dropped; in-flight `pr_review` tasks cleared (poller will re-detect). Supersedes PRs #480 and #481: the reconciler guard and pr_url-write workarounds both become moot when the feature lives in its own table.
jplorier
pushed a commit
to jplorier/optio
that referenced
this pull request
May 5, 2026
…onwiggins#481) Complements jonwiggins#480. The root cause of the external-PR auto-merge bug was that `launchPrReview` persisted the external PR's URL into `tasks.pr_url` — a column whose only valid meaning is "the PR this task opened." Once the reconciler (or anyone else) saw `pr_url` set, the PR-reactive path kicked in and auto-merged the external PR as soon as CI went green. This change makes the data layer match the invariant jonwiggins#480 documented: - `pr-review-service.launchPrReview` and the chat-turn update no longer write `pr_url` / `pr_number` to the task row. The PR being reviewed is tracked on `review_drafts` (root review tasks) and task `metadata` (chat-turn tasks) — both existed already; we just stop duplicating into the column that had the wrong meaning. - New `hydratePrReviewPrUrls` helper denormalizes `pr_url` / `pr_number` onto `pr_review` rows at the HTTP boundary so the shared task chrome (task list, task card, task detail header) keeps rendering PR links. Resolution order: legacy column value → matching review_drafts row → metadata fallback for chat-turn tasks. Applied in list, detail, and search routes. - Migration clears `pr_url` / `pr_number` on in-flight `pr_review` rows (state NOT IN completed/cancelled/failed). Terminal rows keep their data for audit — the non-coding reconciler guard from jonwiggins#480 prevents any re-action on them. After this lands, the column's semantic is strictly enforced: `tasks.pr_url` is only populated for `taskType="coding"` rows, so it becomes structurally impossible for the reconciler to mistake an external PR for its own output.
jplorier
pushed a commit
to jplorier/optio
that referenced
this pull request
May 5, 2026
External PR reviews were bolted onto the `tasks` table via `taskType='pr_review'`, which tangled them into the coding-task reconciler — the PR's CI status and merge conflicts got interpreted as the review task's problem, causing auto-resume loops on every external PR review. Reviews are now a sibling of Repo Tasks and Standalone Tasks: - New schema: `pr_reviews` (canonical record), `pr_review_runs` (each agent execution: initial / rereview / chat), `pr_review_events` (transition log), `pr_review_chat_messages`. `task_logs.task_id` made nullable with a new `pr_review_run_id` column. - New pure decider (`reconcilePrReview`) with its own state machine (queued → waiting_ci → reviewing → ready ⇄ stale → submitted) and action kinds (launchReviewRun, submitReview, markStale). The coding reconciler is now blind to reviews. - New `pr-review-worker` runs agents on the repo-pod infrastructure; removes all `taskType==='pr_review'` branches from `task-worker`. - External PR poller upserts into `pr_reviews`; pr-watcher stale detection points at the new table; workflow_triggers gains `target_type='pr_review'` for scheduled rereviews. - New HTTP surface at `/api/pr-reviews/*`. `/api/tasks` unified layer learns `type: 'pr-review'`. - New UI at `/reviews` and `/reviews/[id]` replaces the ReviewDraftPanel embedded in the task detail page. Merge button now surfaces the reason when disabled and offers "Merge anyway" with a CI-failing warning instead of silently hard-disabling on failing checks. - Old `review_drafts` / `review_chat_messages` tables dropped; in-flight `pr_review` tasks cleared (poller will re-detect). Supersedes PRs jonwiggins#480 and jonwiggins#481: the reconciler guard and pr_url-write workarounds both become moot when the feature lives in its own table.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complements #480. That PR hardened the reconciler so it can't auto-merge non-coding task types. This PR fixes the actual root cause:
launchPrReviewwas writing the external PR's URL intotasks.pr_url, a column whose only valid meaning is "the PR this task opened." Any caller that looked attask.pr_url(reconciler, watcher,isOptioAuthored) could be misled into acting on someone else's PR.After this lands, the column is strictly enforced: only
taskType="coding"rows have a populatedpr_url. It becomes structurally impossible for the reconciler to mistake an external PR for its own output.What changed
pr-review-service.launchPrReviewand the chat-turn update no longer writepr_url/pr_numberto the task row.hydratePrReviewPrUrlshelper intask-service.tsdenormalizespr_url/pr_numberontopr_reviewrows at the HTTP boundary, so shared UI chrome (task list, card, detail header) keeps rendering PR links without web changes. Resolution order: legacy column value → matchingreview_draftsrow →metadata.prUrlfor chat-turn tasks.GET /api/tasks,GET /api/tasks/:id, andsearchTasks.1777100000_backfill_pr_review_pr_url.sqlclearspr_url/pr_numberon in-flightpr_reviewrows (state NOT IN completed/cancelled/failed). Terminal rows keep their data for audit — fix(reconciler): guard PR-reactive actions to coding tasks only #480's non-coding guard prevents any re-action on them.task-service.jsto re-export the new helper as a pass-through.Dependency
Mergeable independently, but merge #480 first — that way the reconciler's non-coding guard is already in place before any existing in-flight rows get their
pr_urlcleared, avoiding a brief window where a reconciler tick could still see the old data.Test plan
pnpm turbo typecheck— 12 packages greenpnpm turbo test— 2024 API + 389 shared tests greenpnpm format:check— cleanbash scripts/check-migration-prefixes.sh— no duplicatesautoMerge=trueon the repo; verify the PR does NOT get merged and the review draft appears in the UI as expectedpr_reviewtasks (denormalization is working)