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
5 changes: 4 additions & 1 deletion apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,8 @@ ade doctor --online --text # also check the latest deskto
ade projects list --text
ade projects inspect /path/to/checkout --json # classify a path (repo root vs linked/ADE-managed worktree) and find its owning project + existing lane
ade init
ade lanes list --text
ade lanes list --text # every git worktree of the project is a lane: this reconciles against git on each call, adopting worktrees made outside ADE and dropping lanes whose worktree is gone from git and disk. There is no attach/adopt step
ade lanes import --branch feature/login --text # lane for an existing branch, checked out in a new managed worktree; refuses when that branch is already checked out (that worktree is already a lane)
ade lanes create "fix-checkout-flow" --parent main
ade lanes create "fix-login" --base origin/main # omit --base to branch from the configured new-lane base (remote-first by default)
ade lanes child --lane lane-parent --name fix-followup # child lane carries the parent's unmerged work; a base-less `ade lanes create`/`--auto-create-lane` from a lane with commits not yet on main prints a non-blocking stderr nudge to use this instead
Expand Down Expand Up @@ -501,6 +502,8 @@ ade --role cto actions list --domain attention --text # discover account-wide Ac
ade --role cto actions run attention.getSnapshot --input-json '{"since":0}' --json
ade actions run git.stageFile --arg laneId=lane-id --arg path=src/index.ts
ade actions run pty.resumeSession --arg sessionId=session-id
ade actions run external-sessions.list --input-json '{"scope":"project","limit":20}' --text # claude/codex/cursor/droid/opencode sessions on this machine; discovery that cannot run — `opencode` is not installed, say — fails the call when that provider is the only one asked for, rather than reporting an empty list; in a multi-provider scan it is skipped and logged
ade actions run external-sessions.import --input-json '{"provider":"codex","sessionId":"thread-id","laneId":"lane-1","target":"cli","mode":"resume"}' --text
ade cursor cloud agents list --text
ade cursor cloud agents create --repo https://github.com/owner/repo --prompt "fix flaky test" --auto-pr
ade --role cto github app-auth login # device-flow authorize the machine ADE GitHub App (headless/brain)
Expand Down
34 changes: 27 additions & 7 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ function createRuntime() {
laneService: {
list: vi.fn(async () => laneRows),
ensurePrimaryLane: vi.fn(async () => laneRows[0]),
listUnregisteredWorktrees: vi.fn(async () => [{ path: "/tmp/untracked-worktree", branch: "feature/untracked" }]),
getLaneWorktreePath: vi.fn((laneId: string) => {
const lane = laneRows.find((row) => row.id === laneId) ?? laneRows[0]!;
return lane.worktreePath;
Expand Down Expand Up @@ -3165,16 +3164,11 @@ describe("adeRpcServer", () => {
expect(response.structuredContent.message).toBe("generated commit message");
});

it("lists and imports unregistered lane worktrees", async () => {
it("imports an existing branch as a lane", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });

const listResponse = await callTool(handler, "list_unregistered_lanes", {});
expect(listResponse?.isError).toBeUndefined();
expect(fixture.runtime.laneService.listUnregisteredWorktrees).toHaveBeenCalledTimes(1);
expect(listResponse.structuredContent.worktrees[0].branch).toBe("feature/untracked");

const importResponse = await callTool(handler, "import_lane", {
branchRef: "feature/untracked",
name: "Imported lane",
Expand Down Expand Up @@ -4851,6 +4845,32 @@ describe("adeRpcServer", () => {
});
});

// Discovery for a provider whose CLI is missing now throws instead of
// returning [], and `ade actions run external-sessions.list` is the only way
// an agent reaches it. The message has to survive to the CLI's stderr as a
// normal action failure, because the alternative an agent sees is a stack
// trace it cannot act on.
it("reports a failed external-session discovery as a readable action error", async () => {
const fixture = createRuntime();
const list = vi.fn(async () => {
throw new Error("OpenCode CLI not found: install `opencode` to import its sessions.");
});
(fixture.runtime as any).externalSessionsService = { list, importExternalSession: vi.fn() };
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "cto-1", role: "cto" });

const listed = await callTool(handler, "run_ade_action", {
domain: "external-sessions",
action: "list",
args: { providers: ["opencode"], scope: "all" },
});

expect(listed.isError).toBe(true);
expect(listed.error.message).toBe(
"OpenCode CLI not found: install `opencode` to import its sessions.",
);
});

it("allows the unbound ade CLI to use explicit-lane external-session actions", async () => {
const fixture = createRuntime();
const persistedSession = {
Expand Down
19 changes: 2 additions & 17 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ const TOOL_SPECS: ToolSpec[] = [
},
{
name: "list_lanes",
description: "List active lanes with metadata and branch status.",
description: "List active lanes with metadata and branch status. Every git worktree of the project is a lane, so this also picks up worktrees created outside ADE and drops lanes whose worktree no longer exists.",
inputSchema: {
type: "object",
additionalProperties: false,
Expand All @@ -706,18 +706,9 @@ const TOOL_SPECS: ToolSpec[] = [
}
}
},
{
name: "list_unregistered_lanes",
description: "List git worktrees that are not yet registered as ADE lanes.",
inputSchema: {
type: "object",
additionalProperties: false,
properties: {}
}
},
{
name: "import_lane",
description: "Import an existing git branch/worktree into ADE lane tracking.",
description: "Create a lane for an existing git branch by checking it out in a new ADE-managed worktree. Fails when the branch is already checked out in another worktree, because that worktree is already a lane.",
inputSchema: {
type: "object",
required: ["branchRef"],
Expand Down Expand Up @@ -1378,7 +1369,6 @@ const READ_ONLY_TOOLS = new Set([
"get_lane_status",
"get_lane_conflict_state",
"list_lanes",
"list_unregistered_lanes",
"git_get_sync_status",
"git_list_branches",
"git_get_user_identity",
Expand Down Expand Up @@ -4189,11 +4179,6 @@ async function runTool(args: {
};
}

if (name === "list_unregistered_lanes") {
const worktrees = await runtime.laneService.listUnregisteredWorktrees();
return { worktrees };
}

if (name === "get_lane_status") {
const laneId = requireLaneIdForTool(runtime, session, toolArgs, "get_lane_status");
return await buildLaneStatus(runtime, laneId);
Expand Down
2 changes: 0 additions & 2 deletions apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,9 +835,7 @@ export async function createAdeRuntime(args: {
logger,
projectRoot,
projectId,
baseRef,
freshProject: !hadAdeDb,
laneService,
projectConfigService,
});

Expand Down
62 changes: 9 additions & 53 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1476,10 +1476,13 @@ const HELP_BY_COMMAND: Record<string, string> = {
lanes: `${ADE_BANNER}
Lanes

Lanes are ADE-managed worktrees and branches. Most commands accept either
--lane <lane-id> or a positional lane id.
Every git worktree of the project is a lane. "ade lanes list" reconciles lane
rows against git on each call, so a worktree you created by hand shows up as a
lane with no attach step, and a lane whose worktree is gone from git and disk
stops being listed. Most commands accept either --lane <lane-id> or a
positional lane id.

$ ade lanes list --text Show lane stack graph and branch names
$ ade lanes list --text Show lane stack graph and branch names (adopts new worktrees)
$ ade lanes show <lane> --text Inspect one lane status
$ ade lane drift --lane <lane> --text Check whether the worktree HEAD drifted off the lane's branch
$ ade lane drift resolve --lane <lane> --switch-back
Expand All @@ -1503,14 +1506,15 @@ const HELP_BY_COMMAND: Record<string, string> = {
$ ade lanes create --branch-name <branch> Override the auto-generated branch name
$ ade lanes child --lane <parent> --name <name> Create a child lane under a parent
Child lanes carry the parent's unmerged work
$ ade lanes import --branch <branch> Register an existing branch/worktree
$ ade lanes import --branch <branch> Create a lane for an existing branch, checked out in a new managed
worktree (fails if the branch is already checked out somewhere —
that worktree is already a lane)
$ ade lanes archive <lane> Archive a lane in ADE
$ ade lanes reclaim-preview <lane> Show reclaimable space and safety warnings
$ ade lanes archive-and-reclaim <lane> --confirm RECLAIM
Archive the lane, then remove its ADE-managed local files
$ ade lanes unarchive <lane> Restore an archived lane and recreate its worktree if needed
$ 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>
Reparent and stack onto a specific branch (e.g. origin/main)
Expand Down Expand Up @@ -4355,41 +4359,6 @@ function buildLanePlan(args: string[]): CliPlan {
],
};
}
if (sub === "attach") {
return {
kind: "execute",
label: "lane attach",
steps: [
actionStep(
"result",
"lane",
"attach",
collectGenericObjectArgs(args, {
worktreePath: readValue(args, ["--path"]) ?? firstPositional(args),
name: readValue(args, ["--name"]),
}),
),
],
};
}
if (sub === "adopt-attached") {
const laneId = requireValue(
readLaneId(args) ?? firstPositional(args),
"laneId",
);
return {
kind: "execute",
label: "lane adopt attached",
steps: [
actionStep(
"result",
"lane",
"adoptAttached",
collectGenericObjectArgs(args, { laneId }),
),
],
};
}
if (sub === "split-unstaged") {
return {
kind: "execute",
Expand Down Expand Up @@ -4434,19 +4403,6 @@ function buildLanePlan(args: string[]): CliPlan {
],
};
}
if (sub === "unregistered" || sub === "list-unregistered") {
return {
kind: "execute",
label: "unregistered lanes",
steps: [
actionCallStep(
"result",
"list_unregistered_lanes",
collectGenericObjectArgs(args),
),
],
};
}
return {
kind: "execute",
label: `lane ${sub}`,
Expand Down
8 changes: 5 additions & 3 deletions apps/ade-cli/src/multiProjectRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1677,9 +1677,11 @@ export function createMultiProjectRpcRequestHandler(
"projects.inspectPath requires path.",
);
}
// Deliberately uncached: desktop main's inspectProjectPathCached relies
// on lane attach/adopt IPC hooks for invalidation, which never fire in
// this long-lived daemon — a cache here could serve pre-attach results.
// Deliberately uncached: desktop main's inspectProjectPathCached is
// invalidated by lane lifecycle IPC, which never fires in this
// long-lived daemon. A worktree becomes a lane the moment `lanes.list`
// next reconciles, so a cache here would keep reporting a path as
// laneless well after it stopped being so.
return await inspectProjectPath(targetPath);
}

Expand Down
2 changes: 0 additions & 2 deletions apps/ade-cli/src/services/sync/syncHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2763,8 +2763,6 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
case "lanes.createChild":
case "lanes.createFromUnstaged":
case "lanes.importBranch":
case "lanes.attach":
case "lanes.adoptAttached":
return result && typeof result === "object"
? decorateLaneSummary(result as LaneSummary)
: result;
Expand Down
33 changes: 19 additions & 14 deletions apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ import type {
AiReviewSummaryArgs,
ApplyLaneTemplateArgs,
ArchiveLaneArgs,
AttachLaneArgs,
ChatTerminalActiveForChatArgs,
ChatTerminalListArgs,
ClosePrArgs,
Expand Down Expand Up @@ -1349,14 +1348,6 @@ function parseImportBranchArgs(value: Record<string, unknown>): ImportBranchLane
};
}

function parseAttachLaneArgs(value: Record<string, unknown>): AttachLaneArgs {
return {
name: requireString(value.name, "lanes.attach requires name."),
attachedPath: requireString(value.attachedPath, "lanes.attach requires attachedPath."),
...(asTrimmedString(value.description) ? { description: asTrimmedString(value.description)! } : {}),
};
}

function parseArchiveLaneArgs(value: Record<string, unknown>, action: string): ArchiveLaneArgs {
return {
laneId: requireString(value.laneId, `${action} requires laneId.`),
Expand Down Expand Up @@ -3585,7 +3576,10 @@ async function buildLaneListSnapshots(
autoRebaseStatus: autoRebaseByLaneId.get(lane.id) ?? null,
conflictStatus: conflictByLaneId.get(lane.id) ?? null,
stateSnapshot: stateByLaneId.get(lane.id) ?? null,
adoptableAttached: lane.laneType === "attached" && lane.archivedAt == null,
// Deprecated, always false. Shipped iOS builds decode this as a
// non-optional Bool; dropping the key blanks their lane list. See the
// field's doc comment in shared/types/lanes.ts.
adoptableAttached: false,
}));
}

Expand Down Expand Up @@ -3806,10 +3800,21 @@ function registerLaneRemoteCommands({ args, register }: RemoteCommandRegistratio
args.laneService.importBranch(parseImportBranchArgs(payload)));
register("lanes.previewBranchSwitch", { viewerAllowed: true }, async (payload) =>
args.laneService.previewBranchSwitch(parseGitCheckoutBranchArgs(payload)));
register("lanes.attach", { viewerAllowed: true, queueable: true }, async (payload) => args.laneService.attach(parseAttachLaneArgs(payload)));
register("lanes.listUnregisteredWorktrees", { viewerAllowed: true }, async () => args.laneService.listUnregisteredWorktrees());
register("lanes.adoptAttached", { viewerAllowed: true, queueable: true }, async (payload) =>
args.laneService.adoptAttached({ laneId: requireString(payload.laneId, "lanes.adoptAttached requires laneId.") }));
// Every git worktree is now a lane the moment it exists, so there is nothing
// left to select, attach, or adopt. These three stay REGISTERED (and so keep
// appearing in hello_ok.features.commandRouting.actions) because they are in
// MOBILE_SYNC_REQUIRED_REMOTE_COMMAND_ACTIONS: dropping them would flip an
// otherwise healthy host into "limited" mode for every phone, new or old.
// Instead the list returns empty — an older phone's attach sheet renders its
// own "nothing to add" state — and the two mutations fail with a message the
// phone surfaces verbatim.
register("lanes.listUnregisteredWorktrees", { viewerAllowed: true }, async () => []);
register("lanes.attach", { viewerAllowed: true, queueable: true }, async () => {
throw new Error("Attaching worktrees is no longer supported — every git worktree in the project already appears as a lane.");
});
register("lanes.adoptAttached", { viewerAllowed: true, queueable: true }, async () => {
throw new Error("Adopting attached lanes is no longer supported — every git worktree in the project is managed as a lane.");
});
register("lanes.rename", { viewerAllowed: true, queueable: true }, async (payload) => {
args.laneService.rename(parseRenameLaneArgs(payload));
return { ok: true };
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, Notification, powerMonitor, protocol, safeStorage, shell } from "electron";

Check warning on line 1 in apps/desktop/src/main/main.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

'shell' is defined but never used. Allowed unused vars must match /^_/u

if (app.isPackaged && process.env.ADE_RUNTIME_PACKAGED === undefined) {
process.env.ADE_RUNTIME_PACKAGED = "1";
Expand Down Expand Up @@ -2717,9 +2717,7 @@
logger,
projectRoot,
projectId,
baseRef,
freshProject: !hadAdeDir,
laneService,
projectConfigService,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2283,6 +2283,8 @@ describe("runtime lane snapshot actions", () => {
autoRebaseStatus,
conflictStatus,
stateSnapshot,
// Deprecated wire-compat field: always false, but still emitted so
// shipped iOS builds can decode the snapshot.
adoptableAttached: false,
},
{
Expand All @@ -2302,7 +2304,7 @@ describe("runtime lane snapshot actions", () => {
autoRebaseStatus: null,
conflictStatus: null,
stateSnapshot: null,
adoptableAttached: true,
adoptableAttached: false,
},
]);
});
Expand Down
4 changes: 0 additions & 4 deletions apps/desktop/src/main/services/adeActions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,8 @@ export const ADE_ACTION_ALLOWLIST: Partial<Record<AdeActionDomain, readonly stri
"putMachinePreferences",
],
lane: [
"adoptAttached",
"archive",
"archiveAndReclaim",
"attach",
"attachLinearIssueToSession",
"cancelDelete",
"create",
Expand Down Expand Up @@ -365,7 +363,6 @@ export const ADE_ACTION_ALLOWLIST: Partial<Record<AdeActionDomain, readonly stri
"listSnapshots",
"listRebaseSuggestions",
"listTemplates",
"listUnregisteredWorktrees",
"listLinearIssuesForLaneSessions",
"listLinearIssuesForSession",
"linkLinearIssues",
Expand Down Expand Up @@ -688,7 +685,6 @@ export const ADE_ACTION_ALLOWLIST: Partial<Record<AdeActionDomain, readonly stri
onboarding: [
"complete",
"detectDefaults",
"detectExistingLanes",
"getStatus",
"markGlossaryTermSeen",
"setDismissed",
Expand Down
Loading
Loading