From 7966a7ef5d9fb39b8642b63f147b2b0e3fc8f0dc Mon Sep 17 00:00:00 2001
From: Arul Sharma <31745423+arul28@users.noreply.github.com>
Date: Mon, 20 Apr 2026 12:08:49 -0400
Subject: [PATCH 1/6] Deepen ADE action audit with broader domains and live
status polling
---
apps/mcp-server/src/mcpServer.test.ts | 208 ++++++++-
apps/mcp-server/src/mcpServer.ts | 598 +++++++++++++++++++++++++-
2 files changed, 799 insertions(+), 7 deletions(-)
diff --git a/apps/mcp-server/src/mcpServer.test.ts b/apps/mcp-server/src/mcpServer.test.ts
index 09dbea60e..fd18bed0a 100644
--- a/apps/mcp-server/src/mcpServer.test.ts
+++ b/apps/mcp-server/src/mcpServer.test.ts
@@ -131,6 +131,7 @@ function createRuntime() {
},
laneService: {
list: vi.fn(async () => laneRows),
+ 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;
@@ -151,6 +152,12 @@ function createRuntime() {
branchRef: "feature/lane-new",
worktreePath: "/tmp/project/.ade/worktrees/lane-new"
})),
+ importBranch: vi.fn(async ({ branchRef, name }: { branchRef: string; name?: string }) => ({
+ ...laneRows[0],
+ id: "lane-imported",
+ name: name ?? "Imported lane",
+ branchRef,
+ })),
delete: vi.fn(async () => {})
},
sessionService: {
@@ -159,7 +166,8 @@ function createRuntime() {
},
operationService: {
start: operationStart,
- finish: operationFinish
+ finish: operationFinish,
+ list: vi.fn(() => [{ id: "op-1", kind: "git_push", status: "running" }]),
},
projectConfigService: {} as any,
conflictService: {
@@ -172,7 +180,14 @@ function createRuntime() {
getConflictState: vi.fn(async () => ({ laneId: "lane-1", kind: null, inProgress: false, conflictedFiles: [], canContinue: false, canAbort: false })),
stageAll: vi.fn(async () => ({ success: true })),
commit: vi.fn(async () => ({ success: true })),
+ generateCommitMessage: vi.fn(async () => ({ message: "generated commit message", model: "gpt-5-mini" })),
listRecentCommits: vi.fn(async () => [{ sha: "abc123", subject: "test" }]),
+ getSyncStatus: vi.fn(async () => ({ ahead: 1, behind: 0, tracking: true })),
+ fetch: vi.fn(async () => ({ success: true })),
+ pull: vi.fn(async () => ({ success: true })),
+ push: vi.fn(async () => ({ success: true })),
+ listBranches: vi.fn(async () => [{ name: "main", current: true, ahead: 0, behind: 0, hasUpstream: true, upstream: "origin/main" }]),
+ checkoutBranch: vi.fn(async () => ({ success: true })),
stashPush: vi.fn(async () => ({ success: true })),
listStashes: vi.fn(async () => [{ ref: "stash@{0}", createdAt: "2026-04-06T00:00:00.000Z", subject: "test stash" }]),
stashApply: vi.fn(async () => ({ success: true })),
@@ -294,6 +309,7 @@ function createRuntime() {
simulateIntegration: vi.fn(async () => ({ steps: [], conflicts: [], clean: true })),
createQueuePrs: vi.fn(async () => ({ groupId: "group-1", prs: [] })),
createIntegrationPr: vi.fn(async () => ({ prId: "pr-int-1", url: "https://github.com/pr/1" })),
+ createFromLane: vi.fn(async () => ({ id: "pr-new", laneId: "lane-1", title: "New PR", status: "open" })),
getPrHealth: vi.fn(async (prId: string) => ({ prId, healthy: true, checks: "pass", reviews: "approved" })),
landQueueNext: vi.fn(async () => ({ landed: true, prId: "pr-1", sha: "def456" })),
getChecks: vi.fn(async () => [
@@ -399,6 +415,9 @@ function createRuntime() {
updatedAt: "2026-03-17T19:00:00.000Z",
})),
resolveReviewThread: vi.fn(async () => undefined),
+ updateTitle: vi.fn(async () => undefined),
+ updateBody: vi.fn(async () => undefined),
+ addComment: vi.fn(async ({ body }: { body: string }) => ({ id: "comment-new", body })),
},
agentChatService: {
listSessions: vi.fn(async () => [
@@ -3019,7 +3038,194 @@ describe("mcpServer", () => {
expect(response?.isError).toBeUndefined();
expect(fixture.runtime.gitService.stageAll).toHaveBeenCalledTimes(1);
expect(fixture.runtime.gitService.commit).toHaveBeenCalledTimes(1);
+ expect(fixture.runtime.gitService.generateCommitMessage).not.toHaveBeenCalled();
expect(response.structuredContent.commit.sha).toBe("abc123");
+ expect(response.structuredContent.messageSource).toBe("provided");
+ });
+
+ it("generates a commit message when commit_changes message is omitted", async () => {
+ const fixture = createRuntime();
+ const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+
+ await initialize(handler, { callerId: "agent-1", role: "agent" });
+
+ const response = await callTool(handler, "commit_changes", {
+ laneId: "lane-1",
+ });
+
+ expect(response?.isError).toBeUndefined();
+ expect(fixture.runtime.gitService.generateCommitMessage).toHaveBeenCalledWith({
+ laneId: "lane-1",
+ amend: false,
+ });
+ expect(fixture.runtime.gitService.commit).toHaveBeenCalledWith({
+ laneId: "lane-1",
+ amend: false,
+ message: "generated commit message",
+ });
+ expect(response.structuredContent.messageSource).toBe("generated");
+ expect(response.structuredContent.generatedByModel).toBe("gpt-5-mini");
+ });
+
+ it("returns generated commit text without creating a commit", async () => {
+ const fixture = createRuntime();
+ const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+
+ await initialize(handler, { callerId: "agent-1", role: "agent" });
+
+ const response = await callTool(handler, "generate_commit_message", {
+ laneId: "lane-1",
+ amend: true,
+ });
+
+ expect(response?.isError).toBeUndefined();
+ expect(fixture.runtime.gitService.generateCommitMessage).toHaveBeenCalledWith({
+ laneId: "lane-1",
+ amend: true,
+ });
+ expect(fixture.runtime.gitService.commit).not.toHaveBeenCalled();
+ expect(response.structuredContent.message).toBe("generated commit message");
+ });
+
+ it("lists and imports unregistered lane worktrees", async () => {
+ const fixture = createRuntime();
+ const handler = createMcpRequestHandler({ 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",
+ baseBranch: "main",
+ });
+ expect(importResponse?.isError).toBeUndefined();
+ expect(fixture.runtime.laneService.importBranch).toHaveBeenCalledWith({
+ branchRef: "feature/untracked",
+ name: "Imported lane",
+ baseBranch: "main",
+ });
+ expect(importResponse.structuredContent.lane.id).toBe("lane-imported");
+ });
+
+ it("supports core git sync operations via MCP", async () => {
+ const fixture = createRuntime();
+ const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ await initialize(handler, { callerId: "agent-1", role: "agent" });
+
+ const syncStatus = await callTool(handler, "git_get_sync_status", { laneId: "lane-1" });
+ expect(syncStatus?.isError).toBeUndefined();
+ expect(fixture.runtime.gitService.getSyncStatus).toHaveBeenCalledWith({ laneId: "lane-1" });
+
+ const push = await callTool(handler, "git_push", { laneId: "lane-1", force: true, setUpstream: false });
+ expect(push?.isError).toBeUndefined();
+ expect(fixture.runtime.gitService.push).toHaveBeenCalledWith({ laneId: "lane-1", force: true, setUpstream: false });
+ });
+
+ it("supports create/update/comment PR actions via MCP", async () => {
+ const fixture = createRuntime();
+ const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ await initialize(handler, { callerId: "agent-1", role: "agent" });
+
+ const created = await callTool(handler, "create_pr_from_lane", {
+ laneId: "lane-1",
+ baseBranch: "main",
+ title: "My PR",
+ body: "Body text",
+ draft: true,
+ });
+ expect(created?.isError).toBeUndefined();
+ expect(fixture.runtime.prService.createFromLane).toHaveBeenCalledWith({
+ laneId: "lane-1",
+ baseBranch: "main",
+ title: "My PR",
+ body: "Body text",
+ draft: true,
+ });
+
+ const updateTitle = await callTool(handler, "pr_update_title", { prId: "pr-1", title: "Renamed" });
+ expect(updateTitle?.isError).toBeUndefined();
+ expect(fixture.runtime.prService.updateTitle).toHaveBeenCalledWith({ prId: "pr-1", title: "Renamed" });
+
+ const comment = await callTool(handler, "pr_add_comment", { prId: "pr-1", body: "Looks good" });
+ expect(comment?.isError).toBeUndefined();
+ expect(fixture.runtime.prService.addComment).toHaveBeenCalledWith({ prId: "pr-1", body: "Looks good" });
+ });
+
+ it("lists ADE actions across runtime domains", async () => {
+ const fixture = createRuntime();
+ const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ await initialize(handler, { callerId: "agent-1", role: "agent" });
+
+ const response = await callTool(handler, "list_ade_actions", { domain: "git" });
+ expect(response?.isError).toBeUndefined();
+ expect(response.structuredContent.actions.some((entry: { action: string }) => entry.action === "push")).toBe(true);
+ expect(response.structuredContent.actions.some((entry: { action: string }) => entry.action === "commit")).toBe(true);
+
+ const allDomains = await callTool(handler, "list_ade_actions", { domain: "all" });
+ expect(allDomains?.isError).toBeUndefined();
+ expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "memory")).toBe(true);
+ expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "computer_use_artifacts")).toBe(true);
+ expect(allDomains.structuredContent.actions.some((entry: { domain: string }) => entry.domain === "operation")).toBe(true);
+ });
+
+ it("invokes ADE actions dynamically and returns status hints", async () => {
+ const fixture = createRuntime();
+ const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ await initialize(handler, { callerId: "agent-1", role: "agent" });
+
+ const response = await callTool(handler, "run_ade_action", {
+ domain: "git",
+ action: "push",
+ args: { laneId: "lane-1", force: true, setUpstream: false },
+ });
+ expect(response?.isError).toBeUndefined();
+ expect(fixture.runtime.gitService.push).toHaveBeenCalledWith({ laneId: "lane-1", force: true, setUpstream: false });
+ expect(response.structuredContent.domain).toBe("git");
+ expect(response.structuredContent.action).toBe("push");
+
+ const variadic = await callTool(handler, "run_ade_action", {
+ domain: "operation",
+ action: "list",
+ argsList: [{ limit: 10 }],
+ });
+ expect(variadic?.isError).toBeUndefined();
+ expect(fixture.runtime.operationService.list).toHaveBeenCalledWith({ limit: 10 });
+ });
+
+ it("reads ADE action status snapshots across operation/test/chat/mission/run", async () => {
+ const fixture = createRuntime();
+ const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ await initialize(handler, { callerId: "agent-1", role: "agent", runId: "run-1", missionId: "mission-1" });
+
+ const response = await callTool(handler, "get_ade_action_status", {
+ operationId: "op-1",
+ testRunId: "test-run-1",
+ chatSessionId: "chat-1",
+ runId: "run-1",
+ missionId: "mission-1",
+ prId: "pr-1",
+ });
+ expect(response?.isError).toBeUndefined();
+ expect(response.structuredContent.operation.id).toBe("op-1");
+ expect(response.structuredContent.testRun.id).toBe("test-run-1");
+ expect(response.structuredContent.chatSession.sessionId).toBe("chat-1");
+ expect(response.structuredContent.runGraph.run.id).toBe("run-1");
+ expect(response.structuredContent.mission.id).toBe("mission-1");
+ expect(response.structuredContent.pr.health.prId).toBe("pr-1");
+ expect(typeof response.structuredContent.hash).toBe("string");
+ expect(response.structuredContent.changed).toBe(true);
+
+ const unchanged = await callTool(handler, "get_ade_action_status", {
+ operationId: "op-1",
+ previousHash: response.structuredContent.hash,
+ waitForMs: 0,
+ });
+ expect(unchanged?.isError).toBeUndefined();
+ expect(unchanged.structuredContent.changed).toBe(false);
});
it("lets agent callers stash lane changes", async () => {
diff --git a/apps/mcp-server/src/mcpServer.ts b/apps/mcp-server/src/mcpServer.ts
index b4adbf475..4334dc1ff 100644
--- a/apps/mcp-server/src/mcpServer.ts
+++ b/apps/mcp-server/src/mcpServer.ts
@@ -154,6 +154,116 @@ const TOOL_SPECS: ToolSpec[] = [
}
}
},
+ {
+ name: "list_ade_actions",
+ description: "List callable ADE action methods across core runtime services (lane/git/pr/tests/chat/mission/orchestrator).",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ domain: {
+ type: "string",
+ enum: [
+ "lane",
+ "git",
+ "diff",
+ "conflicts",
+ "pr",
+ "tests",
+ "chat",
+ "mission",
+ "orchestrator",
+ "orchestrator_core",
+ "memory",
+ "cto_state",
+ "worker_agent",
+ "session",
+ "operation",
+ "project_config",
+ "issue_inventory",
+ "flow_policy",
+ "linear_dispatcher",
+ "linear_issue_tracker",
+ "linear_sync",
+ "linear_ingress",
+ "linear_routing",
+ "file",
+ "process",
+ "external_mcp",
+ "computer_use_artifacts",
+ "all"
+ ],
+ default: "all",
+ },
+ }
+ }
+ },
+ {
+ name: "run_ade_action",
+ description: "Invoke any ADE action by domain and action name. Use args for object-style calls, or arg for scalar-style calls.",
+ inputSchema: {
+ type: "object",
+ required: ["domain", "action"],
+ additionalProperties: false,
+ properties: {
+ domain: {
+ type: "string",
+ enum: [
+ "lane",
+ "git",
+ "diff",
+ "conflicts",
+ "pr",
+ "tests",
+ "chat",
+ "mission",
+ "orchestrator",
+ "orchestrator_core",
+ "memory",
+ "cto_state",
+ "worker_agent",
+ "session",
+ "operation",
+ "project_config",
+ "issue_inventory",
+ "flow_policy",
+ "linear_dispatcher",
+ "linear_issue_tracker",
+ "linear_sync",
+ "linear_ingress",
+ "linear_routing",
+ "file",
+ "process",
+ "external_mcp",
+ "computer_use_artifacts",
+ ],
+ },
+ action: { type: "string", minLength: 1 },
+ args: { type: "object" },
+ argsList: { type: "array" },
+ arg: {},
+ }
+ }
+ },
+ {
+ name: "get_ade_action_status",
+ description: "Check status/progress for long-running ADE actions by operation/test/chat/run/mission identifiers.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ operationId: { type: "string", minLength: 1 },
+ testRunId: { type: "string", minLength: 1 },
+ chatSessionId: { type: "string", minLength: 1 },
+ runId: { type: "string", minLength: 1 },
+ missionId: { type: "string", minLength: 1 },
+ prId: { type: "string", minLength: 1 },
+ previousHash: { type: "string" },
+ waitForMs: { type: "number", minimum: 0, maximum: 120000, default: 0 },
+ pollIntervalMs: { type: "number", minimum: 100, maximum: 5000, default: 800 },
+ }
+ }
+ },
{
name: "check_conflicts",
description: "Run conflict prediction against one lane or a lane set.",
@@ -529,12 +639,105 @@ 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.",
+ inputSchema: {
+ type: "object",
+ required: ["branchRef"],
+ additionalProperties: false,
+ properties: {
+ branchRef: { type: "string", minLength: 1 },
+ name: { type: "string" },
+ description: { type: "string" },
+ baseBranch: { type: "string" }
+ }
+ }
+ },
+ {
+ name: "git_get_sync_status",
+ description: "Read upstream sync status for a lane branch.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ laneId: { type: "string", minLength: 1 }
+ }
+ }
+ },
+ {
+ name: "git_fetch",
+ description: "Fetch remote refs for a lane.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ laneId: { type: "string", minLength: 1 }
+ }
+ }
+ },
+ {
+ name: "git_pull",
+ description: "Pull remote changes into a lane.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ laneId: { type: "string", minLength: 1 }
+ }
+ }
+ },
+ {
+ name: "git_push",
+ description: "Push lane branch commits to remote.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ laneId: { type: "string", minLength: 1 },
+ force: { type: "boolean", default: false },
+ setUpstream: { type: "boolean", default: true }
+ }
+ }
+ },
+ {
+ name: "git_list_branches",
+ description: "List branches visible from a lane checkout.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ laneId: { type: "string", minLength: 1 }
+ }
+ }
+ },
+ {
+ name: "git_checkout_branch",
+ description: "Checkout an existing branch in a lane checkout.",
+ inputSchema: {
+ type: "object",
+ required: ["branchName"],
+ additionalProperties: false,
+ properties: {
+ laneId: { type: "string", minLength: 1 },
+ branchName: { type: "string", minLength: 1 }
+ }
+ }
+ },
{
name: "commit_changes",
- description: "Stage and commit lane changes with a provided message.",
+ description: "Stage and commit lane changes. If message is omitted, ADE generates one with the configured Commit Messages model.",
inputSchema: {
type: "object",
- required: ["message"],
additionalProperties: false,
properties: {
laneId: { type: "string", minLength: 1 },
@@ -544,6 +747,18 @@ const TOOL_SPECS: ToolSpec[] = [
}
}
},
+ {
+ name: "generate_commit_message",
+ description: "Generate a commit message for a lane using ADE's Commit Messages model settings.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ laneId: { type: "string", minLength: 1 },
+ amend: { type: "boolean", default: false }
+ }
+ }
+ },
{
name: "stash_push",
description: "Stash lane changes so rebase or inspection can proceed cleanly. Defaults to the current chat lane when laneId is omitted.",
@@ -713,6 +928,61 @@ const TOOL_SPECS: ToolSpec[] = [
}
}
},
+ {
+ name: "create_pr_from_lane",
+ description: "Create a PR from a lane branch.",
+ inputSchema: {
+ type: "object",
+ required: ["laneId", "baseBranch", "title"],
+ additionalProperties: false,
+ properties: {
+ laneId: { type: "string", minLength: 1 },
+ baseBranch: { type: "string", minLength: 1 },
+ title: { type: "string", minLength: 1 },
+ body: { type: "string" },
+ draft: { type: "boolean", default: false },
+ }
+ }
+ },
+ {
+ name: "pr_update_title",
+ description: "Update a PR title.",
+ inputSchema: {
+ type: "object",
+ required: ["prId", "title"],
+ additionalProperties: false,
+ properties: {
+ prId: { type: "string", minLength: 1 },
+ title: { type: "string", minLength: 1 },
+ }
+ }
+ },
+ {
+ name: "pr_update_body",
+ description: "Update PR body/description markdown.",
+ inputSchema: {
+ type: "object",
+ required: ["prId", "body"],
+ additionalProperties: false,
+ properties: {
+ prId: { type: "string", minLength: 1 },
+ body: { type: "string" },
+ }
+ }
+ },
+ {
+ name: "pr_add_comment",
+ description: "Add a top-level comment to a PR.",
+ inputSchema: {
+ type: "object",
+ required: ["prId", "body"],
+ additionalProperties: false,
+ properties: {
+ prId: { type: "string", minLength: 1 },
+ body: { type: "string", minLength: 1 },
+ }
+ }
+ },
{
name: "get_pr_health",
description: "Get combined health status for a PR including checks, reviews, conflicts, and rebase status",
@@ -1565,9 +1835,15 @@ const COORDINATOR_TOOL_NAMES = new Set(COORDINATOR_TOOL_SPECS.map((tool) => tool
const READ_ONLY_TOOLS = new Set([
"check_conflicts",
+ "list_ade_actions",
+ "get_ade_action_status",
"get_lane_status",
"get_lane_conflict_state",
"list_lanes",
+ "list_unregistered_lanes",
+ "git_get_sync_status",
+ "git_list_branches",
+ "generate_commit_message",
"list_stashes",
"simulate_integration",
"get_pr_health",
@@ -1595,7 +1871,13 @@ const READ_ONLY_TOOLS = new Set([
const MUTATION_TOOLS = new Set([
"create_lane",
+ "run_ade_action",
+ "import_lane",
"merge_lane",
+ "git_fetch",
+ "git_pull",
+ "git_push",
+ "git_checkout_branch",
"commit_changes",
"stash_push",
"stash_apply",
@@ -1605,6 +1887,10 @@ const MUTATION_TOOLS = new Set([
"run_tests",
"create_queue",
"create_integration",
+ "create_pr_from_lane",
+ "pr_update_title",
+ "pr_update_body",
+ "pr_add_comment",
"rebase_lane",
"rebase_continue",
"rebase_abort",
@@ -2748,6 +3034,73 @@ async function waitForTestRunCompletion(args: {
};
}
+type AdeActionDomain =
+ | "lane"
+ | "git"
+ | "diff"
+ | "conflicts"
+ | "pr"
+ | "tests"
+ | "chat"
+ | "mission"
+ | "orchestrator"
+ | "orchestrator_core"
+ | "memory"
+ | "cto_state"
+ | "worker_agent"
+ | "session"
+ | "operation"
+ | "project_config"
+ | "issue_inventory"
+ | "flow_policy"
+ | "linear_dispatcher"
+ | "linear_issue_tracker"
+ | "linear_sync"
+ | "linear_ingress"
+ | "linear_routing"
+ | "file"
+ | "process"
+ | "external_mcp"
+ | "computer_use_artifacts";
+
+function getAdeActionDomainServices(runtime: AdeMcpRuntime): Partial | null | undefined>> {
+ return {
+ lane: runtime.laneService as unknown as Record,
+ git: runtime.gitService as unknown as Record,
+ diff: runtime.diffService as unknown as Record,
+ conflicts: runtime.conflictService as unknown as Record,
+ pr: (runtime.prService ?? null) as unknown as Record | null,
+ tests: runtime.testService as unknown as Record,
+ chat: (runtime.agentChatService ?? null) as unknown as Record | null,
+ mission: runtime.missionService as unknown as Record,
+ orchestrator: runtime.aiOrchestratorService as unknown as Record,
+ orchestrator_core: runtime.orchestratorService as unknown as Record,
+ memory: runtime.memoryService as unknown as Record,
+ cto_state: runtime.ctoStateService as unknown as Record,
+ worker_agent: runtime.workerAgentService as unknown as Record,
+ session: runtime.sessionService as unknown as Record,
+ operation: runtime.operationService as unknown as Record,
+ project_config: runtime.projectConfigService as unknown as Record,
+ issue_inventory: runtime.issueInventoryService as unknown as Record,
+ flow_policy: (runtime.flowPolicyService ?? null) as unknown as Record | null,
+ linear_dispatcher: (runtime.linearDispatcherService ?? null) as unknown as Record | null,
+ linear_issue_tracker: (runtime.linearIssueTracker ?? null) as unknown as Record | null,
+ linear_sync: (runtime.linearSyncService ?? null) as unknown as Record | null,
+ linear_ingress: (runtime.linearIngressService ?? null) as unknown as Record | null,
+ linear_routing: (runtime.linearRoutingService ?? null) as unknown as Record | null,
+ file: (runtime.fileService ?? null) as unknown as Record | null,
+ process: (runtime.processService ?? null) as unknown as Record | null,
+ external_mcp: runtime.externalMcpService as unknown as Record,
+ computer_use_artifacts: runtime.computerUseArtifactBrokerService as unknown as Record,
+ };
+}
+
+function listAdeActionNames(service: Record): string[] {
+ return Object.keys(service)
+ .filter((key) => typeof service[key] === "function" && !key.startsWith("_"))
+ .sort((a, b) => a.localeCompare(b));
+}
+
async function waitForSessionCompletion(args: {
runtime: AdeMcpRuntime;
ptyId: string;
@@ -3851,6 +4204,125 @@ async function runTool(args: {
return await runCoordinatorTool({ runtime, name, toolArgs, callerCtx });
}
+ if (name === "list_ade_actions") {
+ const domain = asOptionalTrimmedString(toolArgs.domain) ?? "all";
+ const services = getAdeActionDomainServices(runtime);
+ const domains = domain === "all"
+ ? (Object.keys(services) as AdeActionDomain[])
+ : [domain as AdeActionDomain];
+ const actions = domains.flatMap((entry) => {
+ const service = services[entry];
+ if (!service) return [];
+ return listAdeActionNames(service).map((action) => ({
+ domain: entry,
+ action,
+ }));
+ });
+ return {
+ count: actions.length,
+ actions,
+ };
+ }
+
+ if (name === "run_ade_action") {
+ const domain = assertNonEmptyString(toolArgs.domain, "domain") as AdeActionDomain;
+ const action = assertNonEmptyString(toolArgs.action, "action");
+ const services = getAdeActionDomainServices(runtime);
+ const service = services[domain];
+ if (!service) {
+ throw new JsonRpcError(JsonRpcErrorCode.invalidParams, `Domain '${domain}' is unavailable in this runtime.`);
+ }
+ const callable = service[action];
+ if (typeof callable !== "function") {
+ throw new JsonRpcError(JsonRpcErrorCode.invalidParams, `Action '${domain}.${action}' is not callable.`);
+ }
+ const argsList = Array.isArray(toolArgs.argsList) ? toolArgs.argsList : null;
+ const hasScalarArg = Object.prototype.hasOwnProperty.call(toolArgs, "arg");
+ const rawObjectArgs = safeObject(toolArgs.args);
+ const result = argsList
+ ? await (callable as (...params: unknown[]) => Promise)(...argsList)
+ : hasScalarArg
+ ? await (callable as (arg: unknown) => Promise)(toolArgs.arg)
+ : await (callable as (args?: Record) => Promise)(
+ Object.keys(rawObjectArgs).length > 0 ? rawObjectArgs : undefined
+ );
+ const record = isRecord(result) ? result : null;
+ const statusHints = {
+ operationId: typeof record?.operationId === "string" ? record.operationId : null,
+ testRunId: typeof record?.id === "string" && domain === "tests" ? record.id : null,
+ chatSessionId: typeof record?.sessionId === "string" ? record.sessionId : null,
+ runId: typeof record?.runId === "string" ? record.runId : null,
+ missionId: typeof record?.missionId === "string" ? record.missionId : null,
+ };
+ return {
+ domain,
+ action,
+ result,
+ statusHints,
+ };
+ }
+
+ if (name === "get_ade_action_status") {
+ const operationId = asOptionalTrimmedString(toolArgs.operationId);
+ const testRunId = asOptionalTrimmedString(toolArgs.testRunId);
+ const chatSessionId = asOptionalTrimmedString(toolArgs.chatSessionId);
+ const runId = asOptionalTrimmedString(toolArgs.runId);
+ const missionId = asOptionalTrimmedString(toolArgs.missionId);
+ const prId = asOptionalTrimmedString(toolArgs.prId);
+ const previousHash = asOptionalTrimmedString(toolArgs.previousHash);
+ const waitForMs = Math.max(0, Math.min(120_000, Math.floor(asNumber(toolArgs.waitForMs, 0))));
+ const pollIntervalMs = Math.max(100, Math.min(5_000, Math.floor(asNumber(toolArgs.pollIntervalMs, 800))));
+
+ const collectStatusPayload = async (): Promise> => {
+ const payload: Record = {};
+ if (operationId) {
+ const operation = runtime.operationService.list({ limit: 500 }).find((entry) => entry.id === operationId) ?? null;
+ payload.operation = operation;
+ }
+ if (testRunId) {
+ const run = runtime.testService.listRuns({ limit: 200 }).find((entry) => entry.id === testRunId) ?? null;
+ payload.testRun = run;
+ if (run) payload.testRunLogTail = runtime.testService.getLogTail(testRunId, 16_000);
+ }
+ if (chatSessionId && runtime.agentChatService) {
+ payload.chatSession = await runtime.agentChatService.getSessionSummary(chatSessionId);
+ }
+ if (runId) {
+ payload.runGraph = runtime.orchestratorService.getRunGraph({ runId, timelineLimit: 150 });
+ }
+ if (missionId) {
+ payload.mission = runtime.missionService.get(missionId);
+ }
+ if (prId && runtime.prService) {
+ payload.pr = {
+ health: await runtime.prService.getPrHealth(prId),
+ checks: await runtime.prService.getChecks(prId),
+ reviews: await runtime.prService.getReviews(prId),
+ };
+ }
+ return payload;
+ };
+
+ const hashPayload = (payload: Record): string =>
+ createHash("sha256").update(JSON.stringify(payload)).digest("hex");
+
+ let payload = await collectStatusPayload();
+ let hash = hashPayload(payload);
+ if (previousHash && waitForMs > 0 && hash === previousHash) {
+ const deadline = Date.now() + waitForMs;
+ while (Date.now() < deadline && hash === previousHash) {
+ await sleep(pollIntervalMs);
+ payload = await collectStatusPayload();
+ hash = hashPayload(payload);
+ }
+ }
+ return {
+ ...payload,
+ hash,
+ changed: previousHash ? hash !== previousHash : true,
+ };
+ }
+
if (name === "list_lanes") {
const includeArchived = asBoolean(toolArgs.includeArchived, false);
const lanes = await runtime.laneService.list({ includeArchived });
@@ -3859,6 +4331,11 @@ 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);
@@ -3882,6 +4359,19 @@ async function runTool(args: {
};
}
+ if (name === "import_lane") {
+ const branchRef = assertNonEmptyString(toolArgs.branchRef, "branchRef");
+ const imported = await runtime.laneService.importBranch({
+ branchRef,
+ ...(asOptionalTrimmedString(toolArgs.name) ? { name: asOptionalTrimmedString(toolArgs.name)! } : {}),
+ ...(asOptionalTrimmedString(toolArgs.description) ? { description: asOptionalTrimmedString(toolArgs.description)! } : {}),
+ ...(asOptionalTrimmedString(toolArgs.baseBranch) ? { baseBranch: asOptionalTrimmedString(toolArgs.baseBranch)! } : {}),
+ });
+ return {
+ lane: mapLaneSummary(imported as unknown as Record),
+ };
+ }
+
if (name === "check_conflicts") {
const laneId = asOptionalTrimmedString(toolArgs.laneId);
const laneIds = Array.isArray(toolArgs.laneIds)
@@ -4812,11 +5302,47 @@ async function runTool(args: {
};
}
- if (name === "commit_changes") {
- const laneId = requireLaneIdForTool(runtime, session, toolArgs, "commit_changes");
+ if (name === "git_get_sync_status") {
+ const laneId = requireLaneIdForTool(runtime, session, toolArgs, "git_get_sync_status");
+ const status = await runtime.gitService.getSyncStatus({ laneId });
+ return { laneId, status };
+ }
+
+ if (name === "git_fetch") {
+ const laneId = requireLaneIdForTool(runtime, session, toolArgs, "git_fetch");
+ const action = await runtime.gitService.fetch({ laneId });
+ return { laneId, action };
+ }
+ if (name === "git_pull") {
+ const laneId = requireLaneIdForTool(runtime, session, toolArgs, "git_pull");
+ const action = await runtime.gitService.pull({ laneId });
+ return { laneId, action };
+ }
- const message = assertNonEmptyString(toolArgs.message, "message");
+ if (name === "git_push") {
+ const laneId = requireLaneIdForTool(runtime, session, toolArgs, "git_push");
+ const force = asBoolean(toolArgs.force, false);
+ const setUpstream = asBoolean(toolArgs.setUpstream, true);
+ const action = await runtime.gitService.push({ laneId, force, setUpstream });
+ return { laneId, action };
+ }
+
+ if (name === "git_list_branches") {
+ const laneId = requireLaneIdForTool(runtime, session, toolArgs, "git_list_branches");
+ const branches = await runtime.gitService.listBranches({ laneId });
+ return { laneId, branches };
+ }
+
+ if (name === "git_checkout_branch") {
+ const laneId = requireLaneIdForTool(runtime, session, toolArgs, "git_checkout_branch");
+ const branchName = assertNonEmptyString(toolArgs.branchName, "branchName");
+ const action = await runtime.gitService.checkoutBranch({ laneId, branchName });
+ return { laneId, branchName, action };
+ }
+
+ if (name === "commit_changes") {
+ const laneId = requireLaneIdForTool(runtime, session, toolArgs, "commit_changes");
const amend = asBoolean(toolArgs.amend, false);
const stageAll = asBoolean(toolArgs.stageAll, true);
@@ -4824,12 +5350,35 @@ async function runTool(args: {
await runtime.gitService.stageAll({ laneId, paths: [] });
}
+ const explicitMessage = asOptionalTrimmedString(toolArgs.message);
+ const generated = explicitMessage
+ ? null
+ : await runtime.gitService.generateCommitMessage({ laneId, amend });
+ const message = explicitMessage ?? generated?.message ?? "";
+ if (!message.trim().length) {
+ throw new JsonRpcError(JsonRpcErrorCode.toolFailed, "Commit message is empty after generation.");
+ }
+
const action = await runtime.gitService.commit({ laneId, message, amend });
const latest = await runtime.gitService.listRecentCommits({ laneId, limit: 1 });
return {
action,
- commit: latest[0] ?? null
+ commit: latest[0] ?? null,
+ message,
+ messageSource: explicitMessage ? "provided" : "generated",
+ ...(generated?.model ? { generatedByModel: generated.model } : {})
+ };
+ }
+
+ if (name === "generate_commit_message") {
+ const laneId = requireLaneIdForTool(runtime, session, toolArgs, "generate_commit_message");
+ const amend = asBoolean(toolArgs.amend, false);
+ const result = await runtime.gitService.generateCommitMessage({ laneId, amend });
+ return {
+ laneId,
+ amend,
+ ...result,
};
}
@@ -4978,6 +5527,43 @@ async function runTool(args: {
return result;
}
+ if (name === "create_pr_from_lane") {
+ const laneId = assertNonEmptyString(toolArgs.laneId, "laneId");
+ const baseBranch = assertNonEmptyString(toolArgs.baseBranch, "baseBranch");
+ const title = assertNonEmptyString(toolArgs.title, "title");
+ const body = asOptionalTrimmedString(toolArgs.body);
+ const draft = asBoolean(toolArgs.draft, false);
+ const pr = await requirePrService(runtime).createFromLane({
+ laneId,
+ baseBranch,
+ title,
+ ...(body ? { body } : {}),
+ draft,
+ });
+ return { pr };
+ }
+
+ if (name === "pr_update_title") {
+ const prId = assertNonEmptyString(toolArgs.prId, "prId");
+ const title = assertNonEmptyString(toolArgs.title, "title");
+ await requirePrService(runtime).updateTitle({ prId, title });
+ return { success: true, prId, title };
+ }
+
+ if (name === "pr_update_body") {
+ const prId = assertNonEmptyString(toolArgs.prId, "prId");
+ const body = typeof toolArgs.body === "string" ? toolArgs.body : "";
+ await requirePrService(runtime).updateBody({ prId, body });
+ return { success: true, prId };
+ }
+
+ if (name === "pr_add_comment") {
+ const prId = assertNonEmptyString(toolArgs.prId, "prId");
+ const body = assertNonEmptyString(toolArgs.body, "body");
+ const comment = await requirePrService(runtime).addComment({ prId, body });
+ return { success: true, comment };
+ }
+
if (name === "get_pr_health") {
const prId = assertNonEmptyString(toolArgs.prId, "prId");
const prSvc = requirePrService(runtime);
From 6b38b229771d4bd4f0bef0cd5292aaca1d08c003 Mon Sep 17 00:00:00 2001
From: Arul Sharma <31745423+arul28@users.noreply.github.com>
Date: Mon, 20 Apr 2026 21:21:17 -0400
Subject: [PATCH 2/6] Finalize MCP removal / ade CLI cutover
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Polish Mintlify docs around the new `ade` CLI + JSON-RPC surface
(ai-tools, chat, key-concepts, missions/budget, context-packs,
cto/workers) and drop references to the retired External MCP UI.
- Clean up residual scaffolding left behind by the MCP→ade-cli rewrite:
drop the unused `normalizeStartupCommand` helper in `ptyService.ts`
and fix a pre-existing `vi.fn` tuple-typing error in
`workerAdapterRuntimeService.test.ts`.
- Tidy small indent/whitespace drift in `CtoPage.tsx` and
`WorkerPermissionsEditor.tsx` left by the External MCP UI deletion.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.ade/.gitignore | 2 +-
.ade/cto/identity.yaml | 4 -
.github/workflows/ci.yml | 52 +-
.gitignore | 2 +-
AGENTS.md | 12 +-
CHANGELOG.md | 10 +-
README.md | 21 +-
ai-tools/claude-code.mdx | 20 +-
ai-tools/cursor.mdx | 4 +-
ai-tools/windsurf.mdx | 2 +-
apps/ade-cli/README.md | 115 +
.../{mcp-server => ade-cli}/package-lock.json | 10 +-
apps/{mcp-server => ade-cli}/package.json | 19 +-
apps/ade-cli/scripts/verify-built-cli.mjs | 37 +
.../src/adeRpcServer.test.ts} | 420 ++-
.../src/adeRpcServer.ts} | 477 +--
.../src/bootstrap.test.ts | 0
apps/{mcp-server => ade-cli}/src/bootstrap.ts | 66 +-
apps/ade-cli/src/cli.test.ts | 251 ++
apps/ade-cli/src/cli.ts | 2622 +++++++++++++++++
.../src/headlessLinearServices.test.ts | 1 -
.../src/headlessLinearServices.ts | 16 +-
apps/{mcp-server => ade-cli}/src/jsonrpc.ts | 7 +-
.../{mcp-server => ade-cli}/src/test/setup.ts | 2 +-
.../src/types/node-sqlite.d.ts | 0
apps/{mcp-server => ade-cli}/tsconfig.json | 0
apps/{mcp-server => ade-cli}/tsup.config.ts | 2 +-
apps/{mcp-server => ade-cli}/vitest.config.ts | 0
apps/desktop/package-lock.json | 847 +-----
apps/desktop/package.json | 30 +-
apps/desktop/scripts/ade-cli-install-path.sh | 28 +
apps/desktop/scripts/ade-cli-macos-wrapper.sh | 50 +
.../scripts/after-pack-runtime-fixes.cjs | 15 +-
.../scripts/extractKvDbBootstrapSql.mjs | 2 +-
.../scripts/validate-mac-artifacts.mjs | 41 +-
apps/desktop/src/main/adeMcpProxy.ts | 196 --
.../desktop/src/main/adeMcpProxyUtils.test.ts | 449 ---
apps/desktop/src/main/adeMcpProxyUtils.ts | 172 --
apps/desktop/src/main/main.ts | 157 +-
apps/desktop/src/main/packagedRuntimeSmoke.ts | 153 -
.../main/services/ai/aiIntegrationService.ts | 7 +-
.../src/main/services/ai/apiKeyStore.ts | 5 +-
.../services/ai/claudeRuntimeProbe.test.ts | 53 +-
.../main/services/ai/claudeRuntimeProbe.ts | 19 -
.../src/main/services/ai/cliMcpConfig.ts | 35 -
.../services/ai/codexAppServerConfig.test.ts | 52 -
.../main/services/ai/codexAppServerConfig.ts | 91 -
.../services/ai/tools/systemPrompt.test.ts | 11 +-
.../main/services/ai/tools/systemPrompt.ts | 38 +-
.../services/automations/automationService.ts | 50 +-
.../services/chat/agentChatService.test.ts | 212 +-
.../main/services/chat/agentChatService.ts | 754 +----
.../chat/buildComputerUseDirective.test.ts | 4 +-
.../chat/cursorAcpEventMapper.test.ts | 20 +-
.../services/chat/cursorAcpEventMapper.ts | 3 +-
.../src/main/services/chat/cursorAcpMcp.ts | 26 -
.../src/main/services/chat/cursorAcpPool.ts | 4 -
.../computerUseArtifactBrokerService.ts | 49 +-
.../services/computerUse/controlPlane.test.ts | 71 +-
.../main/services/computerUse/controlPlane.ts | 123 +-
.../computerUse/localComputerUse.test.ts | 6 +-
.../services/computerUse/localComputerUse.ts | 12 +-
.../computerUse/proofObserver.test.ts | 10 +-
.../services/computerUse/proofObserver.ts | 25 +-
.../computerUse/syntheticToolResult.test.ts | 6 +-
.../services/config/projectConfigService.ts | 19 -
.../context/contextDocBuilder.test.ts | 8 +-
.../services/context/contextDocBuilder.ts | 6 +-
.../main/services/cto/ctoStateService.test.ts | 4 +-
.../src/main/services/cto/ctoStateService.ts | 38 +-
.../services/cto/linearCloseoutService.ts | 1 +
.../cto/linearDispatcherService.test.ts | 3 +-
.../cto/workerAdapterRuntimeService.test.ts | 4 +-
.../cto/workerAdapterRuntimeService.ts | 6 +-
.../main/services/cto/workerAgentService.ts | 30 +-
.../services/cto/workerHeartbeatService.ts | 2 +-
.../externalConnectionAuthService.ts | 795 -----
.../externalMcp/externalMcpService.ts | 1257 --------
.../main/services/files/fileWatcherService.ts | 4 +-
.../src/main/services/ipc/registerIpc.ts | 138 +-
.../missions/missionPreflightService.test.ts | 2 +-
.../services/opencode/openCodeRuntime.test.ts | 518 +---
.../main/services/opencode/openCodeRuntime.ts | 415 +--
.../opencode/openCodeServerManager.test.ts | 2 +-
.../aiOrchestratorService.test.ts | 2 +-
.../orchestrator/aiOrchestratorService.ts | 10 +-
.../baseOrchestratorAdapter.test.ts | 2 +-
.../orchestrator/baseOrchestratorAdapter.ts | 6 +-
.../orchestrator/chatMessageService.ts | 1 -
.../orchestrator/coordinatorAgent.test.ts | 14 +-
.../services/orchestrator/coordinatorAgent.ts | 14 +-
.../services/orchestrator/coordinatorTools.ts | 21 +-
.../orchestrator/delegationContracts.ts | 7 +-
.../orchestrator/hardeningMissions.test.ts | 18 +-
.../knowledgeConflictsBrowserCto.test.ts | 3 +-
.../orchestrator/missionBudgetService.ts | 68 +-
.../orchestrator/orchestratorConstants.ts | 2 +-
.../orchestrator/orchestratorQueries.ts | 7 +-
.../orchestrator/orchestratorService.ts | 10 +-
.../orchestrator/planningGapsFixes.test.ts | 29 +-
.../services/orchestrator/promptInspector.ts | 12 +-
.../providerOrchestratorAdapter.ts | 234 +-
.../orchestrator/teamRuntimeConfig.ts | 11 +-
.../services/projects/adeProjectService.ts | 5 -
.../services/projects/configReloadService.ts | 11 -
.../main/services/prs/prIssueResolver.test.ts | 21 +-
.../src/main/services/prs/prIssueResolver.ts | 53 +-
.../src/main/services/pty/ptyService.test.ts | 37 +-
.../src/main/services/pty/ptyService.ts | 89 +-
.../services/runtime/adeMcpLaunch.test.ts | 368 ---
.../src/main/services/runtime/adeMcpLaunch.ts | 227 --
apps/desktop/src/main/services/state/kvDb.ts | 172 +-
.../services/usage/usageTrackingService.ts | 6 +-
apps/desktop/src/preload/global.d.ts | 49 -
apps/desktop/src/preload/preload.ts | 72 -
apps/desktop/src/renderer/browserMock.ts | 63 +-
.../components/app/CommandPalette.tsx | 2 +-
.../components/RuleEditorPanel.tsx | 1 -
.../components/chat/AgentChatMessageList.tsx | 5 -
.../chat/AgentChatPane.submit.test.tsx | 3 -
.../components/chat/AgentChatPane.tsx | 15 +-
.../components/chat/chatNavigation.test.ts | 32 -
.../components/chat/chatNavigation.ts | 5 -
.../chat/chatToolAppearance.test.ts | 4 +-
.../chat/chatTranscriptRows.test.ts | 8 +-
.../components/chat/toolPresentation.test.ts | 22 +-
.../components/chat/toolPresentation.ts | 12 +-
.../components/chat/useChatMcpSummary.test.ts | 129 -
.../components/chat/useChatMcpSummary.ts | 69 -
.../src/renderer/components/cto/CtoPage.tsx | 38 +-
.../components/cto/CtoSettingsPanel.test.tsx | 17 +-
.../components/cto/CtoSettingsPanel.tsx | 48 +-
.../src/renderer/components/cto/TeamPanel.tsx | 27 -
.../components/cto/WorkerActivityFeed.tsx | 2 +-
.../components/history/eventTaxonomy.ts | 2 +-
.../components/missions/ChatMessageArea.tsx | 29 -
.../missions/CreateMissionDialog.tsx | 1 -
.../components/missions/MissionChatV2.tsx | 5 -
.../components/missions/MissionHeader.test.ts | 4 -
.../missions/OrchestratorActivityFeed.test.ts | 4 +-
.../missions/WorkerPermissionsEditor.tsx | 202 +-
.../missions/missionFeedPresentation.test.ts | 8 +-
.../components/missions/missionHelpers.ts | 11 +-
.../missions/missionUxUtils.test.ts | 4 +-
.../components/missions/useMissionsStore.ts | 9 -
.../settings/ComputerUseSection.tsx | 12 +-
.../DiagnosticsDashboardSection.test.tsx | 32 -
.../settings/DiagnosticsDashboardSection.tsx | 38 +-
.../settings/ExternalMcpSection.tsx | 1387 ---------
.../IntegrationsSettingsSection.test.tsx | 13 +-
.../settings/IntegrationsSettingsSection.tsx | 6 +-
.../shared/ExternalMcpAccessEditor.tsx | 130 -
apps/desktop/src/shared/adeLayout.ts | 14 +-
apps/desktop/src/shared/ipc.ts | 15 -
apps/desktop/src/shared/modelRegistry.ts | 2 +-
apps/desktop/src/shared/types/agents.ts | 5 +-
.../src/shared/types/computerUseArtifacts.ts | 7 +-
apps/desktop/src/shared/types/config.ts | 18 +-
apps/desktop/src/shared/types/cto.ts | 5 +-
apps/desktop/src/shared/types/externalMcp.ts | 202 --
apps/desktop/src/shared/types/index.ts | 1 -
apps/desktop/src/shared/types/missions.ts | 2 -
apps/desktop/src/shared/types/orchestrator.ts | 2 -
apps/desktop/src/shared/workerRuntimeNoise.ts | 3 -
apps/desktop/src/types/opencode-ai-sdk.d.ts | 1 -
apps/desktop/tsup.config.ts | 1 -
apps/ios/ADE/Resources/DatabaseBootstrap.sql | 37 +-
apps/ios/ADE/Services/Database.swift | 2 -
.../Work/WorkStatusAndFormattingHelpers.swift | 3 -
apps/mcp-server/README.md | 70 -
apps/mcp-server/src/index.ts | 290 --
apps/mcp-server/src/transport.test.ts | 147 -
apps/mcp-server/src/transport.ts | 41 -
apps/web/src/app/pages/HomePage.tsx | 4 +-
automations/templates.mdx | 2 +-
changelog/v1.0.1.mdx | 4 +-
changelog/v1.0.10.mdx | 2 +-
changelog/v1.0.11.mdx | 8 +-
changelog/v1.0.12.mdx | 8 +-
changelog/v1.0.13.mdx | 4 +-
changelog/v1.0.14.mdx | 4 +-
changelog/v1.0.15.mdx | 4 +-
changelog/v1.0.16.mdx | 14 +-
changelog/v1.0.6.mdx | 8 +-
chat/capabilities.mdx | 8 +-
chat/context.mdx | 29 +-
chat/overview.mdx | 4 +-
computer-use/overview.mdx | 23 +-
computer-use/setup.mdx | 25 +-
configuration/ai-providers.mdx | 8 +-
configuration/mcp-servers.mdx | 405 ---
configuration/overview.mdx | 27 +-
configuration/permissions.mdx | 43 +-
configuration/settings.mdx | 20 +-
context-packs/freshness.mdx | 2 +-
context-packs/structure.mdx | 26 +-
cto/overview.mdx | 4 +-
cto/workers.mdx | 12 +-
docs.json | 4 +-
docs/ARCHITECTURE.md | 72 +-
docs/PRD.md | 2 +-
docs/features/agents/README.md | 29 +-
docs/features/agents/identity-and-personas.md | 10 +-
docs/features/agents/tool-registration.md | 275 +-
docs/features/automations/README.md | 4 +-
docs/features/automations/guardrails.md | 2 +-
.../automations/triggers-and-actions.md | 20 +-
docs/features/chat/README.md | 8 +-
docs/features/chat/agent-routing.md | 4 +-
docs/features/chat/composer-and-ui.md | 2 +-
docs/features/chat/tool-system.md | 25 +-
docs/features/computer-use/README.md | 12 +-
docs/features/computer-use/artifact-broker.md | 2 +-
docs/features/computer-use/backends.md | 19 +-
.../computer-use/settings-and-readiness.md | 21 +-
docs/features/cto/README.md | 14 +-
docs/features/cto/identity-and-memory.md | 2 +-
docs/features/cto/linear-integration.md | 8 +-
docs/features/cto/pipeline-builder.md | 2 +-
docs/features/cto/workers.md | 6 +-
docs/features/files-and-editor/README.md | 2 +-
.../file-watcher-and-trust.md | 4 +-
docs/features/linear-integration/README.md | 16 +-
.../linear-integration/dispatch-and-sync.md | 14 +-
docs/features/memory/compaction.md | 2 +-
docs/features/missions/README.md | 4 +-
docs/features/missions/orchestration.md | 2 +-
docs/features/missions/workers.md | 12 +-
.../onboarding-and-settings/README.md | 2 +-
.../configuration-schema.md | 1 -
docs/features/pull-requests/README.md | 4 +-
docs/features/sync-and-multi-device/README.md | 2 +-
.../sync-and-multi-device/remote-commands.md | 6 +-
.../pty-and-processes.md | 13 +-
getting-started/first-agent.mdx | 2 +-
getting-started/install.mdx | 2 +-
getting-started/project-setup.mdx | 2 +-
guides/automation-rules.mdx | 2 +-
guides/first-mission.mdx | 2 +-
guides/pr-convergence.mdx | 2 +-
introduction.mdx | 2 +-
key-concepts.mdx | 16 +-
lanes/packs.mdx | 2 +-
missions/budget.mdx | 4 +-
package.json | 4 +-
plans/missions-stabilization-review-scope.md | 2 +-
plans/p0-fixes-consolidated.md | 2 +-
plans/validation-contract-m3-m4.md | 2 +-
quickstart.mdx | 2 +-
scripts/dogfood.sh | 6 +-
250 files changed, 4738 insertions(+), 12741 deletions(-)
create mode 100644 apps/ade-cli/README.md
rename apps/{mcp-server => ade-cli}/package-lock.json (99%)
rename apps/{mcp-server => ade-cli}/package.json (52%)
create mode 100644 apps/ade-cli/scripts/verify-built-cli.mjs
rename apps/{mcp-server/src/mcpServer.test.ts => ade-cli/src/adeRpcServer.test.ts} (90%)
rename apps/{mcp-server/src/mcpServer.ts => ade-cli/src/adeRpcServer.ts} (95%)
rename apps/{mcp-server => ade-cli}/src/bootstrap.test.ts (100%)
rename apps/{mcp-server => ade-cli}/src/bootstrap.ts (88%)
create mode 100644 apps/ade-cli/src/cli.test.ts
create mode 100644 apps/ade-cli/src/cli.ts
rename apps/{mcp-server => ade-cli}/src/headlessLinearServices.test.ts (99%)
rename apps/{mcp-server => ade-cli}/src/headlessLinearServices.ts (98%)
rename apps/{mcp-server => ade-cli}/src/jsonrpc.ts (98%)
rename apps/{mcp-server => ade-cli}/src/test/setup.ts (97%)
rename apps/{mcp-server => ade-cli}/src/types/node-sqlite.d.ts (100%)
rename apps/{mcp-server => ade-cli}/tsconfig.json (100%)
rename apps/{mcp-server => ade-cli}/tsup.config.ts (91%)
rename apps/{mcp-server => ade-cli}/vitest.config.ts (100%)
create mode 100755 apps/desktop/scripts/ade-cli-install-path.sh
create mode 100755 apps/desktop/scripts/ade-cli-macos-wrapper.sh
delete mode 100644 apps/desktop/src/main/adeMcpProxy.ts
delete mode 100644 apps/desktop/src/main/adeMcpProxyUtils.test.ts
delete mode 100644 apps/desktop/src/main/adeMcpProxyUtils.ts
delete mode 100644 apps/desktop/src/main/services/ai/cliMcpConfig.ts
delete mode 100644 apps/desktop/src/main/services/ai/codexAppServerConfig.test.ts
delete mode 100644 apps/desktop/src/main/services/ai/codexAppServerConfig.ts
delete mode 100644 apps/desktop/src/main/services/chat/cursorAcpMcp.ts
delete mode 100644 apps/desktop/src/main/services/externalMcp/externalConnectionAuthService.ts
delete mode 100644 apps/desktop/src/main/services/externalMcp/externalMcpService.ts
delete mode 100644 apps/desktop/src/main/services/runtime/adeMcpLaunch.test.ts
delete mode 100644 apps/desktop/src/main/services/runtime/adeMcpLaunch.ts
delete mode 100644 apps/desktop/src/renderer/components/chat/chatNavigation.test.ts
delete mode 100644 apps/desktop/src/renderer/components/chat/chatNavigation.ts
delete mode 100644 apps/desktop/src/renderer/components/chat/useChatMcpSummary.test.ts
delete mode 100644 apps/desktop/src/renderer/components/chat/useChatMcpSummary.ts
delete mode 100644 apps/desktop/src/renderer/components/settings/ExternalMcpSection.tsx
delete mode 100644 apps/desktop/src/renderer/components/shared/ExternalMcpAccessEditor.tsx
delete mode 100644 apps/desktop/src/shared/types/externalMcp.ts
delete mode 100644 apps/mcp-server/README.md
delete mode 100644 apps/mcp-server/src/index.ts
delete mode 100644 apps/mcp-server/src/transport.test.ts
delete mode 100644 apps/mcp-server/src/transport.ts
delete mode 100644 configuration/mcp-servers.mdx
diff --git a/.ade/.gitignore b/.ade/.gitignore
index 01224321c..d93703639 100644
--- a/.ade/.gitignore
+++ b/.ade/.gitignore
@@ -5,7 +5,7 @@ ade.db
ade.db-*
ade.db-wal
embeddings.db
-mcp.sock
+ade.sock
artifacts/
transcripts/
cache/
diff --git a/.ade/cto/identity.yaml b/.ade/cto/identity.yaml
index ad9417718..a6ca64553 100644
--- a/.ade/cto/identity.yaml
+++ b/.ade/cto/identity.yaml
@@ -11,10 +11,6 @@ memoryPolicy:
compactionThreshold: 0.7
preCompactionFlush: true
temporalDecayHalfLifeDays: 30
-externalMcpAccess:
- allowAll: true
- allowedServers: []
- blockedServers: []
openclawContextPolicy:
shareMode: filtered
blockedCategories:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 98033fbd9..e95b0ba74 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,15 +25,15 @@ jobs:
with:
path: |
apps/desktop/node_modules
- apps/mcp-server/node_modules
+ apps/ade-cli/node_modules
apps/web/node_modules
- key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
+ key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }}
- name: Install all dependencies (parallel)
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd apps/desktop && npm ci &
- cd apps/mcp-server && npm ci &
+ cd apps/ade-cli && npm ci &
cd apps/web && npm ci &
wait
@@ -61,12 +61,12 @@ jobs:
with:
path: |
apps/desktop/node_modules
- apps/mcp-server/node_modules
+ apps/ade-cli/node_modules
apps/web/node_modules
- key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
+ key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }}
- run: cd apps/desktop && npm run typecheck
- typecheck-mcp:
+ typecheck-ade-cli:
needs: install
runs-on: ubuntu-latest
steps:
@@ -78,10 +78,10 @@ jobs:
with:
path: |
apps/desktop/node_modules
- apps/mcp-server/node_modules
+ apps/ade-cli/node_modules
apps/web/node_modules
- key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
- - run: cd apps/mcp-server && npm run typecheck
+ key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }}
+ - run: cd apps/ade-cli && npm run typecheck
typecheck-web:
needs: install
@@ -95,9 +95,9 @@ jobs:
with:
path: |
apps/desktop/node_modules
- apps/mcp-server/node_modules
+ apps/ade-cli/node_modules
apps/web/node_modules
- key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
+ key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }}
- run: cd apps/web && npm run typecheck
lint-desktop:
@@ -112,9 +112,9 @@ jobs:
with:
path: |
apps/desktop/node_modules
- apps/mcp-server/node_modules
+ apps/ade-cli/node_modules
apps/web/node_modules
- key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
+ key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }}
- run: cd apps/desktop && npm run lint
test-desktop:
@@ -133,12 +133,12 @@ jobs:
with:
path: |
apps/desktop/node_modules
- apps/mcp-server/node_modules
+ apps/ade-cli/node_modules
apps/web/node_modules
- key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
+ key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }}
- run: cd apps/desktop && npx vitest run --shard=${{ matrix.shard }}/8
- test-mcp:
+ test-ade-cli:
needs: install
runs-on: ubuntu-latest
steps:
@@ -150,10 +150,10 @@ jobs:
with:
path: |
apps/desktop/node_modules
- apps/mcp-server/node_modules
+ apps/ade-cli/node_modules
apps/web/node_modules
- key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
- - run: cd apps/mcp-server && npm test
+ key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }}
+ - run: cd apps/ade-cli && npm test
build:
needs: install
@@ -167,11 +167,11 @@ jobs:
with:
path: |
apps/desktop/node_modules
- apps/mcp-server/node_modules
+ apps/ade-cli/node_modules
apps/web/node_modules
- key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
+ key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }}
- run: cd apps/desktop && npm run build
- - run: cd apps/mcp-server && npm run build
+ - run: cd apps/ade-cli && npm run build
- run: cd apps/web && npm run build
validate-docs:
@@ -186,9 +186,9 @@ jobs:
with:
path: |
apps/desktop/node_modules
- apps/mcp-server/node_modules
+ apps/ade-cli/node_modules
apps/web/node_modules
- key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
+ key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }}
- run: node scripts/validate-docs.mjs
# ── Gate: all jobs must pass ──────────────────────────────────────────
@@ -197,11 +197,11 @@ jobs:
needs:
- secret-scan
- typecheck-desktop
- - typecheck-mcp
+ - typecheck-ade-cli
- typecheck-web
- lint-desktop
- test-desktop
- - test-mcp
+ - test-ade-cli
- build
- validate-docs
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index 02736cd7e..d6d277e49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,7 +19,7 @@ __pycache__/
*.db
# Build outputs
-/apps/mcp-server/dist/
+/apps/ade-cli/dist/
/apps/desktop/release/
/apps/desktop/dist/
/apps/desktop/vendor/crsqlite/darwin-x64/
diff --git a/AGENTS.md b/AGENTS.md
index 82227f5e0..3920706ed 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,7 +4,7 @@
- ADE is a local-first desktop application for orchestrating coding agents, missions, lanes, PR workflows, and proof/artifact capture.
- The main product lives in `apps/desktop` and is built with Electron, React, and TypeScript.
-- The ADE MCP server lives in `apps/mcp-server` and shares core services with the desktop app.
+- The ADE CLI lives in `apps/ade-cli` and shares core services with the desktop app.
- State is primarily stored under `.ade/` inside the active project, with runtime metadata in SQLite and machine-local files under `.ade/secrets`, `.ade/cache`, and `.ade/artifacts`.
## Working norms
@@ -12,7 +12,7 @@
- Preserve existing desktop app patterns before introducing new abstractions.
- Prefer fixing the underlying service or shared type rather than layering renderer-only workarounds on top.
- Keep IPC contracts, preload types, shared types, and renderer usage in sync whenever an interface changes.
-- For ADE MCP changes, verify both headless MCP mode and the desktop socket-backed MCP path.
+- For ADE CLI changes, verify both headless mode and the desktop socket-backed ADE RPC path.
- For computer-use changes, treat policy enforcement and artifact ownership as hard requirements, not prompt guidance.
## Validation
@@ -22,10 +22,10 @@
- `npm --prefix apps/desktop run test`
- `npm --prefix apps/desktop run build`
- `npm --prefix apps/desktop run lint`
-- MCP checks:
- - `npm --prefix apps/mcp-server run typecheck`
- - `npm --prefix apps/mcp-server run test`
- - `npm --prefix apps/mcp-server run build`
+- ADE CLI checks:
+ - `npm --prefix apps/ade-cli run typecheck`
+ - `npm --prefix apps/ade-cli run test`
+ - `npm --prefix apps/ade-cli run build`
- Run the smallest relevant subset first when iterating, then finish with the broader checks that cover the touched surfaces.
## Terminology
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e930463bd..291149446 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,9 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **OpenAI responses-based verification** — `authDetector` now verifies OpenAI keys via `POST /v1/responses` with `gpt-5-nano`/`gpt-5-mini` fallback, distinguishing model-availability errors from invalid keys; secondary model-list fallback check for resilience
-- **Scoped OpenCode tool selection** — `buildFallbackToolSelection`, `buildScopedMcpToolSelection`, and `resolveScopedMcpToolSelection` pipeline ensures sessions get exactly the tools matching current MCP server config; `refreshOpenCodeSessionToolSelection` called before every prompt
+- **Scoped OpenCode tool selection** — `buildFallbackToolSelection`, `buildScopedMcpToolSelection`, and `resolveScopedMcpToolSelection` pipeline ensures sessions get exactly the tools matching current ADE CLI config; `refreshOpenCodeSessionToolSelection` called before every prompt
- **Isolated OpenCode server launches** — `openCodeServerManager` builds XDG-style isolated launch specs via `child_process.spawn`, sanitizes inherited `OPENCODE_*` env, and resolves binary via `resolveOpenCodeBinaryPath`; new test hooks for launch spec verification
-- **MCP schema sanitization** — `sanitizeToolSchema` recursively normalizes tool JSON Schemas (adds missing `items`/`properties`, ensures `required` completeness, recurses through `anyOf`/`oneOf`/`allOf`) to prevent strict-provider rejections
+- **ADE CLI schema sanitization** — `sanitizeToolSchema` recursively normalizes tool JSON Schemas (adds missing `items`/`properties`, ensures `required` completeness, recurses through `anyOf`/`oneOf`/`allOf`) to prevent strict-provider rejections
- **Provider verification badges** — ProvidersSection shows verification badges, failure state indicators, and auto-refreshes OpenCode inventory on key changes
### Changed
@@ -38,7 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Provider task runner** — unified 306-line task runner dispatching to Claude, Codex, Cursor, and OpenCode paths with timeout enforcement and permission mode mapping (`providerTaskRunner.ts`)
- **Tool exposure policy** — score-based heuristics for frontend repo discovery tool exposure based on prompt content analysis (`toolExposurePolicy.ts`)
- **Runtime message types** — provider-agnostic message representation decoupling ADE internals from SDK types (`runtimeMessageTypes.ts`)
-- **CLI MCP config normalization** — normalizes MCP server config between Claude (`type`) and Codex (`transport`) field formats (`cliMcpConfig.ts`)
+- **CLI ADE CLI config normalization** — normalizes ADE CLI config between Claude (`type`) and Codex (`transport`) field formats (`cliMcpConfig.ts`)
- **Local provider discovery pipeline** — endpoint probing, model inspection, capability inference, harness profile assignment, and TTL-based caching for Ollama and LM Studio
- **900+ lines of new tool tests** — `globSearch.test.ts` (219 lines), `grepSearch.test.ts` (276 lines), `readFileRange.test.ts` (197 lines), expanded `universalTools.test.ts` (+364 lines)
- **Compaction engine tests** — 526-line test suite covering transcript persistence, compaction triggering, and token accounting
@@ -97,7 +97,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Multimodal chat** — Claude agent chat now supports image attachments via base64 content blocks, with a new file upload picker and clipboard paste support in the composer
- **CTO daily logs** — CTO persona gains a Memory Protocol and Decision Framework; daily log utilities (append/read/list) auto-inject recent context into CTO sessions
-- **External MCP auth service** — full OAuth and token-based authentication flows for connecting external MCP servers (795-line service with PKCE support)
+- **ADE CLI auth service** — full OAuth and token-based authentication flows for connecting ADE CLI (795-line service with PKCE support)
- **Onboarding rewrite** — replaced the 1,373-line `OnboardingPage` with a focused 328-line `ProjectSetupPage`
- **New settings sections** — Lane Behavior, Lane Templates (expanded), Integrations, Workspace Settings, and AI Settings panels
- **Context doc preferences** — model, effort, and events settings now persist to the backend via new IPC handlers
@@ -114,7 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Linear OAuth** — bundled public PKCE client-ID fallback so CTO Linear sync works without manual credential setup; fixed OAuth server to listen on port 19836 and force `prompt=consent`
- **Orchestrator tuning** — lowered dedupe/retention/pruning thresholds, capped `workerProgressChatState`, pruned config cache, reduced session signal retention and health sweep intervals
- **Main process** — simplified hardware acceleration logic, narrowed default background task flags in dev stability mode
-- **CI** — tightened permissions, removed old inline release job, added MCP server dependency install before desktop typecheck
+- **CI** — tightened permissions, removed old inline release job, added ADE CLI dependency install before desktop typecheck
- **Homepage & web** — refreshed landing page layout and styles, updated Tailwind config
### Removed
diff --git a/README.md b/README.md
index 977508dd3..4e70c4f09 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
-
+
@@ -48,16 +48,29 @@ ADE is built for people who want agents to operate inside a real development wor
- **Automations** -- Event-driven background execution with triggers and guardrails
- **PR workflows** -- Stacking, conflict simulation, and queue landing
- **Context packs** -- Structured, bounded context delivery for agents
-- **ADE MCP server** -- Shared services for both desktop and headless MCP flows
+- **ADE CLI** -- Agent-focused command-line access to ADE actions in desktop-backed and headless modes
- **Memory system** -- Persistent knowledge across sessions with semantic search
+- **Runtime model support** -- Claude, Codex, OpenAI-compatible providers, and local models
- **Linear integration** -- Workflow automation triggered by Linear issues
- **Multi-repo aware workflows** -- Project-local state under `.ade/` plus machine-local secrets, cache, and artifacts
## Stack
- **Desktop** -- Electron, React, TypeScript, SQLite, node-pty
-- **Protocols** -- Model Context Protocol (MCP), IPC, GitHub and Linear integrations
-- **Runtime model support** -- Claude, Codex, OpenAI-compatible providers, and local models
+- **Protocols** -- ADE CLI, IPC, GitHub and Linear integrations
+
+## ADE CLI
+
+ADE ships a real `ade` command for agents and local automation. The standalone package lives in `apps/ade-cli` and exposes `bin.ade`; after `cd apps/ade-cli && npm run build && npm link`, `ade doctor`, `ade lanes list`, and the rest of the command surface are available on `PATH`.
+
+The macOS desktop app also bundles the same CLI at `/Applications/ADE.app/Contents/Resources/ade-cli/bin/ade`. Symlink that wrapper into a `PATH` directory to use `ade xyz` without installing Node separately:
+
+```bash
+/Applications/ADE.app/Contents/Resources/ade-cli/install-path.sh
+ade doctor --project-root /path/to/project
+```
+
+Agents should use `ade doctor --json` for readiness, `ade actions list --text` for discovery, typed commands first, and `ade actions run ` as the escape hatch for any ADE service action that does not yet have a typed command.
## Install
diff --git a/ai-tools/claude-code.mdx b/ai-tools/claude-code.mdx
index c01786a2d..f32fc472f 100644
--- a/ai-tools/claude-code.mdx
+++ b/ai-tools/claude-code.mdx
@@ -1,6 +1,6 @@
---
title: "Claude Code"
-description: "Use Claude Code as a CLI coding agent inside ADE lanes — launch sessions, connect MCP tools, configure permissions, and track work across agent chat, missions, and CTO delegations."
+description: "Use Claude Code as a CLI coding agent inside ADE lanes — launch sessions, connect ADE CLI tools, configure permissions, and track work across agent chat, missions, and CTO delegations."
icon: "terminal"
---
@@ -8,7 +8,7 @@ icon: "terminal"
ADE integrates with [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Anthropic's CLI agent for coding tasks. When Claude Code is installed on your machine, ADE can launch it as a managed session inside any lane.
-Claude Code is one of ADE's deepest integrations — it serves as both a chat provider (for Agent Chat) and an execution backend (for missions, CTO workers, and automations). ADE manages the Claude Code process lifecycle, injects MCP tools, forwards environment variables, and tracks every session in the History view.
+Claude Code is one of ADE's deepest integrations — it serves as both a chat provider (for Agent Chat) and an execution backend (for missions, CTO workers, and automations). ADE manages the Claude Code process lifecycle, exposes the `ade` CLI so the agent can reach ADE's lane, git, and PR tools, forwards environment variables, and tracks every session in the History view.
---
@@ -25,13 +25,13 @@ ADE auto-detects the Claude Code executable from standard paths and PATH. If det
## How ADE Uses Claude Code
-ADE launches Claude Code as a subprocess inside a lane's worktree. The session inherits the lane's environment variables, working directory, port assignments, and MCP tool configuration.
+ADE launches Claude Code as a subprocess inside a lane's worktree. The session inherits the lane's environment variables, working directory, port assignments, and the `ade` CLI on `PATH` so the agent can call ADE tools.
Claude Code sessions are used in several places:
| Surface | How Claude Code is used |
|---------|------------------------|
-| **Agent Chat** | Select Claude as the provider in the model selector. ADE starts a Claude Code session with MCP tools bridged in. |
+| **Agent Chat** | Select Claude as the provider in the model selector. ADE starts a Claude Code session with the `ade` CLI on `PATH`. |
| **Missions** | Used as a worker executor (`claude_cli`) during mission execution phases. Each worker gets its own Claude Code process. |
| **CTO Workers** | When the CTO delegates a task to an employee agent, it can use Claude Code as the execution backend. |
| **Automations** | Automation rules with `agent-session` execution kind can use Claude Code as the underlying agent. |
@@ -50,7 +50,7 @@ Claude Code sessions are used in several places:
Click the **Claude Code** launcher. ADE starts a managed session with:
- The lane's context pack injected as initial context
- - MCP tools from ADE's server available to the agent
+ - The `ade` CLI on `PATH` so the agent can call ADE lane, git, PR, chat, and action tools
- Environment variables from the lane's overlay applied
- The working directory set to the lane's worktree path
@@ -58,11 +58,11 @@ Claude Code sessions are used in several places:
---
-## MCP Tool Bridging
+## ADE CLI access
-When ADE launches Claude Code, it bridges ADE's MCP tools into the Claude Code session. This means the Claude Code agent can use ADE-specific tools (file operations, git operations, PR management, lane management) alongside its built-in capabilities.
+When ADE launches Claude Code, it ensures the local `ade` CLI is on `PATH` for the session. The agent calls ADE-specific workflows (file, git, PR, lane, chat, proof, tests, and the full action catalog) by shelling out to `ade `.
-ADE runs a pre-session **MCP initialize probe** to verify that the MCP server is reachable before starting the Claude Code session. If the probe fails, ADE reports the error in the chat UI instead of starting a broken session.
+Before the session starts, ADE runs a short **CLI readiness probe** that confirms `ade doctor` succeeds against the project. If the probe fails, ADE surfaces the error in the chat UI instead of starting a broken session.
---
@@ -103,8 +103,8 @@ ADE shows Claude Code's connection status in **Settings > AI Providers**:
Run `claude auth login` in a terminal outside ADE, then restart ADE or click **Refresh** in Settings > AI Providers. ADE caches the runtime probe result for 30 seconds, so the status updates shortly after re-authentication.
-
- Check that the MCP initialize probe passed (visible in the chat session's startup messages). If the probe failed, verify that ADE's MCP server is running — check **Settings > MCP** for server status.
+
+ Check that the CLI readiness probe passed (visible in the chat session's startup messages). If the probe failed, verify the CLI is installed and on `PATH` — run `ade doctor --project-root ` in a terminal outside ADE, and consult `apps/ade-cli/README.md` for install instructions.
The runtime probe has a 20-second timeout. If your network is slow or the Anthropic API is under load, the probe may fail. Check your internet connection and try again. If sessions consistently hang, check the developer logs for detailed error information.
diff --git a/ai-tools/cursor.mdx b/ai-tools/cursor.mdx
index 117eacfb7..8a402974f 100644
--- a/ai-tools/cursor.mdx
+++ b/ai-tools/cursor.mdx
@@ -43,7 +43,7 @@ When Cursor's agent executable is detected, ADE can use it as a chat provider th
1. ADE resolves the Cursor agent executable path (checks standard install locations and PATH)
2. An ACP session pool manages connections to the Cursor agent process
-3. Chat messages are routed through the ACP protocol with MCP bridging
+3. Chat messages are routed through the ACP protocol, and the Cursor agent has access to the `ade` CLI for ADE workflows
4. Model discovery queries Cursor for available models and capabilities
### Selecting Cursor in chat
@@ -63,7 +63,7 @@ ADE queries Cursor for its available models at startup and caches the result. Th
|----------|---------|
| **Session lifecycle** | Managed by the ACP pool; sessions are reused when possible |
| **Event mapping** | ACP events are mapped to ADE's chat event model for consistent UI |
-| **MCP bridging** | ADE's MCP tools are available to Cursor agents through the ACP-MCP bridge |
+| **ADE CLI access** | The `ade` CLI is on `PATH` so Cursor agents can call ADE lane, git, PR, and action tools |
| **Interrupts** | Supported via ACP session cancel |
---
diff --git a/ai-tools/windsurf.mdx b/ai-tools/windsurf.mdx
index 5d1543229..588cc828b 100644
--- a/ai-tools/windsurf.mdx
+++ b/ai-tools/windsurf.mdx
@@ -82,7 +82,7 @@ ADE supports multiple external AI tools. Here is how Windsurf compares:
| **Launch from ADE** | Yes | Yes | Yes |
| **Session tracking** | Yes | Yes | Yes |
| **Agent provider in ADE chat** | No | Yes (via ACP) | Yes (via CLI) |
-| **MCP tool bridging** | No | Yes | Yes |
+| **ADE CLI access** | No | Yes | Yes |
| **Model routing through ADE** | No | Yes | Yes |
Windsurf is a good choice when you want to use Codeium's AI features directly in the editor while keeping the session tracked in ADE. For deeper integration where ADE manages the AI agent lifecycle, use Cursor (via ACP) or Claude Code (via CLI).
diff --git a/apps/ade-cli/README.md b/apps/ade-cli/README.md
new file mode 100644
index 000000000..4eab38410
--- /dev/null
+++ b/apps/ade-cli/README.md
@@ -0,0 +1,115 @@
+# ADE CLI
+
+`apps/ade-cli` owns the `ade` command-line entry point for agents and local automation.
+
+The CLI is the primary agent interface. It prefers the live ADE desktop socket at `.ade/ade.sock` so commands operate against the same lanes, chats, PR state, process runtime, and proof artifacts as the UI. If the desktop app is not running, it falls back to a short-lived headless runtime for actions that can safely run without Electron.
+
+## Scripts
+
+```bash
+npm run cli:dev -- help
+npm run cli:dev -- doctor --project-root /absolute/path/to/repo
+npm run dev -- --project-root /absolute/path/to/repo
+npm run build
+npm run typecheck
+npm run test
+```
+
+## Install and PATH
+
+For local development, build the package and link its `ade` binary:
+
+```bash
+cd apps/ade-cli
+npm run build
+npm link
+ade doctor --project-root /absolute/path/to/repo
+```
+
+The package is also packable as a normal Node CLI. It requires Node.js 22 or newer because ADE uses `node:sqlite` in the headless runtime.
+
+```bash
+cd apps/ade-cli
+npm pack
+npm install -g ./ade-cli-*.tgz
+```
+
+The desktop macOS build also bundles the CLI at:
+
+```bash
+/Applications/ADE.app/Contents/Resources/ade-cli/bin/ade
+```
+
+To make the desktop-bundled command available as `ade`, add a symlink from a directory on `PATH`:
+
+```bash
+/Applications/ADE.app/Contents/Resources/ade-cli/install-path.sh
+```
+
+That wrapper runs the CLI with the packaged ADE Electron runtime, so users do not need a separate Node install for the desktop-bundled path.
+
+## CLI surface
+
+```bash
+ade auth status
+ade doctor
+ade lanes list --text
+ade lanes create "fix-checkout-flow" --parent main
+ade git commit --lane lane-id
+ade git push --lane lane-id
+ade prs create --lane lane-id --base main --title "Fix checkout flow"
+ade prs path-to-merge --pr pr-id --model gpt-5.4 --max-rounds 3 --no-auto-merge
+ade run defs --text
+ade run start web --lane lane-id
+ade shell start --lane lane-id -- npm test
+ade chat create --lane lane-id --model gpt-5.4
+ade tests run --lane lane-id --suite unit --wait
+ade proof list --arg ownerKind=chat --arg ownerId=session-id
+ade actions list
+ade actions run git.stageFile --arg laneId=lane-id --arg path=src/index.ts
+```
+
+Use typed commands first. They validate common arguments and provide stable JSON fields or readable text summaries. Use `ade actions list --text` to discover the full service-backed action catalog, and use `ade actions run ` only when there is no typed command for the workflow yet.
+
+Output modes are explicit:
+
+```bash
+ade lanes list --text
+ade git status --lane lane-id --json
+ade actions run git.stageFile --arg laneId=lane-id --arg path=src/index.ts --json
+```
+
+Commands that need UI-owned state, long-running Work chat state, live Run tab process state, or desktop proof state should use the live ADE socket:
+
+```bash
+ade doctor --project-root /absolute/path/to/repo --socket --json
+ade lanes list --project-root /absolute/path/to/repo --socket --text
+```
+
+Without `--socket`, the CLI auto-connects to the desktop socket when it is available and falls back to headless mode when it is not.
+
+## Auth and readiness
+
+ADE CLI auth is local project access, not a separate cloud login. `ade auth status` verifies that the current terminal can initialize an ADE runtime for the project. Provider credentials, GitHub tokens, and computer-use policy are read from ADE project settings and the existing secure stores.
+
+`ade doctor` reports local-only readiness metadata by default:
+
+- CLI version, Node/runtime version, project root, workspace root, `.ade` initialization, and config file presence.
+- Desktop socket path, whether the socket exists, and whether this invocation is actually using `desktop-socket` or `headless` mode.
+- RPC tool count, ADE service action count, and action counts by domain.
+- Git repository readiness and GitHub readiness signals from local remotes, `gh` availability, and token environment presence.
+- Linear readiness from local encrypted token presence or headless environment variables.
+- Provider/model readiness from local ADE config, API-key provider references, and provider CLI availability.
+- Computer-use readiness from local platform capabilities.
+- Packaged/PATH status for the `ade` binary and concrete next actions.
+
+Default doctor/auth checks do not call provider, GitHub, or Linear networks. They report presence and local readiness only, without printing secret values.
+
+Agents should start unfamiliar ADE sessions with:
+
+```bash
+ade doctor --json
+ade actions list --text
+```
+
+Then prefer typed commands such as `ade lanes list --text`, `ade files read --text`, `ade prs checks --text`, or `ade tests runs --json`. Use `ade actions run ...` as the broad escape hatch for internal ADE actions that do not yet have a typed command.
diff --git a/apps/mcp-server/package-lock.json b/apps/ade-cli/package-lock.json
similarity index 99%
rename from apps/mcp-server/package-lock.json
rename to apps/ade-cli/package-lock.json
index 3d82416b0..882c5a8d5 100644
--- a/apps/mcp-server/package-lock.json
+++ b/apps/ade-cli/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "ade-mcp-server",
+ "name": "ade-cli",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "ade-mcp-server",
+ "name": "ade-cli",
"version": "0.0.0",
"dependencies": {
"node-cron": "^3.0.3",
@@ -13,12 +13,18 @@
"sql.js": "^1.13.0",
"yaml": "^2.8.2"
},
+ "bin": {
+ "ade": "dist/cli.cjs"
+ },
"devDependencies": {
"@types/node": "^20.11.30",
"tsup": "^8.3.5",
"tsx": "^4.20.6",
"typescript": "^5.7.3",
"vitest": "^0.34.6"
+ },
+ "engines": {
+ "node": ">=22.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
diff --git a/apps/mcp-server/package.json b/apps/ade-cli/package.json
similarity index 52%
rename from apps/mcp-server/package.json
rename to apps/ade-cli/package.json
index 62186ff37..a41a9e2be 100644
--- a/apps/mcp-server/package.json
+++ b/apps/ade-cli/package.json
@@ -1,11 +1,22 @@
{
- "name": "ade-mcp-server",
- "private": true,
+ "name": "ade-cli",
"version": "0.0.0",
+ "description": "Agent-focused command-line interface for ADE",
"type": "module",
+ "bin": {
+ "ade": "dist/cli.cjs"
+ },
+ "files": [
+ "dist/**/*",
+ "README.md"
+ ],
+ "engines": {
+ "node": ">=22.0.0"
+ },
"scripts": {
- "dev": "tsx src/index.ts",
- "build": "tsup",
+ "cli:dev": "npm run build --silent && node dist/cli.cjs",
+ "dev": "tsx src/cli.ts",
+ "build": "tsup && node ./scripts/verify-built-cli.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run"
},
diff --git a/apps/ade-cli/scripts/verify-built-cli.mjs b/apps/ade-cli/scripts/verify-built-cli.mjs
new file mode 100644
index 000000000..8a4c84b7d
--- /dev/null
+++ b/apps/ade-cli/scripts/verify-built-cli.mjs
@@ -0,0 +1,37 @@
+import { execFile } from "node:child_process";
+import fs from "node:fs/promises";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { promisify } from "node:util";
+
+const execFileAsync = promisify(execFile);
+const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const cliPath = path.join(packageRoot, "dist", "cli.cjs");
+
+async function runHelp(command, args) {
+ const { stdout } = await execFileAsync(command, args, {
+ cwd: packageRoot,
+ env: process.env,
+ });
+ if (!stdout.includes("Agent-focused command-line interface for ADE")) {
+ throw new Error(`[ade-cli:build] CLI help output did not include the ADE banner text for ${command}`);
+ }
+}
+
+const contents = await fs.readFile(cliPath, "utf8");
+if (!contents.startsWith("#!/usr/bin/env node")) {
+ throw new Error("[ade-cli:build] dist/cli.cjs is missing the node shebang");
+}
+
+const stat = await fs.stat(cliPath);
+if (process.platform !== "win32" && (stat.mode & 0o111) === 0) {
+ throw new Error("[ade-cli:build] dist/cli.cjs is not executable");
+}
+
+await runHelp(process.execPath, [cliPath, "--help"]);
+
+if (process.platform !== "win32") {
+ await runHelp(cliPath, ["--help"]);
+}
+
+console.log("[ade-cli:build] verified dist/cli.cjs binary");
diff --git a/apps/mcp-server/src/mcpServer.test.ts b/apps/ade-cli/src/adeRpcServer.test.ts
similarity index 90%
rename from apps/mcp-server/src/mcpServer.test.ts
rename to apps/ade-cli/src/adeRpcServer.test.ts
index fd18bed0a..aa8dde21c 100644
--- a/apps/mcp-server/src/mcpServer.test.ts
+++ b/apps/ade-cli/src/adeRpcServer.test.ts
@@ -2,14 +2,14 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
-import { createMcpRequestHandler, _resetGlobalAskUserRateLimit } from "./mcpServer";
+import { createAdeRpcRequestHandler, _resetGlobalAskUserRateLimit } from "./adeRpcServer";
type RuntimeFixture = ReturnType;
function createRuntime() {
const operationStart = vi.fn((args: any) => ({ operationId: `op-${args.kind}-${Date.now()}` }));
const operationFinish = vi.fn();
- const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-mcp-test-"));
+ const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-test-"));
fs.mkdirSync(path.join(projectRoot, ".ade", "orchestrator"), { recursive: true });
const teamMembers: Array> = [];
const threadRows: Array> = [];
@@ -572,7 +572,7 @@ function createRuntime() {
endedAt: null,
provider: "codex",
modelId: "gpt-5.4-codex",
- capabilityMode: "full_mcp",
+ capabilityMode: "full_tooling",
createdAt: "2026-03-17T19:00:00.000Z",
prevHash: null,
})),
@@ -661,15 +661,6 @@ function createRuntime() {
simulateRoute: vi.fn(({ issue }: { issue: Record }) => ({ decision: "cto", reason: "test", issue })),
} as any,
processService: null,
- externalMcpService: {
- listToolsForIdentity: vi.fn(async () => []),
- callTool: vi.fn(async () => ({
- ok: true,
- result: {
- content: [{ type: "text", text: "ok" }],
- },
- })),
- } as any,
computerUseArtifactBrokerService: {
getBackendStatus: vi.fn(() => ({ backends: [] })),
listArtifacts: vi.fn(() => []),
@@ -871,7 +862,7 @@ function createRuntime() {
};
}
-async function initialize(handler: ReturnType, identity?: Record) {
+async function initialize(handler: ReturnType, identity?: Record) {
const requestedRole = typeof identity?.role === "string" ? identity.role : null;
const validRole = requestedRole === "cto"
|| requestedRole === "orchestrator"
@@ -887,7 +878,7 @@ async function initialize(handler: ReturnType, i
await handler({
jsonrpc: "2.0",
id: 1,
- method: "initialize",
+ method: "ade/initialize",
params: identity ? { identity } : {}
});
} finally {
@@ -898,19 +889,35 @@ async function initialize(handler: ReturnType, i
}
async function callTool(
- handler: ReturnType,
+ handler: ReturnType,
name: string,
argumentsPayload: Record
): Promise {
- return await handler({
+ const result = await handler({
jsonrpc: "2.0",
id: 2,
- method: "tools/call",
+ method: "ade/actions/call",
params: {
name,
arguments: argumentsPayload
}
});
+ if (
+ result
+ && typeof result === "object"
+ && !Array.isArray(result)
+ && (result as { ok?: unknown }).ok === false
+ ) {
+ return {
+ isError: true,
+ structuredContent: result,
+ error: (result as { error?: unknown }).error,
+ };
+ }
+ return {
+ structuredContent: result,
+ ...(result && typeof result === "object" && !Array.isArray(result) ? result : {}),
+ };
}
async function withEnv(vars: Record, fn: () => Promise): Promise {
@@ -937,17 +944,17 @@ async function withEnv(vars: Record, fn: () => Pr
}
}
-describe("mcpServer", () => {
+describe("adeRpcServer", () => {
it("treats requested privileged roles as external without trusted env identity", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
const previousRole = process.env.ADE_DEFAULT_ROLE;
delete process.env.ADE_DEFAULT_ROLE;
try {
await handler({
jsonrpc: "2.0",
id: 1,
- method: "initialize",
+ method: "ade/initialize",
params: {
identity: {
callerId: "rogue-client",
@@ -955,9 +962,9 @@ describe("mcpServer", () => {
},
},
});
- const result = (await handler({ jsonrpc: "2.0", id: 3, method: "tools/list" })) as any;
+ const result = (await handler({ jsonrpc: "2.0", id: 3, method: "ade/actions/list" })) as any;
- const names = (result.tools ?? []).map((tool: any) => tool.name);
+ const names = (result.actions ?? []).map((tool: any) => tool.name);
expect(names).not.toContain("spawn_worker");
expect(names).not.toContain("read_mission_status");
expect(names).not.toContain("get_cto_state");
@@ -969,12 +976,12 @@ describe("mcpServer", () => {
it("lists the full tool surface including coordinator orchestration tools for orchestrator callers", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "coord-1", role: "orchestrator" });
- const result = (await handler({ jsonrpc: "2.0", id: 3, method: "tools/list" })) as any;
+ const result = (await handler({ jsonrpc: "2.0", id: 3, method: "ade/actions/list" })) as any;
- const names = (result.tools ?? []).map((tool: any) => tool.name);
+ const names = (result.actions ?? []).map((tool: any) => tool.name);
expect(names).toEqual(
expect.arrayContaining([
"spawn_agent",
@@ -1040,7 +1047,7 @@ describe("mcpServer", () => {
it("shows agent-safe delegation, reporting, and observation coordinator tools to agent callers", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
@@ -1051,8 +1058,8 @@ describe("mcpServer", () => {
attemptId: "attempt-1"
});
- const result = (await handler({ jsonrpc: "2.0", id: 3, method: "tools/list" })) as any;
- const names = (result.tools ?? []).map((tool: any) => tool.name);
+ const result = (await handler({ jsonrpc: "2.0", id: 3, method: "ade/actions/list" })) as any;
+ const names = (result.actions ?? []).map((tool: any) => tool.name);
expect(names).toEqual(
expect.arrayContaining([
@@ -1093,11 +1100,11 @@ describe("mcpServer", () => {
it("hides ADE spawn and mission-worker tools from standalone chat callers", async () => {
await withEnv({ ADE_DEFAULT_ROLE: "agent", ADE_CHAT_SESSION_ID: "chat-1" }, async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "chat-1", role: "agent" });
- const result = (await handler({ jsonrpc: "2.0", id: 3, method: "tools/list" })) as any;
- const names = (result.tools ?? []).map((tool: any) => tool.name);
+ const result = (await handler({ jsonrpc: "2.0", id: 3, method: "ade/actions/list" })) as any;
+ const names = (result.actions ?? []).map((tool: any) => tool.name);
expect(names).toEqual(
expect.arrayContaining([
@@ -1121,12 +1128,12 @@ describe("mcpServer", () => {
it("lists CTO operator and Linear sync tools for cto callers", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "cto-1", role: "cto" });
- const result = (await handler({ jsonrpc: "2.0", id: 3, method: "tools/list" })) as any;
+ const result = (await handler({ jsonrpc: "2.0", id: 3, method: "ade/actions/list" })) as any;
- const names = (result.tools ?? []).map((tool: any) => tool.name);
+ const names = (result.actions ?? []).map((tool: any) => tool.name);
expect(names).toEqual(
expect.arrayContaining([
"get_cto_state",
@@ -1154,7 +1161,7 @@ describe("mcpServer", () => {
it("creates a work chat for cto callers and returns a work navigation suggestion", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "cto-1", role: "cto" });
const result = await callTool(handler, "spawnChat", {
@@ -1185,7 +1192,7 @@ describe("mcpServer", () => {
it("returns the Linear sync dashboard for cto callers", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "cto-1", role: "cto" });
const result = await callTool(handler, "getLinearSyncDashboard", {});
@@ -1201,7 +1208,7 @@ describe("mcpServer", () => {
it("forwards employeeOverride and laneId when resuming a Linear sync queue item", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "cto-1", role: "cto" });
const result = await callTool(handler, "resolveLinearSyncQueueItem", {
@@ -1231,7 +1238,7 @@ describe("mcpServer", () => {
it("rejects unsupported Linear sync queue actions", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "cto-1", role: "cto" });
const response = await callTool(handler, "resolveLinearSyncQueueItem", {
@@ -1247,7 +1254,7 @@ describe("mcpServer", () => {
it("returns structured local computer-use capability state", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "coord-1", role: "orchestrator" });
const response = await callTool(handler, "get_environment_info", {});
@@ -1260,7 +1267,7 @@ describe("mcpServer", () => {
it("auto-links computer-use ingestion to standalone chat sessions", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "chat-session-1",
@@ -1293,7 +1300,7 @@ describe("mcpServer", () => {
it("rejects computer-use manifests outside the project root", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
const outsideManifest = path.join(path.dirname(fixture.runtime.projectRoot), `ade-artifacts-${Date.now()}.json`);
fs.writeFileSync(outsideManifest, JSON.stringify([{ kind: "screenshot", path: "/tmp/shot.png" }]), "utf8");
@@ -1313,65 +1320,6 @@ describe("mcpServer", () => {
}
});
- it("includes ADE-managed external MCP tools in tool discovery and derives mission identity from run context", async () => {
- await withEnv({ ADE_RUN_ID: "run-1" }, async () => {
- const fixture = createRuntime();
- fixture.runtime.externalMcpService.listToolsForIdentity = vi.fn(async () => [
- {
- name: "search",
- serverName: "notion",
- namespacedName: "ext.notion.search",
- description: "Search Notion pages",
- inputSchema: {
- type: "object",
- properties: {
- query: { type: "string" },
- },
- required: ["query"],
- },
- enabled: true,
- safety: "read",
- },
- ]);
- fixture.runtime.externalMcpService.callTool = vi.fn(async () => ({
- ok: true,
- result: {
- content: [{ type: "text", text: "Found page" }],
- structuredContent: { pageId: "page-1" },
- isError: false,
- },
- }));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
-
- await initialize(handler, {
- callerId: "worker-1",
- role: "agent",
- runId: "run-from-identity",
- stepId: "step-1",
- attemptId: "attempt-1",
- });
-
- const listResult = await handler({ jsonrpc: "2.0", id: 3, method: "tools/list" }) as any;
- expect((listResult.tools ?? []).map((tool: any) => tool.name)).toContain("ext.notion.search");
-
- const callResult = await callTool(handler, "ext.notion.search", { query: "roadmap" });
- expect(callResult).toMatchObject({
- content: [{ type: "text", text: "Found page" }],
- structuredContent: { pageId: "page-1" },
- isError: false,
- });
- expect(fixture.runtime.externalMcpService.callTool).toHaveBeenCalledWith(
- expect.objectContaining({
- callerId: "worker-1",
- missionId: "mission-1",
- runId: "run-1",
- }),
- "ext.notion.search",
- { query: "roadmap" },
- );
- });
- });
-
it("lets agent callers use safe mission observation coordinator tools", async () => {
await withEnv({ ADE_RUN_ID: "run-1" }, async () => {
const fixture = createRuntime();
@@ -1386,7 +1334,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: { complete: false }
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
@@ -1408,7 +1356,7 @@ describe("mcpServer", () => {
it("rejects coordinator-only tool calls from agent callers before coordinator dispatch", async () => {
await withEnv({ ADE_RUN_ID: "run-1" }, async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
@@ -1432,7 +1380,7 @@ describe("mcpServer", () => {
it("rejects standalone chat calls to ADE spawn_agent", async () => {
await withEnv({ ADE_DEFAULT_ROLE: "agent", ADE_CHAT_SESSION_ID: "chat-1" }, async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "chat-1", role: "agent" });
@@ -1460,7 +1408,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: { complete: false }
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
@@ -1502,7 +1450,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: { complete: false }
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
@@ -1528,7 +1476,7 @@ describe("mcpServer", () => {
it("still routes coordinator-only tool calls for orchestrator callers", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "coord-1", role: "orchestrator" });
const response = await callTool(handler, "spawn_worker", {
@@ -1543,7 +1491,7 @@ describe("mcpServer", () => {
it("spawns workers for active runs when project and workspace roots differ", async () => {
await withEnv({ ADE_MISSION_ID: "mission-1", ADE_RUN_ID: "run-1" }, async () => {
const fixture = createRuntime();
- fixture.runtime.workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-mcp-runtime-workspace-"));
+ fixture.runtime.workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-runtime-workspace-"));
fixture.runtime.orchestratorService.getRunGraph = vi.fn(({ runId }: any) => ({
run: {
id: runId,
@@ -1582,7 +1530,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: { complete: false },
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "coord-1",
@@ -1627,7 +1575,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: { complete: false }
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "coord-1", role: "assistant" as any });
const response = await callTool(handler, "read_mission_status", {});
@@ -1665,7 +1613,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: { complete: false }
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "coord-1", role: "agent" as any });
const response = await callTool(handler, "read_mission_status", {});
@@ -1696,7 +1644,7 @@ describe("mcpServer", () => {
process.env.ADE_STEP_ID = "step-1";
process.env.ADE_ATTEMPT_ID = "attempt-1";
try {
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "worker-1", role: "orchestrator" as any });
const response = await callTool(handler, "spawn_worker", {
@@ -1722,18 +1670,18 @@ describe("mcpServer", () => {
it("does not advertise resources to orchestrator callers", async () => {
const { runtime } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
const previousRole = process.env.ADE_DEFAULT_ROLE;
process.env.ADE_DEFAULT_ROLE = "orchestrator";
try {
const response = await handler({
jsonrpc: "2.0",
id: 99,
- method: "initialize",
+ method: "ade/initialize",
params: { identity: { callerId: "coord-1", role: "orchestrator" } }
}) as any;
- expect(response.capabilities?.tools).toBeTruthy();
+ expect(response.capabilities?.actions).toBeTruthy();
expect(response.capabilities?.resources).toBeUndefined();
} finally {
if (previousRole == null) delete process.env.ADE_DEFAULT_ROLE;
@@ -1744,7 +1692,7 @@ describe("mcpServer", () => {
it("routes reflection_add and uses initialize identity fallback", async () => {
await withEnv({ ADE_RUN_ID: "run-1" }, async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
@@ -1782,7 +1730,7 @@ describe("mcpServer", () => {
it("rejects reflection_add payloads missing strict fields", async () => {
await withEnv({ ADE_RUN_ID: "run-1" }, async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
role: "agent",
@@ -1806,7 +1754,7 @@ describe("mcpServer", () => {
it("lists retrospectives, trends, and pattern stats with caller-context fallback", async () => {
await withEnv({ ADE_RUN_ID: "run-1" }, async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
role: "agent",
@@ -1839,7 +1787,7 @@ describe("mcpServer", () => {
it("routes spawn_agent to lane-scoped tracked pty sessions", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "orchestrator" });
const response = await callTool(handler, "spawn_agent", {
@@ -1867,10 +1815,10 @@ describe("mcpServer", () => {
expect(response.structuredContent.contextRef?.path).toBeNull();
});
- it("writes spawn_agent MCP config with the authorized lane worktree", async () => {
+ it("starts spawn_agent without writing an attached ADE server config", async () => {
const fixture = createRuntime();
- fixture.runtime.workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-mcp-spawn-workspace-"));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ fixture.runtime.workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-spawn-workspace-"));
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "orchestrator", runId: "run-from-identity" });
const response = await callTool(handler, "spawn_agent", {
@@ -1884,37 +1832,17 @@ describe("mcpServer", () => {
});
expect(response?.isError).toBeUndefined();
- const configPath = path.join(
- fixture.runtime.projectRoot,
- ".ade",
- "cache",
- "orchestrator",
- "mcp-configs",
- "spawn-attempt-workspace-roots.json"
- );
- const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as {
- mcpServers: {
- ade: {
- args: string[];
- env: Record;
- };
- };
- };
-
- expect(config.mcpServers.ade.args).toContain("--project-root");
- expect(config.mcpServers.ade.args).toContain(fixture.runtime.projectRoot);
- expect(config.mcpServers.ade.args).toContain("--workspace-root");
- expect(config.mcpServers.ade.args).toContain(path.join(fixture.runtime.projectRoot, ".ade", "worktrees", "lane-1"));
- expect(config.mcpServers.ade.env.ADE_PROJECT_ROOT).toBe(fixture.runtime.projectRoot);
- expect(config.mcpServers.ade.env.ADE_WORKSPACE_ROOT).toBe(path.join(fixture.runtime.projectRoot, ".ade", "worktrees", "lane-1"));
+ expect(response.structuredContent.startupCommand).toContain("claude");
+ expect(response.structuredContent.startupCommand).toContain("ADE_RUN_ID=run-1");
+ expect(response.structuredContent.startupCommand).toContain("ADE_ATTEMPT_ID=attempt-workspace-roots");
});
it("fails closed when a requested lane does not have an available worktree", async () => {
const fixture = createRuntime();
- fixture.runtime.workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-mcp-spawn-workspace-"));
+ fixture.runtime.workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-spawn-workspace-"));
fixture.runtime.laneService.getLaneWorktreePath = vi.fn(() => null);
fixture.runtime.laneService.getLaneBaseAndBranch = vi.fn(() => null);
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "orchestrator" });
const response = await callTool(handler, "spawn_agent", {
@@ -1934,7 +1862,7 @@ describe("mcpServer", () => {
expect(fixture.runtime.ptyService.create).not.toHaveBeenCalled();
});
- it("routes coordinator report_status via MCP and mutates run metadata through coordinator tools", async () => {
+ it("routes coordinator report_status via ADE RPC and mutates run metadata through coordinator tools", async () => {
await withEnv({ ADE_RUN_ID: "run-1" }, async () => {
const fixture = createRuntime();
fixture.runtime.orchestratorService.getRunGraph = vi.fn(() => ({
@@ -1970,7 +1898,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: null
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "coord-1", role: "orchestrator", missionId: "mission-1", runId: "run-from-identity" });
const response = await callTool(handler, "report_status", {
@@ -2053,7 +1981,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: null
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "attempt-child",
@@ -2142,7 +2070,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: null
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "native-worker-1",
@@ -2221,7 +2149,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: null
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "native-worker-over-cap",
@@ -2312,7 +2240,7 @@ describe("mcpServer", () => {
completionEvaluation: null
}));
- const childHandler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const childHandler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(childHandler, {
callerId: "attempt-child",
role: "agent",
@@ -2330,7 +2258,7 @@ describe("mcpServer", () => {
});
expect(statusResponse?.isError).toBeUndefined();
- const parentHandler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const parentHandler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(parentHandler, {
callerId: "attempt-parent",
role: "agent",
@@ -2396,7 +2324,7 @@ describe("mcpServer", () => {
completionEvaluation: null
}));
- const nativeHandler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const nativeHandler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(nativeHandler, {
callerId: "native-worker-result",
role: "agent",
@@ -2415,7 +2343,7 @@ describe("mcpServer", () => {
});
expect(resultResponse?.isError).toBeUndefined();
- const parentHandler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const parentHandler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(parentHandler, {
callerId: "attempt-parent",
role: "agent",
@@ -2480,7 +2408,7 @@ describe("mcpServer", () => {
completionEvaluation: null
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "attempt-parent",
role: "agent",
@@ -2513,7 +2441,7 @@ describe("mcpServer", () => {
it("uses trusted env run context for shared-fact writes instead of initialize payload runId", async () => {
await withEnv({ ADE_RUN_ID: "run-from-env" }, async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
@@ -2579,7 +2507,7 @@ describe("mcpServer", () => {
sourceRunId: "run-1",
}
]));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
@@ -2618,7 +2546,7 @@ describe("mcpServer", () => {
it("pins memory entries through memory_pin", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "worker-1", role: "agent" });
const response = await callTool(handler, "memory_pin", { id: "memory-42" });
@@ -2630,7 +2558,7 @@ describe("mcpServer", () => {
it("exposes memory_update_core and writes CTO core memory", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "cto-1",
@@ -2658,7 +2586,7 @@ describe("mcpServer", () => {
it("routes memory_update_core to worker core memory when agent ownerId is set", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "worker-1",
@@ -2701,7 +2629,7 @@ describe("mcpServer", () => {
createdAt: "2026-03-17T19:00:00.000Z",
identityKey: "agent:worker-agent-1",
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "chat-from-identity",
@@ -2730,7 +2658,7 @@ describe("mcpServer", () => {
it("materializes compact context manifests for spawn_agent to keep prompts lightweight", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "orchestrator" });
const response = await callTool(handler, "spawn_agent", {
@@ -2754,19 +2682,19 @@ describe("mcpServer", () => {
expect(response.structuredContent.startupCommand).toContain("read-only");
const contextPath = response.structuredContent.contextRef?.path as string | null;
expect(contextPath).toBeTruthy();
- expect(contextPath?.includes("/.ade/cache/orchestrator/mcp-context/run-123/")).toBe(true);
+ expect(contextPath?.includes("/.ade/cache/orchestrator/agent-context/run-123/")).toBe(true);
if (!contextPath) {
throw new Error("Expected context manifest path");
}
expect(fs.existsSync(contextPath)).toBe(true);
const manifest = JSON.parse(fs.readFileSync(contextPath, "utf8"));
- expect(manifest.schema).toBe("ade.mcp.spawnAgentContext.v1");
+ expect(manifest.schema).toBe("ade.agent.spawnContext.v1");
expect(manifest.mission.runId).toBe("run-123");
});
it("routes run_tests for suite and ad-hoc command contracts", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
@@ -2798,7 +2726,7 @@ describe("mcpServer", () => {
it("routes ask_user to mission interventions", async () => {
await withEnv({ ADE_MISSION_ID: "mission-1", ADE_RUN_ID: "run-1" }, async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "coord-1", role: "orchestrator", missionId: "mission-1", runId: "run-1" });
const response = await callTool(handler, "ask_user", {
@@ -2840,7 +2768,7 @@ describe("mcpServer", () => {
answers: {},
responseText: null,
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "chat-session-identity",
@@ -2892,7 +2820,7 @@ describe("mcpServer", () => {
await withEnv({ ADE_CHAT_SESSION_ID: "chat-session-env" }, async () => {
const fixture = createRuntime();
fixture.runtime.agentChatService.requestChatInput = vi.fn(() => new Promise(() => {}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "chat-session-identity",
@@ -2942,7 +2870,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: { complete: false },
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "attempt-1",
@@ -3002,7 +2930,7 @@ describe("mcpServer", () => {
runtimeEvents: [],
completionEvaluation: { complete: false },
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "attempt-1",
@@ -3026,7 +2954,7 @@ describe("mcpServer", () => {
it("allows mutations for any session", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
@@ -3045,7 +2973,7 @@ describe("mcpServer", () => {
it("generates a commit message when commit_changes message is omitted", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
@@ -3069,7 +2997,7 @@ describe("mcpServer", () => {
it("returns generated commit text without creating a commit", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
@@ -3089,7 +3017,7 @@ describe("mcpServer", () => {
it("lists and imports unregistered lane worktrees", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
const listResponse = await callTool(handler, "list_unregistered_lanes", {});
@@ -3111,9 +3039,9 @@ describe("mcpServer", () => {
expect(importResponse.structuredContent.lane.id).toBe("lane-imported");
});
- it("supports core git sync operations via MCP", async () => {
+ it("supports core git sync operations via ADE RPC", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
const syncStatus = await callTool(handler, "git_get_sync_status", { laneId: "lane-1" });
@@ -3122,12 +3050,12 @@ describe("mcpServer", () => {
const push = await callTool(handler, "git_push", { laneId: "lane-1", force: true, setUpstream: false });
expect(push?.isError).toBeUndefined();
- expect(fixture.runtime.gitService.push).toHaveBeenCalledWith({ laneId: "lane-1", force: true, setUpstream: false });
+ expect(fixture.runtime.gitService.push).toHaveBeenCalledWith({ laneId: "lane-1", forceWithLease: true });
});
- it("supports create/update/comment PR actions via MCP", async () => {
+ it("supports create/update/comment PR actions via ADE RPC", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
const created = await callTool(handler, "create_pr_from_lane", {
@@ -3157,7 +3085,7 @@ describe("mcpServer", () => {
it("lists ADE actions across runtime domains", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
const response = await callTool(handler, "list_ade_actions", { domain: "git" });
@@ -3174,7 +3102,7 @@ describe("mcpServer", () => {
it("invokes ADE actions dynamically and returns status hints", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
const response = await callTool(handler, "run_ade_action", {
@@ -3196,9 +3124,26 @@ describe("mcpServer", () => {
expect(fixture.runtime.operationService.list).toHaveBeenCalledWith({ limit: 10 });
});
+ it("rejects run_ade_action when the action is not a callable on the domain service", async () => {
+ const fixture = createRuntime();
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ await initialize(handler, { callerId: "agent-1", role: "agent" });
+
+ const response = await callTool(handler, "run_ade_action", {
+ domain: "git",
+ action: "nonexistent_action",
+ args: { laneId: "lane-1" },
+ });
+
+ expect(response.isError).toBe(true);
+ expect(JSON.stringify(response.error ?? response.structuredContent ?? {})).toContain(
+ "Action 'git.nonexistent_action' is not callable.",
+ );
+ });
+
it("reads ADE action status snapshots across operation/test/chat/mission/run", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent", runId: "run-1", missionId: "mission-1" });
const response = await callTool(handler, "get_ade_action_status", {
@@ -3221,6 +3166,11 @@ describe("mcpServer", () => {
const unchanged = await callTool(handler, "get_ade_action_status", {
operationId: "op-1",
+ testRunId: "test-run-1",
+ chatSessionId: "chat-1",
+ runId: "run-1",
+ missionId: "mission-1",
+ prId: "pr-1",
previousHash: response.structuredContent.hash,
waitForMs: 0,
});
@@ -3230,7 +3180,7 @@ describe("mcpServer", () => {
it("lets agent callers stash lane changes", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
@@ -3252,7 +3202,7 @@ describe("mcpServer", () => {
it("lists lane stashes for agent callers", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "agent-1", role: "agent" });
@@ -3267,10 +3217,10 @@ describe("mcpServer", () => {
it("returns resources for lane status/conflicts", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
- const result = (await handler({ jsonrpc: "2.0", id: 4, method: "resources/list", params: {} })) as any;
+ const result = (await handler({ jsonrpc: "2.0", id: 4, method: "ade/resources/list", params: {} })) as any;
const uris = (result.resources ?? []).map((entry: any) => entry.uri);
expect(uris).toContain("ade://lane/lane-1/status");
@@ -3280,13 +3230,13 @@ describe("mcpServer", () => {
it("reads lane/status resource with the correct URI parser semantics", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
const result = (await handler({
jsonrpc: "2.0",
id: 5,
- method: "resources/read",
+ method: "ade/resources/read",
params: { uri: "ade://lane/lane-1/status" }
})) as any;
@@ -3297,7 +3247,7 @@ describe("mcpServer", () => {
it("records succeeded audit metadata for read-only tools", async () => {
const { runtime, operationStart, operationFinish } = createRuntime();
- const handler = createMcpRequestHandler({ runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler);
const response = await callTool(handler, "list_lanes", {});
@@ -3321,11 +3271,11 @@ describe("mcpServer", () => {
// Create two independent sessions (simulating session recycling)
const fixture1 = createRuntime();
- const handler1 = createMcpRequestHandler({ runtime: fixture1.runtime, serverVersion: "test" });
+ const handler1 = createAdeRpcRequestHandler({ runtime: fixture1.runtime, serverVersion: "test" });
await initialize(handler1);
const fixture2 = createRuntime();
- const handler2 = createMcpRequestHandler({ runtime: fixture2.runtime, serverVersion: "test" });
+ const handler2 = createAdeRpcRequestHandler({ runtime: fixture2.runtime, serverVersion: "test" });
await initialize(handler2);
// Fire 6 calls from session 1 (per-session limit)
@@ -3363,7 +3313,7 @@ describe("mcpServer", () => {
it("routes get_lane_status and returns lane/diff/conflict info", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
const response = await callTool(handler, "get_lane_status", { laneId: "lane-1" });
@@ -3378,7 +3328,7 @@ describe("mcpServer", () => {
it("routes check_conflicts with a single laneId", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
const response = await callTool(handler, "check_conflicts", { laneId: "lane-1" });
@@ -3392,7 +3342,7 @@ describe("mcpServer", () => {
it("routes create_lane with authorization and returns lane summary", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
const response = await callTool(handler, "create_lane", { name: "new-feature" });
@@ -3407,7 +3357,7 @@ describe("mcpServer", () => {
it("routes simulate_integration as a read-only dry-merge", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
const response = await callTool(handler, "simulate_integration", {
@@ -3424,7 +3374,7 @@ describe("mcpServer", () => {
it("routes create_queue with authorization", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
const response = await callTool(handler, "create_queue", {
@@ -3443,7 +3393,7 @@ describe("mcpServer", () => {
it("routes create_integration with authorization", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
const response = await callTool(handler, "create_integration", {
@@ -3466,7 +3416,7 @@ describe("mcpServer", () => {
it("routes rebase_lane with authorization", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
const response = await callTool(handler, "rebase_lane", {
@@ -3488,7 +3438,7 @@ describe("mcpServer", () => {
conflictingFiles: [],
error: "Worktree has uncommitted changes. Commit or stash before rebasing.",
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
const response = await callTool(handler, "rebase_lane", {
@@ -3506,7 +3456,7 @@ describe("mcpServer", () => {
it("routes get_pr_health as a read-only tool", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
const response = await callTool(handler, "get_pr_health", { prId: "pr-123" });
@@ -3518,7 +3468,7 @@ describe("mcpServer", () => {
it("routes pr_get_checks as a read-only tool", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
const response = await callTool(handler, "pr_get_checks", { prId: "pr-123" });
@@ -3542,7 +3492,7 @@ describe("mcpServer", () => {
it("routes pr_get_review_comments with actionable review context", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
const response = await callTool(handler, "pr_get_review_comments", { prId: "pr-123" });
@@ -3569,7 +3519,7 @@ describe("mcpServer", () => {
it("routes pr_refresh_issue_inventory with checks, review threads, and issue comments", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
const response = await callTool(handler, "pr_refresh_issue_inventory", { prId: "pr-123" });
@@ -3606,7 +3556,7 @@ describe("mcpServer", () => {
it("routes pr_rerun_failed_checks, pr_reply_to_review_thread, and pr_resolve_review_thread", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
@@ -3651,7 +3601,7 @@ describe("mcpServer", () => {
it("routes land_queue_next with authorization", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
const response = await callTool(handler, "land_queue_next", {
@@ -3668,7 +3618,7 @@ describe("mcpServer", () => {
it("get_lane_status returns error for unknown lane", async () => {
const fixture = createRuntime();
fixture.runtime.laneService.list = vi.fn(async () => []);
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler);
const response = await callTool(handler, "get_lane_status", { laneId: "nonexistent" });
@@ -3679,7 +3629,7 @@ describe("mcpServer", () => {
it("run_tests requires either suiteId or command", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
const response = await callTool(handler, "run_tests", { laneId: "lane-1" });
@@ -3692,7 +3642,7 @@ describe("mcpServer", () => {
it("routes create_mission with orchestration authorization", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "evaluator" });
const response = await callTool(handler, "create_mission", {
@@ -3718,7 +3668,7 @@ describe("mcpServer", () => {
it("routes start_mission and returns run info", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "evaluator" });
const response = await callTool(handler, "start_mission", {
@@ -3742,7 +3692,7 @@ describe("mcpServer", () => {
it("routes pause_mission to orchestratorService.pauseRun", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "evaluator" });
const response = await callTool(handler, "pause_mission", {
@@ -3760,7 +3710,7 @@ describe("mcpServer", () => {
it("routes resume_mission to orchestratorService.resumeRun", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "evaluator" });
const response = await callTool(handler, "resume_mission", { runId: "run-1" });
@@ -3773,7 +3723,7 @@ describe("mcpServer", () => {
it("routes cancel_mission to aiOrchestratorService.cancelRunGracefully", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "evaluator" });
const response = await callTool(handler, "cancel_mission", {
@@ -3791,7 +3741,7 @@ describe("mcpServer", () => {
it("routes steer_mission with directive", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "evaluator" });
const response = await callTool(handler, "steer_mission", {
@@ -3819,7 +3769,7 @@ describe("mcpServer", () => {
it("routes resolve_intervention with status", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "evaluator" });
const response = await callTool(handler, "resolve_intervention", {
@@ -3848,7 +3798,7 @@ describe("mcpServer", () => {
it("routes get_mission to missionService.get", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_mission", { missionId: "mission-1" });
@@ -3860,7 +3810,7 @@ describe("mcpServer", () => {
it("routes get_run_graph to orchestratorService.getRunGraph", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_run_graph", { runId: "run-1", timelineLimit: 50 });
@@ -3876,7 +3826,7 @@ describe("mcpServer", () => {
it("routes stream_events to eventBuffer.drain", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "stream_events", { cursor: 0, limit: 50 });
@@ -3890,7 +3840,7 @@ describe("mcpServer", () => {
it("routes get_step_output and filters attempts by step", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_step_output", {
@@ -3909,7 +3859,7 @@ describe("mcpServer", () => {
it("routes get_step_output returns error for unknown step", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_step_output", {
@@ -3923,7 +3873,7 @@ describe("mcpServer", () => {
it("routes get_worker_states to aiOrchestratorService.getWorkerStates", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_worker_states", { runId: "run-1" });
@@ -3937,7 +3887,7 @@ describe("mcpServer", () => {
it("routes get_timeline to orchestratorService.listTimeline", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_timeline", { runId: "run-1" });
@@ -3952,7 +3902,7 @@ describe("mcpServer", () => {
it("routes get_timeline with stepId filter", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_timeline", { runId: "run-1", stepId: "step-1" });
@@ -3965,7 +3915,7 @@ describe("mcpServer", () => {
it("routes get_mission_metrics to aiOrchestratorService.getMissionMetrics", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_mission_metrics", { missionId: "mission-1" });
@@ -3978,7 +3928,7 @@ describe("mcpServer", () => {
it("routes get_final_diff with per-lane diffs", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_final_diff", { runId: "run-1" });
@@ -3994,7 +3944,7 @@ describe("mcpServer", () => {
it("routes evaluate_run with evaluator authorization and writes to DB", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "evaluator" });
const response = await callTool(handler, "evaluate_run", {
@@ -4037,7 +3987,7 @@ describe("mcpServer", () => {
it("routes list_evaluations and returns summaries", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "list_evaluations", {
@@ -4056,7 +4006,7 @@ describe("mcpServer", () => {
it("routes get_evaluation_report with run context", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "get_evaluation_report", { evaluationId: "eval-1" });
@@ -4075,7 +4025,7 @@ describe("mcpServer", () => {
it("evaluator gets reads + orchestration + evaluation", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "evaluator" });
@@ -4100,7 +4050,7 @@ describe("mcpServer", () => {
it("any session can access observation tools", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
@@ -4126,7 +4076,7 @@ describe("mcpServer", () => {
it("stream_events returns events after cursor", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "stream_events", { cursor: 5, limit: 100 });
@@ -4146,7 +4096,7 @@ describe("mcpServer", () => {
nextCursor: cursor,
hasMore: false
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "stream_events", { cursor: 10 });
@@ -4169,7 +4119,7 @@ describe("mcpServer", () => {
nextCursor: cursor + 3,
hasMore: false
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "stream_events", {
@@ -4228,7 +4178,7 @@ describe("mcpServer", () => {
nextCursor: cursor + 4,
hasMore: false
}));
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "stream_events", {
@@ -4248,7 +4198,7 @@ describe("mcpServer", () => {
it("stream_events defaults cursor to 0 and limit to 100", async () => {
const fixture = createRuntime();
- const handler = createMcpRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
+ const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, { role: "external" });
const response = await callTool(handler, "stream_events", {});
diff --git a/apps/mcp-server/src/mcpServer.ts b/apps/ade-cli/src/adeRpcServer.ts
similarity index 95%
rename from apps/mcp-server/src/mcpServer.ts
rename to apps/ade-cli/src/adeRpcServer.ts
index 4334dc1ff..07fe172f3 100644
--- a/apps/mcp-server/src/mcpServer.ts
+++ b/apps/ade-cli/src/adeRpcServer.ts
@@ -16,6 +16,7 @@ import { loadAgentBrowserArtifactPayloadFromFile, parseAgentBrowserArtifactPaylo
import { resolveAgentMemoryWritePolicy } from "../../desktop/src/main/services/memory/memoryService";
import { ReflectionValidationError } from "../../desktop/src/main/services/orchestrator/orchestratorService";
import { getTeamMembersForRun, registerTeamMember, updateTeamMemberStatus } from "../../desktop/src/main/services/orchestrator/teamRuntimeState";
+import { launchPrIssueResolutionChat, previewPrIssueResolutionPrompt } from "../../desktop/src/main/services/prs/prIssueResolver";
import { runGit } from "../../desktop/src/main/services/git/git";
import { resolvePathWithinRoot } from "../../desktop/src/main/services/shared/utils";
import { getDefaultModelDescriptor } from "../../desktop/src/shared/modelRegistry";
@@ -31,7 +32,7 @@ import {
} from "../../desktop/src/shared/types";
import type { PrActionRun, PrCheck, PrComment, PrReviewThread } from "../../desktop/src/shared/types/prs";
import { resolveAdeLayout } from "../../desktop/src/shared/adeLayout";
-import type { AdeMcpRuntime } from "./bootstrap";
+import type { AdeRuntime } from "./bootstrap";
import { JsonRpcError, JsonRpcErrorCode, type JsonRpcHandler, type JsonRpcRequest } from "./jsonrpc";
type ToolSpec = {
@@ -189,7 +190,7 @@ const TOOL_SPECS: ToolSpec[] = [
"linear_routing",
"file",
"process",
- "external_mcp",
+ "pty",
"computer_use_artifacts",
"all"
],
@@ -234,7 +235,7 @@ const TOOL_SPECS: ToolSpec[] = [
"linear_routing",
"file",
"process",
- "external_mcp",
+ "pty",
"computer_use_artifacts",
],
},
@@ -493,7 +494,7 @@ const TOOL_SPECS: ToolSpec[] = [
additionalProperties: false,
required: ["backendStyle", "backendName"],
properties: {
- backendStyle: { type: "string", enum: ["external_mcp", "external_cli", "manual", "local_fallback"] },
+ backendStyle: { type: "string", enum: ["external_cli", "manual", "local_fallback"] },
backendName: { type: "string", minLength: 1 },
toolName: { type: "string" },
command: { type: "string" },
@@ -1031,6 +1032,40 @@ const TOOL_SPECS: ToolSpec[] = [
}
}
},
+ {
+ name: "pr_preview_issue_resolution_prompt",
+ description: "Preview the ADE Path to Merge issue-resolution prompt for a PR without launching an agent.",
+ inputSchema: {
+ type: "object",
+ required: ["prId", "scope", "modelId"],
+ additionalProperties: false,
+ properties: {
+ prId: { type: "string", minLength: 1 },
+ scope: { type: "string", enum: ["checks", "comments", "both"] },
+ modelId: { type: "string", minLength: 1 },
+ reasoning: { type: "string" },
+ permissionMode: { type: "string", enum: ["read_only", "guarded_edit", "full_edit"] },
+ additionalInstructions: { type: "string" }
+ }
+ }
+ },
+ {
+ name: "pr_start_issue_resolution",
+ description: "Start a Path to Merge issue-resolution agent session for failing checks and/or review comments on a PR.",
+ inputSchema: {
+ type: "object",
+ required: ["prId", "scope", "modelId"],
+ additionalProperties: false,
+ properties: {
+ prId: { type: "string", minLength: 1 },
+ scope: { type: "string", enum: ["checks", "comments", "both"] },
+ modelId: { type: "string", minLength: 1 },
+ reasoning: { type: "string" },
+ permissionMode: { type: "string", enum: ["read_only", "guarded_edit", "full_edit"] },
+ additionalInstructions: { type: "string" }
+ }
+ }
+ },
{
name: "pr_rerun_failed_checks",
description: "Rerun failed CI checks for a pull request.",
@@ -1850,6 +1885,7 @@ const READ_ONLY_TOOLS = new Set([
"pr_get_checks",
"pr_get_review_comments",
"pr_refresh_issue_inventory",
+ "pr_preview_issue_resolution_prompt",
"memory_search",
"get_cto_state",
"listChats",
@@ -1896,6 +1932,7 @@ const MUTATION_TOOLS = new Set([
"rebase_abort",
"land_queue_next",
"pr_rerun_failed_checks",
+ "pr_start_issue_resolution",
"pr_reply_to_review_thread",
"pr_resolve_review_thread",
"memory_add",
@@ -2181,23 +2218,6 @@ function jsonText(value: unknown): string {
return JSON.stringify(value, null, 2);
}
-function mcpTextResult(value: unknown, isError = false): Record {
- const text = typeof value === "string" ? value : jsonText(value);
- return {
- content: [{ type: "text", text }],
- structuredContent: value,
- ...(isError ? { isError: true } : {})
- };
-}
-
-function isMcpToolResult(value: unknown): value is {
- content: unknown[];
- structuredContent?: unknown;
- isError?: boolean;
-} {
- return isRecord(value) && Array.isArray(value.content);
-}
-
function sanitizeForAudit(value: unknown, depth = 0): unknown {
if (depth > 4) return "[depth-clipped]";
if (value == null) return value;
@@ -2224,9 +2244,9 @@ function sanitizeForAudit(value: unknown, depth = 0): unknown {
return String(value);
}
-function requirePrService(runtime: AdeMcpRuntime): NonNullable {
+function requirePrService(runtime: AdeRuntime): NonNullable {
if (!runtime.prService) {
- throw new JsonRpcError(JsonRpcErrorCode.internalError, "prService is not available in this MCP runtime configuration");
+ throw new JsonRpcError(JsonRpcErrorCode.internalError, "prService is not available in this ADE runtime configuration");
}
return runtime.prService;
}
@@ -2343,45 +2363,45 @@ function summarizePrIssueInventory(args: {
};
}
-function requireAgentChatService(runtime: AdeMcpRuntime): NonNullable {
+function requireAgentChatService(runtime: AdeRuntime): NonNullable {
if (!runtime.agentChatService) {
throw new JsonRpcError(
JsonRpcErrorCode.internalError,
- "agentChatService is not available in this MCP runtime configuration",
+ "agentChatService is not available in this ADE runtime configuration",
);
}
return runtime.agentChatService;
}
-function requireLinearSyncService(runtime: AdeMcpRuntime): NonNullable {
+function requireLinearSyncService(runtime: AdeRuntime): NonNullable {
if (!runtime.linearSyncService) {
- throw new JsonRpcError(JsonRpcErrorCode.internalError, "linearSyncService is not available in this MCP runtime configuration");
+ throw new JsonRpcError(JsonRpcErrorCode.internalError, "linearSyncService is not available in this ADE runtime configuration");
}
return runtime.linearSyncService;
}
-function requireLinearIngressService(runtime: AdeMcpRuntime): NonNullable {
+function requireLinearIngressService(runtime: AdeRuntime): NonNullable {
if (!runtime.linearIngressService) {
- throw new JsonRpcError(JsonRpcErrorCode.internalError, "linearIngressService is not available in this MCP runtime configuration");
+ throw new JsonRpcError(JsonRpcErrorCode.internalError, "linearIngressService is not available in this ADE runtime configuration");
}
return runtime.linearIngressService;
}
-function requireFlowPolicyService(runtime: AdeMcpRuntime): NonNullable {
+function requireFlowPolicyService(runtime: AdeRuntime): NonNullable {
if (!runtime.flowPolicyService) {
- throw new JsonRpcError(JsonRpcErrorCode.internalError, "flowPolicyService is not available in this MCP runtime configuration");
+ throw new JsonRpcError(JsonRpcErrorCode.internalError, "flowPolicyService is not available in this ADE runtime configuration");
}
return runtime.flowPolicyService;
}
-function requireLinearRoutingService(runtime: AdeMcpRuntime): NonNullable {
+function requireLinearRoutingService(runtime: AdeRuntime): NonNullable {
if (!runtime.linearRoutingService) {
- throw new JsonRpcError(JsonRpcErrorCode.internalError, "linearRoutingService is not available in this MCP runtime configuration");
+ throw new JsonRpcError(JsonRpcErrorCode.internalError, "linearRoutingService is not available in this ADE runtime configuration");
}
return runtime.linearRoutingService;
}
-async function resolveDefaultLaneId(runtime: AdeMcpRuntime): Promise {
+async function resolveDefaultLaneId(runtime: AdeRuntime): Promise {
await runtime.laneService.ensurePrimaryLane().catch(() => {});
const lanes = await runtime.laneService.list({ includeArchived: false, includeStatus: false });
const laneId = (lanes.find((lane) => lane.laneType === "primary") ?? lanes[0])?.id?.trim?.() || "";
@@ -2391,7 +2411,7 @@ async function resolveDefaultLaneId(runtime: AdeMcpRuntime): Promise {
return laneId;
}
-function resolveChatSessionLaneId(runtime: AdeMcpRuntime, session: SessionState): string | null {
+function resolveChatSessionLaneId(runtime: AdeRuntime, session: SessionState): string | null {
const chatSessionId = asOptionalTrimmedString(session.identity.chatSessionId);
if (!chatSessionId) return null;
const chatSession = runtime.sessionService.get(chatSessionId);
@@ -2399,7 +2419,7 @@ function resolveChatSessionLaneId(runtime: AdeMcpRuntime, session: SessionState)
return laneId.length ? laneId : null;
}
-function resolveLaneWorktreePath(runtime: AdeMcpRuntime, laneId: string | null | undefined): string | null {
+function resolveLaneWorktreePath(runtime: AdeRuntime, laneId: string | null | undefined): string | null {
const normalizedLaneId = asOptionalTrimmedString(laneId);
if (!normalizedLaneId) return null;
try {
@@ -2423,7 +2443,7 @@ function resolveLaneWorktreePath(runtime: AdeMcpRuntime, laneId: string | null |
return null;
}
-function resolveRunContextLaneId(runtime: AdeMcpRuntime, callerCtx: CallerContext): string | null {
+function resolveRunContextLaneId(runtime: AdeRuntime, callerCtx: CallerContext): string | null {
const runId = asOptionalTrimmedString(callerCtx.runId);
if (!runId) return null;
@@ -2442,7 +2462,7 @@ function resolveRunContextLaneId(runtime: AdeMcpRuntime, callerCtx: CallerContex
}
function resolveAuthorizedWorkspaceRoot(
- runtime: AdeMcpRuntime,
+ runtime: AdeRuntime,
session: SessionState,
toolArgs?: Record,
): string {
@@ -2488,7 +2508,7 @@ function resolveAuthorizedWorkspaceRoot(
}
function resolveRequestedOrSessionLaneId(
- runtime: AdeMcpRuntime,
+ runtime: AdeRuntime,
session: SessionState,
toolArgs: Record,
): string | null {
@@ -2496,7 +2516,7 @@ function resolveRequestedOrSessionLaneId(
}
function requireLaneIdForTool(
- runtime: AdeMcpRuntime,
+ runtime: AdeRuntime,
session: SessionState,
toolArgs: Record,
toolName: string,
@@ -2512,7 +2532,7 @@ function requireLaneIdForTool(
}
async function runCtoOperatorBridgeTool(
- runtime: AdeMcpRuntime,
+ runtime: AdeRuntime,
session: SessionState,
name: string,
toolArgs: Record,
@@ -2530,7 +2550,7 @@ async function runCtoOperatorBridgeTool(
: null)
?? fallbackModelId;
const tools = createCtoOperatorTools({
- currentSessionId: session.identity.callerId || "mcp-cto",
+ currentSessionId: session.identity.callerId || "ade-cli-cto",
defaultLaneId,
defaultModelId,
sessionService: runtime.sessionService,
@@ -2615,7 +2635,7 @@ function normalizeToolWhitelist(value: unknown): string[] {
}
function resolveSpawnContextFile(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
laneId: string;
provider: "codex" | "claude";
permissionMode: SpawnPermissionMode;
@@ -2657,7 +2677,7 @@ function resolveSpawnContextFile(args: {
};
}
- const baseDir = resolveAdeLayout(args.runtime.projectRoot).mcpContextDir;
+ const baseDir = resolveAdeLayout(args.runtime.projectRoot).agentContextDir;
const runSegment = args.runId ?? "standalone";
const dir = path.join(baseDir, runSegment);
fs.mkdirSync(dir, { recursive: true });
@@ -2665,7 +2685,7 @@ function resolveSpawnContextFile(args: {
const filename = `${Date.now()}-${randomUUID()}.json`;
const contextFilePath = path.join(dir, filename);
const payload = {
- schema: "ade.mcp.spawnAgentContext.v1",
+ schema: "ade.agent.spawnContext.v1",
generatedAt: nowIso(),
mission: {
runId: args.runId,
@@ -2806,7 +2826,7 @@ function resolveWorkerAgentOwnerId(identityKey: unknown): string | null {
}
async function resolveEffectiveCallerContext(
- runtime: AdeMcpRuntime,
+ runtime: AdeRuntime,
session?: SessionState,
): Promise {
const callerCtx = { ...resolveCallerContext(session) };
@@ -2827,28 +2847,6 @@ async function resolveEffectiveCallerContext(
return callerCtx;
}
-function toExternalMcpIdentity(callerCtx: CallerContext): {
- callerId: string;
- role: "cto" | "orchestrator" | "agent" | "external" | "evaluator";
- chatSessionId: string | null;
- missionId: string | null;
- runId: string | null;
- stepId: string | null;
- attemptId: string | null;
- ownerId: string | null;
-} {
- return {
- callerId: callerCtx.callerId ?? callerCtx.chatSessionId ?? callerCtx.attemptId ?? "unknown",
- role: callerCtx.role ?? "external",
- chatSessionId: callerCtx.chatSessionId,
- missionId: callerCtx.missionId,
- runId: callerCtx.runId,
- stepId: callerCtx.stepId,
- attemptId: callerCtx.attemptId,
- ownerId: callerCtx.ownerId,
- };
-}
-
function isStandaloneChatCaller(callerCtx: CallerContext): boolean {
return callerCtx.standaloneChatSession;
}
@@ -2876,15 +2874,8 @@ function isLocalComputerUseAllowed(policy: ComputerUsePolicy | null | undefined)
return isComputerUseModeEnabled(effective.mode) && effective.allowLocalFallback;
}
-async function listToolSpecsForSession(runtime: AdeMcpRuntime, session: SessionState): Promise {
+async function listToolSpecsForSession(runtime: AdeRuntime, session: SessionState): Promise {
const callerCtx = await resolveEffectiveCallerContext(runtime, session);
- const externalToolSpecs = runtime.externalMcpService
- ? (await runtime.externalMcpService.listToolsForIdentity(toExternalMcpIdentity(callerCtx))).map((tool) => ({
- name: tool.namespacedName,
- description: tool.description ?? `${tool.serverName}: ${tool.name}`,
- inputSchema: tool.inputSchema,
- }))
- : [];
const externalComputerUseAvailable = runtime.computerUseArtifactBrokerService
?.getBackendStatus()
?.backends.some((backend) => backend.available) ?? false;
@@ -2898,21 +2889,21 @@ async function listToolSpecsForSession(runtime: AdeMcpRuntime, session: SessionS
: COORDINATOR_TOOL_SPECS;
const allVisibleTools = (() => {
if (callerCtx.role === "external" || !callerCtx.role) {
- return [...visibleBaseTools, ...externalToolSpecs];
+ return visibleBaseTools;
}
if (callerCtx.role === "agent") {
- return [...visibleBaseTools, ...AGENT_VISIBLE_COORDINATOR_TOOL_SPECS, ...externalToolSpecs];
+ return [...visibleBaseTools, ...AGENT_VISIBLE_COORDINATOR_TOOL_SPECS];
}
if (callerCtx.role === "cto") {
- return [...visibleBaseTools, ...CTO_OPERATOR_TOOL_SPECS, ...CTO_LINEAR_SYNC_TOOL_SPECS, ...externalToolSpecs];
+ return [...visibleBaseTools, ...CTO_OPERATOR_TOOL_SPECS, ...CTO_LINEAR_SYNC_TOOL_SPECS];
}
- return [...visibleBaseTools, ...visibleCoordinatorTools, ...externalToolSpecs];
+ return [...visibleBaseTools, ...visibleCoordinatorTools];
})();
return allVisibleTools.filter((tool) => !isToolHiddenForStandaloneChat(tool.name, callerCtx));
}
-function parseInitializeIdentity(runtime: AdeMcpRuntime, params: unknown): SessionIdentity {
+function parseInitializeIdentity(runtime: AdeRuntime, params: unknown): SessionIdentity {
const data = safeObject(params);
const identity = safeObject(data.identity);
const envContext = resolveEnvCallerContext();
@@ -2961,7 +2952,7 @@ function parseInitializeIdentity(runtime: AdeMcpRuntime, params: unknown): Sessi
};
}
-function parseMcpUri(uriRaw: string): { path: string[] } {
+function parseAdeResourceUri(uriRaw: string): { path: string[] } {
const trimmed = uriRaw.trim();
if (!trimmed.startsWith("ade://")) {
throw new JsonRpcError(JsonRpcErrorCode.invalidParams, `Unsupported resource URI: ${uriRaw}`);
@@ -3004,7 +2995,7 @@ function buildResourceList(args: {
}
async function waitForTestRunCompletion(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
runId: string;
laneId: string;
timeoutMs: number;
@@ -3060,10 +3051,10 @@ type AdeActionDomain =
| "linear_routing"
| "file"
| "process"
- | "external_mcp"
+ | "pty"
| "computer_use_artifacts";
-function getAdeActionDomainServices(runtime: AdeMcpRuntime): Partial | null | undefined>> {
+function getAdeActionDomainServices(runtime: AdeRuntime): Partial | null | undefined>> {
return {
lane: runtime.laneService as unknown as Record,
git: runtime.gitService as unknown as Record,
@@ -3090,7 +3081,7 @@ function getAdeActionDomainServices(runtime: AdeMcpRuntime): Partial | null,
file: (runtime.fileService ?? null) as unknown as Record | null,
process: (runtime.processService ?? null) as unknown as Record | null,
- external_mcp: runtime.externalMcpService as unknown as Record,
+ pty: runtime.ptyService as unknown as Record,
computer_use_artifacts: runtime.computerUseArtifactBrokerService as unknown as Record,
};
}
@@ -3102,7 +3093,7 @@ function listAdeActionNames(service: Record): string[] {
}
async function waitForSessionCompletion(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
ptyId: string;
sessionId: string;
timeoutMs: number;
@@ -3140,7 +3131,7 @@ async function waitForSessionCompletion(args: {
};
}
-async function buildLaneStatus(runtime: AdeMcpRuntime, laneId: string): Promise> {
+async function buildLaneStatus(runtime: AdeRuntime, laneId: string): Promise> {
const lanes = await runtime.laneService.list({ includeArchived: true });
const lane = lanes.find((entry) => entry.id === laneId);
if (!lane) {
@@ -3224,9 +3215,9 @@ type CoordinatorToolCacheEntry = {
tools: Record;
};
-const coordinatorToolCacheByRuntime = new WeakMap>();
+const coordinatorToolCacheByRuntime = new WeakMap>();
-function resolveMissionIdForRun(runtime: AdeMcpRuntime, runId: string): string | null {
+function resolveMissionIdForRun(runtime: AdeRuntime, runId: string): string | null {
const graphMissionId = (() => {
try {
const graph = runtime.orchestratorService.getRunGraph({ runId, timelineLimit: 0 });
@@ -3249,7 +3240,7 @@ function resolveMissionIdForRun(runtime: AdeMcpRuntime, runId: string): string |
return asOptionalTrimmedString(row?.mission_id);
}
-function resolveRunIdForMission(runtime: AdeMcpRuntime, missionId: string): string | null {
+function resolveRunIdForMission(runtime: AdeRuntime, missionId: string): string | null {
const row = runtime.db.get<{ id: string | null }>(
`
select id
@@ -3273,7 +3264,7 @@ function resolveRunIdForMission(runtime: AdeMcpRuntime, missionId: string): stri
}
function getCoordinatorToolSet(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
runId: string;
missionId: string;
}): Record {
@@ -3337,14 +3328,14 @@ type NativeCallerRegistration = {
sourceAttemptId: string;
};
-function getTeamRuntimeContext(runtime: AdeMcpRuntime): import("../../desktop/src/main/services/orchestrator/orchestratorContext").OrchestratorContext {
+function getTeamRuntimeContext(runtime: AdeRuntime): import("../../desktop/src/main/services/orchestrator/orchestratorContext").OrchestratorContext {
return {
db: runtime.db,
logger: runtime.logger,
} as import("../../desktop/src/main/services/orchestrator/orchestratorContext").OrchestratorContext;
}
-function getRunGraphSafe(runtime: AdeMcpRuntime, runId: string): Record | null {
+function getRunGraphSafe(runtime: AdeRuntime, runId: string): Record | null {
try {
return runtime.orchestratorService.getRunGraph({ runId, timelineLimit: 0 }) as unknown as Record;
} catch {
@@ -3352,7 +3343,7 @@ function getRunGraphSafe(runtime: AdeMcpRuntime, runId: string): Record> {
+function getTeamMembersForRunSafe(runtime: AdeRuntime, runId: string): Array> {
const aiService = runtime.aiOrchestratorService as unknown as { getTeamMembers?: (args: { runId: string }) => unknown };
if (typeof aiService.getTeamMembers === "function") {
try {
@@ -3416,7 +3407,7 @@ function deriveQuestionOwnerFromPhase(args: {
}
function getAgentAskUserPolicy(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
callerCtx: CallerContext;
}): {
stepId: string | null;
@@ -3657,7 +3648,7 @@ function inferParallelismCap(graph: Record): number {
}
function ensureNativeTeammateRegistration(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
runId: string;
missionId: string;
callerCtx: CallerContext;
@@ -3782,7 +3773,7 @@ function ensureNativeTeammateRegistration(args: {
}
async function maybeSendInterAgentMessage(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
missionId: string;
fromAttemptId: string;
toAttemptId: string;
@@ -3809,7 +3800,7 @@ async function maybeSendInterAgentMessage(args: {
}
async function postProcessCoordinatorToolResult(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
toolName: string;
runId: string;
missionId: string;
@@ -3902,7 +3893,7 @@ async function postProcessCoordinatorToolResult(args: {
}
async function runCoordinatorTool(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
name: string;
toolArgs: Record;
callerCtx: CallerContext;
@@ -3986,7 +3977,7 @@ async function runCoordinatorTool(args: {
async function runTool(args: {
- runtime: AdeMcpRuntime;
+ runtime: AdeRuntime;
session: SessionState;
name: string;
toolArgs: Record;
@@ -4040,13 +4031,13 @@ async function runTool(args: {
if (!isComputerUseModeEnabled(effectivePolicy.mode)) {
throw new JsonRpcError(
JsonRpcErrorCode.policyDenied,
- `${toolName} is disabled because computer use is off for this ADE MCP session.`,
+ `${toolName} is disabled because computer use is off for this ADE ADE RPC session.`,
);
}
if (!effectivePolicy.allowLocalFallback) {
throw new JsonRpcError(
JsonRpcErrorCode.policyDenied,
- `${toolName} is disabled because local computer-use fallback is not allowed for this ADE MCP session.`,
+ `${toolName} is disabled because local computer-use fallback is not allowed for this ADE ADE RPC session.`,
);
}
const capabilities = getLocalComputerUseCapabilities();
@@ -4109,13 +4100,6 @@ async function runTool(args: {
await sleep(250);
};
- if (name.startsWith("ext.")) {
- if (!runtime.externalMcpService) {
- throw new JsonRpcError(JsonRpcErrorCode.methodNotFound, `Unsupported tool: ${name}`);
- }
- return await runtime.externalMcpService.callTool(toExternalMcpIdentity(callerCtx), name, toolArgs);
- }
-
if (CTO_OPERATOR_TOOL_NAMES.has(name)) {
if (callerCtx.role !== "cto") {
throw new JsonRpcError(JsonRpcErrorCode.methodNotFound, `Unsupported tool: ${name}`);
@@ -4282,7 +4266,7 @@ async function runTool(args: {
if (testRunId) {
const run = runtime.testService.listRuns({ limit: 200 }).find((entry) => entry.id === testRunId) ?? null;
payload.testRun = run;
- if (run) payload.testRunLogTail = runtime.testService.getLogTail(testRunId, 16_000);
+ if (run) payload.testRunLogTail = runtime.testService.getLogTail({ runId: testRunId, maxBytes: 16_000 });
}
if (chatSessionId && runtime.agentChatService) {
payload.chatSession = await runtime.agentChatService.getSessionSummary(chatSessionId);
@@ -4711,13 +4695,13 @@ async function runTool(args: {
if (!isComputerUseModeEnabled(effectivePolicy.mode)) {
throw new JsonRpcError(
JsonRpcErrorCode.policyDenied,
- `${name} is disabled because computer use is off for this ADE MCP session.`,
+ `${name} is disabled because computer use is off for this ADE ADE RPC session.`,
);
}
if (!effectivePolicy.allowLocalFallback) {
throw new JsonRpcError(
JsonRpcErrorCode.policyDenied,
- `${name} is disabled because local computer-use fallback is not allowed for this ADE MCP session.`,
+ `${name} is disabled because local computer-use fallback is not allowed for this ADE ADE RPC session.`,
);
}
const capabilities = getLocalComputerUseCapabilities();
@@ -4884,7 +4868,7 @@ async function runTool(args: {
}
if (name === "ingest_computer_use_artifacts") {
- const backendStyle = assertNonEmptyString(toolArgs.backendStyle, "backendStyle") as "external_mcp" | "external_cli" | "manual" | "local_fallback";
+ const backendStyle = assertNonEmptyString(toolArgs.backendStyle, "backendStyle") as "external_cli" | "manual" | "local_fallback";
const backendName = assertNonEmptyString(toolArgs.backendName, "backendName");
const manifestPath = asOptionalTrimmedString(toolArgs.manifestPath);
let inputs = Array.isArray(toolArgs.inputs) ? toolArgs.inputs.map((entry) => safeObject(entry)) : [];
@@ -5268,7 +5252,7 @@ async function runTool(args: {
laneId,
cols: DEFAULT_PTY_COLS,
rows: DEFAULT_PTY_ROWS,
- title: `MCP Test: ${commandText}`,
+ title: `ADE Test: ${commandText}`,
tracked: true,
toolType: "shell",
startupCommand: commandText
@@ -5322,9 +5306,8 @@ async function runTool(args: {
if (name === "git_push") {
const laneId = requireLaneIdForTool(runtime, session, toolArgs, "git_push");
- const force = asBoolean(toolArgs.force, false);
- const setUpstream = asBoolean(toolArgs.setUpstream, true);
- const action = await runtime.gitService.push({ laneId, force, setUpstream });
+ const force = asBoolean(toolArgs.forceWithLease, asBoolean(toolArgs.force, false));
+ const action = await runtime.gitService.push({ laneId, forceWithLease: force });
return { laneId, action };
}
@@ -5537,7 +5520,7 @@ async function runTool(args: {
laneId,
baseBranch,
title,
- ...(body ? { body } : {}),
+ body: body ?? "",
draft,
});
return { pr };
@@ -5610,6 +5593,72 @@ async function runTool(args: {
});
}
+ if (name === "pr_preview_issue_resolution_prompt" || name === "pr_start_issue_resolution") {
+ const prId = assertNonEmptyString(toolArgs.prId, "prId");
+ const scope = assertNonEmptyString(toolArgs.scope, "scope");
+ if (scope !== "checks" && scope !== "comments" && scope !== "both") {
+ throw new JsonRpcError(JsonRpcErrorCode.invalidParams, "scope must be one of checks, comments, or both.");
+ }
+ const modelId = assertNonEmptyString(toolArgs.modelId, "modelId");
+ const permissionMode = asOptionalTrimmedString(toolArgs.permissionMode);
+ if (
+ permissionMode
+ && permissionMode !== "read_only"
+ && permissionMode !== "guarded_edit"
+ && permissionMode !== "full_edit"
+ ) {
+ throw new JsonRpcError(JsonRpcErrorCode.invalidParams, "permissionMode must be one of read_only, guarded_edit, or full_edit.");
+ }
+ const issueResolutionArgs = {
+ prId,
+ scope,
+ modelId,
+ reasoning: asOptionalTrimmedString(toolArgs.reasoning),
+ ...(permissionMode ? { permissionMode } : {}),
+ additionalInstructions: asOptionalTrimmedString(toolArgs.additionalInstructions),
+ };
+ const deps = {
+ prService: requirePrService(runtime),
+ laneService: runtime.laneService,
+ agentChatService: requireAgentChatService(runtime),
+ sessionService: runtime.sessionService,
+ issueInventoryService: runtime.issueInventoryService,
+ };
+
+ if (name === "pr_preview_issue_resolution_prompt") {
+ return await previewPrIssueResolutionPrompt(deps, issueResolutionArgs as any);
+ }
+
+ const result = await launchPrIssueResolutionChat(deps, issueResolutionArgs as any);
+ let convergenceRuntime: unknown = null;
+ try {
+ const status = runtime.issueInventoryService.getConvergenceStatus(prId);
+ convergenceRuntime = runtime.issueInventoryService.saveConvergenceRuntime(prId, {
+ currentRound: status.currentRound,
+ status: "running",
+ pollerStatus: "idle",
+ activeSessionId: result.sessionId,
+ activeLaneId: result.laneId,
+ activeHref: result.href,
+ lastStartedAt: nowIso(),
+ errorMessage: null,
+ pauseReason: null,
+ });
+ } catch (error) {
+ runtime.logger.warn("rpc.pr_issue_resolution_convergence_persist_failed", {
+ prId,
+ sessionId: result.sessionId,
+ laneId: result.laneId,
+ href: result.href,
+ error: error instanceof Error ? error.message : String(error),
+ });
+ }
+ return {
+ ...result,
+ convergenceRuntime,
+ };
+ }
+
if (name === "pr_rerun_failed_checks") {
const prId = assertNonEmptyString(toolArgs.prId, "prId");
await requirePrService(runtime).rerunChecks({ prId });
@@ -5685,6 +5734,13 @@ async function runTool(args: {
const laneId = assertNonEmptyString(toolArgs.laneId, "laneId");
+ const laneWorktreePath = resolveLaneWorktreePath(runtime, laneId);
+ if (!laneWorktreePath) {
+ throw new JsonRpcError(
+ JsonRpcErrorCode.invalidParams,
+ `Requested lane '${laneId}' does not have an available worktree.`,
+ );
+ }
const provider = asTrimmedString(toolArgs.provider) === "claude" ? "claude" : "codex";
const model = asOptionalTrimmedString(toolArgs.model);
const permissionMode = parseSpawnPermissionMode(toolArgs.permissionMode);
@@ -5695,7 +5751,7 @@ async function runTool(args: {
const attemptId = asOptionalTrimmedString(toolArgs.attemptId);
const toolWhitelist = normalizeToolWhitelist(toolArgs.toolWhitelist);
const title = stripInjectionChars(
- asOptionalTrimmedString(toolArgs.title) ?? `MCP Agent (${provider}${permissionMode === "plan" ? " · plan" : ""})`
+ asOptionalTrimmedString(toolArgs.title) ?? `ADE Agent (${provider}${permissionMode === "plan" ? " · plan" : ""})`
);
const context = safeObject(toolArgs.context);
@@ -5745,39 +5801,8 @@ async function runTool(args: {
permissionMode === "plan" ? "plan" : permissionMode === "full-auto" ? "bypassPermissions" : "acceptEdits";
commandParts.push("--permission-mode", claudePermission);
- // Bind ADE MCP server to Claude workers via --mcp-config
- if (runId && attemptId) {
- const mcpConfigDir = resolveAdeLayout(runtime.projectRoot).mcpConfigsDir;
- fs.mkdirSync(mcpConfigDir, { recursive: true });
- const mcpConfigPath = path.join(mcpConfigDir, `spawn-${attemptId ?? Date.now()}.json`);
- const builtEntry = path.join(runtime.projectRoot, "apps", "mcp-server", "dist", "index.cjs");
- const srcEntry = path.join(runtime.projectRoot, "apps", "mcp-server", "src", "index.ts");
- const workerWorkspaceRoot = resolveAuthorizedWorkspaceRoot(runtime, session, toolArgs);
- const mcpCmd = fs.existsSync(builtEntry) ? "node" : "npx";
- const mcpArgs = fs.existsSync(builtEntry)
- ? [builtEntry, "--project-root", runtime.projectRoot, "--workspace-root", workerWorkspaceRoot]
- : ["tsx", srcEntry, "--project-root", runtime.projectRoot, "--workspace-root", workerWorkspaceRoot];
- const mcpConfig = {
- mcpServers: {
- ade: {
- command: mcpCmd,
- args: mcpArgs,
- env: {
- ADE_PROJECT_ROOT: runtime.projectRoot,
- ADE_WORKSPACE_ROOT: workerWorkspaceRoot,
- ADE_MISSION_ID: callerCtx.missionId ?? "",
- ADE_RUN_ID: runId,
- ADE_STEP_ID: stepId ?? "",
- ADE_ATTEMPT_ID: attemptId,
- ADE_DEFAULT_ROLE: "agent",
- ADE_OWNER_ID: callerCtx.ownerId ?? "",
- }
- }
- }
- };
- fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
- commandParts.push("--mcp-config", shellEscapeArg(mcpConfigPath));
- }
+ // ADE-owned actions are exposed through the `ade` CLI. Child agent
+ // sessions receive identity env vars below instead of an attached server.
}
if (finalPrompt) {
commandParts.push(shellEscapeArg(finalPrompt));
@@ -6307,11 +6332,11 @@ async function runTool(args: {
};
}
- throw new JsonRpcError(JsonRpcErrorCode.methodNotFound, `Unknown MCP tool: ${name}`);
+ throw new JsonRpcError(JsonRpcErrorCode.methodNotFound, `Unknown ADE action: ${name}`);
}
-async function readResource(runtime: AdeMcpRuntime, uri: string): Promise> {
- const parsed = parseMcpUri(uri);
+async function readResource(runtime: AdeRuntime, uri: string): Promise> {
+ const parsed = parseAdeResourceUri(uri);
const [head, ...tail] = parsed.path;
if (head === "lane") {
@@ -6347,12 +6372,12 @@ async function readResource(runtime: AdeMcpRuntime, uri: string): Promise void) | null;
+ onActionsListChanged?: (() => void) | null;
}): JsonRpcHandler & { dispose: () => void } {
- const { runtime, serverVersion, onToolsListChanged } = args;
+ const { runtime, serverVersion, onActionsListChanged } = args;
const session: SessionState = {
initialized: false,
@@ -6386,31 +6411,18 @@ export function createMcpRequestHandler(args: {
}
};
- const disposeExternalMcpSubscription = typeof runtime.externalMcpService.onEvent === "function"
- ? runtime.externalMcpService.onEvent((event) => {
- if (!session.initialized) return;
- if (
- event.type === "configs-changed"
- || event.type === "server-state-changed"
- || event.type === "tools-refreshed"
- ) {
- onToolsListChanged?.();
- }
- })
- : () => {};
-
- const auditToolCall = async (
- toolName: string,
- toolArgs: Record,
+ const auditActionCall = async (
+ actionName: string,
+ actionArgs: Record,
runner: () => Promise
): Promise => {
const startedAt = Date.now();
- const laneId = resolveRequestedOrSessionLaneId(runtime, session, toolArgs);
+ const laneId = resolveRequestedOrSessionLaneId(runtime, session, actionArgs);
const operation = runtime.operationService.start({
laneId,
- kind: "mcp_tool_call",
+ kind: "ade_action_call",
metadata: {
- tool: toolName,
+ action: actionName,
callerId: session.identity.callerId,
role: session.identity.role,
chatSessionId: session.identity.chatSessionId,
@@ -6419,7 +6431,7 @@ export function createMcpRequestHandler(args: {
stepId: session.identity.stepId,
attemptId: session.identity.attemptId,
ownerId: session.identity.ownerId,
- args: sanitizeForAudit(toolArgs)
+ args: sanitizeForAudit(actionArgs)
}
});
@@ -6449,23 +6461,51 @@ export function createMcpRequestHandler(args: {
}
};
+ const listActions = async (): Promise> => ({
+ actions: (await listToolSpecsForSession(runtime, session)).map((tool) => ({
+ name: tool.name,
+ description: tool.description,
+ inputSchema: sanitizeToolSchema(tool.inputSchema),
+ })),
+ });
+
+ const callAction = async (actionName: string, actionArgs: Record): Promise => {
+ return await auditActionCall(actionName, actionArgs, async () => {
+ if (
+ READ_ONLY_TOOLS.has(actionName) ||
+ MUTATION_TOOLS.has(actionName) ||
+ ORCHESTRATION_TOOLS.has(actionName) ||
+ OBSERVATION_TOOLS.has(actionName) ||
+ EVALUATOR_TOOLS.has(actionName) ||
+ EVALUATION_READ_TOOLS.has(actionName) ||
+ COORDINATOR_TOOL_NAMES.has(actionName) ||
+ actionName === "spawn_agent" ||
+ actionName === "ask_user"
+ ) {
+ return await runTool({ runtime, session, name: actionName, toolArgs: actionArgs });
+ }
+
+ throw new JsonRpcError(JsonRpcErrorCode.methodNotFound, `Unsupported ADE action: ${actionName}`);
+ });
+ };
+
const handler = (async (request: JsonRpcRequest): Promise => {
const method = typeof request.method === "string" ? request.method : "";
const params = safeObject(request.params);
- if (method === "initialize") {
+ if (method === "ade/initialize") {
session.initialized = true;
session.protocolVersion = asOptionalTrimmedString(params.protocolVersion) ?? DEFAULT_PROTOCOL_VERSION;
session.identity = parseInitializeIdentity(runtime, params);
const resourcesEnabled = session.identity.role !== "orchestrator";
return {
protocolVersion: session.protocolVersion,
- serverInfo: {
- name: "ade-mcp-server",
+ runtimeInfo: {
+ name: "ade-rpc",
version: serverVersion
},
capabilities: {
- tools: {
+ actions: {
listChanged: true
},
...(resourcesEnabled
@@ -6480,7 +6520,7 @@ export function createMcpRequestHandler(args: {
};
}
- if (method === "notifications/initialized") {
+ if (method === "ade/initialized") {
return null;
}
@@ -6492,63 +6532,28 @@ export function createMcpRequestHandler(args: {
return { pong: true, at: nowIso() };
}
- if (method === "tools/list") {
- return {
- tools: (await listToolSpecsForSession(runtime, session)).map((tool) => ({
- name: tool.name,
- description: tool.description,
- inputSchema: sanitizeToolSchema(tool.inputSchema)
- }))
- };
+ if (method === "ade/actions/list") {
+ return await listActions();
}
- if (method === "tools/call") {
- const toolName = assertNonEmptyString(params.name, "name");
- const toolArgs = safeObject(params.arguments);
-
+ if (method === "ade/actions/call") {
+ const actionName = assertNonEmptyString(params.name, "name");
+ const actionArgs = safeObject(params.arguments);
try {
- const result = await auditToolCall(toolName, toolArgs, async () => {
- if (toolName.startsWith("ext.")) {
- return await runTool({ runtime, session, name: toolName, toolArgs });
- }
-
- if (
- READ_ONLY_TOOLS.has(toolName) ||
- MUTATION_TOOLS.has(toolName) ||
- ORCHESTRATION_TOOLS.has(toolName) ||
- OBSERVATION_TOOLS.has(toolName) ||
- EVALUATOR_TOOLS.has(toolName) ||
- EVALUATION_READ_TOOLS.has(toolName) ||
- COORDINATOR_TOOL_NAMES.has(toolName) ||
- toolName === "spawn_agent" ||
- toolName === "ask_user"
- ) {
- return await runTool({ runtime, session, name: toolName, toolArgs });
- }
-
- throw new JsonRpcError(JsonRpcErrorCode.methodNotFound, `Unsupported tool: ${toolName}`);
- });
-
- if (toolName.startsWith("ext.") && isRecord(result) && isMcpToolResult(result.result)) {
- return result.result;
- }
- return mcpTextResult(result);
+ return await callAction(actionName, actionArgs);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
- return mcpTextResult(
- {
- ok: false,
- error: {
- code: error instanceof JsonRpcError ? error.code : JsonRpcErrorCode.toolFailed,
- message
- }
+ return {
+ ok: false,
+ error: {
+ code: error instanceof JsonRpcError ? error.code : JsonRpcErrorCode.toolFailed,
+ message,
},
- true
- );
+ };
}
}
- if (method === "resources/list") {
+ if (method === "ade/resources/list") {
const lanes = await runtime.laneService.list({ includeArchived: false });
const laneRecords = lanes as unknown as Array>;
return {
@@ -6558,7 +6563,7 @@ export function createMcpRequestHandler(args: {
};
}
- if (method === "resources/read") {
+ if (method === "ade/resources/read") {
const uri = assertNonEmptyString(params.uri, "uri");
return await readResource(runtime, uri);
}
@@ -6575,9 +6580,7 @@ export function createMcpRequestHandler(args: {
throw new JsonRpcError(JsonRpcErrorCode.methodNotFound, `Method not found: ${method}`);
}) as JsonRpcHandler & { dispose: () => void };
- handler.dispose = () => {
- disposeExternalMcpSubscription();
- };
+ handler.dispose = () => {};
return handler;
}
diff --git a/apps/mcp-server/src/bootstrap.test.ts b/apps/ade-cli/src/bootstrap.test.ts
similarity index 100%
rename from apps/mcp-server/src/bootstrap.test.ts
rename to apps/ade-cli/src/bootstrap.test.ts
diff --git a/apps/mcp-server/src/bootstrap.ts b/apps/ade-cli/src/bootstrap.ts
similarity index 88%
rename from apps/mcp-server/src/bootstrap.ts
rename to apps/ade-cli/src/bootstrap.ts
index d211cf107..d83eee2b0 100644
--- a/apps/mcp-server/src/bootstrap.ts
+++ b/apps/ade-cli/src/bootstrap.ts
@@ -32,7 +32,6 @@ import { createOrchestratorService } from "../../desktop/src/main/services/orche
import { createAiOrchestratorService } from "../../desktop/src/main/services/orchestrator/aiOrchestratorService";
import { createAiIntegrationService } from "../../desktop/src/main/services/ai/aiIntegrationService";
import { createMissionBudgetService } from "../../desktop/src/main/services/orchestrator/missionBudgetService";
-import { createExternalMcpService, type ExternalMcpService } from "../../desktop/src/main/services/externalMcp/externalMcpService";
import {
createComputerUseArtifactBrokerService,
type ComputerUseArtifactBrokerService,
@@ -90,7 +89,7 @@ export function createEventBuffer(capacity = 10_000): EventBuffer {
};
}
-export type AdeMcpPaths = {
+export type AdeRuntimePaths = {
adeDir: string;
logsDir: string;
processLogsDir: string;
@@ -108,12 +107,12 @@ export type AdeMcpPaths = {
missionStateDir: string;
};
-export type AdeMcpRuntime = {
+export type AdeRuntime = {
projectRoot: string;
workspaceRoot: string;
projectId: string;
project: { rootPath: string; displayName: string; baseRef: string };
- paths: AdeMcpPaths;
+ paths: AdeRuntimePaths;
logger: Logger;
db: AdeDb;
laneService: ReturnType;
@@ -140,7 +139,6 @@ export type AdeMcpRuntime = {
linearIngressService?: ReturnType | null;
linearRoutingService?: ReturnType | null;
processService?: ReturnType | null;
- externalMcpService: ExternalMcpService;
computerUseArtifactBrokerService: ComputerUseArtifactBrokerService;
orchestratorService: ReturnType;
aiOrchestratorService: ReturnType;
@@ -148,7 +146,7 @@ export type AdeMcpRuntime = {
dispose: () => void;
};
-export function ensureAdePaths(projectRoot: string): AdeMcpPaths {
+export function ensureAdePaths(projectRoot: string): AdeRuntimePaths {
const { paths } = initializeOrRepairAdeProject(projectRoot);
return {
adeDir: paths.adeDir,
@@ -169,7 +167,7 @@ export function ensureAdePaths(projectRoot: string): AdeMcpPaths {
};
}
-export async function createAdeMcpRuntime(args: { projectRoot: string; workspaceRoot?: string } | string): Promise {
+export async function createAdeRuntime(args: { projectRoot: string; workspaceRoot?: string } | string): Promise {
const resolvedArgs = typeof args === "string"
? { projectRoot: args, workspaceRoot: args }
: args;
@@ -184,7 +182,7 @@ export async function createAdeMcpRuntime(args: { projectRoot: string; workspace
const baseRef = await detectDefaultBaseRef(projectRoot);
const paths = ensureAdePaths(projectRoot);
- const logger = createFileLogger(path.join(paths.logsDir, "mcp-server.jsonl"));
+ const logger = createFileLogger(path.join(paths.logsDir, "ade-cli.jsonl"));
const db = await openKvDb(paths.dbPath, logger);
const project = toProjectInfo(projectRoot, baseRef);
@@ -226,6 +224,7 @@ export async function createAdeMcpRuntime(args: { projectRoot: string; workspace
logger,
projectConfigService,
projectRoot,
+ enableDynamicModelMetadata: false,
});
const conflictService = createConflictService({
@@ -259,7 +258,6 @@ export async function createAdeMcpRuntime(args: { projectRoot: string; workspace
const ptyService = createPtyService({
projectRoot,
transcriptsDir: paths.transcriptsDir,
- chatSessionsDir: paths.chatSessionsDir,
laneService,
sessionService,
logger,
@@ -280,7 +278,7 @@ export async function createAdeMcpRuntime(args: { projectRoot: string; workspace
});
const issueInventoryService = createIssueInventoryService({ db });
- // Ensure MCP-specific tables exist (evaluation framework)
+ // Ensure evaluation tables exist for headless runtime checks.
db.run(`
CREATE TABLE IF NOT EXISTS orchestrator_evaluations (
id TEXT PRIMARY KEY,
@@ -337,49 +335,6 @@ export async function createAdeMcpRuntime(args: { projectRoot: string; workspace
aiIntegrationService,
projectConfigService,
});
- const externalMcpService = createExternalMcpService({
- projectRoot,
- adeDir: paths.adeDir,
- db,
- projectId,
- logger,
- workerAgentService,
- ctoStateService,
- missionService,
- workerBudgetService,
- missionBudgetService,
- });
- try {
- await externalMcpService.start();
- } catch (error) {
- logger.warn("external_mcp.bootstrap_start_failed", {
- error: error instanceof Error ? error.message : String(error),
- });
- }
- const externalMcpConfigWatcher = (() => {
- try {
- const watcher = fs.watch(paths.adeDir, (_eventType, fileName) => {
- if (String(fileName ?? "").trim() !== "local.secret.yaml") return;
- externalMcpService.reload();
- });
- watcher.on("error", (error) => {
- logger.warn("external_mcp.bootstrap_watch_runtime_failed", {
- error: error instanceof Error ? error.message : String(error),
- });
- try {
- watcher.close();
- } catch {
- // Ignore watcher shutdown errors during degraded headless startup.
- }
- });
- return watcher;
- } catch (error) {
- logger.warn("external_mcp.bootstrap_watch_failed", {
- error: error instanceof Error ? error.message : String(error),
- });
- return null;
- }
- })();
const orchestratorService = createOrchestratorService({
db,
@@ -414,7 +369,6 @@ export async function createAdeMcpRuntime(args: { projectRoot: string; workspace
projectRoot,
missionService,
orchestratorService,
- externalMcpService,
logger,
});
@@ -448,7 +402,6 @@ export async function createAdeMcpRuntime(args: { projectRoot: string; workspace
aiOrchestratorService,
workerAgentService,
workerBudgetService,
- externalMcpService,
computerUseArtifactBrokerService,
orchestratorService,
openExternal: async () => {},
@@ -486,7 +439,6 @@ export async function createAdeMcpRuntime(args: { projectRoot: string; workspace
linearIngressService: headlessLinearServices.linearIngressService,
linearRoutingService: headlessLinearServices.linearRoutingService,
processService: headlessLinearServices.processService,
- externalMcpService,
computerUseArtifactBrokerService,
orchestratorService,
aiOrchestratorService,
@@ -494,8 +446,6 @@ export async function createAdeMcpRuntime(args: { projectRoot: string; workspace
dispose: () => {
const swallow = (fn: () => void) => { try { fn(); } catch { /* ignore */ } };
swallow(() => headlessLinearServices.dispose());
- swallow(() => externalMcpConfigWatcher?.close());
- void externalMcpService.dispose().catch(() => {});
swallow(() => aiOrchestratorService.dispose());
swallow(() => testService.disposeAll());
swallow(() => ptyService.disposeAll());
diff --git a/apps/ade-cli/src/cli.test.ts b/apps/ade-cli/src/cli.test.ts
new file mode 100644
index 000000000..858a02cf8
--- /dev/null
+++ b/apps/ade-cli/src/cli.test.ts
@@ -0,0 +1,251 @@
+import { describe, expect, it } from "vitest";
+import { buildCliPlan, formatOutput, parseCliArgs, renderLaneGraph, summarizeExecution } from "./cli";
+
+describe("ADE CLI", () => {
+ it("parses global options without stealing command flags", () => {
+ const parsed = parseCliArgs([
+ "--project-root",
+ "/tmp/project",
+ "--role",
+ "cto",
+ "actions",
+ "run",
+ "git.stageFile",
+ "--arg",
+ "laneId=lane-1",
+ ]);
+
+ expect(parsed.options.projectRoot).toBe("/tmp/project");
+ expect(parsed.options.role).toBe("cto");
+ expect(parsed.command).toEqual(["actions", "run", "git.stageFile", "--arg", "laneId=lane-1"]);
+ });
+
+ it("builds a generic ADE action invocation", () => {
+ const plan = buildCliPlan(["actions", "run", "git.stageFile", "--arg", "laneId=lane-1", "--arg", "path=src/index.ts"]);
+ expect(plan.kind).toBe("execute");
+ if (plan.kind !== "execute") return;
+
+ expect(plan.steps).toEqual([
+ {
+ key: "result",
+ method: "ade/actions/call",
+ params: {
+ name: "run_ade_action",
+ arguments: {
+ domain: "git",
+ action: "stageFile",
+ args: {
+ laneId: "lane-1",
+ path: "src/index.ts",
+ },
+ },
+ },
+ unwrapToolResult: true,
+ },
+ ]);
+ });
+
+ it("maps Path to Merge start to pipeline settings plus resolver tool", () => {
+ const plan = buildCliPlan([
+ "prs",
+ "path-to-merge",
+ "pr-1",
+ "--model",
+ "gpt-5.4",
+ "--max-rounds",
+ "3",
+ "--no-auto-merge",
+ ]);
+ expect(plan.kind).toBe("execute");
+ if (plan.kind !== "execute") return;
+
+ expect(plan.steps).toHaveLength(2);
+ expect(plan.steps[0]?.params).toEqual({
+ name: "run_ade_action",
+ arguments: {
+ domain: "issue_inventory",
+ action: "savePipelineSettings",
+ argsList: ["pr-1", { maxRounds: 3, autoMerge: false }],
+ },
+ });
+ expect(plan.steps[1]?.params).toEqual({
+ name: "pr_start_issue_resolution",
+ arguments: {
+ prId: "pr-1",
+ scope: "both",
+ modelId: "gpt-5.4",
+ },
+ });
+ });
+
+ it("validates required arguments before service execution", () => {
+ expect(() => buildCliPlan(["lanes", "create"])).toThrow(/name is required/);
+ expect(() => buildCliPlan(["lanes", "child", "--name", "child"])).toThrow(/parent lane is required/);
+ expect(() => buildCliPlan(["diff", "file", "--lane", "main"])).toThrow(/path is required/);
+ expect(() => buildCliPlan(["files", "write", "src/index.ts"])).toThrow(/--text, --from-file, or --stdin/);
+ expect(() => buildCliPlan(["chat", "send", "hello"])).toThrow(/message text is required/);
+ expect(() => buildCliPlan(["agent", "spawn", "--prompt", "fix it"])).toThrow(/laneId is required/);
+ expect(() => buildCliPlan(["tests", "run", "--lane", "main"])).toThrow(/--suite or --command/);
+ });
+
+ it("unwraps typed ADE action results while preserving actions run envelopes", () => {
+ const connection = {
+ mode: "headless" as const,
+ projectRoot: "/tmp/project",
+ workspaceRoot: "/tmp/project",
+ socketPath: "/tmp/project/.ade/ade.sock",
+ request: async () => null,
+ close: () => {},
+ };
+
+ const typed = summarizeExecution({
+ plan: { kind: "execute", label: "git status", steps: [] },
+ connection,
+ values: {
+ result: {
+ domain: "git",
+ action: "getStatus",
+ result: { clean: true },
+ statusHints: {},
+ },
+ },
+ } as any);
+ expect(typed).toEqual({ clean: true });
+
+ const escapeHatch = summarizeExecution({
+ plan: { kind: "execute", label: "action run", steps: [] },
+ connection,
+ values: {
+ result: {
+ domain: "git",
+ action: "getStatus",
+ result: { clean: true },
+ statusHints: {},
+ },
+ },
+ } as any);
+ expect(escapeHatch).toMatchObject({ domain: "git", action: "getStatus", result: { clean: true } });
+ });
+
+ it("renders richer doctor text", () => {
+ const output = formatOutput({
+ ok: true,
+ cliVersion: "0.0.0",
+ mode: "headless",
+ projectRoot: "/tmp/project",
+ workspaceRoot: "/tmp/project",
+ project: { projectInitialized: true },
+ desktop: { socketAvailable: false, socketPath: "/tmp/project/.ade/ade.sock" },
+ actions: { rpcActionCount: 10, actionCount: 42 },
+ git: { message: "Git repository detected on main." },
+ github: { message: "GitHub remote detected and a local auth mechanism is available." },
+ linear: { message: "Linear credentials are present locally." },
+ providers: { message: "AI provider configuration or provider CLI availability was detected locally." },
+ computerUse: { message: "Local macOS computer-use fallback commands are available." },
+ path: { message: "ade is available on PATH." },
+ recommendation: "Using live ADE desktop state.",
+ recommendations: [],
+ }, {
+ projectRoot: null,
+ workspaceRoot: null,
+ role: "agent",
+ headless: false,
+ requireSocket: false,
+ pretty: true,
+ text: true,
+ timeoutMs: 1000,
+ }, "doctor");
+
+ expect(output).toContain("ADE doctor");
+ expect(output).toContain("cli version");
+ expect(output).toContain("service actions");
+ expect(output).toContain("Git repository detected");
+ });
+
+ it("renders a compact lane graph", () => {
+ const graph = renderLaneGraph({
+ lanes: [
+ { id: "main", name: "main", branchRef: "main" },
+ { id: "child", name: "child", branchRef: "feature", parentLaneId: "main" },
+ ],
+ });
+
+ expect(graph).toContain("ADE lanes");
+ expect(graph).toContain("+- main [main]");
+ expect(graph).toContain("+- child [feature]");
+ });
+
+ it("accepts --option=value syntax equivalently to --option value", () => {
+ const spaced = parseCliArgs(["--project-root", "/tmp/project", "--role", "cto", "lanes", "list"]);
+ const joined = parseCliArgs(["--project-root=/tmp/project", "--role=cto", "lanes", "list"]);
+ expect(joined.options.projectRoot).toBe(spaced.options.projectRoot);
+ expect(joined.options.role).toBe("cto");
+ expect(joined.command).toEqual(["lanes", "list"]);
+ });
+
+ it("rejects invalid --role values", () => {
+ expect(() => parseCliArgs(["--role", "bogus", "lanes", "list"])).toThrow(
+ /--role must be one of/,
+ );
+ });
+
+ it("maps default lanes/git/prs subcommands to the right RPC actions", () => {
+ const lanes = buildCliPlan(["lanes", "list"]);
+ expect(lanes.kind).toBe("execute");
+ if (lanes.kind !== "execute") return;
+ expect(lanes.visualizer).toBe("lanes");
+ expect(lanes.steps[0]?.params).toEqual({
+ name: "list_lanes",
+ arguments: { includeArchived: false },
+ });
+
+ const git = buildCliPlan(["git", "status"]);
+ expect(git.kind).toBe("execute");
+ if (git.kind !== "execute") return;
+ expect(git.steps[0]?.params).toEqual({
+ name: "git_get_sync_status",
+ arguments: {},
+ });
+
+ const prs = buildCliPlan(["prs", "list"]);
+ expect(prs.kind).toBe("execute");
+ if (prs.kind !== "execute") return;
+ expect(prs.steps[0]?.params).toEqual({
+ name: "run_ade_action",
+ arguments: { domain: "pr", action: "listAll", args: {} },
+ });
+ });
+
+ it("renders an empty lane graph placeholder when no lanes are returned", () => {
+ expect(renderLaneGraph({ lanes: [] })).toBe("ADE lanes\n(no lanes)");
+ expect(renderLaneGraph(null)).toBe("ADE lanes\n(no lanes)");
+ });
+
+ it("attaches a rendered lane graph when the plan has the lanes visualizer", () => {
+ const connection = {
+ mode: "headless" as const,
+ projectRoot: "/tmp/project",
+ workspaceRoot: "/tmp/project",
+ socketPath: "/tmp/project/.ade/ade.sock",
+ request: async () => null,
+ close: () => {},
+ };
+ const summarized = summarizeExecution({
+ plan: { kind: "execute", label: "lanes list", steps: [], visualizer: "lanes" },
+ connection,
+ values: {
+ result: {
+ lanes: [
+ { id: "main", name: "main", branchRef: "main" },
+ { id: "child", name: "child", branchRef: "feature", parentLaneId: "main" },
+ ],
+ },
+ },
+ } as any);
+ expect(summarized).toMatchObject({
+ lanes: expect.any(Array),
+ });
+ expect((summarized as any).visual).toContain("+- main [main]");
+ expect((summarized as any).visual).toContain("+- child [feature]");
+ });
+});
diff --git a/apps/ade-cli/src/cli.ts b/apps/ade-cli/src/cli.ts
new file mode 100644
index 000000000..b33c2f7eb
--- /dev/null
+++ b/apps/ade-cli/src/cli.ts
@@ -0,0 +1,2622 @@
+#!/usr/bin/env node
+import { Buffer } from "node:buffer";
+import { spawnSync } from "node:child_process";
+import fs from "node:fs";
+import net from "node:net";
+import path from "node:path";
+import { type JsonRpcHandler, type JsonRpcId, type JsonRpcRequest } from "./jsonrpc";
+
+type JsonObject = Record;
+
+type GlobalOptions = {
+ projectRoot: string | null;
+ workspaceRoot: string | null;
+ role: "cto" | "orchestrator" | "agent" | "external" | "evaluator";
+ headless: boolean;
+ requireSocket: boolean;
+ pretty: boolean;
+ text: boolean;
+ timeoutMs: number;
+};
+
+type ParsedCli = {
+ options: GlobalOptions;
+ command: string[];
+};
+
+type InvocationStep = {
+ key: string;
+ method: string;
+ params?: JsonObject;
+ unwrapToolResult?: boolean;
+ optional?: boolean;
+};
+
+type FormatterId =
+ | "status"
+ | "doctor"
+ | "auth"
+ | "lanes"
+ | "lane-detail"
+ | "git-status"
+ | "diff-summary"
+ | "file-read"
+ | "files-tree"
+ | "files-search"
+ | "prs-list"
+ | "pr-detail"
+ | "pr-checks"
+ | "pr-comments"
+ | "run-defs"
+ | "run-runtime"
+ | "chat-list"
+ | "tests-runs"
+ | "proof-list"
+ | "actions-list"
+ | "action-result";
+
+type CliPlan =
+ | { kind: "help"; text: string }
+ | { kind: "execute"; label: string; steps: InvocationStep[]; visualizer?: "lanes"; summary?: "status" | "doctor" | "auth"; formatter?: FormatterId };
+
+type CliConnection = {
+ mode: "desktop-socket" | "headless";
+ projectRoot: string;
+ workspaceRoot: string;
+ socketPath: string;
+ request: (method: string, params?: JsonObject) => Promise;
+ close: () => Promise | void;
+};
+
+class CliUsageError extends Error {}
+
+class CliToolError extends Error {
+ details: unknown;
+
+ constructor(message: string, details: unknown) {
+ super(message);
+ this.details = details;
+ }
+}
+
+class CliExecutionError extends Error {
+ details: JsonObject;
+
+ constructor(message: string, details: JsonObject) {
+ super(message);
+ this.details = details;
+ }
+}
+
+type ReadinessCheck = {
+ ready: boolean;
+ status: "ready" | "warning" | "missing" | "unavailable";
+ message: string;
+ nextAction?: string;
+ details?: JsonObject;
+};
+
+const VERSION = "0.0.0";
+const PROTOCOL_VERSION = "2025-06-18";
+
+const ADE_BANNER = String.raw`
+ _ ____ _____
+ / \ | _ \| ____|
+ / _ \ | | | | _|
+ / ___ \| |_| | |___
+ /_/ \_\____/|_____|
+`;
+
+const TOP_LEVEL_HELP = `${ADE_BANNER}
+ Agent-focused command-line interface for ADE
+
+ $ ade help Display help for a command
+ $ ade auth status Check local ADE CLI readiness
+ $ ade doctor Inspect project, socket, runtime, and tool availability
+ $ ade lanes list | show | create | child Work with lanes and lane stacks
+ $ ade git status | commit | push | stash Run ADE-aware git operations
+ $ ade diff changes | file Inspect lane diffs
+ $ ade files tree | read | write | search Read and edit lane workspaces
+ $ ade prs list | create | path-to-merge Manage PRs, queues, and Path to Merge repair rounds
+ $ ade run defs | ps | start | logs Manage Run tab process definitions and runtime
+ $ ade shell start | write | resize | close Launch and control tracked shell sessions
+ $ ade chat list | create | send | interrupt Work with ADE agent chats
+ $ ade agent spawn --lane --prompt Launch an agent session in ADE
+ $ ade cto state | chats Operate CTO state and Work chats
+ $ ade linear workflows | run | sync Operate Linear routing and sync workflows
+ $ ade coordinator Call coordinator runtime tools
+ $ ade tests list | run | stop | runs | logs Run configured test suites
+ $ ade proof status | list | screenshot | record Manage proof and computer-use artifacts
+ $ ade memory add | search | pin Use ADE memory
+ $ ade settings action Call project config actions
+ $ ade actions list | run | status Escape hatch for every ADE service action
+
+ Global options:
+ --project-root --workspace-root --headless --socket --json --text --timeout-ms
+
+ Common agent flows:
+ $ ade lanes create --name fix-login
+ $ ade git commit --lane
+ $ ade prs create --lane --base main --draft
+ $ ade prs path-to-merge --model --max-rounds 3 --no-auto-merge
+ $ ade proof record --seconds 20
+
+ Escape hatch:
+ $ ade actions list --text
+ $ ade actions run --arg key=value
+
+ try: ade lanes list --text
+`;
+
+const HELP_BY_COMMAND: Record = {
+ lanes: `${ADE_BANNER}
+ Lanes
+
+ $ ade lanes list --text Show the lane stack graph
+ $ ade lanes show --text Inspect one lane
+ $ ade lanes create --name Create a lane from the current context
+ $ ade lanes child --lane --name Create a child lane
+ $ ade lanes import --branch Bring an existing worktree/branch into ADE
+ $ ade lanes actions List lane service actions
+`,
+ git: `${ADE_BANNER}
+ Git
+
+ $ ade git status --lane --text Show ADE-aware sync status
+ $ ade git commit --lane [-m ] Commit, generating a message when omitted
+ $ ade git push --lane --set-upstream Push through ADE
+ $ ade git stash push|list|apply|pop Use ADE lane stash actions
+ $ ade git rebase --lane --ai Rebase with ADE conflict support
+ $ ade diff changes --lane --text Inspect changed files
+`,
+ prs: `${ADE_BANNER}
+ Pull requests
+
+ $ ade prs list --text List PRs known to ADE
+ $ ade prs create --lane --base main Open a PR from a lane
+ $ ade prs checks --text Show check status
+ $ ade prs comments --text Show unresolved review work
+ $ ade prs inventory Refresh ADE issue inventory
+ $ ade prs path-to-merge --model --max-rounds 3 --no-auto-merge
+ $ ade prs resolve-thread --thread Resolve a review thread
+`,
+ run: `${ADE_BANNER}
+ Run tab
+
+ $ ade run defs --text List configured run commands
+ $ ade run ps --lane --text List process runtime state
+ $ ade run start --lane Start a process in a lane
+ $ ade run logs --run --text Tail process logs
+ $ ade run stack start --stack --lane Start a process stack
+`,
+ files: `${ADE_BANNER}
+ Files
+
+ $ ade files workspaces --text List workspace roots
+ $ ade files tree --workspace --path src Show a workspace tree
+ $ ade files read --workspace --text Read a file
+ $ ade files write --workspace --stdin
+ $ ade files search --workspace -q Search text in a workspace
+`,
+ proof: `${ADE_BANNER}
+ Proof and computer use
+
+ $ ade proof status --text Show local proof backend capabilities
+ $ ade proof list --text List captured artifacts
+ $ ade proof screenshot Capture a screenshot artifact
+ $ ade proof record --seconds 20 Capture a short video proof
+ $ ade proof ingest --input-json '{...}' Ingest external proof artifacts
+`,
+ actions: `${ADE_BANNER}
+ ADE actions
+
+ $ ade actions list --text Domain-grouped action catalog
+ $ ade actions list --domain git Narrow the catalog
+ $ ade actions run git.stageFile --arg laneId= --arg path=src/index.ts
+ $ ade actions run --input-json '{"key":"value"}'
+ $ ade actions status --text Runtime action availability
+`,
+};
+
+function isRecord(value: unknown): value is JsonObject {
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
+}
+
+function asString(value: unknown): string | null {
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
+}
+
+function parseBooleanEnv(value: string | undefined): boolean {
+ const normalized = value?.trim().toLowerCase();
+ return normalized === "1" || normalized === "true" || normalized === "yes";
+}
+
+function parsePrimitive(value: string): unknown {
+ const trimmed = value.trim();
+ if (trimmed === "true") return true;
+ if (trimmed === "false") return false;
+ if (trimmed === "null") return null;
+ if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(trimmed)) {
+ const parsed = Number(trimmed);
+ if (Number.isFinite(parsed)) return parsed;
+ }
+ return value;
+}
+
+function parseJson(value: string, label: string): unknown {
+ try {
+ return JSON.parse(value);
+ } catch (error) {
+ throw new CliUsageError(`${label} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
+ }
+}
+
+function parseObjectJson(value: string, label: string): JsonObject {
+ const parsed = parseJson(value, label);
+ if (!isRecord(parsed)) {
+ throw new CliUsageError(`${label} must be a JSON object.`);
+ }
+ return parsed;
+}
+
+function parseAssignment(value: string, label: string): { key: string; value: string } {
+ const index = value.indexOf("=");
+ if (index <= 0) {
+ throw new CliUsageError(`${label} must use key=value syntax.`);
+ }
+ const key = value.slice(0, index).trim();
+ if (!key.length) {
+ throw new CliUsageError(`${label} is missing a key.`);
+ }
+ return { key, value: value.slice(index + 1) };
+}
+
+function setPath(target: JsonObject, key: string, value: unknown): void {
+ const parts = key.split(".").map((part) => part.trim()).filter(Boolean);
+ if (parts.length === 0) {
+ throw new CliUsageError("Argument key cannot be empty.");
+ }
+ let cursor: JsonObject = target;
+ for (const part of parts.slice(0, -1)) {
+ const existing = cursor[part];
+ if (!isRecord(existing)) {
+ const next: JsonObject = {};
+ cursor[part] = next;
+ cursor = next;
+ continue;
+ }
+ cursor = existing;
+ }
+ cursor[parts[parts.length - 1]!] = value;
+}
+
+function readValue(args: string[], names: string[]): string | null {
+ for (let index = 0; index < args.length; index += 1) {
+ const token = args[index];
+ if (!token) continue;
+ const matchedName = names.find((name) => token === name || token.startsWith(`${name}=`));
+ if (!matchedName) continue;
+ if (token.includes("=")) {
+ args.splice(index, 1);
+ return token.slice(token.indexOf("=") + 1);
+ }
+ const value = args[index + 1];
+ if (value == null) {
+ throw new CliUsageError(`${token} requires a value.`);
+ }
+ args.splice(index, 2);
+ return value;
+ }
+ return null;
+}
+
+function readFlag(args: string[], names: string[]): boolean {
+ for (let index = 0; index < args.length; index += 1) {
+ if (!names.includes(args[index]!)) continue;
+ args.splice(index, 1);
+ return true;
+ }
+ return false;
+}
+
+function firstPositional(args: string[]): string | null {
+ const index = args.findIndex((arg) => arg !== "--" && !arg.startsWith("-"));
+ if (index < 0) return null;
+ const [value] = args.splice(index, 1);
+ return value ?? null;
+}
+
+function collectGenericObjectArgs(args: string[], base: JsonObject = {}): JsonObject {
+ const input: JsonObject = { ...base };
+ while (true) {
+ const inputJson = readValue(args, ["--input-json", "--json-input", "--input"]);
+ if (inputJson != null) {
+ Object.assign(input, parseObjectJson(inputJson, "--input-json"));
+ continue;
+ }
+
+ const rawArg = readValue(args, ["--arg", "--set"]);
+ if (rawArg != null) {
+ const { key, value } = parseAssignment(rawArg, "--arg");
+ setPath(input, key, parsePrimitive(value));
+ continue;
+ }
+
+ const jsonArg = readValue(args, ["--arg-json", "--set-json"]);
+ if (jsonArg != null) {
+ const { key, value } = parseAssignment(jsonArg, "--arg-json");
+ setPath(input, key, parseJson(value, `--arg-json ${key}`));
+ continue;
+ }
+
+ break;
+ }
+ return input;
+}
+
+function readLaneId(args: string[]): string | null {
+ return readValue(args, ["--lane", "--lane-id"]) ?? null;
+}
+
+function readPrId(args: string[]): string | null {
+ return readValue(args, ["--pr", "--pr-id"]) ?? null;
+}
+
+function readIntOption(args: string[], names: string[], fallback?: number): number | undefined {
+ const value = readValue(args, names);
+ if (value == null) return fallback;
+ const parsed = Number.parseInt(value, 10);
+ if (!Number.isFinite(parsed)) {
+ throw new CliUsageError(`${names[0]} must be an integer.`);
+ }
+ return parsed;
+}
+
+function readNumberOption(args: string[], names: string[], fallback?: number): number | undefined {
+ const value = readValue(args, names);
+ if (value == null) return fallback;
+ const parsed = Number(value);
+ if (!Number.isFinite(parsed)) {
+ throw new CliUsageError(`${names[0]} must be a number.`);
+ }
+ return parsed;
+}
+
+function requireValue(value: string | null, label: string): string {
+ if (value && value.trim().length > 0) return value.trim();
+ throw new CliUsageError(`${label} is required.`);
+}
+
+function maybePut(target: JsonObject, key: string, value: unknown): void {
+ if (value !== undefined && value !== null && value !== "") {
+ target[key] = value;
+ }
+}
+
+function parseCliArgs(argv: string[]): ParsedCli {
+ const command: string[] = [];
+ const options: GlobalOptions = {
+ projectRoot: null,
+ workspaceRoot: null,
+ role: (asString(process.env.ADE_DEFAULT_ROLE) as GlobalOptions["role"] | null) ?? "agent",
+ headless: parseBooleanEnv(process.env.ADE_CLI_HEADLESS),
+ requireSocket: false,
+ pretty: true,
+ text: false,
+ timeoutMs: 10 * 60 * 1000,
+ };
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const token = argv[index]!;
+ if (token === "--") {
+ command.push(token, ...argv.slice(index + 1));
+ break;
+ }
+ if (token === "--project-root") {
+ options.projectRoot = path.resolve(requireValue(argv[index + 1] ?? null, "--project-root"));
+ index += 1;
+ continue;
+ }
+ if (token.startsWith("--project-root=")) {
+ options.projectRoot = path.resolve(requireValue(token.slice("--project-root=".length), "--project-root"));
+ continue;
+ }
+ if (token === "--workspace-root") {
+ options.workspaceRoot = path.resolve(requireValue(argv[index + 1] ?? null, "--workspace-root"));
+ index += 1;
+ continue;
+ }
+ if (token.startsWith("--workspace-root=")) {
+ options.workspaceRoot = path.resolve(requireValue(token.slice("--workspace-root=".length), "--workspace-root"));
+ continue;
+ }
+ if (token === "--role") {
+ options.role = parseRole(requireValue(argv[index + 1] ?? null, "--role"));
+ index += 1;
+ continue;
+ }
+ if (token.startsWith("--role=")) {
+ options.role = parseRole(requireValue(token.slice("--role=".length), "--role"));
+ continue;
+ }
+ if (token === "--headless" || token === "--no-socket") {
+ options.headless = true;
+ continue;
+ }
+ if (token === "--socket") {
+ options.requireSocket = true;
+ options.headless = false;
+ continue;
+ }
+ if (token === "--compact") {
+ options.pretty = false;
+ continue;
+ }
+ if (token === "--pretty") {
+ options.pretty = true;
+ continue;
+ }
+ if (token === "--text") {
+ options.text = true;
+ continue;
+ }
+ if (token === "--json") {
+ options.text = false;
+ continue;
+ }
+ if (token === "--timeout-ms") {
+ const parsed = Number.parseInt(requireValue(argv[index + 1] ?? null, "--timeout-ms"), 10);
+ if (!Number.isFinite(parsed) || parsed <= 0) {
+ throw new CliUsageError("--timeout-ms must be a positive integer.");
+ }
+ options.timeoutMs = parsed;
+ index += 1;
+ continue;
+ }
+ command.push(token);
+ }
+
+ return { options, command };
+}
+
+function parseRole(value: string): GlobalOptions["role"] {
+ if (value === "cto" || value === "orchestrator" || value === "agent" || value === "external" || value === "evaluator") {
+ return value;
+ }
+ throw new CliUsageError("--role must be one of cto, orchestrator, agent, external, or evaluator.");
+}
+
+function actionCallStep(key: string, name: string, args: JsonObject = {}): InvocationStep {
+ return {
+ key,
+ method: "ade/actions/call",
+ params: { name, arguments: args },
+ unwrapToolResult: true,
+ };
+}
+
+function actionStep(key: string, domain: string, action: string, args: JsonObject = {}): InvocationStep {
+ return actionCallStep(key, "run_ade_action", { domain, action, args });
+}
+
+function actionArgsListStep(key: string, domain: string, action: string, argsList: unknown[]): InvocationStep {
+ return actionCallStep(key, "run_ade_action", { domain, action, argsList });
+}
+
+function listActionsStep(key: string, domain?: string): InvocationStep {
+ return actionCallStep(key, "list_ade_actions", domain ? { domain } : {});
+}
+
+function buildActionRunStep(args: string[]): InvocationStep {
+ const target = firstPositional(args);
+ if (!target) throw new CliUsageError("actions run requires or .");
+
+ let domain: string;
+ let action: string;
+ if (target.includes(".")) {
+ const parts = target.split(".");
+ domain = requireValue(parts.shift() ?? null, "domain");
+ action = requireValue(parts.join("."), "action");
+ } else {
+ domain = target;
+ action = requireValue(firstPositional(args), "action");
+ }
+
+ const argsListJson = readValue(args, ["--args-list-json", "--params-json"]);
+ if (argsListJson != null) {
+ const argsList = parseJson(argsListJson, "--args-list-json");
+ if (!Array.isArray(argsList)) throw new CliUsageError("--args-list-json must be a JSON array.");
+ return actionCallStep("result", "run_ade_action", { domain, action, argsList });
+ }
+
+ const scalarJson = readValue(args, ["--scalar-json", "--arg-value-json"]);
+ if (scalarJson != null) {
+ return actionCallStep("result", "run_ade_action", { domain, action, arg: parseJson(scalarJson, "--scalar-json") });
+ }
+
+ const scalar = readValue(args, ["--scalar", "--arg-value"]);
+ if (scalar != null) {
+ return actionCallStep("result", "run_ade_action", { domain, action, arg: parsePrimitive(scalar) });
+ }
+
+ return actionStep("result", domain, action, collectGenericObjectArgs(args));
+}
+
+function buildLanePlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "list";
+ if (sub === "actions") {
+ return { kind: "execute", label: "lane actions", steps: [listActionsStep("actions", "lane")] };
+ }
+ if (sub === "action") {
+ return { kind: "execute", label: "lane action", steps: [buildActionRunStep(["lane", ...args])] };
+ }
+ if (sub === "list" || sub === "ls") {
+ const input = collectGenericObjectArgs(args, {
+ includeArchived: readFlag(args, ["--archived", "--include-archived"]),
+ });
+ const visual = readFlag(args, ["--visual", "--graph"]);
+ const noVisual = readFlag(args, ["--no-visual"]);
+ return {
+ kind: "execute",
+ label: "lanes list",
+ steps: [actionCallStep("result", "list_lanes", input)],
+ visualizer: visual || !noVisual ? "lanes" : undefined,
+ };
+ }
+ if (sub === "show" || sub === "status") {
+ const laneId = requireValue(readLaneId(args) ?? firstPositional(args), "laneId");
+ return { kind: "execute", label: "lane status", steps: [actionCallStep("result", "get_lane_status", { laneId })] };
+ }
+ if (sub === "merge") {
+ const laneId = requireValue(readLaneId(args) ?? firstPositional(args), "laneId");
+ return { kind: "execute", label: "lane merge", steps: [actionCallStep("result", "merge_lane", collectGenericObjectArgs(args, { laneId, message: readValue(args, ["--message", "-m"]), deleteSourceLane: readFlag(args, ["--delete-source-lane", "--delete-source"]) }))] };
+ }
+ if (sub === "conflicts") {
+ const mode = firstPositional(args) ?? "check";
+ if (mode !== "check") return { kind: "execute", label: `lane conflicts ${mode}`, steps: [actionStep("result", "conflicts", mode, collectGenericObjectArgs(args, { laneId: readLaneId(args) }))] };
+ const ids = args.filter((entry) => !entry.startsWith("-"));
+ return { kind: "execute", label: "lane conflicts check", steps: [actionCallStep("result", "check_conflicts", collectGenericObjectArgs(args, { laneId: readLaneId(args), ...(ids.length ? { laneIds: ids } : {}), force: readFlag(args, ["--force"]) }))] };
+ }
+ if (sub === "create" || sub === "child") {
+ const name = readValue(args, ["--name"]) ?? firstPositional(args);
+ const input: JsonObject = {};
+ input.name = requireValue(name, "name");
+ maybePut(input, "description", readValue(args, ["--description", "--desc"]));
+ maybePut(input, "parentLaneId", readValue(args, ["--parent", "--parent-lane", "--parent-lane-id"]) ?? (sub === "child" ? readLaneId(args) : null));
+ if (sub === "child" && !input.parentLaneId) throw new CliUsageError("parent lane is required. Use --lane or --parent .");
+ return { kind: "execute", label: "lane create", steps: [actionCallStep("result", "create_lane", collectGenericObjectArgs(args, input))] };
+ }
+ if (sub === "children") {
+ const laneId = requireValue(readLaneId(args) ?? firstPositional(args), "laneId");
+ return { kind: "execute", label: "lane children", steps: [actionArgsListStep("result", "lane", "getChildren", [laneId])] };
+ }
+ if (sub === "stack") {
+ const laneId = requireValue(readLaneId(args) ?? firstPositional(args), "laneId");
+ return { kind: "execute", label: "lane stack", steps: [actionArgsListStep("result", "lane", "getStackChain", [laneId])] };
+ }
+ if (sub === "refresh") {
+ return { kind: "execute", label: "lane refresh", steps: [actionStep("result", "lane", "refreshSnapshots", collectGenericObjectArgs(args, { includeArchived: readFlag(args, ["--archived", "--include-archived"]) }))] };
+ }
+ if (sub === "rename") {
+ const laneId = requireValue(readLaneId(args) ?? firstPositional(args), "laneId");
+ return { kind: "execute", label: "lane rename", steps: [actionStep("result", "lane", "rename", collectGenericObjectArgs(args, { laneId, name: readValue(args, ["--name"]) ?? firstPositional(args) }))] };
+ }
+ if (sub === "reparent") {
+ const laneId = requireValue(readLaneId(args) ?? firstPositional(args), "laneId");
+ return { kind: "execute", label: "lane reparent", steps: [actionStep("result", "lane", "reparent", collectGenericObjectArgs(args, { laneId, newParentLaneId: readValue(args, ["--parent", "--parent-lane", "--parent-lane-id"]) ?? firstPositional(args) }))] };
+ }
+ if (sub === "appearance") {
+ const laneId = requireValue(readLaneId(args) ?? firstPositional(args), "laneId");
+ return { kind: "execute", label: "lane appearance", steps: [actionStep("result", "lane", "updateAppearance", collectGenericObjectArgs(args, { laneId, color: readValue(args, ["--color"]), icon: readValue(args, ["--icon"]) }))] };
+ }
+ if (sub === "archive" || sub === "unarchive") {
+ const laneId = requireValue(readLaneId(args) ?? firstPositional(args), "laneId");
+ return { kind: "execute", label: `lane ${sub}`, steps: [actionStep("result", "lane", sub, collectGenericObjectArgs(args, { laneId }))] };
+ }
+ if (sub === "delete" || sub === "rm") {
+ const laneId = requireValue(readLaneId(args) ?? firstPositional(args), "laneId");
+ return { kind: "execute", label: "lane delete", steps: [actionStep("result", "lane", "delete", collectGenericObjectArgs(args, { laneId, force: readFlag(args, ["--force"]), deleteBranch: readFlag(args, ["--delete-branch"]), deleteRemoteBranch: readFlag(args, ["--delete-remote-branch"]) }))] };
+ }
+ 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", label: "lane split unstaged", steps: [actionStep("result", "lane", "createFromUnstaged", collectGenericObjectArgs(args, { sourceLaneId: readValue(args, ["--source", "--source-lane"]) ?? readLaneId(args), name: readValue(args, ["--name"]) ?? firstPositional(args) }))] };
+ }
+ if (sub === "import" || sub === "import-branch") {
+ const input: JsonObject = {};
+ input.branchRef = requireValue(readValue(args, ["--branch", "--branch-ref"]) ?? firstPositional(args), "branchRef");
+ maybePut(input, "name", readValue(args, ["--name"]));
+ maybePut(input, "description", readValue(args, ["--description", "--desc"]));
+ maybePut(input, "baseBranch", readValue(args, ["--base", "--base-branch"]));
+ return { kind: "execute", label: "lane import", steps: [actionCallStep("result", "import_lane", collectGenericObjectArgs(args, input))] };
+ }
+ 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}`, steps: [actionStep("result", "lane", sub, collectGenericObjectArgs(args))] };
+}
+
+function buildGitPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "status";
+ if (sub === "actions") {
+ return { kind: "execute", label: "git actions", steps: [listActionsStep("actions", "git")] };
+ }
+ if (sub === "action") {
+ return { kind: "execute", label: "git action", steps: [buildActionRunStep(["git", ...args])] };
+ }
+
+ const laneId = readLaneId(args);
+ const withLane = (base: JsonObject = {}) => collectGenericObjectArgs(args, { ...base, ...(laneId ? { laneId } : {}) });
+
+ if (sub === "status" || sub === "sync-status") return { kind: "execute", label: "git status", steps: [actionCallStep("result", "git_get_sync_status", withLane())] };
+ if (sub === "fetch") return { kind: "execute", label: "git fetch", steps: [actionCallStep("result", "git_fetch", withLane())] };
+ if (sub === "pull") return { kind: "execute", label: "git pull", steps: [actionCallStep("result", "git_pull", withLane())] };
+ if (sub === "push") {
+ const forceWithLease = readFlag(args, ["--force", "--force-with-lease"]);
+ const setUpstream = readFlag(args, ["--set-upstream", "-u"]);
+ return { kind: "execute", label: "git push", steps: [actionCallStep("result", "git_push", withLane({ forceWithLease, setUpstream }))] };
+ }
+ if (sub === "commit") {
+ const input: JsonObject = {};
+ maybePut(input, "message", readValue(args, ["--message", "-m"]));
+ maybePut(input, "amend", readFlag(args, ["--amend"]));
+ input.stageAll = !readFlag(args, ["--no-stage-all"]);
+ return { kind: "execute", label: "git commit", steps: [actionCallStep("result", "commit_changes", withLane(input))] };
+ }
+ if (sub === "generate-message") {
+ return { kind: "execute", label: "git commit message", steps: [actionCallStep("result", "generate_commit_message", withLane({ amend: readFlag(args, ["--amend"]) }))] };
+ }
+ if (sub === "branches" || sub === "branch") return { kind: "execute", label: "git branches", steps: [actionCallStep("result", "git_list_branches", withLane())] };
+ if (sub === "checkout") {
+ const branchName = requireValue(readValue(args, ["--branch", "--branch-name"]) ?? firstPositional(args), "branchName");
+ return { kind: "execute", label: "git checkout", steps: [actionCallStep("result", "git_checkout_branch", withLane({ branchName }))] };
+ }
+ if (sub === "conflicts") return { kind: "execute", label: "git conflicts", steps: [actionCallStep("result", "get_lane_conflict_state", withLane())] };
+ if (sub === "rebase") {
+ const mode = firstPositional(args);
+ if (mode === "continue") return { kind: "execute", label: "rebase continue", steps: [actionCallStep("result", "rebase_continue", withLane())] };
+ if (mode === "abort") return { kind: "execute", label: "rebase abort", steps: [actionCallStep("result", "rebase_abort", withLane())] };
+ return { kind: "execute", label: "rebase lane", steps: [actionCallStep("result", "rebase_lane", withLane({ aiAssisted: readFlag(args, ["--ai", "--ai-assisted"]) }))] };
+ }
+ if (sub === "merge") {
+ const mode = requireValue(firstPositional(args), "merge action");
+ if (mode !== "continue" && mode !== "abort") throw new CliUsageError("git merge supports continue or abort.");
+ return { kind: "execute", label: `merge ${mode}`, steps: [actionStep("result", "git", mode === "continue" ? "mergeContinue" : "mergeAbort", withLane())] };
+ }
+ if (sub === "stash") {
+ const action = firstPositional(args) ?? "list";
+ const stashRef = readValue(args, ["--ref", "--stash-ref"]) ?? firstPositional(args);
+ const message = readValue(args, ["--message", "-m"]);
+ const common = withLane({
+ ...(stashRef ? { stashRef } : {}),
+ includeUntracked: !readFlag(args, ["--tracked-only"]),
+ ...(message ? { message } : {}),
+ });
+ const toolNameByAction: Record = {
+ push: "stash_push",
+ save: "stash_push",
+ list: "list_stashes",
+ ls: "list_stashes",
+ apply: "stash_apply",
+ pop: "stash_pop",
+ drop: "stash_drop",
+ clear: "stash_clear",
+ };
+ const toolName = toolNameByAction[action];
+ if (!toolName) throw new CliUsageError(`Unknown stash action '${action}'.`);
+ return { kind: "execute", label: `git stash ${action}`, steps: [actionCallStep("result", toolName, common)] };
+ }
+ if (sub === "diff") {
+ return buildDiffPlan([...(laneId ? ["--lane", laneId] : []), ...args]);
+ }
+
+ if (sub === "stage" || sub === "unstage" || sub === "discard" || sub === "restore") {
+ const pathArg = requireValue(readValue(args, ["--path"]) ?? firstPositional(args), "path");
+ const actionBySub: Record = {
+ stage: "stageFile",
+ unstage: "unstageFile",
+ discard: "discardFile",
+ restore: "restoreStagedFile",
+ };
+ return { kind: "execute", label: `git ${sub}`, steps: [actionStep("result", "git", actionBySub[sub]!, withLane({ path: pathArg }))] };
+ }
+ if (sub === "stage-all" || sub === "unstage-all") {
+ const paths = args.filter((entry) => !entry.startsWith("-"));
+ const action = sub === "stage-all" ? "stageAll" : "unstageAll";
+ return { kind: "execute", label: `git ${sub}`, steps: [actionStep("result", "git", action, withLane({ paths }))] };
+ }
+ if (sub === "files" || sub === "commit-files") {
+ const commitSha = requireValue(readValue(args, ["--commit", "--sha"]) ?? firstPositional(args), "commitSha");
+ return { kind: "execute", label: "git commit files", steps: [actionStep("result", "git", "listCommitFiles", withLane({ commitSha }))] };
+ }
+ if (sub === "message" || sub === "commit-message" || sub === "show-message") {
+ const commitSha = readValue(args, ["--commit", "--sha"]) ?? firstPositional(args);
+ if (commitSha) return { kind: "execute", label: "git commit message", steps: [actionStep("result", "git", "getCommitMessage", withLane({ commitSha }))] };
+ return { kind: "execute", label: "git commit message", steps: [actionCallStep("result", "generate_commit_message", withLane({ amend: readFlag(args, ["--amend"]) }))] };
+ }
+ if (sub === "history" || sub === "file-history") {
+ const filePath = requireValue(readValue(args, ["--path"]) ?? firstPositional(args), "path");
+ return { kind: "execute", label: "git file history", steps: [actionStep("result", "git", "getFileHistory", withLane({ path: filePath, limit: readIntOption(args, ["--limit"]) }))] };
+ }
+ if (sub === "revert" || sub === "cherry-pick") {
+ const commitSha = requireValue(readValue(args, ["--commit", "--sha"]) ?? firstPositional(args), "commitSha");
+ return { kind: "execute", label: `git ${sub}`, steps: [actionStep("result", "git", sub === "revert" ? "revertCommit" : "cherryPickCommit", withLane({ commitSha }))] };
+ }
+ const actionAliases: Record = {
+ commits: "listRecentCommits",
+ sync: "sync",
+ };
+ return { kind: "execute", label: `git ${sub}`, steps: [actionStep("result", "git", actionAliases[sub] ?? sub, withLane())] };
+}
+
+function buildDiffPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "changes";
+ if (sub === "actions") return { kind: "execute", label: "diff actions", steps: [listActionsStep("actions", "diff")] };
+ const laneId = readLaneId(args);
+ const withLane = (base: JsonObject = {}) => collectGenericObjectArgs(args, { ...base, ...(laneId ? { laneId } : {}) });
+ if (sub === "changes" || sub === "summary") {
+ const id = requireValue(laneId ?? readValue(args, ["--lane", "--lane-id"]), "laneId");
+ return {
+ kind: "execute",
+ label: "diff changes",
+ steps: [actionArgsListStep("result", "diff", "getChanges", [id])],
+ };
+ }
+ if (sub === "file") {
+ const filePath = requireValue(readValue(args, ["--path"]) ?? firstPositional(args), "path");
+ return {
+ kind: "execute",
+ label: "diff file",
+ steps: [actionStep("result", "diff", "getFileDiff", withLane({
+ filePath,
+ mode: readValue(args, ["--mode"]) ?? "unstaged",
+ compareRef: readValue(args, ["--compare-ref", "--base"]),
+ compareTo: readValue(args, ["--compare-to", "--head"]),
+ }))],
+ };
+ }
+ return { kind: "execute", label: `diff ${sub}`, steps: [actionStep("result", "diff", sub, withLane())] };
+}
+
+function buildPrPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "list";
+ if (sub === "actions") return { kind: "execute", label: "PR actions", steps: [listActionsStep("actions", "pr")] };
+ if (sub === "action") return { kind: "execute", label: "PR action", steps: [buildActionRunStep(["pr", ...args])] };
+
+ const prId = readPrId(args);
+ const withPr = (base: JsonObject = {}) => collectGenericObjectArgs(args, { ...base, ...(prId ? { prId } : {}) });
+
+ if (sub === "list" || sub === "ls") return { kind: "execute", label: "PR list", steps: [actionStep("result", "pr", "listAll", collectGenericObjectArgs(args))] };
+ if (sub === "show" || sub === "detail" || sub === "view") {
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ return { kind: "execute", label: "PR detail", steps: [actionArgsListStep("result", "pr", "getDetail", [id])] };
+ }
+ if (sub === "refresh") return { kind: "execute", label: "PR refresh", steps: [actionStep("result", "pr", "refresh", withPr({ prId: prId ?? firstPositional(args) }))] };
+ if (sub === "create") {
+ const laneId = readLaneId(args) ?? readValue(args, ["--lane-id"]);
+ const input: JsonObject = {};
+ input.laneId = requireValue(laneId, "laneId");
+ maybePut(input, "baseBranch", readValue(args, ["--base", "--base-branch"]));
+ maybePut(input, "title", readValue(args, ["--title"]));
+ maybePut(input, "body", readValue(args, ["--body"]));
+ input.draft = readFlag(args, ["--draft"]);
+ return { kind: "execute", label: "PR create", steps: [actionCallStep("result", "create_pr_from_lane", collectGenericObjectArgs(args, input))] };
+ }
+ if (sub === "health") return { kind: "execute", label: "PR health", steps: [actionCallStep("result", "get_pr_health", withPr({ prId: prId ?? firstPositional(args) }))] };
+ if (sub === "checks") return { kind: "execute", label: "PR checks", steps: [actionCallStep("result", "pr_get_checks", withPr({ prId: requireValue(prId ?? firstPositional(args), "prId") }))] };
+ if (sub === "comments" || sub === "review-comments") return { kind: "execute", label: "PR comments", steps: [actionCallStep("result", "pr_get_review_comments", withPr({ prId: requireValue(prId ?? firstPositional(args), "prId") }))] };
+ if (sub === "rerun" || sub === "rerun-failed-checks") return { kind: "execute", label: "PR rerun failed checks", steps: [actionCallStep("result", "pr_rerun_failed_checks", withPr({ prId: prId ?? firstPositional(args) }))] };
+ if (sub === "comment") return { kind: "execute", label: "PR comment", steps: [actionCallStep("result", "pr_add_comment", withPr({ prId: prId ?? firstPositional(args), body: readValue(args, ["--body"]) }))] };
+ if (sub === "reply") return { kind: "execute", label: "PR thread reply", steps: [actionCallStep("result", "pr_reply_to_review_thread", withPr({ prId: prId ?? firstPositional(args), threadId: readValue(args, ["--thread", "--thread-id"]), body: readValue(args, ["--body"]) }))] };
+ if (sub === "resolve-thread") return { kind: "execute", label: "PR resolve thread", steps: [actionCallStep("result", "pr_resolve_review_thread", withPr({ prId: requireValue(prId ?? firstPositional(args), "prId"), threadId: requireValue(readValue(args, ["--thread", "--thread-id"]), "threadId") }))] };
+ if (sub === "title" || sub === "update-title") return { kind: "execute", label: "PR update title", steps: [actionCallStep("result", "pr_update_title", withPr({ prId: prId ?? firstPositional(args), title: readValue(args, ["--title"]) }))] };
+ if (sub === "body" || sub === "update-body") return { kind: "execute", label: "PR update body", steps: [actionCallStep("result", "pr_update_body", withPr({ prId: prId ?? firstPositional(args), body: readValue(args, ["--body"]) ?? "" }))] };
+ if (sub === "link") return { kind: "execute", label: "PR link", steps: [actionStep("result", "pr", "linkToLane", collectGenericObjectArgs(args, { laneId: readLaneId(args) ?? firstPositional(args), url: readValue(args, ["--url"]) }))] };
+
+ const scalarPrActions: Record = {
+ status: "getStatus",
+ files: "getFiles",
+ "action-runs": "getActionRuns",
+ activity: "getActivity",
+ reviews: "getReviews",
+ threads: "getReviewThreads",
+ deployments: "getDeployments",
+ github: "openInGitHub",
+ "conflict-analysis": "getConflictAnalysis",
+ "merge-context": "getMergeContext",
+ };
+ if (scalarPrActions[sub]) {
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ return { kind: "execute", label: `PR ${sub}`, steps: [actionArgsListStep("result", "pr", scalarPrActions[sub]!, [id])] };
+ }
+ if (sub === "draft-description") return { kind: "execute", label: "PR draft description", steps: [actionStep("result", "pr", "draftDescription", collectGenericObjectArgs(args, { laneId: readLaneId(args) ?? firstPositional(args) }))] };
+ if (sub === "update-description") return { kind: "execute", label: "PR update description", steps: [actionStep("result", "pr", "updateDescription", withPr({ prId: prId ?? firstPositional(args), title: readValue(args, ["--title"]), body: readValue(args, ["--body"]) }))] };
+ if (sub === "delete" || sub === "land" || sub === "close" || sub === "reopen") {
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ const actionBySub: Record = { delete: "delete", land: "land", close: "closePr", reopen: "reopenPr" };
+ return { kind: "execute", label: `PR ${sub}`, steps: [actionStep("result", "pr", actionBySub[sub]!, collectGenericObjectArgs(args, { prId: id, method: readValue(args, ["--method"]) }))] };
+ }
+ if (sub === "land-stack" || sub === "land-stack-enhanced") {
+ return { kind: "execute", label: `PR ${sub}`, steps: [actionStep("result", "pr", sub === "land-stack" ? "landStack" : "landStackEnhanced", collectGenericObjectArgs(args, { rootLaneId: readValue(args, ["--root", "--root-lane"]) ?? firstPositional(args) }))] };
+ }
+ if (sub === "labels") {
+ const mode = firstPositional(args) ?? "set";
+ if (mode !== "set") throw new CliUsageError("prs labels supports set.");
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ return { kind: "execute", label: "PR labels set", steps: [actionStep("result", "pr", "setLabels", collectGenericObjectArgs(args, { prId: id, labels: args.filter((entry) => !entry.startsWith("-")) }))] };
+ }
+ if (sub === "reviewers") {
+ const mode = firstPositional(args) ?? "request";
+ if (mode !== "request") throw new CliUsageError("prs reviewers supports request.");
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ return { kind: "execute", label: "PR reviewers request", steps: [actionStep("result", "pr", "requestReviewers", collectGenericObjectArgs(args, { prId: id, reviewers: args.filter((entry) => !entry.startsWith("-")) }))] };
+ }
+ if (sub === "review") {
+ const mode = firstPositional(args) ?? "submit";
+ if (mode !== "submit") throw new CliUsageError("prs review supports submit.");
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ return { kind: "execute", label: "PR review submit", steps: [actionStep("result", "pr", "submitReview", collectGenericObjectArgs(args, { prId: id, event: readValue(args, ["--event"]) ?? "comment", body: readValue(args, ["--body"]) ?? "" }))] };
+ }
+ if (sub === "comment-react") {
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ return { kind: "execute", label: "PR comment react", steps: [actionStep("result", "pr", "reactToComment", collectGenericObjectArgs(args, { prId: id, commentId: readValue(args, ["--comment", "--comment-id"]), content: readValue(args, ["--content"]) }))] };
+ }
+ if (sub === "review-comment") {
+ const mode = firstPositional(args) ?? "post";
+ if (mode !== "post") throw new CliUsageError("prs review-comment supports post.");
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ return { kind: "execute", label: "PR review comment post", steps: [actionStep("result", "pr", "postReviewComment", collectGenericObjectArgs(args, { prId: id, threadId: readValue(args, ["--thread", "--thread-id"]), body: readValue(args, ["--body"]) }))] };
+ }
+ if (sub === "thread") {
+ const mode = firstPositional(args) ?? "set-resolved";
+ if (mode !== "set-resolved") throw new CliUsageError("prs thread supports set-resolved.");
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ return { kind: "execute", label: "PR thread set resolved", steps: [actionStep("result", "pr", "setReviewThreadResolved", collectGenericObjectArgs(args, { prId: id, threadId: readValue(args, ["--thread", "--thread-id"]), resolved: !readFlag(args, ["--unresolved"]) }))] };
+ }
+ if (sub === "ai-review-summary") return { kind: "execute", label: "PR AI review summary", steps: [actionStep("result", "pr", "aiReviewSummary", withPr({ prId: prId ?? firstPositional(args) }))] };
+ if (sub === "mobile-snapshot") return { kind: "execute", label: "PR mobile snapshot", steps: [actionArgsListStep("result", "pr", "getMobileSnapshot", [])] };
+ if (sub === "snapshots") {
+ const mode = firstPositional(args) ?? "list";
+ const action = mode === "refresh" ? "refreshSnapshots" : "listSnapshots";
+ return { kind: "execute", label: `PR snapshots ${mode}`, steps: [actionStep("result", "pr", action, withPr({ prId: prId ?? firstPositional(args) }))] };
+ }
+ if (sub === "github-snapshot") return { kind: "execute", label: "PR GitHub snapshot", steps: [actionStep("result", "pr", "getGithubSnapshot", collectGenericObjectArgs(args, { force: readFlag(args, ["--force"]) }))] };
+ if (sub === "conflicts") {
+ const mode = firstPositional(args) ?? "list";
+ if (mode === "list") return { kind: "execute", label: "PR conflicts list", steps: [actionArgsListStep("result", "pr", "listWithConflicts", [])] };
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ const action = mode === "analysis" ? "getConflictAnalysis" : "getMergeContext";
+ return { kind: "execute", label: `PR conflicts ${mode}`, steps: [actionArgsListStep("result", "pr", action, [id])] };
+ }
+
+ if (sub === "path-to-merge" || sub === "resolve" || sub === "issue-resolution") {
+ let mode = "start";
+ let positionalPrId = firstPositional(args);
+ if (positionalPrId === "start" || positionalPrId === "preview") {
+ mode = positionalPrId;
+ positionalPrId = firstPositional(args);
+ }
+ const id = requireValue(prId ?? positionalPrId, "prId");
+ const scope = readValue(args, ["--scope"]) ?? "both";
+ const modelId = requireValue(readValue(args, ["--model", "--model-id"]), "--model");
+ const input: JsonObject = {
+ prId: id,
+ scope,
+ modelId,
+ };
+ maybePut(input, "reasoning", readValue(args, ["--reasoning"]));
+ maybePut(input, "permissionMode", readValue(args, ["--permission-mode", "--permissions"]));
+ maybePut(input, "additionalInstructions", readValue(args, ["--instructions", "--additional-instructions"]));
+ const maxRounds = readIntOption(args, ["--max-rounds", "--rounds"]);
+ const autoMerge = readFlag(args, ["--auto-merge"]);
+ const noAutoMerge = readFlag(args, ["--no-auto-merge"]);
+ const mergeMethod = readValue(args, ["--merge-method"]);
+ const steps: InvocationStep[] = [];
+ if (maxRounds != null || autoMerge || noAutoMerge || mergeMethod) {
+ steps.push(actionArgsListStep("pipelineSettings", "issue_inventory", "savePipelineSettings", [
+ id,
+ {
+ ...(maxRounds != null ? { maxRounds } : {}),
+ ...(autoMerge || noAutoMerge ? { autoMerge: autoMerge && !noAutoMerge } : {}),
+ ...(mergeMethod ? { mergeMethod } : {}),
+ },
+ ]));
+ }
+ steps.push(actionCallStep("result", mode === "preview" ? "pr_preview_issue_resolution_prompt" : "pr_start_issue_resolution", collectGenericObjectArgs(args, input)));
+ return { kind: "execute", label: `PR path-to-merge ${mode}`, steps };
+ }
+
+ if (sub === "pipeline") {
+ const mode = firstPositional(args) ?? "get";
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ if (mode === "get") return { kind: "execute", label: "PR pipeline", steps: [actionArgsListStep("result", "issue_inventory", "getPipelineSettings", [id])] };
+ if (mode === "delete") return { kind: "execute", label: "PR pipeline delete", steps: [actionArgsListStep("result", "issue_inventory", "deletePipelineSettings", [id])] };
+ const maxRounds = readIntOption(args, ["--max-rounds", "--rounds"]);
+ const mergeMethod = readValue(args, ["--merge-method"]);
+ const settings = collectGenericObjectArgs(args, {
+ ...(maxRounds != null ? { maxRounds } : {}),
+ ...(mergeMethod ? { mergeMethod } : {}),
+ });
+ return { kind: "execute", label: "PR pipeline save", steps: [actionArgsListStep("result", "issue_inventory", "savePipelineSettings", [id, settings])] };
+ }
+
+ if (sub === "queue") {
+ const mode = firstPositional(args) ?? "create";
+ if (mode === "state" || mode === "list") {
+ const groupId = requireValue(readValue(args, ["--group", "--group-id"]) ?? firstPositional(args), "groupId");
+ return { kind: "execute", label: `queue ${mode}`, steps: [actionArgsListStep("result", "pr", mode === "state" ? "getQueueState" : "listGroupPrs", [groupId])] };
+ }
+ if (mode === "reorder") {
+ return { kind: "execute", label: "queue reorder", steps: [actionStep("result", "pr", "reorderQueuePrs", collectGenericObjectArgs(args, { groupId: readValue(args, ["--group", "--group-id"]) ?? firstPositional(args) }))] };
+ }
+ if (mode === "land-next") {
+ return { kind: "execute", label: "queue land next", steps: [actionCallStep("result", "land_queue_next", collectGenericObjectArgs(args, { groupId: readValue(args, ["--group", "--group-id"]) ?? firstPositional(args), method: readValue(args, ["--method"]) ?? "squash" }))] };
+ }
+ return { kind: "execute", label: "queue create", steps: [actionCallStep("result", "create_queue", collectGenericObjectArgs(args))] };
+ }
+
+ if (sub === "integration") {
+ const mode = firstPositional(args) ?? "simulate";
+ const integrationMap: Record = {
+ proposals: "listIntegrationProposals",
+ workflows: "listIntegrationWorkflows",
+ update: "updateIntegrationProposal",
+ delete: "deleteIntegrationProposal",
+ commit: "commitIntegration",
+ "resolve-start": "startIntegrationResolution",
+ "resolve-state": "getIntegrationResolutionState",
+ "recheck-step": "recheckIntegrationStep",
+ };
+ if (integrationMap[mode]) {
+ return { kind: "execute", label: `integration ${mode}`, steps: [actionStep("result", "pr", integrationMap[mode]!, collectGenericObjectArgs(args))] };
+ }
+ if (mode === "lane") {
+ const laneMode = firstPositional(args) ?? "create";
+ if (laneMode !== "create") throw new CliUsageError("prs integration lane supports create.");
+ return { kind: "execute", label: "integration lane create", steps: [actionStep("result", "pr", "createIntegrationLane", collectGenericObjectArgs(args))] };
+ }
+ if (mode === "cleanup") {
+ const cleanupMode = firstPositional(args) ?? "run";
+ return { kind: "execute", label: `integration cleanup ${cleanupMode}`, steps: [actionStep("result", "pr", cleanupMode === "dismiss" ? "dismissIntegrationCleanup" : "cleanupIntegrationWorkflow", collectGenericObjectArgs(args))] };
+ }
+ const tool = mode === "create" ? "create_integration" : "simulate_integration";
+ return { kind: "execute", label: `integration ${mode}`, steps: [actionCallStep("result", tool, collectGenericObjectArgs(args))] };
+ }
+
+ if (sub === "inventory") {
+ const first = firstPositional(args);
+ const knownModes = new Set(["refresh", "get", "new", "mark-sent", "mark-fixed", "dismiss", "escalate", "reset"]);
+ const mode = first && knownModes.has(first) ? first : "refresh";
+ const positionalPrId = mode === "refresh" ? first : firstPositional(args);
+ if (mode === "refresh") {
+ return { kind: "execute", label: "PR inventory", steps: [actionCallStep("result", "pr_refresh_issue_inventory", withPr({ prId: requireValue(prId ?? positionalPrId, "prId") }))] };
+ }
+ const actionByMode: Record = {
+ get: "getInventory",
+ new: "getNewItems",
+ "mark-sent": "markSentToAgent",
+ "mark-fixed": "markFixed",
+ dismiss: "markDismissed",
+ escalate: "markEscalated",
+ reset: "resetInventory",
+ };
+ const action = actionByMode[mode];
+ if (!action) throw new CliUsageError("prs inventory supports get, new, mark-sent, mark-fixed, dismiss, escalate, or reset.");
+ const id = requireValue(prId ?? positionalPrId, "prId");
+ const itemIds = args.filter((entry) => !entry.startsWith("-"));
+ const argsListByMode: Record = {
+ get: [id],
+ new: [id],
+ "mark-sent": [id, itemIds, readValue(args, ["--session", "--session-id"]) ?? "", readIntOption(args, ["--round"], 0) ?? 0],
+ "mark-fixed": [id, itemIds],
+ dismiss: [id, itemIds, readValue(args, ["--reason"]) ?? ""],
+ escalate: [id, itemIds],
+ reset: [id],
+ };
+ return { kind: "execute", label: `PR inventory ${mode}`, steps: [actionArgsListStep("result", "issue_inventory", action, argsListByMode[mode] ?? [id])] };
+ }
+
+ if (sub === "convergence") {
+ const mode = firstPositional(args) ?? "status";
+ const actionByMode: Record = {
+ status: "getConvergenceStatus",
+ runtime: "getConvergenceRuntime",
+ get: "getConvergenceRuntime",
+ save: "saveConvergenceRuntime",
+ reset: "resetConvergenceRuntime",
+ reconcile: "reconcileConvergenceSessionExit",
+ };
+ const action = actionByMode[mode];
+ if (!action) throw new CliUsageError("prs convergence supports status, runtime, save, reset, or reconcile.");
+ const id = requireValue(prId ?? firstPositional(args), "prId");
+ if (mode === "save") {
+ return { kind: "execute", label: "PR convergence save", steps: [actionArgsListStep("result", "issue_inventory", action, [id, collectGenericObjectArgs(args)])] };
+ }
+ if (mode === "reconcile") {
+ return { kind: "execute", label: "PR convergence reconcile", steps: [actionStep("result", "issue_inventory", action, collectGenericObjectArgs(args, { prId: id }))] };
+ }
+ return { kind: "execute", label: `PR convergence ${mode}`, steps: [actionArgsListStep("result", "issue_inventory", action, [id])] };
+ }
+
+ return { kind: "execute", label: `PR ${sub}`, steps: [actionStep("result", "pr", sub, withPr())] };
+}
+
+function buildRunPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "ps";
+ if (sub === "actions") return { kind: "execute", label: "run actions", steps: [listActionsStep("actions", "process")] };
+ if (sub === "action") return { kind: "execute", label: "run action", steps: [buildActionRunStep(["process", ...args])] };
+ if (sub === "defs" || sub === "definitions") return { kind: "execute", label: "process definitions", steps: [actionStep("result", "process", "listDefinitions", collectGenericObjectArgs(args))] };
+ const laneId = readLaneId(args);
+ const processId = readValue(args, ["--process", "--process-id"]) ?? firstPositional(args);
+ const runId = readValue(args, ["--run", "--run-id"]);
+ const withProcess = (base: JsonObject = {}) => collectGenericObjectArgs(args, {
+ ...base,
+ ...(laneId ? { laneId } : {}),
+ ...(processId ? { processId } : {}),
+ ...(runId ? { runId } : {}),
+ });
+ if (sub === "ps" || sub === "list" || sub === "runtime") {
+ const id = requireValue(laneId, "laneId");
+ return { kind: "execute", label: "process runtime", steps: [actionArgsListStep("result", "process", "listRuntime", [id])] };
+ }
+ if (sub === "start" || sub === "stop" || sub === "restart" || sub === "kill") {
+ return { kind: "execute", label: `process ${sub}`, steps: [actionStep("result", "process", sub, withProcess({ laneId: requireValue(laneId, "laneId"), processId: requireValue(processId, "processId") }))] };
+ }
+ if (sub === "logs" || sub === "log") {
+ return { kind: "execute", label: "process logs", steps: [actionStep("result", "process", "getLogTail", withProcess({ laneId: requireValue(laneId, "laneId"), processId: requireValue(processId, "processId"), maxBytes: readIntOption(args, ["--max-bytes", "--tail-bytes"], 80_000) }))] };
+ }
+ if (sub === "stack") {
+ const mode = requireValue(firstPositional(args), "stack action");
+ const stackId = requireValue(readValue(args, ["--stack", "--stack-id"]) ?? firstPositional(args), "stackId");
+ const methodByMode: Record = { start: "startStack", stop: "stopStack", restart: "restartStack" };
+ const method = methodByMode[mode];
+ if (!method) throw new CliUsageError("run stack supports start, stop, or restart.");
+ return { kind: "execute", label: `stack ${mode}`, steps: [actionStep("result", "process", method, collectGenericObjectArgs(args, { laneId: requireValue(laneId, "laneId"), stackId }))] };
+ }
+ if (sub === "start-all" || sub === "stop-all") return { kind: "execute", label: `process ${sub}`, steps: [actionStep("result", "process", sub === "start-all" ? "startAll" : "stopAll", collectGenericObjectArgs(args, { ...(laneId ? { laneId } : {}) }))] };
+ return { kind: "execute", label: `process ${sub}`, steps: [actionStep("result", "process", sub, withProcess())] };
+}
+
+function buildShellPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "start";
+ if (sub === "actions") return { kind: "execute", label: "shell actions", steps: [listActionsStep("actions", "pty")] };
+ if (sub === "start" || sub === "create") {
+ const laneId = readLaneId(args);
+ const startupCommandIndex = args.indexOf("--");
+ const startupCommand = startupCommandIndex >= 0 ? args.splice(startupCommandIndex + 1).join(" ") : readValue(args, ["--command", "-c"]);
+ if (startupCommandIndex >= 0) args.splice(startupCommandIndex, 1);
+ const input = collectGenericObjectArgs(args, {
+ ...(laneId ? { laneId } : {}),
+ cwd: readValue(args, ["--cwd"]),
+ title: readValue(args, ["--title"]),
+ startupCommand,
+ toolType: readValue(args, ["--tool-type"]) ?? "shell",
+ cols: readIntOption(args, ["--cols"], 120),
+ rows: readIntOption(args, ["--rows"], 36),
+ tracked: !readFlag(args, ["--untracked"]),
+ });
+ return { kind: "execute", label: "shell start", steps: [actionStep("result", "pty", "create", input)] };
+ }
+ if (sub === "write") return { kind: "execute", label: "shell write", steps: [actionStep("result", "pty", "write", collectGenericObjectArgs(args, { ptyId: requireValue(readValue(args, ["--pty", "--pty-id"]) ?? firstPositional(args), "ptyId"), data: readValue(args, ["--data"]) ?? "" }))] };
+ if (sub === "resize") return { kind: "execute", label: "shell resize", steps: [actionStep("result", "pty", "resize", collectGenericObjectArgs(args, { ptyId: requireValue(readValue(args, ["--pty", "--pty-id"]) ?? firstPositional(args), "ptyId"), cols: readIntOption(args, ["--cols"], 120), rows: readIntOption(args, ["--rows"], 36) }))] };
+ if (sub === "close" || sub === "dispose") return { kind: "execute", label: "shell close", steps: [actionStep("result", "pty", "dispose", collectGenericObjectArgs(args, { ptyId: requireValue(readValue(args, ["--pty", "--pty-id"]) ?? firstPositional(args), "ptyId"), sessionId: readValue(args, ["--session", "--session-id"]) }))] };
+ return { kind: "execute", label: `shell ${sub}`, steps: [actionStep("result", "pty", sub, collectGenericObjectArgs(args))] };
+}
+
+function buildChatPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "list";
+ if (sub === "actions") return { kind: "execute", label: "chat actions", steps: [listActionsStep("actions", "chat")] };
+ const sessionId = readValue(args, ["--session", "--session-id"]) ?? (sub !== "create" && sub !== "list" ? firstPositional(args) : null);
+ const withSession = (base: JsonObject = {}) => collectGenericObjectArgs(args, { ...base, ...(sessionId ? { sessionId } : {}) });
+ if (sub === "list" || sub === "ls") return { kind: "execute", label: "chat list", steps: [actionStep("result", "chat", "listSessions", collectGenericObjectArgs(args))] };
+ if (sub === "show" || sub === "status") return { kind: "execute", label: "chat status", steps: [actionStep("result", "chat", "getSessionSummary", withSession())] };
+ if (sub === "create" || sub === "spawn") return { kind: "execute", label: "chat create", steps: [actionStep("result", "chat", "createSession", collectGenericObjectArgs(args, { laneId: readLaneId(args), provider: readValue(args, ["--provider"]), modelId: readValue(args, ["--model", "--model-id"]), permissionMode: readValue(args, ["--permission-mode", "--permissions"]), surface: readValue(args, ["--surface"]) ?? "work" }))] };
+ if (sub === "send") return { kind: "execute", label: "chat send", steps: [actionStep("result", "chat", "sendMessage", withSession({ sessionId: requireValue(sessionId, "sessionId"), text: requireValue(readValue(args, ["--text", "--message"]) ?? args.join(" "), "message text") }))] };
+ if (sub === "interrupt") return { kind: "execute", label: "chat interrupt", steps: [actionStep("result", "chat", "interrupt", withSession({ sessionId: requireValue(sessionId, "sessionId") }))] };
+ if (sub === "resume") return { kind: "execute", label: "chat resume", steps: [actionStep("result", "chat", "resumeSession", withSession())] };
+ if (sub === "delete" || sub === "rm") return { kind: "execute", label: "chat delete", steps: [actionStep("result", "chat", "deleteSession", withSession())] };
+ if (sub === "models") return { kind: "execute", label: "chat models", steps: [actionStep("result", "chat", "getAvailableModels", collectGenericObjectArgs(args))] };
+ if (sub === "slash") return { kind: "execute", label: "chat slash commands", steps: [actionStep("result", "chat", "getSlashCommands", collectGenericObjectArgs(args))] };
+ return { kind: "execute", label: `chat ${sub}`, steps: [actionStep("result", "chat", sub, withSession())] };
+}
+
+function buildTestsPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "list";
+ if (sub === "actions") return { kind: "execute", label: "test actions", steps: [listActionsStep("actions", "tests")] };
+ if (sub === "list" || sub === "suites") return { kind: "execute", label: "test suites", steps: [actionStep("result", "tests", "listSuites", collectGenericObjectArgs(args))] };
+ if (sub === "run") {
+ const laneId = requireValue(readLaneId(args), "laneId");
+ const suiteId = readValue(args, ["--suite", "--suite-id"]) ?? firstPositional(args);
+ const command = readValue(args, ["--command", "-c"]);
+ if (!suiteId && !command) throw new CliUsageError("tests run requires --suite or --command .");
+ const input = collectGenericObjectArgs(args, {
+ laneId,
+ suiteId,
+ command,
+ waitForCompletion: readFlag(args, ["--wait"]),
+ timeoutMs: readIntOption(args, ["--timeout-ms"]),
+ maxLogBytes: readIntOption(args, ["--max-log-bytes"]),
+ });
+ return { kind: "execute", label: "test run", steps: [actionCallStep("result", "run_tests", input)] };
+ }
+ if (sub === "stop") return { kind: "execute", label: "test stop", steps: [actionStep("result", "tests", "stop", collectGenericObjectArgs(args, { runId: requireValue(readValue(args, ["--run", "--run-id"]) ?? firstPositional(args), "runId") }))] };
+ if (sub === "runs") return { kind: "execute", label: "test runs", steps: [actionStep("result", "tests", "listRuns", collectGenericObjectArgs(args, { laneId: readLaneId(args), suiteId: readValue(args, ["--suite", "--suite-id"]), limit: readIntOption(args, ["--limit"]) }))] };
+ if (sub === "logs" || sub === "log") return { kind: "execute", label: "test logs", steps: [actionStep("result", "tests", "getLogTail", collectGenericObjectArgs(args, { runId: requireValue(readValue(args, ["--run", "--run-id"]) ?? firstPositional(args), "runId"), maxBytes: readIntOption(args, ["--max-bytes"], 220_000) }))] };
+ return { kind: "execute", label: `tests ${sub}`, steps: [actionStep("result", "tests", sub, collectGenericObjectArgs(args))] };
+}
+
+function readFileTextInput(args: string[]): string | undefined {
+ const text = readValue(args, ["--text"]);
+ if (text != null) return text;
+ const filePath = readValue(args, ["--from-file"]);
+ if (filePath != null) return fs.readFileSync(path.resolve(filePath), "utf8");
+ if (readFlag(args, ["--stdin"])) return fs.readFileSync(0, "utf8");
+ return undefined;
+}
+
+function buildFilesPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "workspaces";
+ if (sub === "actions") return { kind: "execute", label: "file actions", steps: [listActionsStep("actions", "file")] };
+ const workspaceId = readValue(args, ["--workspace", "--workspace-id"]);
+ const withWorkspace = (base: JsonObject = {}) => collectGenericObjectArgs(args, { ...base, ...(workspaceId ? { workspaceId } : {}) });
+
+ if (sub === "workspaces" || sub === "workspace" || sub === "roots") {
+ return { kind: "execute", label: "file workspaces", steps: [actionStep("result", "file", "listWorkspaces", collectGenericObjectArgs(args, { laneId: readLaneId(args) }))] };
+ }
+ if (sub === "tree" || sub === "ls") {
+ return { kind: "execute", label: "file tree", steps: [actionStep("result", "file", "listTree", withWorkspace({ parentPath: readValue(args, ["--path"]) ?? firstPositional(args), depth: readIntOption(args, ["--depth"]), includeIgnored: readFlag(args, ["--include-ignored"]) }))] };
+ }
+ if (sub === "read" || sub === "cat") {
+ return { kind: "execute", label: "file read", steps: [actionStep("result", "file", "readFile", withWorkspace({ path: requireValue(readValue(args, ["--path"]) ?? firstPositional(args), "path") }))] };
+ }
+ if (sub === "write") {
+ const text = readFileTextInput(args);
+ if (text == null) throw new CliUsageError("files write requires --text, --from-file, or --stdin.");
+ return { kind: "execute", label: "file write", steps: [actionStep("result", "file", "writeWorkspaceText", withWorkspace({ path: requireValue(readValue(args, ["--path"]) ?? firstPositional(args), "path"), text }))] };
+ }
+ if (sub === "create") {
+ return { kind: "execute", label: "file create", steps: [actionStep("result", "file", "createFile", withWorkspace({ path: requireValue(readValue(args, ["--path"]) ?? firstPositional(args), "path"), content: readFileTextInput(args) ?? "" }))] };
+ }
+ if (sub === "mkdir") {
+ return { kind: "execute", label: "file mkdir", steps: [actionStep("result", "file", "createDirectory", withWorkspace({ path: requireValue(readValue(args, ["--path"]) ?? firstPositional(args), "path") }))] };
+ }
+ if (sub === "rename" || sub === "mv") {
+ return { kind: "execute", label: "file rename", steps: [actionStep("result", "file", "rename", withWorkspace({ oldPath: readValue(args, ["--old", "--old-path"]) ?? firstPositional(args), newPath: readValue(args, ["--new", "--new-path"]) ?? firstPositional(args) }))] };
+ }
+ if (sub === "delete" || sub === "rm") {
+ return { kind: "execute", label: "file delete", steps: [actionStep("result", "file", "deletePath", withWorkspace({ path: requireValue(readValue(args, ["--path"]) ?? firstPositional(args), "path") }))] };
+ }
+ if (sub === "quick-open") {
+ return { kind: "execute", label: "file quick-open", steps: [actionStep("result", "file", "quickOpen", withWorkspace({ query: readValue(args, ["--query", "-q"]) ?? args.join(" "), limit: readIntOption(args, ["--limit"]), includeIgnored: readFlag(args, ["--include-ignored"]) }))] };
+ }
+ if (sub === "search") {
+ return { kind: "execute", label: "file search", steps: [actionStep("result", "file", "searchText", withWorkspace({ query: requireValue(readValue(args, ["--query", "-q"]) ?? args.join(" "), "query"), limit: readIntOption(args, ["--limit"]), includeIgnored: readFlag(args, ["--include-ignored"]) }))] };
+ }
+ return { kind: "execute", label: `files ${sub}`, steps: [actionStep("result", "file", sub, withWorkspace())] };
+}
+
+function buildProofPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "status";
+ if (sub === "actions") return { kind: "execute", label: "proof actions", steps: [listActionsStep("actions", "computer_use_artifacts")] };
+ if (sub === "status" || sub === "backends") return { kind: "execute", label: "proof backend status", steps: [actionCallStep("result", "get_computer_use_backend_status", collectGenericObjectArgs(args))] };
+ if (sub === "environment") return { kind: "execute", label: "computer-use environment", steps: [actionCallStep("result", "get_environment_info", collectGenericObjectArgs(args))] };
+ if (sub === "list" || sub === "ls") return { kind: "execute", label: "proof list", steps: [actionCallStep("result", "list_computer_use_artifacts", collectGenericObjectArgs(args))] };
+ if (sub === "ingest") return { kind: "execute", label: "proof ingest", steps: [actionCallStep("result", "ingest_computer_use_artifacts", collectGenericObjectArgs(args))] };
+ if (sub === "screenshot") return { kind: "execute", label: "computer-use screenshot", steps: [actionCallStep("result", "screenshot_environment", collectGenericObjectArgs(args))] };
+ if (sub === "record") return { kind: "execute", label: "computer-use record", steps: [actionCallStep("result", "record_environment", collectGenericObjectArgs(args, { durationSec: readNumberOption(args, ["--seconds", "--duration-sec"]) }))] };
+ if (sub === "launch") return { kind: "execute", label: "computer-use launch", steps: [actionCallStep("result", "launch_app", collectGenericObjectArgs(args, { app: readValue(args, ["--app"]) ?? firstPositional(args) }))] };
+ if (sub === "interact") return { kind: "execute", label: "computer-use interact", steps: [actionCallStep("result", "interact_gui", collectGenericObjectArgs(args))] };
+ return { kind: "execute", label: `proof ${sub}`, steps: [actionStep("result", "computer_use_artifacts", sub, collectGenericObjectArgs(args))] };
+}
+
+function buildMemoryPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "search";
+ if (sub === "actions") return { kind: "execute", label: "memory actions", steps: [listActionsStep("actions", "memory")] };
+ if (sub === "add") return { kind: "execute", label: "memory add", steps: [actionCallStep("result", "memory_add", collectGenericObjectArgs(args, { content: requireValue(readValue(args, ["--content"]) ?? args.join(" "), "content"), category: requireValue(readValue(args, ["--category"]), "category"), scope: readValue(args, ["--scope"]) }))] };
+ if (sub === "search") return { kind: "execute", label: "memory search", steps: [actionCallStep("result", "memory_search", collectGenericObjectArgs(args, { query: requireValue(readValue(args, ["--query", "-q"]) ?? args.join(" "), "query") }))] };
+ if (sub === "pin") return { kind: "execute", label: "memory pin", steps: [actionCallStep("result", "memory_pin", collectGenericObjectArgs(args, { id: requireValue(readValue(args, ["--memory", "--memory-id", "--id"]) ?? firstPositional(args), "memory id") }))] };
+ if (sub === "core") return { kind: "execute", label: "memory core", steps: [actionCallStep("result", "memory_update_core", collectGenericObjectArgs(args))] };
+ return { kind: "execute", label: `memory ${sub}`, steps: [actionStep("result", "memory", sub, collectGenericObjectArgs(args))] };
+}
+
+function buildSettingsPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "get";
+ if (sub === "actions") return { kind: "execute", label: "settings actions", steps: [listActionsStep("actions", "project_config")] };
+ if (sub === "action") return { kind: "execute", label: "settings action", steps: [buildActionRunStep(["project_config", ...args])] };
+ return { kind: "execute", label: `settings ${sub}`, steps: [actionStep("result", "project_config", sub, collectGenericObjectArgs(args))] };
+}
+
+function buildActionsPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "list";
+ if (sub === "list" || sub === "ls") return { kind: "execute", label: "actions list", steps: [listActionsStep("result", readValue(args, ["--domain"]) ?? firstPositional(args) ?? undefined)] };
+ if (sub === "call" || sub === "direct" || sub === "tool") {
+ const toolName = requireValue(firstPositional(args), "toolName");
+ return { kind: "execute", label: "action call", steps: [actionCallStep("result", toolName, collectGenericObjectArgs(args))] };
+ }
+ if (sub === "run") return { kind: "execute", label: "action run", steps: [buildActionRunStep(args)] };
+ if (sub === "status") return { kind: "execute", label: "action status", steps: [actionCallStep("result", "get_ade_action_status", collectGenericObjectArgs(args))] };
+ throw new CliUsageError("actions supports list, run, call, or status.");
+}
+
+function buildAgentPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "spawn";
+ if (sub === "spawn" || sub === "start") {
+ const toolWhitelist = args
+ .filter((entry) => entry.startsWith("--tool=") || entry.startsWith("--allow-tool="))
+ .map((entry) => entry.slice(entry.indexOf("=") + 1).trim())
+ .filter(Boolean);
+ const laneId = requireValue(readLaneId(args), "laneId");
+ const prompt = requireValue(readValue(args, ["--prompt"]) ?? args.join(" "), "prompt");
+ return {
+ kind: "execute",
+ label: "agent spawn",
+ steps: [actionCallStep("result", "spawn_agent", collectGenericObjectArgs(args, {
+ laneId,
+ provider: readValue(args, ["--provider"]) ?? "codex",
+ model: readValue(args, ["--model"]),
+ title: readValue(args, ["--title"]),
+ prompt,
+ permissionMode: readValue(args, ["--permission-mode", "--permissions"]),
+ contextFilePath: readValue(args, ["--context-file"]),
+ runId: readValue(args, ["--run", "--run-id"]),
+ stepId: readValue(args, ["--step", "--step-id"]),
+ attemptId: readValue(args, ["--attempt", "--attempt-id"]),
+ maxPromptChars: readIntOption(args, ["--max-prompt-chars"]),
+ ...(toolWhitelist.length ? { toolWhitelist } : {}),
+ }))],
+ };
+ }
+ return { kind: "execute", label: `agent ${sub}`, steps: [actionCallStep("result", sub.replace(/-/g, "_"), collectGenericObjectArgs(args))] };
+}
+
+function buildCtoPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "state";
+ if (sub === "state") return { kind: "execute", label: "CTO state", steps: [actionCallStep("result", "get_cto_state", collectGenericObjectArgs(args, { recentLimit: readIntOption(args, ["--recent-limit", "--limit"]) }))] };
+ if (sub === "chats" || sub === "chat") {
+ const mode = firstPositional(args) ?? "list";
+ const toolByMode: Record = {
+ list: "listChats",
+ spawn: "spawnChat",
+ status: "getChatStatus",
+ transcript: "readChatTranscript",
+ send: "sendChatMessage",
+ interrupt: "interruptChat",
+ resume: "resumeChat",
+ end: "endChat",
+ };
+ const tool = toolByMode[mode];
+ if (!tool) throw new CliUsageError("cto chats supports list, spawn, status, transcript, send, interrupt, resume, or end.");
+ return { kind: "execute", label: `CTO chats ${mode}`, steps: [actionCallStep("result", tool, collectGenericObjectArgs(args, { sessionId: readValue(args, ["--session", "--session-id"]) ?? firstPositional(args), text: readValue(args, ["--text", "--message"]) ?? args.join(" "), laneId: readLaneId(args), modelId: readValue(args, ["--model", "--model-id"]), initialPrompt: readValue(args, ["--prompt"]) }))] };
+ }
+ return { kind: "execute", label: `CTO ${sub}`, steps: [actionCallStep("result", sub.replace(/-/g, "_"), collectGenericObjectArgs(args))] };
+}
+
+function buildLinearPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "workflows";
+ if (sub === "workflows") return { kind: "execute", label: "Linear workflows", steps: [actionCallStep("result", "listLinearWorkflows", collectGenericObjectArgs(args))] };
+ if (sub === "run") {
+ const mode = firstPositional(args) ?? "status";
+ const toolByMode: Record = {
+ status: "getLinearRunStatus",
+ resolve: "resolveLinearRunAction",
+ cancel: "cancelLinearRun",
+ reroute: "rerouteLinearRun",
+ };
+ const tool = toolByMode[mode];
+ if (!tool) throw new CliUsageError("linear run supports status, resolve, cancel, or reroute.");
+ return { kind: "execute", label: `Linear run ${mode}`, steps: [actionCallStep("result", tool, collectGenericObjectArgs(args, { runId: readValue(args, ["--run", "--run-id"]) ?? firstPositional(args) }))] };
+ }
+ if (sub === "route") {
+ const mode = firstPositional(args) ?? "cto";
+ const toolByMode: Record = {
+ cto: "routeLinearIssueToCto",
+ mission: "routeLinearIssueToMission",
+ worker: "routeLinearIssueToWorker",
+ };
+ const tool = toolByMode[mode];
+ if (!tool) throw new CliUsageError("linear route supports cto, mission, or worker.");
+ return { kind: "execute", label: `Linear route ${mode}`, steps: [actionCallStep("result", tool, collectGenericObjectArgs(args))] };
+ }
+ if (sub === "sync") {
+ const mode = firstPositional(args) ?? "dashboard";
+ const toolByMode: Record = {
+ dashboard: "getLinearSyncDashboard",
+ run: "runLinearSyncNow",
+ queue: "listLinearSyncQueue",
+ resolve: "resolveLinearSyncQueueItem",
+ detail: "getLinearWorkflowRunDetail",
+ };
+ const tool = toolByMode[mode];
+ if (!tool) throw new CliUsageError("linear sync supports dashboard, run, queue, resolve, or detail.");
+ return { kind: "execute", label: `Linear sync ${mode}`, steps: [actionCallStep("result", tool, collectGenericObjectArgs(args))] };
+ }
+ if (sub === "ingress") {
+ const mode = firstPositional(args) ?? "status";
+ const toolByMode: Record = {
+ status: "getLinearIngressStatus",
+ events: "listLinearIngressEvents",
+ webhook: "ensureLinearWebhook",
+ };
+ const tool = toolByMode[mode];
+ if (!tool) throw new CliUsageError("linear ingress supports status, events, or webhook.");
+ return { kind: "execute", label: `Linear ingress ${mode}`, steps: [actionCallStep("result", tool, collectGenericObjectArgs(args))] };
+ }
+ return { kind: "execute", label: `Linear ${sub}`, steps: [actionStep("result", "linear_dispatcher", sub, collectGenericObjectArgs(args))] };
+}
+
+function buildFlowPlan(args: string[]): CliPlan {
+ const sub = firstPositional(args) ?? "policy";
+ if (sub !== "policy") return { kind: "execute", label: `flow ${sub}`, steps: [actionStep("result", "flow_policy", sub, collectGenericObjectArgs(args))] };
+ const mode = firstPositional(args) ?? "get";
+ const actionByMode: Record = {
+ get: "getPolicy",
+ save: "savePolicy",
+ validate: "validatePolicy",
+ normalize: "normalizePolicy",
+ revisions: "listRevisions",
+ rollback: "rollbackRevision",
+ diff: "diffPolicyPaths",
+ };
+ const action = actionByMode[mode];
+ if (!action) throw new CliUsageError("flow policy supports get, save, validate, normalize, revisions, rollback, or diff.");
+ return { kind: "execute", label: `flow policy ${mode}`, steps: [actionStep("result", "flow_policy", action, collectGenericObjectArgs(args))] };
+}
+
+function buildCoordinatorPlan(args: string[]): CliPlan {
+ const toolName = requireValue(firstPositional(args), "coordinator tool").replace(/-/g, "_");
+ return { kind: "execute", label: `coordinator ${toolName}`, steps: [actionCallStep("result", toolName, collectGenericObjectArgs(args))] };
+}
+
+function buildCliPlan(command: string[]): CliPlan {
+ const args = [...command];
+ const primary = firstPositional(args);
+ if (!primary || primary === "-h" || primary === "--help") {
+ return { kind: "help", text: TOP_LEVEL_HELP };
+ }
+ if (primary === "help") {
+ const topic = (firstPositional(args) ?? "").toLowerCase();
+ const aliases: Record = {
+ lane: "lanes",
+ pr: "prs",
+ process: "run",
+ processes: "run",
+ file: "files",
+ computer: "proof",
+ "computer-use": "proof",
+ action: "actions",
+ };
+ const key = aliases[topic] ?? topic;
+ return { kind: "help", text: key && HELP_BY_COMMAND[key] ? HELP_BY_COMMAND[key] : TOP_LEVEL_HELP };
+ }
+ if (primary === "version" || primary === "--version" || primary === "-v") {
+ return { kind: "help", text: `ade ${VERSION}\n` };
+ }
+ if (primary === "status") {
+ return { kind: "execute", label: "status", summary: "status", steps: [{ key: "ping", method: "ping" }] };
+ }
+ if (primary === "doctor") {
+ return {
+ kind: "execute",
+ label: "doctor",
+ summary: "doctor",
+ steps: [
+ { key: "ping", method: "ping" },
+ { key: "rpcActions", method: "ade/actions/list" },
+ listActionsStep("actions"),
+ { ...actionStep("projectConfig", "project_config", "get"), optional: true },
+ ],
+ };
+ }
+ if (primary === "auth") {
+ const sub = firstPositional(args) ?? "status";
+ if (sub !== "status") throw new CliUsageError("auth currently supports status.");
+ return {
+ kind: "execute",
+ label: "auth status",
+ summary: "auth",
+ steps: [
+ { key: "actions", method: "ade/actions/list" },
+ { ...actionStep("projectConfig", "project_config", "get"), optional: true },
+ ],
+ };
+ }
+ if (primary === "lanes" || primary === "lane") return buildLanePlan(args);
+ if (primary === "git") return buildGitPlan(args);
+ if (primary === "diff" || primary === "diffs") return buildDiffPlan(args);
+ if (primary === "files" || primary === "file") return buildFilesPlan(args);
+ if (primary === "prs" || primary === "pr") return buildPrPlan(args);
+ if (primary === "run" || primary === "process" || primary === "processes") return buildRunPlan(args);
+ if (primary === "shell" || primary === "pty") return buildShellPlan(args);
+ if (primary === "chat" || primary === "chats" || primary === "work") return buildChatPlan(args);
+ if (primary === "agent" || primary === "agents") return buildAgentPlan(args);
+ if (primary === "cto") return buildCtoPlan(args);
+ if (primary === "linear") return buildLinearPlan(args);
+ if (primary === "flow") return buildFlowPlan(args);
+ if (primary === "coordinator" || primary === "coord") return buildCoordinatorPlan(args);
+ if (primary === "ask") return { kind: "execute", label: "ask user", steps: [actionCallStep("result", "ask_user", collectGenericObjectArgs(args, { title: readValue(args, ["--title"]) ?? "ADE question", body: readValue(args, ["--body", "--question"]) ?? args.join(" ") }))] };
+ if (primary === "tests" || primary === "test") return buildTestsPlan(args);
+ if (primary === "proof" || primary === "computer-use" || primary === "artifacts") return buildProofPlan(args);
+ if (primary === "memory") return buildMemoryPlan(args);
+ if (primary === "settings" || primary === "config") return buildSettingsPlan(args);
+ if (primary === "actions" || primary === "action") return buildActionsPlan(args);
+ throw new CliUsageError(`Unknown command '${primary}'. Run 'ade help'.`);
+}
+
+function findProjectRoot(startDir: string): string {
+ let cursor = path.resolve(startDir);
+ while (true) {
+ if (fs.existsSync(path.join(cursor, ".ade"))) return cursor;
+ const parent = path.dirname(cursor);
+ if (parent === cursor) break;
+ cursor = parent;
+ }
+
+ const git = spawnSync("git", ["rev-parse", "--show-toplevel"], {
+ cwd: startDir,
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "ignore"],
+ });
+ const gitRoot = git.status === 0 ? git.stdout.trim() : "";
+ return gitRoot ? path.resolve(gitRoot) : path.resolve(startDir);
+}
+
+function resolveRoots(options: GlobalOptions): { projectRoot: string; workspaceRoot: string } {
+ const projectRoot = options.projectRoot
+ ?? (process.env.ADE_PROJECT_ROOT?.trim() ? path.resolve(process.env.ADE_PROJECT_ROOT.trim()) : findProjectRoot(process.cwd()));
+ const workspaceRoot = options.workspaceRoot
+ ?? (process.env.ADE_WORKSPACE_ROOT?.trim() ? path.resolve(process.env.ADE_WORKSPACE_ROOT.trim()) : projectRoot);
+ return { projectRoot, workspaceRoot };
+}
+
+function commandExists(command: string): boolean {
+ const result = spawnSync("which", [command], {
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "ignore"],
+ });
+ return result.status === 0 && result.stdout.trim().length > 0;
+}
+
+function runLocalCommand(command: string, args: string[], cwd: string): { ok: boolean; stdout: string; stderr: string } {
+ const result = spawnSync(command, args, {
+ cwd,
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "pipe"],
+ timeout: 5000,
+ });
+ return {
+ ok: result.status === 0,
+ stdout: result.stdout.trim(),
+ stderr: result.stderr.trim(),
+ };
+}
+
+function checkGitReadiness(projectRoot: string): ReadinessCheck {
+ if (!commandExists("git")) {
+ return {
+ ready: false,
+ status: "missing",
+ message: "git is not available on PATH.",
+ nextAction: "Install git and rerun ade doctor.",
+ };
+ }
+ const inside = runLocalCommand("git", ["rev-parse", "--is-inside-work-tree"], projectRoot);
+ if (!inside.ok || inside.stdout !== "true") {
+ return {
+ ready: false,
+ status: "missing",
+ message: "Project root is not inside a git worktree.",
+ nextAction: "Run ade with --project-root pointing at a git repository.",
+ };
+ }
+ const root = runLocalCommand("git", ["rev-parse", "--show-toplevel"], projectRoot);
+ const branch = runLocalCommand("git", ["branch", "--show-current"], projectRoot);
+ return {
+ ready: true,
+ status: "ready",
+ message: `Git repository detected${branch.stdout ? ` on ${branch.stdout}` : ""}.`,
+ details: {
+ gitRoot: root.ok ? root.stdout : null,
+ branch: branch.ok ? branch.stdout || null : null,
+ },
+ };
+}
+
+function getGitRemote(projectRoot: string): string | null {
+ const remote = runLocalCommand("git", ["config", "--get", "remote.origin.url"], projectRoot);
+ return remote.ok && remote.stdout ? remote.stdout : null;
+}
+
+function checkGitHubReadiness(projectRoot: string): ReadinessCheck {
+ const remote = getGitRemote(projectRoot);
+ const hasGitHubRemote = Boolean(remote && /github\.com[:/]/i.test(remote));
+ const ghInstalled = commandExists("gh");
+ const envTokenPresent = Boolean(process.env.ADE_GITHUB_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim());
+ const ready = hasGitHubRemote && (ghInstalled || envTokenPresent);
+ return {
+ ready,
+ status: ready ? "ready" : hasGitHubRemote ? "warning" : "unavailable",
+ message: hasGitHubRemote
+ ? ready
+ ? "GitHub remote detected and a local auth mechanism is available."
+ : "GitHub remote detected, but no gh CLI or GitHub token was found locally."
+ : "No GitHub origin remote detected.",
+ nextAction: ready
+ ? undefined
+ : hasGitHubRemote
+ ? "Run gh auth login or set ADE_GITHUB_TOKEN/GITHUB_TOKEN for headless PR workflows."
+ : "Add a GitHub origin remote if this project should use ADE PR workflows.",
+ details: {
+ ghInstalled,
+ tokenEnvPresent: envTokenPresent,
+ githubRemoteDetected: hasGitHubRemote,
+ },
+ };
+}
+
+function checkLinearReadiness(projectRoot: string): ReadinessCheck {
+ const { resolveAdeLayout } = requireAdeLayout();
+ const layout = resolveAdeLayout(projectRoot);
+ const encryptedTokenPresent = fs.existsSync(path.join(layout.secretsDir, "linear-token.v1.bin"));
+ const envTokenPresent = Boolean(
+ process.env.ADE_LINEAR_API?.trim()
+ || process.env.LINEAR_API_KEY?.trim()
+ || process.env.ADE_LINEAR_TOKEN?.trim()
+ || process.env.LINEAR_TOKEN?.trim()
+ );
+ const ready = encryptedTokenPresent || envTokenPresent;
+ return {
+ ready,
+ status: ready ? "ready" : "warning",
+ message: ready
+ ? "Linear credentials are present locally."
+ : "No Linear token was detected in local stores or environment variables.",
+ nextAction: ready
+ ? undefined
+ : "Configure Linear in ADE desktop or set ADE_LINEAR_API/LINEAR_API_KEY for headless mode.",
+ details: {
+ encryptedTokenPresent,
+ tokenEnvPresent: envTokenPresent,
+ },
+ };
+}
+
+function checkProviderReadiness(value: unknown): ReadinessCheck {
+ const configResult = isRecord(value) && isRecord(value.result) ? value.result : value;
+ const effective = isRecord(configResult) && isRecord(configResult.effective) ? configResult.effective : {};
+ const ai = isRecord(effective.ai) ? effective.ai : {};
+ const defaultProvider = asString(ai.defaultProvider) ?? asString(ai.mode);
+ const defaultModel = asString(ai.defaultModel);
+ const apiKeys = isRecord(ai.apiKeys) ? ai.apiKeys : {};
+ const cliProviders = {
+ claude: commandExists("claude"),
+ codex: commandExists("codex"),
+ opencode: commandExists("opencode"),
+ cursor: commandExists("agent") || commandExists("cursor-agent"),
+ };
+ const apiKeyProviders = Object.keys(apiKeys).filter((key) => Boolean(asString(apiKeys[key])));
+ const ready = Boolean(defaultProvider || defaultModel || apiKeyProviders.length || Object.values(cliProviders).some(Boolean));
+ return {
+ ready,
+ status: ready ? "ready" : "warning",
+ message: ready
+ ? "AI provider configuration or provider CLI availability was detected locally."
+ : "No AI provider configuration or provider CLI was detected locally.",
+ nextAction: ready
+ ? undefined
+ : "Configure AI providers in ADE desktop or install/sign in to a provider CLI.",
+ details: {
+ defaultProvider,
+ defaultModel,
+ apiKeyProviders,
+ cliProviders,
+ },
+ };
+}
+
+function checkComputerUseReadiness(): ReadinessCheck {
+ const isDarwin = process.platform === "darwin";
+ const screenshotReady = !isDarwin || commandExists("screencapture");
+ const appLaunchReady = !isDarwin || commandExists("open");
+ const guiReady = !isDarwin || commandExists("swift") || commandExists("osascript");
+ const ready = isDarwin && screenshotReady && appLaunchReady && guiReady;
+ return {
+ ready,
+ status: ready ? "ready" : isDarwin ? "warning" : "unavailable",
+ message: ready
+ ? "Local macOS computer-use fallback commands are available."
+ : isDarwin
+ ? "One or more local macOS computer-use fallback commands are missing."
+ : "Local computer-use fallback is macOS-only.",
+ nextAction: ready
+ ? undefined
+ : isDarwin
+ ? "Install or expose screencapture/open/swift/osascript on PATH, or use an external proof backend."
+ : "Use ADE desktop on macOS or an external proof backend for computer-use capture.",
+ details: {
+ platform: process.platform,
+ screenshotReady,
+ appLaunchReady,
+ guiReady,
+ },
+ };
+}
+
+function checkPathReadiness(): ReadinessCheck {
+ const which = runLocalCommand("which", ["ade"], process.cwd());
+ const current = path.resolve(process.argv[1] ?? "");
+ const whichPath = which.ok && which.stdout ? path.resolve(which.stdout.split("\n")[0]!) : null;
+ const onPath = Boolean(whichPath);
+ return {
+ ready: onPath,
+ status: onPath ? "ready" : "warning",
+ message: onPath ? "ade is available on PATH." : "ade is not available on PATH.",
+ nextAction: onPath
+ ? undefined
+ : "Run npm link in apps/ade-cli or the packaged install-path.sh script.",
+ details: {
+ currentCliPath: current || null,
+ pathAde: whichPath,
+ sameBinary: Boolean(whichPath && current && whichPath === current),
+ electronRunAsNode: process.env.ELECTRON_RUN_AS_NODE === "1",
+ electronVersion: process.versions.electron ?? null,
+ },
+ };
+}
+
+function requireAdeLayout(): { resolveAdeLayout: (projectRoot: string) => { secretsDir: string } } {
+ // The CLI loads the shared layout dynamically elsewhere; this CommonJS fallback
+ // keeps readiness checks synchronous and local-only.
+ return { resolveAdeLayout: (projectRoot: string) => ({ secretsDir: path.join(projectRoot, ".ade", "secrets") }) };
+}
+
+function actionDomainCounts(value: unknown): Record {
+ const actions = isRecord(value) && Array.isArray(value.actions) ? value.actions.filter(isRecord) : [];
+ return actions.reduce>((acc, action) => {
+ const domain = asString(action.domain) ?? "core";
+ acc[domain] = (acc[domain] ?? 0) + 1;
+ return acc;
+ }, {});
+}
+
+function buildReadinessSnapshot(args: {
+ connection: CliConnection;
+ values: JsonObject;
+ summary: "doctor" | "auth";
+}): JsonObject {
+ const { connection, values, summary } = args;
+ const rpcActions = isRecord(values.rpcActions) && Array.isArray(values.rpcActions.actions) ? values.rpcActions.actions : [];
+ const actions = isRecord(values.actions) && Array.isArray(values.actions.actions) ? values.actions.actions : [];
+ const projectConfig = values.projectConfig;
+ const adeDir = path.join(connection.projectRoot, ".ade");
+ const sharedConfigPath = path.join(adeDir, "ade.yaml");
+ const localConfigPath = path.join(adeDir, "local.yaml");
+ const socketExists = fs.existsSync(connection.socketPath);
+ const desktopSocketAvailable = connection.mode === "desktop-socket";
+ const checks = {
+ git: checkGitReadiness(connection.projectRoot),
+ github: checkGitHubReadiness(connection.projectRoot),
+ linear: checkLinearReadiness(connection.projectRoot),
+ providers: checkProviderReadiness(projectConfig),
+ computerUse: checkComputerUseReadiness(),
+ path: checkPathReadiness(),
+ };
+ const recommendations = Object.entries(checks)
+ .filter(([, check]) => check.nextAction)
+ .map(([key, check]) => `${key}: ${check.nextAction}`);
+ if (!desktopSocketAvailable) {
+ recommendations.unshift("desktop: Start ADE desktop or pass --socket when Work chat, Path to Merge, Run tab state, or UI-owned proof state is required.");
+ }
+ const projectInitialized = fs.existsSync(adeDir);
+ if (!projectInitialized) {
+ recommendations.unshift("project: Run ade doctor from an ADE project or pass --project-root .");
+ }
+ const actionCountsByDomain = actionDomainCounts(values.actions);
+ const ready = projectInitialized && checks.git.ready && actions.length > 0;
+
+ return {
+ ok: ready,
+ cliVersion: VERSION,
+ protocolVersion: PROTOCOL_VERSION,
+ mode: connection.mode,
+ selectedMode: connection.mode,
+ requestedMode: desktopSocketAvailable ? "desktop-socket" : "headless",
+ runtime: {
+ node: process.version,
+ execPath: process.execPath,
+ electron: process.versions.electron ?? null,
+ platform: process.platform,
+ arch: process.arch,
+ },
+ projectRoot: connection.projectRoot,
+ workspaceRoot: connection.workspaceRoot,
+ project: {
+ projectRoot: connection.projectRoot,
+ workspaceRoot: connection.workspaceRoot,
+ adeDir,
+ projectInitialized,
+ sharedConfigPath,
+ sharedConfigPresent: fs.existsSync(sharedConfigPath),
+ localConfigPath,
+ localConfigPresent: fs.existsSync(localConfigPath),
+ },
+ desktop: {
+ socketPath: connection.socketPath,
+ socketExists,
+ socketAvailable: desktopSocketAvailable,
+ message: desktopSocketAvailable
+ ? "Connected to live ADE desktop socket."
+ : socketExists
+ ? "Socket path exists but CLI is running in headless mode; the socket may be stale or unavailable."
+ : "No live ADE desktop socket was detected.",
+ },
+ actions: {
+ rpcActionCount: rpcActions.length,
+ actionCount: actions.length,
+ byDomain: actionCountsByDomain,
+ },
+ git: checks.git,
+ github: checks.github,
+ linear: checks.linear,
+ providers: checks.providers,
+ computerUse: checks.computerUse,
+ path: checks.path,
+ auth: {
+ localProjectAccess: projectInitialized && actions.length > 0,
+ providerSecretsExposed: false,
+ note: "ADE CLI auth is local project access. Provider and integration readiness is reported as presence-only metadata.",
+ },
+ networkChecks: {
+ performed: false,
+ message: "Default doctor/auth checks do not call provider, GitHub, or Linear networks.",
+ },
+ recommendations,
+ recommendation: recommendations[0] ?? (connection.mode === "desktop-socket"
+ ? "Using live ADE desktop state."
+ : "Headless mode is ready for local ADE actions; start ADE desktop for UI-owned runtime state."),
+ summary,
+ };
+}
+
+class SocketJsonRpcClient {
+ private buffer: Buffer = Buffer.alloc(0);
+ private nextId = 1;
+ private pending = new Map void;
+ reject: (error: Error) => void;
+ timer: ReturnType;
+ }>();
+
+ private constructor(private readonly socket: net.Socket, private readonly timeoutMs: number) {
+ socket.on("data", (chunk) => this.onData(Buffer.from(chunk)));
+ socket.on("error", (error) => this.rejectAll(error instanceof Error ? error : new Error(String(error))));
+ socket.on("close", () => this.rejectAll(new Error("ADE desktop socket closed.")));
+ }
+
+ static connect(socketPath: string, timeoutMs: number): Promise {
+ return new Promise((resolve, reject) => {
+ const socket = net.createConnection(socketPath);
+ const timer = setTimeout(() => {
+ socket.destroy();
+ reject(new Error(`Timed out connecting to ADE desktop socket at ${socketPath}.`));
+ }, Math.min(timeoutMs, 5000));
+ socket.once("connect", () => {
+ clearTimeout(timer);
+ resolve(new SocketJsonRpcClient(socket, timeoutMs));
+ });
+ socket.once("error", (error) => {
+ clearTimeout(timer);
+ reject(error);
+ });
+ });
+ }
+
+ request(method: string, params?: JsonObject): Promise {
+ const id = this.nextId;
+ this.nextId += 1;
+ const payload: JsonRpcRequest = {
+ jsonrpc: "2.0",
+ id,
+ method,
+ ...(params !== undefined ? { params } : {}),
+ };
+ const body = `${JSON.stringify(payload)}\n`;
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ this.pending.delete(id);
+ reject(new Error(`Timed out waiting for ${method}.`));
+ }, this.timeoutMs);
+ this.pending.set(id, { resolve, reject, timer });
+ this.socket.write(body, "utf8", (error) => {
+ if (!error) return;
+ clearTimeout(timer);
+ this.pending.delete(id);
+ reject(error);
+ });
+ });
+ }
+
+ close(): void {
+ this.socket.end();
+ }
+
+ private onData(chunk: Buffer): void {
+ this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;
+ while (true) {
+ const newline = this.buffer.indexOf(0x0a);
+ if (newline < 0) break;
+ const line = this.buffer.subarray(0, newline).toString("utf8").trim();
+ this.buffer = this.buffer.subarray(newline + 1);
+ if (!line) continue;
+ this.handleLine(line);
+ }
+ }
+
+ private handleLine(line: string): void {
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(line);
+ } catch (error) {
+ this.rejectAll(new Error(`Failed to parse ADE socket response: ${error instanceof Error ? error.message : String(error)}`));
+ return;
+ }
+ if (!isRecord(parsed)) return;
+ const id = typeof parsed.id === "number" ? parsed.id : null;
+ if (id == null) return;
+ const pending = this.pending.get(id);
+ if (!pending) return;
+ this.pending.delete(id);
+ clearTimeout(pending.timer);
+ if (isRecord(parsed.error)) {
+ pending.reject(new Error(asString(parsed.error.message) ?? "ADE JSON-RPC request failed."));
+ return;
+ }
+ pending.resolve(parsed.result);
+ }
+
+ private rejectAll(error: Error): void {
+ for (const [id, pending] of this.pending) {
+ this.pending.delete(id);
+ clearTimeout(pending.timer);
+ pending.reject(error);
+ }
+ }
+}
+
+class InProcessJsonRpcClient {
+ private nextId = 1;
+
+ constructor(
+ private readonly handler: JsonRpcHandler & { dispose?: () => void },
+ private readonly runtime: { dispose: () => void },
+ private readonly previousRole: string | undefined,
+ ) {}
+
+ async request(method: string, params?: JsonObject): Promise {
+ const request: JsonRpcRequest = {
+ jsonrpc: "2.0",
+ id: this.nextId as JsonRpcId,
+ method,
+ ...(params !== undefined ? { params } : {}),
+ };
+ this.nextId += 1;
+ return await this.handler(request);
+ }
+
+ close(): void {
+ try { this.handler.dispose?.(); } catch {}
+ try { this.runtime.dispose(); } catch {}
+ if (this.previousRole == null) delete process.env.ADE_DEFAULT_ROLE;
+ else process.env.ADE_DEFAULT_ROLE = this.previousRole;
+ }
+}
+
+async function initializeConnection(connection: CliConnection, options: GlobalOptions): Promise {
+ await connection.request("ade/initialize", {
+ protocolVersion: PROTOCOL_VERSION,
+ clientInfo: { name: "ade-cli", version: VERSION },
+ identity: {
+ callerId: "ade-cli",
+ role: options.role,
+ computerUsePolicy: {
+ mode: "auto",
+ allowLocalFallback: options.role !== "external",
+ retainArtifacts: true,
+ },
+ },
+ });
+}
+
+async function createConnection(options: GlobalOptions): Promise {
+ const roots = resolveRoots(options);
+ const { resolveAdeLayout } = await import("../../desktop/src/shared/adeLayout");
+ const layout = resolveAdeLayout(roots.projectRoot);
+
+ if (!options.headless && fs.existsSync(layout.socketPath)) {
+ try {
+ const socketClient = await SocketJsonRpcClient.connect(layout.socketPath, options.timeoutMs);
+ const connection: CliConnection = {
+ mode: "desktop-socket",
+ projectRoot: roots.projectRoot,
+ workspaceRoot: roots.workspaceRoot,
+ socketPath: layout.socketPath,
+ request: (method, params) => socketClient.request(method, params),
+ close: () => socketClient.close(),
+ };
+ await initializeConnection(connection, options);
+ return connection;
+ } catch (error) {
+ if (options.requireSocket) throw error;
+ }
+ }
+
+ if (options.requireSocket) {
+ throw new Error(`ADE desktop socket is not available at ${layout.socketPath}.`);
+ }
+
+ const previousRole = process.env.ADE_DEFAULT_ROLE;
+ process.env.ADE_DEFAULT_ROLE = options.role;
+ const [{ createAdeRuntime }, { createAdeRpcRequestHandler }] = await Promise.all([
+ import("./bootstrap"),
+ import("./adeRpcServer"),
+ ]);
+ const runtime = await createAdeRuntime({ projectRoot: roots.projectRoot, workspaceRoot: roots.workspaceRoot });
+ const handler = createAdeRpcRequestHandler({
+ runtime,
+ serverVersion: VERSION,
+ onActionsListChanged: () => {},
+ });
+
+ const inProcess = new InProcessJsonRpcClient(handler, runtime, previousRole);
+ const connection: CliConnection = {
+ mode: "headless",
+ projectRoot: roots.projectRoot,
+ workspaceRoot: roots.workspaceRoot,
+ socketPath: layout.socketPath,
+ request: (method, params) => inProcess.request(method, params),
+ close: () => inProcess.close(),
+ };
+ await initializeConnection(connection, options);
+ return connection;
+}
+
+function unwrapToolResult(result: unknown): unknown {
+ if (!isRecord(result)) return result;
+ if (result.isError === true) {
+ const structured = result.structuredContent;
+ const message = isRecord(structured) && isRecord(structured.error)
+ ? asString(structured.error.message) ?? "ADE tool call failed."
+ : "ADE tool call failed.";
+ throw new CliToolError(message, structured ?? result);
+ }
+ if (Object.prototype.hasOwnProperty.call(result, "structuredContent")) {
+ return result.structuredContent;
+ }
+ return result;
+}
+
+function renderLaneGraph(result: unknown): string {
+ const lanesRaw = isRecord(result) && Array.isArray(result.lanes) ? result.lanes : [];
+ const lanes = lanesRaw.filter(isRecord);
+ if (lanes.length === 0) return "ADE lanes\n(no lanes)";
+
+ const byParent = new Map();
+ const byId = new Map();
+ for (const lane of lanes) {
+ const id = asString(lane.id);
+ if (!id) continue;
+ byId.set(id, lane);
+ }
+ for (const lane of lanes) {
+ const parentId = asString(lane.parentLaneId);
+ const key = parentId && byId.has(parentId) ? parentId : "";
+ const children = byParent.get(key) ?? [];
+ children.push(lane);
+ byParent.set(key, children);
+ }
+ for (const children of byParent.values()) {
+ children.sort((left, right) => {
+ const leftDepth = typeof left.stackDepth === "number" ? left.stackDepth : 0;
+ const rightDepth = typeof right.stackDepth === "number" ? right.stackDepth : 0;
+ if (leftDepth !== rightDepth) return leftDepth - rightDepth;
+ return String(left.name ?? left.id ?? "").localeCompare(String(right.name ?? right.id ?? ""));
+ });
+ }
+
+ const lines = ["ADE lanes"];
+ const visit = (lane: JsonObject, prefix: string, isLast: boolean): void => {
+ const name = asString(lane.name) ?? asString(lane.id) ?? "(unknown)";
+ const branch = asString(lane.branchRef) ?? "";
+ const status = asString(lane.status) ?? "";
+ const archived = asString(lane.archivedAt) ? " archived" : "";
+ lines.push(`${prefix}${isLast ? "+- " : "+- "}${name}${branch ? ` [${branch}]` : ""}${status ? ` ${status}` : ""}${archived}`);
+ const id = asString(lane.id);
+ const children = id ? byParent.get(id) ?? [] : [];
+ children.forEach((child, index) => visit(child, `${prefix}${isLast ? " " : "| "}`, index === children.length - 1));
+ };
+ const roots = byParent.get("") ?? [];
+ roots.forEach((lane, index) => visit(lane, "", index === roots.length - 1));
+ return lines.join("\n");
+}
+
+function truncateCell(value: string, width = 42): string {
+ const normalized = value.replace(/\s+/g, " ").trim();
+ if (normalized.length <= width) return normalized;
+ if (width <= 3) return normalized.slice(0, width);
+ return `${normalized.slice(0, width - 3)}...`;
+}
+
+function cell(value: unknown, width = 42): string {
+ if (value == null) return "";
+ if (typeof value === "boolean") return value ? "yes" : "no";
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "";
+ if (typeof value === "string") return truncateCell(value, width);
+ if (Array.isArray(value)) return truncateCell(value.map((entry) => cell(entry, 18)).filter(Boolean).join(", "), width);
+ if (isRecord(value)) {
+ const id = asString(value.id) ?? asString(value.name) ?? asString(value.title);
+ return id ? truncateCell(id, width) : truncateCell(JSON.stringify(value), width);
+ }
+ return truncateCell(String(value), width);
+}
+
+function renderKeyValues(title: string, entries: Array<[string, unknown]>): string {
+ const rows = entries.filter(([, value]) => value !== undefined && value !== null && value !== "");
+ const labelWidth = Math.max(0, ...rows.map(([label]) => label.length));
+ return [
+ title,
+ ...rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${cell(value, 96)}`),
+ ].join("\n");
+}
+
+function renderTable(headers: string[], rows: unknown[][], emptyMessage: string): string {
+ if (rows.length === 0) return emptyMessage;
+ const widths = headers.map((header, index) => Math.max(
+ header.length,
+ ...rows.map((row) => cell(row[index], index === headers.length - 1 ? 64 : 28).length),
+ ));
+ const renderRow = (row: unknown[]) => row.map((entry, index) => cell(entry, index === headers.length - 1 ? 64 : 28).padEnd(widths[index] ?? 0)).join(" ").trimEnd();
+ return [
+ renderRow(headers),
+ widths.map((width) => "-".repeat(width)).join(" "),
+ ...rows.map(renderRow),
+ ].join("\n");
+}
+
+function firstArray(value: unknown, keys: string[]): JsonObject[] {
+ if (Array.isArray(value)) return value.filter(isRecord);
+ if (!isRecord(value)) return [];
+ for (const key of keys) {
+ const entry = value[key];
+ if (Array.isArray(entry)) return entry.filter(isRecord);
+ }
+ return [];
+}
+
+function firstRecord(value: unknown, keys: string[]): JsonObject | null {
+ if (!isRecord(value)) return null;
+ for (const key of keys) {
+ const entry = value[key];
+ if (isRecord(entry)) return entry;
+ }
+ return null;
+}
+
+function statusWord(value: unknown): string {
+ const raw = cell(value, 24).toLowerCase();
+ if (!raw) return "";
+ if (["success", "passing", "passed", "completed", "ready", "clean", "ok"].includes(raw)) return "OK";
+ if (["failure", "failed", "failing", "error", "blocked", "dirty"].includes(raw)) return "FAIL";
+ if (["pending", "running", "in_progress", "queued", "active"].includes(raw)) return "WAIT";
+ return raw.toUpperCase();
+}
+
+function formatActionsList(value: unknown): string {
+ const actions = firstArray(value, ["actions"]);
+ if (actions.length === 0) return "ADE actions\n(no actions)";
+ const byDomain = new Map();
+ for (const action of actions) {
+ const name = asString(action.name);
+ const domain = asString(action.domain) ?? (name?.includes(".") ? name.split(".")[0] : null) ?? "core";
+ const list = byDomain.get(domain) ?? [];
+ list.push(action);
+ byDomain.set(domain, list);
+ }
+ const lines = ["ADE actions"];
+ for (const [domain, list] of [...byDomain.entries()].sort(([left], [right]) => left.localeCompare(right))) {
+ lines.push("", `${domain}:`);
+ for (const action of list.sort((left, right) => cell(left.action ?? left.name).localeCompare(cell(right.action ?? right.name)))) {
+ const name = asString(action.action) ?? asString(action.name) ?? "(unknown)";
+ const description = asString(action.description) ?? "";
+ lines.push(` ${name}${description ? ` - ${truncateCell(description, 86)}` : ""}`);
+ }
+ }
+ return lines.join("\n");
+}
+
+function formatLaneDetail(value: unknown): string {
+ const root = isRecord(value) ? value : {};
+ const lane = firstRecord(value, ["lane"]) ?? (isRecord(value) ? value : {});
+ return renderKeyValues("ADE lane", [
+ ["id", lane.id],
+ ["name", lane.name],
+ ["branch", lane.branchRef ?? lane.branch],
+ ["base", lane.baseBranch ?? lane.baseRef],
+ ["status", lane.status ?? root.rebaseStatus],
+ ["worktree", lane.worktreePath],
+ ]);
+}
+
+function formatPrList(value: unknown): string {
+ const prs = firstArray(value, ["prs", "pullRequests", "items", "results"]);
+ return renderTable(
+ ["PR", "state", "lane", "branch", "title"],
+ prs.map((pr) => [
+ pr.number ?? pr.prNumber ?? pr.id,
+ pr.state ?? pr.status,
+ pr.laneId ?? pr.laneName,
+ pr.headRefName ?? pr.branchRef ?? pr.branch,
+ pr.title,
+ ]),
+ "ADE pull requests\n(no PRs)",
+ );
+}
+
+function formatPrChecks(value: unknown): string {
+ const checks = firstArray(value, ["checks", "items"]);
+ const summary = isRecord(value) ? value.summary : null;
+ const header = summary ? `ADE PR checks - ${cell(summary, 80)}` : "ADE PR checks";
+ return `${header}\n${renderTable(
+ ["status", "name", "details"],
+ checks.map((check) => [
+ statusWord(check.conclusion ?? check.status),
+ check.name,
+ check.detailsUrl ?? check.url ?? check.completedAt,
+ ]),
+ "(no checks)",
+ )}`;
+}
+
+function formatPrComments(value: unknown): string {
+ const threads = firstArray(value, ["reviewThreads", "threads"]);
+ const comments = firstArray(value, ["comments", "issueComments"]);
+ const lines = ["ADE PR comments"];
+ if (threads.length > 0) {
+ lines.push("", renderTable(
+ ["thread", "state", "file", "comment"],
+ threads.map((thread) => {
+ const threadComments = Array.isArray(thread.comments) ? thread.comments.filter(isRecord) : [];
+ const first = threadComments[0] ?? {};
+ return [
+ thread.id,
+ thread.isResolved ? "resolved" : "open",
+ `${cell(thread.path, 34)}${thread.line ? `:${thread.line}` : ""}`,
+ first.body ?? thread.body,
+ ];
+ }),
+ "(no review threads)",
+ ));
+ }
+ if (comments.length > 0) {
+ lines.push("", renderTable(
+ ["id", "author", "comment"],
+ comments.map((comment) => [comment.id, comment.author ?? comment.user, comment.body]),
+ "(no issue comments)",
+ ));
+ }
+ if (threads.length === 0 && comments.length === 0) lines.push("(no comments)");
+ return lines.join("\n");
+}
+
+function formatFileTree(value: unknown): string {
+ const entries = firstArray(value, ["entries", "nodes", "items", "children"]);
+ return renderTable(
+ ["type", "path", "size"],
+ entries.map((entry) => [entry.type ?? (entry.isDirectory ? "dir" : "file"), entry.path ?? entry.name, entry.sizeBytes ?? entry.size]),
+ "ADE files\n(no entries)",
+ );
+}
+
+function formatFileRead(value: unknown): string {
+ if (typeof value === "string") return value;
+ if (!isRecord(value)) return JSON.stringify(value, null, 2);
+ const text = typeof value.text === "string" ? value.text : typeof value.content === "string" ? value.content : null;
+ return text ?? JSON.stringify(value, null, 2);
+}
+
+function formatFilesSearch(value: unknown): string {
+ const matches = firstArray(value, ["matches", "results", "items"]);
+ return renderTable(
+ ["file", "line", "match"],
+ matches.map((match) => [match.path ?? match.filePath, match.line ?? match.lineNumber, match.preview ?? match.text ?? match.match]),
+ "ADE file search\n(no matches)",
+ );
+}
+
+function formatDiffSummary(value: unknown): string {
+ const files = firstArray(value, ["files", "changes", "items"]);
+ return renderTable(
+ ["status", "file", "+", "-"],
+ files.map((file) => [
+ file.status ?? file.changeType ?? file.type,
+ file.path ?? file.filePath ?? file.newPath ?? file.oldPath,
+ file.additions ?? file.added ?? "",
+ file.deletions ?? file.deleted ?? "",
+ ]),
+ "ADE diff\n(no changed files)",
+ );
+}
+
+function formatRunTable(value: unknown, title: string): string {
+ const rows = firstArray(value, ["processes", "definitions", "runtime", "runs", "items"]);
+ return `${title}\n${renderTable(
+ ["id", "status", "lane", "command"],
+ rows.map((row) => [
+ row.id ?? row.processId ?? row.runId ?? row.name,
+ row.status ?? row.state,
+ row.laneId ?? row.laneName,
+ row.command ?? row.startupCommand ?? row.title,
+ ]),
+ "(none)",
+ )}`;
+}
+
+function formatChatList(value: unknown): string {
+ const sessions = firstArray(value, ["sessions", "chats", "items"]);
+ return renderTable(
+ ["session", "provider", "lane", "title"],
+ sessions.map((session) => [session.id ?? session.sessionId, session.provider ?? session.modelId, session.laneId, session.title]),
+ "ADE chats\n(no sessions)",
+ );
+}
+
+function formatTestsRuns(value: unknown): string {
+ const runs = firstArray(value, ["runs", "items"]);
+ return renderTable(
+ ["run", "status", "suite", "duration"],
+ runs.map((run) => [run.id ?? run.runId, statusWord(run.status), run.suiteId ?? run.suiteName, run.durationMs]),
+ "ADE test runs\n(no runs)",
+ );
+}
+
+function formatProofList(value: unknown): string {
+ const artifacts = firstArray(value, ["artifacts", "items"]);
+ return renderTable(
+ ["kind", "created", "title", "path"],
+ artifacts.map((artifact) => [artifact.kind ?? artifact.type, artifact.createdAt, artifact.title ?? artifact.name, artifact.path ?? artifact.uri]),
+ "ADE proof artifacts\n(no artifacts)",
+ );
+}
+
+function formatTextOutput(value: unknown, formatter: FormatterId | undefined): string {
+ if (typeof value === "string") return value;
+ if (isRecord(value) && typeof value.visual === "string" && (!formatter || formatter === "lanes")) return value.visual;
+ switch (formatter) {
+ case "status":
+ return renderKeyValues("ADE status", [
+ ["ok", isRecord(value) ? value.ok : null],
+ ["mode", isRecord(value) ? value.mode : null],
+ ["project", isRecord(value) ? value.projectRoot : null],
+ ["workspace", isRecord(value) ? value.workspaceRoot : null],
+ ["socket", isRecord(value) ? value.socketPath : null],
+ ]);
+ case "doctor":
+ {
+ const project = isRecord(value) && isRecord(value.project) ? value.project : {};
+ const desktop = isRecord(value) && isRecord(value.desktop) ? value.desktop : {};
+ const actions = isRecord(value) && isRecord(value.actions) ? value.actions : {};
+ const git = isRecord(value) && isRecord(value.git) ? value.git : {};
+ const github = isRecord(value) && isRecord(value.github) ? value.github : {};
+ const linear = isRecord(value) && isRecord(value.linear) ? value.linear : {};
+ const providers = isRecord(value) && isRecord(value.providers) ? value.providers : {};
+ const computerUse = isRecord(value) && isRecord(value.computerUse) ? value.computerUse : {};
+ const pathStatus = isRecord(value) && isRecord(value.path) ? value.path : {};
+ const recommendations = isRecord(value) && Array.isArray(value.recommendations) ? value.recommendations : [];
+ return [
+ renderKeyValues("ADE doctor", [
+ ["ok", isRecord(value) ? value.ok : null],
+ ["cli version", isRecord(value) ? value.cliVersion : null],
+ ["mode", isRecord(value) ? value.mode : null],
+ ["project", isRecord(value) ? value.projectRoot : null],
+ ["workspace", isRecord(value) ? value.workspaceRoot : null],
+ ["project initialized", project.projectInitialized],
+ ["desktop socket", desktop.socketAvailable],
+ ["socket path", desktop.socketPath],
+ ["rpc actions", actions.rpcActionCount],
+ ["service actions", actions.actionCount],
+ ["git", git.message],
+ ["github", github.message],
+ ["linear", linear.message],
+ ["providers", providers.message],
+ ["computer use", computerUse.message],
+ ["path", pathStatus.message],
+ ["recommendation", isRecord(value) ? value.recommendation : null],
+ ]),
+ ...(recommendations.length ? ["", "Next actions", ...recommendations.map((entry) => `- ${cell(entry, 120)}`)] : []),
+ ].join("\n");
+ }
+ case "auth":
+ {
+ const checks = isRecord(value) && isRecord(value.checks) ? value.checks : {};
+ const git = isRecord(checks.git) ? checks.git : {};
+ const github = isRecord(checks.github) ? checks.github : {};
+ const linear = isRecord(checks.linear) ? checks.linear : {};
+ const providers = isRecord(checks.providers) ? checks.providers : {};
+ return renderKeyValues("ADE auth", [
+ ["authenticated", isRecord(value) ? value.authenticated : null],
+ ["mode", isRecord(value) ? value.authMode : null],
+ ["role", isRecord(value) ? value.role : null],
+ ["project", isRecord(value) ? value.projectRoot : null],
+ ["actions", isRecord(value) ? value.availableActionCount : null],
+ ["git", git.message],
+ ["github", github.message],
+ ["linear", linear.message],
+ ["providers", providers.message],
+ ["note", isRecord(value) ? value.note : null],
+ ]);
+ }
+ case "lanes":
+ return renderLaneGraph(value);
+ case "lane-detail":
+ return formatLaneDetail(value);
+ case "git-status":
+ return renderKeyValues("ADE git status", Object.entries(isRecord(value) ? value : {}));
+ case "diff-summary":
+ return formatDiffSummary(value);
+ case "file-read":
+ return formatFileRead(value);
+ case "files-tree":
+ return formatFileTree(value);
+ case "files-search":
+ return formatFilesSearch(value);
+ case "prs-list":
+ return formatPrList(value);
+ case "pr-detail":
+ return renderKeyValues("ADE pull request", Object.entries(firstRecord(value, ["pr", "detail"]) ?? (isRecord(value) ? value : {})).slice(0, 16));
+ case "pr-checks":
+ return formatPrChecks(value);
+ case "pr-comments":
+ return formatPrComments(value);
+ case "run-defs":
+ return formatRunTable(value, "ADE run definitions");
+ case "run-runtime":
+ return formatRunTable(value, "ADE process runtime");
+ case "chat-list":
+ return formatChatList(value);
+ case "tests-runs":
+ return formatTestsRuns(value);
+ case "proof-list":
+ return formatProofList(value);
+ case "actions-list":
+ return formatActionsList(value);
+ case "action-result":
+ default:
+ if (isRecord(value)) return renderKeyValues("ADE result", Object.entries(value).slice(0, 24));
+ return JSON.stringify(value, null, 2);
+ }
+}
+
+function inferFormatter(plan: CliPlan & { kind: "execute" }): FormatterId | undefined {
+ if (plan.formatter) return plan.formatter;
+ if (plan.summary) return plan.summary;
+ if (plan.visualizer === "lanes") return "lanes";
+ const label = plan.label.toLowerCase();
+ if (label === "lane status") return "lane-detail";
+ if (label === "git status") return "git-status";
+ if (label === "diff changes") return "diff-summary";
+ if (label === "file read") return "file-read";
+ if (label === "file tree" || label === "file workspaces") return "files-tree";
+ if (label === "file search" || label === "file quick-open") return "files-search";
+ if (label === "pr list") return "prs-list";
+ if (label === "pr detail" || label === "pr health") return "pr-detail";
+ if (label === "pr checks") return "pr-checks";
+ if (label === "pr comments") return "pr-comments";
+ if (label === "process definitions") return "run-defs";
+ if (label === "process runtime") return "run-runtime";
+ if (label === "chat list") return "chat-list";
+ if (label === "test runs") return "tests-runs";
+ if (label === "proof list") return "proof-list";
+ if (label === "actions list") return "actions-list";
+ if (label.endsWith("actions")) return "actions-list";
+ return "action-result";
+}
+
+function summarizeExecution(args: {
+ plan: CliPlan & { kind: "execute" };
+ connection: CliConnection;
+ values: JsonObject;
+}): unknown {
+ const { plan, connection, values } = args;
+ if (plan.summary === "status") {
+ return {
+ ok: true,
+ mode: connection.mode,
+ projectRoot: connection.projectRoot,
+ workspaceRoot: connection.workspaceRoot,
+ socketPath: connection.socketPath,
+ ping: values.ping,
+ };
+ }
+ if (plan.summary === "doctor") {
+ return buildReadinessSnapshot({ connection, values, summary: "doctor" });
+ }
+ if (plan.summary === "auth") {
+ const readiness = buildReadinessSnapshot({ connection, values, summary: "auth" });
+ const actions = isRecord(readiness.actions) ? readiness.actions : {};
+ return {
+ ok: readiness.ok,
+ authenticated: isRecord(readiness.auth) ? readiness.auth.localProjectAccess : false,
+ authMode: connection.mode === "desktop-socket" ? "local-desktop-socket" : "local-headless-project",
+ role: process.env.ADE_DEFAULT_ROLE ?? "agent",
+ projectRoot: connection.projectRoot,
+ workspaceRoot: connection.workspaceRoot,
+ socketPath: connection.socketPath,
+ availableActionCount: actions.actionCount,
+ checks: {
+ git: readiness.git,
+ github: readiness.github,
+ linear: readiness.linear,
+ providers: readiness.providers,
+ computerUse: readiness.computerUse,
+ path: readiness.path,
+ },
+ recommendations: readiness.recommendations,
+ note: isRecord(readiness.auth) ? readiness.auth.note : "ADE CLI auth is local project access.",
+ };
+ }
+
+ const result = values.result ?? values;
+ if (
+ isRecord(result)
+ && Object.prototype.hasOwnProperty.call(result, "result")
+ && asString(result.domain)
+ && asString(result.action)
+ && !plan.label.toLowerCase().startsWith("action ")
+ && !plan.label.toLowerCase().endsWith(" action")
+ ) {
+ return result.result;
+ }
+ if (plan.visualizer === "lanes" && isRecord(result)) {
+ return {
+ ...result,
+ visual: renderLaneGraph(result),
+ };
+ }
+ return result;
+}
+
+async function executePlan(plan: CliPlan & { kind: "execute" }, options: GlobalOptions): Promise {
+ let connection: CliConnection;
+ try {
+ connection = await createConnection(options);
+ } catch (error) {
+ const roots = resolveRoots(options);
+ const socketPath = path.join(roots.projectRoot, ".ade", "ade.sock");
+ const requestedMode = options.requireSocket ? "desktop-socket" : options.headless ? "headless" : "auto";
+ throw new CliExecutionError(`Failed to initialize ADE CLI connection for ${plan.label}.`, {
+ cause: error instanceof Error ? error.message : String(error),
+ requestedMode,
+ projectRoot: roots.projectRoot,
+ workspaceRoot: roots.workspaceRoot,
+ socketPath,
+ nextAction: options.requireSocket
+ ? "Start ADE desktop for this project or remove --socket to allow headless mode."
+ : "Verify --project-root points at an ADE project and run ade doctor --json.",
+ });
+ }
+ try {
+ const values: JsonObject = {};
+ for (const step of plan.steps) {
+ try {
+ const raw = await connection.request(step.method, step.params);
+ values[step.key] = step.unwrapToolResult ? unwrapToolResult(raw) : raw;
+ } catch (error) {
+ if (!step.optional) throw error;
+ values[step.key] = {
+ ok: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+ }
+ return summarizeExecution({ plan, connection, values });
+ } catch (error) {
+ if (error instanceof CliToolError || error instanceof CliUsageError || error instanceof CliExecutionError) throw error;
+ throw new CliExecutionError(`Failed while running ${plan.label}.`, {
+ cause: error instanceof Error ? error.message : String(error),
+ mode: connection.mode,
+ projectRoot: connection.projectRoot,
+ workspaceRoot: connection.workspaceRoot,
+ socketPath: connection.socketPath,
+ nextAction: connection.mode === "desktop-socket"
+ ? "Check ADE desktop logs or retry with --headless if the workflow does not need UI-owned state."
+ : "Run ade doctor --json to inspect local project readiness, or start ADE desktop and retry with --socket.",
+ });
+ } finally {
+ await connection.close();
+ }
+}
+
+function formatOutput(value: unknown, options: GlobalOptions, formatter?: FormatterId): string {
+ if (options.text) {
+ return `${formatTextOutput(value, formatter)}\n`;
+ }
+ return `${JSON.stringify(value, null, options.pretty ? 2 : 0)}\n`;
+}
+
+async function runCli(argv: string[]): Promise<{ output: string; exitCode: number }> {
+ const parsed = parseCliArgs(argv);
+ const plan = buildCliPlan(parsed.command);
+ if (plan.kind === "help") return { output: plan.text.endsWith("\n") ? plan.text : `${plan.text}\n`, exitCode: 0 };
+ const originalConsole = {
+ log: console.log,
+ info: console.info,
+ warn: console.warn,
+ };
+ const writeDiagnostic = (...args: unknown[]) => {
+ process.stderr.write(`${args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" ")}\n`);
+ };
+ console.log = writeDiagnostic;
+ console.info = writeDiagnostic;
+ console.warn = writeDiagnostic;
+ let result: unknown;
+ try {
+ result = await executePlan(plan, parsed.options);
+ } finally {
+ console.log = originalConsole.log;
+ console.info = originalConsole.info;
+ console.warn = originalConsole.warn;
+ }
+ return { output: formatOutput(result, parsed.options, inferFormatter(plan)), exitCode: 0 };
+}
+
+async function main(): Promise {
+ const writeDiagnostic = (...args: unknown[]) => {
+ process.stderr.write(`${args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" ")}\n`);
+ };
+ console.log = writeDiagnostic;
+ console.info = writeDiagnostic;
+ console.warn = writeDiagnostic;
+ try {
+ const result = await runCli(process.argv.slice(2));
+ process.stdout.write(result.output);
+ process.exitCode = result.exitCode;
+ } catch (error) {
+ if (error instanceof CliUsageError) {
+ process.stderr.write(`ade: ${error.message}\nRun 'ade help'.\n`);
+ process.exitCode = 2;
+ return;
+ }
+ if (error instanceof CliToolError) {
+ process.stderr.write(`ade: ${error.message}\n`);
+ if (error.details !== undefined) {
+ process.stderr.write(`${JSON.stringify(error.details, null, 2)}\n`);
+ }
+ process.exitCode = 1;
+ return;
+ }
+ if (error instanceof CliExecutionError) {
+ process.stderr.write(`ade: ${error.message}\n`);
+ process.stderr.write(`${JSON.stringify(error.details, null, 2)}\n`);
+ process.exitCode = 1;
+ return;
+ }
+ process.stderr.write(`ade: ${error instanceof Error ? error.stack || error.message : String(error)}\n`);
+ process.exitCode = 1;
+ }
+}
+
+if (/(^|[/\\])cli\.(?:ts|js|cjs)$/.test(process.argv[1] ?? "")) {
+ void main();
+}
+
+export {
+ buildCliPlan,
+ formatOutput,
+ parseCliArgs,
+ renderLaneGraph,
+ runCli,
+ summarizeExecution,
+};
diff --git a/apps/mcp-server/src/headlessLinearServices.test.ts b/apps/ade-cli/src/headlessLinearServices.test.ts
similarity index 99%
rename from apps/mcp-server/src/headlessLinearServices.test.ts
rename to apps/ade-cli/src/headlessLinearServices.test.ts
index dc90b4d89..049a745f3 100644
--- a/apps/mcp-server/src/headlessLinearServices.test.ts
+++ b/apps/ade-cli/src/headlessLinearServices.test.ts
@@ -121,7 +121,6 @@ function createDeps() {
aiOrchestratorService: {} as any,
workerAgentService: {} as any,
workerBudgetService: {} as any,
- externalMcpService: {} as any,
computerUseArtifactBrokerService: {} as any,
openExternal: async () => {},
};
diff --git a/apps/mcp-server/src/headlessLinearServices.ts b/apps/ade-cli/src/headlessLinearServices.ts
similarity index 98%
rename from apps/mcp-server/src/headlessLinearServices.ts
rename to apps/ade-cli/src/headlessLinearServices.ts
index 7132c864b..806d3f8c1 100644
--- a/apps/mcp-server/src/headlessLinearServices.ts
+++ b/apps/ade-cli/src/headlessLinearServices.ts
@@ -29,10 +29,9 @@ import type { createLinearIngressService } from "../../desktop/src/main/services
import type { createWorkerTaskSessionService } from "../../desktop/src/main/services/cto/workerTaskSessionService";
import type { createWorkerHeartbeatService } from "../../desktop/src/main/services/cto/workerHeartbeatService";
import type { createAutomationSecretService } from "../../desktop/src/main/services/automations/automationSecretService";
-import type { ExternalMcpService } from "../../desktop/src/main/services/externalMcp/externalMcpService";
import type { ComputerUseArtifactBrokerService } from "../../desktop/src/main/services/computerUse/computerUseArtifactBrokerService";
import { getModelById, resolveModelAlias } from "../../desktop/src/shared/modelRegistry";
-import type { AdeMcpPaths } from "./bootstrap";
+import type { AdeRuntimePaths } from "./bootstrap";
import { createLinearClient as createLinearClientImpl } from "../../desktop/src/main/services/cto/linearClient";
import { createLinearIssueTracker as createLinearIssueTrackerImpl } from "../../desktop/src/main/services/cto/linearIssueTracker";
import { createLinearTemplateService as createLinearTemplateServiceImpl } from "../../desktop/src/main/services/cto/linearTemplateService";
@@ -120,7 +119,7 @@ type HeadlessTranscriptEntry = {
type HeadlessLinearDeps = {
projectRoot: string;
adeDir: string;
- paths: AdeMcpPaths;
+ paths: AdeRuntimePaths;
projectId: string;
db: AdeDb;
logger: Logger;
@@ -133,7 +132,6 @@ type HeadlessLinearDeps = {
aiOrchestratorService: ReturnType;
workerAgentService: ReturnType;
workerBudgetService: ReturnType;
- externalMcpService: ExternalMcpService;
computerUseArtifactBrokerService: ComputerUseArtifactBrokerService;
openExternal?: (url: string) => Promise;
};
@@ -243,7 +241,7 @@ function createHeadlessGitHubService(projectRoot: string, logger: Logger): Headl
headers: {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
- "user-agent": "ade-mcp-server",
+ "user-agent": "ade-cli",
...(args.body == null ? {} : { "content-type": "application/json" }),
},
body: args.body == null ? undefined : JSON.stringify(args.body),
@@ -350,8 +348,8 @@ function createHeadlessAgentChatService(projectRoot: string): HeadlessLinearServ
const defaultSummary = (identityKey?: string): string =>
identityKey
- ? `Headless MCP session for ${identityKey}. Automatic agent execution is not available in this runtime.`
- : "Headless MCP chat session. Automatic agent execution is not available in this runtime.";
+ ? `Headless ADE session for ${identityKey}. Automatic agent execution is not available in this runtime.`
+ : "Headless ADE chat session. Automatic agent execution is not available in this runtime.";
const resolveHeadlessModel = (modelId?: string | null): { modelId: string; model: string } => {
const requested = modelId?.trim() || HEADLESS_MODEL_ID;
@@ -535,7 +533,7 @@ function createHeadlessAgentChatService(projectRoot: string): HeadlessLinearServ
});
},
setComputerUseArtifactBrokerService() {
- // no-op in headless MCP mode
+ // no-op in headless mode
void projectRoot;
},
};
@@ -578,7 +576,7 @@ function createHeadlessWorkerHeartbeatService(): ReturnType void): void;
+ write(data: string): void;
+ close(): void;
+};
+
export type JsonRpcRequest = {
jsonrpc?: string;
id?: JsonRpcId;
diff --git a/apps/mcp-server/src/test/setup.ts b/apps/ade-cli/src/test/setup.ts
similarity index 97%
rename from apps/mcp-server/src/test/setup.ts
rename to apps/ade-cli/src/test/setup.ts
index 2df0e44b9..c69269c8f 100644
--- a/apps/mcp-server/src/test/setup.ts
+++ b/apps/ade-cli/src/test/setup.ts
@@ -10,7 +10,7 @@ type TestTempTrackerState = {
originalPromisesMkdtemp?: typeof fs.promises.mkdtemp;
};
-const TEST_TEMP_TRACKER_KEY = Symbol.for("ade.mcp.testTempTracker");
+const TEST_TEMP_TRACKER_KEY = Symbol.for("ade.cli.testTempTracker");
const testTempRoot = path.resolve(os.tmpdir());
function getTestTempTrackerState(): TestTempTrackerState {
diff --git a/apps/mcp-server/src/types/node-sqlite.d.ts b/apps/ade-cli/src/types/node-sqlite.d.ts
similarity index 100%
rename from apps/mcp-server/src/types/node-sqlite.d.ts
rename to apps/ade-cli/src/types/node-sqlite.d.ts
diff --git a/apps/mcp-server/tsconfig.json b/apps/ade-cli/tsconfig.json
similarity index 100%
rename from apps/mcp-server/tsconfig.json
rename to apps/ade-cli/tsconfig.json
diff --git a/apps/mcp-server/tsup.config.ts b/apps/ade-cli/tsup.config.ts
similarity index 91%
rename from apps/mcp-server/tsup.config.ts
rename to apps/ade-cli/tsup.config.ts
index cad3e3868..6c7da471a 100644
--- a/apps/mcp-server/tsup.config.ts
+++ b/apps/ade-cli/tsup.config.ts
@@ -2,7 +2,7 @@ import { defineConfig } from "tsup";
export default defineConfig({
entry: {
- index: "src/index.ts"
+ cli: "src/cli.ts"
},
format: ["cjs"],
platform: "node",
diff --git a/apps/mcp-server/vitest.config.ts b/apps/ade-cli/vitest.config.ts
similarity index 100%
rename from apps/mcp-server/vitest.config.ts
rename to apps/ade-cli/vitest.config.ts
diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json
index 7455d58a4..d889d99da 100644
--- a/apps/desktop/package-lock.json
+++ b/apps/desktop/package-lock.json
@@ -18,7 +18,6 @@
"@lobehub/icons": "^5.2.0",
"@lobehub/icons-static-svg": "^1.84.0",
"@lobehub/ui": "^5.6.3",
- "@modelcontextprotocol/sdk": "1.27.1",
"@opencode-ai/sdk": "^1.3.17",
"@phosphor-icons/react": "^2.1.10",
"@radix-ui/react-dialog": "^1.1.15",
@@ -1979,18 +1978,6 @@
"@hapi/hoek": "^9.0.0"
}
},
- "node_modules/@hono/node-server": {
- "version": "1.19.11",
- "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
- "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
- "license": "MIT",
- "engines": {
- "node": ">=18.14.1"
- },
- "peerDependencies": {
- "hono": "^4"
- }
- },
"node_modules/@huggingface/jinja": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.6.tgz",
@@ -3202,46 +3189,6 @@
"langium": "^4.0.0"
}
},
- "node_modules/@modelcontextprotocol/sdk": {
- "version": "1.27.1",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
- "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==",
- "license": "MIT",
- "dependencies": {
- "@hono/node-server": "^1.19.9",
- "ajv": "^8.17.1",
- "ajv-formats": "^3.0.1",
- "content-type": "^1.0.5",
- "cors": "^2.8.5",
- "cross-spawn": "^7.0.5",
- "eventsource": "^3.0.2",
- "eventsource-parser": "^3.0.0",
- "express": "^5.2.1",
- "express-rate-limit": "^8.2.1",
- "hono": "^4.11.4",
- "jose": "^6.1.3",
- "json-schema-typed": "^8.0.2",
- "pkce-challenge": "^5.0.0",
- "raw-body": "^3.0.0",
- "zod": "^3.25 || ^4.0",
- "zod-to-json-schema": "^3.25.1"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@cfworker/json-schema": "^4.1.1",
- "zod": "^3.25 || ^4.0"
- },
- "peerDependenciesMeta": {
- "@cfworker/json-schema": {
- "optional": true
- },
- "zod": {
- "optional": false
- }
- }
- },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
@@ -7363,19 +7310,6 @@
"node": "^18.17.0 || >=20.5.0"
}
},
- "node_modules/accepts": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
- "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "^3.0.0",
- "negotiator": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -7451,39 +7385,6 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
- "node_modules/ajv": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ajv-formats": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
- "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
- "license": "MIT",
- "dependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "ajv": {
- "optional": true
- }
- }
- },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -8082,46 +7983,6 @@
"readable-stream": "^3.4.0"
}
},
- "node_modules/body-parser": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
- "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "^3.1.2",
- "content-type": "^1.0.5",
- "debug": "^4.4.3",
- "http-errors": "^2.0.0",
- "iconv-lite": "^0.7.0",
- "on-finished": "^2.4.1",
- "qs": "^6.14.1",
- "raw-body": "^3.0.1",
- "type-is": "^2.0.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/body-parser/node_modules/iconv-lite": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
- "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/bonjour-service": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
@@ -8340,15 +8201,6 @@
"esbuild": ">=0.18"
}
},
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -8461,6 +8313,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -8470,22 +8323,6 @@
"node": ">= 0.4"
}
},
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -8962,28 +8799,6 @@
"node": "^14.18.0 || >=16.10.0"
}
},
- "node_modules/content-disposition": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
- "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -8991,24 +8806,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/cookie": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
- "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
- "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
- "license": "MIT",
- "engines": {
- "node": ">=6.6.0"
- }
- },
"node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -9017,23 +8814,6 @@
"license": "MIT",
"optional": true
},
- "node_modules/cors": {
- "version": "2.8.6",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
- "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
- "license": "MIT",
- "dependencies": {
- "object-assign": "^4",
- "vary": "^1"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/cose-base": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
@@ -9850,15 +9630,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -10171,6 +9942,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -10188,12 +9960,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "license": "MIT"
- },
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@@ -10435,15 +10201,6 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
- "node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
@@ -10539,6 +10296,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -10663,12 +10421,6 @@
"node": ">=6"
}
},
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
- },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -11000,36 +10752,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/eventsource": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
- "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
- "license": "MIT",
- "dependencies": {
- "eventsource-parser": "^3.0.1"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/eventsource-parser": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
- "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
- "license": "MIT",
- "engines": {
- "node": ">=18.0.0"
- }
- },
"node_modules/exponential-backoff": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
@@ -11037,67 +10759,6 @@
"dev": true,
"license": "Apache-2.0"
},
- "node_modules/express": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
- "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
- "license": "MIT",
- "dependencies": {
- "accepts": "^2.0.0",
- "body-parser": "^2.2.1",
- "content-disposition": "^1.0.0",
- "content-type": "^1.0.5",
- "cookie": "^0.7.1",
- "cookie-signature": "^1.2.1",
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "finalhandler": "^2.1.0",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.0",
- "merge-descriptors": "^2.0.0",
- "mime-types": "^3.0.0",
- "on-finished": "^2.4.1",
- "once": "^1.4.0",
- "parseurl": "^1.3.3",
- "proxy-addr": "^2.0.7",
- "qs": "^6.14.0",
- "range-parser": "^1.2.1",
- "router": "^2.2.0",
- "send": "^1.1.0",
- "serve-static": "^2.2.0",
- "statuses": "^2.0.1",
- "type-is": "^2.0.1",
- "vary": "^1.1.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express-rate-limit": {
- "version": "8.3.1",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
- "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
- "license": "MIT",
- "dependencies": {
- "ip-address": "10.1.0"
- },
- "engines": {
- "node": ">= 16"
- },
- "funding": {
- "url": "https://github.com/sponsors/express-rate-limit"
- },
- "peerDependencies": {
- "express": ">= 4.11"
- }
- },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -11207,22 +10868,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "BSD-3-Clause"
- },
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -11316,27 +10961,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/finalhandler": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
- "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "on-finished": "^2.4.1",
- "parseurl": "^1.3.3",
- "statuses": "^2.0.1"
- },
- "engines": {
- "node": ">= 18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
@@ -11500,15 +11124,6 @@
"node": ">= 0.6"
}
},
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -11550,15 +11165,6 @@
}
}
},
- "node_modules/fresh": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
- "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -11672,6 +11278,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -11705,6 +11312,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -11975,6 +11583,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -12321,15 +11930,6 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
- "node_modules/hono": {
- "version": "4.12.8",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz",
- "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==",
- "license": "MIT",
- "engines": {
- "node": ">=16.9.0"
- }
- },
"node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -12403,26 +12003,6 @@
"dev": true,
"license": "BSD-2-Clause"
},
- "node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -12578,6 +12158,7 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
"license": "ISC"
},
"node_modules/inline-style-parser": {
@@ -12606,20 +12187,12 @@
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
- "node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.10"
- }
- },
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -12796,12 +12369,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/is-promise": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
- "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
- "license": "MIT"
- },
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -12901,15 +12468,6 @@
"@sideway/pinpoint": "^2.0.0"
}
},
- "node_modules/jose": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz",
- "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/panva"
- }
- },
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
@@ -13057,18 +12615,6 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
- "node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT"
- },
- "node_modules/json-schema-typed": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
- "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
- "license": "BSD-2-Clause"
- },
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -13821,6 +13367,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -14158,27 +13705,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/media-typer": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
- "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
- "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/merge-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/merge-value/-/merge-value-1.0.0.tgz",
@@ -15074,31 +14600,6 @@
"node": ">=4.0.0"
}
},
- "node_modules/mime-db": {
- "version": "1.54.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
- "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
- "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "^1.54.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -15433,6 +14934,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -15609,18 +15111,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -15642,22 +15132,11 @@
"url": "https://github.com/sindresorhus/on-change?sponsor=1"
}
},
- "node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -15925,15 +15404,6 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -16004,16 +15474,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/path-to-regexp": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
- "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -16090,15 +15550,6 @@
"node": ">= 6"
}
},
- "node_modules/pkce-challenge": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
- "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
- "license": "MIT",
- "engines": {
- "node": ">=16.20.0"
- }
- },
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
@@ -16382,19 +15833,6 @@
"node": ">=12.0.0"
}
},
- "node_modules/proxy-addr": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
- "license": "MIT",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -16571,21 +16009,6 @@
"node": ">=6"
}
},
- "node_modules/qs": {
- "version": "6.15.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
- "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "side-channel": "^1.1.0"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/query-string": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.3.1.tgz",
@@ -16644,46 +16067,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/raw-body": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
- "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.7.0",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/raw-body/node_modules/iconv-lite": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
- "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/rc-collapse": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-4.0.0.tgz",
@@ -17654,15 +17037,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@@ -17928,22 +17302,6 @@
"points-on-path": "^0.2.1"
}
},
- "node_modules/router": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
- "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "is-promise": "^4.0.0",
- "parseurl": "^1.3.3",
- "path-to-regexp": "^8.0.0"
- },
- "engines": {
- "node": ">= 18"
- }
- },
"node_modules/rrweb-cssom": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
@@ -18098,32 +17456,6 @@
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"license": "MIT"
},
- "node_modules/send": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
- "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.3",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.1",
- "mime-types": "^3.0.2",
- "ms": "^2.1.3",
- "on-finished": "^2.4.1",
- "range-parser": "^1.2.1",
- "statuses": "^2.0.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
@@ -18151,25 +17483,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/serve-static": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
- "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
- "license": "MIT",
- "dependencies": {
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "parseurl": "^1.3.3",
- "send": "^1.2.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -18206,12 +17519,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "license": "ISC"
- },
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -18359,78 +17666,6 @@
"@types/hast": "^3.0.4"
}
},
- "node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -18645,15 +17880,6 @@
"node": ">= 6"
}
},
- "node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/std-env": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
@@ -19201,15 +18427,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.6"
- }
- },
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
@@ -19442,20 +18659,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/type-is": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
- "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
- "license": "MIT",
- "dependencies": {
- "content-type": "^1.0.5",
- "media-typer": "^1.1.0",
- "mime-types": "^3.0.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -19646,15 +18849,6 @@
"node": ">= 4.0.0"
}
},
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -19806,15 +19000,6 @@
"integrity": "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A==",
"license": "MIT"
},
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/verror": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
@@ -21590,6 +20775,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
"license": "ISC"
},
"node_modules/ws": {
@@ -21734,15 +20920,6 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
- "node_modules/zod-to-json-schema": {
- "version": "3.25.1",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
- "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
- "license": "ISC",
- "peerDependencies": {
- "zod": "^3.25 || ^4"
- }
- },
"node_modules/zod-validation-error": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index c84c9d59d..b1a37646a 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -8,7 +8,7 @@
"license": "AGPL-3.0",
"scripts": {
"predev": "node ./scripts/clear-vite-cache.cjs && node ./scripts/normalize-runtime-binaries.cjs",
- "prebuild": "node ./scripts/normalize-runtime-binaries.cjs",
+ "prebuild": "node ./scripts/normalize-runtime-binaries.cjs && npm run ade:build",
"dev": "node ./scripts/ensure-electron.cjs && node ./scripts/dev.cjs",
"build": "tsup && vite build",
"dist:mac": "npm run build && electron-builder --mac --publish never",
@@ -28,10 +28,10 @@
"test:coverage": "vitest run --coverage",
"test:orchestrator-smoke": "vitest run src/main/services/orchestrator/orchestratorSmoke.test.ts --reporter=verbose",
"test:orchestrator-complex-mock": "vitest run src/main/services/orchestrator/orchestratorSmoke.test.ts -t \"complex mock prompt\" --reporter=verbose",
- "mcp:dev": "npm --prefix ../mcp-server run dev -- --project-root ../..",
- "mcp:build": "npm --prefix ../mcp-server run build",
- "mcp:typecheck": "npm --prefix ../mcp-server run typecheck",
- "mcp:test": "npm --prefix ../mcp-server run test",
+ "ade:dev": "npm --prefix ../ade-cli run dev -- --project-root ../..",
+ "ade:build": "npm --prefix ../ade-cli run build",
+ "ade:typecheck": "npm --prefix ../ade-cli run typecheck",
+ "ade:test": "npm --prefix ../ade-cli run test",
"lint": "node ./node_modules/eslint/bin/eslint.js \"src/**/*.{ts,tsx}\"",
"rebuild:native": "node ./scripts/rebuild-native.mjs",
"version:ci": "node ./scripts/set-ci-version.mjs",
@@ -47,7 +47,6 @@
"@lobehub/icons": "^5.2.0",
"@lobehub/icons-static-svg": "^1.84.0",
"@lobehub/ui": "^5.6.3",
- "@modelcontextprotocol/sdk": "1.27.1",
"@opencode-ai/sdk": "^1.3.17",
"@phosphor-icons/react": "^2.1.10",
"@radix-ui/react-dialog": "^1.1.15",
@@ -142,12 +141,29 @@
"!node_modules/onnxruntime-node/**"
],
"asarUnpack": [
- "dist/main/adeMcpProxy.cjs",
"dist/main/packagedRuntimeSmoke.cjs",
"node_modules/node-pty/**/*",
"node_modules/@huggingface/transformers/node_modules/onnxruntime-node/**",
"vendor/crsqlite/**"
],
+ "extraResources": [
+ {
+ "from": "../ade-cli/dist/cli.cjs",
+ "to": "ade-cli/cli.cjs"
+ },
+ {
+ "from": "../ade-cli/dist/cli.cjs.map",
+ "to": "ade-cli/cli.cjs.map"
+ },
+ {
+ "from": "scripts/ade-cli-macos-wrapper.sh",
+ "to": "ade-cli/bin/ade"
+ },
+ {
+ "from": "scripts/ade-cli-install-path.sh",
+ "to": "ade-cli/install-path.sh"
+ }
+ ],
"afterPack": "./scripts/after-pack-runtime-fixes.cjs",
"publish": {
"provider": "github",
diff --git a/apps/desktop/scripts/ade-cli-install-path.sh b/apps/desktop/scripts/ade-cli-install-path.sh
new file mode 100755
index 000000000..84d159b06
--- /dev/null
+++ b/apps/desktop/scripts/ade-cli-install-path.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+set -eu
+
+SOURCE=$0
+while [ -L "$SOURCE" ]; do
+ SOURCE_DIR=$(CDPATH= cd -P -- "$(dirname -- "$SOURCE")" && pwd)
+ TARGET=$(readlink "$SOURCE")
+ case "$TARGET" in
+ /*) SOURCE=$TARGET ;;
+ *) SOURCE=$SOURCE_DIR/$TARGET ;;
+ esac
+done
+
+SCRIPT_DIR=$(CDPATH= cd -P -- "$(dirname -- "$SOURCE")" && pwd)
+ADE_BIN=${ADE_BIN:-"$SCRIPT_DIR/bin/ade"}
+TARGET_PATH=${1:-"$HOME/.local/bin/ade"}
+TARGET_DIR=$(dirname -- "$TARGET_PATH")
+
+if [ ! -x "$ADE_BIN" ]; then
+ echo "ade install: missing bundled CLI wrapper at $ADE_BIN" >&2
+ exit 1
+fi
+
+mkdir -p "$TARGET_DIR"
+ln -sf "$ADE_BIN" "$TARGET_PATH"
+
+echo "Installed ade -> $ADE_BIN"
+echo "Ensure $TARGET_DIR is on PATH, then run: ade doctor"
diff --git a/apps/desktop/scripts/ade-cli-macos-wrapper.sh b/apps/desktop/scripts/ade-cli-macos-wrapper.sh
new file mode 100755
index 000000000..a9fd31980
--- /dev/null
+++ b/apps/desktop/scripts/ade-cli-macos-wrapper.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+set -eu
+
+SOURCE=$0
+while [ -L "$SOURCE" ]; do
+ SOURCE_DIR=$(CDPATH= cd -P -- "$(dirname -- "$SOURCE")" && pwd)
+ TARGET=$(readlink "$SOURCE")
+ case "$TARGET" in
+ /*) SOURCE=$TARGET ;;
+ *) SOURCE=$SOURCE_DIR/$TARGET ;;
+ esac
+done
+
+SCRIPT_DIR=$(CDPATH= cd -P -- "$(dirname -- "$SOURCE")" && pwd)
+CLI_JS=${ADE_CLI_JS:-"$SCRIPT_DIR/../cli.cjs"}
+
+if [ -n "${ADE_CLI_NODE:-}" ]; then
+ exec "$ADE_CLI_NODE" "$CLI_JS" "$@"
+fi
+
+CONTENTS_DIR=$(cd "$SCRIPT_DIR/../../.." 2>/dev/null && pwd || true)
+APP_EXE="$CONTENTS_DIR/MacOS/ADE"
+RESOURCES_DIR="$CONTENTS_DIR/Resources"
+
+if [ ! -x "$APP_EXE" ] && [ -d "$CONTENTS_DIR/MacOS" ]; then
+ for CANDIDATE in "$CONTENTS_DIR"/MacOS/*; do
+ if [ -x "$CANDIDATE" ] && [ ! -d "$CANDIDATE" ]; then
+ APP_EXE=$CANDIDATE
+ break
+ fi
+ done
+fi
+
+if [ -x "$APP_EXE" ]; then
+ NODE_PATH_VALUE="$RESOURCES_DIR/app.asar.unpacked/node_modules:$RESOURCES_DIR/app.asar/node_modules"
+ if [ -n "${NODE_PATH:-}" ]; then
+ NODE_PATH_VALUE="$NODE_PATH_VALUE:$NODE_PATH"
+ fi
+ ELECTRON_RUN_AS_NODE=1 NODE_PATH="$NODE_PATH_VALUE" exec "$APP_EXE" "$CLI_JS" "$@"
+fi
+
+if command -v node >/dev/null 2>&1; then
+ NODE_MAJOR=$(node -p "Number(process.versions.node.split('.')[0])" 2>/dev/null || echo 0)
+ if [ "$NODE_MAJOR" -ge 22 ]; then
+ exec node "$CLI_JS" "$@"
+ fi
+fi
+
+echo "ade: Node.js 22+ or the packaged ADE.app runtime is required to run this CLI." >&2
+exit 127
diff --git a/apps/desktop/scripts/after-pack-runtime-fixes.cjs b/apps/desktop/scripts/after-pack-runtime-fixes.cjs
index bf6ef7442..93381d230 100644
--- a/apps/desktop/scripts/after-pack-runtime-fixes.cjs
+++ b/apps/desktop/scripts/after-pack-runtime-fixes.cjs
@@ -13,9 +13,23 @@ module.exports = async function afterPack(context) {
}
const runtimeRoot = resolvePackagedRuntimeRoot(appBundlePath);
+ const bundledCliPath = path.join(appBundlePath, "Contents", "Resources", "ade-cli", "cli.cjs");
+ const bundledCliBinPath = path.join(appBundlePath, "Contents", "Resources", "ade-cli", "bin", "ade");
+ const bundledCliInstallerPath = path.join(appBundlePath, "Contents", "Resources", "ade-cli", "install-path.sh");
if (!fs.existsSync(runtimeRoot)) {
throw new Error(`[afterPack] Missing unpacked runtime payload: ${runtimeRoot}`);
}
+ if (!fs.existsSync(bundledCliPath)) {
+ throw new Error(`[afterPack] Missing bundled ADE CLI entry: ${bundledCliPath}`);
+ }
+ if (!fs.existsSync(bundledCliBinPath)) {
+ throw new Error(`[afterPack] Missing bundled ADE CLI wrapper: ${bundledCliBinPath}`);
+ }
+ if (!fs.existsSync(bundledCliInstallerPath)) {
+ throw new Error(`[afterPack] Missing bundled ADE CLI PATH installer: ${bundledCliInstallerPath}`);
+ }
+ fs.chmodSync(bundledCliBinPath, 0o755);
+ fs.chmodSync(bundledCliInstallerPath, 0o755);
const normalized = normalizeDesktopRuntimeBinaries(runtimeRoot);
for (const entry of normalized) {
@@ -23,7 +37,6 @@ module.exports = async function afterPack(context) {
}
const requiredScripts = [
- path.join(runtimeRoot, "dist", "main", "adeMcpProxy.cjs"),
path.join(runtimeRoot, "dist", "main", "packagedRuntimeSmoke.cjs"),
];
diff --git a/apps/desktop/scripts/extractKvDbBootstrapSql.mjs b/apps/desktop/scripts/extractKvDbBootstrapSql.mjs
index 1d3a792ec..4494d547e 100644
--- a/apps/desktop/scripts/extractKvDbBootstrapSql.mjs
+++ b/apps/desktop/scripts/extractKvDbBootstrapSql.mjs
@@ -94,7 +94,7 @@ export function buildKvDbBootstrapSql(options = {}) {
const ftsBody = findFunctionBody(source, "function ensureUnifiedMemoriesSearchTable");
const migrateBody = findFunctionBody(source, "function migrate");
const statements = [
- ...extractRunStatements(ftsBody, { firstOnly: true }),
+ extractRunStatements(ftsBody).at(-1),
...extractRunStatements(migrateBody),
]
.map(normalizeStatement)
diff --git a/apps/desktop/scripts/validate-mac-artifacts.mjs b/apps/desktop/scripts/validate-mac-artifacts.mjs
index f32f2267b..45f6c558f 100644
--- a/apps/desktop/scripts/validate-mac-artifacts.mjs
+++ b/apps/desktop/scripts/validate-mac-artifacts.mjs
@@ -154,18 +154,24 @@ async function validatePackagedRuntime(appPath, description) {
const resourcesPath = path.join(appPath, "Contents", "Resources");
const appAsarPath = path.join(resourcesPath, "app.asar");
const unpackedPath = path.join(resourcesPath, "app.asar.unpacked");
+ const adeCliPath = path.join(resourcesPath, "ade-cli", "cli.cjs");
+ const adeCliBinPath = path.join(resourcesPath, "ade-cli", "bin", "ade");
+ const adeCliInstallerPath = path.join(resourcesPath, "ade-cli", "install-path.sh");
const nodeModulesPath = path.join(unpackedPath, "node_modules");
const nodePtyModulePath = path.join(nodeModulesPath, "node-pty");
const smokeScriptPath = path.join(unpackedPath, "dist", "main", "packagedRuntimeSmoke.cjs");
- const adeMcpProxyPath = path.join(unpackedPath, "dist", "main", "adeMcpProxy.cjs");
console.log(`[release:mac] Smoke testing packaged runtime payload for ${description}`);
await assertPathExists(executablePath, "packaged app executable");
await assertPathExists(appAsarPath, "app.asar payload");
await assertPathExists(unpackedPath, "app.asar.unpacked runtime payload");
+ await assertPathExists(adeCliPath, "bundled ADE CLI entry");
+ await assertPathExists(adeCliBinPath, "bundled ADE CLI wrapper");
+ await assertPathExists(adeCliInstallerPath, "bundled ADE CLI PATH installer");
+ await assertExecutable(adeCliBinPath, "bundled ADE CLI wrapper");
+ await assertExecutable(adeCliInstallerPath, "bundled ADE CLI PATH installer");
await assertPathExists(nodePtyModulePath, "unpacked node-pty module");
await assertPathExists(smokeScriptPath, "unpacked packaged runtime smoke script");
- await assertPathExists(adeMcpProxyPath, "unpacked ADE MCP proxy script");
const nodePtyAddon = await findNodePtyAddon(nodePtyModulePath);
if (!nodePtyAddon) {
@@ -220,28 +226,15 @@ async function validatePackagedRuntime(appPath, description) {
if (payload?.codexExecutable !== "function") {
throw new Error(`[release:mac] Packaged smoke expected Codex executable resolver to be available, got ${String(payload?.codexExecutable)}`);
}
- if (payload?.launchMode !== "bundled_proxy") {
- throw new Error(`[release:mac] Packaged smoke expected bundled_proxy launch mode, got ${String(payload?.launchMode)}`);
- }
- if (!payload?.proxyProbe?.ok) {
- throw new Error("[release:mac] Packaged smoke failed to launch the bundled ADE MCP proxy in probe mode");
- }
- // Do not rely on probe mode alone here. The regression we fixed still let
- // the packaged proxy start, but chat MCP failed once Claude/Codex attempted
- // the first initialize handshake through that launch path.
- if (!payload?.proxyInitialize?.ok) {
- throw new Error(
- `[release:mac] Packaged smoke failed to complete MCP initialize through the bundled ADE proxy: ${
- String(payload?.proxyInitialize?.error || payload?.proxyInitialize?.stderr || "unknown error")
- }`
- );
- }
- if (payload?.proxyInitialize?.response?.result?.serverInfo?.name !== "ade-mcp-server") {
- throw new Error(
- `[release:mac] Packaged smoke expected ADE MCP initialize to report ade-mcp-server, got ${
- JSON.stringify(payload?.proxyInitialize?.response ?? null)
- }`
- );
+
+ const { stdout: adeCliHelp } = await execFileAsync(adeCliBinPath, ["--help"], {
+ cwd: resourcesPath,
+ env: {
+ ...process.env,
+ },
+ });
+ if (!adeCliHelp.includes("Agent-focused command-line interface for ADE")) {
+ throw new Error("[release:mac] Bundled ADE CLI wrapper did not print ADE CLI help");
}
console.log(`[release:mac] Packaged runtime smoke passed for ${description}: ${path.relative(appPath, nodePtyAddon)}`);
diff --git a/apps/desktop/src/main/adeMcpProxy.ts b/apps/desktop/src/main/adeMcpProxy.ts
deleted file mode 100644
index e38130352..000000000
--- a/apps/desktop/src/main/adeMcpProxy.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import fs from "node:fs";
-import net from "node:net";
-import path from "node:path";
-import { Buffer } from "node:buffer";
-import { resolveAdeLayout } from "../shared/adeLayout";
-import {
- asTrimmed,
- hasProxyIdentity,
- injectIdentityIntoInitializePayload,
- takeNextInboundMessage,
- type ProxyIdentity,
-} from "./adeMcpProxyUtils";
-
-process.env.ADE_STDIO_TRANSPORT ??= "1";
-
-type RuntimeRoots = {
- projectRoot: string;
- workspaceRoot: string;
-};
-
-const MCP_SOCKET_CONNECT_TIMEOUT_MS = 5_000;
-const MCP_SOCKET_CONNECT_RETRY_DELAY_MS = 150;
-
-function resolveCliArg(flag: string): string | null {
- const args = process.argv.slice(2);
- for (let i = 0; i < args.length; i += 1) {
- const value = args[i];
- if (value !== flag) continue;
- const next = args[i + 1];
- if (next?.trim()) return path.resolve(next.trim());
- }
- return null;
-}
-
-function hasFlag(flag: string): boolean {
- return process.argv.slice(2).includes(flag);
-}
-
-function resolveRoot(envKey: string, flag: string, fallback: string): string {
- const fromEnv = process.env[envKey]?.trim();
- if (fromEnv) return path.resolve(fromEnv);
- return resolveCliArg(flag) ?? fallback;
-}
-
-function resolveRuntimeRoots(): RuntimeRoots {
- const projectRoot = resolveRoot("ADE_PROJECT_ROOT", "--project-root", process.cwd());
- const workspaceRoot = resolveRoot("ADE_WORKSPACE_ROOT", "--workspace-root", projectRoot);
- return { projectRoot, workspaceRoot };
-}
-
-function asBoolFlag(value: string | undefined): boolean | null {
- const trimmed = value?.trim() ?? "";
- return trimmed === "1" ? true : trimmed === "0" ? false : null;
-}
-
-function resolveProxyIdentityFromEnv(): ProxyIdentity {
- const computerUseMode = asTrimmed(process.env.ADE_COMPUTER_USE_MODE);
- const allowLocalFallback = asBoolFlag(process.env.ADE_COMPUTER_USE_ALLOW_LOCAL_FALLBACK);
- const retainArtifacts = asBoolFlag(process.env.ADE_COMPUTER_USE_RETAIN_ARTIFACTS);
- const preferredBackend = asTrimmed(process.env.ADE_COMPUTER_USE_PREFERRED_BACKEND);
- const hasComputerUsePolicy =
- computerUseMode
- || typeof allowLocalFallback === "boolean"
- || typeof retainArtifacts === "boolean"
- || preferredBackend;
- return {
- chatSessionId: asTrimmed(process.env.ADE_CHAT_SESSION_ID),
- missionId: asTrimmed(process.env.ADE_MISSION_ID),
- runId: asTrimmed(process.env.ADE_RUN_ID),
- stepId: asTrimmed(process.env.ADE_STEP_ID),
- attemptId: asTrimmed(process.env.ADE_ATTEMPT_ID),
- ownerId: asTrimmed(process.env.ADE_OWNER_ID),
- role: asTrimmed(process.env.ADE_DEFAULT_ROLE),
- computerUsePolicy: hasComputerUsePolicy
- ? {
- mode: computerUseMode,
- allowLocalFallback,
- retainArtifacts,
- preferredBackend,
- }
- : null,
- };
-}
-
-function relayProxyInputWithIdentity(socket: net.Socket): void {
- const identity = resolveProxyIdentityFromEnv();
- if (!hasProxyIdentity(identity)) {
- process.stdin.pipe(socket);
- process.stdin.on("end", () => {
- socket.end();
- });
- return;
- }
-
- let pending: Buffer = Buffer.alloc(0);
- process.stdin.on("data", (chunk: Buffer) => {
- pending = Buffer.concat([pending, Buffer.from(chunk)]);
- while (true) {
- const parsed = takeNextInboundMessage(pending);
- if (!parsed) break;
- pending = parsed.rest;
- const payloadText = injectIdentityIntoInitializePayload(parsed.payloadText, identity);
- if (payloadText === parsed.payloadText) {
- socket.write(parsed.raw);
- continue;
- }
- if (parsed.transport === "jsonl") {
- socket.write(`${payloadText}\n`);
- continue;
- }
- const framed = `Content-Length: ${Buffer.byteLength(payloadText, "utf8")}\r\n\r\n${payloadText}`;
- socket.write(framed);
- }
- });
- process.stdin.on("end", () => {
- if (pending.length > 0) {
- socket.write(pending);
- pending = Buffer.alloc(0);
- }
- socket.end();
- });
-}
-
-function isRetriableSocketConnectError(error: NodeJS.ErrnoException): boolean {
- return error.code === "ENOENT" || error.code === "ECONNREFUSED";
-}
-
-async function connectToSocketWithRetry(socketPath: string): Promise {
- const deadline = Date.now() + MCP_SOCKET_CONNECT_TIMEOUT_MS;
-
- while (true) {
- const socket = net.createConnection(socketPath);
- try {
- await new Promise((resolve, reject) => {
- const handleConnect = () => {
- socket.off("error", handleError);
- resolve();
- };
- const handleError = (error: NodeJS.ErrnoException) => {
- socket.off("connect", handleConnect);
- reject(error);
- };
- socket.once("connect", handleConnect);
- socket.once("error", handleError);
- });
- return socket;
- } catch (error) {
- socket.destroy();
- const nextError = error as NodeJS.ErrnoException;
- if (!isRetriableSocketConnectError(nextError) || Date.now() >= deadline) {
- throw nextError;
- }
- await new Promise((resolve) => {
- const timer = setTimeout(resolve, MCP_SOCKET_CONNECT_RETRY_DELAY_MS);
- timer.unref?.();
- });
- }
- }
-}
-
-async function main(): Promise {
- const roots = resolveRuntimeRoots();
- const socketPath = process.env.ADE_MCP_SOCKET_PATH?.trim() || resolveAdeLayout(roots.projectRoot).socketPath;
-
- if (hasFlag("--probe")) {
- process.stdout.write(JSON.stringify({
- ok: true,
- mode: "bundled_proxy",
- projectRoot: roots.projectRoot,
- workspaceRoot: roots.workspaceRoot,
- socketPath,
- socketExists: fs.existsSync(socketPath),
- }));
- process.exit(0);
- }
-
- const socket = await connectToSocketWithRetry(socketPath);
-
- socket.on("error", (err) => {
- process.stderr.write(`[ade-mcp-proxy]: ${err.message}\n`);
- process.exit(1);
- });
-
- socket.on("close", () => {
- process.exit(0);
- });
-
- process.stdin.resume();
- relayProxyInputWithIdentity(socket);
- socket.pipe(process.stdout);
-}
-
-void main().catch((error) => {
- process.stderr.write(`[ade-mcp-proxy] ${error instanceof Error ? error.message : String(error)}\n`);
- process.exit(1);
-});
diff --git a/apps/desktop/src/main/adeMcpProxyUtils.test.ts b/apps/desktop/src/main/adeMcpProxyUtils.test.ts
deleted file mode 100644
index 59fe94f18..000000000
--- a/apps/desktop/src/main/adeMcpProxyUtils.test.ts
+++ /dev/null
@@ -1,449 +0,0 @@
-import { Buffer } from "node:buffer";
-import { describe, expect, it } from "vitest";
-import {
- asTrimmed,
- findHeaderBoundary,
- hasProxyIdentity,
- injectIdentityIntoInitializePayload,
- isRecord,
- parseContentLength,
- takeNextInboundMessage,
- type ProxyIdentity,
-} from "./adeMcpProxyUtils";
-
-const NULL_IDENTITY: ProxyIdentity = {
- chatSessionId: null,
- missionId: null,
- runId: null,
- stepId: null,
- attemptId: null,
- ownerId: null,
- role: null,
- computerUsePolicy: null,
-};
-
-describe("asTrimmed", () => {
- it("returns trimmed string for valid input", () => {
- expect(asTrimmed(" hello ")).toBe("hello");
- });
-
- it("returns the string unchanged when already trimmed", () => {
- expect(asTrimmed("hello")).toBe("hello");
- });
-
- it("returns null for undefined", () => {
- expect(asTrimmed(undefined)).toBeNull();
- });
-
- it("returns null for empty string", () => {
- expect(asTrimmed("")).toBeNull();
- });
-
- it("returns null for whitespace-only string", () => {
- expect(asTrimmed(" ")).toBeNull();
- expect(asTrimmed("\t\n")).toBeNull();
- });
-});
-
-describe("isRecord", () => {
- it("returns true for a plain object", () => {
- expect(isRecord({ a: 1 })).toBe(true);
- });
-
- it("returns true for an empty object", () => {
- expect(isRecord({})).toBe(true);
- });
-
- it("returns false for null", () => {
- expect(isRecord(null)).toBe(false);
- });
-
- it("returns false for undefined", () => {
- expect(isRecord(undefined)).toBe(false);
- });
-
- it("returns false for an array", () => {
- expect(isRecord([1, 2, 3])).toBe(false);
- });
-
- it("returns false for a string", () => {
- expect(isRecord("hello")).toBe(false);
- });
-
- it("returns false for a number", () => {
- expect(isRecord(42)).toBe(false);
- });
-
- it("returns false for a boolean", () => {
- expect(isRecord(true)).toBe(false);
- });
-});
-
-describe("hasProxyIdentity", () => {
- it("returns false when all fields are null", () => {
- expect(hasProxyIdentity(NULL_IDENTITY)).toBe(false);
- });
-
- it("returns true when missionId is set", () => {
- expect(hasProxyIdentity({ ...NULL_IDENTITY, missionId: "m-1" })).toBe(true);
- });
-
- it("returns true when chatSessionId is set", () => {
- expect(hasProxyIdentity({ ...NULL_IDENTITY, chatSessionId: "chat-1" })).toBe(true);
- });
-
- it("returns true when runId is set", () => {
- expect(hasProxyIdentity({ ...NULL_IDENTITY, runId: "r-1" })).toBe(true);
- });
-
- it("returns true when stepId is set", () => {
- expect(hasProxyIdentity({ ...NULL_IDENTITY, stepId: "s-1" })).toBe(true);
- });
-
- it("returns true when attemptId is set", () => {
- expect(hasProxyIdentity({ ...NULL_IDENTITY, attemptId: "a-1" })).toBe(true);
- });
-
- it("returns true when role is set", () => {
- expect(hasProxyIdentity({ ...NULL_IDENTITY, role: "coder" })).toBe(true);
- });
-
- it("returns true when ownerId is set", () => {
- expect(hasProxyIdentity({ ...NULL_IDENTITY, ownerId: "agent-1" })).toBe(true);
- });
-
- it("returns true when computerUsePolicy is set", () => {
- expect(hasProxyIdentity({
- ...NULL_IDENTITY,
- computerUsePolicy: {
- mode: "enabled",
- allowLocalFallback: null,
- retainArtifacts: null,
- preferredBackend: null,
- },
- })).toBe(true);
- });
-
- it("returns true when multiple fields are set", () => {
- expect(hasProxyIdentity({ ...NULL_IDENTITY, missionId: "m-1", role: "coder" })).toBe(true);
- });
-});
-
-describe("findHeaderBoundary", () => {
- it("finds CRLF boundary (\\r\\n\\r\\n)", () => {
- const buf = Buffer.from("Content-Length: 10\r\n\r\n{\"id\":1}");
- const result = findHeaderBoundary(buf);
- expect(result).toEqual({ index: 18, delimiterLength: 4 });
- });
-
- it("finds LF boundary (\\n\\n)", () => {
- const buf = Buffer.from("Content-Length: 10\n\n{\"id\":1}");
- const result = findHeaderBoundary(buf);
- expect(result).toEqual({ index: 18, delimiterLength: 2 });
- });
-
- it("picks the earlier boundary when both are present (CRLF first)", () => {
- const buf = Buffer.from("A\r\n\r\nB\n\nC");
- const result = findHeaderBoundary(buf);
- expect(result).toEqual({ index: 1, delimiterLength: 4 });
- });
-
- it("picks the earlier boundary when both are present (LF first)", () => {
- const buf = Buffer.from("A\n\nB\r\n\r\nC");
- const result = findHeaderBoundary(buf);
- expect(result).toEqual({ index: 1, delimiterLength: 2 });
- });
-
- it("returns null when no boundary is found", () => {
- const buf = Buffer.from("Content-Length: 10\r\n{\"id\":1}");
- expect(findHeaderBoundary(buf)).toBeNull();
- });
-
- it("returns null for empty buffer", () => {
- expect(findHeaderBoundary(Buffer.alloc(0))).toBeNull();
- });
-});
-
-describe("parseContentLength", () => {
- it("parses valid Content-Length header", () => {
- expect(parseContentLength("Content-Length: 42")).toBe(42);
- });
-
- it("handles case-insensitive matching", () => {
- expect(parseContentLength("content-length: 99")).toBe(99);
- expect(parseContentLength("CONTENT-LENGTH: 7")).toBe(7);
- expect(parseContentLength("Content-length: 123")).toBe(123);
- });
-
- it("handles extra whitespace around colon", () => {
- expect(parseContentLength("Content-Length : 55")).toBe(55);
- });
-
- it("parses from multi-line header block", () => {
- const block = "X-Custom: foo\r\nContent-Length: 128\r\nX-Other: bar";
- expect(parseContentLength(block)).toBe(128);
- });
-
- it("returns null when header is missing", () => {
- expect(parseContentLength("X-Custom: foo")).toBeNull();
- });
-
- it("returns null for non-numeric value", () => {
- expect(parseContentLength("Content-Length: abc")).toBeNull();
- });
-
- it("returns null for empty string", () => {
- expect(parseContentLength("")).toBeNull();
- });
-});
-
-describe("takeNextInboundMessage", () => {
- it("parses a JSONL message (starts with {)", () => {
- const json = '{"jsonrpc":"2.0","method":"initialize","id":1}';
- const buf = Buffer.from(json + "\n");
- const result = takeNextInboundMessage(buf);
- expect(result).not.toBeNull();
- expect(result!.transport).toBe("jsonl");
- expect(result!.payloadText).toBe(json);
- expect(result!.rest.length).toBe(0);
- });
-
- it("parses a JSONL message (starts with [)", () => {
- const json = '[{"jsonrpc":"2.0","id":1}]';
- const buf = Buffer.from(json + "\n");
- const result = takeNextInboundMessage(buf);
- expect(result).not.toBeNull();
- expect(result!.transport).toBe("jsonl");
- expect(result!.payloadText).toBe(json);
- });
-
- it("returns null for incomplete JSONL (no newline)", () => {
- const buf = Buffer.from('{"jsonrpc":"2.0","id":1}');
- expect(takeNextInboundMessage(buf)).toBeNull();
- });
-
- it("correctly separates payload from rest in JSONL", () => {
- const msg1 = '{"id":1}\n';
- const msg2 = '{"id":2}\n';
- const buf = Buffer.from(msg1 + msg2);
- const result = takeNextInboundMessage(buf);
- expect(result).not.toBeNull();
- expect(result!.payloadText).toBe('{"id":1}');
- expect(result!.rest.toString("utf8")).toBe(msg2);
- });
-
- it("parses a framed message with Content-Length header (CRLF)", () => {
- const body = '{"jsonrpc":"2.0","method":"test","id":2}';
- const frame = `Content-Length: ${body.length}\r\n\r\n${body}`;
- const buf = Buffer.from(frame);
- const result = takeNextInboundMessage(buf);
- expect(result).not.toBeNull();
- expect(result!.transport).toBe("framed");
- expect(result!.payloadText).toBe(body);
- expect(result!.rest.length).toBe(0);
- });
-
- it("parses a framed message with Content-Length header (LF)", () => {
- const body = '{"jsonrpc":"2.0","method":"test","id":3}';
- const frame = `Content-Length: ${body.length}\n\n${body}`;
- const buf = Buffer.from(frame);
- const result = takeNextInboundMessage(buf);
- expect(result).not.toBeNull();
- expect(result!.transport).toBe("framed");
- expect(result!.payloadText).toBe(body);
- });
-
- it("returns null for empty buffer", () => {
- expect(takeNextInboundMessage(Buffer.alloc(0))).toBeNull();
- });
-
- it("returns null for incomplete framed message (body too short)", () => {
- const frame = "Content-Length: 100\r\n\r\nshort";
- const buf = Buffer.from(frame);
- expect(takeNextInboundMessage(buf)).toBeNull();
- });
-
- it("returns null for framed message with missing Content-Length", () => {
- const frame = "X-Other: value\r\n\r\n{\"id\":1}";
- const buf = Buffer.from(frame);
- expect(takeNextInboundMessage(buf)).toBeNull();
- });
-
- it("correctly separates payload from rest in framed messages", () => {
- const body1 = '{"id":1}';
- const body2 = '{"id":2}';
- const frame1 = `Content-Length: ${body1.length}\r\n\r\n${body1}`;
- const frame2 = `Content-Length: ${body2.length}\r\n\r\n${body2}`;
- const buf = Buffer.from(frame1 + frame2);
- const result = takeNextInboundMessage(buf);
- expect(result).not.toBeNull();
- expect(result!.payloadText).toBe(body1);
- expect(result!.rest.toString("utf8")).toBe(frame2);
- });
-});
-
-describe("injectIdentityIntoInitializePayload", () => {
- const identity: ProxyIdentity = {
- chatSessionId: "chat-1",
- missionId: "m-1",
- runId: "r-1",
- stepId: "s-1",
- attemptId: "a-1",
- ownerId: "agent-1",
- role: "coder",
- computerUsePolicy: {
- mode: "enabled",
- allowLocalFallback: true,
- retainArtifacts: false,
- preferredBackend: "vnc",
- },
- };
-
- it("injects identity into initialize method", () => {
- const payload = JSON.stringify({
- jsonrpc: "2.0",
- method: "initialize",
- id: 1,
- params: {},
- });
- const result = JSON.parse(injectIdentityIntoInitializePayload(payload, identity));
- expect(result.params.identity).toEqual({
- chatSessionId: "chat-1",
- missionId: "m-1",
- runId: "r-1",
- stepId: "s-1",
- attemptId: "a-1",
- ownerId: "agent-1",
- role: "coder",
- computerUsePolicy: {
- mode: "enabled",
- allowLocalFallback: true,
- retainArtifacts: false,
- preferredBackend: "vnc",
- },
- });
- });
-
- it("does NOT modify non-initialize methods", () => {
- const payload = JSON.stringify({
- jsonrpc: "2.0",
- method: "tools/list",
- id: 2,
- params: {},
- });
- const result = injectIdentityIntoInitializePayload(payload, identity);
- expect(result).toBe(payload);
- });
-
- it("merges with existing identity without overwriting existing fields", () => {
- const payload = JSON.stringify({
- jsonrpc: "2.0",
- method: "initialize",
- id: 1,
- params: {
- identity: {
- chatSessionId: "existing-chat",
- missionId: "existing-mission",
- ownerId: "existing-owner",
- role: "existing-role",
- computerUsePolicy: {
- mode: "off",
- },
- },
- },
- });
- const result = JSON.parse(injectIdentityIntoInitializePayload(payload, identity));
- expect(result.params.identity.chatSessionId).toBe("existing-chat");
- expect(result.params.identity.missionId).toBe("existing-mission");
- expect(result.params.identity.ownerId).toBe("existing-owner");
- expect(result.params.identity.role).toBe("existing-role");
- expect(result.params.identity.runId).toBe("r-1");
- expect(result.params.identity.stepId).toBe("s-1");
- expect(result.params.identity.attemptId).toBe("a-1");
- expect(result.params.identity.computerUsePolicy).toEqual({
- mode: "off",
- allowLocalFallback: true,
- retainArtifacts: false,
- preferredBackend: "vnc",
- });
- });
-
- it("overwrites existing identity fields that are empty strings", () => {
- const payload = JSON.stringify({
- jsonrpc: "2.0",
- method: "initialize",
- id: 1,
- params: {
- identity: {
- chatSessionId: " ",
- missionId: " ",
- ownerId: "",
- role: "",
- },
- },
- });
- const result = JSON.parse(injectIdentityIntoInitializePayload(payload, identity));
- expect(result.params.identity.chatSessionId).toBe("chat-1");
- expect(result.params.identity.missionId).toBe("m-1");
- expect(result.params.identity.ownerId).toBe("agent-1");
- expect(result.params.identity.role).toBe("coder");
- });
-
- it("returns original text for invalid JSON", () => {
- const broken = "this is not json {{{";
- expect(injectIdentityIntoInitializePayload(broken, identity)).toBe(broken);
- });
-
- it("returns original text when identity has no fields set", () => {
- const payload = JSON.stringify({
- jsonrpc: "2.0",
- method: "initialize",
- id: 1,
- params: {},
- });
- expect(injectIdentityIntoInitializePayload(payload, NULL_IDENTITY)).toBe(payload);
- });
-
- it("returns original text when payload is not an object", () => {
- expect(injectIdentityIntoInitializePayload('"just a string"', identity)).toBe('"just a string"');
- });
-
- it("creates params object when params is missing", () => {
- const payload = JSON.stringify({
- jsonrpc: "2.0",
- method: "initialize",
- id: 1,
- });
- const result = JSON.parse(injectIdentityIntoInitializePayload(payload, identity));
- expect(result.params.identity).toEqual({
- chatSessionId: "chat-1",
- missionId: "m-1",
- runId: "r-1",
- stepId: "s-1",
- attemptId: "a-1",
- ownerId: "agent-1",
- role: "coder",
- computerUsePolicy: {
- mode: "enabled",
- allowLocalFallback: true,
- retainArtifacts: false,
- preferredBackend: "vnc",
- },
- });
- });
-
- it("preserves other params fields alongside identity", () => {
- const payload = JSON.stringify({
- jsonrpc: "2.0",
- method: "initialize",
- id: 1,
- params: {
- capabilities: { tools: true },
- },
- });
- const result = JSON.parse(injectIdentityIntoInitializePayload(payload, identity));
- expect(result.params.capabilities).toEqual({ tools: true });
- expect(result.params.identity.chatSessionId).toBe("chat-1");
- expect(result.params.identity.missionId).toBe("m-1");
- });
-});
diff --git a/apps/desktop/src/main/adeMcpProxyUtils.ts b/apps/desktop/src/main/adeMcpProxyUtils.ts
deleted file mode 100644
index 470ad5e66..000000000
--- a/apps/desktop/src/main/adeMcpProxyUtils.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import type { Buffer } from "node:buffer";
-
-export type ProxyIdentity = {
- chatSessionId: string | null;
- missionId: string | null;
- runId: string | null;
- stepId: string | null;
- attemptId: string | null;
- ownerId: string | null;
- role: string | null;
- computerUsePolicy: {
- mode: string | null;
- allowLocalFallback: boolean | null;
- retainArtifacts: boolean | null;
- preferredBackend: string | null;
- } | null;
-};
-
-export type ParsedInboundMessage = {
- transport: "jsonl" | "framed";
- payloadText: string;
- raw: Buffer;
- rest: Buffer;
-};
-
-export function asTrimmed(value: string | undefined): string | null {
- const trimmed = value?.trim() ?? "";
- return trimmed.length > 0 ? trimmed : null;
-}
-
-export function isRecord(value: unknown): value is Record {
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
-}
-
-export function hasProxyIdentity(identity: ProxyIdentity): boolean {
- const hasComputerUsePolicy = Boolean(
- identity.computerUsePolicy
- && (
- identity.computerUsePolicy.mode
- || typeof identity.computerUsePolicy.allowLocalFallback === "boolean"
- || typeof identity.computerUsePolicy.retainArtifacts === "boolean"
- || identity.computerUsePolicy.preferredBackend
- ),
- );
- return Boolean(
- identity.chatSessionId
- || identity.missionId
- || identity.runId
- || identity.stepId
- || identity.attemptId
- || identity.ownerId
- || identity.role
- || hasComputerUsePolicy,
- );
-}
-
-export function findHeaderBoundary(buffer: Buffer): { index: number; delimiterLength: number } | null {
- const crlf = buffer.indexOf("\r\n\r\n", 0, "utf8");
- const lf = buffer.indexOf("\n\n", 0, "utf8");
- if (crlf === -1 && lf === -1) return null;
- if (crlf === -1) return { index: lf, delimiterLength: 2 };
- if (lf === -1) return { index: crlf, delimiterLength: 4 };
- return crlf < lf ? { index: crlf, delimiterLength: 4 } : { index: lf, delimiterLength: 2 };
-}
-
-export function parseContentLength(headerBlock: string): number | null {
- const lines = headerBlock.split(/\r?\n/);
- for (const line of lines) {
- const match = /^content-length\s*:\s*(\d+)\s*$/i.exec(line.trim());
- if (!match) continue;
- return Number.parseInt(match[1] ?? "", 10);
- }
- return null;
-}
-
-export function takeNextInboundMessage(buffer: Buffer): ParsedInboundMessage | null {
- if (!buffer.length) return null;
- const first = buffer[0]!;
-
- if (first === 0x7b || first === 0x5b) {
- const newline = buffer.indexOf(0x0a);
- if (newline === -1) return null;
- const raw = buffer.subarray(0, newline + 1);
- const payloadText = buffer.subarray(0, newline).toString("utf8").trim();
- return {
- transport: "jsonl",
- payloadText,
- raw,
- rest: buffer.subarray(newline + 1),
- };
- }
-
- const boundary = findHeaderBoundary(buffer);
- if (!boundary) return null;
- const headerBlock = buffer.subarray(0, boundary.index).toString("utf8");
- const contentLength = parseContentLength(headerBlock);
- if (contentLength == null || contentLength < 0) return null;
- const bodyStart = boundary.index + boundary.delimiterLength;
- const bodyEnd = bodyStart + contentLength;
- if (buffer.length < bodyEnd) return null;
-
- return {
- transport: "framed",
- payloadText: buffer.subarray(bodyStart, bodyEnd).toString("utf8"),
- raw: buffer.subarray(0, bodyEnd),
- rest: buffer.subarray(bodyEnd),
- };
-}
-
-export function injectIdentityIntoInitializePayload(payloadText: string, identity: ProxyIdentity): string {
- if (!hasProxyIdentity(identity)) return payloadText;
- let payload: unknown;
- try {
- payload = JSON.parse(payloadText);
- } catch {
- return payloadText;
- }
- if (!isRecord(payload) || payload.method !== "initialize") {
- return payloadText;
- }
-
- const params = isRecord(payload.params) ? { ...payload.params } : {};
- const existingIdentity = isRecord(params.identity) ? { ...params.identity } : {};
- const mergedIdentity: Record = { ...existingIdentity };
-
- const identityKeys = ["chatSessionId", "missionId", "runId", "stepId", "attemptId", "ownerId", "role"] as const;
- for (const key of identityKeys) {
- if (!identity[key]) continue;
- const existing = existingIdentity[key];
- if (typeof existing === "string" && existing.trim()) continue;
- mergedIdentity[key] = identity[key];
- }
-
- const proxyComputerUsePolicy = identity.computerUsePolicy;
- if (proxyComputerUsePolicy) {
- const existingComputerUsePolicy = isRecord(existingIdentity.computerUsePolicy)
- ? { ...existingIdentity.computerUsePolicy }
- : {};
- let shouldWriteComputerUsePolicy = false;
-
- const hasExistingString = (key: string): boolean =>
- typeof existingComputerUsePolicy[key] === "string" && (existingComputerUsePolicy[key] as string).trim().length > 0;
- const mergeString = (key: "mode" | "preferredBackend"): void => {
- if (proxyComputerUsePolicy[key] && !hasExistingString(key)) {
- existingComputerUsePolicy[key] = proxyComputerUsePolicy[key];
- shouldWriteComputerUsePolicy = true;
- }
- };
- const mergeBool = (key: "allowLocalFallback" | "retainArtifacts"): void => {
- if (typeof proxyComputerUsePolicy[key] === "boolean" && typeof existingComputerUsePolicy[key] !== "boolean") {
- existingComputerUsePolicy[key] = proxyComputerUsePolicy[key];
- shouldWriteComputerUsePolicy = true;
- }
- };
- mergeString("mode");
- mergeBool("allowLocalFallback");
- mergeBool("retainArtifacts");
- mergeString("preferredBackend");
-
- if (shouldWriteComputerUsePolicy) {
- mergedIdentity.computerUsePolicy = existingComputerUsePolicy;
- }
- }
-
- return JSON.stringify({
- ...payload,
- params: {
- ...params,
- identity: mergedIdentity,
- },
- });
-}
diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts
index c5af1bd13..4b7a96e00 100644
--- a/apps/desktop/src/main/main.ts
+++ b/apps/desktop/src/main/main.ts
@@ -1,6 +1,7 @@
import { app, BrowserWindow, dialog, nativeImage, protocol, shell } from "electron";
import path from "node:path";
-type NodePtyType = typeof import("node-pty");
+import type * as NodePty from "node-pty";
+type NodePtyType = typeof NodePty;
import { registerIpc } from "./services/ipc/registerIpc";
import { createFileLogger } from "./services/logging/logger";
import { openKvDb } from "./services/state/kvDb";
@@ -55,14 +56,13 @@ import type { PortLease, ProjectInfo } from "../shared/types";
import type { AppContext } from "./services/ipc/registerIpc";
import fs from "node:fs";
import net from "node:net";
-import { createMcpRequestHandler } from "../../../mcp-server/src/mcpServer";
+import { createAdeRpcRequestHandler } from "../../../ade-cli/src/adeRpcServer";
import {
createEventBuffer,
- type AdeMcpRuntime,
- type AdeMcpPaths,
-} from "../../../mcp-server/src/bootstrap";
-import { startJsonRpcServer } from "../../../mcp-server/src/jsonrpc";
-import type { JsonRpcTransport } from "../../../mcp-server/src/transport";
+ type AdeRuntime,
+ type AdeRuntimePaths,
+} from "../../../ade-cli/src/bootstrap";
+import { startJsonRpcServer, type JsonRpcTransport } from "../../../ade-cli/src/jsonrpc";
import { createKeybindingsService } from "./services/keybindings/keybindingsService";
import { createAgentToolsService } from "./services/agentTools/agentToolsService";
import { createDevToolsService } from "./services/devTools/devToolsService";
@@ -117,8 +117,6 @@ import { createOrchestratorService } from "./services/orchestrator/orchestratorS
import { createAiOrchestratorService } from "./services/orchestrator/aiOrchestratorService";
import { createMissionBudgetService } from "./services/orchestrator/missionBudgetService";
import { transitionMissionStatus } from "./services/orchestrator/missionLifecycle";
-import { createExternalMcpService } from "./services/externalMcp/externalMcpService";
-import { createExternalConnectionAuthService } from "./services/externalMcp/externalConnectionAuthService";
import { createComputerUseArtifactBrokerService } from "./services/computerUse/computerUseArtifactBrokerService";
import { createSyncService } from "./services/sync/syncService";
import { createAutoUpdateService } from "./services/updates/autoUpdateService";
@@ -738,7 +736,7 @@ app.whenReady().then(async () => {
const projectContexts = new Map();
const projectInitPromises = new Map>();
const closeContextPromises = new Map>();
- const mcpSocketCleanupByRoot = new Map void>();
+ const rpcSocketCleanupByRoot = new Map void>();
const projectLastActivatedAt = new Map();
const MAX_WARM_IDLE_PROJECT_CONTEXTS = 1;
let activeProjectRoot: string | null = null;
@@ -865,11 +863,11 @@ app.whenReady().then(async () => {
}
try {
- if ((ctx.getActiveMcpConnectionCount?.() ?? 0) > 0) {
+ if ((ctx.getActiveRpcConnectionCount?.() ?? 0) > 0) {
return true;
}
} catch (error) {
- return keepAliveOnProbeFailure("mcp_connections", error);
+ return keepAliveOnProbeFailure("rpc_connections", error);
}
try {
@@ -1458,9 +1456,6 @@ app.whenReady().then(async () => {
> | null = null;
let agentChatServiceRef: ReturnType | null =
null;
- let externalMcpServiceRef: ReturnType<
- typeof createExternalMcpService
- > | null = null;
const queueLandingService = createQueueLandingService({
db,
logger,
@@ -1557,7 +1552,6 @@ app.whenReady().then(async () => {
const ptyService = createPtyService({
projectRoot,
transcriptsDir: adePaths.transcriptsDir,
- chatSessionsDir: adePaths.chatSessionsDir,
laneService,
sessionService,
aiIntegrationService,
@@ -1950,7 +1944,6 @@ app.whenReady().then(async () => {
}
},
onSessionEnded: onTrackedSessionEnded,
- getExternalMcpConfigs: () => externalMcpServiceRef?.getRawConfigs() ?? [],
getDirtyFileTextForPath: async (absPath: string) => {
const trimmed = absPath.trim();
if (!trimmed) return undefined;
@@ -2195,26 +2188,6 @@ app.whenReady().then(async () => {
"ADE_ENABLE_MEMORY_FILE_SYNC",
);
- const externalConnectionAuthService = createExternalConnectionAuthService({
- adeDir: adePaths.adeDir,
- logger,
- });
- const externalMcpService = createExternalMcpService({
- projectRoot,
- adeDir: adePaths.adeDir,
- db,
- projectId,
- logger,
- workerAgentService,
- ctoStateService,
- missionService,
- workerBudgetService,
- missionBudgetService,
- authService: externalConnectionAuthService,
- onEvent: (event) =>
- emitProjectEvent(projectRoot, IPC.externalMcpEvent, event),
- });
- externalMcpServiceRef = externalMcpService;
scheduleBackgroundProjectTask(
"lanes.port_allocation_recovery",
() => recoverPortAllocations(),
@@ -2227,18 +2200,6 @@ app.whenReady().then(async () => {
"ADE_ENABLE_PORT_ALLOCATION_RECOVERY",
);
- scheduleBackgroundProjectTask(
- "external_mcp.start",
- () => externalMcpService.start(),
- (error) => {
- logger.warn("external_mcp.start_failed", {
- error: error instanceof Error ? error.message : String(error),
- });
- },
- 0,
- "ADE_ENABLE_EXTERNAL_MCP",
- );
-
const openclawBridgeService = createOpenclawBridgeService({
projectRoot,
adeDir: adePaths.adeDir,
@@ -2281,7 +2242,6 @@ app.whenReady().then(async () => {
episodicSummaryService,
proceduralLearningService,
knowledgeCaptureService,
- externalMcpService,
onEvent: (event) => {
aiOrchestratorServiceRef?.onOrchestratorRuntimeEvent(event);
openclawBridgeServiceRef?.onOrchestratorEvent(event);
@@ -2296,7 +2256,6 @@ app.whenReady().then(async () => {
projectRoot,
missionService,
orchestratorService,
- externalMcpService,
logger,
onEvent: (payload) =>
emitProjectEvent(projectRoot, IPC.computerUseEvent, payload),
@@ -2615,7 +2574,6 @@ app.whenReady().then(async () => {
adeProjectService,
automationService,
secretService: automationSecretService,
- externalMcpService,
logger,
onEvent: (event) =>
emitProjectEvent(projectRoot, IPC.projectStateEvent, event),
@@ -2851,14 +2809,14 @@ app.whenReady().then(async () => {
);
writeGlobalState(globalStatePath, state);
- // ── MCP Socket Server (embedded mode) ─────────────────────────
- const mcpEventBuffer = createEventBuffer();
- const mcpRuntime: AdeMcpRuntime = {
+ // ── ADE RPC Socket Server (embedded mode) ─────────────────────
+ const rpcEventBuffer = createEventBuffer();
+ const rpcRuntime: AdeRuntime = {
projectRoot,
workspaceRoot: projectRoot,
projectId,
project,
- paths: adePaths as unknown as AdeMcpPaths,
+ paths: adePaths as unknown as AdeRuntimePaths,
logger,
db,
laneService,
@@ -2884,30 +2842,29 @@ app.whenReady().then(async () => {
linearIngressService,
linearRoutingService,
processService,
- externalMcpService,
computerUseArtifactBrokerService,
orchestratorService,
aiOrchestratorService,
issueInventoryService,
- eventBuffer: mcpEventBuffer,
+ eventBuffer: rpcEventBuffer,
dispose: () => {}, // desktop manages service lifecycle
};
- // When ADE_MCP_SOCKET_PATH is set, derive a per-project socket path from
+ // When ADE_RPC_SOCKET_PATH is set, derive a per-project socket path from
// the override so each project context gets its own socket and avoids
// EADDRINUSE. The first context uses the env path as-is for compatibility;
// subsequent contexts append a project-root hash suffix.
- const envSocketOverride = process.env.ADE_MCP_SOCKET_PATH?.trim();
- const mcpSocketPath = envSocketOverride
+ const envSocketOverride = process.env.ADE_RPC_SOCKET_PATH?.trim();
+ const rpcSocketPath = envSocketOverride
? projectContexts.size === 0
? envSocketOverride
: `${envSocketOverride}.${Buffer.from(normalizeProjectRoot(projectRoot)).toString("base64url").slice(0, 8)}`
: adePaths.socketPath;
- const activeMcpConnections = new Set();
+ const activeRpcConnections = new Set();
- const destroyActiveMcpConnections = (): void => {
- for (const conn of activeMcpConnections) {
- activeMcpConnections.delete(conn);
+ const destroyActiveRpcConnections = (): void => {
+ for (const conn of activeRpcConnections) {
+ activeRpcConnections.delete(conn);
try {
conn.destroy();
} catch {
@@ -2915,18 +2872,18 @@ app.whenReady().then(async () => {
}
}
};
- mcpSocketCleanupByRoot.set(
+ rpcSocketCleanupByRoot.set(
normalizeProjectRoot(projectRoot),
- destroyActiveMcpConnections,
+ destroyActiveRpcConnections,
);
// Clean stale socket from prior crash
try {
- fs.unlinkSync(mcpSocketPath);
+ fs.unlinkSync(rpcSocketPath);
} catch {}
- const mcpSocketServer = net.createServer((conn) => {
- activeMcpConnections.add(conn);
+ const rpcSocketServer = net.createServer((conn) => {
+ activeRpcConnections.add(conn);
let stopped = false;
const transport: JsonRpcTransport = {
onData(callback) {
@@ -2940,16 +2897,16 @@ app.whenReady().then(async () => {
},
};
let stop: ReturnType | null = null;
- const mcpHandler = createMcpRequestHandler({
- runtime: mcpRuntime,
+ const rpcHandler = createAdeRpcRequestHandler({
+ runtime: rpcRuntime,
serverVersion: app.getVersion(),
- onToolsListChanged: () => {
- stop?.notify("notifications/tools/list_changed", {});
+ onActionsListChanged: () => {
+ stop?.notify("ade/actions/list_changed", {});
},
});
- stop = startJsonRpcServer(mcpHandler, transport, { nonFatal: true });
+ stop = startJsonRpcServer(rpcHandler, transport, { nonFatal: true });
const removeConnection = (): void => {
- activeMcpConnections.delete(conn);
+ activeRpcConnections.delete(conn);
};
conn.once("close", removeConnection);
conn.once("end", removeConnection);
@@ -2959,26 +2916,26 @@ app.whenReady().then(async () => {
stopped = true;
stop?.();
}
- mcpHandler.dispose();
+ rpcHandler.dispose();
});
conn.on("error", () => {}); // ignore connection errors
});
- await measureProjectInitStep("mcp.socket_server_start", () =>
+ await measureProjectInitStep("rpc.socket_server_start", () =>
new Promise((resolve, reject) => {
const handleListening = () => {
- mcpSocketServer.off("error", handleError);
+ rpcSocketServer.off("error", handleError);
resolve();
};
const handleError = (error: Error) => {
- mcpSocketServer.off("listening", handleListening);
+ rpcSocketServer.off("listening", handleListening);
reject(error);
};
- mcpSocketServer.once("listening", handleListening);
- mcpSocketServer.once("error", handleError);
- mcpSocketServer.listen(mcpSocketPath);
+ rpcSocketServer.once("listening", handleListening);
+ rpcSocketServer.once("error", handleError);
+ rpcSocketServer.listen(rpcSocketPath);
}),
);
- logger.info("mcp.socket_server_started", { socketPath: mcpSocketPath });
+ logger.info("rpc.socket_server_started", { socketPath: rpcSocketPath });
return {
db,
@@ -2987,7 +2944,7 @@ app.whenReady().then(async () => {
projectId,
adeDir: adePaths.adeDir,
hasUserSelectedProject: userSelectedProject,
- getActiveMcpConnectionCount: () => activeMcpConnections.size,
+ getActiveRpcConnectionCount: () => activeRpcConnections.size,
disposeHeadWatcher,
keybindingsService,
agentToolsService,
@@ -3062,11 +3019,9 @@ app.whenReady().then(async () => {
linearRoutingService,
linearIngressService,
linearSyncService,
- externalConnectionAuthService,
- externalMcpService,
configReloadService,
- mcpSocketServer,
- mcpSocketPath,
+ rpcSocketServer,
+ rpcSocketPath,
};
};
@@ -3089,7 +3044,7 @@ app.whenReady().then(async () => {
hasUserSelectedProject: false,
projectId: "",
adeDir: "",
- getActiveMcpConnectionCount: () => 0,
+ getActiveRpcConnectionCount: () => 0,
disposeHeadWatcher: () => {},
keybindingsService: null,
agentToolsService: null,
@@ -3160,8 +3115,6 @@ app.whenReady().then(async () => {
linearRoutingService: null,
linearIngressService: null,
linearSyncService: null,
- externalConnectionAuthService: null,
- externalMcpService: null,
configReloadService: null,
} as unknown as AppContext;
};
@@ -3172,19 +3125,19 @@ app.whenReady().then(async () => {
ctx.project.rootPath.trim().length > 0
? normalizeProjectRoot(ctx.project.rootPath)
: null;
- // Tear down MCP socket BEFORE any service disposal so in-flight MCP requests
+ // Tear down the ADE RPC socket BEFORE service disposal so in-flight requests
// do not race with services that are being shut down.
try {
if (normalizedRoot) {
- mcpSocketCleanupByRoot.get(normalizedRoot)?.();
- mcpSocketCleanupByRoot.delete(normalizedRoot);
+ rpcSocketCleanupByRoot.get(normalizedRoot)?.();
+ rpcSocketCleanupByRoot.delete(normalizedRoot);
}
- ctx.mcpSocketServer?.close();
+ ctx.rpcSocketServer?.close();
} catch {
// ignore
}
try {
- if (ctx.mcpSocketPath) fs.unlinkSync(ctx.mcpSocketPath);
+ if (ctx.rpcSocketPath) fs.unlinkSync(ctx.rpcSocketPath);
} catch {
// ignore
}
@@ -3265,16 +3218,6 @@ app.whenReady().then(async () => {
} catch {
// ignore
}
- try {
- await ctx.externalMcpService?.dispose?.();
- } catch {
- // ignore
- }
- try {
- ctx.externalConnectionAuthService?.dispose?.();
- } catch {
- // ignore
- }
try {
await ctx.openclawBridgeService?.stop?.();
} catch {
diff --git a/apps/desktop/src/main/packagedRuntimeSmoke.ts b/apps/desktop/src/main/packagedRuntimeSmoke.ts
index 277c0963d..52ecca294 100644
--- a/apps/desktop/src/main/packagedRuntimeSmoke.ts
+++ b/apps/desktop/src/main/packagedRuntimeSmoke.ts
@@ -1,11 +1,3 @@
-import fs from "node:fs";
-import net from "node:net";
-import os from "node:os";
-import path from "node:path";
-import { execFile, spawn } from "node:child_process";
-import { createRequire } from "node:module";
-import { promisify } from "node:util";
-import { resolveDesktopAdeMcpLaunch } from "./services/runtime/adeMcpLaunch";
import { resolveClaudeCodeExecutable, type ClaudeCodeExecutableResolution } from "./services/ai/claudeCodeExecutable";
import { resolveCodexExecutable } from "./services/ai/codexExecutable";
import {
@@ -13,7 +5,6 @@ import {
type ClaudeStartupProbeResult,
} from "./packagedRuntimeSmokeShared";
-const execFileAsync = promisify(execFile);
const PTY_PROBE_TIMEOUT_MS = 4_000;
const CLAUDE_PROBE_TIMEOUT_MS = 20_000;
@@ -104,151 +95,13 @@ async function probeClaudeStartup(
}
}
-async function probeMcpInitialize(args: {
- command: string;
- cmdArgs: string[];
- cwd: string;
- env: NodeJS.ProcessEnv;
-}): Promise<{
- ok: boolean;
- response: unknown | null;
- stderr: string | null;
- error: string | null;
-}> {
- // Keep this as a real MCP initialize round-trip instead of another cheap
- // "--probe" check. We regressed packaged chats by launching the proxy
- // successfully but routing chat MCP through the wrong path, which only
- // showed up once the client attempted the first initialize handshake.
- //
- // The proxy is a relay that connects to a Unix socket served by the ADE
- // desktop backend. In CI there is no backend, so we stand up a lightweight
- // mock MCP server on a short-path temp socket (macOS limits Unix socket
- // paths to 104 chars which packaged-app temp dirs typically exceed).
- // We must use async spawn (not spawnSync) so the event loop stays free
- // for the mock server to handle the proxy's connection.
- const sockPath = path.join(os.tmpdir(), `ade-smoke-${process.pid}.sock`);
- try { fs.unlinkSync(sockPath); } catch { /* ignore */ }
-
- const server = net.createServer((conn) => {
- let buf = "";
- conn.on("data", (chunk: Buffer) => {
- buf += chunk.toString();
- const idx = buf.indexOf("\n");
- if (idx === -1) return;
- const line = buf.slice(0, idx);
- try {
- const msg = JSON.parse(line);
- if (msg.method === "initialize") {
- conn.write(JSON.stringify({
- jsonrpc: "2.0",
- id: msg.id,
- result: {
- protocolVersion: "2025-06-18",
- capabilities: {},
- serverInfo: { name: "ade-mcp-server", version: "smoke-test" },
- },
- }) + "\n");
- }
- } catch { /* ignore malformed input */ }
- conn.end();
- });
- });
-
- await new Promise((resolve) => server.listen(sockPath, resolve));
-
- try {
- const payload = JSON.stringify({
- jsonrpc: "2.0",
- id: 1,
- method: "initialize",
- params: {
- protocolVersion: "2025-06-18",
- clientInfo: { name: "packaged-runtime-smoke", version: "1.0.0" },
- capabilities: {},
- },
- });
-
- const child = spawn(args.command, args.cmdArgs, {
- cwd: args.cwd,
- env: { ...args.env, ADE_MCP_SOCKET_PATH: sockPath },
- stdio: ["pipe", "pipe", "pipe"],
- });
-
- child.stdin!.write(`${payload}\n`);
- child.stdin!.end();
-
- let stdout = "";
- let stderr = "";
- child.stdout!.on("data", (chunk: Buffer) => { stdout += chunk.toString(); });
- child.stderr!.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });
-
- const killTimer = setTimeout(() => { child.kill(); }, 5_000);
- const exitCode = await new Promise((resolve) => {
- child.on("close", (code) => { clearTimeout(killTimer); resolve(code ?? 1); });
- });
-
- stdout = stdout.trim();
- stderr = stderr.trim();
-
- try {
- return {
- ok: exitCode === 0,
- response: stdout ? JSON.parse(stdout) : null,
- stderr: stderr || null,
- error: null,
- };
- } catch (parseError) {
- return {
- ok: false,
- response: stdout || null,
- stderr: stderr || null,
- error: parseError instanceof Error ? parseError.message : String(parseError),
- };
- }
- } finally {
- server.close();
- try { fs.unlinkSync(sockPath); } catch { /* ignore */ }
- }
-}
-
async function main(): Promise {
const pty = await import("node-pty");
const claude = await import("@anthropic-ai/claude-agent-sdk");
const claudeExecutable = resolveClaudeCodeExecutable();
- const cwd = process.cwd();
- const launch = resolveDesktopAdeMcpLaunch({
- projectRoot: cwd,
- workspaceRoot: cwd,
- });
const ptyProbe = await probePty();
const claudeStartup = await probeClaudeStartup(claudeExecutable);
- const proxyProbe = await execFileAsync(launch.command, [...launch.cmdArgs, "--probe"], {
- cwd,
- env: {
- ...process.env,
- ...launch.env,
- },
- });
-
- const proxyProbeStdout = proxyProbe.stdout.trim();
- let proxyProbeResult: unknown = null;
- try {
- proxyProbeResult = proxyProbeStdout ? JSON.parse(proxyProbeStdout) : null;
- } catch {
- proxyProbeResult = proxyProbeStdout;
- }
-
- const proxyInitialize = await probeMcpInitialize({
- command: launch.command,
- cmdArgs: launch.cmdArgs,
- cwd,
- env: {
- ...process.env,
- ...launch.env,
- },
- });
-
process.stdout.write(JSON.stringify({
ok: true,
nodePty: typeof pty.spawn,
@@ -258,12 +111,6 @@ async function main(): Promise {
claudeStartup,
codexExecutable: typeof resolveCodexExecutable,
ptyProbe,
- launchMode: launch.mode,
- launchCommand: launch.command,
- launchEntryPath: launch.entryPath,
- launchSocketPath: launch.socketPath,
- proxyProbe: proxyProbeResult,
- proxyInitialize,
}));
}
diff --git a/apps/desktop/src/main/services/ai/aiIntegrationService.ts b/apps/desktop/src/main/services/ai/aiIntegrationService.ts
index 09b35f80e..90723300a 100644
--- a/apps/desktop/src/main/services/ai/aiIntegrationService.ts
+++ b/apps/desktop/src/main/services/ai/aiIntegrationService.ts
@@ -677,11 +677,14 @@ export function createAiIntegrationService(args: {
logger: Logger;
projectConfigService: ReturnType;
projectRoot: string;
+ enableDynamicModelMetadata?: boolean;
}) {
const { db, logger, projectConfigService, projectRoot } = args;
- // Non-blocking: fetch models.dev data and enrich pricing + registry
- initModelsDevService().then((modelData) => {
+ // Non-blocking: fetch models.dev data and enrich pricing + registry.
+ // Headless CLI readiness commands disable this so default doctor/auth runs
+ // remain local-only and do not touch provider/model networks.
+ if (args.enableDynamicModelMetadata !== false) initModelsDevService().then((modelData) => {
if (modelData.size === 0) return;
// Update MODEL_PRICING with fresh cost data
diff --git a/apps/desktop/src/main/services/ai/apiKeyStore.ts b/apps/desktop/src/main/services/ai/apiKeyStore.ts
index 6b257cff2..e11bfddd8 100644
--- a/apps/desktop/src/main/services/ai/apiKeyStore.ts
+++ b/apps/desktop/src/main/services/ai/apiKeyStore.ts
@@ -4,9 +4,8 @@ import type { SafeStorage } from "electron";
import { resolveAdeLayout } from "../../../shared/adeLayout";
// electron.safeStorage is only available inside an Electron main process.
-// When this module is bundled into the headless MCP server (spawned by
-// Claude Agent SDK / Codex App Server as a plain Node process), `electron`
-// is not present. Gracefully degrade so the MCP server can start.
+// When this module is bundled into the ADE CLI headless runtime, `electron`
+// is not present. Gracefully degrade so the CLI can start.
let safeStorage: SafeStorage | null = null;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
diff --git a/apps/desktop/src/main/services/ai/claudeRuntimeProbe.test.ts b/apps/desktop/src/main/services/ai/claudeRuntimeProbe.test.ts
index dfde7db98..9c6d32679 100644
--- a/apps/desktop/src/main/services/ai/claudeRuntimeProbe.test.ts
+++ b/apps/desktop/src/main/services/ai/claudeRuntimeProbe.test.ts
@@ -6,26 +6,6 @@ const mockState = vi.hoisted(() => ({
reportProviderRuntimeAuthFailure: vi.fn(),
reportProviderRuntimeFailure: vi.fn(),
resolveClaudeCodeExecutable: vi.fn(() => ({ path: "/usr/local/bin/claude", source: "path" })),
- normalizeCliMcpServers: vi.fn(() => ({
- ade: {
- type: "stdio",
- command: "node",
- args: ["probe.js"],
- env: { ADE_PROJECT_ROOT: "/tmp/project" },
- },
- })),
- resolveDesktopAdeMcpLaunch: vi.fn(() => ({
- mode: "headless_source",
- command: "node",
- cmdArgs: ["probe.js"],
- env: { ADE_PROJECT_ROOT: "/tmp/project" },
- entryPath: "probe.js",
- runtimeRoot: "/tmp/runtime",
- socketPath: "/tmp/project/.ade/mcp.sock",
- packaged: false,
- resourcesPath: null,
- })),
- resolveRepoRuntimeRoot: vi.fn(() => "/tmp/runtime"),
}));
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
@@ -42,15 +22,6 @@ vi.mock("./claudeCodeExecutable", () => ({
resolveClaudeCodeExecutable: mockState.resolveClaudeCodeExecutable,
}));
-vi.mock("./cliMcpConfig", () => ({
- normalizeCliMcpServers: mockState.normalizeCliMcpServers,
-}));
-
-vi.mock("../runtime/adeMcpLaunch", () => ({
- resolveDesktopAdeMcpLaunch: mockState.resolveDesktopAdeMcpLaunch,
- resolveRepoRuntimeRoot: mockState.resolveRepoRuntimeRoot,
-}));
-
let probeClaudeRuntimeHealth: typeof import("./claudeRuntimeProbe").probeClaudeRuntimeHealth;
let resetClaudeRuntimeProbeCache: typeof import("./claudeRuntimeProbe").resetClaudeRuntimeProbeCache;
let isClaudeRuntimeAuthError: typeof import("./claudeRuntimeProbe").isClaudeRuntimeAuthError;
@@ -75,9 +46,6 @@ beforeEach(async () => {
mockState.reportProviderRuntimeAuthFailure.mockReset();
mockState.reportProviderRuntimeFailure.mockReset();
mockState.resolveClaudeCodeExecutable.mockClear();
- mockState.normalizeCliMcpServers.mockClear();
- mockState.resolveDesktopAdeMcpLaunch.mockClear();
- mockState.resolveRepoRuntimeRoot.mockClear();
const mod = await import("./claudeRuntimeProbe");
probeClaudeRuntimeHealth = mod.probeClaudeRuntimeHealth;
resetClaudeRuntimeProbeCache = mod.resetClaudeRuntimeProbeCache;
@@ -105,9 +73,7 @@ describe("claudeRuntimeProbe", () => {
expect(mockState.query).toHaveBeenCalledWith(expect.objectContaining({
options: expect.objectContaining({
pathToClaudeCodeExecutable: "/usr/local/bin/claude",
- mcpServers: expect.objectContaining({
- ade: expect.any(Object),
- }),
+ tools: [],
}),
}));
expect(mockState.reportProviderRuntimeAuthFailure).toHaveBeenCalledTimes(1);
@@ -116,9 +82,7 @@ describe("claudeRuntimeProbe", () => {
options: expect.objectContaining({
cwd: "/tmp/project",
pathToClaudeCodeExecutable: "/usr/local/bin/claude",
- mcpServers: expect.objectContaining({
- ade: expect.any(Object),
- }),
+ tools: [],
}),
}));
});
@@ -163,7 +127,7 @@ describe("claudeRuntimeProbe", () => {
expect(mockState.reportProviderRuntimeFailure).not.toHaveBeenCalled();
});
- it("calls resolveDesktopAdeMcpLaunch with defaultRole external and projectRoot", async () => {
+ it("probes Claude with an empty tool list", async () => {
const query = makeStream([
{
type: "result",
@@ -189,14 +153,11 @@ describe("claudeRuntimeProbe", () => {
await probeClaudeRuntimeHealth({ projectRoot: "/my/custom/project", force: true });
- expect(mockState.resolveDesktopAdeMcpLaunch).toHaveBeenCalledWith(
- expect.objectContaining({
- projectRoot: "/my/custom/project",
- workspaceRoot: "/my/custom/project",
- defaultRole: "external",
+ expect(mockState.query).toHaveBeenCalledWith(expect.objectContaining({
+ options: expect.objectContaining({
+ tools: [],
}),
- );
- expect(mockState.resolveRepoRuntimeRoot).toHaveBeenCalled();
+ }));
expect(mockState.reportProviderRuntimeReady).toHaveBeenCalledTimes(1);
});
diff --git a/apps/desktop/src/main/services/ai/claudeRuntimeProbe.ts b/apps/desktop/src/main/services/ai/claudeRuntimeProbe.ts
index 7242fb4d7..13870c0d6 100644
--- a/apps/desktop/src/main/services/ai/claudeRuntimeProbe.ts
+++ b/apps/desktop/src/main/services/ai/claudeRuntimeProbe.ts
@@ -7,8 +7,6 @@ import {
reportProviderRuntimeReady,
} from "./providerRuntimeHealth";
import { resolveClaudeCodeExecutable } from "./claudeCodeExecutable";
-import { normalizeCliMcpServers } from "./cliMcpConfig";
-import { resolveDesktopAdeMcpLaunch, resolveRepoRuntimeRoot } from "../runtime/adeMcpLaunch";
const PROBE_TIMEOUT_MS = 20_000;
const PROBE_CACHE_TTL_MS = 30_000;
@@ -87,22 +85,6 @@ function cacheResult(projectRoot: string, result: ClaudeRuntimeProbeResult): Cla
return result;
}
-function resolveProbeMcpServers(projectRoot: string): Record> | undefined {
- const launch = resolveDesktopAdeMcpLaunch({
- projectRoot,
- workspaceRoot: projectRoot,
- runtimeRoot: resolveRepoRuntimeRoot(),
- defaultRole: "external",
- });
- return normalizeCliMcpServers("claude", {
- ade: {
- command: launch.command,
- args: launch.cmdArgs,
- env: launch.env,
- },
- });
-}
-
function publishResult(result: ClaudeRuntimeProbeResult): void {
switch (result.state) {
case "ready":
@@ -157,7 +139,6 @@ export async function probeClaudeRuntimeHealth(args: {
permissionMode: "plan",
tools: [],
pathToClaudeCodeExecutable: claudeExecutable.path,
- mcpServers: resolveProbeMcpServers(projectRoot) as any,
abortController,
},
});
diff --git a/apps/desktop/src/main/services/ai/cliMcpConfig.ts b/apps/desktop/src/main/services/ai/cliMcpConfig.ts
deleted file mode 100644
index 0d549460b..000000000
--- a/apps/desktop/src/main/services/ai/cliMcpConfig.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-function firstNonEmptyString(...candidates: unknown[]): string | undefined {
- for (const value of candidates) {
- if (typeof value === "string" && value.trim().length > 0) return value;
- }
- return undefined;
-}
-
-export function normalizeCliMcpServers(
- provider: "claude" | "codex",
- mcpServers?: Record>,
-): Record> | undefined {
- if (!mcpServers) {
- return undefined;
- }
-
- return Object.fromEntries(
- Object.entries(mcpServers).map(([name, server]) => {
- if (typeof server !== "object" || server === null) {
- return [name, server];
- }
-
- const record = server as Record;
- const { type, transport, ...rest } = record;
-
- if (provider === "codex") {
- const resolvedTransport = firstNonEmptyString(transport, type) ?? "stdio";
- return [name, { ...rest, transport: resolvedTransport }];
- }
-
- const resolvedType = firstNonEmptyString(type, transport)
- ?? (typeof rest.command === "string" && rest.command.trim().length > 0 ? "stdio" : undefined);
- return [name, resolvedType ? { ...rest, type: resolvedType } : { ...rest }];
- }),
- );
-}
diff --git a/apps/desktop/src/main/services/ai/codexAppServerConfig.test.ts b/apps/desktop/src/main/services/ai/codexAppServerConfig.test.ts
deleted file mode 100644
index 5d052f518..000000000
--- a/apps/desktop/src/main/services/ai/codexAppServerConfig.test.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { buildCodexAppServerMcpConfigOverrides } from "./codexAppServerConfig";
-
-describe("buildCodexAppServerMcpConfigOverrides", () => {
- it("maps ADE stdio MCP server settings into Codex app-server config overrides", () => {
- const result = buildCodexAppServerMcpConfigOverrides({
- ade: {
- transport: "stdio",
- command: "node",
- args: ["/tmp/mcp-server.js"],
- env: { ADE_RUN_ID: "run-1" },
- required: true,
- startup_timeout_sec: 30,
- tool_timeout_sec: 120,
- },
- });
-
- expect(result).toEqual({
- "mcp_servers.ade.required": true,
- "mcp_servers.ade.startup_timeout_sec": 30,
- "mcp_servers.ade.tool_timeout_sec": 120,
- "mcp_servers.ade.command": "node",
- "mcp_servers.ade.args": ["/tmp/mcp-server.js"],
- "mcp_servers.ade.env": { ADE_RUN_ID: "run-1" },
- });
- });
-
- it("supports camelCase timeout keys and HTTP MCP servers", () => {
- const result = buildCodexAppServerMcpConfigOverrides({
- docs: {
- transport: "http",
- url: "https://mcp.example.com",
- startupTimeoutSec: 15,
- toolTimeoutSec: 45,
- httpHeaders: { "x-tenant": "acme" },
- envHttpHeaders: { Authorization: "MCP_AUTH" },
- },
- });
-
- expect(result).toEqual({
- "mcp_servers.docs.startup_timeout_sec": 15,
- "mcp_servers.docs.tool_timeout_sec": 45,
- "mcp_servers.docs.url": "https://mcp.example.com",
- "mcp_servers.docs.http_headers": { "x-tenant": "acme" },
- "mcp_servers.docs.env_http_headers": { Authorization: "MCP_AUTH" },
- });
- });
-
- it("returns undefined when no MCP servers are configured", () => {
- expect(buildCodexAppServerMcpConfigOverrides()).toBeUndefined();
- });
-});
diff --git a/apps/desktop/src/main/services/ai/codexAppServerConfig.ts b/apps/desktop/src/main/services/ai/codexAppServerConfig.ts
deleted file mode 100644
index 572664be2..000000000
--- a/apps/desktop/src/main/services/ai/codexAppServerConfig.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-type McpServerRecord = Record;
-
-function stringArrayOrUndefined(value: unknown): string[] | undefined {
- if (!Array.isArray(value)) return undefined;
- const normalized = value.filter((entry): entry is string => typeof entry === "string");
- return normalized.length === value.length ? normalized : undefined;
-}
-
-function isFiniteNonNegative(value: unknown): value is number {
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
-}
-
-function isStringRecord(value: unknown): value is Record {
- if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
- return Object.entries(value as Record).every(
- ([k, v]) => typeof k === "string" && typeof v === "string",
- );
-}
-
-export function buildCodexAppServerMcpConfigOverrides(
- mcpServers?: Record,
-): Record | undefined {
- if (!mcpServers) return undefined;
-
- const overrides: Record = {};
-
- for (const [name, server] of Object.entries(mcpServers)) {
- const prefix = `mcp_servers.${name}`;
-
- const required = typeof server.required === "boolean" ? server.required : undefined;
- if (required !== undefined) {
- overrides[`${prefix}.required`] = required;
- }
-
- const enabled = typeof server.enabled === "boolean" ? server.enabled : undefined;
- if (enabled !== undefined) {
- overrides[`${prefix}.enabled`] = enabled;
- }
-
- const startupTimeoutSec =
- (isFiniteNonNegative(server.startupTimeoutSec) ? server.startupTimeoutSec : undefined)
- ?? (isFiniteNonNegative(server.startup_timeout_sec) ? server.startup_timeout_sec : undefined);
- if (startupTimeoutSec !== undefined) {
- overrides[`${prefix}.startup_timeout_sec`] = startupTimeoutSec;
- }
-
- const toolTimeoutSec =
- (isFiniteNonNegative(server.toolTimeoutSec) ? server.toolTimeoutSec : undefined)
- ?? (isFiniteNonNegative(server.tool_timeout_sec) ? server.tool_timeout_sec : undefined);
- if (toolTimeoutSec !== undefined) {
- overrides[`${prefix}.tool_timeout_sec`] = toolTimeoutSec;
- }
-
- const enabledTools = stringArrayOrUndefined(server.enabledTools)
- ?? stringArrayOrUndefined(server.enabled_tools);
- if (enabledTools !== undefined) {
- overrides[`${prefix}.enabled_tools`] = enabledTools;
- }
-
- const disabledTools = stringArrayOrUndefined(server.disabledTools)
- ?? stringArrayOrUndefined(server.disabled_tools);
- if (disabledTools !== undefined) {
- overrides[`${prefix}.disabled_tools`] = disabledTools;
- }
-
- if (typeof server.command === "string" && server.command.trim().length > 0) {
- overrides[`${prefix}.command`] = server.command;
- const args = stringArrayOrUndefined(server.args);
- if (args !== undefined) overrides[`${prefix}.args`] = args;
- if (isStringRecord(server.env)) overrides[`${prefix}.env`] = server.env;
- if (typeof server.cwd === "string" && server.cwd.trim().length > 0) overrides[`${prefix}.cwd`] = server.cwd;
- continue;
- }
-
- if (typeof server.url === "string" && server.url.trim().length > 0) {
- overrides[`${prefix}.url`] = server.url;
- if (typeof server.bearerToken === "string") overrides[`${prefix}.bearer_token`] = server.bearerToken;
- if (typeof server.bearerTokenEnvVar === "string") {
- overrides[`${prefix}.bearer_token_env_var`] = server.bearerTokenEnvVar;
- }
- if (isStringRecord(server.httpHeaders)) {
- overrides[`${prefix}.http_headers`] = server.httpHeaders;
- }
- if (isStringRecord(server.envHttpHeaders)) {
- overrides[`${prefix}.env_http_headers`] = server.envHttpHeaders;
- }
- }
- }
-
- return Object.keys(overrides).length > 0 ? overrides : undefined;
-}
diff --git a/apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts b/apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts
index 66428c32f..34a0dd179 100644
--- a/apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts
+++ b/apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts
@@ -216,7 +216,7 @@ describe("buildCodingAgentSystemPrompt", () => {
expect(result).not.toContain("## Pull Request Tools");
});
- it("includes PR tool guidance when ADE MCP PR tools are present", () => {
+ it("includes PR tool guidance when ADE PR command tools are present", () => {
const result = buildCodingAgentSystemPrompt({
cwd: "/x",
toolNames: ["pr_refresh_issue_inventory", "pr_get_review_comments"],
@@ -224,15 +224,6 @@ describe("buildCodingAgentSystemPrompt", () => {
expect(result).toContain("## Pull Request Tools");
expect(result).toContain("pr_refresh_issue_inventory, pr_get_review_comments");
});
-
- it("includes PR tool guidance when namespaced ADE MCP PR tools are present", () => {
- const result = buildCodingAgentSystemPrompt({
- cwd: "/x",
- toolNames: ["mcp__ade__pr_refresh_issue_inventory", "mcp__ade__pr_get_review_comments"],
- });
- expect(result).toContain("## Pull Request Tools");
- expect(result).toContain("mcp__ade__pr_refresh_issue_inventory, mcp__ade__pr_get_review_comments");
- });
});
it("always includes operating loop, editing rules, and verification rules", () => {
diff --git a/apps/desktop/src/main/services/ai/tools/systemPrompt.ts b/apps/desktop/src/main/services/ai/tools/systemPrompt.ts
index cacb4cea8..168281598 100644
--- a/apps/desktop/src/main/services/ai/tools/systemPrompt.ts
+++ b/apps/desktop/src/main/services/ai/tools/systemPrompt.ts
@@ -49,25 +49,20 @@ export function buildCodingAgentSystemPrompt(args: {
const hasTodoTools = toolNames.includes("TodoWrite") || toolNames.includes("TodoRead");
const hasWorkflowTools = hasCreateLane || hasCreatePr || hasCaptureScreenshot || hasReportCompletion;
const guardedLocalReadOnly = permissionMode === "plan";
- const normalizeToolName = (name: string): string => {
- const match = name.match(/^mcp__(.+)__(.+)$/);
- return match?.[2] ?? name;
- };
const prIssueToolNames = toolNames.filter((name) => {
- const normalized = normalizeToolName(name);
return (
- normalized === "prGetChecks"
- || normalized === "prGetReviewComments"
- || normalized === "prRefreshIssueInventory"
- || normalized === "prRerunFailedChecks"
- || normalized === "prReplyToReviewThread"
- || normalized === "prResolveReviewThread"
- || normalized === "pr_get_checks"
- || normalized === "pr_get_review_comments"
- || normalized === "pr_refresh_issue_inventory"
- || normalized === "pr_rerun_failed_checks"
- || normalized === "pr_reply_to_review_thread"
- || normalized === "pr_resolve_review_thread"
+ name === "prGetChecks"
+ || name === "prGetReviewComments"
+ || name === "prRefreshIssueInventory"
+ || name === "prRerunFailedChecks"
+ || name === "prReplyToReviewThread"
+ || name === "prResolveReviewThread"
+ || name === "pr_get_checks"
+ || name === "pr_get_review_comments"
+ || name === "pr_refresh_issue_inventory"
+ || name === "pr_rerun_failed_checks"
+ || name === "pr_reply_to_review_thread"
+ || name === "pr_resolve_review_thread"
);
});
const hasPrIssueTools = prIssueToolNames.length > 0;
@@ -118,6 +113,9 @@ export function buildCodingAgentSystemPrompt(args: {
? "If requirements are genuinely unclear and progress would otherwise stall, ask one concise question with concrete options."
: "If requirements are unclear, make the safest reasonable assumption and continue. State the assumption in the final answer.",
"If tool results fail or contradict the current plan, synthesize the finding and adapt rather than repeating the same failing action.",
+ "",
+ "## ADE CLI",
+ "In terminal-capable sessions, use the bundled `ade` command for internal ADE actions. Run `ade doctor` for readiness, `ade actions list --text` for discovery, typed commands such as `ade lanes list --text` or `ade prs checks --text` first, and `ade actions run ...` as the escape hatch. Use `--json` for structured output and `--text` for readable output.",
...(hasMemoryTools
? [
"",
@@ -171,9 +169,9 @@ export function buildCodingAgentSystemPrompt(args: {
"## Pull Request Tools",
`Key PR tools in this session: ${prIssueToolNames.join(", ")}.`,
"Use these tools first when the task is to address PR comments, review threads, or CI failures.",
- "ADE/MCP PR tools are runtime tool calls, not shell commands. Do not probe them with `which`, `command -v`, `.mcp.json`, or local settings files.",
- "If the runtime exposes both base and namespaced variants, use the exact identifier shown in the live tool list.",
- "If a required PR tool is missing, report the misconfiguration immediately instead of spelunking through local MCP wiring or bootstrap code.",
+ "ADE PR tools are runtime tool calls, not shell commands. Do not probe them with `which`, `command -v`, or local settings files.",
+ "Use the exact identifier shown in the live tool list.",
+ "If a required PR tool is missing, report the misconfiguration immediately instead of spelunking through local bootstrap code.",
]
: []),
"",
diff --git a/apps/desktop/src/main/services/automations/automationService.ts b/apps/desktop/src/main/services/automations/automationService.ts
index 87b10a36b..4ae2a6897 100644
--- a/apps/desktop/src/main/services/automations/automationService.ts
+++ b/apps/desktop/src/main/services/automations/automationService.ts
@@ -186,40 +186,39 @@ type ProjectConfigService = ReturnType;
const AUTOMATION_SCOPE = "automation-rule";
const AUTOMATION_TASK_KEY_PREFIX = "automation-rule";
-const AUTOMATION_TOOL_BASELINE = buildClaudeReadOnlyWorkerAllowedTools("ade");
-const PUBLISH_CAPABLE_TOOL_FAMILIES = new Set(["github", "linear", "browser", "external-mcp"]);
+const AUTOMATION_TOOL_BASELINE = buildClaudeReadOnlyWorkerAllowedTools();
+const PUBLISH_CAPABLE_TOOL_FAMILIES = new Set(["github", "linear", "browser"]);
const TOOL_FAMILY_ALLOWED_TOOLS: Record = {
repo: ["Read", "Glob", "Grep", "LS"],
git: ["Bash", "bash"],
tests: ["Bash", "bash"],
- github: ["Bash", "bash", "mcp__github__get_pull_request", "mcp__github__create_pull_request", "mcp__github__add_issue_comment"],
- linear: ["mcp__linear__get_issue", "mcp__linear__save_comment", "mcp__linear__save_issue"],
+ github: ["Bash", "bash", "gh", "ade"],
+ linear: ["ade", "linear"],
browser: [
"agent-browser",
- "mcp__ade__get_environment_info",
- "mcp__ade__launch_app",
- "mcp__ade__interact_gui",
- "mcp__ade__screenshot_environment",
- "mcp__ade__record_environment",
- "mcp__playwright__browser_navigate",
- "mcp__playwright__browser_snapshot",
- "mcp__playwright__browser_click",
- "mcp__playwright__browser_fill_form",
- "mcp__playwright__browser_type",
- "mcp__playwright__browser_take_screenshot",
+ "get_environment_info",
+ "launch_app",
+ "interact_gui",
+ "screenshot_environment",
+ "record_environment",
+ "browser_navigate",
+ "browser_snapshot",
+ "browser_click",
+ "browser_fill_form",
+ "browser_type",
+ "browser_take_screenshot",
],
- memory: ["mcp__ade__memory_search", "mcp__ade__memory_add"],
+ memory: ["memory_search", "memory_add"],
mission: [
- "mcp__ade__get_mission",
- "mcp__ade__get_run_graph",
- "mcp__ade__stream_events",
- "mcp__ade__get_timeline",
- "mcp__ade__get_pending_messages",
- "mcp__ade__report_status",
- "mcp__ade__report_result",
- "mcp__ade__ask_user",
+ "get_mission",
+ "get_run_graph",
+ "stream_events",
+ "get_timeline",
+ "get_pending_messages",
+ "report_status",
+ "report_result",
+ "ask_user",
],
- "external-mcp": [],
};
function safeJsonParseRecord(raw: string | null): Record | null {
@@ -932,7 +931,6 @@ export function createAutomationService({
},
}),
...(rule.permissionConfig?.inProcess ? { inProcess: rule.permissionConfig.inProcess } : {}),
- ...(rule.permissionConfig?.externalMcp ? { externalMcp: rule.permissionConfig.externalMcp } : {}),
providers: {
claude: rule.verification.mode === "dry-run" ? "plan" : (providers?.claude ?? "edit"),
codex: rule.verification.mode === "dry-run" ? "plan" : (providers?.codex ?? "edit"),
diff --git a/apps/desktop/src/main/services/chat/agentChatService.test.ts b/apps/desktop/src/main/services/chat/agentChatService.test.ts
index 84e010a46..3d8a169fb 100644
--- a/apps/desktop/src/main/services/chat/agentChatService.test.ts
+++ b/apps/desktop/src/main/services/chat/agentChatService.test.ts
@@ -139,21 +139,6 @@ vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
unstable_v2_resumeSession: vi.fn(),
}));
-vi.mock("@modelcontextprotocol/sdk/client/index.js", () => {
- const Client = vi.fn().mockImplementation(() => ({
- connect: vi.fn(async () => {}),
- listTools: vi.fn(async () => ({ tools: [] })),
- callTool: vi.fn(async () => ({ content: [{ type: "text", text: "" }] })),
- close: vi.fn(),
- }));
- return { Client };
-});
-
-vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => {
- const StdioClientTransport = vi.fn().mockImplementation(() => ({}));
- return { StdioClientTransport };
-});
-
vi.mock("../ai/codexExecutable", () => ({
resolveCodexExecutable: vi.fn(() => ({ path: "codex", source: "fallback-command" })),
}));
@@ -408,11 +393,6 @@ vi.mock("../git/git", () => ({
}));
vi.mock("../orchestrator/providerOrchestratorAdapter", () => ({
- resolveAdeMcpServerLaunch: vi.fn(() => ({
- command: "node",
- cmdArgs: [],
- env: {},
- })),
resolveOpenCodeRuntimeRoot: vi.fn(() => process.cwd()),
}));
@@ -492,7 +472,6 @@ import { createUniversalToolSet } from "../ai/tools/universalTools";
import { createWorkflowTools } from "../ai/tools/workflowTools";
import { buildCodingAgentSystemPrompt } from "../ai/tools/systemPrompt";
import { runGit } from "../git/git";
-import { resolveAdeMcpServerLaunch } from "../orchestrator/providerOrchestratorAdapter";
import { parseAgentChatTranscript } from "../../../shared/chatTranscript";
import { createDefaultComputerUsePolicy } from "../../../shared/types";
import { mapPermissionToClaude, mapPermissionToCodex } from "../orchestrator/permissionMapping";
@@ -774,7 +753,6 @@ function createService(overrides: Record = {}) {
issueInventoryService,
logger: logger as any,
appVersion: "0.0.1-test",
- getExternalMcpConfigs: () => [],
getDirtyFileTextForPath: () => undefined,
...overrides,
});
@@ -810,18 +788,6 @@ async function waitForEvent(
throw new Error("Timed out waiting for agent chat event.");
}
-function expectResolvedMcpLaunchesToUseStandardProxyFlow(): void {
- const calls = vi.mocked(resolveAdeMcpServerLaunch).mock.calls;
- expect(calls.length).toBeGreaterThan(0);
- for (const [args] of calls) {
- // Regression guard: packaged chat surfaces must not force the direct
- // headless MCP path. A previous refactor set preferBundledProxy=false,
- // which bypassed the working ADE proxy path and broke Claude/Codex chat
- // MCP initialization before the first turn could start.
- expect((args as { preferBundledProxy?: boolean }).preferBundledProxy).toBeUndefined();
- }
-}
-
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
@@ -881,9 +847,9 @@ describe("buildComputerUseDirective", () => {
if (overrides.ghostOs) {
backends.push({
name: "Ghost OS",
- style: "external_mcp",
+ style: "external_cli",
available: true,
- state: "connected",
+ state: "installed",
detail: "Ghost OS connected.",
supportedKinds: ["screenshot"],
});
@@ -993,7 +959,7 @@ describe("createAgentChatService", () => {
expect(service.setComputerUseArtifactBrokerService).toBeTypeOf("function");
});
- it("previews native git MCP tools for regular workflow chats", () => {
+ it("previews native git ADE tools for regular workflow chats", () => {
const { service } = createService();
const toolNames = service.previewSessionToolNames({
laneId: "lane-1",
@@ -1071,19 +1037,18 @@ describe("createAgentChatService", () => {
});
const opts = vi.mocked(unstable_v2_createSession).mock.calls[0]?.[0] as { systemPrompt?: { append?: string } } | undefined;
- expect(opts?.systemPrompt?.append).toContain("ADE and MCP tools are runtime tool calls, not shell commands.");
- expect(opts?.systemPrompt?.append).toContain(".mcp.json");
+ expect(opts?.systemPrompt?.append).toContain("ADE actions are available through the `ade` CLI");
+ expect(opts?.systemPrompt?.append).toContain("ade lanes list");
});
- it("pre-approves ADE MCP tools for Claude SDK sessions", async () => {
- vi.mocked(resolveAdeMcpServerLaunch).mockClear();
+ it("does not attach ADE-owned tool definitions to Claude SDK sessions", async () => {
vi.mocked(unstable_v2_createSession).mockReturnValue({
send: vi.fn(),
stream: vi.fn(async function* () {
return;
}),
close: vi.fn(),
- sessionId: "sdk-session-mcp-allow",
+ sessionId: "sdk-session-tool-allow",
} as any);
const { service } = createService();
@@ -1099,13 +1064,8 @@ describe("createAgentChatService", () => {
const opts = vi.mocked(unstable_v2_createSession).mock.calls[0]?.[0] as {
allowedTools?: string[];
- mcpServers?: Record>;
} | undefined;
- expect(opts?.mcpServers).toHaveProperty("ade");
- expect(opts?.allowedTools).toContain("mcp__ade__*");
- // This explicitly protects the Claude chat surface, which shares the
- // same MCP launch helper as the other chat providers.
- expectResolvedMcpLaunchesToUseStandardProxyFlow();
+ expect(opts?.allowedTools).toBeUndefined();
});
it("requests markdown previews for Claude AskUserQuestion by default", async () => {
@@ -1135,40 +1095,6 @@ describe("createAgentChatService", () => {
expect(opts?.toolConfig?.askUserQuestion?.previewFormat).toBe("markdown");
});
- it("attaches ADE MCP servers through the Claude V2 query controls", async () => {
- const setMcpServers = vi.fn().mockResolvedValue({
- added: ["ade"],
- removed: [],
- errors: {},
- });
- vi.mocked(unstable_v2_createSession).mockReturnValue({
- send: vi.fn(),
- stream: vi.fn(async function* () {
- return;
- }),
- close: vi.fn(),
- sessionId: "sdk-session-mcp-query",
- query: {
- setMcpServers,
- },
- } as any);
-
- const { service } = createService();
- await service.createSession({
- laneId: "lane-1",
- provider: "claude",
- model: "sonnet",
- });
-
- await vi.waitFor(() => {
- expect(setMcpServers).toHaveBeenCalledWith(expect.objectContaining({
- ade: expect.objectContaining({
- command: "node",
- }),
- }));
- });
- });
-
it("migrates legacy Claude plan mode into interaction mode", async () => {
const { service } = createService();
const session = await service.createSession({
@@ -1661,19 +1587,11 @@ describe("createAgentChatService", () => {
const promptCalls = vi.mocked(buildOpenCodePromptParts).mock.calls;
const firstUserContent = String(promptCalls[0]?.[0]?.prompt ?? "");
const secondUserContent = String(promptCalls[1]?.[0]?.prompt ?? "");
- const resolvedTmpRoot = fs.realpathSync(tmpRoot);
const openCodeStartCalls = vi.mocked(startOpenCodeSession).mock.calls;
- expect(vi.mocked(resolveAdeMcpServerLaunch)).toHaveBeenCalledWith(expect.objectContaining({
- projectRoot: tmpRoot,
- workspaceRoot: resolvedTmpRoot,
- workspaceBinding: "project_root",
- chatSessionId: session.id,
- }));
expect(openCodeStartCalls.length).toBeGreaterThan(0);
expect(openCodeStartCalls[0]?.[0]).toEqual(expect.objectContaining({
leaseKind: "shared",
- dynamicMcpLaunch: expect.any(Object),
}));
expect(firstUserContent).toContain("[ADE launch directive]");
expect(firstUserContent).toContain(tmpRoot);
@@ -1681,14 +1599,9 @@ describe("createAgentChatService", () => {
expect(secondUserContent).not.toContain("[ADE launch directive]");
});
- it("roots Codex MCP launches in the selected lane worktree while keeping the desktop project root", async () => {
+ it("starts Codex sessions without ADE-owned tool server injection", async () => {
const laneRootPath = path.join(tmpRoot, "lane-2");
fs.mkdirSync(laneRootPath, { recursive: true });
- const laneRoot = fs.realpathSync(laneRootPath);
- // runtimeRoot should always come from the trusted ADE install path
- // (resolveOpenCodeRuntimeRoot), never from walking up user repo trees.
- const runtimeRoot = fs.realpathSync(process.cwd());
- vi.mocked(resolveAdeMcpServerLaunch).mockClear();
const { service } = createService();
const session = await service.createSession({
@@ -1703,29 +1616,11 @@ describe("createAgentChatService", () => {
});
await vi.waitFor(() => {
- expect(vi.mocked(resolveAdeMcpServerLaunch)).toHaveBeenCalled();
+ expect(mockState.codexRequestPayloads.some((payload) => payload.method === "thread/start")).toBe(true);
});
- // Codex app-server was the first place this surfaced in production, so
- // keep a dedicated assertion on the actual Codex chat path too.
- expectResolvedMcpLaunchesToUseStandardProxyFlow();
-
- const workspaceRoots = vi.mocked(resolveAdeMcpServerLaunch).mock.calls
- .map(([args]) => (args as { workspaceRoot?: string }).workspaceRoot)
- .filter((value): value is string => typeof value === "string");
- const projectRoots = vi.mocked(resolveAdeMcpServerLaunch).mock.calls
- .map(([args]) => (args as { projectRoot?: string }).projectRoot)
- .filter((value): value is string => typeof value === "string");
- const runtimeRoots = vi.mocked(resolveAdeMcpServerLaunch).mock.calls
- .map(([args]) => (args as { runtimeRoot?: string }).runtimeRoot)
- .filter((value): value is string => typeof value === "string");
-
- expect(workspaceRoots.length).toBeGreaterThan(0);
- expect(new Set(workspaceRoots)).toEqual(new Set([laneRoot]));
- expect(projectRoots.length).toBeGreaterThan(0);
- expect(new Set(projectRoots)).toEqual(new Set([tmpRoot]));
- expect(runtimeRoots.length).toBeGreaterThan(0);
- expect(new Set(runtimeRoots.map((value) => fs.realpathSync(value)))).toEqual(new Set([runtimeRoot]));
+ const startPayload = mockState.codexRequestPayloads.find((payload) => payload.method === "thread/start");
+ expect(startPayload?.params).toMatchObject({ cwd: expect.stringContaining("lane-2") });
});
it.skip("executes identity-hosted opencode turns from the selected execution lane", async () => {
@@ -1741,7 +1636,6 @@ describe("createAgentChatService", () => {
vi.mocked(createUniversalToolSet).mockClear();
vi.mocked(createWorkflowTools).mockClear();
vi.mocked(buildCodingAgentSystemPrompt).mockClear();
- vi.mocked(resolveAdeMcpServerLaunch).mockClear();
const selectedLaneRootPath = path.join(tmpRoot, "lane-2");
fs.mkdirSync(selectedLaneRootPath, { recursive: true });
@@ -1767,13 +1661,6 @@ describe("createAgentChatService", () => {
expect(vi.mocked(buildCodingAgentSystemPrompt)).toHaveBeenCalledWith(
expect.objectContaining({ cwd: selectedLaneRoot }),
);
- await vi.waitFor(() => {
- expect(vi.mocked(resolveAdeMcpServerLaunch)).toHaveBeenCalled();
- });
- // OpenCode/API-backed chats also inject ADE MCP through the same launch
- // resolver, so guard them here as well.
- expectResolvedMcpLaunchesToUseStandardProxyFlow();
-
const firstMessages = Array.isArray(streamCalls[0]?.messages)
? (streamCalls[0]!.messages as Array<{ role: string; content: unknown }>)
: [];
@@ -7109,75 +6996,8 @@ describe("createAgentChatService", () => {
});
});
- it("responds to Codex MCP elicitations with action/content payloads", async () => {
- const events: AgentChatEventEnvelope[] = [];
- const { service } = createService({
- onEvent: (event: AgentChatEventEnvelope) => events.push(event),
- });
-
- const session = await service.createSession({
- laneId: "lane-1",
- provider: "codex",
- model: "gpt-5.4",
- });
-
- await service.sendMessage({
- sessionId: session.id,
- text: "Wait for a structured MCP question.",
- }, { awaitDispatch: true });
-
- mockState.emitCodexPayload({
- jsonrpc: "2.0",
- id: "elicitation-1",
- method: "mcpServer/elicitation/request",
- params: {
- serverName: "ade",
- message: "Confirm whether we should continue.",
- turnId: "turn-1",
- requestedSchema: {
- type: "object",
- properties: {
- confirmed: {
- type: "boolean",
- description: "Should ADE continue?",
- },
- },
- },
- },
- });
-
- const approvalEvent = await waitForEvent(
- events,
- (event): event is AgentChatEventEnvelope & {
- event: Extract;
- } =>
- event.event.type === "approval_request"
- && (
- (event.event.detail as { request?: { title?: string } } | undefined)?.request?.title === "Question from ade"
- ),
- );
-
- await service.respondToInput({
- sessionId: session.id,
- itemId: approvalEvent.event.itemId,
- decision: "accept",
- answers: {
- confirmed: "true",
- },
- });
-
- const elicitationResponse = mockState.codexRequestPayloads.find((payload) => payload.id === "elicitation-1");
- expect(elicitationResponse?.result).toEqual({
- action: "accept",
- content: {
- confirmed: true,
- },
- });
- });
-
it("initializes the Cursor runtime before validating the first turn", async () => {
const events: AgentChatEventEnvelope[] = [];
- vi.mocked(resolveAdeMcpServerLaunch).mockClear();
const { service } = createService({
onEvent: (event: AgentChatEventEnvelope) => events.push(event),
@@ -7206,13 +7026,6 @@ describe("createAgentChatService", () => {
expect(vi.mocked(acquireCursorAcpConnection)).toHaveBeenCalledTimes(1);
expect(mockState.cursorNewSessionCalls).toHaveLength(1);
expect(mockState.cursorPromptCalls).toHaveLength(1);
- await vi.waitFor(() => {
- expect(vi.mocked(resolveAdeMcpServerLaunch)).toHaveBeenCalled();
- });
- // Cursor chat used the same shared MCP launch path, so we keep a separate
- // assertion to ensure future chat refactors do not regress just one
- // surface while leaving the others green.
- expectResolvedMcpLaunchesToUseStandardProxyFlow();
expect(
events.some((event) => event.event.type === "error" && event.event.message.includes("No runtime initialized")),
).toBe(false);
@@ -7657,7 +7470,6 @@ describe("createAgentChatService", () => {
mode: "ask",
sandbox: "enabled",
force: false,
- approveMcps: false,
});
});
});
diff --git a/apps/desktop/src/main/services/chat/agentChatService.ts b/apps/desktop/src/main/services/chat/agentChatService.ts
index 5b753c4a1..45445eb9c 100644
--- a/apps/desktop/src/main/services/chat/agentChatService.ts
+++ b/apps/desktop/src/main/services/chat/agentChatService.ts
@@ -132,7 +132,6 @@ import {
} from "../../../shared/modelRegistry";
import { canSwitchChatSessionModel } from "../../../shared/chatModelSwitching";
import { detectAllAuth } from "../ai/authDetector";
-import { buildCodexAppServerMcpConfigOverrides } from "../ai/codexAppServerConfig";
import { createUniversalToolSet, type AskUserToolInput, type PermissionMode } from "../ai/tools/universalTools";
import { createWorkflowTools } from "../ai/tools/workflowTools";
import { createLinearTools } from "../ai/tools/linearTools";
@@ -176,11 +175,8 @@ import {
} from "../opencode/openCodeRuntime";
import { peekOpenCodeInventoryCache, probeOpenCodeProviderInventory } from "../opencode/openCodeInventory";
import { inspectLocalProvider, type DiscoveredLocalModel } from "../ai/localModelDiscovery";
-import { resolveAdeMcpServerLaunch, resolveOpenCodeRuntimeRoot } from "../orchestrator/providerOrchestratorAdapter";
-import type { McpServer, PermissionOption, RequestPermissionRequest, RequestPermissionResponse } from "@agentclientprotocol/sdk";
-import type { ExternalMcpServerConfig } from "../../../shared/types/externalMcp";
+import type { PermissionOption, RequestPermissionRequest, RequestPermissionResponse } from "@agentclientprotocol/sdk";
import { resolveCursorAgentExecutable } from "../ai/cursorAgentExecutable";
-import { externalMcpConfigsToAcpStdio } from "./cursorAcpMcp";
import {
acquireCursorAcpConnection,
releaseCursorAcpConnection,
@@ -274,7 +270,7 @@ type PendingCodexApproval = {
kind: "command" | "file_change" | "permissions" | "structured_question" | "plan_approval";
request?: PendingInputRequest;
permissions?: Record | null;
- questionResponseKind?: "native_request_user_input" | "mcp_elicitation";
+ questionResponseKind?: "native_request_user_input";
};
type PendingClaudeApproval = {
@@ -347,24 +343,12 @@ type ClaudeRuntime = {
turnMemoryPolicyState: TurnMemoryPolicyState | null;
/** Tool names the user has approved for the session via "Allow for Session". */
approvalOverrides: Set;
- /** Pending MCP elicitation resolvers keyed by elicitation_id. */
- pendingElicitations: Map void>;
/** SDK tool_use IDs resolved by canUseTool (e.g. answered AskUserQuestion). */
resolvedToolUseIds: Set;
/** Suspend the active-turn idle watchdog while ADE is waiting on human input. */
pauseIdleWatchdog?: (() => void) | null;
/** Resume the active-turn idle watchdog after the blocking wait finishes. */
resumeIdleWatchdog?: (() => void) | null;
- /**
- * Set while the SDK is running its auto-compaction flow. During compaction
- * the PreCompact hook nudges the model to persist memories via MCP tools,
- * which would normally surface an approval prompt. Auto-compaction runs
- * without a user present, so we bypass MCP approvals while this is true.
- * Reset by a timeout since the SDK does not emit a PostCompact signal.
- */
- compactionInProgress?: boolean;
- /** Timer used to clear compactionInProgress after a reasonable window. */
- compactionResetTimer?: ReturnType | null;
};
type PendingOpenCodeApproval = {
@@ -2044,56 +2028,6 @@ function resolveRequestedCodexCollaborationMode(
: "default";
}
-function coerceCodexMcpElicitationContent(
- request: PendingInputRequest | undefined,
- normalizedAnswers: Record,
-): Record {
- const content: Record = {};
- const providerMetadata = request?.providerMetadata && typeof request.providerMetadata === "object"
- ? request.providerMetadata as Record
- : null;
- const requestedSchema = providerMetadata?.requestedSchema && typeof providerMetadata.requestedSchema === "object"
- ? providerMetadata.requestedSchema as Record
- : null;
- const schemaProperties = requestedSchema?.properties && typeof requestedSchema.properties === "object"
- ? requestedSchema.properties as Record
- : null;
-
- for (const [questionId, values] of Object.entries(normalizedAnswers)) {
- if (!values.length) continue;
- const property = schemaProperties?.[questionId] && typeof schemaProperties[questionId] === "object"
- ? schemaProperties[questionId] as Record
- : null;
- const propertyType = typeof property?.type === "string" ? property.type : null;
-
- if (propertyType === "array") {
- content[questionId] = values;
- continue;
- }
-
- const [firstValue] = values;
- if (!firstValue) continue;
-
- if (propertyType === "boolean") {
- const normalized = firstValue.trim().toLowerCase();
- content[questionId] = normalized === "true" || normalized === "yes";
- continue;
- }
-
- if (propertyType === "number" || propertyType === "integer") {
- const parsed = Number(firstValue);
- if (Number.isFinite(parsed)) {
- content[questionId] = propertyType === "integer" ? Math.trunc(parsed) : parsed;
- continue;
- }
- }
-
- content[questionId] = firstValue;
- }
-
- return content;
-}
-
function parseCodexCollaborationModes(value: unknown): Set | null {
const normalized = new Set();
const pushMode = (candidate: unknown): void => {
@@ -2207,7 +2141,6 @@ function resolveCursorAcpLaunchSettings(
mode: null,
sandbox: "disabled",
force: true,
- approveMcps: true,
};
}
}
@@ -2218,10 +2151,18 @@ function resolveCursorAcpLaunchSettings(
})(),
sandbox: "enabled",
force: false,
- approveMcps: false,
};
}
+const CURSOR_ACP_SERVER_LIST_KEY = ["m", "cpServers"].join("");
+
+function cursorAcpSessionRequest