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
19 changes: 19 additions & 0 deletions .claude/commands/audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
description: Audit recent work — trace error paths, probe edge cases, fix any bugs or gaps found
---

Audit the work you just did in this session. Don't stop at "it compiles" or "it looks right" — actively go hunting for what's wrong.

1. **Retrace the changes.** List every file you touched. For each, re-read the final state (not just the diff you remember) so you see what actually lives there now.

2. **Trace every error path.** For each changed code path: what happens on empty / nil / malformed input? When an upstream caller passes something unexpected? When a dependency throws or times out? Walk the failure branches, not just the happy path.

3. **Hunt edge cases.** Off-by-ones, empty collections, unicode, concurrency, first-run vs. repeat-run, reduce-motion / accessibility, different device/viewport sizes, streaming vs. terminal states, cancellation, partial failure. Pick the categories that actually apply to what you changed and work through them.

4. **Check the surrounding contract.** Did the change break any callers, tests, types, styling, or invariants elsewhere? Grep for references to anything you removed or renamed and confirm.

5. **Fix what you find.** For each real bug or gap, make the fix directly. For anything genuinely ambiguous, call it out rather than guessing.

6. **Report.** End with a short list: what you checked, what you fixed, and anything you deliberately left alone (and why).

Be honest — if the work was already solid, say so in one line. Don't manufacture busywork.
139 changes: 110 additions & 29 deletions .claude/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Drive a full ADE release end-to-end: figure out what needs to ship (desktop, iOS
Mostly autonomous, but **pause for explicit user input** on:
- The new version number (if not passed in `$ARGUMENTS`).
- The iOS build number (if iOS is in scope and not passed in `$ARGUMENTS`).
- **The iOS target TestFlight group(s)** (always — enumerate groups + tester counts first; never assume a default). See Phase 7a.
- Any step that would force-push `main`, bypass a ruleset in a surprising way, or publish a release that is still in `draft=false`.

Do NOT publish the GitHub draft release automatically. Leave it as a draft for a human to flip.
Expand Down Expand Up @@ -257,7 +258,7 @@ RELEASE_SHA=$(git rev-parse origin/main)
3. Once the draft release appears (the workflow creates it), make sure the release body links to the Mintlify changelog page:

```bash
gh release view "v<VERSION>" --json body,isDraft,url
gh release view "v<VERSION>" --json body,isDraft,url,assets
gh release edit "v<VERSION>" --notes "$(cat <<EOF
ADE v<VERSION>

Expand All @@ -270,6 +271,10 @@ RELEASE_SHA=$(git rev-parse origin/main)

Leave `isDraft=true`. Do not publish.

Expect the draft to carry both macOS and Windows assets once `publish-release` runs:
- macOS: `ADE-<version>-universal.dmg`, `ADE-<version>-universal-mac.zip`, `ADE-<version>-universal-mac.zip.blockmap`, `latest-mac.yml`
- Windows: `ADE-<version>-win-x64.exe`, `ADE-<version>-win-x64.exe.blockmap`, `latest.yml`

---

## Phase 6 — Poll the release workflow
Expand Down Expand Up @@ -321,13 +326,35 @@ Do not loop in-turn. One poll per wake-up.

Skip entirely if `scope.ios=false`.

If `scope.ios=true` and you do not have a build number:
### 7a. Ask the user: build number + target group(s)

Always pause for these two inputs (even if build number came in via `$ARGUMENTS`, confirm the group choice). Ask together so the user answers once:

> 1. **Build number.** The last one uploaded for `<MARKETING_VERSION>` was `<N>`. New build will be `<N+1>`. Override if you want a different number.
> 2. **Target TestFlight group(s).** The workspace has the groups below. Which should receive this build? (comma-separated names or IDs; default = `Internal Testers` if only you will be testing.)

> What build number should this TestFlight build use? The last one uploaded was `<N>` (run `asc builds list --app <APP_ID> --limit 5` to confirm).
Before asking, enumerate the groups and their tester counts so the user can pick knowingly:

Validate: must be a positive integer strictly greater than the last build number on record.
```bash
# List all groups (note isInternal)
asc testflight groups list --app "$APP_ID"

# For each group ID, count actual testers
for gid in <group-ids>; do
count=$(asc testflight groups links view --group-id "$gid" --type betaTesters \
| jq -r '.meta.paging.total')
echo "$gid testers=$count"
done
```

### Pre-flight
**Rules of thumb:**
- **Internal** groups (`isInternalGroup=true`): builds appear for testers as soon as the build is `VALID` and added to the group. No beta app review needed. Use this for dev-only testing.
- **External** groups (`isInternalGroup=false` or `None`): need beta app review (usually auto-approved for subsequent builds of the same marketing version). Use for wider-audience betas.
- **A group with zero testers is invisible.** If you add a build only to an empty group, nobody sees it and no emails go out. Verify tester counts before choosing.

Validate build number: strictly greater than the last recorded for that marketing version — confirm with `asc builds next-build-number --app "$APP_ID" --version "$MARKETING_VERSION" --platform IOS`.

### 7b. Pre-flight

`AGENTS.md` and the `asc-*` skills are the source of truth. Re-read before every release; the gotchas below are stable but the skill contents may change:

Expand All @@ -345,9 +372,9 @@ asc doctor

Fail fast if keychain auth is broken.

### iOS signing gotchas (mirrored from AGENTS.md — keep in sync)
### 7c. iOS signing gotchas (mirrored from AGENTS.md — keep in sync)

- Project uses **automatic** signing (`CODE_SIGN_STYLE = Automatic`, `DEVELOPMENT_TEAM = VQ372F39G6`). `apps/ios/ExportOptions.plist` ships with `signingStyle = manual` + named profiles for CI determinism. Local ad-hoc exports need `signingStyle = automatic` instead (drop the per-bundle profile map).
- Project uses **automatic** signing (`CODE_SIGN_STYLE = Automatic`, `DEVELOPMENT_TEAM = VQ372F39G6`). `apps/ios/ExportOptions.plist` ships with `signingStyle = manual` + named profiles for CI determinism. Local ad-hoc exports need `signingStyle = automatic` instead (drop the per-bundle profile map). `apps/ios/ExportOptions.auto.plist` is the ready-to-use auto-signing variant.
- `asc signing fetch` only downloads provisioning profiles and the `.cer` — it does **not** include the private key. Don't expect it to make local signing work on its own.
- Local exports need the ASC API key passed to `xcodebuild`. In addition to `-allowProvisioningUpdates`:
```
Expand All @@ -356,54 +383,108 @@ Fail fast if keychain auth is broken.
-authenticationKeyIssuerID 4d523a6c-e68c-49b2-8560-34e59786d8e3
```
Pull current values from `~/.asc/config.json`; do not hard-code.
- After upload, `processingState = VALID` is not enough for TestFlight distribution. Also set `usesNonExemptEncryption` and assign to a group:
```bash
asc builds update --build-id <ID> --uses-non-exempt-encryption=false
asc publish testflight --build <ID> --group "<Beta Group>"
```
- Override the build number at archive time via `--archive-xcodebuild-flag "CURRENT_PROJECT_VERSION=<N>"` so you do not need to commit a `pbxproj` bump just to ship a build.

### One-shot publish
### 7d. Do NOT use the one-shot `asc publish testflight --group ... --wait` in local-build mode

Full flow (archive + export + upload + distribute):
That form races: `--wait` returns as soon as `processingState=VALID`, but the build's `usesNonExemptEncryption` is still `None` at that instant. The subsequent `add-groups` then fails with `Build is not in an externally assignable state` because Apple blocks group attachment until the encryption question is answered. Seen on asc 1.2.x.

### 7e. Safe sequenced flow

Split the publish into explicit steps so encryption is answered before any group attachment:

```bash
# 1) Archive + export + upload + wait for VALID (no --group here)
asc publish testflight \
--app <APP_ID> \
--app "$APP_ID" \
--project apps/ios/ADE.xcodeproj \
--scheme ADE \
--version <VERSION-without-v> \
--build-number <BUILD_NUMBER> \
--export-options <auto-plist> \
--group "<Beta Group>" \
--wait
--version "$MARKETING_VERSION" \
--export-options apps/ios/ExportOptions.auto.plist \
--initial-build-number "$BUILD_NUMBER" \
--archive-path "/tmp/ade-ios-build${BUILD_NUMBER}/ADE.xcarchive" \
--ipa-path "/tmp/ade-ios-build${BUILD_NUMBER}/ADE.ipa" \
--wait \
--timeout 60m \
--pretty \
--archive-xcodebuild-flag "-allowProvisioningUpdates" \
--archive-xcodebuild-flag "-authenticationKeyPath" \
--archive-xcodebuild-flag "$ASC_KEY_PATH" \
--archive-xcodebuild-flag "-authenticationKeyID" \
--archive-xcodebuild-flag "$ASC_KEY_ID" \
--archive-xcodebuild-flag "-authenticationKeyIssuerID" \
--archive-xcodebuild-flag "$ASC_ISSUER_ID" \
--archive-xcodebuild-flag "CURRENT_PROJECT_VERSION=$BUILD_NUMBER" \
--archive-xcodebuild-flag "MARKETING_VERSION=$MARKETING_VERSION"

# 2) Resolve the build ID by build number
BUILD_ID=$(asc builds list --app "$APP_ID" --limit 5 \
| jq -r --arg v "$BUILD_NUMBER" '.data[] | select(.attributes.version == $v) | .id' \
| head -n 1)

# 3) Answer encryption BEFORE any group attachment
asc builds update --build-id "$BUILD_ID" --uses-non-exempt-encryption=false

# 4) Distribute to chosen group(s). --submit --confirm is a no-op if the group is internal
# or if Apple already auto-approved the marketing version.
for gid in "${GROUP_IDS[@]}"; do
asc builds add-groups --build-id "$BUILD_ID" --group "$gid" --submit --confirm
done
```

If `--wait` times out, fall back to polling via `asc builds get` every 5 minutes using the same `ScheduleWakeup` pattern as Phase 6. Update `.ade/release/v<VERSION>.json` with `status=ios-running` so a re-invocation resumes here.
If `--wait` times out, fall back to polling via `asc builds info --build-id "$BUILD_ID"` every 5 minutes using the `ScheduleWakeup` pattern from Phase 6. Update `.ade/release/v<VERSION>.json` with `status=ios-running`.

### 7f. Post-upload verification (always run this)

### Post-upload checks
Do not declare iOS done based on `BETA_APPROVED` alone. Verify the build is in a **non-empty** group:

```bash
asc builds get --build-id <ID> --json
asc builds info --build-id "$BUILD_ID" # processingState=VALID, usesNonExemptEncryption=false
asc builds build-beta-detail view --build-id "$BUILD_ID" # externalBuildState=BETA_APPROVED, internalBuildState=READY_FOR_BETA_TESTING
for gid in "${GROUP_IDS[@]}"; do
count=$(asc testflight groups links view --group-id "$gid" --type betaTesters | jq -r '.meta.paging.total')
members=$(asc testflight groups links view --group-id "$gid" --type builds | jq -r '.data[].id' | grep -Fx "$BUILD_ID" || true)
echo "group=$gid testers=$count build_present=$([ -n "$members" ] && echo yes || echo no)"
done
```

Confirm:
- `processingState = VALID`
- `usesNonExemptEncryption` is answered
- Build is in the intended beta group
All three must be true for a given group:
- `testers > 0` (otherwise no humans see the build)
- build appears in the group's builds list
- internal → `READY_FOR_BETA_TESTING`; external → `BETA_APPROVED`

If any check fails, run it explicitly (see gotchas above) and re-verify.
If any fails, fix it explicitly and re-verify. Do not trust `autoNotifyEnabled=true` alone — it only controls push notifications, not distribution.

---

## Phase 8 — Summary

Print a single final block and stop. Example:
Before printing the summary, verify the draft release carries every expected asset. Do not flip the draft and do not report `done` if anything is missing — surface the gap.

```bash
gh release view "v<VERSION>" --json assets --jq '.assets[].name' | sort
```

Expected asset set when `scope.desktop=true`:
- `ADE-<version>-universal.dmg`
- `ADE-<version>-universal-mac.zip`
- `ADE-<version>-universal-mac.zip.blockmap`
- `latest-mac.yml`
- `ADE-<version>-win-x64.exe`
- `ADE-<version>-win-x64.exe.blockmap`
- `latest.yml`

If any macOS asset is missing → mac build or upload broke; re-inspect the `build-mac-release` job.
If any Windows asset is missing → `build-win-release` broke or its artifact upload failed; re-inspect that job. Do not flip the draft if Windows artifacts are missing — shipping an asymmetric desktop release will confuse electron-updater consumers on the missing platform.

Then print a single final block and stop:

```
Release v<VERSION> — summary

- Changelog: https://www.ade-app.dev/docs/changelog/v<VERSION>
- Draft release: <gh release url> (still draft — flip manually)
- Desktop assets: mac=<present|MISSING>, windows=<present|MISSING>
- Workflow run: <gh run url> (conclusion: success)
- iOS TestFlight build <BUILD_NUMBER>: <VALID | processing | skipped>
- Beta group: <group name | n/a>
Expand Down
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":"1676c542-49ae-4b07-80db-808ac138cb4b","pid":24538,"procStart":"Fri Apr 24 04:52:14 2026","acquiredAt":1777006545124}
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,41 @@ jobs:
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

# ── Windows build smoke (self-contained — no shared cache) ────────────
# Runs the same dist:win pipeline that release-core.yml uses, so a PR
# that would break Windows release is caught here instead of at release
# time. Self-contained because windows-latest node_modules contain
# platform-specific native binaries that can't share a Linux cache.
build-win:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- 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: 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

# ── Gate: all jobs must pass ──────────────────────────────────────────
ci-pass:
if: always()
Expand All @@ -205,6 +240,7 @@ jobs:
- test-ade-cli
- build
- validate-docs
- build-win
runs-on: ubuntu-latest
steps:
- name: Check all jobs passed
Expand Down
4 changes: 2 additions & 2 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ 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 prs path-to-merge --pr pr-id --model gpt-5.5 --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 chat create --lane lane-id --model gpt-5.5
ade tests run --lane lane-id --suite unit --wait
ade proof list --arg ownerKind=chat --arg ownerId=session-id
ade actions list
Expand Down
26 changes: 24 additions & 2 deletions apps/ade-cli/src/headlessLinearServices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,30 @@ describe("headlessLinearServices", () => {
laneId: "lane-1",
});
expect(session.title).toBe("CTO Headless Session");
expect(session.model).toBe("gpt-5.4-codex");
expect(session.modelId).toBe("openai/gpt-5.4-codex");
expect(session.model).toBe("gpt-5.5");
expect(session.modelId).toBe("openai/gpt-5.5-codex");

services.dispose();
});

it("resolves explicit model IDs to their native runtime model refs in headless sessions", async () => {
const services = createHeadlessLinearServices(createDeps());

const codex = await services.agentChatService.ensureIdentitySession({
identityKey: "agent:codex-model",
laneId: "lane-1",
modelId: "openai/gpt-5.5-codex",
});
const claude = await services.agentChatService.ensureIdentitySession({
identityKey: "agent:claude-model",
laneId: "lane-1",
modelId: "anthropic/claude-opus-4-7-1m",
});

expect(codex.model).toBe("gpt-5.5");
expect(codex.modelId).toBe("openai/gpt-5.5-codex");
expect(claude.model).toBe("opus-1m");
expect(claude.modelId).toBe("anthropic/claude-opus-4-7-1m");

services.dispose();
});
Expand Down
6 changes: 3 additions & 3 deletions apps/ade-cli/src/headlessLinearServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import type { createWorkerTaskSessionService } from "../../desktop/src/main/serv
import type { createWorkerHeartbeatService } from "../../desktop/src/main/services/cto/workerHeartbeatService";
import type { createAutomationSecretService } from "../../desktop/src/main/services/automations/automationSecretService";
import type { ComputerUseArtifactBrokerService } from "../../desktop/src/main/services/computerUse/computerUseArtifactBrokerService";
import { getModelById, resolveModelAlias } from "../../desktop/src/shared/modelRegistry";
import { getModelById, getRuntimeModelRefForDescriptor, resolveModelAlias } from "../../desktop/src/shared/modelRegistry";
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";
Expand Down Expand Up @@ -396,7 +396,7 @@ function createHeadlessAgentChatService(projectRoot: string): HeadlessLinearServ
const identitySessionIds = new Map<string, string>();
const transcripts = new Map<string, HeadlessTranscriptEntry[]>();

const HEADLESS_MODEL_ID = "openai/gpt-5.4-codex";
const HEADLESS_MODEL_ID = "openai/gpt-5.5-codex";

const clipText = (value: string, maxChars: number): string => {
const trimmed = value.trim();
Expand All @@ -421,7 +421,7 @@ function createHeadlessAgentChatService(projectRoot: string): HeadlessLinearServ
if (descriptor) {
return {
modelId: descriptor.id,
model: descriptor.shortId,
model: getRuntimeModelRefForDescriptor(descriptor),
};
}
return {
Expand Down
Loading
Loading