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
167 changes: 163 additions & 4 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,61 @@ git fetch origin main

If not on dev or working tree is dirty, abort with a clear message.

### Step 1.5: Pre-flight compiled-binary smoke test (MANDATORY before any other step)

> **Why this is first**: releases have ended up with zero working binaries because a module-init crash or bundler bug only surfaces in `bun build --compile` output, not in `bun run`. CI catches it — but only AFTER the tag is pushed and a GitHub Release is created. By then the damage (empty release, broken `releases/latest`, broken `install.sh`) is already live. Failing here, before any user-visible change, keeps the blast radius at "no release was cut."

Run locally on the native target. This takes ~15-30s and is cheaper than discovering the problem after tag+release.

```bash
# Guard: only run this for Node/Bun projects with a CLI entry point + build-binaries script.
if [ -f scripts/build-binaries.sh ] && [ -f packages/cli/src/cli.ts ]; then
TMP_BINARY=$(mktemp)
trap "rm -f $TMP_BINARY" EXIT

# Compile for the native target only (not full cross-compile — that's CI's job).
# Match the real release flags so any bundler quirk reproduces locally.
bun build \
--compile \
--minify \
--target=bun \
--outfile="$TMP_BINARY" \
packages/cli/src/cli.ts

# Smoke test: the binary must start and exit 0 on a safe, non-interactive command.
# `version` or `--help` are both acceptable — pick one that does NOT touch the
# network, database, or require env vars.
if ! "$TMP_BINARY" version > /tmp/archon-preflight.log 2>&1; then
echo "ERROR: compiled binary crashed at startup"
cat /tmp/archon-preflight.log
echo ""
echo "This usually means a dependency has a module-init-time side effect that"
echo "fails in a compiled binary context (readFileSync of a path that only"
echo "exists in node_modules, etc.). Fix before cutting the release — do NOT"
echo "proceed to version bump."
exit 1
fi

# Also grep for known crash markers that exit 0 but print a fatal error
# (some module-init errors are caught by top-level try/catch but still log).
if grep -qE "Expected CommonJS module|TypeError:|ReferenceError:|SyntaxError:" /tmp/archon-preflight.log; then
echo "ERROR: compiled binary emitted a runtime error despite exit 0"
cat /tmp/archon-preflight.log
exit 1
fi

echo "Pre-flight binary smoke: PASSED"
fi
```

If this fails, **abort the release entirely** — do not bump version, do not modify CHANGELOG, do not create a PR. Surface the error to the user, point at the failing output, and stop. Recovery is: fix the bundler / dependency issue on a feature branch, merge to dev, re-run `/release`.

**Common failure modes this catches:**
- Bun `--bytecode` flag producing broken bytecode for the current module graph
- A dependency (e.g. an SDK) reading `package.json` or other files at module top level via paths that resolve fine in `node_modules/` but not next to a compiled binary
- Circular imports that break under minification but work under plain `bun run`
- A newly added package that ships CJS with an unusual wrapper shape

### Step 2: Detect Stack and Current Version

Detect the project's package manager and version file:
Expand Down Expand Up @@ -211,21 +266,53 @@ After the tag is pushed, `.github/workflows/release.yml` builds platform binarie

```bash
echo "Waiting for release workflow to finish uploading binaries..."
WORKFLOW_FAILED=0
for i in {1..30}; do
ASSET_COUNT=$(gh release view "vx.y.z" --repo coleam00/Archon --json assets --jq '.assets | length')
# Expect 7 assets: 5 binaries (darwin-arm64, darwin-x64, linux-arm64, linux-x64, windows-x64.exe) + archon-web.tar.gz + checksums.txt
if [ "$ASSET_COUNT" -ge 7 ]; then
echo "All $ASSET_COUNT assets uploaded"
break
fi

# Short-circuit: if the release workflow itself has failed, stop waiting.
# Hanging for 15 min when CI already crashed just delays the recovery path.
WORKFLOW_STATUS=$(gh run list --workflow release.yml --event push --limit 1 --json conclusion,status --jq '.[0] | "\(.status)|\(.conclusion)"')
if [[ "$WORKFLOW_STATUS" == "completed|failure" ]]; then
echo "Release workflow FAILED — no point waiting longer"
WORKFLOW_FAILED=1
break
fi

echo " Assets so far: $ASSET_COUNT/7 — waiting 30s (attempt $i/30)..."
sleep 30
done

if [ "$ASSET_COUNT" -lt 7 ]; then
echo "ERROR: Release workflow did not finish uploading assets after 15 minutes"
echo "Check https://github.com/coleam00/Archon/actions for the release workflow run"
exit 1
if [ "$WORKFLOW_FAILED" -eq 1 ] || [ "$ASSET_COUNT" -lt 7 ]; then
# Triage: rerun once in case it's transient, then check again.
RUN_ID=$(gh run list --workflow release.yml --event push --limit 1 --json databaseId --jq '.[0].databaseId')
echo "Release workflow failed on run $RUN_ID. Rerunning failed jobs once to confirm..."
gh run rerun "$RUN_ID" --failed
gh run watch "$RUN_ID" --exit-status --interval 30 || true

# Re-check asset count + run status after rerun.
ASSET_COUNT=$(gh release view "vx.y.z" --repo coleam00/Archon --json assets --jq '.assets | length')
if [ "$ASSET_COUNT" -ge 7 ]; then
echo "Rerun succeeded — all assets now present"
else
echo ""
echo "===== DETERMINISTIC CI FAILURE ====="
echo "The release workflow failed on two consecutive runs. This is NOT a flake."
echo "The tag and release exist but have no (or incomplete) assets."
echo ""
echo "install.sh and similar 'releases/latest' paths are now 404-ing."
echo "Proceeding with Homebrew/tap sync would publish a formula pointing at"
echo "missing or inconsistent binaries."
echo ""
echo "Jump to the 'Recovery: deterministic release CI failure' section at the"
echo "bottom of this skill and execute it. Do NOT continue past this point."
exit 1
fi
fi
```

Expand Down Expand Up @@ -376,9 +463,81 @@ Also run `/test-release curl-mac x.y.z` to cover the curl install path. The two

If you have a VPS available, also run `/test-release curl-vps x.y.z <vps-target>` to verify the Linux binary.

## Recovery: deterministic release CI failure

Reached here because Step 10 detected two consecutive workflow failures. The tag `vx.y.z` is pushed, the GitHub release exists, but assets are missing or incomplete. Every `install.sh` run currently resolves `releases/latest` to this broken release and 404s on download. Homebrew users are safe because Step 10's atomic formula update was blocked.

**Do not re-run the release workflow a third time hoping it succeeds.** If the failure was reproducible twice, it's a code bug — you need to ship code to fix it.

### Immediate mitigation (restore `install.sh`)

Delete the GitHub Release so `releases/latest` falls back to the previous version. Keep the git tag — tag immutability matters and there are no shipped artifacts pointing at it anyway.

```bash
gh release delete "vx.y.z" --yes
# Do NOT delete the tag:
# git push --delete origin vx.y.z ← do not run
# Tag stays so git history records the attempt; no release means no assets
# means releases/latest resolves to the prior working release.
```

Verify:

```bash
gh api repos/coleam00/Archon/releases/latest --jq '.tag_name'
# should now print the prior version (e.g. v0.3.6), not vx.y.z
```

### Diagnose

The release workflow logs tell you which target failed and at what stage (compile vs. smoke-test vs. upload):

```bash
gh run list --workflow release.yml --limit 2 --json databaseId,conclusion
gh run view <RUN_ID> --log-failed
```

Common causes:
- **Bundler/bytecode bug** — Bun `--bytecode` produces invalid output for the current module graph. Symptom: `TypeError: Expected CommonJS module to have a function wrapper` at binary startup. Historically caused by a new dependency's CJS/ESM shape interacting with `--bytecode` — dropping the flag or lazy-importing the offending module has been the fix.
- **Module-init crash** — a dependency does `readFileSync('package.json')` or similar at module top level via a path that exists in `node_modules/` but not next to a compiled binary. Symptom: every binary subcommand crashes immediately; error typically mentions a missing file adjacent to `process.execPath`. Fix by lazy-importing the dependency behind the code path that actually uses it.
- **Smoke-test timeout on Windows** — not actually a bug in the code; the Windows runner is slow. Rerun once; if it recurs, bump the test timeout.

Step 1.5 now runs a local compiled-binary smoke test before any user-visible step. If the failure mode above reproduces locally, you've found it. If it doesn't, the bug is platform-specific (Windows cross-compile, Linux glibc, etc.) and you need the CI logs.

### Fix and re-release as the NEXT patch

**Do not reuse `vx.y.z`.** Cut `vx.y.(z+1)` (or next-minor if warranted) with the fix. Rationale:
- Tag immutability: `vx.y.z` is already recorded in git history and release cache
- Semver clarity: users and tooling should see a new version number when the bits change
- Audit trail: "v0.3.7 was cut but had no shipped binaries; v0.3.8 is the first release with <fix>" is cleaner than rewriting v0.3.7

Steps:

1. Cut a fix branch off dev, implement the fix, PR to dev, merge.
2. Re-run `/release` (it will bump to the next patch — e.g. `0.3.8` — automatically).
3. Step 1.5's pre-flight smoke will catch the same bug locally if the fix didn't actually fix it. Iterate until it passes before tagging.

### CHANGELOG note for the hotfix release

Include a line in the new release's CHANGELOG that references the broken prior version so users understand why there's no binary artifact under that tag:

```markdown
### Fixed

- **First release with working compiled binaries after vx.y.z's <bug>.** vx.y.z was tagged but its binary smoke test failed deterministically (see RUN_ID in CI history). The tag is preserved for history; this release (vx.y.(z+1)) is the first with shipped binaries. `install.sh` and Homebrew were never updated to vx.y.z, so users were not exposed to the broken state.
```

### What NOT to do

- **Do not force-push or rewrite the tag.** Once a tag exists, it's a public promise of that SHA. Deleting and re-creating to a different SHA is tag-spoofing and breaks any downstream that cached the original.
- **Do not skip this recovery path to "just push more binaries to the broken release".** The release exists with a specific commit SHA; uploading binaries built from a newer SHA creates binary/source drift that is hard to diagnose later.
- **Do not update the Homebrew formula before v0.3.(z+1) is fully shipped.** The formula should always point at a version with all 7 assets uploaded and `/test-release brew` passing.

## Important Rules

- NEVER force push
- **NEVER skip Step 1.5 (pre-flight compiled-binary smoke).** If the stack is a Bun/Node project with a build-binaries script, the `bun build --compile` smoke test runs before version bump, PR, or tag. Skipping it means every bundler regression or module-init crash only surfaces after the tag is pushed — by which point `releases/latest` is already 404-ing for every user. The ~30s cost is paid to keep the failure mode local.
- If Step 1.5 fails, **abort the release** and fix the underlying issue on a feature branch. Do not "just skip it" and hope CI doesn't repro the problem.
- NEVER skip the review step — always show the changelog before committing
- NEVER include "Co-Authored-By: Claude" or any AI attribution in the commit
- NEVER add emoji to changelog entries unless the user asks
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.8] - 2026-04-22

Hotfix for v0.3.7 — restore working compiled binaries. v0.3.7 was tagged but never shipped any working assets: two distinct bugs (Pi SDK's module-init `package.json` read, and Bun `--bytecode` producing broken output for this project's module graph) made every compiled archon binary crash at startup. The v0.3.7 GitHub Release was deleted immediately (the tag remains for history); v0.3.8 is the first release with working `archon-{darwin,linux}-{arm64,x64}` and `archon-windows-x64.exe` binaries since v0.3.6. Homebrew and `install.sh` were never updated to v0.3.7, so users were not exposed to the broken state.

### Fixed

- **Compiled archon binaries no longer crash at startup when the Pi provider is bundled.** `@mariozechner/pi-coding-agent/dist/config.js` runs `readFileSync(getPackageJsonPath(), 'utf-8')` at module top-level, which inside a compiled binary resolves to `dirname(process.execPath) + '/package.json'` — a path that doesn't exist next to `/usr/local/bin/archon`, making every archon command (including `archon version`) crash with ENOENT before it ran. The Pi SDK and all Pi-dependent helper modules are now dynamically imported inside `PiProvider.sendQuery()`; registering Pi and instantiating the provider no longer touches Pi's module-init side effects. A regression test (`provider-lazy-load.test.ts`) walks the same `registerCommunityProviders()` + `getAgentProvider('pi')` path the CLI and server take and asserts neither SDK package was resolved. Claude and Codex providers keep their static import style — their SDKs have no equivalent module-init side effect. Unblocks the v0.3.7 release binaries that could not ship because of this bug. (#1355)
- **Release binary compile no longer silently produces broken bytecode.** `scripts/build-binaries.sh` dropped the `--bytecode` flag: Bun 1.3.11's bytecode step failed with `Failed to generate bytecode for ./cli.js` against the 0.3.7 module graph and fell through to producing a binary that crashed at module instantiation with "Expected CommonJS module to have a function wrapper". Windows was already excluded; this removes the flag everywhere. Release parity preserved via `--minify`. (#1354)

## [0.3.7] - 2026-04-22

Pi community provider, home-scoped workflows/commands/scripts, worktree policy, Web UI approval-gate auto-resume, three-path env model, and a breaking change to Claude Code binary resolution for compiled binary users.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "archon",
"version": "0.3.7",
"version": "0.3.8",
"private": true,
"workspaces": [
"packages/*"
Expand Down
2 changes: 1 addition & 1 deletion packages/adapters/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/adapters",
"version": "0.3.7",
"version": "0.3.8",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/cli",
"version": "0.3.7",
"version": "0.3.8",
"type": "module",
"main": "./src/cli.ts",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/core",
"version": "0.3.7",
"version": "0.3.8",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/docs-web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/docs-web",
"version": "0.3.7",
"version": "0.3.8",
"private": true,
"scripts": {
"dev": "astro dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/git/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/git",
"version": "0.3.7",
"version": "0.3.8",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/isolation/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/isolation",
"version": "0.3.7",
"version": "0.3.8",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/paths/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/paths",
"version": "0.3.7",
"version": "0.3.8",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
4 changes: 2 additions & 2 deletions packages/providers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/providers",
"version": "0.3.7",
"version": "0.3.8",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand All @@ -18,7 +18,7 @@
"./registry": "./src/registry.ts"
},
"scripts": {
"test": "bun test src/claude/provider.test.ts && bun test src/codex/provider.test.ts && bun test src/registry.test.ts && bun test src/codex/binary-guard.test.ts && bun test src/codex/binary-resolver.test.ts && bun test src/codex/binary-resolver-dev.test.ts && bun test src/claude/binary-resolver.test.ts && bun test src/claude/binary-resolver-dev.test.ts && bun test src/community/pi/model-ref.test.ts && bun test src/community/pi/config.test.ts && bun test src/community/pi/event-bridge.test.ts && bun test src/community/pi/options-translator.test.ts && bun test src/community/pi/session-resolver.test.ts && bun test src/community/pi/provider.test.ts",
"test": "bun test src/claude/provider.test.ts && bun test src/codex/provider.test.ts && bun test src/registry.test.ts && bun test src/codex/binary-guard.test.ts && bun test src/codex/binary-resolver.test.ts && bun test src/codex/binary-resolver-dev.test.ts && bun test src/claude/binary-resolver.test.ts && bun test src/claude/binary-resolver-dev.test.ts && bun test src/community/pi/model-ref.test.ts && bun test src/community/pi/config.test.ts && bun test src/community/pi/event-bridge.test.ts && bun test src/community/pi/options-translator.test.ts && bun test src/community/pi/session-resolver.test.ts && bun test src/community/pi/provider.test.ts && bun test src/community/pi/provider-lazy-load.test.ts",
"type-check": "bun x tsc --noEmit"
},
"dependencies": {
Expand Down
57 changes: 57 additions & 0 deletions packages/providers/src/community/pi/provider-lazy-load.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Regression test: Pi SDK must not load at module-import time.
*
* Pi's `@mariozechner/pi-coding-agent/dist/config.js` runs
* `readFileSync(getPackageJsonPath(), 'utf-8')` at module top-level. Inside
* a compiled Archon binary `getPackageJsonPath()` resolves to
* `dirname(process.execPath) + '/package.json'`, which doesn't exist — so
* any static import chain from `@archon/providers` into the Pi SDK crashes
* archon at startup with ENOENT before any command runs (v0.3.7 symptom).
*
* Detection strategy: replace both Pi SDK packages with `mock.module`
* factories that flip a boolean the first time something resolves them.
* Walk the same registration path the CLI and server take and assert
* neither flag tipped. A throwing factory would abort the failing import
* before the `expect` calls run, producing a crash at resolution time with
* no assertion context — counters keep failures actionable.
*
* Runs in its own `bun test` invocation because Bun's `mock.module` is
* process-wide and would poison `provider.test.ts`, which installs benign
* stubs for the same modules (see CLAUDE.md on test isolation).
*/
import { expect, mock, test } from 'bun:test';

// Counter-based detection — see the file header for why not `throw`.
let piCodingAgentLoaded = false;
let piAiLoaded = false;

mock.module('@mariozechner/pi-coding-agent', () => {
piCodingAgentLoaded = true;
return {};
});
mock.module('@mariozechner/pi-ai', () => {
piAiLoaded = true;
return {};
});

test('registering and instantiating the Pi provider does not eagerly load the Pi SDK', async () => {
// Go through the same public entrypoint the CLI and server call.
// `registerCommunityProviders()` pulls in the full registration path
// (registry.ts → registration.ts → provider.ts → provider's helpers).
const { clearRegistry, getAgentProvider, registerCommunityProviders } =
await import('../../registry');

clearRegistry();
registerCommunityProviders();

const provider = getAgentProvider('pi');
expect(provider.getType()).toBe('pi');
expect(provider.getCapabilities()).toBeDefined();

// If either of these fails, someone reintroduced a static (non-type)
// `import { ... }` from a Pi SDK package somewhere in the module chain
// reachable from `registerCommunityProviders()`. Fix by moving that value
// import inside `PiProvider.sendQuery()`'s dynamic-import block.
expect(piCodingAgentLoaded).toBe(false);
expect(piAiLoaded).toBe(false);
});
Loading
Loading