Skip to content

Lane Branch Selector#208

Merged
arul28 merged 3 commits into
mainfrom
ade/lane-branch-selector-2ce11f13
Apr 26, 2026
Merged

Lane Branch Selector#208
arul28 merged 3 commits into
mainfrom
ade/lane-branch-selector-2ce11f13

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Apr 26, 2026

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

Summary by CodeRabbit

New Features

  • Added lane-scoped branch switching with preview functionality before execution.
  • Enabled creation of new branches with configurable start points and base references.
  • Introduced active work detection and acknowledgment when switching branches.
  • Enhanced branch lists with ownership and profile metadata across all lanes.

Bug Fixes

  • Fixed stale pull request associations when switching lanes to different branches.

Greptile Summary

This PR introduces lane-scoped branch switching: any worktree lane can now switch to an existing branch or create a new one, with preview/acknowledgement for dirty worktrees, duplicate ownership, and active terminal/process sessions. The DB migration, iOS schema, test coverage, and renderer UX are all included.

  • P1 — sanitizeToolSchema regression in adeRpcServer.ts: the new logic sets out.required = [] for any tool whose schema has no explicit required array, where previously all properties were forced required. Any TOOL_SPECS entry that relied on the old implicit all-required behaviour will now expose every parameter as optional to the LLM, causing silent omissions and assertNonEmptyString failures at call time.
  • P2 — mode not validated in parseGitCheckoutBranchArgs: any non-empty string is cast directly to the "existing" | "create" type; an invalid value degrades silently to "existing" mode rather than returning a clear error.
  • P2 — DatabaseBootstrap.sql deduplication DELETE is unguarded: the cleanup DELETE on lane_branch_profiles runs unconditionally without error handling, unlike the desktop migration's wrapped try/catch.

Confidence Score: 3/5

Not safe to merge without addressing the sanitizeToolSchema regression; it silently broadens LLM tool parameter optionality across the entire tool suite.

One confirmed P1 in adeRpcServer.ts (sanitizeToolSchema change removes required-field enforcement for tools without explicit required arrays) pulls the score to 3. The rest of the feature — lane switching logic, DB migration, preview flow, iOS parity, and tests — is well-implemented.

apps/ade-cli/src/adeRpcServer.ts (sanitizeToolSchema change), apps/desktop/src/main/services/sync/syncRemoteCommandService.ts (mode validation)

Important Files Changed

Filename Overview
apps/ade-cli/src/adeRpcServer.ts Extends git_checkout_branch tool spec with optional mode/startPoint/baseRef/acknowledgeActiveWork params; fixes sanitizeToolSchema so partial required arrays are preserved — but this silently removes required-field enforcement for any existing tools that had no explicit required array.
apps/desktop/src/main/services/lanes/laneService.ts Core implementation of lane-scoped branch switching: adds previewBranchSwitch, switchBranch, listBranchProfiles, listBranchOwners, and the lane_branch_profiles upsert helpers. DB writes inside switchBranch are correctly wrapped in a manual begin/commit/rollback block. findActiveBranchOwner still compares a normalized key against the un-normalized lanes.branch_ref column (previously flagged, still present).
apps/desktop/src/main/services/git/gitOperationsService.ts checkoutBranch now delegates fully to laneService.switchBranch; listBranches enriched with per-branch ownership and profile metadata. Primary-lane-only restriction removed, enabling branch switching on any lane type.
apps/desktop/src/main/services/state/kvDb.ts Adds lane_branch_profiles table with indices; includes a best-effort deduplication DELETE for rows that may have been inserted concurrently. UNIQUE constraint intentionally omitted due to CRR restrictions; enforced at application layer.
apps/desktop/src/main/services/sync/syncRemoteCommandService.ts parseGitCheckoutBranchArgs extended with optional mode/startPoint/baseRef/acknowledgeActiveWork; mode value is not validated against the allowed enum before being cast to the TypeScript type.
apps/desktop/src/renderer/components/lanes/LanesPage.tsx Branch selector expanded from primary-lane-only to all lanes; adds create-branch form, pending active-work acknowledgement flow, and ownership badges. Race condition on fetch is correctly guarded with a cancelled flag.
apps/ios/ADE/Resources/DatabaseBootstrap.sql Adds lane_branch_profiles table and indices matching the desktop migration; deduplication DELETE runs unconditionally without a try/catch or conditional guard, unlike the desktop version.
apps/desktop/src/renderer/components/lanes/laneUtils.ts Adds validateBranchName (git check-ref-format rules), stripRemotePrefix, and extends formatBranchCheckoutError with optional lane name — all well-tested.
apps/ios/ADE/Services/SyncService.swift Adds previewBranchSwitch command and extends checkoutPrimaryBranch to forward mode/startPoint/baseRef/acknowledgeActiveWork; clean update mirroring the TypeScript API.
apps/desktop/src/main/services/prs/prService.ts Adds getRowForLaneBranch helper that looks up PR rows by (lane_id, head_branch) rather than just lane_id, fixing stale PR associations after a branch switch.

Sequence Diagram

sequenceDiagram
    participant UI as Renderer / iOS
    participant IPC as IPC / SyncCommand
    participant GOS as gitOperationsService
    participant LS as laneService
    participant DB as SQLite (kvDb)
    participant Git as git worktree

    UI->>IPC: previewBranchSwitch(laneId, branchName, mode)
    IPC->>LS: previewBranchSwitch(args)
    LS->>Git: show-ref (resolve branch existence)
    LS->>Git: git status --porcelain (dirty check)
    LS->>DB: findActiveBranchOwner (duplicate lane)
    LS->>DB: getActiveWorkForLane (terminals/processes)
    LS-->>UI: LaneBranchSwitchPreview {dirty, duplicateLaneId, activeWork}

    alt activeWork non-empty
        UI->>UI: show acknowledgement prompt
        UI->>IPC: git.checkoutBranch acknowledgeActiveWork=true
    else clean
        UI->>IPC: git.checkoutBranch
    end

    IPC->>GOS: checkoutBranch(args)
    GOS->>LS: switchBranch(args)
    LS->>LS: previewBranchSwitch (re-validate dirty/duplicate)
    LS->>DB: upsertBranchProfileForRow (old branch)
    alt mode=create
        LS->>Git: rev-parse --verify baseRef
        LS->>Git: checkout -b branchName startPoint
    else mode=existing
        LS->>Git: checkout [--track] branchName
    end
    LS->>DB: BEGIN
    LS->>DB: upsertBranchProfileForRow (new branch)
    LS->>DB: UPDATE lanes SET branch_ref, base_ref, parent_lane_id
    LS->>DB: DELETE stale pull_requests + child rows
    LS->>DB: COMMIT
    LS-->>GOS: LaneBranchSwitchResult
    GOS-->>UI: GitActionResult
Loading

Comments Outside Diff (3)

  1. apps/desktop/src/main/services/lanes/laneService.ts, line 1089-1098 (link)

    P1 backfillLaneBranchProfiles runs on every listLanes call, including cache hits

    backfillLaneBranchProfiles() is invoked before the cache check, so every call to listLanes — the 15-second poll, PTY data events, conflict updates — iterates all lane rows and issues one upsert per lane, even when the result would be served from cache. With many lanes this degrades the hot path noticeably.

    Move the backfill inside the cache-miss branch, or rely solely on the one-shot startup call (already present at the initialization block) and skip the per-call invocation entirely.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/lanes/laneService.ts
    Line: 1089-1098
    
    Comment:
    **`backfillLaneBranchProfiles` runs on every `listLanes` call, including cache hits**
    
    `backfillLaneBranchProfiles()` is invoked before the cache check, so every call to `listLanes` — the 15-second poll, PTY data events, conflict updates — iterates all lane rows and issues one upsert per lane, even when the result would be served from cache. With many lanes this degrades the hot path noticeably.
    
    Move the backfill inside the cache-miss branch, or rely solely on the one-shot startup call (already present at the initialization block) and skip the per-call invocation entirely.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  2. apps/desktop/src/main/services/lanes/laneService.ts, line 1196-1215 (link)

    P2 O(N) ensureBranchProfileForRow calls per lane listing on every cache miss

    ensureBranchProfileForRow(row) is called for every lane row while building the LaneSummary list. Each call performs a SELECT (and potentially an UPDATE/INSERT) against lane_branch_profiles. For a project with many lanes, this means N extra round-trips to SQLite on every cache miss, in addition to the backfillLaneBranchProfiles that already ran for all lanes earlier in the same code path.

    Consider pre-loading all profiles for the project in a single SELECT * FROM lane_branch_profiles WHERE project_id = ? query before the loop, then indexing them by lane_id in a Map so that each toLaneSummary call can do an O(1) lookup without hitting the database.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/lanes/laneService.ts
    Line: 1196-1215
    
    Comment:
    **O(N) `ensureBranchProfileForRow` calls per lane listing on every cache miss**
    
    `ensureBranchProfileForRow(row)` is called for every lane row while building the `LaneSummary` list. Each call performs a `SELECT` (and potentially an `UPDATE`/`INSERT`) against `lane_branch_profiles`. For a project with many lanes, this means N extra round-trips to SQLite on every cache miss, in addition to the `backfillLaneBranchProfiles` that already ran for all lanes earlier in the same code path.
    
    Consider pre-loading all profiles for the project in a single `SELECT * FROM lane_branch_profiles WHERE project_id = ?` query before the loop, then indexing them by `lane_id` in a `Map` so that each `toLaneSummary` call can do an O(1) lookup without hitting the database.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  3. apps/ade-cli/src/adeRpcServer.ts, line 2032-2037 (link)

    P1 sanitizeToolSchema silently removes required-field enforcement for tools without an explicit required array

    The old path forced out.required = propKeys whenever the array was absent or didn't include every property key. The new path only fires when required is not yet an array, setting it to []. Any TOOL_SPECS entry whose inputSchema.properties object carries no required field will now be serialised to the LLM with no required parameters, whereas before those tools had every parameter marked required. This means the LLM can silently omit arguments that the runTool handler expects (e.g., assertNonEmptyString(...) calls that throw on undefined), causing runtime errors in tool execution.

    Only tools that already have an explicit, correct required array — like the updated git_checkout_branch — are safe. All other tools with no required declaration need one added before this change ships.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/ade-cli/src/adeRpcServer.ts
    Line: 2032-2037
    
    Comment:
    **`sanitizeToolSchema` silently removes required-field enforcement for tools without an explicit `required` array**
    
    The old path forced `out.required = propKeys` whenever the array was absent **or** didn't include every property key. The new path only fires when `required` is not yet an array, setting it to `[]`. Any `TOOL_SPECS` entry whose `inputSchema.properties` object carries no `required` field will now be serialised to the LLM with **no** required parameters, whereas before those tools had every parameter marked required. This means the LLM can silently omit arguments that the `runTool` handler expects (e.g., `assertNonEmptyString(...)` calls that throw on undefined), causing runtime errors in tool execution.
    
    Only tools that already have an explicit, correct `required` array — like the updated `git_checkout_branch` — are safe. All other tools with no `required` declaration need one added before this change ships.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/ade-cli/src/adeRpcServer.ts
Line: 2032-2037

Comment:
**`sanitizeToolSchema` silently removes required-field enforcement for tools without an explicit `required` array**

The old path forced `out.required = propKeys` whenever the array was absent **or** didn't include every property key. The new path only fires when `required` is not yet an array, setting it to `[]`. Any `TOOL_SPECS` entry whose `inputSchema.properties` object carries no `required` field will now be serialised to the LLM with **no** required parameters, whereas before those tools had every parameter marked required. This means the LLM can silently omit arguments that the `runTool` handler expects (e.g., `assertNonEmptyString(...)` calls that throw on undefined), causing runtime errors in tool execution.

Only tools that already have an explicit, correct `required` array — like the updated `git_checkout_branch` — are safe. All other tools with no `required` declaration need one added before this change ships.

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/sync/syncRemoteCommandService.ts
Line: 2213

Comment:
**`mode` is cast without runtime validation**

`asTrimmedString(value.mode)` only verifies the string is non-empty; the cast to `GitCheckoutBranchArgs["mode"]` is a TypeScript-only assertion. An invalid payload value like `"checkout"` or `"delete"` passes through to `laneService.switchBranch`. The service falls back to the `else` branch (treating it as `"existing"`), so behaviour degrades silently rather than surfacing a clear error.

```typescript
// suggested guard
const rawMode = asTrimmedString(value.mode);
if (rawMode && rawMode !== "existing" && rawMode !== "create") {
  throw new Error(`git.checkoutBranch: mode must be "existing" or "create", got "${rawMode}".`);
}
...(rawMode ? { mode: rawMode as GitCheckoutBranchArgs["mode"] } : {}),
```

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/ios/ADE/Resources/DatabaseBootstrap.sql
Line: 3298-3311

Comment:
**Duplicate-cleanup DELETE runs unconditionally and without error handling in `DatabaseBootstrap.sql`**

On the desktop side (`kvDb.ts`) the equivalent query is wrapped in a `try/catch` labelled "best-effort migration". In `DatabaseBootstrap.sql` the `DELETE` statement runs unconditionally with no surrounding guard. On a **fresh iOS install** the table is empty and the statement is a no-op, but on an existing installation where CRR replication has placed the table in an unusual state a runtime error here would abort the entire bootstrap. Consider wrapping it in a SQLite `BEGIN / EXCEPTION` guard (or making it conditional on the table having rows) to match the defensive posture used on the desktop.

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

Reviews (3): Last reviewed commit: "ship: iter 1 — fix test-desktop (8) + 16..." | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 26, 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 26, 2026 11:27pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

This PR introduces lane-scoped branch switching with multi-step preview and confirmation workflow. Changes span CLI, IPC, git operations, lane services, database persistence, and both desktop and iOS UI layers to support checking out existing branches or creating new branches with configurable start points and base references, including active-work acknowledgment handling.

Changes

Cohort / File(s) Summary
CLI and RPC Tools
apps/ade-cli/src/adeRpcServer.ts, apps/ade-cli/src/cli.ts, apps/ade-cli/src/cli.test.ts
Extended git_checkout_branch RPC with optional mode (existing/create), startPoint, baseRef, and acknowledgeActiveWork parameters. Updated CLI git checkout subcommand to parse --create/-b, --start-point, --base, and --ack-active-work flags and forward as structured arguments.
IPC and Sync Command Services
apps/desktop/src/main/services/ipc/registerIpc.ts, apps/desktop/src/main/services/sync/syncRemoteCommandService.ts, apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts
Registered new IPC endpoints lanesPreviewBranchSwitch and lanesSwitchBranch. Added lanes.previewBranchSwitch sync remote command; enhanced checkout-branch argument parsing to conditionally include optional fields when present.
Desktop Tools
apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts
Updated gitCheckoutBranch tool schema to accept optional startPoint, baseRef, and acknowledgeActiveWork; replaced create boolean with mode string ("existing"/"create") in execution logic.
Lane Service Core
apps/desktop/src/main/services/lanes/laneService.ts, apps/desktop/src/main/services/lanes/laneService.branchSwitch.test.ts
Added new methods: listBranchProfiles, previewBranchSwitch, and switchBranch. Introduced lane_branch_profiles table integration with persistent branch profile tracking, dirty-state detection, duplicate-lane blocking, and active-work gating. Includes PR row cleanup on branch switch.
Git Operations Service
apps/desktop/src/main/services/git/gitOperationsService.ts, apps/desktop/src/main/services/git/gitOperationsService.branchSwitch.test.ts
Refactored listBranches to enrich results with lane ownership and profile metadata. Rewrote checkoutBranch to delegate to laneService.switchBranch with operation tracking; updated argument signature to GitCheckoutBranchArgs.
PR Service
apps/desktop/src/main/services/prs/prService.ts
Added DB lookup for existing PR rows by lane_id and normalized head_branch. Enhanced upsertRow to prioritize lane+branch matching before falling back to repo adoption.
Data Persistence
apps/desktop/src/main/services/state/kvDb.ts, apps/ios/ADE/Resources/DatabaseBootstrap.sql
Created new lane_branch_profiles table with fields for branch identity, base ref, parent lane, source ref, and checkout tracking timestamps. Added indexes for lane and normalized branch lookups.
Shared Type Definitions
apps/desktop/src/shared/types/git.ts, apps/desktop/src/shared/types/lanes.ts, apps/desktop/src/shared/types/sync.ts
Extended GitBranchSummary and GitCheckoutBranchArgs with ownership/profile/PR metadata and optional checkout parameters. Added new types: LaneBranchProfile, LaneBranchSwitchMode, LaneBranchActiveWorkItem, LaneBranchSwitchArgs, LaneBranchSwitchPreview, LaneBranchSwitchResult. Added lanes.previewBranchSwitch to SyncRemoteCommandAction.
Preload and IPC Bindings
apps/desktop/src/preload/global.d.ts, apps/desktop/src/preload/preload.ts, apps/desktop/src/shared/ipc.ts, apps/desktop/src/renderer/browserMock.ts
Added previewBranchSwitch and switchBranch methods to window.ade.lanes API with proper IPC channel routing and browser mock implementations.
Desktop Lane UI Components
apps/desktop/src/renderer/components/lanes/LaneStackPane.tsx, apps/desktop/src/renderer/components/lanes/LanesPage.tsx
Updated LaneStackPane to render branch ref as secondary label with tooltip. Refactored LanesPage to support lane-scoped branch lists, preview-before-switch workflow, branch creation with start/base refs, duplicate/dirty conflict detection, and pending-confirmation handling for active work.
Desktop Lane Utilities
apps/desktop/src/renderer/components/lanes/laneUtils.ts, apps/desktop/src/renderer/components/lanes/laneUtils.test.ts
Extended LaneBranchOption with ownership and profile metadata. Added validateBranchName, formatBranchCheckoutError (now lane-aware), and stripRemotePrefix utilities with comprehensive test coverage.
iOS Service and Models
apps/ios/ADE/Models/RemoteModels.swift, apps/ios/ADE/Services/SyncService.swift
Extended LaneSummary with missionId and laneRole. Added decoding models for branch profiles and switch previews. Introduced previewBranchSwitch API and updated checkoutPrimaryBranch to accept mode, startPoint, baseRef, and acknowledgeActiveWork parameters.
iOS UI Components
apps/ios/ADE/Views/Lanes/LaneActionsCard.swift, apps/ios/ADE/Views/Lanes/LaneBranchPickerSheet.swift, apps/ios/ADE/Views/Lanes/LaneDetailGitSection.swift, apps/ios/ADE/Views/Lanes/LaneListViewParts.swift
Added lane metadata display and mission lane rule enforcement to LaneActionsCard. Completely refactored LaneBranchPickerSheet to support preview workflow, branch creation with start/base ref selection, pending-confirmation for active work, and enhanced branch list UI with ownership/profile/PR indicators. Updated supporting views to pass and render new lane metadata and branch refs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

desktop, ios, cli

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% 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 'Lane Branch Selector' accurately reflects the main feature added: a new branch selector/switcher for lanes, enabling users to switch or create branches within any lane.
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/lane-branch-selector-2ce11f13

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 26, 2026

@copilot review but do not make fixes

Comment on lines +779 to +798
db.run(`
create table if not exists lane_branch_profiles (
id text primary key,
project_id text not null,
lane_id text not null,
branch_ref text not null,
normalized_branch_ref text not null,
base_ref text not null,
parent_lane_id text,
source_branch_ref text,
created_at text not null,
updated_at text not null,
last_checked_out_at text,
foreign key(project_id) references projects(id),
foreign key(lane_id) references lanes(id),
foreign key(parent_lane_id) references lanes(id)
)
`);
db.run("create index if not exists idx_lane_branch_profiles_lane on lane_branch_profiles(project_id, lane_id)");
db.run("create index if not exists idx_lane_branch_profiles_project_branch on lane_branch_profiles(project_id, normalized_branch_ref)");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Missing UNIQUE constraint on (project_id, lane_id, normalized_branch_ref)

The upsert in upsertBranchProfileForRow is a manual check-then-insert: it reads with getBranchProfileRow, and if no row is found, executes an INSERT. Without a unique constraint there is no database-level guard against concurrent callers (or retry logic) inserting duplicate (project_id, lane_id, normalized_branch_ref) tuples. The getBranchProfileRow query uses limit 1, so a duplicate would be silently hidden but would waste storage and could surface later if limit 1 ordering changes.

Add the constraint to the schema and the same addition is needed in DatabaseBootstrap.sql for the iOS side.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/state/kvDb.ts
Line: 779-798

Comment:
**Missing `UNIQUE` constraint on `(project_id, lane_id, normalized_branch_ref)`**

The upsert in `upsertBranchProfileForRow` is a manual check-then-insert: it reads with `getBranchProfileRow`, and if no row is found, executes an `INSERT`. Without a unique constraint there is no database-level guard against concurrent callers (or retry logic) inserting duplicate `(project_id, lane_id, normalized_branch_ref)` tuples. The `getBranchProfileRow` query uses `limit 1`, so a duplicate would be silently hidden but would waste storage and could surface later if `limit 1` ordering changes.

Add the constraint to the schema and the same addition is needed in `DatabaseBootstrap.sql` for the iOS side.

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

Fix in Claude Code

Comment thread apps/desktop/src/main/services/lanes/laneService.ts
Comment thread apps/desktop/src/main/services/lanes/laneService.ts
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

logger.warn("laneService.repairLegacyPrimaryBaseRootLanes_failed", { error: err instanceof Error ? err.message : String(err) });
}
try {
backfillLaneBranchProfiles();
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] [🟡 Investigate]

backfillLaneBranchProfiles() is called at line 1090 inside listLanes() — before the cache check at line 1095. The function iterates all lanes via getAllLaneRows(true) and calls upsertBranchProfileForRow for each, which issues a SELECT (via getBranchProfileRow) and potentially an INSERT/UPDATE per lane. Since listLanes is a hot path (called from the renderer on lane list refreshes, IPC handlers, etc.), this adds O(n) DB round-trips on every invocation regardless of whether the lane-list cache is warm.

// laneService.ts
try {
  backfillLaneBranchProfiles();
} catch (err) { ... }

const cacheKey = ...;
const cached = laneListCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
  return cached.rows.map(cloneLaneSummary);
}

The constructor already calls backfillLaneBranchProfiles() at initialization (line 1325), so the listLanes-level call appears to be a belt-and-suspenders measure for data that may have been created between constructor time and first list. However, the backfill is a migration/repair concern that becomes a no-op only after querying the DB. Consider guarding with a flag so the repeated backfill is skipped once the initial pass completes, or move it below the cache check so warm-cache calls skip it entirely.

);

const getRowForLaneBranch = (laneId: string, headBranch: string): PullRequestRow | null => {
const normalizedHead = normalizeBranchName(headBranch).trim();
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] [🟡 Investigate]

getRowForLaneBranch normalizes headBranch and returns null if the result is empty (line 869-870). The old getRowForLane had no such guard — it would always find a PR row for the lane regardless of branch. Now, if summary.headBranch is ever empty, whitespace-only, or normalizes to empty for any reason, the upsert at line 1280-1282 will fail to find the existing PR row and INSERT a duplicate instead of UPDATEing. While GitHub PR data should always have headBranch, defensive callers or edge cases (detached worktree, deleted remote branch) could trigger this.

const getRowForLaneBranch = (laneId: string, headBranch: string): PullRequestRow | null => {
    const normalizedHead = normalizeBranchName(headBranch).trim();
    if (!normalizedHead) return null;

Verify that all callers of upsertPr always supply a non-empty headBranch, or add a fallback to getRowForLane when headBranch is empty to avoid silent PR duplication.

arul28 and others added 2 commits April 26, 2026 18:52
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add gitOperationsService and laneService branchSwitch test suites
- Extend cli + syncRemoteCommandService tests for new git checkout flags
- Fix laneService.switchBranch stale-PR cleanup (UPDATE→DELETE; lane_id is NOT NULL)
- Document branch switching contract in docs/features/lanes/README.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28 arul28 force-pushed the ade/lane-branch-selector-2ce11f13 branch from 1e0952a to aa2815f Compare April 26, 2026 22:54
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 26, 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.

Actionable comments posted: 16

♻️ Duplicate comments (1)
apps/desktop/src/main/services/prs/prService.ts (1)

868-870: ⚠️ Potential issue | 🟠 Major

Add a lane-level fallback when normalized headBranch is empty.

If normalization trims to empty and returns null, the upsert path can miss an existing lane row and drift into incorrect insert/adoption behavior. This concern was previously raised and still applies.

Suggested fix
 const getRowForLaneBranch = (laneId: string, headBranch: string): PullRequestRow | null => {
   const normalizedHead = normalizeBranchName(headBranch).trim();
-  if (!normalizedHead) return null;
+  if (!normalizedHead) return getRowForLane(laneId);
   return db.get<PullRequestRow>(
🤖 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 868 - 870, When
normalizeBranchName(headBranch).trim() yields an empty string in
getRowForLaneBranch, don’t immediately return null; instead implement a
lane-level fallback that searches for an existing PullRequestRow for the given
laneId (e.g., lookup existing rows keyed by laneId or scan the prs rows
collection) and return that row if found to avoid creating a duplicate/new
adoption path; only return null if no lane-level row exists. This change keeps
the upsert/adoption logic consistent when normalizedHead is empty.
🧹 Nitpick comments (12)
apps/desktop/src/main/services/state/kvDb.ts (1)

792-794: Consider ON DELETE CASCADE for the lane_id FK.

Profiles are scoped per-lane and become orphaned when a lane is deleted. The existing FK_CONSTRAINTS map in this file (lines 231‑261) is the right place to register "lane_branch_profiles:lane_id": { references: "lanes(id)", action: "on delete cascade" } so the retrofit migration applies it consistently to existing DBs as well. Without this, deleted lanes leave stale profile rows that can interfere with the duplicate-branch ownership checks added by the PR.

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

In `@apps/desktop/src/main/services/state/kvDb.ts` around lines 792 - 794, The
lane_id foreign key on lane_branch_profiles should cascade deletes so per-lane
profiles don't become orphaned; add an entry to the existing FK_CONSTRAINTS map
for the key "lane_branch_profiles:lane_id" with references "lanes(id)" and
action "on delete cascade", and update the lane_branch_profiles table FK
definition to include ON DELETE CASCADE so the retrofit migration will apply the
constraint to existing DBs; locate the FK_CONSTRAINTS map and the
lane_branch_profiles table definition to make these changes.
apps/ios/ADE/Views/Lanes/LaneListViewParts.swift (1)

136-146: Extract the duplicated lane title/branch header into a helper view.

The lane-name + branch-ref header is now duplicated verbatim across the rebase-suggestion card (lines 136‑146) and the auto-rebase attention card (lines 206‑216). Extracting a small @ViewBuilder (e.g. laneAttentionTitle(_ lane: LaneSummary)) keeps presentation consistent if either drifts later in the PR series.

♻️ Sketch
`@ViewBuilder`
private func laneAttentionTitle(_ lane: LaneSummary) -> some View {
  HStack(spacing: 6) {
    Text(lane.name)
      .font(.subheadline.weight(.semibold))
      .foregroundStyle(ADEColor.textPrimary)
      .lineLimit(1)
    Text(lane.branchRef)
      .font(.system(.caption2, design: .monospaced))
      .foregroundStyle(ADEColor.textMuted)
      .lineLimit(1)
      .truncationMode(.middle)
  }
}

Also applies to: 206-216

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

In `@apps/ios/ADE/Views/Lanes/LaneListViewParts.swift` around lines 136 - 146, The
duplicated HStack rendering the lane title/branch (used in the rebase-suggestion
card and the auto-rebase attention card) should be extracted into a small
`@ViewBuilder` helper to keep presentation consistent; add a private function like
laneAttentionTitle(_ lane: LaneSummary) that returns the HStack (matching the
provided sketch) and replace the inline HStack usages (the
Text(snapshot.lane.name)/Text(snapshot.lane.branchRef) blocks at the two
duplicated sites) with calls to laneAttentionTitle(snapshot.lane) so both cards
use the single shared view.
apps/desktop/src/renderer/components/lanes/LaneStackPane.tsx (1)

259-277: Prefer displaying a normalized branch name in the lane label.

Rendering raw lane.branchRef can be noisy (refs/heads/...) and truncates useful info earlier. Consider showing a cleaned branch label while keeping the full ref in tooltip/details.

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

In `@apps/desktop/src/renderer/components/lanes/LaneStackPane.tsx` around lines
259 - 277, Replace the noisy raw branch ref shown in the second span by
computing a normalized branch name (e.g., strip the "refs/heads/" prefix or
other common prefixes) and render that normalized name in the visible text while
keeping the full lane.branchRef in the title/tooltip; update the rendering in
LaneStackPane (the span that currently uses lane.branchRef and title={`Branch:
${lane.branchRef}`}) to use the normalized value for display but retain
lane.branchRef for the title/details so truncation still shows the cleaned
branch name.
apps/ade-cli/src/cli.ts (1)

1051-1058: Consider only sending acknowledgeActiveWork when the flag is set.

acknowledgeActiveWork is currently always forwarded as a boolean (false when the flag is absent). The RPC schema accepts that, but to keep payloads minimal and consistent with startPoint/baseRef (which are spread conditionally), you may want to gate it the same way:

♻️ Suggested change
       steps: [actionCallStep("result", "git_checkout_branch", withLane({
         branchName,
         mode: create ? "create" : "existing",
         ...(startPoint ? { startPoint } : {}),
         ...(baseRef ? { baseRef } : {}),
-        acknowledgeActiveWork,
+        ...(acknowledgeActiveWork ? { acknowledgeActiveWork: true } : {}),
       }))]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ade-cli/src/cli.ts` around lines 1051 - 1058, The request is to stop
always sending acknowledgeActiveWork as a boolean; update the argument passed
into actionCallStep("result","git_checkout_branch", withLane({...})) so that
acknowledgeActiveWork is only included when the flag is set (e.g. spread
...(acknowledgeActiveWork ? { acknowledgeActiveWork } : {}) similar to how
startPoint and baseRef are handled), keeping branchName and mode unchanged and
using the same conditional-spread pattern to minimize the RPC payload.
apps/desktop/src/renderer/browserMock.ts (1)

2452-2467: Mock switchBranch always returns the primary lane regardless of input.

switchBranch: resolvedArg({ lane: MOCK_LANES[0], ... }) returns lane-main (the primary lane) for every invocation. This is fine to render the UI, but if any renderer component relies on the response lane.id matching the requested laneId (e.g., to update local state for the lane that was switched), the browser mock will quietly drive the wrong lane. Consider resolving the lane from the argument so the mock mirrors the real IPC contract more closely.

♻️ Suggested refinement
-      switchBranch: resolvedArg({
-        lane: MOCK_LANES[0],
-        previousBranchRef: "main",
-        activeWork: [],
-      }),
+      switchBranch: async (args: { laneId?: string } = {}) => {
+        const lane =
+          MOCK_LANES.find((l: any) => l.id === args?.laneId) ?? MOCK_LANES[0];
+        return {
+          lane,
+          previousBranchRef: lane.branchRef ?? "main",
+          activeWork: [],
+        };
+      },

You may also want previewBranchSwitch to echo the requested laneId/targetBranchRef for the same reason.

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

In `@apps/desktop/src/renderer/browserMock.ts` around lines 2452 - 2467, The mock
always returns MOCK_LANES[0] for switchBranch which causes incorrect lane.id in
callers; change the resolved value for switchBranch to look up the requested
laneId from the incoming args and return that lane (e.g., find by id in
MOCK_LANES and fall back to MOCK_LANES[0] if not found), and similarly update
previewBranchSwitch to echo the requested laneId/targetBranchRef from the args
instead of hardcoding "mock"/"main", so the mock mirrors the real IPC contract
(refer to switchBranch, previewBranchSwitch, and MOCK_LANES).
apps/desktop/src/main/services/sync/syncRemoteCommandService.ts (2)

828-830: Minor: simplify acknowledgeActiveWork parsing.

asOptionalBoolean is invoked twice for the same value. A single binding keeps it tidier and avoids the re-evaluation:

Proposed refactor
-    ...(asOptionalBoolean(value.acknowledgeActiveWork) !== undefined
-      ? { acknowledgeActiveWork: asOptionalBoolean(value.acknowledgeActiveWork) }
-      : {}),
+    ...(typeof value.acknowledgeActiveWork === "boolean"
+      ? { acknowledgeActiveWork: value.acknowledgeActiveWork }
+      : {}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/sync/syncRemoteCommandService.ts` around lines
828 - 830, The code calls asOptionalBoolean(value.acknowledgeActiveWork) twice;
capture it once in a local binding (e.g., const ack =
asOptionalBoolean(value.acknowledgeActiveWork)) and then use that binding in the
conditional that builds the object (use ack !== undefined ? {
acknowledgeActiveWork: ack } : {}), updating the object spread in the same
expression where acknowledgeActiveWork is set.

825-825: Unvalidated mode cast.

value.mode is cast to the "existing" | "create" union with only an asTrimmedString non-empty check — any arbitrary string (e.g. "foo") survives parsing and is passed downstream. laneService.switchBranch only branches on mode === "create" (defaulting otherwise to "existing"), so an unknown value silently degrades to "existing" rather than rejecting the request. Consider validating against the allowed set, mirroring how parseLandPrArgs/parseSubmitPrReviewArgs handle their unions.

Proposed fix
-    ...(asTrimmedString(value.mode) ? { mode: value.mode as GitCheckoutBranchArgs["mode"] } : {}),
+    ...((): Partial<Pick<GitCheckoutBranchArgs, "mode">> => {
+      const mode = asTrimmedString(value.mode);
+      if (!mode) return {};
+      if (mode !== "existing" && mode !== "create") {
+        throw new Error("git.checkoutBranch requires mode to be 'existing' or 'create'.");
+      }
+      return { mode };
+    })(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/sync/syncRemoteCommandService.ts` at line 825,
The code currently casts value.mode to GitCheckoutBranchArgs["mode"] after only
checking non-empty via asTrimmedString, allowing invalid strings to pass
through; update the logic that constructs the options (the expression using
...(asTrimmedString(value.mode) ? { mode: value.mode as
GitCheckoutBranchArgs["mode"] } : {})) to explicitly validate value.mode against
the allowed union ("existing" | "create") before including it, mirroring the
validation style used by parseLandPrArgs/parseSubmitPrReviewArgs; if the mode is
not one of the allowed values either omit the field or return/throw a validation
error so laneService.switchBranch only ever receives a valid "create" or
"existing" value.
apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts (1)

713-732: Tighten preview result assertion to match the requested mode.

This test sends mode: "create" but asserts mode: "existing" in the returned payload, which weakens intent clarity. Prefer overriding the mock for this test and asserting mode: "create".

♻️ Suggested test clarity tweak
 it("lanes.previewBranchSwitch routes to laneService.previewBranchSwitch with optional fields", async () => {
+  laneService.previewBranchSwitch.mockResolvedValueOnce({
+    laneId: "lane-1",
+    currentBranchRef: "main",
+    targetBranchRef: "feature/foo",
+    mode: "create",
+    dirty: false,
+    duplicateLaneId: null,
+    duplicateLaneName: null,
+    activeWork: [],
+    targetProfile: null,
+  });
   const result = await service.execute(makePayload("lanes.previewBranchSwitch", {
     laneId: "lane-1",
     branchName: "feature/foo",
     mode: "create",
     startPoint: "main",
     baseRef: "main",
     acknowledgeActiveWork: true,
   }));
@@
-  expect(result).toMatchObject({ laneId: "lane-1", mode: "existing" });
+  expect(result).toMatchObject({ laneId: "lane-1", mode: "create" });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts` around
lines 713 - 732, The test sends mode: "create" but the mocked
laneService.previewBranchSwitch returns a preview with mode: "existing", so
update the test to explicitly mock laneService.previewBranchSwitch to return a
preview object with mode: "create" (e.g., call mockReturnValueOnce or
mockResolvedValueOnce on laneService.previewBranchSwitch) before calling
service.execute(makePayload(...)), then change the assertion to
expect(result).toMatchObject({ laneId: "lane-1", mode: "create" }) so the
returned payload matches the requested mode; locate references to
service.execute, makePayload, and laneService.previewBranchSwitch in the test
and add the mock override there.
apps/ade-cli/src/cli.test.ts (1)

522-585: Add one socket-backed round-trip for checkout args.

These cases only verify buildCliPlan(). The same checkout fields are normalized again in apps/ade-cli/src/adeRpcServer.ts, so the desktop socket-backed path can drift from the headless planner without this suite failing. Based on learnings, For ADE CLI changes, verify both headless mode and the desktop socket-backed ADE RPC path.

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

In `@apps/ade-cli/src/cli.test.ts` around lines 522 - 585, Add tests that exercise
the desktop socket-backed ADE RPC path in addition to the headless planner: for
each existing checkout test (those calling buildCliPlan), create a parallel
assertion that sends the same CLI args through the ADE RPC server socket
round-trip (use the RPC entrypoint implemented in adeRpcServer, e.g., the
handler that accepts CLI requests) and assert the resulting step params match
the expected git_checkout_branch shape; specifically call the RPC path used by
adeRpcServer for checkout, deserialize the response to the same plan structure,
and verify mode, branchName, startPoint/baseRef presence, and
acknowledgeActiveWork to ensure the socket-backed normalization stays in sync
with buildCliPlan.
apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts (1)

2650-2668: Document the new checkout fields in the tool description and .describe() annotations.

The schema gained startPoint, baseRef, and acknowledgeActiveWork, but the tool description still reads "Switch to or create a git branch in a lane." and none of the new fields carry .describe() text. Since this is the LLM-facing contract, the model has no signal for when startPoint/baseRef apply (create vs. existing mode) or what acknowledgeActiveWork gates. Consider tightening the description and per-field docs so the operator emits these correctly with the new lane branch switching flow.

📝 Proposed wording
   tools.gitCheckoutBranch = tool({
-    description: "Switch to or create a git branch in a lane.",
+    description:
+      "Switch to an existing branch or create a new branch in a lane. " +
+      "When create=true, optionally specify startPoint (commit/branch to base the new branch on) and baseRef (upstream tracking ref). " +
+      "Set acknowledgeActiveWork=true only after the user has confirmed it is safe to leave behind uncommitted or in-progress work in the current branch.",
     inputSchema: z.object({
-      laneId: z.string().optional(),
-      branch: z.string().min(1),
-      create: z.boolean().optional().default(false),
-      startPoint: z.string().optional(),
-      baseRef: z.string().optional(),
-      acknowledgeActiveWork: z.boolean().optional().default(false),
+      laneId: z.string().optional().describe("Lane to operate in. Defaults to the primary lane."),
+      branch: z.string().min(1).describe("Branch name to switch to or create."),
+      create: z.boolean().optional().default(false).describe("If true, create a new branch instead of checking out an existing one."),
+      startPoint: z.string().optional().describe("Commit/branch to base a newly created branch on (only used when create=true)."),
+      baseRef: z.string().optional().describe("Upstream/base ref to track for the branch (only used when create=true)."),
+      acknowledgeActiveWork: z.boolean().optional().default(false).describe("Acknowledge that leaving active/uncommitted work is intentional. Required when the lane currently has dirty state."),
     }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts` around lines
2650 - 2668, Update the LLM-facing contract for tools.gitCheckoutBranch: expand
the tool description to mention both create and existing branch modes and when
startPoint, baseRef, and acknowledgeActiveWork apply, and add .describe()
annotations on the inputSchema fields startPoint, baseRef, and
acknowledgeActiveWork to give the model clear guidance (e.g., startPoint and
baseRef are used when create=true to set the starting ref and base, and
acknowledgeActiveWork is a boolean that must be true to proceed if there is
uncommitted or in-progress work). Ensure the description and per-field
.describe() text explicitly indicate the create vs existing behavior so the
operator emits the correct arguments to gitService.checkoutBranch.
apps/desktop/src/main/services/lanes/laneService.branchSwitch.test.ts (1)

476-522: Optional: Add a parented-lane case for mode='create'.

lane-c here has no parent, so the suite doesn't observe what happens to parent_lane_id / base_ref on the lane row when a child lane creates a fresh branch. Given switchBranch writes parent_lane_id from the (just-upserted) target profile, a regression that re-roots a child lane would slip through. Worth a sibling test that seeds lane-c with parentLaneId: "lane-main" and asserts the post-switch lane row's parent/base reflect the intended design.

🤖 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.branchSwitch.test.ts` around
lines 476 - 522, Add a sibling test that covers the parented-lane case for
mode='create': seed the DB with lane-c having parentLaneId: "lane-main" (use
insertLane with parentLaneId and branchRef "feature/c"), call
service.switchBranch({ laneId: "lane-c", branchName: "feature/new", mode:
"create", baseRef: "main" }) the same way as the existing test, then assert the
updated lane row's parentLaneId and baseRef reflect the intended behavior (check
service.listBranchProfiles("lane-c") and the returned result.lane to verify
parentLaneId/baseRef); use the same mocked runGit/runGitOrThrow setup and ensure
the checkout '-b' behavior is still asserted so regressions that re-root a child
lane are detected.
apps/desktop/src/renderer/components/lanes/laneUtils.ts (1)

239-271: Optional: Tighten stripRemotePrefix and centralize branch-name rules.

Two small things worth considering:

  1. The non-refs/remotes/ fallback unconditionally strips the first slash-delimited segment, so a stray call with a local-style name like feature/foo would silently return foo. Today that's gated by callers passing only remote rows, but it's a footgun for future reuse — consider explicitly requiring an origin/-style prefix before stripping.
  2. validateBranchName here and LaneBranchPickerSheet.validateBranchName are kept in lock-step manually; any future rule update will need both. Since these rules already mirror git check-ref-format, you might keep a brief comment pointing to that spec so the two stay aligned.
 export function stripRemotePrefix(name: string): string {
   if (name.startsWith("refs/remotes/")) {
     const rest = name.slice("refs/remotes/".length);
     const slash = rest.indexOf("/");
     return slash >= 0 ? rest.slice(slash + 1) : rest;
   }
-  const firstSlash = name.indexOf("/");
-  if (firstSlash > 0 && firstSlash < name.length - 1) {
-    return name.slice(firstSlash + 1);
-  }
-  return name;
+  // Only strip a leading remote namespace (e.g. "origin/feature/x" → "feature/x").
+  // Never collapse a regular slashed branch name like "feature/x" → "x".
+  if (name.startsWith("origin/")) {
+    return name.slice("origin/".length);
+  }
+  return name;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/lanes/laneUtils.ts` around lines 239 -
271, Update stripRemotePrefix to avoid stripping arbitrary local names: only
remove a prefix when the name startsWith "refs/remotes/" or matches an explicit
remote-style prefix like /^[^/]+\/.+/ (i.e., a single non-empty segment followed
by a slash), otherwise return the name unchanged; change the fallback logic in
stripRemotePrefix (the function) accordingly. Also centralize the branch-name
rules by reusing validateBranchName from this module in
LaneBranchPickerSheet.validateBranchName (import the function rather than
duplicating rules) and add a short comment above validateBranchName referencing
git check-ref-format so future edits stay aligned.
🤖 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/ade-cli/src/adeRpcServer.ts`:
- Around line 702-715: The RPC schema for the tool "git_checkout_branch"
accidentally advertises optional fields as required because the
inputSchema.required currently expands to every property; fix by ensuring the
required array only lists the truly mandatory key (keep required: ["branchName"]
and do not include mode, startPoint, baseRef, or acknowledgeActiveWork), i.e.,
leave mode, startPoint, baseRef, and acknowledgeActiveWork only under properties
so sanitizeToolSchema won’t mark them mandatory; reference the schema object for
name "git_checkout_branch" and the property keys mode, startPoint, baseRef, and
acknowledgeActiveWork when making the change.
- Around line 5265-5276: The code silently downgrades any non-"create" mode to
"existing"; instead validate toolArgs.mode and reject invalid values: check the
string value of toolArgs.mode (use the local mode variable) and if it is
provided and is not exactly "create" or "existing", return/throw an RPC
invalidParams error rather than mapping it to "existing"; then pass the
validated mode through to runtime.gitService.checkoutBranch (do not coerce
unknown strings to "existing"). This validation should live alongside where mode
is derived and before calling checkoutBranch to ensure checkoutBranch only
receives "create", "existing", or undefined.

In `@apps/ade-cli/src/cli.ts`:
- Around line 1044-1047: The VALUE_CARRIER_FLAGS set is missing the new
value-bearing options used by readValue (specifically "--start-point", "--from",
and "--base-ref"), causing hasHelpFlag to misinterpret a following "--help" as a
request instead of a value; update the VALUE_CARRIER_FLAGS collection to include
"--start-point", "--from", and "--base-ref" (keeping alphabetical/grouping
style) so hasHelpFlag treats help tokens as potential values for the flags read
by readValue (see references to VALUE_CARRIER_FLAGS, hasHelpFlag, and the
readValue calls for startPoint/baseRef).

In `@apps/desktop/src/main/services/git/gitOperationsService.ts`:
- Around line 1102-1107: The branch-picker path is calling laneService.list() on
every git.listBranches() request which causes an O(N) DB scan; replace the full
list call with a lightweight lookup or cached snapshot: add or use a fast lookup
like laneService.getByBranchRef/ findByBranchRef (query by branchRef) and call
that per branch instead of laneService.list(), or maintain a cached
activeLaneOwners map in gitOperationsService (updated on lane
create/update/delete events) and read from that cache synchronously; update the
code that currently uses laneService.list(), activeLaneOwners, and args.laneId
to use the new per-branch lookup or the cache to avoid enumerating all lanes.
- Around line 1098-1100: The call to laneService.listBranchProfiles(args.laneId)
inside listBranches() can throw and currently bubbles up, preventing the branch
picker from loading; wrap that call in a try/catch so the lookup is best-effort:
if listBranchProfiles(...) throws, catch the error, log or warn (similar to the
existing owner lookup behavior), and assign branchProfiles = new Set() so the
method continues gracefully; reference the
laneService.listBranchProfiles(args.laneId) invocation and the branchProfiles
variable inside the listBranches() function when making the change.

In `@apps/desktop/src/main/services/lanes/laneService.ts`:
- Around line 2025-2153: Post-checkout DB writes (starting at
upsertBranchProfileForRow and including the update lanes statement and the
stale-PR deletes) are not atomic, so a later DB error can leave the worktree on
the new branch but the DB inconsistent; wrap the sequence from the profile
upsert through the stale PR DELETEs in a single transaction (e.g.,
db.transaction(...) or equivalent) so the upsertBranchProfileForRow, the update
lanes (db.run with branch_ref/base_ref/parent_lane_id), and the multi-step
deletes for pr_convergence_state / pr_pipeline_settings / pr_issue_inventory /
pr_group_members and pull_requests are committed or rolled back together while
keeping the runGitOrThrow checkout outside the transaction; ensure you still
call invalidateLaneListCache() after a successful commit (and do not swallow
errors from runGitOrThrow or the transaction).
- Around line 633-725: Add a DB-level UNIQUE constraint on lane_branch_profiles
for (project_id, lane_id, normalized_branch_ref) and a one-time dedup migration
that consolidates duplicates (preserve one row per unique key and update any FK
references if needed); update your bootstrap SQL (DatabaseBootstrap.sql) and any
KV DB schema code to include the same unique index. Replace the
check-then-insert logic in upsertBranchProfileForRow with a single atomic INSERT
... ON CONFLICT(project_id, lane_id, normalized_branch_ref) DO UPDATE ... that
sets the columns (branch_ref, base_ref, parent_lane_id, source_branch_ref,
updated_at, last_checked_out_at) and returns the row, so getBranchProfileRow and
listBranchProfiles can rely on a single canonical row per key. Ensure the
migration runs once before normal operation and remove/guard any client-side
duplicate-creation code paths in the code that call upsertBranchProfileForRow.
- Around line 2059-2120: When creating/upserting a branch profile for a branch
the lane hasn't checked out before (both the mode === "create" branch and the
existing branch-without-profile path), don't set parentLaneId to null; instead
inherit the lane's current parent and base ref. Update the
upsertBranchProfileForRow calls that set parentLaneId: null to use
existingProfile?.parentLaneId ?? row.parent_lane_id (or similar accessor) and
set baseRef to args.baseRef?.trim() || existingProfile?.baseRef || row.base_ref
|| defaultBaseRef; use the same logic when constructing the profile in both the
create branch block and the branch-without-prior-profile path so
targetBranchRef, upsertBranchProfileForRow, toLaneBranchProfile,
targetProfileRow, and getBranchProfileRow all see the inherited parent_lane_id
and base_ref rather than null.

In `@apps/desktop/src/main/services/state/kvDb.ts`:
- Around line 779-798: The lane_branch_profiles table lacks a uniqueness
constraint for the natural key, which allows duplicate (project_id, lane_id,
normalized_branch_ref) rows; add a UNIQUE index (e.g. CREATE UNIQUE INDEX IF NOT
EXISTS idx_lane_branch_profiles_unique ON lane_branch_profiles(project_id,
lane_id, normalized_branch_ref)) instead of an inline UNIQUE clause to avoid
retrofitLegacyPrimaryKeyNotNullSchema issues, and ensure you run a one-time
dedupe migration to remove existing duplicates before creating the unique index
so the index creation does not fail; update the same DB bootstrapping code that
creates idx_lane_branch_profiles_lane and
idx_lane_branch_profiles_project_branch to include this unique index creation.

In `@apps/desktop/src/renderer/components/lanes/LanesPage.tsx`:
- Around line 529-538: When branchLane changes (or before starting a new fetch)
clear the shared laneBranches so stale branches from the previous lane aren't
shown: inside the useEffect that watches branchDropdownOpen and branchLane?.id
(the effect using window.ade.git.listBranches), call setLaneBranches([])
immediately when entering the effect (or when branchLane changes) and ensure the
catch handler also clears laneBranches on failure; keep the cancelled flag and
setLaneBranchesLoading toggles as currently implemented to avoid race
conditions.
- Around line 547-553: The branch-dropdown effect currently only clears search
state but not the create-branch defaults, so pickers like newBranchStartPoint
and newBranchBaseRef persist across lanes; update the same useEffect (the one
watching branchDropdownOpen) to also reset those defaults by calling the state
setters for newBranchStartPoint and newBranchBaseRef (e.g.,
setNewBranchStartPoint(null) and setNewBranchBaseRef(null) or the appropriate
clearing values) when branchDropdownOpen becomes true so a ref picked in one
lane won't be reused in the next.

In `@apps/ios/ADE/Resources/DatabaseBootstrap.sql`:
- Around line 57-76: Add a unique index (not an inline UNIQUE constraint) on
lane_branch_profiles for the tuple (project_id, lane_id, normalized_branch_ref)
to prevent duplicate rows during concurrent upserts; follow the pattern used for
linear_issue_claims and name it clearly (e.g.,
idx_lane_branch_profiles_unique_project_lane_branch). Before creating the unique
index, deduplicate any existing rows in lane_branch_profiles so the migration
won't fail on installs that already contain duplicates. This change protects the
race in getBranchProfileRow / upsertBranchProfileForRow where a get-then-insert
can produce duplicates and avoids relying on inline constraints that the desktop
migration (kvDb.ts) strips.

In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 2839-2845: The checkoutPrimaryBranch function currently has
defaulted parameters (mode, startPoint, baseRef, acknowledgeActiveWork) which
lets existing callers bypass the new preview/confirm flow; change the function
signature of checkoutPrimaryBranch (the method defined here) to remove the
defaults so callers must explicitly pass mode, startPoint, baseRef, and
acknowledgeActiveWork, then update all call sites (e.g., the call in
LanesTabView.swift) to supply the appropriate explicit values for these
parameters (choose the correct mode string and nil or concrete values for
startPoint/baseRef and true/false for acknowledgeActiveWork) so
dirty-state/active-work/duplicate-ownership flows always go through the new
confirmation UI.

In `@apps/ios/ADE/Views/Lanes/LaneActionsCard.swift`:
- Around line 50-59: The branchSwitchDisabledReason getter wrongly treats a nil
laneRole as a worker lane; update the final conditional in
branchSwitchDisabledReason to only disable switching for explicit worker roles
(e.g., require laneRole == "worker") or check that laneRole is non-nil and
equals "worker" instead of using laneRole != "result"; keep the existing
attached-lane and result-lane checks (referencing branchSwitchDisabledReason,
laneType, missionId, laneRole) so callers that omit laneRole do not get
inadvertently locked out.

In `@apps/ios/ADE/Views/Lanes/LaneBranchPickerSheet.swift`:
- Around line 550-573: The branch-name validator validateBranchName currently
only treats ASCII space/tab/newline as illegal; change the whitespace check to
use Swift's Unicode-aware Character.isWhitespace (e.g., replace the illegal set
entry for " ", "\t", "\n" and the contains(where:) call to check
$0.isWhitespace) so characters like U+00A0/U+2028 are rejected consistent with
the TypeScript regex; update the corresponding error message in
validateBranchName if needed and ensure this runs before previewBranchSwitch is
invoked.

In `@apps/ios/ADE/Views/Lanes/LaneListViewParts.swift`:
- Around line 137-145: Long lane names are starving the branch-ref Text in the
horizontal layout; update the Text for the branch ref (the Text showing
snapshot.lane.branchRef) to get higher layout priority so its monospaced middle
truncation remains useful (e.g., add .layoutPriority(1) to the branch-ref Text)
and/or reduce the name Text's priority (the Text showing snapshot.lane.name) so
the name truncates first; apply the same change to the second occurrence at the
other block (lines showing snapshot.lane.name and snapshot.lane.branchRef in the
later group).

---

Duplicate comments:
In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 868-870: When normalizeBranchName(headBranch).trim() yields an
empty string in getRowForLaneBranch, don’t immediately return null; instead
implement a lane-level fallback that searches for an existing PullRequestRow for
the given laneId (e.g., lookup existing rows keyed by laneId or scan the prs
rows collection) and return that row if found to avoid creating a duplicate/new
adoption path; only return null if no lane-level row exists. This change keeps
the upsert/adoption logic consistent when normalizedHead is empty.

---

Nitpick comments:
In `@apps/ade-cli/src/cli.test.ts`:
- Around line 522-585: Add tests that exercise the desktop socket-backed ADE RPC
path in addition to the headless planner: for each existing checkout test (those
calling buildCliPlan), create a parallel assertion that sends the same CLI args
through the ADE RPC server socket round-trip (use the RPC entrypoint implemented
in adeRpcServer, e.g., the handler that accepts CLI requests) and assert the
resulting step params match the expected git_checkout_branch shape; specifically
call the RPC path used by adeRpcServer for checkout, deserialize the response to
the same plan structure, and verify mode, branchName, startPoint/baseRef
presence, and acknowledgeActiveWork to ensure the socket-backed normalization
stays in sync with buildCliPlan.

In `@apps/ade-cli/src/cli.ts`:
- Around line 1051-1058: The request is to stop always sending
acknowledgeActiveWork as a boolean; update the argument passed into
actionCallStep("result","git_checkout_branch", withLane({...})) so that
acknowledgeActiveWork is only included when the flag is set (e.g. spread
...(acknowledgeActiveWork ? { acknowledgeActiveWork } : {}) similar to how
startPoint and baseRef are handled), keeping branchName and mode unchanged and
using the same conditional-spread pattern to minimize the RPC payload.

In `@apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts`:
- Around line 2650-2668: Update the LLM-facing contract for
tools.gitCheckoutBranch: expand the tool description to mention both create and
existing branch modes and when startPoint, baseRef, and acknowledgeActiveWork
apply, and add .describe() annotations on the inputSchema fields startPoint,
baseRef, and acknowledgeActiveWork to give the model clear guidance (e.g.,
startPoint and baseRef are used when create=true to set the starting ref and
base, and acknowledgeActiveWork is a boolean that must be true to proceed if
there is uncommitted or in-progress work). Ensure the description and per-field
.describe() text explicitly indicate the create vs existing behavior so the
operator emits the correct arguments to gitService.checkoutBranch.

In `@apps/desktop/src/main/services/lanes/laneService.branchSwitch.test.ts`:
- Around line 476-522: Add a sibling test that covers the parented-lane case for
mode='create': seed the DB with lane-c having parentLaneId: "lane-main" (use
insertLane with parentLaneId and branchRef "feature/c"), call
service.switchBranch({ laneId: "lane-c", branchName: "feature/new", mode:
"create", baseRef: "main" }) the same way as the existing test, then assert the
updated lane row's parentLaneId and baseRef reflect the intended behavior (check
service.listBranchProfiles("lane-c") and the returned result.lane to verify
parentLaneId/baseRef); use the same mocked runGit/runGitOrThrow setup and ensure
the checkout '-b' behavior is still asserted so regressions that re-root a child
lane are detected.

In `@apps/desktop/src/main/services/state/kvDb.ts`:
- Around line 792-794: The lane_id foreign key on lane_branch_profiles should
cascade deletes so per-lane profiles don't become orphaned; add an entry to the
existing FK_CONSTRAINTS map for the key "lane_branch_profiles:lane_id" with
references "lanes(id)" and action "on delete cascade", and update the
lane_branch_profiles table FK definition to include ON DELETE CASCADE so the
retrofit migration will apply the constraint to existing DBs; locate the
FK_CONSTRAINTS map and the lane_branch_profiles table definition to make these
changes.

In `@apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts`:
- Around line 713-732: The test sends mode: "create" but the mocked
laneService.previewBranchSwitch returns a preview with mode: "existing", so
update the test to explicitly mock laneService.previewBranchSwitch to return a
preview object with mode: "create" (e.g., call mockReturnValueOnce or
mockResolvedValueOnce on laneService.previewBranchSwitch) before calling
service.execute(makePayload(...)), then change the assertion to
expect(result).toMatchObject({ laneId: "lane-1", mode: "create" }) so the
returned payload matches the requested mode; locate references to
service.execute, makePayload, and laneService.previewBranchSwitch in the test
and add the mock override there.

In `@apps/desktop/src/main/services/sync/syncRemoteCommandService.ts`:
- Around line 828-830: The code calls
asOptionalBoolean(value.acknowledgeActiveWork) twice; capture it once in a local
binding (e.g., const ack = asOptionalBoolean(value.acknowledgeActiveWork)) and
then use that binding in the conditional that builds the object (use ack !==
undefined ? { acknowledgeActiveWork: ack } : {}), updating the object spread in
the same expression where acknowledgeActiveWork is set.
- Line 825: The code currently casts value.mode to GitCheckoutBranchArgs["mode"]
after only checking non-empty via asTrimmedString, allowing invalid strings to
pass through; update the logic that constructs the options (the expression using
...(asTrimmedString(value.mode) ? { mode: value.mode as
GitCheckoutBranchArgs["mode"] } : {})) to explicitly validate value.mode against
the allowed union ("existing" | "create") before including it, mirroring the
validation style used by parseLandPrArgs/parseSubmitPrReviewArgs; if the mode is
not one of the allowed values either omit the field or return/throw a validation
error so laneService.switchBranch only ever receives a valid "create" or
"existing" value.

In `@apps/desktop/src/renderer/browserMock.ts`:
- Around line 2452-2467: The mock always returns MOCK_LANES[0] for switchBranch
which causes incorrect lane.id in callers; change the resolved value for
switchBranch to look up the requested laneId from the incoming args and return
that lane (e.g., find by id in MOCK_LANES and fall back to MOCK_LANES[0] if not
found), and similarly update previewBranchSwitch to echo the requested
laneId/targetBranchRef from the args instead of hardcoding "mock"/"main", so the
mock mirrors the real IPC contract (refer to switchBranch, previewBranchSwitch,
and MOCK_LANES).

In `@apps/desktop/src/renderer/components/lanes/LaneStackPane.tsx`:
- Around line 259-277: Replace the noisy raw branch ref shown in the second span
by computing a normalized branch name (e.g., strip the "refs/heads/" prefix or
other common prefixes) and render that normalized name in the visible text while
keeping the full lane.branchRef in the title/tooltip; update the rendering in
LaneStackPane (the span that currently uses lane.branchRef and title={`Branch:
${lane.branchRef}`}) to use the normalized value for display but retain
lane.branchRef for the title/details so truncation still shows the cleaned
branch name.

In `@apps/desktop/src/renderer/components/lanes/laneUtils.ts`:
- Around line 239-271: Update stripRemotePrefix to avoid stripping arbitrary
local names: only remove a prefix when the name startsWith "refs/remotes/" or
matches an explicit remote-style prefix like /^[^/]+\/.+/ (i.e., a single
non-empty segment followed by a slash), otherwise return the name unchanged;
change the fallback logic in stripRemotePrefix (the function) accordingly. Also
centralize the branch-name rules by reusing validateBranchName from this module
in LaneBranchPickerSheet.validateBranchName (import the function rather than
duplicating rules) and add a short comment above validateBranchName referencing
git check-ref-format so future edits stay aligned.

In `@apps/ios/ADE/Views/Lanes/LaneListViewParts.swift`:
- Around line 136-146: The duplicated HStack rendering the lane title/branch
(used in the rebase-suggestion card and the auto-rebase attention card) should
be extracted into a small `@ViewBuilder` helper to keep presentation consistent;
add a private function like laneAttentionTitle(_ lane: LaneSummary) that returns
the HStack (matching the provided sketch) and replace the inline HStack usages
(the Text(snapshot.lane.name)/Text(snapshot.lane.branchRef) blocks at the two
duplicated sites) with calls to laneAttentionTitle(snapshot.lane) so both cards
use the single shared view.
🪄 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: b41a880e-7913-47bb-9f9d-00d45b6aedfa

📥 Commits

Reviewing files that changed from the base of the PR and between 244b82b and aa2815f.

⛔ Files ignored due to path filters (1)
  • docs/features/lanes/README.md is excluded by !docs/**
📒 Files selected for processing (31)
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts
  • apps/desktop/src/main/services/git/gitOperationsService.branchSwitch.test.ts
  • apps/desktop/src/main/services/git/gitOperationsService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/lanes/laneService.branchSwitch.test.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/main/services/state/kvDb.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.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/lanes/LaneStackPane.tsx
  • apps/desktop/src/renderer/components/lanes/LanesPage.tsx
  • apps/desktop/src/renderer/components/lanes/laneUtils.test.ts
  • apps/desktop/src/renderer/components/lanes/laneUtils.ts
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/types/git.ts
  • apps/desktop/src/shared/types/lanes.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Resources/DatabaseBootstrap.sql
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Lanes/LaneActionsCard.swift
  • apps/ios/ADE/Views/Lanes/LaneBranchPickerSheet.swift
  • apps/ios/ADE/Views/Lanes/LaneDetailGitSection.swift
  • apps/ios/ADE/Views/Lanes/LaneListViewParts.swift

Comment thread apps/ade-cli/src/adeRpcServer.ts
Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
Comment thread apps/ade-cli/src/cli.ts
Comment thread apps/desktop/src/main/services/git/gitOperationsService.ts Outdated
Comment thread apps/desktop/src/main/services/git/gitOperationsService.ts
Comment thread apps/ios/ADE/Resources/DatabaseBootstrap.sql
Comment thread apps/ios/ADE/Services/SyncService.swift Outdated
Comment thread apps/ios/ADE/Views/Lanes/LaneActionsCard.swift
Comment thread apps/ios/ADE/Views/Lanes/LaneBranchPickerSheet.swift
Comment thread apps/ios/ADE/Views/Lanes/LaneListViewParts.swift
Test fix:
- aiOrchestratorService health-sweep test: bump wait budget 40→200 (1s→5s) for CI
  loadiness; raise per-test timeout 10s→15s.

Review comments addressed:
- adeRpcServer: declare required[] on git.checkout_branch schema so optional
  fields stay optional; reject unknown checkout modes with invalidParams.
- cli: add --start-point/--from/--base-ref to VALUE_CARRIER_FLAGS.
- gitOperationsService: best-effort listBranchProfiles + new
  laneService.listBranchOwners (cheap, scoped) replaces full lane list per
  branch picker open.
- laneService: wrap post-checkout writes (profile upsert + lanes update +
  stale-PR cleanup) in a single db.transaction; bootstrap-time
  lane_branch_profiles dedup sweep covers the missing UNIQUE on CRR tables.
- prService: lane-level fallback when normalized headBranch trims to empty.
- LanesPage: clear laneBranches on lane change/fetch failure; reset
  newBranchStartPoint and newBranchBaseRef when target lane changes.
- iOS schema parity (DatabaseBootstrap.sql), explicit switch params at
  LanesTabView call sites, allow switch when laneRole nil, Unicode whitespace
  match for normalizeBranchKey, layoutPriority on LaneListViewParts.

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

arul28 commented Apr 26, 2026

@copilot review but do not make fixes

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant