Skip to content

feat(task-board): keep a card In Progress while its reviewer runs - #6689

Merged
pedrofrxncx merged 3 commits into
mainfrom
feat/review-cycle-off-lane
Aug 28, 2026
Merged

feat(task-board): keep a card In Progress while its reviewer runs#6689
pedrofrxncx merged 3 commits into
mainfrom
feat/review-cycle-off-lane

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

A card moved to In Review the moment its PR was opened, so it sat in the reviewers' lane for the whole time an agent was still working on it. In Review read as "waiting on a person" when nobody was waiting on anything.

Now:

when lane
agent opens a PR In Progress — review cycle opens
reviewer records a verdict In Review
hand-off (no PR, stale preview, retries spent) In Review, unassigned

Why this needed a column

The lane could not simply be changed. The review cycle — the boundary that decides which reviewer verdicts still count — was derived from the newest status_changed → in_review activity, which made the lane load-bearing: moving a card mid-review would reset the boundary and invalidate its own reviewer's verdict. That is the exact failure mode that stranded 13 prod cards holding an approval that could never merge.

So the cycle stops riding on the lane:

  • review_cycle_started_at (migration 189) is the boundary, backfilled from the timeline so in-flight cycles survive the deploy.
  • reviewCycleStart(activity, cycleStartedAt) reads the column, falling back to the old activity scan only for cards stamped before the migration. The option is a required field on every cycle reducer — a caller that forgot it would silently count a previous cycle's verdicts as current, so the compiler asks at each of the 11 call sites.
  • inReviewPhase(item) replaces status === "in_review" in every automatic path, and listItemsPendingReview mirrors it (open cycle or the In Review lane — either alone leaves a real case unswept).
  • openReviewCycleIfInProgress only stamps a card with no cycle open, so the PR-open hook, the MCP tool hook and the thread-finish backstop are all idempotent. That predicate is the same guarantee the old conditional flip carried.

Two regressions decoupling opens, closed explicitly

  1. A failed reviewer run would have scheduled a Super Agent retry or parked the card on To Do, because the card is now In Progress. reactToFailedTaskRun skips a card with an open cycle — the reviewer has its own budget (spentAttemptsThisCycle), and the sweeper spends it.
  2. closesOwnReview gated on the lane, so an author's own run could have marked its work Done while a reviewer was still reading it. It gates on the phase now.

Testing

Real Postgres (embedded), not an in-memory fake:

  • migration up and down from an empty database
  • the backfill against a seeded pre-migration card → picks the newest → in_review stamp, matching the old derivation
  • bun test apps/api/src packages/shared apps/web/srcno new failures vs. the branch point (183 pre-existing, unchanged)
  • bun run check, bun run lint, bun run fmt, knip clean

New/inverted tests: cycle idempotency + re-open-after-close + org scoping + the sweeper work list (real PG); inReviewPhase unit cases; and the tests that encoded the old lane behaviour are inverted rather than appended — advanceToDoneIfMerged, closesOwnReview, refuseIfMergePending, the PR-open reaction, and the implementer prompt.

Screenshots

None — no visual change. The card renders in the In Progress column, which is existing lane rendering.


Summary by cubic

Keeps a card In Progress while its reviewer runs: linking a PR now opens a review cycle instead of moving the card to In Review, so the board only shows In Review once it's a person's turn.

The cycle boundary is now review_cycle_started_at (migration 190) rather than the newest → in_review transition, backfilled from the timeline so in-flight cycles survive the deploy. inReviewPhase replaces status === "in_review" in every automatic path, and openReviewCycleIfInProgress is idempotent across the PR-open hook, MCP tool hook, and thread-finish backstop. A late PR link can no longer strand a card: a Super-Agent-owned card sitting In Review with no open cycle is pulled back to In Progress and stamped, while a card whose cycle is already open is left where it is.

Regressions closed by the decoupling

  • A failed reviewer run no longer schedules a Super Agent retry or parks the card on To Do.
  • closesOwnReview gates on the phase, so an author's run can't mark its work Done while a reviewer is still reading it.
  • The thread-finish backstop treats the linked PR as what makes a card reviewable, so a card that attaches its repo at runtime stays In Progress instead of being parked In Review.
  • The starting-sandbox status is only announced on a real pod boot, not on warm pod resumes.

Written for commit 13d3666. Summary will update on new commits.

Review in cubic

Pedro França added 2 commits August 28, 2026 13:09
A card moved to In Review the moment its PR was opened, so it sat in the
reviewers' lane for the whole time an agent was still working on it. In
Review read as "waiting on a person" when nobody was waiting on anything.

The lane could not simply be changed, because the review CYCLE — the
boundary that decides which reviewer verdicts still count — was derived
from the newest `status_changed → in_review` activity. Moving a card mid
review would have reset that boundary and invalidated its own reviewer's
verdict, which is the failure mode that stranded 13 prod cards holding an
approval that could never merge.

So the cycle stops riding on the lane. `review_cycle_started_at` is a
column (migration 189, backfilled from the timeline so in-flight cycles
survive the deploy), and `reviewCycleStart` reads it, falling back to the
old activity scan only for cards stamped before it.

With that decoupled, the lane is free to say what a person wants to read:

  agent opens a PR   → review cycle opens, card STAYS In Progress
  reviewer decides   → In Review (`parkReviewedCardForHuman`)
  hand-off / retries → In Review, unassigned

`inReviewPhase` is the predicate every automatic path now takes instead of
`status === "in_review"`, and the sweeper's work list mirrors it. Two
regressions that decoupling opens are closed explicitly: a failed REVIEWER
run no longer schedules a Super Agent retry or parks the card on To Do
(the reviewer has its own budget), and `closesOwnReview` gates on the
phase, so an author's run can't mark its own work Done while a reviewer
is still reading it.

Verified against real Postgres: migration up/down from an empty database,
the backfill against a seeded pre-migration card, and the full unit +
integration suite with no new failures.
A card went to In Review and stayed there for the whole reviewer run — the
exact thing this branch exists to prevent.

`advanceLinkedTasksToReviewOnThreadFinish` reads `listPrs` to decide
repo-backed vs repo-less. A run that links its PR moments AFTER its thread goes
terminal is read as repo-less and parked In Review. The link then lands on a
card that is no longer In Progress, so `openReviewCycleIfInProgress` matched
nothing and the cycle never opened at all. Observed on a real board at 52
seconds between the two:

  15:42:47  thread finished, listPrs empty  -> card moved to In Review
  15:43:39  PR #423 linked                  -> update matched nothing, no cycle
  15:49:37  sweeper dispatched the reviewer via the in_review lane branch

The card therefore read In Review while an agent reviewed it, and with
`review_cycle_started_at` NULL every verdict fell back to the legacy activity
scan.

The predicate now also matches a Super-Agent-owned card sitting In Review with
NO cycle open, and takes it back to In Progress in the same statement. That is
safe precisely because the cycle is null: a card whose cycle never opened has
had no reviewer and no verdict, so there is nothing behind it to invalidate.
The Super Agent guard keeps it off a card a person has taken over
(`handTaskToHuman` clears the assignee), and one statement means the two
columns cannot interleave with the sweeper's read.

Real-Postgres tests: the late-link rescue, an In Review card with a cycle
already open left alone (a verdict put it there), a human-owned card left
alone, and a shipped card left alone. The test that asserted the old narrow
rule is inverted rather than appended.
@pedrofrxncx
pedrofrxncx force-pushed the feat/review-cycle-off-lane branch from e42cb0d to 1750b36 Compare August 28, 2026 16:09
… repo column

Three consecutive prod cards were parked In Review the moment their Super
Agent run finished, on top of an already-open review cycle, and sat there for
the whole reviewer run — the exact behaviour this branch removes.

`advanceLinkedTasksToReviewOnThreadFinish` decided repo-backed vs repo-less
with `item.repo != null && listPrs().length > 0`. The `repo` conjunct was a
cheap way to skip the query for a card that could not have a pull request, but
`repo` is only stamped on a card CREATED against a repository — a run that
finds its own with `TASK_ADD_REPO` links a PR and leaves it null. Those cards
read as repo-less forever, so the backstop took the "its answer IS its
deliverable" branch and moved them.

The fourth card, created against a repo, stayed In Progress exactly as
intended, which is what made the pattern legible.

A linked PR is the whole test now. And the backstop skips a card whose cycle is
already open whatever the PR read says: only the repo-less branch can move a
card, and moving one out from under its reviewer is the failure this exists to
prevent.

Real-Postgres tests for both: a PR-carrying card with a null repo stays In
Progress with its cycle stamped, and a card with an open cycle is left where it
is.
@pedrofrxncx
pedrofrxncx enabled auto-merge (squash) August 28, 2026 17:11
@pedrofrxncx
pedrofrxncx merged commit 6d2a0e2 into main Aug 28, 2026
34 checks passed
@pedrofrxncx
pedrofrxncx deleted the feat/review-cycle-off-lane branch August 28, 2026 17:13
decocms Bot pushed a commit that referenced this pull request Aug 28, 2026
PR: #6689 feat(task-board): keep a card In Progress while its reviewer runs
Bump type: minor

- decocms (apps/api/package.json): 4.295.1 -> 4.296.0
- @decocms/native (apps/native/package.json): 4.295.1 -> 4.296.0
- @decocms/shared (packages/shared/package.json): 0.69.0 -> 0.70.0

Deploy-Scope: both
pedrofrxncx added a commit that referenced this pull request Aug 31, 2026
)

* feat(task-board): keep a card In Progress while its reviewer runs

A card moved to In Review the moment its PR was opened, so it sat in the
reviewers' lane for the whole time an agent was still working on it. In
Review read as "waiting on a person" when nobody was waiting on anything.

The lane could not simply be changed, because the review CYCLE — the
boundary that decides which reviewer verdicts still count — was derived
from the newest `status_changed → in_review` activity. Moving a card mid
review would have reset that boundary and invalidated its own reviewer's
verdict, which is the failure mode that stranded 13 prod cards holding an
approval that could never merge.

So the cycle stops riding on the lane. `review_cycle_started_at` is a
column (migration 189, backfilled from the timeline so in-flight cycles
survive the deploy), and `reviewCycleStart` reads it, falling back to the
old activity scan only for cards stamped before it.

With that decoupled, the lane is free to say what a person wants to read:

  agent opens a PR   → review cycle opens, card STAYS In Progress
  reviewer decides   → In Review (`parkReviewedCardForHuman`)
  hand-off / retries → In Review, unassigned

`inReviewPhase` is the predicate every automatic path now takes instead of
`status === "in_review"`, and the sweeper's work list mirrors it. Two
regressions that decoupling opens are closed explicitly: a failed REVIEWER
run no longer schedules a Super Agent retry or parks the card on To Do
(the reviewer has its own budget), and `closesOwnReview` gates on the
phase, so an author's run can't mark its own work Done while a reviewer
is still reading it.

Verified against real Postgres: migration up/down from an empty database,
the backfill against a seeded pre-migration card, and the full unit +
integration suite with no new failures.

* fix(task-board): open the review cycle when the PR link loses the race

A card went to In Review and stayed there for the whole reviewer run — the
exact thing this branch exists to prevent.

`advanceLinkedTasksToReviewOnThreadFinish` reads `listPrs` to decide
repo-backed vs repo-less. A run that links its PR moments AFTER its thread goes
terminal is read as repo-less and parked In Review. The link then lands on a
card that is no longer In Progress, so `openReviewCycleIfInProgress` matched
nothing and the cycle never opened at all. Observed on a real board at 52
seconds between the two:

  15:42:47  thread finished, listPrs empty  -> card moved to In Review
  15:43:39  PR #423 linked                  -> update matched nothing, no cycle
  15:49:37  sweeper dispatched the reviewer via the in_review lane branch

The card therefore read In Review while an agent reviewed it, and with
`review_cycle_started_at` NULL every verdict fell back to the legacy activity
scan.

The predicate now also matches a Super-Agent-owned card sitting In Review with
NO cycle open, and takes it back to In Progress in the same statement. That is
safe precisely because the cycle is null: a card whose cycle never opened has
had no reviewer and no verdict, so there is nothing behind it to invalidate.
The Super Agent guard keeps it off a card a person has taken over
(`handTaskToHuman` clears the assignee), and one statement means the two
columns cannot interleave with the sweeper's read.

Real-Postgres tests: the late-link rescue, an In Review card with a cycle
already open left alone (a verdict put it there), a human-owned card left
alone, and a shipped card left alone. The test that asserted the old narrow
rule is inverted rather than appended.

* fix(task-board): a linked PR is what makes a card reviewable, not its repo column

Three consecutive prod cards were parked In Review the moment their Super
Agent run finished, on top of an already-open review cycle, and sat there for
the whole reviewer run — the exact behaviour this branch removes.

`advanceLinkedTasksToReviewOnThreadFinish` decided repo-backed vs repo-less
with `item.repo != null && listPrs().length > 0`. The `repo` conjunct was a
cheap way to skip the query for a card that could not have a pull request, but
`repo` is only stamped on a card CREATED against a repository — a run that
finds its own with `TASK_ADD_REPO` links a PR and leaves it null. Those cards
read as repo-less forever, so the backstop took the "its answer IS its
deliverable" branch and moved them.

The fourth card, created against a repo, stayed In Progress exactly as
intended, which is what made the pattern legible.

A linked PR is the whole test now. And the backstop skips a card whose cycle is
already open whatever the PR read says: only the repo-less branch can move a
card, and moving one out from under its reviewer is the failure this exists to
prevent.

Real-Postgres tests for both: a PR-carrying card with a null repo stays In
Progress with its cycle stamped, and a card with an open cycle is left where it
is.

---------

Co-authored-by: Pedro França <pedrofrxncx@deco.cx>
pedrofrxncx pushed a commit that referenced this pull request Aug 31, 2026
PR: #6689 feat(task-board): keep a card In Progress while its reviewer runs
Bump type: minor

- decocms (apps/api/package.json): 4.295.1 -> 4.296.0
- @decocms/native (apps/native/package.json): 4.295.1 -> 4.296.0
- @decocms/shared (packages/shared/package.json): 0.69.0 -> 0.70.0

Deploy-Scope: both
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