Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ ade lanes list --text
ade lanes create "fix-checkout-flow" --parent main
ade lanes create "lin-123" --linear-issue-json '{"id":"...","identifier":"LIN-123","title":"...","projectId":"...","projectSlug":"...","teamId":"...","teamKey":"...","stateId":"...","stateName":"Todo","stateType":"unstarted","priority":2,"priorityLabel":"high","labels":[],"assigneeId":null,"assigneeName":null,"createdAt":"...","updatedAt":"..."}'
ade lanes reparent lane-child --parent lane-parent --stack-base-branch main
ade lanes delete lane-id --force --delete-branch
ade lanes create-from-linear --issue-id ENG-431 --start-chat --provider codex --model <model>
ade lanes batch-create-from-linear --linear-issues-json '[{"id":"...","identifier":"ENG-431"},{"id":"...","identifier":"ENG-440"}]'
ade chat attach-linear-issue <session> --issue-id ENG-431
Expand Down
27 changes: 27 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,33 @@ describe("ADE CLI", () => {
});
});

it("maps lane delete flags to the shared lane action", () => {
const plan = buildCliPlan([
"lanes",
"delete",
"lane-old",
"--force",
"--delete-branch",
"--delete-remote-branch",
]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") throw new Error(`expected execute plan, got ${plan.kind}`);
expect(plan.label).toBe("lane delete");
expect(plan.steps[0]?.params).toEqual({
name: "run_ade_action",
arguments: {
domain: "lane",
action: "delete",
args: {
laneId: "lane-old",
force: true,
deleteBranch: true,
deleteRemoteBranch: true,
},
},
});
});

it("forwards PR GitHub snapshot full-history flag to the runtime action", () => {
const snapshot = buildCliPlan([
"prs",
Expand Down
1 change: 1 addition & 0 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,7 @@ const HELP_BY_COMMAND: Record<string, string> = {
$ ade lanes import --branch <branch> Register an existing branch/worktree
$ ade lanes archive <lane> Archive a lane in ADE
$ ade lanes unarchive <lane> Restore an archived lane
$ ade lanes delete <lane> --force Delete a lane and clean up its worktree
$ ade lanes attach --path <worktree> --name <n> Attach an external worktree
$ ade lanes reparent <lane> --parent <parent> Move lane onto a new parent (runs git rebase)
$ ade lanes reparent <lane> --parent <parent> --stack-base-branch <branch>
Expand Down
47 changes: 47 additions & 0 deletions apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
CLAUDE_TERMINAL_SUBMIT_CONFIRM_DELAY_MS,
Expand All @@ -21,6 +25,7 @@ import {
isPromptLineBackspace,
isPromptWordBackspace,
isTerminalSessionFastPollActive,
isLaneWorktreeAvailable,
isTerminalSessionWorking,
isTerminalSessionResumable,
shouldToggleLatestFailedLineOnBlankEnter,
Expand Down Expand Up @@ -310,6 +315,48 @@ describe("lane delete form helpers", () => {
});
});

describe("lane worktree availability", () => {
function laneAt(worktreePath: string): LaneSummary {
return {
id: "lane-1",
name: "Lane one",
laneType: "worktree",
baseRef: "main",
branchRef: "feature/lane-one",
worktreePath,
parentLaneId: null,
childCount: 0,
stackDepth: 0,
parentStatus: null,
isEditProtected: false,
status: { dirty: false, ahead: 0, behind: 0, remoteBehind: 0, rebaseInProgress: false },
color: null,
icon: null,
tags: [],
createdAt: "2026-05-20T00:00:00.000Z",
};
}

it("rejects an existing stale directory that resolves to another git root", () => {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-tui-stale-lane-"));
try {
const init = spawnSync("git", ["init"], { cwd: repoRoot, encoding: "utf8" });
const staleLanePath = path.join(repoRoot, ".ade", "worktrees", "stale-lane");
fs.mkdirSync(staleLanePath, { recursive: true });

if (init.status === 0) {
expect(isLaneWorktreeAvailable(laneAt(repoRoot))).toBe(true);
} else {
fs.mkdirSync(path.join(repoRoot, ".git"), { recursive: true });
expect(isLaneWorktreeAvailable(laneAt(repoRoot))).toBe(true);
}
expect(isLaneWorktreeAvailable(laneAt(staleLanePath))).toBe(false);
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
});
});

describe("right pane context defaults", () => {
function laneForContext(overrides: Partial<LaneSummary> = {}): LaneSummary {
return {
Expand Down
46 changes: 44 additions & 2 deletions apps/ade-cli/src/tuiClient/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1009,14 +1009,56 @@ function formatGoalBannerLine(goal: CodexThreadGoal | null): string | null {
import { subagentSnapshotsFromEvents } from "../../../desktop/src/shared/chatSubagents";
export { subagentSnapshotsFromEvents };

function isLaneWorktreeAvailable(lane: LaneSummary | null | undefined): boolean {
const LANE_WORKTREE_AVAILABILITY_CACHE_TTL_MS = 2_000;
const laneWorktreeAvailabilityCache = new Map<string, { checkedAt: number; mtimeMs: number; available: boolean }>();

function cacheLaneWorktreeAvailability(root: string, stat: fs.Stats, checkedAt: number, available: boolean): boolean {
laneWorktreeAvailabilityCache.set(root, { checkedAt, mtimeMs: stat.mtimeMs, available });
return available;
}

function normalizeWorktreePath(root: string): string {
const resolved = path.resolve(root);
try {
return fs.realpathSync.native(resolved);
} catch {
return resolved;
}
}

export function isLaneWorktreeAvailable(lane: LaneSummary | null | undefined): boolean {
const root = lane?.worktreePath?.trim();
if (!root) return false;
const resolvedRoot = normalizeWorktreePath(root);
let stat: fs.Stats;
try {
return fs.statSync(root).isDirectory();
stat = fs.statSync(resolvedRoot);
if (!stat.isDirectory()) return false;
} catch {
return false;
}
const cached = laneWorktreeAvailabilityCache.get(resolvedRoot);
const now = Date.now();
if (cached && cached.mtimeMs === stat.mtimeMs && now - cached.checkedAt < LANE_WORKTREE_AVAILABILITY_CACHE_TTL_MS) {
return cached.available;
}
const markerExists = fs.existsSync(path.join(resolvedRoot, ".git"));
if (!markerExists) {
return cacheLaneWorktreeAvailability(resolvedRoot, stat, now, false);
}
const probe = spawnSync("git", ["rev-parse", "--path-format=absolute", "--show-toplevel"], {
cwd: resolvedRoot,
encoding: "utf8",
timeout: 8_000,
Comment on lines +1049 to +1052
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

});
let available: boolean;
if (probe.status === 0) {
const topLevel = probe.stdout.trim();
available = topLevel ? normalizeWorktreePath(topLevel) === resolvedRoot : true;
} else {
available = false;
}
return cacheLaneWorktreeAvailability(resolvedRoot, stat, now, available);
}

function laneWorktreeUnavailableMessage(lane: LaneSummary | null | undefined): string | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ describe("buildCodingAgentSystemPrompt", () => {
it("returns a prompt containing the cwd", () => {
const result = buildCodingAgentSystemPrompt({ cwd: "/my/project" });
expect(result).toContain("/my/project");
expect(result).toContain("Read-only inspection outside this path is allowed");
expect(result).toContain("mutating commands only inside this path");
});

it("defaults to coding mode and edit permission mode", () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/services/ai/tools/systemPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export function buildCodingAgentSystemPrompt(args: {

return [
`You are ADE's software engineering agent working in ${args.cwd}.`,
"This session is bound to that worktree. Read, edit, and run commands only inside this path unless ADE explicitly relaunches you in a different lane.",
"This session is bound to that worktree for writes and mutations. Read-only inspection outside this path is allowed when needed, but edit files and run mutating commands only inside this path unless ADE explicitly relaunches you in a different lane.",
...(orchestrationDirective ? [orchestrationDirective] : []),
...(runtime
? [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3200,7 +3200,8 @@ describe("createAgentChatService", () => {
}));
expect(firstUserContent).toContain("[ADE launch directive]");
expect(firstUserContent).toContain(tmpRoot);
expect(firstUserContent).toContain("only inside that worktree");
expect(firstUserContent).toContain("Read-only inspection outside that worktree is allowed");
expect(firstUserContent).toContain("mutating commands only inside that worktree");
expect(firstUserContent).toContain("only normal reason to skip ADE CLI");
expect(firstUserContent).toContain("ade actions list --text");
expect(secondUserContent).not.toContain("[ADE launch directive]");
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/main/services/chat/agentChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3532,7 +3532,7 @@ function buildLaneWorktreeDirective(args: { laneId: string; laneWorktreePath: st
return [
"[ADE launch directive]",
`ADE launched this session in lane '${laneId}' at worktree '${laneWorktreePath}'.`,
"Read, edit, and run commands only inside that worktree. Do not switch to project root, another lane, or another repo unless ADE explicitly relaunches you there.",
"Read-only inspection outside that worktree is allowed when needed. Edit files and run mutating commands only inside that worktree unless ADE explicitly relaunches you elsewhere.",
].join("\n");
}

Expand Down Expand Up @@ -11928,7 +11928,7 @@ export function createAgentChatService(args: {
"",
"## ADE Workspace",
`ADE launched this session in lane worktree: ${managed.laneWorktreePath}.`,
"Read, edit, and run commands only inside that worktree. Do not switch to project root, another lane, or another repo unless ADE explicitly relaunches you there.",
"Read-only inspection outside that worktree is allowed when needed. Edit files and run mutating commands only inside that worktree unless ADE explicitly relaunches you elsewhere.",
"",
...slashCommandsSection,
"",
Expand Down Expand Up @@ -15933,7 +15933,7 @@ export function createAgentChatService(args: {
"",
"## ADE Workspace",
`ADE launched this session in lane worktree: ${managed.laneWorktreePath}.`,
"Read, edit, and run commands only inside that worktree. Do not switch to project root, another lane, or another repo unless ADE explicitly relaunches you there.",
"Read-only inspection outside that worktree is allowed when needed. Edit files and run mutating commands only inside that worktree unless ADE explicitly relaunches you elsewhere.",
"",
...(linearDirective ? [linearDirective, ""] : []),
...slashCommandsSection,
Expand Down
Loading
Loading