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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"sessionId":"bab81aa2-1bdb-495e-9bf6-3d87ede93f1f","pid":85962,"procStart":"Thu Apr 23 05:29:47 2026","acquiredAt":1776922287064}
{"sessionId":"5364eda2-5696-4227-b94c-5f2678de1f2e","pid":64448,"procStart":"Thu Apr 23 18:51:52 2026","acquiredAt":1776973376438}
87 changes: 80 additions & 7 deletions .github/workflows/release-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
needs: verify
runs-on: macos-15
concurrency:
group: release-${{ inputs.release_tag }}
group: release-${{ inputs.release_tag }}-mac
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -110,7 +110,6 @@ jobs:
run: cd apps/desktop && npm run validate:mac:artifacts

- name: Upload validated artifacts to workflow run
if: ${{ !inputs.publish }}
uses: actions/upload-artifact@v4
with:
name: ade-mac-release-${{ inputs.release_tag }}
Expand All @@ -121,19 +120,93 @@ jobs:
apps/desktop/release/latest-mac.yml
if-no-files-found: error

build-win-release:
needs: verify
runs-on: windows-latest
concurrency:
group: release-${{ inputs.release_tag }}-win
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.target_ref }}
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
apps/desktop/package-lock.json
apps/ade-cli/package-lock.json

- name: Install desktop dependencies
run: cd apps/desktop && npm ci

- name: Install ADE CLI dependencies
run: cd apps/ade-cli && npm ci

- name: Stamp release version
env:
ADE_RELEASE_TAG: ${{ inputs.release_tag }}
run: cd apps/desktop && npm run version:release

- name: Reset release output
shell: pwsh
run: |
Remove-Item -Recurse -Force apps/desktop/release, apps/desktop/.cache -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path apps/desktop/.cache | Out-Null

- name: Build and validate Windows release
env:
ELECTRON_CACHE: ${{ github.workspace }}\apps\desktop\.cache\electron
ELECTRON_BUILDER_CACHE: ${{ github.workspace }}\apps\desktop\.cache\electron-builder
run: cd apps/desktop && npm run dist:win

- name: Upload validated Windows artifacts to workflow run
uses: actions/upload-artifact@v4
with:
name: ade-win-release-${{ inputs.release_tag }}
path: |
apps/desktop/release/*.exe
apps/desktop/release/*.exe.blockmap
apps/desktop/release/latest.yml
if-no-files-found: error

publish-release:
if: ${{ inputs.publish }}
needs:
- build-mac-release
- build-win-release
runs-on: ubuntu-latest
steps:
- name: Download macOS release artifacts
uses: actions/download-artifact@v4
with:
name: ade-mac-release-${{ inputs.release_tag }}
path: release-assets/mac

- name: Download Windows release artifacts
uses: actions/download-artifact@v4
with:
name: ade-win-release-${{ inputs.release_tag }}
path: release-assets/win

- name: Create or update draft GitHub release
if: ${{ inputs.publish }}
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ inputs.release_tag }}
TARGET_REF: ${{ inputs.target_ref }}
run: |
shopt -s nullglob
files=(
apps/desktop/release/*.dmg
apps/desktop/release/*.zip
apps/desktop/release/*-mac.zip.blockmap
apps/desktop/release/latest-mac.yml
release-assets/mac/*.dmg
release-assets/mac/*.zip
release-assets/mac/*-mac.zip.blockmap
release-assets/mac/latest-mac.yml
release-assets/win/*.exe
release-assets/win/*.exe.blockmap
release-assets/win/latest.yml
)

if [ "${#files[@]}" -eq 0 ]; then
Expand Down
135 changes: 118 additions & 17 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { createAdeRpcRequestHandler, _resetGlobalAskUserRateLimit } from "./adeRpcServer";

type RuntimeFixture = ReturnType<typeof createRuntime>;
const originalPlatform = process.platform;

function setPlatform(value: NodeJS.Platform): void {
Object.defineProperty(process, "platform", {
value,
configurable: true,
});
}

afterEach(() => {
setPlatform(originalPlatform);
});

function createRuntime() {
const operationStart = vi.fn((args: any) => ({ operationId: `op-${args.kind}-${Date.now()}` }));
Expand Down Expand Up @@ -946,6 +958,14 @@ async function withEnv<T>(vars: Record<string, string | undefined>, fn: () => Pr
}
}

function createFakePathExecutable(dir: string, name: string): string {
fs.mkdirSync(dir, { recursive: true });
const executablePath = path.join(dir, process.platform === "win32" ? `${name}.cmd` : name);
fs.writeFileSync(executablePath, process.platform === "win32" ? "@echo off\r\n" : "#!/bin/sh\n");
if (process.platform !== "win32") fs.chmodSync(executablePath, 0o755);
return executablePath;
}

describe("adeRpcServer", () => {
it("treats requested privileged roles as external without trusted env identity", async () => {
const { runtime } = createRuntime();
Expand Down Expand Up @@ -1805,15 +1825,19 @@ describe("adeRpcServer", () => {

it("routes spawn_agent to lane-scoped tracked pty sessions", async () => {
const fixture = createRuntime();
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-spawn-bin-"));
const claudePath = createFakePathExecutable(binDir, "claude");
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

await initialize(handler, { role: "orchestrator" });
const response = await callTool(handler, "spawn_agent", {
laneId: "lane-1",
provider: "claude",
model: "claude-sonnet-4-6",
prompt: "Implement API wiring",
title: "Orchestrator Spawn"
const response = await withEnv({ PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}` }, async () => {
await initialize(handler, { role: "orchestrator" });
return await callTool(handler, "spawn_agent", {
laneId: "lane-1",
provider: "claude",
model: "claude-sonnet-4-6",
prompt: "Implement API wiring",
title: "Orchestrator Spawn"
});
});

expect(response?.isError).toBeUndefined();
Expand All @@ -1823,7 +1847,12 @@ describe("adeRpcServer", () => {
cols: 120,
rows: 36,
tracked: true,
toolType: "claude-orchestrated"
toolType: "claude-orchestrated",
command: claudePath,
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 All @@ -1836,23 +1865,95 @@ describe("adeRpcServer", () => {
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-cli-spawn-workspace-"));
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-spawn-bin-"));
const claudePath = createFakePathExecutable(binDir, "claude");
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

await initialize(handler, { role: "orchestrator", runId: "run-from-identity" });
const response = await callTool(handler, "spawn_agent", {
laneId: "lane-1",
provider: "claude",
model: "claude-sonnet-4-6",
prompt: "Implement API wiring",
title: "Orchestrator Spawn",
runId: "run-1",
attemptId: "attempt-workspace-roots"
const response = await withEnv({ PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}` }, async () => {
await initialize(handler, { role: "orchestrator", runId: "run-from-identity" });
return await callTool(handler, "spawn_agent", {
laneId: "lane-1",
provider: "claude",
model: "claude-sonnet-4-6",
prompt: "Implement API wiring",
title: "Orchestrator Spawn",
runId: "run-1",
attemptId: "attempt-workspace-roots"
});
});

expect(response?.isError).toBeUndefined();
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: claudePath,
env: expect.objectContaining({
ADE_RUN_ID: "run-1",
ADE_ATTEMPT_ID: "attempt-workspace-roots",
ADE_DEFAULT_ROLE: "agent",
}),
})
);
});

it("keeps spawn_agent on shell startup when the provider executable cannot be resolved", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

const response = await withEnv({ PATH: fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-empty-path-")) }, async () => {
await initialize(handler, { role: "orchestrator" });
return await callTool(handler, "spawn_agent", {
laneId: "lane-1",
provider: "claude",
prompt: "Implement API wiring",
});
});

expect(response?.isError).toBeUndefined();
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(
expect.not.objectContaining({
command: expect.any(String),
})
);
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(
expect.objectContaining({
startupCommand: expect.stringContaining("claude"),
})
);
});

it("does not use POSIX env assignment in unresolved Windows spawn_agent startup commands", async () => {
setPlatform("win32");
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

const response = await withEnv({ PATH: fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-empty-win-path-")) }, async () => {
await initialize(handler, { role: "orchestrator" });
return await callTool(handler, "spawn_agent", {
laneId: "lane-1",
provider: "claude",
prompt: "Implement API wiring",
runId: "run-1",
attemptId: "attempt-win-fallback",
});
});

expect(response?.isError).toBeUndefined();
expect(response.structuredContent.startupCommand).toContain("claude");
expect(response.structuredContent.startupCommand).not.toContain("ADE_RUN_ID=run-1");
expect(response.structuredContent.startupCommand).not.toContain("ADE_ATTEMPT_ID=attempt-win-fallback");
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(
expect.objectContaining({
env: expect.objectContaining({
ADE_RUN_ID: "run-1",
ADE_ATTEMPT_ID: "attempt-win-fallback",
ADE_DEFAULT_ROLE: "agent",
}),
startupCommand: response.structuredContent.startupCommand,
})
);
});

it("rejects config-toml permission mode for Claude spawn_agent sessions", async () => {
Expand Down
Loading
Loading