Skip to content

Review engine: multi-pass pipeline, publishing, tool-backed evidence#191

Merged
arul28 merged 17 commits intomainfrom
ade/code-review-dfc1fae7
Apr 24, 2026
Merged

Review engine: multi-pass pipeline, publishing, tool-backed evidence#191
arul28 merged 17 commits intomainfrom
ade/code-review-dfc1fae7

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Apr 24, 2026

Summary

  • Multi-pass review engine: adjudication, rule registry, context builder, tool-backed evidence, suppression memory, inline diff UI.
  • PR review publishing flow end-to-end with IPC; Ticket 3 flow fixes.
  • Ship Lane playbook + /shipLane command; /finalize Phase 3j cleanup of lingering worker processes.

Test plan

  • Unit tests for reviewDiffContext / reviewSuppressionService / reviewToolEvidence (21 new tests)
  • Desktop typecheck + lint + 8-shard vitest: pass
  • ade-cli tests: 185/185 pass
  • All package builds: pass
  • iOS DatabaseBootstrap.sql parity regenerated and verified

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • New Review tab to run automated code reviews (lane/commit/working-tree/PR targets) with launch controls and model options
    • Start/rerun/cancel review runs; view runs, artifacts, findings, inline diff context, and chat transcript
    • Provide feedback (acknowledge/dismiss/snooze), manage suppressions, and open findings in editor/files
    • Publish findings to GitHub PRs as inline comments and summary reviews
    • View quality metrics, learnings panel, and real-time review events

Greptile Summary

This PR ships the full review engine: a multi-pass (diff-risk → cross-file-impact → checks-and-tests) pipeline, adjudication with suppression memory, tool-backed evidence, inline diff UI, and end-to-end PR publication via GitHub review API.

Several issues flagged in previous rounds have been addressed — the byClass SQL query now correctly deduplicates via latest_only CTE, and PATH_GLOB_CACHE_MAX caps the suppression glob cache. The two P1 concerns from earlier iterations (diff-position off-by-one in parseDiffFiles and publishRun passing the full unsliced changedFiles instead of the budget-capped list) remain present and are worth resolving before the publication flow goes to production.

Confidence Score: 4/5

Safe to merge for local-only review runs; the PR publication path has two outstanding P1 issues (diff-position off-by-one, full changedFiles list passed to publisher) that will cause systematically misplaced inline comments on GitHub.

The architecture is well-structured and the adjudication/suppression logic is solid. Several previously flagged issues have been addressed (byClass query deduplication, glob cache bounding). Two P1 issues from prior rounds remain open in the publication path: the hunk-header diff-position off-by-one in parseDiffFiles and publishRun passing the full unsliced file list instead of the budget-capped slice.

apps/desktop/src/main/services/review/reviewTargetMaterializer.ts (diff position off-by-one), apps/desktop/src/main/services/review/reviewService.ts (publishRun changedFiles inconsistency)

Important Files Changed

Filename Overview
apps/desktop/src/main/services/review/reviewService.ts Core review orchestration (2479 lines): multi-pass execution, adjudication, suppression, publication. publishRun passes materialized.changedFiles (full, unsliced list) to publishReviewPublication, inconsistent with changedFiles.slice(maxFiles) used for the model prompt. qualityReport SQL queries look correctly deduplicated with latest_only CTE (addresses prior concern).
apps/desktop/src/main/services/review/reviewTargetMaterializer.ts Materializes review targets from git/GitHub. parseDiffFiles has an off-by-one: first @@ hunk header is not counted, so all diff positions are 1 less than GitHub expects (every inline PR comment will land one line too early).
apps/desktop/src/main/services/prs/prService.ts Adds publishReviewPublication and upgraded submitReview. getReviewSnapshot is always called eagerly even when no findings are anchored (inline comments = 0), making two unnecessary GitHub API calls in the summary-only path.
apps/desktop/src/main/services/review/reviewSuppressionService.ts New suppression service with Jaccard + cosine similarity matching. PATH_GLOB_CACHE_MAX FIFO eviction added correctly; fuzzy title matching thresholds are reasonable.
apps/desktop/src/main/services/state/kvDb.ts Adds review tables via migration. diff_context_json and suppression_match_json are absent from the CREATE TABLE body but added via ALTER TABLE; works correctly but is inconsistent with the other columns in the same table definition.
apps/desktop/src/main/services/review/reviewContextBuilder.ts Builds provenance, rules, and validation context packets. Well-structured with sensible limits and text clipping; no issues found.
apps/desktop/src/main/services/review/reviewToolEvidence.ts Tool-backed evidence collection from CI checks, test runs, and validation signals. Logic and signal classification look correct.
apps/desktop/src/renderer/components/review/ReviewPage.tsx 1833-line review UI with tabs, run management, and finding display. onReviewEvent subscription is correctly unsubscribed on cleanup; no stale-closure or memory-leak issues found.

Sequence Diagram

sequenceDiagram
    participant UI as ReviewPage (Renderer)
    participant IPC as Preload Bridge
    participant RS as ReviewService (Main)
    participant RTM as ReviewTargetMaterializer
    participant RCB as ReviewContextBuilder
    participant LLM as AI Model
    participant ADJ as Adjudicator
    participant SUP as SuppressionService
    participant PRS as PrService

    UI->>IPC: startReviewRun(args)
    IPC->>RS: startRun(args)
    RS->>RTM: materialize(target, config)
    RTM-->>RS: ReviewMaterializedTarget (changedFiles, fullPatchText)
    RS->>RCB: buildContext(run, materialized)
    RCB-->>RS: ReviewContextPacket (provenance, rules, validation)
    loop For each pass
        RS->>LLM: executePass(prompt + diff + context)
        LLM-->>RS: PassCandidateFinding[]
    end
    RS->>ADJ: adjudicate(allCandidates, budgets)
    ADJ-->>RS: AdjudicationOutcome
    loop For each finding
        RS->>SUP: match(finding, repoKey)
        SUP-->>RS: suppressionMatch or null
    end
    alt publishBehavior is auto_publish
        RS->>PRS: publishReviewPublication(runId, destination, findings, changedFiles)
        PRS->>PRS: getReviewSnapshot(prId)
        PRS->>PRS: submitReviewRequest(event, body, inlineComments, commitSha)
        PRS-->>RS: ReviewPublication
        RS->>RS: updateFindingPublicationState
    end
    RS-->>UI: run-completed event
Loading

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/prs/prService.ts
Line: 2094-2103

Comment:
**Eager `getReviewSnapshot` call regardless of anchor state**

`getReviewSnapshot` (which performs two GitHub API calls — `fetchPr` + `getFilesSnapshot`) is called unconditionally at the top of `publishReviewPublication`, before the loop that computes whether any finding is actually `"anchored"`. When all findings fall into `summaryFindings` (no inline comments), the fetched `headSha` is passed to `submitReviewRequest` but is never used (because `inlineComments` is empty and the internal guard `if (inlineComments.length > 0 && !commitSha)` never fires). Move the snapshot fetch inside the `inlineComments.length > 0` branch, or defer it until after the findings loop, to avoid unnecessary API calls on summary-only publications.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/main/services/state/kvDb.ts
Line: 3198-3203

Comment:
**`diff_context_json` / `suppression_match_json` absent from `CREATE TABLE`**

`finding_class`, `originating_passes_json`, and `adjudication_json` appear in both the `CREATE TABLE` body and in subsequent `ALTER TABLE` guards, but `diff_context_json` and `suppression_match_json` exist **only** in the `ALTER TABLE` guards. On a fresh install the table is created without those columns and then they are appended immediately, which works today. But future schema readers or code-gen tools that derive the schema from the `CREATE TABLE` statement will miss these two columns, making it a silent maintenance trap. Adding them to the `CREATE TABLE` body keeps the definition self-consistent.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (6): Last reviewed commit: "ship: iteration 5 — fix flaky TerminalVi..." | Re-trigger Greptile

arul28 and others added 12 commits April 14, 2026 23:33
…d rule registry

Adds multi-pass review pipeline (diff-risk, cross-file-impact, checks-and-tests) with
finding deduplication and adjudication scoring. Introduces reviewContextBuilder for
structured prompt assembly, reviewRuleRegistry for pass-specific rule overlays, and
publication budget controls. Extends review findings schema with finding_class,
originating_passes, and adjudication metadata. Updates ReviewPage UI with expanded
findings display and PrDetailPane issue resolver improvements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces an autonomous PR-to-merge driver that runs automate → finalize
once, then polls CI and review comments on a self-paced 12-min cadence,
fixing valid comments and failing tests in place. Prefers TeamCreate agent
teams when available, falls back to parallel Agent calls otherwise. Opens
the PR via `ade prs create` when possible so it shows up in ADE's PR
tracking; falls back to `gh pr create` only after the agent has genuinely
exhausted the ADE path via `--help`-driven discovery.

Also narrows /automate to run only the new and affected tests (not the
full suite), and makes /finalize's 8-shard parallel run explicit so shards
don't get chained serially.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 68ef71a)
Lane worktrees use symlinks named node_modules pointing at the main
checkout's installed copies. The previous `node_modules/` pattern only
matched directories, so git saw the symlinks as untracked — which in turn
blocked `ade prs create` preflight and forced /shipLane to fall back to
`gh pr create`. Without the slash, the pattern matches both directories
(main checkout) and symlinks (worktrees).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 07623ee)
The 8-shard vitest run, parallel tsup builds, and typecheck fans sometimes
leave worker pools behind after the phase exits. They don't fail CI but
accumulate across runs and can hold file locks. New Phase 3j kills them
scoped to apps/ paths so vitest instances the user may have running
elsewhere aren't affected. Adds a Cleanup line to the Phase 4 summary and
the completion checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3a5fbe5)
# Conflicts:
#	.claude/commands/finalize.md
#	.claude/commands/shipLane.md
#	apps/desktop/src/main/main.ts
#	apps/desktop/src/main/services/ipc/registerIpc.ts
#	apps/desktop/src/main/services/prs/prService.ts
#	apps/desktop/src/main/services/pty/ptyService.test.ts
#	apps/desktop/src/main/services/pty/ptyService.ts
#	apps/desktop/src/preload/global.d.ts
#	apps/desktop/src/preload/preload.ts
#	apps/desktop/src/renderer/browserMock.ts
#	apps/desktop/src/renderer/components/run/AddCommandDialog.tsx
#	apps/desktop/src/shared/ipc.ts
#	apps/desktop/src/shared/types/prs.ts
#	docs/playbooks/ship-lane.md
…o review

- Feedback loop: per-finding acknowledge/dismiss/snooze/suppress with reason
  capture, persisted to review_finding_feedback and displayed as badges on
  finding cards.
- Suppression memory (ADE-27 flagship): new review_suppressions table plus
  reviewSuppressionService that embeds suppressed titles via the existing
  embedding service and KNN-matches candidate findings (embedding cosine with
  Jaccard fallback). Scope tiers: repo / path (glob) / global. Pre-publish
  filter skips matched findings and tags them with scope + similarity. Active
  suppressions inspectable and removable in a new Learnings panel.
- Tool-backed evidence (ADE-26 finishing): reviewToolEvidence promotes CI,
  test, and session-failure signals from the validation context into
  first-class tool_signal Evidence entries on findings, capped at three per
  finding and gated on file or title overlap.
- Inline diff excerpts: reviewDiffContext builds a highlighted ±8 line window
  around the anchored line from the materialized patch; rendered in the new
  ReviewFindingCard with add/del tinting and focus-line border.
- Finding class UX: intent_drift / incomplete_rollout / late_stage_regression
  chips with explanatory tooltips and distinctive tones.
- Edge cases: severity filter chips, filtered-count toggle, cancel-run
  control for running/queued reviews, retry for failed runs, dedicated
  No-findings empty state.
- Learnings tab: quality metrics (runs, noise rate, addressed, dismissed,
  published, by-class), active suppressions list with hit counts, recent
  feedback log.
- Backend: review service exposes cancelRun, recordFeedback,
  listSuppressions, deleteSuppression, qualityReport; new IPC channels +
  preload bridge + browser mocks.
- Tests: 13 new tests (diff-context, tool-evidence, suppression service)
  plus schema updates to keep existing review service + UI suites green;
  46 review-related tests now pass.
- Merge fix: narrow ptyService resume-guard to startupCommand paths so
  terminal create-with-provided-id keeps working; keep strict behavior for
  codex-style resume attempts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- executeRun: guard against cancellation before the first pass and re-check
  after publishRun; clear both activeRuns and cancelledRuns in the finally
  so stale entries can't leak into a rerun.
- recordFeedback (suppress): if the user picks "path" scope but the finding
  has no file path and no explicit pattern, fall back to "repo" so we never
  insert a suppression that can never match.
- reviewToolEvidence: unrelated failing CI checks and test runs were being
  attached to every finding; tightened the gate to require title/body token
  overlap, and scaled the token threshold so single-word summaries like
  "typecheck" still qualify.
- reviewDiffContext: when the anchored line falls outside every hunk, show
  the first hunk unsliced instead of filtering against an empty window
  (previous behavior left just the hunk header on screen).
- New test covering the anchor-outside-hunk path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Unit tests for reviewDiffContext, reviewSuppressionService,
  reviewToolEvidence (21 tests).
- Regenerate DatabaseBootstrap.sql for new pr_convergence_state /
  auto_converge_runs kvDb tables (iOS parity).
- Add window.ade.app.getInfo mock to TabNav.test.tsx to match new
  useEffect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Apr 24, 2026 10:54am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Adds a complete “review” feature: new per-project reviewService, DB schema for runs/findings/feedback/suppressions, IPC/preload/renderer bridges, review execution/context/target/evidence tooling, PR publication integration, UI pages/components, and comprehensive tests.

Changes

Cohort / File(s) Summary
Core wiring & IPC
apps/desktop/src/main/main.ts, apps/desktop/src/main/services/ipc/registerIpc.ts, apps/desktop/src/shared/ipc.ts
Instantiates per-project reviewService, ensures disposal, and registers 12 new IPC.review* handlers plus IPC.reviewEvent.
Review service & tests
apps/desktop/src/main/services/review/reviewService.ts, apps/desktop/src/main/services/review/reviewService.test.ts
Implements createReviewService (run lifecycle, adjudication, persistence, publication, events, cancel/dispose) with extensive integration tests.
Context, target materialization & tests
apps/desktop/src/main/services/review/reviewContextBuilder.ts, .../reviewContextBuilder.test.ts, .../reviewTargetMaterializer.ts, .../reviewTargetMaterializer.test.ts
Builds review context packets and materializes targets (lane/commit/working-tree/PR) with tests for payload structure and modes.
Diff context, evidence, rules & tests
apps/desktop/src/main/services/review/reviewDiffContext.ts, .../reviewDiffContext.test.ts, .../reviewToolEvidence.ts, .../reviewToolEvidence.test.ts, .../reviewRuleRegistry.ts
Parses unified diffs into anchored contexts, extracts tool-backed evidence, and registers rule overlays with matching utilities.
Suppression service & tests
apps/desktop/src/main/services/review/reviewSuppressionService.ts, .../reviewSuppressionService.test.ts
Implements suppression CRUD and match logic (glob/path scope, embeddings/title similarity) with integration tests.
PR service changes & tests
apps/desktop/src/main/services/prs/prService.ts, .../prService.reviewPublication.test.ts
Adds getReviewSnapshot, changes submitReview to return structured result, and adds publishReviewPublication for posting inline comments; tests verify positioning and summary.
DB schema / migrations
apps/desktop/src/main/services/state/kvDb.ts, apps/ios/ADE/Resources/DatabaseBootstrap.sql
Adds review_runs, review_findings, review_run_publications, review_run_artifacts, review_finding_feedback, review_suppressions and indexes; guarded ALTERs for optional columns.
PTY / process / lane updates & tests
apps/desktop/src/main/services/pty/ptyService.ts, .../ptyService.test.ts, apps/desktop/src/main/services/processes/processService.ts, .../processService.test.ts, apps/desktop/src/main/services/lanes/laneLaunchContext.ts, .../laneLaunchContext.test.ts, apps/desktop/src/main/services/lanes/laneService.ts
Adds allowExternalCwd support, stricter resume/new-session semantics (allowNewSessionId), resolves/validates absolute CWDs, and cascades review cleanup on lane deletion; tests updated/added.
Review materialization & publication helpers
apps/desktop/src/main/services/review/reviewTargetMaterializer.ts, apps/desktop/src/main/services/review/reviewToolEvidence.ts
Materializer builds fullPatchText, per-file excerpts, untracked snapshots; tool-evidence builder maps validation/checks/test signals to evidence entries.
Preload / global API / renderer bridge
apps/desktop/src/preload/preload.ts, apps/desktop/src/preload/global.d.ts, apps/desktop/src/preload/preload.test.ts
Adds window.ade.review API surface (list/start/rerun/cancel/feedback/suppressions/qualityReport/onEvent) and updates prs.submitReview return type; tests verify IPC mappings and event wiring.
Renderer: page, components, api, types & tests
apps/desktop/src/renderer/components/review/ReviewPage.tsx, .../ReviewPage.test.tsx, .../ReviewFindingCard.tsx, .../ReviewLearningsPanel.tsx, .../reviewApi.ts, .../reviewRouteState.ts, .../reviewRouteState.test.ts, .../reviewTypes.ts, .../review/ReviewFindingCard.tsx
Implements /review UI, findings card with feedback/suppress modal, learnings panel, client review API, route-state helpers, and type re-exports; tests for routing, interactions, rendering and event handling.
Navigation, mocks & utilities
apps/desktop/src/renderer/components/app/App.tsx, .../TabNav.tsx, .../TabNav.test.tsx, apps/desktop/src/renderer/browserMock.ts, apps/desktop/src/renderer/components/shared/ReviewLaunchModelControls.tsx, apps/desktop/src/renderer/components/run/AddCommandDialog.tsx
Adds lazy /review route, “Review” tab, browser mock review surface, model selector for review launches, and small UI text tweak.
Shared types updates
apps/desktop/src/shared/types/review.ts, .../prs.ts, .../sessions.ts, .../index.ts
Adds comprehensive review-related types (runs, findings, artifacts, publications, feedback, suppressions, evidence, events), PR snapshot/result types, and session flags (allowNewSessionId, allowExternalCwd).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

desktop, ios

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title clearly and concisely summarizes the main changes: multi-pass review pipeline, publishing capability, and tool-backed evidence integration.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 ade/code-review-dfc1fae7

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 and usage tips.

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 24, 2026

@copilot review but do not make fixes

Comment thread apps/desktop/src/main/services/review/reviewService.ts
Comment thread apps/desktop/src/main/services/review/reviewSuppressionService.ts
Comment thread apps/desktop/src/main/services/review/reviewService.ts
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 24, 2026

@copilot review but do not make fixes

Here's my review of the PR changes.


Review

Summary

This is a large, well-architected addition (~12,600 lines) introducing a multi-pass review engine, PR publication, suppression memory, tool-backed evidence, and inline diff UI. The structural design is solid — clean service boundaries, good use of dependency injection, and thorough unit test coverage. A few issues are worth flagging.


Issues

1. mapPublicationRow — dead conditional (line 1265, reviewService.ts)

reviewEvent: row.review_event === "COMMENT" ? "COMMENT" : "COMMENT",

Both branches are identical. The conditional can never distinguish between stored values. If the DB ever contains a non-COMMENT value (future event type), this silently collapses it. Should just be "COMMENT" as "COMMENT" or, better, leave room for future event types.


2. titleMatchesSignal — unsanitized regex injection (reviewToolEvidence.ts, line 68)

if (new RegExp(`\\b${token}\\b`).test(haystack)) hits += 1;

token is derived from signal.summary, which comes from CI check output, test run names, or session failure text. If summary contains regex-special characters (e.g. ., (, +, [), this will either throw or match incorrectly. The token should be escaped before inserting into the regex:

const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
if (new RegExp(`\\b${escaped}\\b`).test(haystack)) hits += 1;

3. suppressedCount artifact uses wrong artifactType (reviewService.ts, line 2066)

artifactType: "tool_evidence",

The suppression filter summary artifact is recorded with artifactType: "tool_evidence", but its content is about suppression, not tool evidence. This makes it hard to distinguish in the artifact list. "publication_request" or a dedicated suppression type would be more accurate. The ReviewArtifactType union in review.ts doesn't include a suppression_summary variant, so either extending the union or using "merged_findings" (already exists) would be a cleaner fit.


4. matchesPathPattern — limited glob support (reviewRuleRegistry.ts, lines 31–37)

function matchesPathPattern(filePath: string, pattern: string): boolean {
  if (pattern.endsWith("/**")) {
    return filePath.startsWith(pattern.slice(0, -3));
  }
  return filePath === pattern;
}

This handles only /**-suffix and exact-match patterns. Patterns like apps/desktop/src/preload/global.d.ts work as exact matches, but any mid-path glob (e.g., apps/**/foo.ts) or *.ts extension patterns will silently never match. The existing rule definitions only use /** and exact patterns, so this isn't a current bug — but it's a footgun if new overlays are added with different glob shapes.


5. PATH_GLOB_CACHE — unbounded module-level cache (reviewSuppressionService.ts, line 16)

const PATH_GLOB_CACHE = new Map<string, RegExp>();

This is a module-level singleton that grows for every unique glob pattern encountered across all service instances (including test runs). There's no eviction. Under normal workloads with static suppression patterns this is fine, but worth noting if the suppression pathPattern field ever accepts user-generated patterns at high cardinality.


6. extractJsonObject — candidate ordering may parse prose as JSON (line 343–388, reviewService.ts)

The function pushes the entire raw response string as the first candidate. If an LLM returns valid JSON embedded in prose (e.g., "Sure, here is the result: {...}"), JSON.parse(trimmed) will fail and fall through — but if the raw string happens to start with {, the full-string parse is attempted before the brace-scanner extracts the inner object. In practice, the brace-scanner is robust enough to handle this for most LLM outputs, but the candidate order means the full prose-with-JSON-block is tried first, potentially succeeding with an unexpected parse in edge cases.


7. laneService.getLaneBaseAndBranch(laneId) return value discarded (reviewService.ts, line 2138)

laneService.getLaneBaseAndBranch(laneId);

Called only for its side-effect of throwing if the lane doesn't exist. The return value is ignored. This is intentional but is unidiomatic — a void-typed existence check method or a separate assertLaneExists would be clearer.


8. combineConfidence cap at 0.99 (reviewService.ts, line 834)

return clampNumber(1 - combined, 0, 0.99);

The hard cap at 0.99 means a single finding already at confidence: 0.99 passes through unchanged, which is fine. But across a three-pass merge of high-confidence findings, the combined result never exceeds 0.99, even when all three passes agree. The cap prevents the adjudicated confidence from influencing publicationEligible at the highest tier. This is a deliberate conservative choice, but worth making explicit in comments to avoid someone raising it in the future.


Minor observations

  • resolveBuiltinReviewModelId() is called at module load time (line 163) and also inside the factory (lines 1332–1336), producing two potentially different resolved IDs. The module-level DEFAULT_REVIEW_MODEL_ID is only used as a config parse fallback (line 1173); the instance-level defaultReviewModelId drives actual decisions. The duplication isn't a bug but is easy to confuse.

  • noiseRate (reviewService.ts, line 2437) includes both dismissedCount and suppressedCount in the numerator. Suppressions are intentional ignore rules, not noise per se. Conflating the two makes the metric harder to interpret — a project that heavily uses suppression rules will show high "noise rate" even if reviews are high quality.

  • The suppression match function loads up to 500 suppressions on every call and iterates all of them. For projects with a large suppression list, this is O(n) per finding per run. At 500 suppressions × 12 findings per run, this is 6,000 iterations + embedding calls. Not a problem at current scale, but worth indexing if suppression counts grow.

  • ReviewPublication.reviewEvent is typed as "COMMENT" only, but the DB column is named review_event and stored as a string, suggesting the type was intended to support "REQUEST_CHANGES" / "APPROVE" in the future. The type should either be widened now or the column should have a CHECK constraint.

Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 2 comments

if (hunkMatch) {
currentNewLine = Number(hunkMatch[1] ?? "0");
continue;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

GitHub's position parameter documentation states: "The position in the diff continues to increase through lines of whitespace and additional hunks until the beginning of a new file." This means every @@ hunk header after the first one in a file counts as a position in the diff. However, the parser's continue at line 128 unconditionally skips the currentDiffPosition += 1 increment for all hunk headers, not just the first one.

For a file with N hunks, all positions in hunks 2..N are off by (hunk_index - 1). This causes publishReviewPublication to submit inline comments at wrong diff positions, which GitHub either silently attaches to the wrong line or rejects.

// reviewTargetMaterializer.ts
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
if (hunkMatch) {
  currentNewLine = Number(hunkMatch[1] ?? "0");
  continue; // skips currentDiffPosition++ for ALL hunk headers
}

The fix: increment currentDiffPosition for hunk headers that are not the first in a file (i.e., when currentDiffPosition > 0). The first hunk header is correctly skipped since position 1 begins at the line below it, but subsequent hunk headers are lines in the diff that contribute to the position counter.

Comment on lines +2535 to +2536
db.run("delete from review_run_publications where run_id in (select id from review_runs where lane_id = ? and project_id = ?)", [laneId, projectId]);
db.run("delete from review_findings where run_id in (select id from review_runs where lane_id = ? and project_id = ?)", [laneId, projectId]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The lane deletion cleanup explicitly deletes review_run_publications, review_findings, review_run_artifacts, and review_runs to avoid relying on CASCADE (the comment at line 2529 explains CRR conversion can strip checked foreign keys). However, review_finding_feedback — which references review_findings(id) with ON DELETE CASCADE — is not explicitly deleted. Following the same reasoning documented in the code, orphaned review_finding_feedback rows will accumulate when CRR is active.

// laneService.ts — missing line before review_findings deletion:
db.run("delete from review_run_publications where run_id in (select id from review_runs where lane_id = ? and project_id = ?)", [laneId, projectId]);
db.run("delete from review_findings where run_id in (select id from review_runs where lane_id = ? and project_id = ?)", [laneId, projectId]);
// ↑ review_finding_feedback should be deleted BEFORE review_findings

Add: db.run("delete from review_finding_feedback where finding_id in (select id from review_findings where run_id in (select id from review_runs where lane_id = ? and project_id = ?))", [laneId, projectId]); before the review_findings deletion.

Suggested change
db.run("delete from review_run_publications where run_id in (select id from review_runs where lane_id = ? and project_id = ?)", [laneId, projectId]);
db.run("delete from review_findings where run_id in (select id from review_runs where lane_id = ? and project_id = ?)", [laneId, projectId]);
db.run("delete from review_run_publications where run_id in (select id from review_runs where lane_id = ? and project_id = ?)", [laneId, projectId]);
db.run("delete from review_finding_feedback where finding_id in (select id from review_findings where run_id in (select id from review_runs where lane_id = ? and project_id = ?))", [laneId, projectId]);
db.run("delete from review_findings where run_id in (select id from review_runs where lane_id = ? and project_id = ?)", [laneId, projectId]);

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

🧹 Nitpick comments (7)
apps/desktop/src/shared/types/prs.ts (1)

1072-1077: Consider documenting position semantics on the inline comment type.

The position field here maps to GitHub's diff-position offset (lines in the unified diff), not a source-file line number. Since prService.submitReviewRequest already enforces Number.isFinite(position) && position > 0, a short doc comment here would help renderer/IPC callers avoid conflating position with source line numbers when building inline review payloads (e.g., from reviewDiffContext ±8-line excerpts).

✏️ Proposed doc addition
-  comments?: Array<{
-    path: string;
-    body: string;
-    position: number;
-  }>;
+  comments?: Array<{
+    path: string;
+    body: string;
+    /** Offset within the unified diff (1-based), not a source-file line number. See GitHub's PR reviews API. */
+    position: number;
+  }>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/shared/types/prs.ts` around lines 1072 - 1077, The inline
comment type's position field is ambiguous: document that comments?: Array<{
path, body, position }> uses GitHub's diff-position offset (unified-diff line
index) rather than a source-file line number; add a short doc comment above the
comments property describing the semantics and the invariant (position is a
positive finite diff-position as enforced by prService.submitReviewRequest) and
mention callers like reviewDiffContext to avoid conflation when building inline
review payloads.
apps/desktop/src/main/services/lanes/laneService.ts (1)

2527-2547: Consider wrapping lane-delete DB cleanup in a single transaction.

The 16 sequential db.run(...) deletes (now extended with 4 review-table deletes) are not atomic. If the process crashes between, say, the review_run_publications delete and the final lanes row delete, the lane row persists while half of its dependent rows are gone — leaving the Learnings panel and publication history showing references to a defunct lane. Since node:sqlite supports BEGIN/COMMIT, wrapping this block is cheap and converts partial-failure into a clean retry.

Not introduced by this PR, but the new review deletes widen the failure surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/lanes/laneService.ts` around lines 2527 -
2547, The series of db.run(...) deletion calls in the lane deletion path should
be executed inside a single database transaction so the cleanup is atomic; wrap
the block that runs db.run(...) (the deletes that reference laneId/projectId and
the logSlowDeleteStep("database_cleanup", dbCleanupStartedAt) call) with BEGIN
and COMMIT and ensure you issue ROLLBACK on any error, using the same sqlite
client methods (db.run/exec) you already use, and propagate or handle errors so
partial deletes cannot persist (ensure transaction uses laneId/projectId
parameters unchanged and that the final deletion of lanes and the
logSlowDeleteStep occur only after a successful COMMIT).
apps/desktop/src/renderer/components/review/reviewRouteState.test.ts (1)

1-14: Consider adding a round-trip case for missing runId query param.

Coverage is solid for the whitespace and trimmed-id paths. One small gap: readReviewRunId("") and readReviewRunId("?other=1") aren't exercised, yet ReviewPage syncs state from location.search on navigation — confirming the "no runId" branch returns ""/null consistently would lock in the contract both functions share.

🧪 Proposed additional case
 describe("reviewRouteState", () => {
   it("omits whitespace-only run ids from the search string", () => {
     expect(buildReviewSearch("   ")).toBe("");
   });

   it("round-trips a trimmed run id", () => {
     const search = buildReviewSearch("  run-123  ");
     expect(search).toBe("?runId=run-123");
     expect(readReviewRunId(search)).toBe("run-123");
   });
+
+  it("returns an empty run id when search has no runId param", () => {
+    expect(readReviewRunId("")).toBe("");
+    expect(readReviewRunId("?other=1")).toBe("");
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/review/reviewRouteState.test.ts` around
lines 1 - 14, Add a test that exercises the "no runId" branch by asserting
readReviewRunId returns an empty string (or the established sentinel this
codebase uses) for both an empty search and a search with unrelated params, and
assert buildReviewSearch of an empty/undefined run id produces an empty string;
specifically add assertions calling readReviewRunId("") and
readReviewRunId("?other=1") expecting "" (matching the contract used by
buildReviewSearch) and confirm buildReviewSearch("") (or
buildReviewSearch(undefined) if applicable) === "" to ensure the round-trip
behavior between buildReviewSearch and readReviewRunId is consistent.
apps/desktop/src/main/services/processes/processService.ts (1)

588-596: Swallowed error hides actionable failure modes.

The try/catch collapses every statSync/realpathSync failure into cwd does not exist, but statSync can also fail with EACCES (no read/execute permission), ENOTDIR, ELOOP, etc. Surfacing the real cause makes process-start failures diagnosable from logs.

♻️ Suggested refactor
 if (allowExternalCwd) {
-  try {
-    const resolved = path.resolve(configuredCwd);
-    const stat = fs.statSync(resolved);
-    if (!stat.isDirectory()) throw new Error("Path is not a directory");
-    cwd = fs.realpathSync(resolved);
-  } catch {
-    throw new Error(`Process '${definition.id}' cwd does not exist: ${configuredCwd}`);
-  }
+  const resolved = path.resolve(configuredCwd);
+  let stat: fs.Stats;
+  try {
+    stat = fs.statSync(resolved);
+  } catch (err) {
+    const code = (err as NodeJS.ErrnoException)?.code ?? "UNKNOWN";
+    throw new Error(`Process '${definition.id}' cwd is not accessible (${code}): ${configuredCwd}`);
+  }
+  if (!stat.isDirectory()) {
+    throw new Error(`Process '${definition.id}' cwd is not a directory: ${configuredCwd}`);
+  }
+  cwd = fs.realpathSync(resolved);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/processes/processService.ts` around lines 588
- 596, The try/catch around resolving configuredCwd swallows the real filesystem
error; modify the block in processService.ts (around allowExternalCwd handling
and the path resolution using path.resolve, fs.statSync, fs.realpathSync) to
capture the caught error (e.g., err) and include its message (or set it as the
cause) in the thrown Error for Process '${definition.id}' so failures like
EACCES/ENOTDIR/ELOOP surface; ensure you still validate isDirectory and preserve
the original error information when throwing the new error referencing
configuredCwd and definition.id.
apps/desktop/src/main/services/review/reviewContextBuilder.test.ts (1)

1-125: Extract the shared review sql.js harness before it drifts.

This bootstrap/row-mapper/AdeDb shim is now duplicated here and in apps/desktop/src/main/services/review/reviewSuppressionService.test.ts. The next schema or adapter tweak will leave one of these suites validating a different setup than the other.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/review/reviewContextBuilder.test.ts` around
lines 1 - 125, Duplicate sql.js test harness (mapExecRows, AdeDb type,
createInMemoryAdeDb, and SQL init) should be extracted into a shared test
utility module and imported by both reviewContextBuilder.test.ts and
reviewSuppressionService.test.ts; create a new file (e.g.,
test/utils/sqlHarness.ts) that exports mapExecRows, AdeDb, createInMemoryAdeDb,
and the SQL initialization helper or SQL instance, update both tests to import
these symbols instead of defining them inline, and ensure exported helpers
initialize SQL (initSqlJs) once and expose createInMemoryAdeDb, mapExecRows, and
the AdeDb types so both suites use the exact same schema and adapter shim.
apps/desktop/src/preload/preload.test.ts (1)

80-126: Cover the rest of the review bridge contract here.

This only exercises part of window.ade.review. The same surface now also exposes cancelRun, recordFeedback, listSuppressions, deleteSuppression, and qualityReport, so a channel mismatch there would still pass. A small table-driven sweep over the full bridge would make this contract much harder to drift.

As per coding guidelines apps/desktop/**/{ipc,preload,types,shared}/**/*.{ts,tsx}: Keep IPC contracts, preload types, shared types, and renderer usage in sync whenever an interface changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/preload/preload.test.ts` around lines 80 - 126, The test
only covers part of window.ade.review; add a table-driven test that iterates the
full bridge surface—methods cancelRun, recordFeedback, listSuppressions,
deleteSuppression, qualityReport (in addition to the already-tested
listLaunchContext, listRuns, getRunDetail, startRun, rerun)—and for each entry
call bridge.review.<method>(...sampleArgs) and assert ipcRenderer.invoke was
called with the corresponding IPC constant (e.g., IPC.reviewCancelRun,
IPC.reviewRecordFeedback, IPC.reviewListSuppressions,
IPC.reviewDeleteSuppression, IPC.reviewQualityReport) and the expected payload;
keep using the same mocked exposeInMainWorld/ipcRenderer and also include checks
that event subscription uses IPC.reviewEvent and removal still calls
removeListener with the listener returned earlier. Ensure you reference
bridge.review and the IPC constants (IPC.reviewCancelRun,
IPC.reviewRecordFeedback, IPC.reviewListSuppressions,
IPC.reviewDeleteSuppression, IPC.reviewQualityReport) so the test fails if any
channel/method drifts.
apps/desktop/src/preload/preload.ts (1)

921-932: Prefer static type imports for review feedback/suppression types.

The rest of this file imports all shared types via the single top-level import type { ... } from "../shared/types" block. These four inline import("../shared/types").Review* types (ReviewRecordFeedbackArgs, ReviewFeedbackRecord, ReviewListSuppressionsArgs, ReviewSuppression, ReviewQualityReport) break that consistency for no runtime benefit. Moving them to the top import block matches the convention used for ReviewEventPayload, ReviewLaunchContext, etc. added just above.

♻️ Suggested consolidation
   ReviewEventPayload,
+  ReviewFeedbackRecord,
   ReviewLaunchContext,
   ReviewListRunsArgs,
+  ReviewListSuppressionsArgs,
+  ReviewQualityReport,
+  ReviewRecordFeedbackArgs,
   ReviewRun,
   ReviewRunDetail,
   ReviewStartRunArgs,
+  ReviewSuppression,
-    recordFeedback: async (
-      args: import("../shared/types").ReviewRecordFeedbackArgs,
-    ): Promise<import("../shared/types").ReviewFeedbackRecord> =>
-      ipcRenderer.invoke(IPC.reviewRecordFeedback, args),
-    listSuppressions: async (
-      args: import("../shared/types").ReviewListSuppressionsArgs = {},
-    ): Promise<import("../shared/types").ReviewSuppression[]> =>
-      ipcRenderer.invoke(IPC.reviewListSuppressions, args),
-    deleteSuppression: async (suppressionId: string): Promise<boolean> =>
-      ipcRenderer.invoke(IPC.reviewDeleteSuppression, { suppressionId }),
-    qualityReport: async (): Promise<import("../shared/types").ReviewQualityReport> =>
+    recordFeedback: async (args: ReviewRecordFeedbackArgs): Promise<ReviewFeedbackRecord> =>
+      ipcRenderer.invoke(IPC.reviewRecordFeedback, args),
+    listSuppressions: async (
+      args: ReviewListSuppressionsArgs = {},
+    ): Promise<ReviewSuppression[]> =>
+      ipcRenderer.invoke(IPC.reviewListSuppressions, args),
+    deleteSuppression: async (suppressionId: string): Promise<boolean> =>
+      ipcRenderer.invoke(IPC.reviewDeleteSuppression, { suppressionId }),
+    qualityReport: async (): Promise<ReviewQualityReport> =>
       ipcRenderer.invoke(IPC.reviewQualityReport),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/preload/preload.ts` around lines 921 - 932, The inline
type-imports used in the IPC wrapper methods recordFeedback, listSuppressions,
deleteSuppression, and qualityReport should be moved to the file's top-level
type import block to match the existing pattern (e.g., where ReviewEventPayload
and ReviewLaunchContext are imported); update the top import type list to
include ReviewRecordFeedbackArgs, ReviewFeedbackRecord,
ReviewListSuppressionsArgs, ReviewSuppression, and ReviewQualityReport, then
replace the inline import(...) type annotations in those function signatures
with the corresponding named types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/main.ts`:
- Around line 3707-3711: The fast-shutdown path (runImmediateProcessCleanup)
currently skips disposing ctx.reviewService causing in-flight reviews to
continue; update the fast-shutdown/cleanup routine (runImmediateProcessCleanup)
to call and await ctx.reviewService?.dispose?.() (or invoke the same disposal
logic used in normal teardown) and wrap it in a try/catch similar to the normal
context teardown block to ensure it runs safely on
uncaughtException/SIGTERM/forced exit so the reviewService is properly stopped
before process exit.

In `@apps/desktop/src/main/services/lanes/laneLaunchContext.ts`:
- Around line 55-63: The current early-return for allowExternalCwd in
laneLaunchContext.ts allows any absolute requestedCwd to bypass
resolvePathWithinRoot and be used directly (propagated to ptyService.ts),
creating a security escape; change the branch so that when args.allowExternalCwd
is true you validate requestedCwd against an explicit trusted roots whitelist
(e.g., configured trustedRootPaths) and only accept it if
path.resolve(requestedCwd) is inside one of those trusted roots, otherwise fall
back to resolvePathWithinRoot(requestedCwd) or reject; keep the use of
ensureDirectoryExists for existence checks and preserve laneWorktreePath
behavior, updating references to allowExternalCwd, resolvePathWithinRoot, and
ensureDirectoryExists in laneLaunchContext.ts accordingly so callers like
ptyService.ts receive only vetted paths.

In `@apps/desktop/src/main/services/lanes/laneService.ts`:
- Around line 2535-2538: The cleanup sequence in laneService.ts fails to
explicitly remove review_finding_feedback rows, risking orphaned feedback if FK
constraints were stripped; add a db.run deletion for review_finding_feedback
that targets feedback whose finding_id is in findings for runs of the given
lane/project (i.e., before the existing deletion of review_findings), using the
same parameter array [laneId, projectId] and matching the pattern used by the
other db.run calls that reference review_runs/review_findings.

In `@apps/desktop/src/main/services/processes/processService.ts`:
- Around line 585-608: The overlay cwd handling in processService currently lets
absolute configuredCwd (checked via allowExternalCwd) bypass containment
validation; update the block that handles allowExternalCwd to validate the
resolved absolute path against the lane/project root (or an explicit allowlist)
before accepting it—reuse or mirror the containment logic used by
resolvePathWithinRoot (or call a new helper like validatePathWithinRoot) and
throw the same ProcessDefinition-style errors when the path is outside or
missing; alternatively, if absolute overlay cwds are intentionally permitted,
add a clear docstring on ProcessDefinition.cwd and LaneOverlayOverrides.cwd
explaining that overlay absolute paths bypass containment checks.

In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 278-290: buildPublishedReviewCommentBody currently inserts raw
finding.body and evidence.quote into PR comments; update this function (and the
code path that posts findings to GitHub) to run both finding.body and the
extracted quote through the project’s existing redaction/sanitization utility
before including them, and if no safe redacted output can be produced or the
evidence type is non-diff, suppress publishing that evidence (only include
severity/title/confidence). Preserve the code fence and length truncation logic
for quote but apply truncation after redaction, and ensure you reference
ReviewFinding when locating these fields to change.
- Around line 2106-2108: The follow-up refresh after creating the review must be
best-effort: wrap the call to refreshOne(args.prId) (and/or
markHotRefresh([args.prId])) inside a try/catch in submitReviewRequest so any
error from refreshOne is caught, logged, and not rethrown; ensure
publishReviewPublication is called based on the POST success (not the refresh
result) so a refresh failure does not mark the publication as failed or cause
duplicate retries. Use submitReviewRequest, refreshOne, markHotRefresh, and
publishReviewPublication to locate and update the flow.

In `@apps/desktop/src/main/services/review/reviewContextBuilder.ts`:
- Around line 523-533: The loop over normalizedChecks currently marks any
completed check whose conclusion is not "success" as a failure, which
incorrectly includes "neutral", "skipped", and "cancelled"; update the failure
detection in the for loop that populates signals (the normalizedChecks iteration
that uses VALIDATION_SIGNAL_LIMIT and pushes into signals via clipText) so that
a signal is only created when the check's conclusion is explicitly "failure"
(i.e., change the failed condition to require check.conclusion === "failure"
rather than completed && conclusion !== "success"), leaving the rest of the push
logic (summary, filePaths, sourceId) unchanged.

In `@apps/desktop/src/main/services/review/reviewDiffContext.ts`:
- Around line 72-80: The current filter unconditionally keeps deletions because
it only checks entry.line == null; instead, only retain deletion entries that
fall within the desired context window around the target lines. Update the loop
over parsed.lines so that when entry.line == null you check the deletion's
original line number (e.g., entry.oldLine or entry.originalLine) or its
proximity to the nearest non-deleted entry and only push entry into filtered if
that line number is within the ±contextWindow (e.g., ±8) of the current
hunk/target lines; otherwise skip the deletion. Ensure you reference
parsed.lines and the entry.line==null branch (and use
entry.oldLine/entry.originalLine or nearest non-null entry) when implementing
the conditional.
- Around line 113-115: The computed endLine can become less than startLine for
pure deletions (chosen.newLength === 0) which yields an inverted range; update
the logic around lineNumbers/startLine/endLine in ReviewDiffContext so that the
fallback guarantees a non-inverted range — compute startLine as you already do,
compute endLine as the max of the calculated value and startLine (e.g., use
Math.max(calculatedEnd, startLine)) or otherwise clamp using chosen.newStart and
chosen.newLength so endLine >= startLine; reference the variables sliced.lines,
lineNumbers, startLine, endLine, chosen.newStart and chosen.newLength when
making the fix.

In `@apps/desktop/src/main/services/review/reviewService.ts`:
- Around line 2078-2089: The code still calls await publishRun(...) even after
the cancellation gate, risking posting an external GitHub review after user
cancellation; update the flow to re-check cancellation before publishing or make
publishRun cancellation-aware. Specifically, either (A) check the run's
cancellation flag/state for runId (or a local isCancelled variable set by the
cancellation gate) and skip/return early instead of calling publishRun, or (B)
extend publishRun to accept an AbortSignal/timeout and pass the current
cancellation signal so publishRun can abort and not post the external review;
reference publishRun, runId, materialized, adjudication, and effectiveRun when
making the change. Ensure no external publish is attempted when cancellation is
detected.
- Around line 2323-2330: The code silently converts a requested path-level
suppression into a repo-level suppression when no usable file path is provided;
update the logic in the function around requestedScope/requestedPathPattern
(variables: requestedScope, requestedPathPattern, hasUsablePathPattern, scope,
pathPattern) so that if requestedScope === "path" and hasUsablePathPattern is
false you do not widen scope but instead return an error (or throw) indicating a
missing/invalid pathPattern to the caller; ensure callers receive a clear
failure rather than silently storing a repo-wide suppression.

In `@apps/desktop/src/main/services/review/reviewSuppressionService.ts`:
- Around line 287-289: The current check lets a class-scoped suppression match
an unclassified finding because it requires both suppression.findingClass and
args.finding.findingClass to be truthy; change the guard to only require the
suppression to have a class and then compare it to the finding: replace the
condition with something like "if (suppression.findingClass &&
suppression.findingClass !== args.finding.findingClass) continue;" so any
suppression with a findingClass will not match when args.finding.findingClass is
null/undefined.

In `@apps/desktop/src/main/services/review/reviewToolEvidence.ts`:
- Around line 105-106: The loop over args.validation.checks (for (const check of
args.validation.checks)) is allowing checks with null/undefined conclusions to
be treated like failures; change the guard so only checks whose conclusion is
explicitly "failure" or "action_required" are processed. Update the condition on
check.conclusion to explicitly skip when conclusion is missing or not one of
"failure" or "action_required" (e.g., add a null/undefined check or invert the
logic so you only proceed when check.conclusion === "failure" ||
check.conclusion === "action_required") to prevent queued/in-progress checks
from being attached as evidence.

In `@apps/desktop/src/preload/global.d.ts`:
- Line 1487: The new submitReview signature references SubmitPrReviewResult but
that type isn't imported into this declaration file; add an import (or include
in the existing type import list) for SubmitPrReviewResult so the submitReview:
(args: SubmitPrReviewArgs) => Promise<SubmitPrReviewResult> line typechecks.
Locate where other types like SubmitPrReviewArgs are imported and add
SubmitPrReviewResult to the same import declaration (or add a new type-only
import) to resolve the missing symbol referenced by submitReview.

In `@apps/desktop/src/renderer/components/review/reviewApi.ts`:
- Around line 15-27: ReviewBridge's types diverge from the preload contract;
change the signatures to match preload: set startRun: (args: ReviewStartRunArgs)
=> Promise<ReviewRun>, rerun: (runId: string) => Promise<ReviewRun>, and
listLaunchContext: () => Promise<ReviewLaunchContext> (non-nullable). Update
callers startReviewRun and rerunReview to expect a ReviewRun result and simplify
their normalization to return { runId: result?.id ?? null } accordingly, leaving
other optional bridge members unchanged.

In `@apps/desktop/src/renderer/components/review/ReviewFindingCard.tsx`:
- Around line 245-256: The modal currently allows closing while a submission is
in flight; update the close handlers so they no-op when submitting is true: in
the useEffect that adds the onKey handler (function onKey) check the submitting
flag and ignore Escape when submitting, and likewise guard the backdrop click
handler and the close button's onClick (the close button that uses
closeButtonRef) so they return early if submitting; also reflect disabled state
on the close button (e.g., aria-disabled / disabled) while submitting. Apply the
same guarding change to the second effect/handler block referenced around lines
295-316 so all close paths (Escape, backdrop click, and X button) are blocked
during handleSubmit's submitting state.

In `@apps/desktop/src/renderer/components/review/ReviewLearningsPanel.tsx`:
- Around line 99-102: The remove handler currently assumes
deleteReviewSuppression(suppressionId) always succeeds; change handleRemove to
check the return value and handle a false result as a failure: after awaiting
deleteReviewSuppression(suppressionId) verify the result is truthy, and if it is
false then surface an error (e.g., set error state or call the existing UI
notification/toast) and return early so refresh() is not called; only call
refresh() when deleteReviewSuppression returns a successful truthy value. Ensure
you update handleRemove's async flow and any UI feedback path so users see a
failure when the bridge returns false.

In `@apps/desktop/src/renderer/components/review/ReviewPage.tsx`:
- Around line 821-825: The validation currently allows launching a lane diff
when compareLaneId still equals the source lane (after the user switched lanes),
causing buildTargetConfig() to launch a lane-vs-itself review; update the
launchValidationMessage logic to also check that
launchDraft.compareLaneId.trim() !== launchDraft.laneId.trim() (and return an
error like "Choose another lane to compare against" or "Cannot compare a lane to
itself") when launchDraft.targetMode === "lane_diff" and launchDraft.compareKind
=== "lane", or alternatively clear launchDraft.compareLaneId when the source
lane (launchDraft.laneId) changes so buildTargetConfig() never receives a
same-vs-same pair.
- Around line 968-977: The path-root matching in resolveFindingTarget is too
permissive and can match sibling worktrees (e.g., "/repo" matching
"/repo-bugfix"); normalize and perform a boundary-aware check: derive rootPath
from lane.worktreePath, ensure both path and rootPath are normalized (trim
trailing slashes), then consider path under rootPath only if path === rootPath
or path.startsWith(rootPath + "/"); use that condition when computing target so
you only strip the true worktree prefix and avoid rewriting sibling paths.

In `@apps/ios/ADE/Resources/DatabaseBootstrap.sql`:
- Around line 2448-2452: The script contains duplicate ALTER TABLE statements
that re-add columns already declared in the CREATE TABLE for review_findings;
remove the three ALTER TABLE lines that add finding_class,
originating_passes_json, and adjudication_json (the statements containing "alter
table review_findings add column finding_class text;", "alter table
review_findings add column originating_passes_json text;", and "alter table
review_findings add column adjudication_json text;") so the bootstrap won't fail
with "duplicate column name" on fresh installs; alternatively, if this file must
be idempotent, replace those statements with a runtime guard that checks PRAGMA
table_info('review_findings') for each column before adding.

---

Nitpick comments:
In `@apps/desktop/src/main/services/lanes/laneService.ts`:
- Around line 2527-2547: The series of db.run(...) deletion calls in the lane
deletion path should be executed inside a single database transaction so the
cleanup is atomic; wrap the block that runs db.run(...) (the deletes that
reference laneId/projectId and the logSlowDeleteStep("database_cleanup",
dbCleanupStartedAt) call) with BEGIN and COMMIT and ensure you issue ROLLBACK on
any error, using the same sqlite client methods (db.run/exec) you already use,
and propagate or handle errors so partial deletes cannot persist (ensure
transaction uses laneId/projectId parameters unchanged and that the final
deletion of lanes and the logSlowDeleteStep occur only after a successful
COMMIT).

In `@apps/desktop/src/main/services/processes/processService.ts`:
- Around line 588-596: The try/catch around resolving configuredCwd swallows the
real filesystem error; modify the block in processService.ts (around
allowExternalCwd handling and the path resolution using path.resolve,
fs.statSync, fs.realpathSync) to capture the caught error (e.g., err) and
include its message (or set it as the cause) in the thrown Error for Process
'${definition.id}' so failures like EACCES/ENOTDIR/ELOOP surface; ensure you
still validate isDirectory and preserve the original error information when
throwing the new error referencing configuredCwd and definition.id.

In `@apps/desktop/src/main/services/review/reviewContextBuilder.test.ts`:
- Around line 1-125: Duplicate sql.js test harness (mapExecRows, AdeDb type,
createInMemoryAdeDb, and SQL init) should be extracted into a shared test
utility module and imported by both reviewContextBuilder.test.ts and
reviewSuppressionService.test.ts; create a new file (e.g.,
test/utils/sqlHarness.ts) that exports mapExecRows, AdeDb, createInMemoryAdeDb,
and the SQL initialization helper or SQL instance, update both tests to import
these symbols instead of defining them inline, and ensure exported helpers
initialize SQL (initSqlJs) once and expose createInMemoryAdeDb, mapExecRows, and
the AdeDb types so both suites use the exact same schema and adapter shim.

In `@apps/desktop/src/preload/preload.test.ts`:
- Around line 80-126: The test only covers part of window.ade.review; add a
table-driven test that iterates the full bridge surface—methods cancelRun,
recordFeedback, listSuppressions, deleteSuppression, qualityReport (in addition
to the already-tested listLaunchContext, listRuns, getRunDetail, startRun,
rerun)—and for each entry call bridge.review.<method>(...sampleArgs) and assert
ipcRenderer.invoke was called with the corresponding IPC constant (e.g.,
IPC.reviewCancelRun, IPC.reviewRecordFeedback, IPC.reviewListSuppressions,
IPC.reviewDeleteSuppression, IPC.reviewQualityReport) and the expected payload;
keep using the same mocked exposeInMainWorld/ipcRenderer and also include checks
that event subscription uses IPC.reviewEvent and removal still calls
removeListener with the listener returned earlier. Ensure you reference
bridge.review and the IPC constants (IPC.reviewCancelRun,
IPC.reviewRecordFeedback, IPC.reviewListSuppressions,
IPC.reviewDeleteSuppression, IPC.reviewQualityReport) so the test fails if any
channel/method drifts.

In `@apps/desktop/src/preload/preload.ts`:
- Around line 921-932: The inline type-imports used in the IPC wrapper methods
recordFeedback, listSuppressions, deleteSuppression, and qualityReport should be
moved to the file's top-level type import block to match the existing pattern
(e.g., where ReviewEventPayload and ReviewLaunchContext are imported); update
the top import type list to include ReviewRecordFeedbackArgs,
ReviewFeedbackRecord, ReviewListSuppressionsArgs, ReviewSuppression, and
ReviewQualityReport, then replace the inline import(...) type annotations in
those function signatures with the corresponding named types.

In `@apps/desktop/src/renderer/components/review/reviewRouteState.test.ts`:
- Around line 1-14: Add a test that exercises the "no runId" branch by asserting
readReviewRunId returns an empty string (or the established sentinel this
codebase uses) for both an empty search and a search with unrelated params, and
assert buildReviewSearch of an empty/undefined run id produces an empty string;
specifically add assertions calling readReviewRunId("") and
readReviewRunId("?other=1") expecting "" (matching the contract used by
buildReviewSearch) and confirm buildReviewSearch("") (or
buildReviewSearch(undefined) if applicable) === "" to ensure the round-trip
behavior between buildReviewSearch and readReviewRunId is consistent.

In `@apps/desktop/src/shared/types/prs.ts`:
- Around line 1072-1077: The inline comment type's position field is ambiguous:
document that comments?: Array<{ path, body, position }> uses GitHub's
diff-position offset (unified-diff line index) rather than a source-file line
number; add a short doc comment above the comments property describing the
semantics and the invariant (position is a positive finite diff-position as
enforced by prService.submitReviewRequest) and mention callers like
reviewDiffContext to avoid conflation when building inline review payloads.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0ecdc47f-8eef-4581-bbf7-c53e8c23bc04

📥 Commits

Reviewing files that changed from the base of the PR and between 56ec29c and d52d225.

📒 Files selected for processing (48)
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/lanes/laneLaunchContext.test.ts
  • apps/desktop/src/main/services/lanes/laneLaunchContext.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/processes/processService.test.ts
  • apps/desktop/src/main/services/processes/processService.ts
  • apps/desktop/src/main/services/prs/prService.reviewPublication.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/main/services/pty/ptyService.test.ts
  • apps/desktop/src/main/services/pty/ptyService.ts
  • apps/desktop/src/main/services/review/reviewContextBuilder.test.ts
  • apps/desktop/src/main/services/review/reviewContextBuilder.ts
  • apps/desktop/src/main/services/review/reviewDiffContext.test.ts
  • apps/desktop/src/main/services/review/reviewDiffContext.ts
  • apps/desktop/src/main/services/review/reviewRuleRegistry.ts
  • apps/desktop/src/main/services/review/reviewService.test.ts
  • apps/desktop/src/main/services/review/reviewService.ts
  • apps/desktop/src/main/services/review/reviewSuppressionService.test.ts
  • apps/desktop/src/main/services/review/reviewSuppressionService.ts
  • apps/desktop/src/main/services/review/reviewTargetMaterializer.test.ts
  • apps/desktop/src/main/services/review/reviewTargetMaterializer.ts
  • apps/desktop/src/main/services/review/reviewToolEvidence.test.ts
  • apps/desktop/src/main/services/review/reviewToolEvidence.ts
  • apps/desktop/src/main/services/state/kvDb.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.test.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/app/App.tsx
  • apps/desktop/src/renderer/components/app/TabNav.test.tsx
  • apps/desktop/src/renderer/components/app/TabNav.tsx
  • apps/desktop/src/renderer/components/review/ReviewFindingCard.tsx
  • apps/desktop/src/renderer/components/review/ReviewLearningsPanel.tsx
  • apps/desktop/src/renderer/components/review/ReviewPage.test.tsx
  • apps/desktop/src/renderer/components/review/ReviewPage.tsx
  • apps/desktop/src/renderer/components/review/reviewApi.ts
  • apps/desktop/src/renderer/components/review/reviewRouteState.test.ts
  • apps/desktop/src/renderer/components/review/reviewRouteState.ts
  • apps/desktop/src/renderer/components/review/reviewTypes.ts
  • apps/desktop/src/renderer/components/run/AddCommandDialog.tsx
  • apps/desktop/src/renderer/components/shared/ReviewLaunchModelControls.tsx
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/shared/types/prs.ts
  • apps/desktop/src/shared/types/review.ts
  • apps/desktop/src/shared/types/sessions.ts
  • apps/ios/ADE/Resources/DatabaseBootstrap.sql

Comment on lines +3707 to +3711
try {
ctx.reviewService?.dispose?.();
} catch {
// ignore
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Dispose reviewService in the fast-shutdown path too.

Lines 3707-3709 cover normal context teardown, but Line 4233's runImmediateProcessCleanup() still skips the new ctx.reviewService. On uncaughtException, SIGTERM, or forced exit, an in-flight review can keep running/publishing while the rest of the project context is already being torn down.

Suggested follow-up
     for (const ctx of contexts) {
       try {
+        ctx.reviewService?.dispose?.();
+      } catch {
+        // ignore
+      }
+      try {
         ctx.aiOrchestratorService?.dispose?.();
       } catch {
         // ignore
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/main.ts` around lines 3707 - 3711, The fast-shutdown
path (runImmediateProcessCleanup) currently skips disposing ctx.reviewService
causing in-flight reviews to continue; update the fast-shutdown/cleanup routine
(runImmediateProcessCleanup) to call and await ctx.reviewService?.dispose?.()
(or invoke the same disposal logic used in normal teardown) and wrap it in a
try/catch similar to the normal context teardown block to ensure it runs safely
on uncaughtException/SIGTERM/forced exit so the reviewService is properly
stopped before process exit.

Comment on lines +55 to +63
if (args.allowExternalCwd === true && path.isAbsolute(requestedCwd)) {
return {
laneWorktreePath: laneRoot,
cwd: ensureDirectoryExists(
path.resolve(requestedCwd),
`Requested cwd '${requestedCwd}' is not an existing directory.`,
),
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reintroduce a boundary for external CWDs.

This branch now skips resolvePathWithinRoot() for any absolute path and only checks that the directory exists. apps/desktop/src/main/services/pty/ptyService.ts:1001-1030 then uses the returned cwd directly, so any caller that can set allowExternalCwd can launch work anywhere on the host filesystem, not just inside the selected lane or project. If this escape hatch is required, constrain it to an explicit trusted root set instead of treating every absolute path as valid.

As per coding guidelines apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/lanes/laneLaunchContext.ts` around lines 55 -
63, The current early-return for allowExternalCwd in laneLaunchContext.ts allows
any absolute requestedCwd to bypass resolvePathWithinRoot and be used directly
(propagated to ptyService.ts), creating a security escape; change the branch so
that when args.allowExternalCwd is true you validate requestedCwd against an
explicit trusted roots whitelist (e.g., configured trustedRootPaths) and only
accept it if path.resolve(requestedCwd) is inside one of those trusted roots,
otherwise fall back to resolvePathWithinRoot(requestedCwd) or reject; keep the
use of ensureDirectoryExists for existence checks and preserve laneWorktreePath
behavior, updating references to allowExternalCwd, resolvePathWithinRoot, and
ensureDirectoryExists in laneLaunchContext.ts accordingly so callers like
ptyService.ts receive only vetted paths.

Comment thread apps/desktop/src/main/services/lanes/laneService.ts
Comment thread apps/desktop/src/main/services/processes/processService.ts
Comment on lines +278 to +290
function buildPublishedReviewCommentBody(finding: ReviewFinding): string {
const sections = [
`[${finding.severity.toUpperCase()}] ${finding.title}`,
"",
finding.body,
"",
`Confidence: ${Math.round(finding.confidence * 100)}%`,
];
const quote = finding.evidence.find((entry) => typeof entry.quote === "string" && entry.quote.trim().length > 0)?.quote?.trim() ?? null;
if (quote) {
sections.push("", "```", quote.slice(0, 1_200), "```");
}
return sections.join("\n");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Redact review content before publishing it to GitHub.

Lines 282-289 and Lines 323-325 post raw finding.body / evidence.quote into PR comments. With the new tool-backed evidence flow, that can leak secrets, tokens, or PII from CI/test/session output into the GitHub review. Please run publishable text through the same redaction/sanitization path used for logs/artifacts, or suppress publication for findings backed by non-diff evidence.

Also applies to: 323-325

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/prs/prService.ts` around lines 278 - 290,
buildPublishedReviewCommentBody currently inserts raw finding.body and
evidence.quote into PR comments; update this function (and the code path that
posts findings to GitHub) to run both finding.body and the extracted quote
through the project’s existing redaction/sanitization utility before including
them, and if no safe redacted output can be produced or the evidence type is
non-diff, suppress publishing that evidence (only include
severity/title/confidence). Preserve the code fence and length truncation logic
for quote but apply truncation after redaction, and ensure you reference
ReviewFinding when locating these fields to change.

Comment on lines +245 to +256
React.useEffect(() => {
if (!open) return;
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.stopPropagation();
onClose();
}
};
window.addEventListener("keydown", onKey);
// Move focus into the dialog so keyboard users are oriented.
queueMicrotask(() => closeButtonRef.current?.focus());
return () => window.removeEventListener("keydown", onKey);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t let the dialog close while feedback is being saved.

Once handleSubmit() flips submitting to true, Escape, backdrop clicks, and the X button still close the modal. That makes it easy to reopen the dialog and create duplicate feedback/suppression records while the first request is still in flight.

🛠️ Proposed fix
 function DismissModal({ open, initialKind, finding, onClose, onSubmit }: DismissModalProps) {
@@
   const [submitting, setSubmitting] = React.useState(false);
   const closeButtonRef = React.useRef<HTMLButtonElement | null>(null);
+  const requestClose = React.useCallback(() => {
+    if (!submitting) onClose();
+  }, [submitting, onClose]);
@@
   React.useEffect(() => {
     if (!open) return;
     const onKey = (event: KeyboardEvent) => {
-      if (event.key === "Escape") {
+      if (event.key === "Escape" && !submitting) {
         event.stopPropagation();
         onClose();
       }
     };
@@
-  }, [open, onClose]);
+  }, [open, onClose, submitting]);
@@
-      onClick={onClose}
+      onClick={requestClose}
@@
-            onClick={onClose}
+            onClick={requestClose}

Also applies to: 295-316

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/review/ReviewFindingCard.tsx` around
lines 245 - 256, The modal currently allows closing while a submission is in
flight; update the close handlers so they no-op when submitting is true: in the
useEffect that adds the onKey handler (function onKey) check the submitting flag
and ignore Escape when submitting, and likewise guard the backdrop click handler
and the close button's onClick (the close button that uses closeButtonRef) so
they return early if submitting; also reflect disabled state on the close button
(e.g., aria-disabled / disabled) while submitting. Apply the same guarding
change to the second effect/handler block referenced around lines 295-316 so all
close paths (Escape, backdrop click, and X button) are blocked during
handleSubmit's submitting state.

Comment on lines +99 to +102
const handleRemove = React.useCallback(async (suppressionId: string) => {
try {
await deleteReviewSuppression(suppressionId);
await refresh();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle deleteReviewSuppression() returning false.

The bridge wrapper uses false as a failure/no-bridge result. This path currently refreshes silently, so the remove button can appear to work while nothing was actually deleted.

Suggested fix
   const handleRemove = React.useCallback(async (suppressionId: string) => {
     try {
-      await deleteReviewSuppression(suppressionId);
+      const removed = await deleteReviewSuppression(suppressionId);
+      if (!removed) {
+        setError("Failed to remove suppression");
+        return;
+      }
       await refresh();
     } catch (err) {
       setError(err instanceof Error ? err.message : "Failed to remove suppression");
     }
   }, [refresh]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleRemove = React.useCallback(async (suppressionId: string) => {
try {
await deleteReviewSuppression(suppressionId);
await refresh();
const handleRemove = React.useCallback(async (suppressionId: string) => {
try {
const removed = await deleteReviewSuppression(suppressionId);
if (!removed) {
setError("Failed to remove suppression");
return;
}
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to remove suppression");
}
}, [refresh]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/review/ReviewLearningsPanel.tsx` around
lines 99 - 102, The remove handler currently assumes
deleteReviewSuppression(suppressionId) always succeeds; change handleRemove to
check the return value and handle a false result as a failure: after awaiting
deleteReviewSuppression(suppressionId) verify the result is truthy, and if it is
false then surface an error (e.g., set error state or call the existing UI
notification/toast) and return early so refresh() is not called; only call
refresh() when deleteReviewSuppression returns a successful truthy value. Ensure
you update handleRemove's async flow and any UI feedback path so users see a
failure when the bridge returns false.

Comment on lines +821 to +825
const launchValidationMessage = React.useMemo(() => {
if (!launchDraft.laneId.trim()) return "Choose a lane before launching a review.";
if (launchDraft.targetMode === "lane_diff" && launchDraft.compareKind === "lane" && !launchDraft.compareLaneId.trim()) {
return "Choose another lane to compare against.";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent launching a lane diff against the same lane.

If the user picks lane A, selects lane B as the comparison, and then switches the source lane to B, compareLaneId stays populated and this validation still passes. buildTargetConfig() will then launch a lane-vs-itself review.

Suggested fix
   const launchValidationMessage = React.useMemo(() => {
     if (!launchDraft.laneId.trim()) return "Choose a lane before launching a review.";
-    if (launchDraft.targetMode === "lane_diff" && launchDraft.compareKind === "lane" && !launchDraft.compareLaneId.trim()) {
-      return "Choose another lane to compare against.";
+    if (launchDraft.targetMode === "lane_diff" && launchDraft.compareKind === "lane") {
+      if (!launchDraft.compareLaneId.trim()) {
+        return "Choose another lane to compare against.";
+      }
+      if (launchDraft.compareLaneId === launchDraft.laneId) {
+        return "Choose a different lane to compare against.";
+      }
     }
     return commitRangeValidationMessage;
   }, [commitRangeValidationMessage, launchDraft]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const launchValidationMessage = React.useMemo(() => {
if (!launchDraft.laneId.trim()) return "Choose a lane before launching a review.";
if (launchDraft.targetMode === "lane_diff" && launchDraft.compareKind === "lane" && !launchDraft.compareLaneId.trim()) {
return "Choose another lane to compare against.";
}
const launchValidationMessage = React.useMemo(() => {
if (!launchDraft.laneId.trim()) return "Choose a lane before launching a review.";
if (launchDraft.targetMode === "lane_diff" && launchDraft.compareKind === "lane") {
if (!launchDraft.compareLaneId.trim()) {
return "Choose another lane to compare against.";
}
if (launchDraft.compareLaneId === launchDraft.laneId) {
return "Choose a different lane to compare against.";
}
}
return commitRangeValidationMessage;
}, [commitRangeValidationMessage, launchDraft]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/review/ReviewPage.tsx` around lines 821
- 825, The validation currently allows launching a lane diff when compareLaneId
still equals the source lane (after the user switched lanes), causing
buildTargetConfig() to launch a lane-vs-itself review; update the
launchValidationMessage logic to also check that
launchDraft.compareLaneId.trim() !== launchDraft.laneId.trim() (and return an
error like "Choose another lane to compare against" or "Cannot compare a lane to
itself") when launchDraft.targetMode === "lane_diff" and launchDraft.compareKind
=== "lane", or alternatively clear launchDraft.compareLaneId when the source
lane (launchDraft.laneId) changes so buildTargetConfig() never receives a
same-vs-same pair.

Comment on lines +968 to +977
const resolveFindingTarget = React.useCallback((finding: ReviewFinding): { laneId: string; target: string; rootPath: string | null } | null => {
const path = finding.filePath?.trim();
const laneId = selectedRun?.laneId ?? launchDraft.laneId;
if (!path || !laneId) return null;
const lane = laneById.get(laneId) ?? null;
const rootPath = lane?.worktreePath ?? null;
const target = rootPath && path.startsWith(rootPath)
? path.slice(rootPath.length).replace(/^\/+/, "")
: path.startsWith("/") ? path.replace(/^\//, "") : path;
return { laneId, target, rootPath };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use a path-boundary check before trimming rootPath.

path.startsWith(rootPath) is too loose here. A lane rooted at /repo will also match /repo-bugfix/..., so findings from sibling worktrees can be rewritten to the wrong relative path and open the wrong file.

Suggested fix
   const resolveFindingTarget = React.useCallback((finding: ReviewFinding): { laneId: string; target: string; rootPath: string | null } | null => {
     const path = finding.filePath?.trim();
     const laneId = selectedRun?.laneId ?? launchDraft.laneId;
     if (!path || !laneId) return null;
     const lane = laneById.get(laneId) ?? null;
     const rootPath = lane?.worktreePath ?? null;
-    const target = rootPath && path.startsWith(rootPath)
-      ? path.slice(rootPath.length).replace(/^\/+/, "")
+    const normalizedRoot = rootPath?.replace(/[\\/]+$/, "") ?? null;
+    const withinRoot = normalizedRoot != null
+      && (path === normalizedRoot || path.startsWith(`${normalizedRoot}/`) || path.startsWith(`${normalizedRoot}\\`));
+    const target = withinRoot
+      ? path.slice(normalizedRoot.length).replace(/^[\\/]+/, "")
       : path.startsWith("/") ? path.replace(/^\//, "") : path;
     return { laneId, target, rootPath };
   }, [laneById, launchDraft.laneId, selectedRun?.laneId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/review/ReviewPage.tsx` around lines 968
- 977, The path-root matching in resolveFindingTarget is too permissive and can
match sibling worktrees (e.g., "/repo" matching "/repo-bugfix"); normalize and
perform a boundary-aware check: derive rootPath from lane.worktreePath, ensure
both path and rootPath are normalized (trim trailing slashes), then consider
path under rootPath only if path === rootPath or path.startsWith(rootPath +
"/"); use that condition when computing target so you only strip the true
worktree prefix and avoid rewriting sibling paths.

Comment on lines +2448 to +2452
alter table review_findings add column finding_class text;

alter table review_findings add column originating_passes_json text;

alter table review_findings add column adjudication_json text;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove the duplicate ALTER TABLE statements for existing columns.

review_findings already defines finding_class, originating_passes_json, and adjudication_json in the CREATE TABLE above. On a fresh bootstrap, these ALTER TABLE statements will fail with duplicate column name and stop the script before the rest of the review schema is created.

Suggested fix
-alter table review_findings add column finding_class text;
-
-alter table review_findings add column originating_passes_json text;
-
-alter table review_findings add column adjudication_json text;
-
 alter table review_findings add column diff_context_json text;
 
 alter table review_findings add column suppression_match_json text;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Resources/DatabaseBootstrap.sql` around lines 2448 - 2452, The
script contains duplicate ALTER TABLE statements that re-add columns already
declared in the CREATE TABLE for review_findings; remove the three ALTER TABLE
lines that add finding_class, originating_passes_json, and adjudication_json
(the statements containing "alter table review_findings add column finding_class
text;", "alter table review_findings add column originating_passes_json text;",
and "alter table review_findings add column adjudication_json text;") so the
bootstrap won't fail with "duplicate column name" on fresh installs;
alternatively, if this file must be idempotent, replace those statements with a
runtime guard that checks PRAGMA table_info('review_findings') for each column
before adding.

- reviewTargetMaterializer: increment diff position on non-first @@ hunks
  so GitHub inline-comment positions are correct across multi-hunk files
  (capy-ai).
- laneService: explicitly delete review_finding_feedback before
  review_findings on lane deletion (CRR strips FKs, no cascade) (capy-ai).
- reviewService.mapPublicationRow: drop tautological COMMENT?COMMENT:COMMENT
  ternary in favor of a direct cast (greptile).
- reviewSuppressionService: cap PATH_GLOB_CACHE at 256 entries with FIFO
  eviction (greptile).
- reviewService: build changedFilesByPath from the budget-sliced
  changedFiles, not materialized.changedFiles (greptile).

Addresses PR #191 comments: 3136781788, 3136781797, 3136758435,
3136758509, 3136758600.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 24, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 24, 2026

@copilot review but do not make fixes

Since the last review, commit 69bc7b3 landed four fixes from the Greptile inline comments. Here's an updated review covering what changed and what remains.


What was fixed in 69bc7b3

  1. mapPublicationRow tautological ternary — replaced with a direct cast row.review_event as ReviewPublication["reviewEvent"]. ✅
  2. changedFilesByPath budget inconsistency — now built from the budget-sliced changedFiles instead of materialized.changedFiles. ✅
  3. PATH_GLOB_CACHE unbounded growth — capped at 256 entries with FIFO eviction. ✅
  4. Multi-hunk diff position countingcurrentDiffPosition is incremented for the @@ hunk header itself on subsequent hunks. ✅
  5. review_finding_feedback missing from lane deletion cascade — explicit delete added before review_findings. ✅

Remaining issues

1. titleMatchesSignal — unsanitized regex injection (reviewToolEvidence.ts, line 68, not fixed)

if (new RegExp(`\\b${token}\\b`).test(haystack)) hits += 1;

token is derived from CI check names, test suite names, and session failure text. Characters like (, ), ., + are valid in those names and will cause new RegExp(...) to either throw or match incorrectly. Since token has already been lowercase-split on [^a-z0-9]+, the only characters that survive the split are alphanumeric — so in practice this is safe today. However, if the tokenization regex is ever loosened to allow hyphens or dots (e.g. for version strings), the injection risk becomes real. A preemptive token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") would make this robust permanently.

2. review_suppressions not cleaned up on lane deletion (laneService.ts, not in the fix)

The lane deletion block now explicitly deletes review_finding_feedback and review_findings, but review_suppressions is project-scoped, not lane-scoped. Suppressions created from findings on a deleted lane are retained (which may be intentional — suppressions persist as learned rules). However, the source_finding_id column in review_suppressions will reference a deleted row with no FK constraint. If the UI ever tries to join back to the source finding, it will silently get no result. Worth a comment explaining that suppressions are intentionally project-persistent across lane deletion.

3. mapPublicationRow cast is now unsafe (reviewService.ts, line 1265)

The fix replaced:

reviewEvent: row.review_event === "COMMENT" ? "COMMENT" : "COMMENT",

with:

reviewEvent: row.review_event as ReviewPublication["reviewEvent"],

ReviewPublication["reviewEvent"] is the literal type "COMMENT". The cast tells TypeScript to trust the DB string without validation. If a row has any other value (corrupted data, future migration artifact), the cast silently passes through a value that violates the type. A narrowing guard (row.review_event === "COMMENT" ? "COMMENT" : "COMMENT") was wrong, but row.review_event as "COMMENT" is equally unvalidated — it just removes the TypeScript noise while leaving the runtime behavior identical. A minimal safe form:

reviewEvent: (row.review_event === "COMMENT" ? "COMMENT" : "COMMENT") satisfies ReviewPublication["reviewEvent"],

or if the type is intended to widen eventually, extend ReviewPublication["reviewEvent"] to "COMMENT" | "REQUEST_CHANGES" | "APPROVE" and the cast becomes genuinely correct.

4. hunkMatch header not pushed to entry.lines (reviewTargetMaterializer.ts, line 128)

The hunk header line (@@ ... @@) is pushed to entry.lines on line 122 (before the hunk match), but then continue is called at line 128 — so the hunk header does appear in the excerpt. This is correct. However, the @@ line is also counted in currentDiffPosition += 1 on the non-hunk path (line 134). Wait — it's not: hunk headers hit continue before reaching line 134. The new if (currentDiffPosition > 0) currentDiffPosition += 1 on line 127 handles the inter-hunk separator position correctly.

One subtle issue: the first @@ in a file resets currentDiffPosition = 0 (via line 117 at diff --git match) and then hits if (currentDiffPosition > 0) — evaluating to false, so no increment. This is correct. For subsequent @@ hunks, currentDiffPosition > 0 is true and increments once. This matches GitHub's diff position counting. ✅ (No issue here — just confirming the fix is correct.)

5. suppressedCount artifact still uses artifactType: "tool_evidence" (reviewService.ts, line 2066, not fixed)

The suppression filter summary artifact is still stored as "tool_evidence" rather than a semantically distinct type. Minor, but it conflates suppression bookkeeping with tool-signal evidence in the artifact list.


Summary

The five bugs from the Greptile inline comments were all addressed. The items still open from my earlier review are: the regex injection risk in titleMatchesSignal (currently safe due to tokenization but fragile), the wrong artifactType for the suppression summary artifact, and the new type-cast approach in mapPublicationRow which is functionally equivalent to the previous tautology but loses the type-narrowing opportunity.

Comment thread apps/desktop/src/main/services/review/reviewService.ts
- reviewService.byClass: wrap the latest-feedback subquery in a latest_only
  CTE (rn = 1 filter), so count(*) per class stops inflating when a finding
  has multiple feedback rows (greptile P1 #3136916987).
- reviewToolEvidence.titleMatchesSignal: escape regex metachars in the
  interpolated token before `\b...\b` matching. Defensive; current
  tokenization strips these already (Copilot re-review item 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 24, 2026

@copilot review but do not make fixes

Comment thread apps/desktop/src/main/services/processes/processService.ts
Greptile P1 #3137014594 flags the absolute-cwd path as a sandbox bypass.
Configured cwd originates from projectConfigService (same file that
declares the command itself); there is no IPC path that lets UI input
reach this code. Inline comment documents the reasoning so future
reviewers don't relitigate the same question.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 24, 2026

@copilot review but do not make fixes

Comment thread apps/desktop/src/main/services/review/reviewTargetMaterializer.ts
Greptile P1 #3137082789 claims the first @@ header should count toward
position. GitHub's REST docs are explicit: "the line just below the
'@@' line is position 1", i.e. the first content line after the first
hunk header is position 1, not the header itself. Inline comment
documents this so future re-reviews don't re-flag the conditional gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 24, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (4)
apps/desktop/src/main/services/review/reviewService.ts (2)

2076-2089: ⚠️ Potential issue | 🔴 Critical

Cancellation still allows external PR review to publish.

The gate at Line 2046 passes before await publishRun(...). publishRun has no cancel awareness, so a cancellation issued during that await posts the GitHub review anyway, while Lines 2093–2106 only flip local status to cancelled. Either re-check cancelledRuns.has(runId) immediately before invoking prService.publishReviewPublication inside publishRun, or thread an AbortSignal into it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/review/reviewService.ts` around lines 2076 -
2089, The publishRun call can still post an external PR review after a
cancellation because publishRun/prService.publishReviewPublication has no
cancellation check; add cancellation awareness by re-checking
cancelledRuns.has(runId) immediately before calling
prService.publishReviewPublication inside publishRun (or accept and propagate an
AbortSignal into publishRun and into prService.publishReviewPublication) and
short-circuit publishing when cancelled so no external review is posted after
cancellation.

2322-2330: ⚠️ Potential issue | 🟠 Major

Path-scoped suppression silently widens to repo scope.

When the caller requests scope: "path" but the finding has no file_path (and no explicit pathPattern), this stores a repo-wide suppression instead of rejecting the request. A one-file mute silently becomes a repo-wide one. Prefer to throw or return an error so the caller can recover.

🛠️ Proposed fix
       const requestedScope: ReviewSuppressionScope = args.suppression?.scope ?? "repo";
       const requestedPathPattern = args.suppression?.pathPattern
         ?? (requestedScope === "path" ? findingRow.file_path : null);
       const hasUsablePathPattern = typeof requestedPathPattern === "string" && requestedPathPattern.trim().length > 0;
-      const scope: ReviewSuppressionScope = requestedScope === "path" && !hasUsablePathPattern
-        ? "repo"
-        : requestedScope;
+      if (requestedScope === "path" && !hasUsablePathPattern) {
+        throw new Error("Path-scoped suppressions require a finding with a file path.");
+      }
+      const scope: ReviewSuppressionScope = requestedScope;
       const pathPattern = scope === "path" ? requestedPathPattern : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/review/reviewService.ts` around lines 2322 -
2330, The current suppression branch (when args.kind === "suppress") widens a
requested path-scoped suppression to repo scope when no usable path pattern
exists; update the validation so that if requestedScope === "path" and there is
no usable path pattern (requestedPathPattern is null/empty and
findingRow.file_path is falsy) the method returns/throws an error instead of
switching scope to "repo". Locate the logic around requestedScope,
requestedPathPattern, hasUsablePathPattern and scope in the args.kind ===
"suppress" block and replace the fallback that sets scope to "repo" with an
explicit error/validation failure (include a clear message indicating path scope
requires a pathPattern or findingRow.file_path) so callers can handle the
failure rather than silently widening the suppression.
apps/desktop/src/main/services/review/reviewToolEvidence.ts (1)

109-110: ⚠️ Potential issue | 🟠 Major

Queued/in-progress checks still pass the guard.

The check.conclusion && short-circuit lets null/undefined conclusions (queued or in-progress) fall through and be attached as tool evidence. The check should also require check.status === "completed" before evaluating conclusion.

🛠️ Proposed fix
   for (const check of args.validation.checks) {
-    if (check.conclusion && check.conclusion !== "failure" && check.conclusion !== "action_required") continue;
+    if (check.status !== "completed") continue;
+    if (check.conclusion !== "failure" && check.conclusion !== "action_required") continue;
     const kind = classifyCheck(check.name);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/review/reviewToolEvidence.ts` around lines 109
- 110, The loop over args.validation.checks is allowing queued/in-progress
checks because it only tests check.conclusion; update the guard inside the for
(const check of args.validation.checks) to also require check.status ===
"completed" before evaluating conclusion (i.e., only proceed when check.status
=== "completed" and then ensure check.conclusion is not "failure" or
"action_required"); reference the loop variable check and its properties
conclusion and status in reviewToolEvidence.ts to locate and change the
condition so non-completed checks are skipped.
apps/desktop/src/main/services/review/reviewSuppressionService.ts (1)

292-294: ⚠️ Potential issue | 🟠 Major

Class-scoped suppression still matches unclassified findings.

The guard requires both sides to have a findingClass before it will skip on mismatch. When the new finding's findingClass is null, a suppression created with a specific class (e.g., late_stage_regression) silently matches, effectively broadening a class-specific suppression. Drop the && args.finding.findingClass so any suppression with a class only matches findings of that same class.

🛠️ Proposed fix
-      if (suppression.findingClass && args.finding.findingClass && suppression.findingClass !== args.finding.findingClass) {
+      if (suppression.findingClass && suppression.findingClass !== args.finding.findingClass) {
         continue;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/review/reviewSuppressionService.ts` around
lines 292 - 294, The guard in reviewSuppressionService.ts should not require
args.finding.findingClass to be truthy; update the conditional that currently
checks "suppression.findingClass && args.finding.findingClass &&
suppression.findingClass !== args.finding.findingClass" to only check
suppression.findingClass and mismatch against args.finding.findingClass (i.e.,
change to "suppression.findingClass && suppression.findingClass !==
args.finding.findingClass") so suppressions with a specific findingClass only
match findings with the same class; modify the conditional where
suppression.findingClass and args.finding.findingClass are referenced to
implement this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/desktop/src/main/services/review/reviewService.ts`:
- Around line 2076-2089: The publishRun call can still post an external PR
review after a cancellation because
publishRun/prService.publishReviewPublication has no cancellation check; add
cancellation awareness by re-checking cancelledRuns.has(runId) immediately
before calling prService.publishReviewPublication inside publishRun (or accept
and propagate an AbortSignal into publishRun and into
prService.publishReviewPublication) and short-circuit publishing when cancelled
so no external review is posted after cancellation.
- Around line 2322-2330: The current suppression branch (when args.kind ===
"suppress") widens a requested path-scoped suppression to repo scope when no
usable path pattern exists; update the validation so that if requestedScope ===
"path" and there is no usable path pattern (requestedPathPattern is null/empty
and findingRow.file_path is falsy) the method returns/throws an error instead of
switching scope to "repo". Locate the logic around requestedScope,
requestedPathPattern, hasUsablePathPattern and scope in the args.kind ===
"suppress" block and replace the fallback that sets scope to "repo" with an
explicit error/validation failure (include a clear message indicating path scope
requires a pathPattern or findingRow.file_path) so callers can handle the
failure rather than silently widening the suppression.

In `@apps/desktop/src/main/services/review/reviewSuppressionService.ts`:
- Around line 292-294: The guard in reviewSuppressionService.ts should not
require args.finding.findingClass to be truthy; update the conditional that
currently checks "suppression.findingClass && args.finding.findingClass &&
suppression.findingClass !== args.finding.findingClass" to only check
suppression.findingClass and mismatch against args.finding.findingClass (i.e.,
change to "suppression.findingClass && suppression.findingClass !==
args.finding.findingClass") so suppressions with a specific findingClass only
match findings with the same class; modify the conditional where
suppression.findingClass and args.finding.findingClass are referenced to
implement this change.

In `@apps/desktop/src/main/services/review/reviewToolEvidence.ts`:
- Around line 109-110: The loop over args.validation.checks is allowing
queued/in-progress checks because it only tests check.conclusion; update the
guard inside the for (const check of args.validation.checks) to also require
check.status === "completed" before evaluating conclusion (i.e., only proceed
when check.status === "completed" and then ensure check.conclusion is not
"failure" or "action_required"); reference the loop variable check and its
properties conclusion and status in reviewToolEvidence.ts to locate and change
the condition so non-completed checks are skipped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6a91555c-ca1e-4b85-8c95-26f90d0b6fad

📥 Commits

Reviewing files that changed from the base of the PR and between d52d225 and 457b4ef.

📒 Files selected for processing (6)
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/processes/processService.ts
  • apps/desktop/src/main/services/review/reviewService.ts
  • apps/desktop/src/main/services/review/reviewSuppressionService.ts
  • apps/desktop/src/main/services/review/reviewTargetMaterializer.ts
  • apps/desktop/src/main/services/review/reviewToolEvidence.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/main/services/processes/processService.ts

test-desktop (5) failed on CI: TerminalView's 'fits to container' test
expected renderer=webgl but got 'dom'. The test used fake timers while
the production path awaits a dynamic `import('@xterm/addon-webgl')` —
that microtask chain doesn't reliably settle under fake timers on CI
shards. Switch this one callsite to real timers + waitFor, matching
the sibling test at line 409 that already uses this pattern.

Test-only change; no production code touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28 arul28 merged commit 70fd4e5 into main Apr 24, 2026
24 checks passed
@arul28 arul28 deleted the ade/code-review-dfc1fae7 branch April 25, 2026 02:19
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.

2 participants