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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 887ffe4e94..f1f781a886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. 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 5937d54658..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", @@ -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'; 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", 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