Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1823,7 +1823,12 @@ describe("adeRpcServer", () => {
cols: 120,
rows: 36,
tracked: true,
toolType: "claude-orchestrated"
toolType: "claude-orchestrated",
command: "claude",
args: expect.arrayContaining(["--model", "claude-sonnet-4-6", "--permission-mode", "default", "Implement API wiring"]),
env: expect.objectContaining({
ADE_DEFAULT_ROLE: "agent",
}),
})
);
expect(response.structuredContent.startupCommand).toContain("claude");
Expand Down Expand Up @@ -1853,6 +1858,16 @@ describe("adeRpcServer", () => {
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");
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(
expect.objectContaining({
command: "claude",
env: expect.objectContaining({
ADE_RUN_ID: "run-1",
ADE_ATTEMPT_ID: "attempt-workspace-roots",
ADE_DEFAULT_ROLE: "agent",
}),
})
);
});

it("rejects config-toml permission mode for Claude spawn_agent sessions", async () => {
Expand Down
52 changes: 36 additions & 16 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5886,46 +5886,63 @@ async function runTool(args: {
}
const finalPrompt = promptSegments.join("\n").trim();

const commandParts: string[] = [provider];
const commandArgs: string[] = [];
const commandPreviewParts: string[] = [provider];
if (model) {
commandParts.push("--model", shellEscapeArg(model));
commandArgs.push("--model", model);
commandPreviewParts.push("--model", shellEscapeArg(model));
}
if (provider === "codex") {
if (permissionMode === "full-auto") {
commandParts.push("--dangerously-bypass-approvals-and-sandbox");
commandArgs.push("--dangerously-bypass-approvals-and-sandbox");
commandPreviewParts.push("--dangerously-bypass-approvals-and-sandbox");
} else if (permissionMode === "default") {
commandParts.push("--full-auto");
commandArgs.push("--full-auto");
commandPreviewParts.push("--full-auto");
} else if (permissionMode === "config-toml") {
// No explicit Codex permission flags; let the host config.toml decide.
} else if (permissionMode === "plan") {
commandParts.push("--sandbox", "read-only", "--ask-for-approval", "on-request");
commandArgs.push("--sandbox", "read-only", "--ask-for-approval", "on-request");
commandPreviewParts.push("--sandbox", "read-only", "--ask-for-approval", "on-request");
} else {
commandParts.push("--sandbox", "workspace-write", "--ask-for-approval", "untrusted");
commandArgs.push("--sandbox", "workspace-write", "--ask-for-approval", "untrusted");
commandPreviewParts.push("--sandbox", "workspace-write", "--ask-for-approval", "untrusted");
}
} else {
const claudePermission =
permissionMode === "plan" ? "plan" : permissionMode === "full-auto" ? "bypassPermissions" : permissionMode === "edit" ? "acceptEdits" : "default";
commandParts.push("--permission-mode", claudePermission);
commandArgs.push("--permission-mode", claudePermission);
commandPreviewParts.push("--permission-mode", shellEscapeArg(claudePermission));

// 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));
commandArgs.push(finalPrompt);
commandPreviewParts.push(shellEscapeArg(finalPrompt));
}

// Prepend env vars for worker identity
// Attach worker identity through the process environment. The startup
// command remains a display/resume preview only; the actual launch uses
// command/args/env so it works on Windows without POSIX inline assignment.
const workerEnv: Record<string, string> = {};
const envPrefixParts: string[] = [];
if (runId) envPrefixParts.push(`ADE_RUN_ID=${shellEscapeArg(runId)}`);
if (stepId) envPrefixParts.push(`ADE_STEP_ID=${shellEscapeArg(stepId)}`);
if (attemptId) envPrefixParts.push(`ADE_ATTEMPT_ID=${shellEscapeArg(attemptId)}`);
if (callerCtx.missionId) envPrefixParts.push(`ADE_MISSION_ID=${shellEscapeArg(callerCtx.missionId)}`);
if (callerCtx.ownerId) envPrefixParts.push(`ADE_OWNER_ID=${shellEscapeArg(callerCtx.ownerId)}`);
const addWorkerEnv = (key: string, value: string | null | undefined) => {
if (!value) return;
workerEnv[key] = value;
envPrefixParts.push(`${key}=${shellEscapeArg(value)}`);
};
addWorkerEnv("ADE_RUN_ID", runId);
addWorkerEnv("ADE_STEP_ID", stepId);
addWorkerEnv("ADE_ATTEMPT_ID", attemptId);
addWorkerEnv("ADE_MISSION_ID", callerCtx.missionId);
addWorkerEnv("ADE_OWNER_ID", callerCtx.ownerId);
workerEnv.ADE_DEFAULT_ROLE = "agent";
envPrefixParts.push("ADE_DEFAULT_ROLE=agent");

const startupCommand = envPrefixParts.length > 0
? `${envPrefixParts.join(" ")} ${commandParts.join(" ")}`
: commandParts.join(" ");
? `${envPrefixParts.join(" ")} ${commandPreviewParts.join(" ")}`
: commandPreviewParts.join(" ");

const created = await runtime.ptyService.create({
laneId,
Expand All @@ -5934,6 +5951,9 @@ async function runTool(args: {
title,
tracked: true,
toolType: `${provider}-orchestrated`,
command: provider,
args: commandArgs,
env: workerEnv,
startupCommand
});

Expand Down
40 changes: 32 additions & 8 deletions apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { createDiffService } from "../../desktop/src/main/services/diffs/diffSer
import { createMissionService } from "../../desktop/src/main/services/missions/missionService";
import { createPtyService } from "../../desktop/src/main/services/pty/ptyService";
import { createTestService } from "../../desktop/src/main/services/tests/testService";
import { createProcessService } from "../../desktop/src/main/services/processes/processService";
import { augmentProcessPathWithShellAndKnownCliDirs, setPathEnvValue } from "../../desktop/src/main/services/ai/cliExecutableResolver";
import type { createAgentChatService } from "../../desktop/src/main/services/chat/agentChatService";
import type { createPrService } from "../../desktop/src/main/services/prs/prService";
import { createIssueInventoryService } from "../../desktop/src/main/services/prs/issueInventoryService";
Expand All @@ -37,7 +39,6 @@ import {
type ComputerUseArtifactBrokerService,
} from "../../desktop/src/main/services/computerUse/computerUseArtifactBrokerService";
import type { createFileService } from "../../desktop/src/main/services/files/fileService";
import type { createProcessService } from "../../desktop/src/main/services/processes/processService";
import { createHeadlessLinearServices } from "./headlessLinearServices";
import { createEventBuffer, type BufferedEvent, type EventBuffer } from "./eventBuffer";

Expand Down Expand Up @@ -121,6 +122,17 @@ export function ensureAdePaths(projectRoot: string): AdeRuntimePaths {
};
}

function createHeadlessAdeCliAgentEnv(baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
const next: NodeJS.ProcessEnv = { ...baseEnv };
const nextPath = augmentProcessPathWithShellAndKnownCliDirs({
env: next,
includeInteractiveShell: true,
timeoutMs: 1_000,
});
if (nextPath) setPathEnvValue(next, nextPath);
return next;
}

export async function createAdeRuntime(args: { projectRoot: string; workspaceRoot?: string } | string): Promise<AdeRuntime> {
const resolvedArgs = typeof args === "string"
? { projectRoot: args, workspaceRoot: args }
Expand Down Expand Up @@ -218,6 +230,7 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo
broadcastData: () => {},
broadcastExit: () => {},
onSessionEnded: () => {},
getAdeCliAgentEnv: createHeadlessAdeCliAgentEnv,
loadPty: () => nodePty
});

Expand All @@ -231,6 +244,22 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo
broadcastEvent: () => {}
});
const issueInventoryService = createIssueInventoryService({ db });
const eventBuffer = createEventBuffer();

function pushEvent(category: BufferedEvent["category"], payload: Record<string, unknown>): void {
eventBuffer.push({ timestamp: new Date().toISOString(), category, payload });
}

const processService = createProcessService({
db,
projectId,
logger,
laneService,
projectConfigService,
sessionService,
ptyService,
broadcastEvent: (event) => pushEvent("runtime", event as unknown as Record<string, unknown>),
});

// Ensure evaluation tables exist for headless runtime checks.
db.run(`
Expand All @@ -257,12 +286,6 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo
ON orchestrator_evaluations(run_id, evaluated_at)
`);

const eventBuffer = createEventBuffer();

function pushEvent(category: BufferedEvent["category"], payload: Record<string, unknown>): void {
eventBuffer.push({ timestamp: new Date().toISOString(), category, payload });
}

const memoryService = createMemoryService(db);
const ctoStateService = createCtoStateService({
db,
Expand Down Expand Up @@ -392,13 +415,14 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo
linearSyncService: headlessLinearServices.linearSyncService,
linearIngressService: headlessLinearServices.linearIngressService,
linearRoutingService: headlessLinearServices.linearRoutingService,
processService: headlessLinearServices.processService,
processService,
computerUseArtifactBrokerService,
orchestratorService,
aiOrchestratorService,
eventBuffer,
dispose: () => {
const swallow = (fn: () => void) => { try { fn(); } catch { /* ignore */ } };
swallow(() => processService.disposeAll());
swallow(() => headlessLinearServices.dispose());
swallow(() => aiOrchestratorService.dispose());
swallow(() => testService.disposeAll());
Expand Down
7 changes: 6 additions & 1 deletion apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { buildCliPlan, formatOutput, parseCliArgs, renderLaneGraph, summarizeExecution, unwrapToolResult } from "./cli";
import { buildCliPlan, formatOutput, parseCliArgs, renderLaneGraph, shouldAttemptDesktopSocketConnection, summarizeExecution, unwrapToolResult } from "./cli";

describe("ADE CLI", () => {
it("parses global options without stealing command flags", () => {
Expand Down Expand Up @@ -235,6 +235,11 @@ describe("ADE CLI", () => {
expect(output).toContain("Git repository detected");
});

it("attempts Windows named-pipe desktop sockets without filesystem existence checks", () => {
expect(shouldAttemptDesktopSocketConnection("\\\\.\\pipe\\ade-123")).toBe(true);
expect(shouldAttemptDesktopSocketConnection("//./pipe/ade-123")).toBe(true);
});

it("renders a compact lane graph", () => {
const graph = renderLaneGraph({
lanes: [
Expand Down
35 changes: 26 additions & 9 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fs from "node:fs";
import net from "node:net";
import path from "node:path";
import { type JsonRpcHandler, type JsonRpcId, type JsonRpcRequest } from "./jsonrpc";
import { isAdeMcpNamedPipePath } from "../../desktop/src/shared/adeMcpIpc";

type JsonObject = Record<string, unknown>;

Expand Down Expand Up @@ -1641,7 +1642,8 @@ function resolveRoots(options: GlobalOptions): { projectRoot: string; workspaceR
}

function commandExists(command: string): boolean {
const result = spawnSync("which", [command], {
const lookupCommand = process.platform === "win32" ? "where" : "which";
const result = spawnSync(lookupCommand, [command], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
Expand Down Expand Up @@ -1787,9 +1789,9 @@ function checkProviderReadiness(value: unknown): ReadinessCheck {

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 screenshotReady = isDarwin && commandExists("screencapture");
const appLaunchReady = isDarwin && commandExists("open");
const guiReady = isDarwin && (commandExists("swift") || commandExists("osascript"));
const ready = isDarwin && screenshotReady && appLaunchReady && guiReady;
return {
ready,
Expand All @@ -1814,9 +1816,11 @@ function checkComputerUseReadiness(): ReadinessCheck {
}

function checkPathReadiness(): ReadinessCheck {
const which = runLocalCommand("which", ["ade"], process.cwd());
const lookup = process.platform === "win32"
? runLocalCommand("where", ["ade"], process.cwd())
: 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 whichPath = lookup.ok && lookup.stdout ? path.resolve(lookup.stdout.split(/\r?\n/)[0]!) : null;
const onPath = Boolean(whichPath);
return {
ready: onPath,
Expand All @@ -1831,6 +1835,7 @@ function checkPathReadiness(): ReadinessCheck {
sameBinary: Boolean(whichPath && current && whichPath === current),
electronRunAsNode: process.env.ELECTRON_RUN_AS_NODE === "1",
electronVersion: process.versions.electron ?? null,
lookupCommand: process.platform === "win32" ? "where" : "which",
},
};
}
Expand Down Expand Up @@ -1862,8 +1867,10 @@ function buildReadinessSnapshot(args: {
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 socketExists = isAdeMcpNamedPipePath(connection.socketPath)
? desktopSocketAvailable
: fs.existsSync(connection.socketPath);
const checks = {
git: checkGitReadiness(connection.projectRoot),
github: checkGitHubReadiness(connection.projectRoot),
Expand Down Expand Up @@ -2082,6 +2089,10 @@ class InProcessJsonRpcClient {
}
}

export function shouldAttemptDesktopSocketConnection(socketPath: string): boolean {
return isAdeMcpNamedPipePath(socketPath) || fs.existsSync(socketPath);
}
Comment on lines +2092 to +2094

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Named-pipe probe may add up to 5s latency when desktop is not running.

shouldAttemptDesktopSocketConnection unconditionally returns true for Windows named-pipe paths (since there is no cheap fs.existsSync equivalent), so headless invocations on Windows without a running desktop will always pay the full SocketJsonRpcClient.connect timeout (min(options.timeoutMs, 5000) at line 1980) before falling back to the in-process runtime. Consider using a shorter, dedicated connect probe for pipes (similar to the MCP proxy --probe pattern referenced in the PR description) when options.headless is false but the caller hasn't passed --socket, so typical ade ... invocations on Windows don't block for 5s each time. Non-blocking; flag for follow-up.

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

In `@apps/ade-cli/src/cli.ts` around lines 2092 - 2094, The current
shouldAttemptDesktopSocketConnection always returns true for Windows named pipes
which causes callers (e.g., SocketJsonRpcClient.connect) to block up to the full
connect timeout; change the behavior so when isAdeMcpNamedPipePath(socketPath)
is true and the invocation is not explicitly using --socket (caller-provided
flag) and options.headless is false, perform a short dedicated probe instead of
unconditionally attempting the full connect: add a new fastPipeProbe(path,
timeoutMs) and call it from shouldAttemptDesktopSocketConnection (or from the
caller before calling SocketJsonRpcClient.connect), using a much smaller timeout
(e.g., 100–500ms) to decide whether to attempt the full connection; reference
isAdeMcpNamedPipePath, shouldAttemptDesktopSocketConnection, and
SocketJsonRpcClient.connect to locate where to add the probe and where to use
the shorter timeout.


async function initializeConnection(connection: CliConnection, options: GlobalOptions): Promise<void> {
await connection.request("ade/initialize", {
protocolVersion: PROTOCOL_VERSION,
Expand All @@ -2103,7 +2114,7 @@ async function createConnection(options: GlobalOptions): Promise<CliConnection>
const { resolveAdeLayout } = await import("../../desktop/src/shared/adeLayout");
const layout = resolveAdeLayout(roots.projectRoot);

if (!options.headless && fs.existsSync(layout.socketPath)) {
if (!options.headless && shouldAttemptDesktopSocketConnection(layout.socketPath)) {
try {
const socketClient = await SocketJsonRpcClient.connect(layout.socketPath, options.timeoutMs);
const connection: CliConnection = {
Expand Down Expand Up @@ -2665,7 +2676,13 @@ async function executePlan(plan: CliPlan & { kind: "execute" }, options: GlobalO
connection = await createConnection(options);
} catch (error) {
const roots = resolveRoots(options);
const socketPath = path.join(roots.projectRoot, ".ade", "ade.sock");
let socketPath = path.join(roots.projectRoot, ".ade", "ade.sock");
try {
const { resolveAdeLayout } = await import("../../desktop/src/shared/adeLayout");
socketPath = resolveAdeLayout(roots.projectRoot).socketPath;
} catch {
// Keep the conventional Unix fallback if shared layout loading fails.
}
const requestedMode = options.requireSocket ? "desktop-socket" : options.headless ? "headless" : "auto";
const cause = error instanceof Error ? error.message : String(error);
const sourceRuntimeInterop = isSourceRuntimeInteropError(cause);
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"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:win": "npm run validate:win:artifacts && npm run build && electron-builder --win --x64 --publish never",
"dist:mac": "npm run build && electron-builder --mac --publish never",
"dist:mac:dir": "npm run build && electron-builder --dir --mac --publish never -c.mac.identity=null -c.mac.notarize=false",
"dist:mac:signed": "node ./scripts/require-macos-release-secrets.cjs && npm run build && electron-builder --mac --publish never",
Expand All @@ -19,6 +20,7 @@
"dist:mac:universal:signed:zip": "node ./scripts/require-macos-release-secrets.cjs && npm run build && electron-builder --mac zip --universal --publish never",
"notarize:mac:dmg": "node ./scripts/notarize-mac-dmg.mjs",
"validate:mac:artifacts": "node ./scripts/validate-mac-artifacts.mjs",
"validate:win:artifacts": "node ./scripts/validate-win-artifacts.mjs",
"release:mac:local": "node ./scripts/release-mac-local.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run",
Expand Down Expand Up @@ -162,9 +164,17 @@
"from": "scripts/ade-cli-macos-wrapper.sh",
"to": "ade-cli/bin/ade"
},
{
"from": "scripts/ade-cli-windows-wrapper.cmd",
"to": "ade-cli/bin/ade.cmd"
},
{
"from": "scripts/ade-cli-install-path.sh",
"to": "ade-cli/install-path.sh"
},
{
"from": "scripts/ade-cli-install-path.cmd",
"to": "ade-cli/install-path.cmd"
}
],
"afterPack": "./scripts/after-pack-runtime-fixes.cjs",
Expand All @@ -176,6 +186,12 @@
"publishAutoUpdate": true
},
"npmRebuild": false,
"win": {
"target": [
"nsis"
],
"artifactName": "${productName}-${version}-win-${arch}.${ext}"
},
"mac": {
"target": [
"dmg",
Expand Down
Loading
Loading