Skip to content

Fix stuck lane delete and Codex resume recovery#508

Merged
arul28 merged 2 commits into
mainfrom
ade/delete-bugs-36d2e5b7
Jun 2, 2026
Merged

Fix stuck lane delete and Codex resume recovery#508
arul28 merged 2 commits into
mainfrom
ade/delete-bugs-36d2e5b7

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Jun 2, 2026

Summary

  • allow lane deletion to finish when the worktree is already unregistered or stale, while preserving cleanup warnings for partial filesystem removal
  • guard git reads and mutations against stale lane directories resolving to the main checkout
  • backfill missing Codex resume targets and add CLI/TUI parity for stale lane worktree detection
  • update feature docs for the lane, history, chat, agent tooling, and terminal/session flows

Validation

  • npm --prefix apps/desktop run typecheck
  • npm --prefix apps/desktop run lint
  • npm --prefix apps/desktop run build
  • npm --prefix apps/ade-cli run typecheck
  • npm --prefix apps/ade-cli run test (full run had transient failures; affected files passed isolated reruns)
  • npm --prefix apps/ade-cli run build
  • npm --prefix apps/web run typecheck
  • npm --prefix apps/web run build
  • npm run test:desktop:sharded equivalent via all 8 shards (transient shard failures passed isolated reruns)
  • node scripts/validate-docs.mjs
  • docs feature README source-map check
  • PRD markdown link check

Summary by CodeRabbit

  • New Features

    • Added ade lanes delete command with --force, --delete-branch, and --delete-remote-branch options.
  • Bug Fixes

    • Enhanced detection and handling of missing or stale lane worktrees.
    • Improved lane deletion robustness with better error recovery.
  • Improvements

    • Updated AI agent permissions to allow read-only inspection outside lane boundaries.
    • Enhanced resume session handling with local backfill capability.

Greptile Summary

This PR adds stale-worktree guards across ADE: before any lane-scoped git mutation or read, the service verifies that the saved worktreePath is still the actual Git worktree root (via git rev-parse --show-toplevel), preventing operations from silently running against the primary checkout when a lane directory has been removed or orphaned. It also relaxes the agent system-prompt boundary to allow read-only inspection outside the worktree while keeping writes/mutations lane-scoped, exposes ade lanes delete through the CLI, and fixes Codex session resume by backfilling storage targets on demand.

  • Stale-worktree guard: assertLaneWorktreeRoot (git service) and isExpectedGitWorktreeRoot (lane service) are added and called before every lane-scoped git operation; isLaneWorktreeAvailable in the TUI is similarly hardened with a cached spawnSync probe.
  • Agent boundary clarification: Three copies of the lane-directive text are updated to explicitly permit read-only inspection outside the worktree while restricting file edits and mutating commands to the lane path.
  • PTY / Codex resume: resolveEndedResumeSession is made fully async and attempts an on-demand resume-target backfill (including Codex rollout storage) before declaring a missing target.

Confidence Score: 3/5

The stale-worktree guard is a valuable safety feature but two defects can cause valid worktrees to be falsely rejected on macOS, and the TUI now blocks the event loop during rendering.

Both service files compare git rev-parse output using path.resolve without symlink resolution, which fails on macOS where /tmp resolves to /private/tmp. The TUI adds a spawnSync with an 8-second timeout in the synchronous render path. These affect the core lane-git path and TUI responsiveness.

apps/desktop/src/main/services/git/gitOperationsService.ts and apps/desktop/src/main/services/lanes/laneService.ts (normAbs symlink resolution); apps/ade-cli/src/tuiClient/app.tsx (spawnSync in render path)

Important Files Changed

Filename Overview
apps/ade-cli/src/tuiClient/app.tsx Enhanced isLaneWorktreeAvailable with a TTL-cached .git marker check and a spawnSync git probe — but the 8-second synchronous timeout runs in the Ink TUI render path, which can block the event loop on every cache miss.
apps/desktop/src/main/services/git/gitOperationsService.ts Added assertLaneWorktreeRoot guard to every lane-scoped git operation; normAbs uses path.resolve rather than fs.realpathSync.native, which can produce false stale-worktree rejections on macOS where /tmp is a symlink.
apps/desktop/src/main/services/lanes/laneService.ts Added isExpectedGitWorktreeRoot / assertExpectedGitWorktreeRoot helpers guarding status reads, stash listing, and change inspection; contains the same symlink issue in normAbs and a test-specific error-message bypass in production code.
apps/desktop/src/main/services/pty/ptyService.ts resolveEndedResumeSession refactored to async, eliminating the instanceof Promise dual-path. On-demand resume-target backfill attempted during resume-launch before reporting a missing target.
apps/desktop/src/main/services/chat/agentChatService.ts Worktree directive updated in three places to allow read-only inspection outside the lane worktree while restricting mutating operations — straightforward text change with no logic issues.
apps/ade-cli/src/cli.ts Added lanes delete to the CLI help text and buildCliPlan handler with --force, --delete-branch, and --delete-remote-branch flags.

Comments Outside Diff (1)

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

    P1 Same symlink-resolution gap as gitOperationsService.ts: normAbs uses only path.resolve, so the normAbs(topLevel) === normAbs(worktreePath) comparison in isExpectedGitWorktreeRoot can fail on macOS when the worktree path was stored without resolving /tmp to /private/tmp.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/lanes/laneService.ts
    Line: 256-258
    
    Comment:
    Same symlink-resolution gap as `gitOperationsService.ts`: `normAbs` uses only `path.resolve`, so the `normAbs(topLevel) === normAbs(worktreePath)` comparison in `isExpectedGitWorktreeRoot` can fail on macOS when the worktree path was stored without resolving `/tmp` to `/private/tmp`.
    
    
    
    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
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
apps/ade-cli/src/tuiClient/app.tsx:1049-1052
**`spawnSync` with 8-second timeout in UI render path**

`isLaneWorktreeAvailable` is called synchronously from `laneWorktreeUnavailableMessage` during TUI rendering. After the 2-second cache TTL expires, any lane whose directory has a `.git` marker will trigger `spawnSync("git", ...)` with `timeout: 8_000`. This blocks the Node.js event loop and the entire Ink TUI for up to 8 seconds on each cache miss. The original code did only an `fs.statSync`. With git lock contention or a slow filesystem this becomes a hard freeze.

### Issue 2 of 5
apps/desktop/src/main/services/git/gitOperationsService.ts:67-69
`normAbs` uses `path.resolve`, which does **not** resolve symlinks. On macOS `/tmp` is a symlink to `/private/tmp`, so if `lane.worktreePath` is stored as `/tmp/ade-worktrees/my-lane` but `git rev-parse --show-toplevel` returns `/private/tmp/ade-worktrees/my-lane`, the equality check fails and a valid worktree is rejected as stale. `normalizeWorktreePath` in `app.tsx` explicitly uses `fs.realpathSync.native` for this reason.

```suggestion
function normAbs(p: string): string {
  const resolved = path.resolve(p);
  try {
    return fs.realpathSync.native(resolved);
  } catch {
    return resolved;
  }
}
```

### Issue 3 of 5
apps/desktop/src/main/services/lanes/laneService.ts:256-258
Same symlink-resolution gap as `gitOperationsService.ts`: `normAbs` uses only `path.resolve`, so the `normAbs(topLevel) === normAbs(worktreePath)` comparison in `isExpectedGitWorktreeRoot` can fail on macOS when the worktree path was stored without resolving `/tmp` to `/private/tmp`.

```suggestion
function normAbs(p: string): string {
  const resolved = path.resolve(p);
  try {
    return fs.realpathSync.native(resolved);
  } catch {
    return resolved;
  }
}
```

### Issue 4 of 5
apps/desktop/src/main/services/lanes/laneService.ts:274-280
This catch branch returns `true` when the error message matches `"Unexpected git call:"` — a pattern that only originates from test mocks. Embedding test-fixture bypass logic in production code means any future mock refactor could silently disable the guard, and any production error starting with that phrase would also bypass validation.

```suggestion
  } catch {
    return false;
  }
```

### Issue 5 of 5
apps/desktop/src/main/services/git/gitOperationsService.ts:451
`assertLaneWorktreeRoot` silently returns (passes validation) when `runGit` yields `null`, `undefined`, or an object without a numeric `exitCode`. In those cases the guard is skipped and the caller proceeds to mutate an unverified path. The same silent-pass pattern exists in `isExpectedGitWorktreeRoot` in `laneService.ts` (line 269). Treating an unrecognised response as valid inverts the safe-default principle.

```suggestion
      if (!topLevelRes || typeof topLevelRes.exitCode !== "number") {
        throw laneWorktreeMissingError(lane);
      }
```

Reviews (1): Last reviewed commit: "ship: prepare lane for review" | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Jun 2, 2026 5:45am

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Jun 2, 2026

@copilot review but do not make fixes

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR hardens lane operations against stale or missing worktrees by introducing comprehensive validation, updates AI agent guidance to permit read-only inspection outside worktrees, refactors PTY session resume logic to async with backfill support, and documents the new ade lanes delete CLI command.

Changes

Worktree validation and deletion resilience

Layer / File(s) Summary
CLI lane delete command
apps/ade-cli/README.md, apps/ade-cli/src/cli.ts, apps/ade-cli/src/cli.test.ts
Adds ade lanes delete CLI command with --force, --delete-branch, and --delete-remote-branch flags, documented in help text, examples, and validated in CLI test.
CLI-side worktree availability caching and validation
apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts, apps/ade-cli/src/tuiClient/app.tsx
isLaneWorktreeAvailable now caches availability using resolved real paths and validates by checking .git presence or running git rev-parse --show-toplevel to confirm the resolved root matches the lane path, with TTL-based caching to avoid repeated filesystem probes.
Git operations worktree root validation
apps/desktop/src/main/services/git/gitOperationsService.ts
Adds assertLaneWorktreeRoot helper that runs git rev-parse --show-toplevel, compares normalized paths, and throws "Lane worktree is missing" error on mismatch; updates isMissingWorktreeError to recognize the new message; wires validation into runLaneOperation and multiple read-only git operations to fail fast when worktrees are stale or missing.
Git operations test suite for worktree guards
apps/desktop/src/main/services/git/gitOperationsService.test.ts
Test setup resets git mocks after each test; new and updated test cases verify stageAll, generateCommitMessage, and listRecentCommits reject stale worktrees with the expected error; multiple existing tests updated to expect rev-parse --path-format=absolute --show-toplevel as the first git call, with mocked responses for lane and main worktree paths.
Lane service stale worktree detection and delete hardening
apps/desktop/src/main/services/lanes/laneService.ts
Adds isExpectedGitWorktreeRoot / assertExpectedGitWorktreeRoot helpers to validate lane worktrees; updates detectBranchRef, computeLaneStatus, inspectWorktreeChanges, and listGitStashes to early-return when worktree root is invalid; refactors lane delete pipeline to record non-fatal step failures, provide specific stale/missing detail messages, detect whether worktrees are still registered, attempt best-effort git worktree prune, and downgrade cleanup failures into warnings instead of hard errors.
Lane service stale worktree and delete warning tests
apps/desktop/src/main/services/lanes/laneService.test.ts
New test suite verifies that list({ includeStatus: true }) does not invoke git status for stale lane directories inside the repo; new delete test simulates residual files that trigger ENOTEMPTY and git worktree remove failure, asserting the delete completes as completed_with_warnings with the lane DB row removed but directory remaining.
Agent system prompt and worktree guidance updates
apps/desktop/src/main/services/ai/tools/systemPrompt.ts, apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts, apps/desktop/src/main/services/chat/agentChatService.ts, apps/desktop/src/main/services/chat/agentChatService.test.ts
Updates buildCodingAgentSystemPrompt to explicitly allow read-only inspection outside the worktree path while restricting edits and mutating commands to inside the path; updates agentChatService launch directives in three locations to permit read-only inspection; test assertions updated to verify both permission types are present in generated prompts.
PTY resume session async refactor with backfill support
apps/desktop/src/main/services/pty/ptyService.ts, apps/desktop/src/main/services/pty/ptyService.test.ts
resolveEndedResumeSession converted to async and refactored to compute resume target from session metadata and parsed resume commands; when target is missing (and not "cursor"), attempts backfilling via transcript/local data read before re-deriving target; for Codex sessions, detects "updated before resumable thread created" pattern and throws appropriately; call sites updated to always await the now-async function.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly related PRs

  • arul28/ADE#290: Both PRs update the lane deletion workflow to support non-fatal failures and surface a completed_with_warnings outcome (notably in apps/desktop/src/main/services/lanes/laneService.ts delete-step handling).

Suggested labels

desktop

🚥 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
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.
Title check ✅ Passed The PR title 'Fix stuck lane delete and Codex resume recovery' accurately summarizes the main changes: it addresses deletion issues in lane management and recovery problems in Codex resume functionality, which are reflected across the file summaries.

✏️ 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/delete-bugs-36d2e5b7

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.

Comment on lines +1049 to +1052
const probe = spawnSync("git", ["rev-parse", "--path-format=absolute", "--show-toplevel"], {
cwd: resolvedRoot,
encoding: "utf8",
timeout: 8_000,
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 spawnSync with 8-second timeout in UI render path

isLaneWorktreeAvailable is called synchronously from laneWorktreeUnavailableMessage during TUI rendering. After the 2-second cache TTL expires, any lane whose directory has a .git marker will trigger spawnSync("git", ...) with timeout: 8_000. This blocks the Node.js event loop and the entire Ink TUI for up to 8 seconds on each cache miss. The original code did only an fs.statSync. With git lock contention or a slow filesystem this becomes a hard freeze.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ade-cli/src/tuiClient/app.tsx
Line: 1049-1052

Comment:
**`spawnSync` with 8-second timeout in UI render path**

`isLaneWorktreeAvailable` is called synchronously from `laneWorktreeUnavailableMessage` during TUI rendering. After the 2-second cache TTL expires, any lane whose directory has a `.git` marker will trigger `spawnSync("git", ...)` with `timeout: 8_000`. This blocks the Node.js event loop and the entire Ink TUI for up to 8 seconds on each cache miss. The original code did only an `fs.statSync`. With git lock contention or a slow filesystem this becomes a hard freeze.

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

Fix in Claude Code

Comment on lines +67 to +69
function normAbs(p: string): string {
return path.resolve(p);
}
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 normAbs uses path.resolve, which does not resolve symlinks. On macOS /tmp is a symlink to /private/tmp, so if lane.worktreePath is stored as /tmp/ade-worktrees/my-lane but git rev-parse --show-toplevel returns /private/tmp/ade-worktrees/my-lane, the equality check fails and a valid worktree is rejected as stale. normalizeWorktreePath in app.tsx explicitly uses fs.realpathSync.native for this reason.

Suggested change
function normAbs(p: string): string {
return path.resolve(p);
}
function normAbs(p: string): string {
const resolved = path.resolve(p);
try {
return fs.realpathSync.native(resolved);
} catch {
return resolved;
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/git/gitOperationsService.ts
Line: 67-69

Comment:
`normAbs` uses `path.resolve`, which does **not** resolve symlinks. On macOS `/tmp` is a symlink to `/private/tmp`, so if `lane.worktreePath` is stored as `/tmp/ade-worktrees/my-lane` but `git rev-parse --show-toplevel` returns `/private/tmp/ade-worktrees/my-lane`, the equality check fails and a valid worktree is rejected as stale. `normalizeWorktreePath` in `app.tsx` explicitly uses `fs.realpathSync.native` for this reason.

```suggestion
function normAbs(p: string): string {
  const resolved = path.resolve(p);
  try {
    return fs.realpathSync.native(resolved);
  } catch {
    return resolved;
  }
}
```

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

Fix in Claude Code

Comment on lines +274 to +280
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
// Test mocks can throw for unhandled git probes. The real runGit helper
// returns a GitRunResult for normal git failures, so keep those tests on
// their existing path without weakening production validation.
return /^Unexpected git call:/i.test(message);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 This catch branch returns true when the error message matches "Unexpected git call:" — a pattern that only originates from test mocks. Embedding test-fixture bypass logic in production code means any future mock refactor could silently disable the guard, and any production error starting with that phrase would also bypass validation.

Suggested change
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
// Test mocks can throw for unhandled git probes. The real runGit helper
// returns a GitRunResult for normal git failures, so keep those tests on
// their existing path without weakening production validation.
return /^Unexpected git call:/i.test(message);
}
} catch {
return false;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/lanes/laneService.ts
Line: 274-280

Comment:
This catch branch returns `true` when the error message matches `"Unexpected git call:"` — a pattern that only originates from test mocks. Embedding test-fixture bypass logic in production code means any future mock refactor could silently disable the guard, and any production error starting with that phrase would also bypass validation.

```suggestion
  } catch {
    return false;
  }
```

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

["rev-parse", "--path-format=absolute", "--show-toplevel"],
{ cwd: lane.worktreePath, timeoutMs: 8_000 },
);
if (!topLevelRes || typeof topLevelRes.exitCode !== "number") return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 assertLaneWorktreeRoot silently returns (passes validation) when runGit yields null, undefined, or an object without a numeric exitCode. In those cases the guard is skipped and the caller proceeds to mutate an unverified path. The same silent-pass pattern exists in isExpectedGitWorktreeRoot in laneService.ts (line 269). Treating an unrecognised response as valid inverts the safe-default principle.

Suggested change
if (!topLevelRes || typeof topLevelRes.exitCode !== "number") return;
if (!topLevelRes || typeof topLevelRes.exitCode !== "number") {
throw laneWorktreeMissingError(lane);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/git/gitOperationsService.ts
Line: 451

Comment:
`assertLaneWorktreeRoot` silently returns (passes validation) when `runGit` yields `null`, `undefined`, or an object without a numeric `exitCode`. In those cases the guard is skipped and the caller proceeds to mutate an unverified path. The same silent-pass pattern exists in `isExpectedGitWorktreeRoot` in `laneService.ts` (line 269). Treating an unrecognised response as valid inverts the safe-default principle.

```suggestion
      if (!topLevelRes || typeof topLevelRes.exitCode !== "number") {
        throw laneWorktreeMissingError(lane);
      }
```

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

Fix in Claude Code

@arul28 arul28 deleted the ade/delete-bugs-36d2e5b7 branch June 2, 2026 08:07
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