From 0de826c311c79807cc1965dea83a12542972215e Mon Sep 17 00:00:00 2001 From: Rasmus Widing <152263317+Wirasm@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:58:01 +0300 Subject: [PATCH 1/4] fix(build): drop --bytecode from compiled-binary build (#1354) Bun 1.3.11's bytecode generation produces broken output for our module graph ("TypeError: Expected CommonJS module to have a function wrapper" at runtime, reproduces natively on darwin-arm64 too). The compile already fails with "Failed to generate bytecode for ./cli.js" and proceeds without it, but the resulting binary still crashes at module instantiation. Only --minify remains. Binary size is unchanged in practice (bytecode was being skipped after the error anyway), but now the build succeeds cleanly and the binary runs. Surfaced while building the v0.3.7 release. --- scripts/build-binaries.sh | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index 8b9cd086b1..43ba159626 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -75,17 +75,13 @@ for target_pair in "${TARGETS[@]}"; do IFS=':' read -r target outfile <<< "$target_pair" echo "Building $target → $outfile" - # --bytecode excluded for Windows cross-compile (inconsistent Bun support) - BYTECODE_FLAG="" - if [[ "$target" != *windows* ]]; then - BYTECODE_FLAG="--bytecode" - fi - - # Always --minify to match release parity + # --bytecode disabled: Bun 1.3.11 produces broken bytecode for our module graph + # (likely triggered by @mariozechner/pi-coding-agent's CJS/ESM interop shape) — + # "TypeError: Expected CommonJS module to have a function wrapper" at runtime. + # Always --minify to match release parity. bun build \ --compile \ --minify \ - $BYTECODE_FLAG \ --target="$target" \ --outfile="$outfile" \ packages/cli/src/cli.ts From 4397ed14faf230456910b60438ca209d8f0bb739 Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Wed, 22 Apr 2026 14:11:37 +0300 Subject: [PATCH 2/4] chore(release-skill): add pre-flight binary smoke + deterministic-CI-failure recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps caused the v0.3.7 release to ship an empty GitHub Release and break install.sh for all users for ~30 min. Both are now handled in the skill: 1. Step 1.5 — mandatory pre-flight `bun build --compile` smoke test before any user-visible step (version bump, PR, tag). Compiles the native target only (~15-30s), runs `archon version`, greps the output for runtime-error markers, aborts if anything fails. This would have caught both v0.3.7 bugs (the --bytecode flag producing broken bytecode, and the Pi SDK's readFileSync of a path only present in node_modules) before the tag was ever pushed. Escape hatch: fix the underlying bug on a feature branch and re-run /release — no workaround to "just skip". 2. Step 10 now detects deterministic CI failure (two reruns with same error) and jumps to a new "Recovery: deterministic release CI failure" section instead of looping until timeout. Recovery documents: - Delete the empty GitHub Release (keep the tag — immutability) so releases/latest falls back to the prior working version and install.sh stops 404-ing within seconds. - Diagnose via `gh run view --log-failed`, with a cheat-sheet of common failure modes (bundler bytecode, module-init crashes, Windows timeouts). - Cut the NEXT patch version with the fix — never reuse the broken tag, never force-push, never upload binaries built from a different SHA onto the original release. - CHANGELOG note template that references the broken prior version so the audit trail is clear for anyone inspecting tags later. Also added two Important Rules: - Never skip Step 1.5. The ~30s cost keeps failure modes local. - If Step 1.5 fails, abort the release. No "skip and hope CI passes." Skill grows from 389 → 548 lines. No changes to Steps 2–9, 11, 12. --- .claude/skills/release/SKILL.md | 167 +++++++++++++++++++++++++++++++- 1 file changed, 163 insertions(+), 4 deletions(-) diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 57e7c4ba3a..4f90f70978 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -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: @@ -211,6 +266,7 @@ 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 @@ -218,14 +274,45 @@ for i in {1..30}; do 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 ``` @@ -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 ` 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 --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 " 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 .** 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 From 5294fcdc82d82e15acc9fb521052006720cb361d Mon Sep 17 00:00:00 2001 From: Rasmus Widing <152263317+Wirasm@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:13:29 +0300 Subject: [PATCH 3/4] fix(providers/pi): lazy-load Pi SDK to unbreak compiled archon binary (#1355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(providers/pi): lazy-load Pi SDK to unbreak compiled archon binary 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 next to `/usr/local/bin/archon` — so archon crashes with ENOENT at startup before any command runs. v0.3.7's release binary build appeared to compile clean (CI fell over first on an unrelated --bytecode issue) but even the fixed-bytecode binary fails the same way locally. Convert all Pi SDK value imports and Pi-dependent helper imports to dynamic imports inside `PiProvider.sendQuery()`. Type-only imports stay static (erased by TS). Effect: registering the Pi provider and creating an instance no longer loads Pi's SDK — load happens only when a Pi workflow actually runs. Claude and Codex providers keep their static import style (their SDKs have no module-init side effects that fail in a binary); see the file header comment in provider.ts for why Pi is the deliberate outlier. Class constructors (AuthStorage, ModelRegistry, SettingsManager) are accessed via `piCodingAgent.X` rather than destructured to keep eslint's naming-convention rule happy without a disable. Add a regression test (provider-lazy-load.test.ts) that mocks @mariozechner/pi-coding-agent and @mariozechner/pi-ai, walks the same registerCommunityProviders() → getAgentProvider('pi') path the CLI and server take, and asserts neither SDK module was loaded. Runs in its own `bun test` invocation (mock.module is process-wide). Verified locally: `bun build --compile --minify --target=bun-darwin-arm64` produces a binary whose `archon version` runs cleanly and reports Build: binary, where previously every command crashed at boot. * address review: changelog, docstring, type tighten, Theme type-only import From the multi-agent review on #1355: - Add CHANGELOG.md entries under [Unreleased] ### Fixed for this PR's Pi lazy-load fix and the --bytecode removal from #1354 — both user-visible fixes (compiled binary unusable without them). - Rewrite the test-header docstring in provider-lazy-load.test.ts to describe counter-based detection instead of "mocks throw" (contradicted the actual code directly below it). - Tighten `lookupPiModel`'s first parameter from `unknown` to a local `GetModelFn` alias, moving the runtime-string cast to the single call site with a pointer to the docblock. - Update the class docblock on `PiProvider` — "v1 capabilities are all false" was stale; PI_CAPABILITIES has seven flags set to true. - `ui-context-stub.ts` imports `Theme` as a non-type value even though every usage is in a type position. Fold it into the existing `import type {…}` block so a future runtime-class `Theme` in Pi can't reintroduce an eager module load via this sibling. No behavior change. Type-check, lint, format, tests, and a local darwin-arm64 compile + version smoke all clean. --- CHANGELOG.md | 5 + packages/providers/package.json | 2 +- .../community/pi/provider-lazy-load.test.ts | 57 ++++++++++++ .../providers/src/community/pi/provider.ts | 91 +++++++++++++------ .../src/community/pi/ui-context-stub.ts | 2 +- 5 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 packages/providers/src/community/pi/provider-lazy-load.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 887ffe4e94..262a6de436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### 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. diff --git a/packages/providers/package.json b/packages/providers/package.json index 5937d54658..25227c7a7b 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -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": { diff --git a/packages/providers/src/community/pi/provider-lazy-load.test.ts b/packages/providers/src/community/pi/provider-lazy-load.test.ts new file mode 100644 index 0000000000..04ee0d1919 --- /dev/null +++ b/packages/providers/src/community/pi/provider-lazy-load.test.ts @@ -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); +}); diff --git a/packages/providers/src/community/pi/provider.ts b/packages/providers/src/community/pi/provider.ts index f0171df202..e4b6804762 100644 --- a/packages/providers/src/community/pi/provider.ts +++ b/packages/providers/src/community/pi/provider.ts @@ -1,11 +1,5 @@ import { createLogger } from '@archon/paths'; -import { - AuthStorage, - ModelRegistry, - SettingsManager, - createAgentSession, -} from '@mariozechner/pi-coding-agent'; -import { getModel, type Api, type Model } from '@mariozechner/pi-ai'; +import type { Api, Model } from '@mariozechner/pi-ai'; import type { IAgentProvider, @@ -16,12 +10,20 @@ import type { import { PI_CAPABILITIES } from './capabilities'; import { parsePiConfig } from './config'; -import { bridgeSession } from './event-bridge'; import { parsePiModelRef } from './model-ref'; -import { resolvePiSkills, resolvePiThinkingLevel, resolvePiTools } from './options-translator'; -import { createNoopResourceLoader } from './resource-loader'; -import { resolvePiSession } from './session-resolver'; -import { createArchonUIBridge, createArchonUIContext } from './ui-context-stub'; + +// IMPORTANT: Do NOT add static `import { ... } from '@mariozechner/*'` here, +// and do NOT statically import sibling modules that themselves import runtime +// values from Pi (options-translator, resource-loader, session-resolver, +// ui-context-stub, event-bridge). Pi's `@mariozechner/pi-coding-agent/dist/config.js` +// runs `readFileSync(getPackageJsonPath(), "utf-8")` at module load; inside a +// compiled Archon binary `getPackageJsonPath()` resolves to +// `dirname(process.execPath) + "/package.json"` — a path that doesn't exist — +// and archon crashes at startup before any command runs (v0.3.7 symptom). +// +// All Pi SDK value bindings and Pi-dependent helper modules are dynamically +// imported inside `sendQuery()` below, which runs only when a Pi workflow is +// actually invoked. Type-only imports above are fine — TS erases them. /** * Map Pi provider id → env var name used by pi-ai's getEnvApiKey(). @@ -55,14 +57,18 @@ function getLog(): ReturnType { * Typed wrapper around Pi's `getModel` for a runtime-string provider/model * pair. Pi's getModel signature constrains `TModelId` to * `keyof MODELS[TProvider]`, which isn't knowable from a runtime string — - * the cast through `unknown` is the only way to bypass it. Isolating that - * escape hatch behind one searchable name keeps it auditable. + * the local `GetModelFn` alias is the narrowest shape that still lets us + * bypass that constraint. Isolating the escape hatch behind one searchable + * name keeps it auditable. Takes `getModel` as a parameter because the Pi + * SDK is loaded dynamically (see the header comment on this file for why). */ -function lookupPiModel(provider: string, modelId: string): Model | undefined { - return (getModel as unknown as (p: string, m: string) => Model | undefined)( - provider, - modelId - ); +type GetModelFn = (provider: string, modelId: string) => Model | undefined; +function lookupPiModel( + getModel: GetModelFn, + provider: string, + modelId: string +): Model | undefined { + return getModel(provider, modelId); } /** @@ -95,11 +101,12 @@ ${JSON.stringify(schema, null, 2)}`; * (no reuse) with in-memory auth/session/settings, so the server never * touches `~/.pi/` and concurrent calls don't collide. * - * v1 capabilities are all false (see `capabilities.ts`): sessionResume, - * thinkingControl, skills, mcp, etc. map to Pi features but require - * intentional wiring before they can be declared. Under-declaring is - * honest; the dag-executor emits warnings for any nodeConfig field not - * supported. + * Capabilities (see `capabilities.ts` for the canonical list): Pi declares + * `sessionResume`, `skills`, `toolRestrictions`, `structuredOutput`, + * `envInjection`, `effortControl`, and `thinkingControl`. Features Pi does + * not currently support through Archon (`mcp`, `hooks`, `agents`, + * `costControl`, `fallbackModel`, `sandbox`) stay off; the dag-executor + * surfaces a warning for any unsupported nodeConfig field. */ export class PiProvider implements IAgentProvider { async *sendQuery( @@ -108,6 +115,33 @@ export class PiProvider implements IAgentProvider { resumeSessionId?: string, requestOptions?: SendQueryOptions ): AsyncGenerator { + // Lazy-load Pi SDK and all Pi-dependent helper modules here. Must not move + // these imports to module scope — see the header comment for the failure + // mode (archon compiled binary crashes at startup when Pi's config.js + // reads a package.json that doesn't exist next to the executable). + // + // Class constructors (AuthStorage, ModelRegistry, SettingsManager) are + // accessed via `piCodingAgent.X` rather than destructured, because + // destructured PascalCase bindings trip eslint's naming-convention rule. + const [ + piCodingAgent, + piAi, + { bridgeSession }, + { resolvePiSkills, resolvePiThinkingLevel, resolvePiTools }, + { createNoopResourceLoader }, + { resolvePiSession }, + { createArchonUIBridge, createArchonUIContext }, + ] = await Promise.all([ + import('@mariozechner/pi-coding-agent'), + import('@mariozechner/pi-ai'), + import('./event-bridge'), + import('./options-translator'), + import('./resource-loader'), + import('./session-resolver'), + import('./ui-context-stub'), + ]); + const { createAgentSession } = piCodingAgent; + const assistantConfig = requestOptions?.assistantConfig ?? {}; const piConfig = parsePiConfig(assistantConfig); @@ -146,7 +180,8 @@ export class PiProvider implements IAgentProvider { // 2. Look up the Model via Pi's static catalog. `lookupPiModel` returns // undefined when not found; we guard explicitly below. - const model = lookupPiModel(parsed.provider, parsed.modelId); + // Cast to the runtime-string-friendly shape — see `lookupPiModel`'s docblock. + const model = lookupPiModel(piAi.getModel as GetModelFn, parsed.provider, parsed.modelId); if (!model) { throw new Error( `Pi model not found: provider='${parsed.provider}' model='${parsed.modelId}'. ` + @@ -174,7 +209,7 @@ export class PiProvider implements IAgentProvider { // OAuth refresh note: Pi refreshes expired access tokens against the // provider's OAuth server and rewrites ~/.pi/agent/auth.json under a // file lock (same mechanism pi CLI uses — safe for concurrent access). - const authStorage = AuthStorage.create(); + const authStorage = piCodingAgent.AuthStorage.create(); const envVarName = PI_PROVIDER_ENV_VARS[parsed.provider]; const envOverride = envVarName @@ -265,8 +300,8 @@ export class PiProvider implements IAgentProvider { // when piConfig.enableExtensions is true — Pi's community extension // ecosystem (tools + lifecycle hooks from ~/.pi/agent/extensions/ and // packages installed via `pi install npm:`). - const modelRegistry = ModelRegistry.inMemory(authStorage); - const settingsManager = SettingsManager.inMemory(); + const modelRegistry = piCodingAgent.ModelRegistry.inMemory(authStorage); + const settingsManager = piCodingAgent.SettingsManager.inMemory(); // Default ON: extensions (community packages like @plannotator/pi-extension // or your own local ones) are a core reason users run Pi. Opt out with // `assistants.pi.enableExtensions: false` (or `interactive: false`) in diff --git a/packages/providers/src/community/pi/ui-context-stub.ts b/packages/providers/src/community/pi/ui-context-stub.ts index 99f18c63c5..70917a0068 100644 --- a/packages/providers/src/community/pi/ui-context-stub.ts +++ b/packages/providers/src/community/pi/ui-context-stub.ts @@ -3,8 +3,8 @@ import type { ExtensionUIDialogOptions, ExtensionWidgetOptions, TerminalInputHandler, + Theme, } from '@mariozechner/pi-coding-agent'; -import { Theme } from '@mariozechner/pi-coding-agent'; import type { MessageChunk } from '../../types'; From e646cd428274d63b0260d10fe3b216e288ffaa83 Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Wed, 22 Apr 2026 14:16:33 +0300 Subject: [PATCH 4/4] Release 0.3.8 --- CHANGELOG.md | 4 ++++ package.json | 2 +- packages/adapters/package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/docs-web/package.json | 2 +- packages/git/package.json | 2 +- packages/isolation/package.json | 2 +- packages/paths/package.json | 2 +- packages/providers/package.json | 2 +- packages/server/package.json | 2 +- packages/web/package.json | 2 +- packages/workflows/package.json | 2 +- 13 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 262a6de436..f1f781a886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ 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) diff --git a/package.json b/package.json index 42cc8a3714..9153069c82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "archon", - "version": "0.3.7", + "version": "0.3.8", "private": true, "workspaces": [ "packages/*" diff --git a/packages/adapters/package.json b/packages/adapters/package.json index 19dbeedd9d..de465130f6 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -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", diff --git a/packages/cli/package.json b/packages/cli/package.json index 1f00737409..f74cbfd087 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@archon/cli", - "version": "0.3.7", + "version": "0.3.8", "type": "module", "main": "./src/cli.ts", "bin": { diff --git a/packages/core/package.json b/packages/core/package.json index 31820f43f7..d024002644 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/docs-web/package.json b/packages/docs-web/package.json index 38ab7a44a7..a77bad6eda 100644 --- a/packages/docs-web/package.json +++ b/packages/docs-web/package.json @@ -1,6 +1,6 @@ { "name": "@archon/docs-web", - "version": "0.3.7", + "version": "0.3.8", "private": true, "scripts": { "dev": "astro dev", diff --git a/packages/git/package.json b/packages/git/package.json index dbcee1820b..66357d975d 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -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", diff --git a/packages/isolation/package.json b/packages/isolation/package.json index c145585304..fe000efdb7 100644 --- a/packages/isolation/package.json +++ b/packages/isolation/package.json @@ -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", diff --git a/packages/paths/package.json b/packages/paths/package.json index 4fe6345982..5df8eb3ace 100644 --- a/packages/paths/package.json +++ b/packages/paths/package.json @@ -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", diff --git a/packages/providers/package.json b/packages/providers/package.json index 25227c7a7b..88ecb3ddce 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -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", diff --git a/packages/server/package.json b/packages/server/package.json index 4c99e4f88f..7615d6399c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@archon/server", - "version": "0.3.7", + "version": "0.3.8", "type": "module", "main": "./src/index.ts", "scripts": { diff --git a/packages/web/package.json b/packages/web/package.json index 2ff34166ff..a132d517a2 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@archon/web", - "version": "0.3.7", + "version": "0.3.8", "private": true, "type": "module", "scripts": { diff --git a/packages/workflows/package.json b/packages/workflows/package.json index 18da91fdc8..ccaa6eaf57 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -1,6 +1,6 @@ { "name": "@archon/workflows", - "version": "0.3.7", + "version": "0.3.8", "type": "module", "exports": { "./schemas/*": "./src/schemas/*.ts",