Skip to content

fix(reviews): stop writing external PR URLs to pr_review task rows#481

Merged
jonwiggins merged 1 commit into
mainfrom
fix/pr-review-no-prurl
Apr 22, 2026
Merged

fix(reviews): stop writing external PR URLs to pr_review task rows#481
jonwiggins merged 1 commit into
mainfrom
fix/pr-review-no-prurl

Conversation

@jonwiggins

Copy link
Copy Markdown
Owner

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: launchPrReview was writing the external PR's URL into tasks.pr_url, a column whose only valid meaning is "the PR this task opened." Any caller that looked at task.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 populated pr_url. It becomes structurally impossible for the reconciler to mistake an external PR for its own output.

What changed

  • pr-review-service.launchPrReview and the chat-turn update no longer write pr_url / pr_number to the task row.
  • New hydratePrReviewPrUrls helper in task-service.ts denormalizes pr_url / pr_number onto pr_review rows 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 → matching review_drafts row → metadata.prUrl for chat-turn tasks.
  • Applied in GET /api/tasks, GET /api/tasks/:id, and searchTasks.
  • Migration 1777100000_backfill_pr_review_pr_url.sql clears pr_url / pr_number on in-flight pr_review rows (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.
  • Updated existing test mocks of task-service.js to 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_url cleared, avoiding a brief window where a reconciler tick could still see the old data.

Test plan

  • pnpm turbo typecheck — 12 packages green
  • pnpm turbo test — 2024 API + 389 shared tests green
  • pnpm format:check — clean
  • bash scripts/check-migration-prefixes.sh — no duplicates
  • After merge: trigger a manual review on an external PR with autoMerge=true on the repo; verify the PR does NOT get merged and the review draft appears in the UI as expected
  • After merge: verify task list, task card, and task detail page all still show the PR link for pr_review tasks (denormalization is working)

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 jonwiggins merged commit bd6cde6 into main Apr 22, 2026
7 checks passed
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.
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