Skip to content

WIP: "review unsettled threads" view that summarizes in-progress work#4417

Open
t3dotgg wants to merge 13 commits into
mainfrom
t3code/sidebar-work-overview
Open

WIP: "review unsettled threads" view that summarizes in-progress work#4417
t3dotgg wants to merge 13 commits into
mainfrom
t3code/sidebar-work-overview

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Adds a one-click "Review unsettled work" sweep to sidebar v2. Clicking the new checklist button in the sidebar header opens /review-sweep and kicks off an agentic pass over every unsettled, unarchived thread across all connected environments:

  • an LLM summarizes each thread (what was asked, where it landed)
  • suggests a corrected title when the current one is misleading or a placeholder
  • recommends settling threads whose work clearly concluded

Results stream in progressively (client-side pool, concurrency 3, 50-thread cap favoring most-recently-active). Each card has Apply title / Settle / Dismiss / Retry; the header has Apply All. Nothing applies without a click. Results are ephemeral per run — no new persisted thread state.

Mock/design doc: https://b8x4s2sr11ko.postplan.dev

Current state:
image

Implementation

  • contracts: ReviewThreadSummaryInput/Result + ReviewThreadNotFoundError, new review.summarizeThread WS RPC, new reviewSweep capability flag so clients skip pre-feature servers under version skew.
  • server: new generateThreadReview op on the TextGeneration service, implemented in all five provider backends (Claude/Codex/Cursor/Grok/OpenCode) sharing buildThreadReviewPrompt (transcript capped to last 20 non-streaming messages, ~2k chars each) and a normalizeThreadReview sanitizer. ReviewService.summarizeThread reads the transcript via ProjectionSnapshotQuery and uses the server's textGenerationModelSelection.
  • client-runtime: summarizeThread RPC command atom in the review atom group.
  • web: reviewSweepStore.ts (non-persisted Zustand store + module-level runner with runId staleness guards; sweeps survive navigation), /review-sweep route + ReviewSweepView, SidebarV2 header button. Applying reuses the existing thread.meta.update and thread.settle commands.

Settle-safety invariant

A running / awaiting-approval / awaiting-input thread can never be settled through this feature, enforced at four independent layers:

  1. prompt rules (active threads: recommendSettle must be false)
  2. normalizeThreadReview forces it off for active threads
  3. the server re-derives activity from its own projection and never trusts the client's canSettleNow
  4. the client re-checks canSettle against the live shell at apply time

Testing

  • Full monorepo typecheck, lint, and fmt clean
  • New tests: ReviewService.summarizeThread (not-found, transcript extraction/streaming exclusion, stale-canSettleNow server override), buildThreadReviewPrompt (caps, active-thread rule), normalizeThreadReview (sanitization, placeholder-title drop)
  • Independent Codex (gpt-5.6-sol) review of the diff; both findings (server trusting client canSettleNow; cap sorting by createdAt instead of last activity) fixed with regression coverage
  • Not run: live end-to-end sweep in an authenticated browser session (pair-gated) — needs a manual pass: enable sidebar v2 in Beta settings, click the checklist icon, watch cards stream in, apply a title + settle

🤖 Generated with Claude Code


Note

Medium Risk
Touches thread settle recommendations, external LLM prompts (workspace cwd), and automated gh pr merge with agent handoff on conflicts—guarded by layered settle checks but still operationally sensitive.

Overview
Adds a “Review unsettled work” flow: sidebar entry, /review-sweep, and a background sweep over unsettled threads (capped, concurrent LLM calls) that returns summaries, title fixes, settle hints, and optional PR context—nothing applies without an explicit click.

Server & contracts: Advertises reviewSweep capability; new review.summarizeThread and review.mergePullRequest RPCs with typed inputs/results. ReviewService loads thread projection, resolves workspace cwd (fails if missing), re-derives activity server-side (pending approvals/input, running session, queued turn start) so settle recommendations ignore stale client canSettleNow, optionally enriches via gh pr view (degrades on failure), and runs generateThreadReview across all text-generation backends with shared prompt/normalization. mergePullRequest re-checks live GitHub merge-readiness then squash-merges via gh.

Web: Zustand reviewSweepStore runs the sweep off-React, mirrors sidebar PR state for candidate parity, supports apply title/settle, dismiss/retry, sequential merge queue (conflicts hand off to the thread agent), and completion toasts. UI buckets results (merge-ready, settle, title, attention, etc.) with bulk actions.

Tests: Broad ReviewService coverage for summarize/merge paths; prompt and normalizeThreadReview unit tests.

Reviewed by Cursor Bugbot for commit 3e757bd. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add 'review unsettled threads' sweep with summarize and merge PR capabilities

  • Adds a new reviewSummarizeThread and reviewMergePullRequest WebSocket RPC pair, backed by ReviewService methods that generate AI thread summaries (with settle/title recommendations) and trigger squash merges via GitHub CLI.
  • Introduces generateThreadReview across all text generation backends (Claude, Codex, Cursor, Grok, OpenCode) using a new prompt builder that limits transcript to 20 messages and enforces active-thread settle guards.
  • Adds reviewSweepStore.ts, a Zustand-backed module that collects unsettled thread candidates, runs concurrent AI reviews, and provides actions to apply suggested titles, settle threads, and run a sequential merge queue with conflict handoff to the thread agent.
  • Adds a /review-sweep route rendering ReviewSweepView.tsx with categorized results, and a sidebar 'Review unsettled work' button that opens a launch dialog and publishes PR states to the sweep store.
  • Risk: the merge queue runs sequentially and hands off conflicts to the thread agent via a startTurn message; if the agent is inactive or the branch is stale, conflicts may silently queue without user visibility.

Macroscope summarized 3e757bd.

Summary by CodeRabbit

  • New Features

    • Added a Review Sweep workflow for quickly reviewing unsettled threads across projects.
    • View AI-generated summaries, suggested titles, and settle recommendations.
    • Apply recommendations individually or all at once, dismiss items, retry failed reviews, and re-run sweeps.
    • Added sidebar access through the “Review unsettled work” control.
    • Review results are generated through supported AI providers.
  • Bug Fixes

    • Settle recommendations are suppressed while a thread is still active or has queued work.
    • Review transcripts exclude streaming messages for more reliable summaries.

Adds a sidebar-v2 button that reviews every unsettled thread across all
connected environments: an LLM summarizes each thread, suggests a
corrected title when the current one is misleading, and recommends
settling threads whose work has concluded. Results stream into a new
/review-sweep page with per-item Apply buttons and Apply All — nothing
applies without a click.

- contracts: ReviewThreadSummaryInput/Result, review.summarizeThread WS
  RPC, reviewSweep capability flag (old servers are skipped)
- server: generateThreadReview TextGeneration op across all five
  providers, conservative prompt with transcript capping, and
  ReviewService.summarizeThread reading the projection
- client-runtime: summarizeThread RPC command atom
- web: reviewSweepStore runner (concurrency 3, runId-guarded, survives
  navigation), /review-sweep view, SidebarV2 entry button

Settle recommendations are blocked for active threads at four layers:
prompt rules, normalizeThreadReview, a server-side projection re-check
(never trusts the client's canSettleNow), and a client canSettle
re-check at apply time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main feature: a review sweep view for unsettled threads and in-progress work.
Description check ✅ Passed The description is detailed and covers what changed, why, UI behavior, and testing, though it doesn't use the repository's exact template headings.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/sidebar-work-overview

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 24, 2026
Comment thread apps/web/src/reviewSweepStore.ts
Comment thread apps/web/src/reviewSweepStore.ts Outdated
Comment thread apps/server/src/review/ReviewService.ts
Comment thread apps/web/src/reviewSweepStore.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

6 blocking correctness issues found. This WIP PR introduces a significant new feature with ~1500+ lines of new code, new server capabilities, GitHub PR merging functionality, and multiple unresolved review comments identifying bugs in merge/settle logic. New features of this scope require human review.

You can customize Macroscope's approvability policy. Learn more.

@t3dotgg t3dotgg changed the title Add Review Sweep: one-click agentic review of unsettled work WIP: "review unsettled threads" view that summarizes in-progress work Jul 24, 2026
- macroscope: guard apply-title/apply-settle completions with the runId
  captured before the RPC so a Re-run mid-flight never stamps applied
  state onto the new run's cards; applyAll re-reads items from the
  store each step so mid-apply dismissals are respected, and bails out
  entirely when the run changes.
- cursor: the server's activity re-check now also treats a queued turn
  start (user message no session has adopted yet) as active, mirroring
  the decider's thread.settle guard; sweep candidate ordering uses
  NaN-safe toSortableTimestamp instead of raw Date.parse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Addressed all four bot findings in 8aad08d:

  • macroscope (runId race on apply): applySweepTitle/applySweepSettle now capture the runId before the RPC and skip patchItem when it no longer matches, so a Re-run mid-flight never marks the new run's cards as already applied. applyAllSweepRecommendations bails out when the run changes.
  • macroscope (stale items snapshot in Apply All): both loops re-read the item from the store on each iteration, so dismissing a card mid-apply is respected.
  • cursor (queued turns miss settle block): the server-side activity re-check now also detects a queued turn start (user message no session has adopted yet, within the 2-minute grace window), mirroring the decider's thread.settle guard and the client's hasQueuedTurnStart. Regression test added.
  • cursor (unsafe Date.parse sort): candidate ordering now uses the NaN-safe toSortableTimestamp helper.

🤖 Generated with Claude Code

The sidebar button now only navigates to /review-sweep; the sweep no
longer auto-starts. The idle screen shows exactly what a run will do
before any model call happens: how many threads will be reviewed (and
how many the cap would skip), which text-generation model each
environment's server will use, and a cost/latency heads-up when the
run is large (15+ threads). Starting requires an explicit click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx Outdated
Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx
Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx
t3dotgg and others added 2 commits July 23, 2026 19:42
The environment/model breakdown previously aggregated every sweep
candidate, so with more than SWEEP_MAX_THREADS unsettled threads it
could list environments whose threads would all be skipped and row
totals exceeding the actual call count. The summary now sorts and caps
candidates with the same helpers startReviewSweep uses, so the rows
always describe exactly the calls a run would make.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Environment labels can collide (e.g. identical hostnames); keying the
rows by label would make React mis-reconcile them. Key by the unique
environment id instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Latest bugbot pass (which reviewed dc66ff1):

  • Pre-run counts ignore truncation (medium): already fixed in f8b9d79, which landed just before this review posted — the summary now sorts and caps candidates with the same helpers startReviewSweep uses, so the per-environment rows describe exactly the calls a run would make.
  • Duplicate environment label keys (low): fixed in f8e63b2 — model rows are keyed by environment id instead of the potentially-colliding label.

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/server/src/review/ReviewService.ts (1)

163-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mislabeled operation on non-generation errors.

mapProjectionError and the server-settings load failure both tag TextGenerationError.operation as "generateThreadReview", even though the actual failing step is a projection read or settings load, not the generation call. This will misdirect error triage toward the text-generation provider when the real fault is elsewhere.

Also applies to: 211-220

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/review/ReviewService.ts` around lines 163 - 168, Update the
TextGenerationError operation labels in mapProjectionError and the
server-settings load failure handling to identify their actual failing steps
rather than "generateThreadReview". Use distinct operation values for the thread
projection read and server-settings load, while preserving the existing error
details and cause propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/review/ReviewService.ts`:
- Around line 178-186: Update the cwd resolution in the review summary flow
around resolveThreadWorkspaceCwd so an unresolved thread workspace does not fall
back to process.cwd(). Instead, propagate a typed retryable error when the
project shell is unavailable or no workspace path can be resolved, preserving
the resolved workspace path for valid threads.

---

Nitpick comments:
In `@apps/server/src/review/ReviewService.ts`:
- Around line 163-168: Update the TextGenerationError operation labels in
mapProjectionError and the server-settings load failure handling to identify
their actual failing steps rather than "generateThreadReview". Use distinct
operation values for the thread projection read and server-settings load, while
preserving the existing error details and cause propagation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2fc3dff3-55aa-451c-833a-e81b598de55f

📥 Commits

Reviewing files that changed from the base of the PR and between bb38c33 and f8e63b2.

📒 Files selected for processing (26)
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/git/GitManager.test.ts
  • apps/server/src/review/ReviewService.test.ts
  • apps/server/src/review/ReviewService.ts
  • apps/server/src/server.test.ts
  • apps/server/src/server.ts
  • apps/server/src/textGeneration/ClaudeTextGeneration.ts
  • apps/server/src/textGeneration/CodexTextGeneration.ts
  • apps/server/src/textGeneration/CursorTextGeneration.ts
  • apps/server/src/textGeneration/GrokTextGeneration.ts
  • apps/server/src/textGeneration/OpenCodeTextGeneration.ts
  • apps/server/src/textGeneration/TextGeneration.test.ts
  • apps/server/src/textGeneration/TextGeneration.ts
  • apps/server/src/textGeneration/TextGenerationPrompts.test.ts
  • apps/server/src/textGeneration/TextGenerationPrompts.ts
  • apps/server/src/textGeneration/TextGenerationUtils.ts
  • apps/server/src/ws.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/reviewSweep/ReviewSweepView.tsx
  • apps/web/src/reviewSweepStore.ts
  • apps/web/src/routeTree.gen.ts
  • apps/web/src/routes/review-sweep.tsx
  • packages/client-runtime/src/state/review.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/review.ts
  • packages/contracts/src/rpc.ts

Comment on lines +178 to +186
const projectOption = yield* projectionSnapshotQuery
.getProjectShellById(thread.projectId)
.pipe(Effect.mapError(mapProjectionError));
const cwd =
resolveThreadWorkspaceCwd({
thread,
projects: Option.isSome(projectOption) ? [projectOption.value] : [],
}) ?? process.cwd();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don't fall back to process.cwd() when the thread's workspace can't be resolved.

If getProjectShellById returns None (deleted/renamed project, projection lag, etc.) and the thread has no worktreePath, cwd silently becomes the server process's own working directory rather than the thread's project. That directory is then handed straight to the provider CLI spawn (see ClaudeTextGeneration.generateThreadReview / CodexTextGeneration.generateThreadReview), which can read local context files (e.g. AGENTS.md/CLAUDE.md) from that cwd and mix unrelated content into the prompt sent to an external LLM. Since review-sweep runs unattended over up to 50 threads with concurrency 3, this silent fallback could review "the server's own directory" instead of failing loudly.

Prefer failing the summary for that thread (surfacing a typed error the client can show as a retry-able failure) over guessing a cwd.

🔒 Proposed fix: fail instead of silently falling back
-    const cwd =
-      resolveThreadWorkspaceCwd({
-        thread,
-        projects: Option.isSome(projectOption) ? [projectOption.value] : [],
-      }) ?? process.cwd();
+    const cwd = resolveThreadWorkspaceCwd({
+      thread,
+      projects: Option.isSome(projectOption) ? [projectOption.value] : [],
+    });
+    if (cwd === undefined) {
+      return yield* new TextGenerationError({
+        operation: "generateThreadReview",
+        detail: `Unable to resolve a workspace cwd for thread '${input.threadId}'.`,
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/review/ReviewService.ts` around lines 178 - 186, Update the
cwd resolution in the review summary flow around resolveThreadWorkspaceCwd so an
unresolved thread workspace does not fall back to process.cwd(). Instead,
propagate a typed retryable error when the project shell is unavailable or no
workspace path can be resolved, preserving the resolved workspace path for valid
threads.

t3dotgg and others added 2 commits July 23, 2026 22:12
The provider CLI spawn reads local context files (AGENTS.md, CLAUDE.md)
from its cwd, so silently falling back to process.cwd() when a thread
has no worktree and its project shell is missing would mix the server's
own directory contents into an external LLM prompt. Return a typed
TextGenerationError instead — the sweep UI already renders it as a
retry-able error card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…verview

# Conflicts:
#	apps/server/src/environment/ServerEnvironment.ts
#	packages/contracts/src/environment.ts
@t3dotgg

t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Two more updates:

  • coderabbit (major, security): fixed in 27a70df-range commit "Fail thread review loudly when workspace cwd is unresolvable" — summarizeThread no longer falls back to process.cwd() when a thread has no worktree and its project shell is missing. It now returns a typed TextGenerationError (rendered as a retry-able error card in the sweep UI), so the provider CLI can never read the server's own directory context into an external LLM prompt. Regression test added.
  • Conflicts with main resolved: merged origin/main (thread snoozing, feat(sidebar-v2): thread snoozing #4311). Both features add a capability flag and touch the SidebarV2 header; resolution keeps threadSnooze and reviewSweep side by side. Snoozed threads remain unsettled work, so the sweep intentionally still reviews them. All typechecks and the 31 touched-area tests pass post-merge.

🤖 Generated with Claude Code

Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx
Comment thread apps/web/src/reviewSweepStore.ts
Comment thread apps/server/src/review/ReviewService.ts
- Group sweep cards and resolve project titles by scoped
  (environmentId, projectId) key — bare project ids can collide across
  connected environments.
- Withdraw a settle recommendation from the card when the apply-time
  canSettle guard refuses it, instead of leaving a CTA the guard will
  keep rejecting.
- Document why summarizeThread's two projection reads are ordered
  transcript-then-shell: reading the activity view last catches
  sessions that start mid-read, and the reverse skew only affects
  summary staleness — settles are re-validated at apply time and by
  the decider, so no consistency window can settle live work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Cursor's post-merge review pass, addressed in c68e9fa:

  • Project groups ignore environment scope (medium): fixed — grouping and title lookup now key on scoped (environmentId, projectId) instead of the bare project id, which can collide across environments.
  • Stale settle CTA after activity block (medium): fixed — when the apply-time canSettle guard refuses a settle, the recommendation is withdrawn from the card (alongside the existing toast) instead of leaving a CTA the guard will keep rejecting.
  • Inconsistent projection reads for settle (medium): not a bug, documented instead. The transcript→shell read order is deliberate: reading the activity view last catches sessions that start between the reads. The reverse skew can only make summary text slightly stale — a settle recommendation still passes the client's apply-time canSettle re-check and the decider's thread.settle guards, so no consistency window can settle live work. Rationale is now in a comment at the read site.

🤖 Generated with Claude Code

Real-data testing surfaced the gap the plan had flagged: the sweep
passed changeRequestState: null, so threads the sidebar auto-settles
(merged/closed PR + idle) counted as unsettled — 18 candidates vs ~7
sidebar cards. SidebarV2 now mirrors its per-row PR-state map into the
sweep store, isSweepCandidate consults it, and the pre-run summary
subscribes to a version counter so its count tracks PR states as they
stream in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
// Bump the store version so the pre-run summary recomputes its candidate
// count as PR states stream in from the sidebar rows.
useReviewSweepStore.getState().bumpCandidateVersion();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remount wipes mirrored PR states

Medium Severity

When SidebarV2 unmounts and remounts (e.g., navigating to settings), publishSweepChangeRequestStates overwrites the module-level PR state map with an empty one. This clears cached PR states, causing isSweepCandidate to incorrectly treat settled threads as unsettled. The sweep then over-includes these threads, leading to unnecessary work and model calls, and this incorrect state may persist for some threads.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4faf0e9. Configure here.

return !effectiveSettled(shell, {
now: options.now,
autoSettleAfterDays: options.autoSettleAfterDays,
changeRequestState,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweep clock breaks settle parity

Low Severity

The change to isSweepCandidate now feeds PR state, enabling the merged/closed idle settlement path. However, the sweep uses wall-clock now while SidebarV2 uses a minute-quantized nowMinute. This discrepancy can cause the sweep to omit threads near the one-hour idle boundary that the sidebar still shows as active, breaking the intended partition match.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4faf0e9. Configure here.

Real-data feedback: uniform gray cards grouped by project buried the
actionable items. The results view is now organized by what the user
should do, ordered easiest-to-clear first:

  1. Ready to settle (one-click, emerald accent)
  2. Title fixes (sky)
  3. Needs your attention (amber — awaiting review/input)
  4. In flight / review-failed / still-reviewing tails

Cards are denser, carry a per-bucket accent stripe and a single
primary action, and gain a metadata row: project favicon + name,
branch, thread age, and diff size. Diff stats (+adds/−dels/files) come
from the thread's latest ready checkpoint, returned by the review RPC
as a new optional diffStats field — no extra client requests. Within
each bucket, smallest diff sorts first to build clearing momentum.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
const group = grouped.get(bucket);
return group && group.length > 0 ? [[bucket, group] as const] : [];
});
}, [visibleItems]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium reviewSweep/ReviewSweepView.tsx:441

The buckets memo depends only on visibleItems, but classifySweepItem reads live shell state via readThreadShell(item.ref) to decide whether a thread is inFlight or attention. When a thread's shell changes from running to blocked (or vice versa) without its SweepItem changing, the memo does not recompute, so the card stays in the stale bucket instead of reflecting the live shell state the classification is meant to prioritize. Consider adding a dependency that changes when thread shells update (for example, the array of shells from useThreadShells) so the bucketing recomputes alongside live state.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/reviewSweep/ReviewSweepView.tsx around line 441:

The `buckets` memo depends only on `visibleItems`, but `classifySweepItem` reads live shell state via `readThreadShell(item.ref)` to decide whether a thread is `inFlight` or `attention`. When a thread's shell changes from running to blocked (or vice versa) without its `SweepItem` changing, the memo does not recompute, so the card stays in the stale bucket instead of reflecting the live shell state the classification is meant to prioritize. Consider adding a dependency that changes when thread shells update (for example, the array of shells from `useThreadShells`) so the bucketing recomputes alongside live state.

additions: latestCheckpoint.files.reduce((sum, file) => sum + file.additions, 0),
deletions: latestCheckpoint.files.reduce((sum, file) => sum + file.deletions, 0),
}
: undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diff stats use last turn only

Medium Severity

The diffStats calculation uses only the latest checkpoint's diff. Checkpoints store per-turn changes, not cumulative thread diffs. This understates the total diff for multi-turn threads, impacting features like sorting that assume it reflects the whole thread's effort.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f508f42. Configure here.

const blocked = shell?.hasPendingApprovals === true || shell?.hasPendingUserInput === true;
if (blocked) return "attention";
if (working) return "inFlight";
return "attention";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Queued work mislabeled as attention

Low Severity

Live classification treats only session running/starting as in flight. A just-sent user message that has not been adopted yet (hasQueuedTurnStart) falls through to attention with “Waiting on you,” even though the next step is the agent, not the user.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f508f42. Configure here.

Real-data feedback round two:

- Results render as a responsive grid (1/2/3 columns) instead of a
  full-width list.
- While the sweep runs, the page shows only a progress count
  ("Reviewing your work — 7 of 18"); results reveal all at once when
  every thread is done, so triage happens in one pass over a stable
  layout instead of chasing cards as they pop in.
- The model now returns a one-sentence imperative nextStep ("Review
  and merge PR #4415.") which becomes the card body — a direct answer
  to "what should I do here". Summaries are capped at one sentence and
  demoted to an info-icon tooltip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The screenshot from real-data testing showed nextStep rambling into
status recaps ("The requested comparison and hosted write-up are
complete, and..."). The prompt now demands a verb-first command of at
most 10 words with explicit GOOD/BAD examples, and normalizeThreadReview
backstops drift by truncating to the first sentence, hard-capped at 80
chars. Attention-bucket cards fall back to the summary when an older
server omits nextStep, so they always say what's being waited on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
{item.status === "error" ? (
<p className="line-clamp-2 text-sm text-red-400/90">{item.errorMessage}</p>
) : nextStep ? (
<p className="line-clamp-2 text-sm text-foreground/90">{nextStep}</p>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retry shows blank reviewing card

Medium Severity

The removal of the per-card spinner means that when retrySweepItem is called, cards transition to a running state without the overall sweep phase changing. This leaves retrying cards without a loading indicator or their body content until the retry finishes, making them appear stuck or empty.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ade8487. Configure here.

const nextStep =
nextStepFirstSentence.length > 80
? `${nextStepFirstSentence.slice(0, 77).trimEnd()}...`
: nextStepFirstSentence;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nextStep truncates on internal periods

Medium Severity

The nextStep processing logic truncates commands at the first period, exclamation mark, or question mark. This can cut off valid parts of commands, such as file extensions or version numbers, resulting in incomplete or misleading next steps.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ade8487. Configure here.

: null;
return {
summary: summary.length > 0 ? summary : "No summary available.",
nextStep: nextStep.length > 0 ? nextStep : "Review this thread.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale settle command after suppression

Medium Severity

When settle is forced off for an active thread, recommendSettle and settleReason are cleared but nextStep is left unchanged. Cards now lead with nextStep, so a suppressed settle can still show as the primary command. The same gap appears when apply-time activity withdraws the settle recommendation without updating nextStep.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ade8487. Configure here.

Four features from real-data testing feedback:

- The sidebar button now opens a launch modal (scope + model + cost
  summary) instead of navigating. Starting closes the modal and shows
  a "running in background" toast; completion raises a toast with an
  actionable-count summary and a "View results" link. The sweep was
  already navigation-proof; now the UX matches.

- Reviews investigate deeper: when a thread's branch has a PR, the
  server fetches state, review decision, CI rollup, mergeability, and
  the last five comments via `gh pr view`, feeds them into the review
  prompt, and returns a prStatus with a computed mergeReady flag
  (open + clean merge + CI not failing + not blocked on review).
  Lookups degrade to "no PR context" on any failure.

- New "Merge ready" bucket at the top of the results grid with
  per-card "Merge & settle" and a section-level "Merge all (N)".

- Merging runs through a SEQUENTIAL queue: each PR is re-validated
  against live GitHub state before merging (an earlier queue entry may
  have just changed the base), merged PRs settle their thread
  immediately, and a conflict marks the card and hands the rebase to
  the thread's own agent via a turn message — the queue continues past
  it. review.mergePullRequest returns conflict/already-closed/not-ready
  as outcomes rather than errors so the queue can branch on them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +270 to +276
const recent = input.recentMessages.slice(-THREAD_REVIEW_RECENT_MESSAGE_COUNT);
const transcript = recent
.map(
(message) =>
`[${message.role}] ${limitSection(message.text, THREAD_REVIEW_MESSAGE_CHAR_LIMIT)}`,
)
.join("\n\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium textGeneration/TextGenerationPrompts.ts:270

limitSection(transcript, 24_000) truncates from the front, so when the selected messages exceed 24,000 characters the newest messages are dropped and only older ones remain. This can cause the model to miss the thread's final outcome and produce an incorrect summary or settle recommendation. Consider applying the aggregate cap from the tail so the newest messages are preserved.

  const recent = input.recentMessages.slice(-THREAD_REVIEW_RECENT_MESSAGE_COUNT);
+  let transcriptChars = 0;
  const transcript = recent
    .map(
      (message) =>
        `[${message.role}] ${limitSection(message.text, THREAD_REVIEW_MESSAGE_CHAR_LIMIT)}`,
    )
+    .reverse()
+    .filter((line) => (transcriptChars += line.length + 2) <= 24_000)
+    .reverse()
    .join("\n\n");
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/textGeneration/TextGenerationPrompts.ts around lines 270-276:

`limitSection(transcript, 24_000)` truncates from the front, so when the selected messages exceed 24,000 characters the newest messages are dropped and only older ones remain. This can cause the model to miss the thread's final outcome and produce an incorrect summary or settle recommendation. Consider applying the aggregate cap from the tail so the newest messages are preserved.

}

const outcome = result.value.outcome;
if (outcome === "merged" || outcome === "already-closed") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/reviewSweepStore.ts:486

mergeOne treats the server outcome "already-closed" as "merged", so a PR that was merely closed (not merged) gets its card marked merged and its thread automatically settled. A merge-ready PR can be closed without merging between the sweep review and this action, and this branch incorrectly concludes the work as merged and settles the thread. Consider only matching the "merged" outcome here and handling "already-closed" as a non-merged terminal state.

Suggested change
if (outcome === "merged" || outcome === "already-closed") {
if (outcome === "merged") {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/reviewSweepStore.ts around line 486:

`mergeOne` treats the server outcome `"already-closed"` as `"merged"`, so a PR that was merely closed (not merged) gets its card marked `merged` and its thread automatically settled. A merge-ready PR can be closed without merging between the sweep review and this action, and this branch incorrectly concludes the work as merged and settles the thread. Consider only matching the `"merged"` outcome here and handling `"already-closed"` as a non-merged terminal state.

after.patchItem(key, { mergeStatus: "conflicted", mergeDetail: result.value.detail });
// Hand the conflict to the thread's own agent: it has the branch
// checked out and the full context to rebase and resolve.
await runAtomCommand(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/reviewSweepStore.ts:509

When threadEnvironment.startTurn fails in the conflict branch, mergeOne still returns "conflict" and leaves the card marked conflicted, so the UI shows "agent rebasing" and the queue toast reports the handoff succeeded — but no agent was actually asked to resolve it. The PR can remain stuck with no signal for the user to intervene. Consider checking the startTurn result and, on failure, patching the card to failed with the error detail and returning "failed" instead of "conflict".

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/reviewSweepStore.ts around line 509:

When `threadEnvironment.startTurn` fails in the conflict branch, `mergeOne` still returns `"conflict"` and leaves the card marked `conflicted`, so the UI shows "agent rebasing" and the queue toast reports the handoff succeeded — but no agent was actually asked to resolve it. The PR can remain stuck with no signal for the user to intervene. Consider checking the `startTurn` result and, on failure, patching the card to `failed` with the error detail and returning `"failed"` instead of `"conflict"`.

Comment on lines +135 to +145
if (item.result.prStatus?.mergeReady && !item.settleApplied) return "mergeReady";
if (item.result.recommendSettle && !item.settleApplied) return "settle";
if (item.result.suggestedTitle && !item.titleApplied) return "title";
// Live shell wins over review-time knowledge: a thread blocked on the user
// right now belongs in "attention" even if the review predates the block.
const shell = readThreadShell(item.ref);
const working = shell?.session?.status === "running" || shell?.session?.status === "starting";
const blocked = shell?.hasPendingApprovals === true || shell?.hasPendingUserInput === true;
if (blocked) return "attention";
if (working) return "inFlight";
return "attention";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium reviewSweep/ReviewSweepView.tsx:135

A completed item with recommendSettle: true is classified into the settle bucket even when its live shell now has pending approvals, pending user input, or a running session. This contradicts the intended live-state precedence: the comment says "Live shell wins over review-time knowledge," but the recommendSettle check runs before the blocked/working checks, so a thread that became blocked after the review still shows a stale Settle action (rejected only after the user clicks). Reorder so the live-shell checks take priority over the recommendation checks.

-  if (item.result.prStatus?.mergeReady && !item.settleApplied) return "mergeReady";
-  if (item.result.recommendSettle && !item.settleApplied) return "settle";
-  if (item.result.suggestedTitle && !item.titleApplied) return "title";
   // Live shell wins over review-time knowledge: a thread blocked on the user
   // right now belongs in "attention" even if the review predates the block.
   const shell = readThreadShell(item.ref);
   const working = shell?.session?.status === "running" || shell?.session?.status === "starting";
   const blocked = shell?.hasPendingApprovals === true || shell?.hasPendingUserInput === true;
   if (blocked) return "attention";
   if (working) return "inFlight";
+  if (item.result.prStatus?.mergeReady && !item.settleApplied) return "mergeReady";
+  if (item.result.recommendSettle && !item.settleApplied) return "settle";
+  if (item.result.suggestedTitle && !item.titleApplied) return "title";
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/reviewSweep/ReviewSweepView.tsx around lines 135-145:

A completed item with `recommendSettle: true` is classified into the `settle` bucket even when its live shell now has pending approvals, pending user input, or a running session. This contradicts the intended live-state precedence: the comment says "Live shell wins over review-time knowledge," but the `recommendSettle` check runs before the `blocked`/`working` checks, so a thread that became blocked after the review still shows a stale Settle action (rejected only after the user clicks). Reorder so the live-shell checks take priority over the recommendation checks.

},
{ reportFailure: false },
);
if (settleResult._tag === "Success") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/reviewSweepStore.ts:499

When a PR merge succeeds but the subsequent threadEnvironment.settle call fails, mergeOne ignores the failure and returns "merged" with mergeStatus set to "merged". This leaves the thread unsettled with no error surfaced and no retry path — the card stops offering any action once mergeStatus becomes "merged". Consider surfacing the settle failure (e.g., via a toast or mergeDetail) so the user knows the thread still needs to be settled.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/reviewSweepStore.ts around line 499:

When a PR merge succeeds but the subsequent `threadEnvironment.settle` call fails, `mergeOne` ignores the failure and returns `"merged"` with `mergeStatus` set to `"merged"`. This leaves the thread unsettled with no error surfaced and no retry path — the card stops offering any action once `mergeStatus` becomes `"merged"`. Consider surfacing the settle failure (e.g., via a toast or `mergeDetail`) so the user knows the thread still needs to be settled.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.

There are 12 total unresolved issues (including 7 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.

and resolve; re-run the sweep once it pushes to pick the PR back up. */
export async function runMergeQueue(): Promise<void> {
if (mergeQueueActive) return;
mergeQueueActive = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concurrent merges race the queue

Medium Severity

The merge queue's sequential re-validation is compromised. The per-card merge function (mergeSweepItem) can run concurrently with the main merge queue (runMergeQueue) or with other mergeSweepItem calls. This results in multiple gh pr merge operations on the same base, undermining the queue's design, because mergeQueueActive only gates runMergeQueue.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.

if (settleResult._tag === "Success") {
useReviewSweepStore.getState().patchItem(key, { settleApplied: true });
}
return "merged";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closed PRs reported as merged

High Severity

The mergeOne function conflates already-closed PRs with successfully merged ones. Because the server uses already-closed for both, PRs that were simply closed (not merged) are incorrectly marked as 'merged' and auto-settled, removing them from the review sweep.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.

if (settleResult._tag === "Success") {
useReviewSweepStore.getState().patchItem(key, { settleApplied: true });
}
return "merged";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged cards stuck without settle

Medium Severity

After a successful merge, mergeOne settles without a canSettle check and ignores settle failures silently. When settle is refused (active session, pending approval/input, etc.), settleApplied stays false, so classifySweepItem keeps the card in mergeReady while the only CTA is for idle/failed — leaving a Merged badge with no way to settle from the sweep UI.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.

if (settleResult._tag === "Success") {
useReviewSweepStore.getState().patchItem(key, { settleApplied: true });
}
return "merged";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-run drops post-merge settle

Medium Severity

If runId changes after mergePullRequest succeeds but before the follow-up settle, mergeOne returns skipped and never settles. The GitHub merge cannot be undone, so a Re-run mid-merge can leave a merged PR with an unsettled thread and no applied state on the new run.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.

threadId: input.threadId,
outcome: "conflict" as const,
detail: mergeResult.detail ?? "gh pr merge failed.",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge failures fake conflicts

Medium Severity

Any gh pr merge failure is returned as conflict, and the client always starts a rebase handoff turn with a base-branch-conflict message. Auth, branch-protection, squash-disabled, and network failures therefore kick the wrong agent workflow.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One error-handling convention issue found in the new review service code. See inline comment.

Posted via Macroscope — Effect Service Conventions

Comment on lines +440 to +444
const mapProjectionError = (cause: unknown) =>
new ReviewMergeError({
threadId: input.threadId,
detail: `Failed to read the thread projection: ${String(cause)}`,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapProjectionError stringifies the underlying failure into detail (${String(cause)}) and drops the structured cause entirely, since ReviewMergeError has no cause field. This both copies arbitrary defect text into detail and loses the error chain/stack — the opposite of the sibling mapProjectionError in summarizeThread (lines 307-312), which wraps the same projection read via TextGenerationError with a static detail and a preserved cause.

Add a cause: Schema.optional(Schema.Defect()) field to ReviewMergeError in packages/contracts/src/review.ts and wrap with a static detail instead:

const mapProjectionError = (cause: unknown) =>
  new ReviewMergeError({
    threadId: input.threadId,
    detail: "Failed to read the thread projection.",
    cause,
  });

Posted via Macroscope — Effect Service Conventions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant