WIP: "review unsettled threads" view that summarizes in-progress work#4417
WIP: "review unsettled threads" view that summarizes in-progress work#4417t3dotgg wants to merge 13 commits into
Conversation
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>
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
ApprovabilityVerdict: 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. |
- 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>
|
Addressed all four bot findings in 8aad08d:
🤖 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>
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>
|
Latest bugbot pass (which reviewed dc66ff1):
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/server/src/review/ReviewService.ts (1)
163-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMislabeled
operationon non-generation errors.
mapProjectionErrorand the server-settings load failure both tagTextGenerationError.operationas"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
📒 Files selected for processing (26)
apps/server/src/environment/ServerEnvironment.tsapps/server/src/git/GitManager.test.tsapps/server/src/review/ReviewService.test.tsapps/server/src/review/ReviewService.tsapps/server/src/server.test.tsapps/server/src/server.tsapps/server/src/textGeneration/ClaudeTextGeneration.tsapps/server/src/textGeneration/CodexTextGeneration.tsapps/server/src/textGeneration/CursorTextGeneration.tsapps/server/src/textGeneration/GrokTextGeneration.tsapps/server/src/textGeneration/OpenCodeTextGeneration.tsapps/server/src/textGeneration/TextGeneration.test.tsapps/server/src/textGeneration/TextGeneration.tsapps/server/src/textGeneration/TextGenerationPrompts.test.tsapps/server/src/textGeneration/TextGenerationPrompts.tsapps/server/src/textGeneration/TextGenerationUtils.tsapps/server/src/ws.tsapps/web/src/components/SidebarV2.tsxapps/web/src/components/reviewSweep/ReviewSweepView.tsxapps/web/src/reviewSweepStore.tsapps/web/src/routeTree.gen.tsapps/web/src/routes/review-sweep.tsxpackages/client-runtime/src/state/review.tspackages/contracts/src/environment.tspackages/contracts/src/review.tspackages/contracts/src/rpc.ts
| const projectOption = yield* projectionSnapshotQuery | ||
| .getProjectShellById(thread.projectId) | ||
| .pipe(Effect.mapError(mapProjectionError)); | ||
| const cwd = | ||
| resolveThreadWorkspaceCwd({ | ||
| thread, | ||
| projects: Option.isSome(projectOption) ? [projectOption.value] : [], | ||
| }) ?? process.cwd(); | ||
|
|
There was a problem hiding this comment.
🔒 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.
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
|
Two more updates:
🤖 Generated with Claude Code |
- 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>
|
Cursor's post-merge review pass, addressed in c68e9fa:
🤖 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(); | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 4faf0e9. Configure here.
| return !effectiveSettled(shell, { | ||
| now: options.now, | ||
| autoSettleAfterDays: options.autoSettleAfterDays, | ||
| changeRequestState, |
There was a problem hiding this comment.
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.
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]); |
There was a problem hiding this comment.
🟡 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; |
There was a problem hiding this comment.
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.
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"; |
There was a problem hiding this comment.
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.
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> |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit ade8487. Configure here.
| const nextStep = | ||
| nextStepFirstSentence.length > 80 | ||
| ? `${nextStepFirstSentence.slice(0, 77).trimEnd()}...` | ||
| : nextStepFirstSentence; |
There was a problem hiding this comment.
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.
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.", |
There was a problem hiding this comment.
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.
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>
| 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"); |
There was a problem hiding this comment.
🟡 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") { |
There was a problem hiding this comment.
🟠 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.
| 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( |
There was a problem hiding this comment.
🟡 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"`.
| 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"; |
There was a problem hiding this comment.
🟡 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") { |
There was a problem hiding this comment.
🟡 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.
There was a problem hiding this comment.
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).
❌ 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; |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.
| if (settleResult._tag === "Success") { | ||
| useReviewSweepStore.getState().patchItem(key, { settleApplied: true }); | ||
| } | ||
| return "merged"; |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.
| if (settleResult._tag === "Success") { | ||
| useReviewSweepStore.getState().patchItem(key, { settleApplied: true }); | ||
| } | ||
| return "merged"; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.
| if (settleResult._tag === "Success") { | ||
| useReviewSweepStore.getState().patchItem(key, { settleApplied: true }); | ||
| } | ||
| return "merged"; |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.
| threadId: input.threadId, | ||
| outcome: "conflict" as const, | ||
| detail: mergeResult.detail ?? "gh pr merge failed.", | ||
| }; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 3e757bd. Configure here.
There was a problem hiding this comment.
One error-handling convention issue found in the new review service code. See inline comment.
Posted via Macroscope — Effect Service Conventions
| const mapProjectionError = (cause: unknown) => | ||
| new ReviewMergeError({ | ||
| threadId: input.threadId, | ||
| detail: `Failed to read the thread projection: ${String(cause)}`, | ||
| }); |
There was a problem hiding this comment.
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


Summary
Adds a one-click "Review unsettled work" sweep to sidebar v2. Clicking the new checklist button in the sidebar header opens
/review-sweepand kicks off an agentic pass over every unsettled, unarchived thread across all connected environments: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:

Implementation
ReviewThreadSummaryInput/Result+ReviewThreadNotFoundError, newreview.summarizeThreadWS RPC, newreviewSweepcapability flag so clients skip pre-feature servers under version skew.generateThreadReviewop on theTextGenerationservice, implemented in all five provider backends (Claude/Codex/Cursor/Grok/OpenCode) sharingbuildThreadReviewPrompt(transcript capped to last 20 non-streaming messages, ~2k chars each) and anormalizeThreadReviewsanitizer.ReviewService.summarizeThreadreads the transcript viaProjectionSnapshotQueryand uses the server'stextGenerationModelSelection.summarizeThreadRPC command atom in the review atom group.reviewSweepStore.ts(non-persisted Zustand store + module-level runner withrunIdstaleness guards; sweeps survive navigation),/review-sweeproute +ReviewSweepView, SidebarV2 header button. Applying reuses the existingthread.meta.updateandthread.settlecommands.Settle-safety invariant
A running / awaiting-approval / awaiting-input thread can never be settled through this feature, enforced at four independent layers:
recommendSettle must be false)normalizeThreadReviewforces it off for active threadscanSettleNowcanSettleagainst the live shell at apply timeTesting
ReviewService.summarizeThread(not-found, transcript extraction/streaming exclusion, stale-canSettleNowserver override),buildThreadReviewPrompt(caps, active-thread rule),normalizeThreadReview(sanitization, placeholder-title drop)canSettleNow; cap sorting bycreatedAtinstead of last activity) fixed with regression coverage🤖 Generated with Claude Code
Note
Medium Risk
Touches thread settle recommendations, external LLM prompts (workspace cwd), and automated
gh pr mergewith 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
reviewSweepcapability; newreview.summarizeThreadandreview.mergePullRequestRPCs with typed inputs/results.ReviewServiceloads thread projection, resolves workspacecwd(fails if missing), re-derives activity server-side (pending approvals/input, running session, queued turn start) so settle recommendations ignore stale clientcanSettleNow, optionally enriches viagh pr view(degrades on failure), and runsgenerateThreadReviewacross all text-generation backends with shared prompt/normalization.mergePullRequestre-checks live GitHub merge-readiness then squash-merges viagh.Web: Zustand
reviewSweepStoreruns 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
ReviewServicecoverage for summarize/merge paths; prompt andnormalizeThreadReviewunit 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
reviewSummarizeThreadandreviewMergePullRequestWebSocket RPC pair, backed byReviewServicemethods that generate AI thread summaries (with settle/title recommendations) and trigger squash merges via GitHub CLI.generateThreadReviewacross 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./review-sweeproute 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.startTurnmessage; 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
Bug Fixes