Skip to content
Closed
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
42 changes: 0 additions & 42 deletions .claude/commands/finalize.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ It guarantees three outcomes:
2. Docs are current
3. Local CI checks pass

It does **not** guarantee that remote PR review is complete after a push. GitHub's
first visible check list can look quiet before delayed checks, bot reviews, and
inline comments arrive. After pushing a finalized branch, hand off to
`/shipLane` or an equivalent PR poll loop. Use the ship-lane cadence: poll
immediately after a push, wait 270s if CI has not registered, wait 720s while CI
is running, and wait 1800s only when CI is done and the PR is just waiting on
review.

**Usage:** `/finalize`

## Execution Mode: Autonomous
Expand Down Expand Up @@ -420,34 +412,6 @@ Kill selectively only if the parent is clearly gone (PPID == 1 on macOS/Linux).

Report killed PIDs in the Phase 4 summary under "Cleanup" so the user can see what happened.

### 3k. Remote PR poll handoff

If this finalize run is followed by a push or PR update, do not treat the first
`gh pr checks` result as authoritative proof that remote review is done. Some
checks and bot review systems appear late or post comments after the initial CI
surface looks complete. In particular:

- `gh pr checks` can omit delayed or still-registering provider checks.
- Bot reviewers can post inline comments after CI jobs have already gone green.
- The absence of new comments immediately after a push is not evidence that no
more comments are coming.

Handoff rule:

```bash
# After the branch is pushed, continue with /shipLane or equivalent:
# - poll PR checks, status rollup, review comments, issue comments, and reviews
# - poll immediately after a push so early CI registration/failures are visible
# - if CI has not started yet, wait 270s
# - if any check is QUEUED/IN_PROGRESS/PENDING, wait 720s
# - if CI is done and the PR is only waiting on review, wait 1800s
# - poll again before declaring the PR clean or ready for human merge
```

If `/finalize` is running as a sub-step inside `/shipLane`, return a summary that
explicitly says remote checks/comments still require the ship-lane poll loop.
Do not report "PR clean" from `/finalize` alone.

---

## Phase 4: Summary
Expand Down Expand Up @@ -483,11 +447,6 @@ Do not report "PR clean" from `/finalize` alone.
### Cleanup:
- Orphan processes killed: N (PIDs: [list] or "none")

### Remote PR Handoff:
- Post-push polling required: YES
- Poll loop: `/shipLane` branch-specific cadence
- Reason: delayed checks and bot comments may arrive after first visible green state

### Status: Ready to push / Issues found
```

Expand All @@ -507,4 +466,3 @@ Before marking complete:
- [ ] All apps build successfully
- [ ] Doc validation passed
- [ ] Orphan worker processes cleaned up (vitest/tsup/tsc) — scoped to apps/ paths only
- [ ] Remote PR review is not declared clean by finalize alone; after push, `/shipLane` or an equivalent poll loop must use the branch-specific cadence and re-check comments/reviews
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