From 5df6d061eff9ea5b3bf8e4fa5ea683737a371126 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 15 May 2026 10:31:11 -0300 Subject: [PATCH 1/4] fix: expose bin/* in exports for Node 22+ compat [Story 124.13] @aiox-squads/core 5.2.2/5.2.3/5.2.4 broke under Node 22+ with ERR_PACKAGE_PATH_NOT_EXPORTED. The exports field introduced during Epic 124 only exposed ./resilience/* and ./installer/*, blocking the compat/aiox-core/bin/aiox-core.js shim from require()-ing the canonical CLI binaries. Fix: - Add "./bin/*": "./bin/*" pass-through pattern to exports. - Bump @aiox-squads/core 5.2.4 -> 5.2.5. - Bump compat aiox-core 5.2.4 -> 5.2.5 + dependency pin. Validated locally on Node 22.16 against packed tarballs: - aiox --version: 5.2.5 - aiox-core --version (legacy wrapper): 5.2.5 - require('@aiox-squads/core/bin/aiox.js'): loads CLI without ERR_PACKAGE_PATH_NOT_EXPORTED Closes #734 Co-Authored-By: Claude Opus 4.7 (1M context) --- compat/aiox-core/package.json | 4 ++-- package.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/compat/aiox-core/package.json b/compat/aiox-core/package.json index 2a932d64c5..085cb9359a 100644 --- a/compat/aiox-core/package.json +++ b/compat/aiox-core/package.json @@ -1,6 +1,6 @@ { "name": "aiox-core", - "version": "5.2.4", + "version": "5.2.5", "description": "Compatibility wrapper for @aiox-squads/core.", "license": "MIT", "bin": { @@ -15,7 +15,7 @@ "README.md" ], "dependencies": { - "@aiox-squads/core": "5.2.4" + "@aiox-squads/core": "5.2.5" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index 116fbc3867..0b97ff9b85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aiox-squads/core", - "version": "5.2.4", + "version": "5.2.5", "description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework", "bin": { "aiox": "bin/aiox.js", @@ -45,6 +45,7 @@ "exports": { "./resilience": "./.aiox-core/core/resilience/index.js", "./resilience/agent-immortality": "./.aiox-core/core/resilience/agent-immortality.js", + "./bin/*": "./bin/*", "./installer/aiox-core-installer": "./packages/installer/src/installer/aiox-core-installer.js", "./installer/enterprise-detector": "./packages/installer/src/enterprise/enterprise-detector.js", "./installer/enterprise-errors": "./packages/installer/src/enterprise/enterprise-errors.js", From 09b80b87d7aff27bc673927e8587a0726ee77999 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 15 May 2026 10:31:32 -0300 Subject: [PATCH 2/4] ci: add Node 22/24 exports regression guard [Story 124.13] The existing publish_legacy_aiox_core smoke uses npx --yes which resolves bin shims internally and does NOT trigger Node's package-exports gate. That gap let Issue #734 (ERR_PACKAGE_PATH_NOT_EXPORTED) slip into 3 consecutive releases (5.2.2/.3/.4) unnoticed by CI. Add smoke_test_exports job: - Matrix Node 20/22/24 - Runs after publish (depends on build + publish + publish_legacy_aiox_core) - Forces external require('@aiox-squads/core/bin/aiox.js') which DOES trigger the exports gate - notify job updated to surface failures This is a regression guard, not a replacement for the existing bin-shim smoke. Both paths are now validated. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/npm-publish.yml | 61 +++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e385546dc0..d1c8fb8d71 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -456,19 +456,74 @@ jobs: echo "❌ Smoke test timeout for ${PKG_SPEC}" exit 1 + smoke_test_exports: + # Issue #734 regression guard: validates that @aiox-squads/core exposes + # bin/* via package exports. The npx smoke above does NOT detect this + # because npm bin shims resolve internally without triggering Node's + # package-exports gate. This job forces require() resolution from an + # external context across Node 20/22/24 — the matrix that revealed + # ERR_PACKAGE_PATH_NOT_EXPORTED affected v5.2.2/.3/.4. + needs: [build, publish, publish_legacy_aiox_core] + if: ${{ needs.publish.result == 'success' || needs.publish_legacy_aiox_core.result == 'success' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: ['20', '22', '24'] + steps: + - name: Setup Node.js ${{ matrix.node }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + registry-url: ${{ env.NPM_REGISTRY }} + + - name: Wait for npm propagation + env: + CORE_SPEC: '@aiox-squads/core@${{ needs.build.outputs.version }}' + run: | + for i in 1 2 3 4 5 6; do + if npm view "${CORE_SPEC}" version > /dev/null 2>&1; then + echo "✅ ${CORE_SPEC} available on npm" + exit 0 + fi + echo "⏳ Waiting for ${CORE_SPEC} propagation... (attempt $i/6)" + sleep 15 + done + echo "❌ ${CORE_SPEC} not visible after 90s — cannot run exports smoke" + exit 1 + + - name: Force require() resolution to trigger exports gate + env: + CORE_SPEC: '@aiox-squads/core@${{ needs.build.outputs.version }}' + run: | + SMOKE_DIR=$(mktemp -d) + cd "${SMOKE_DIR}" + npm init -y >/dev/null 2>&1 + npm install "${CORE_SPEC}" 2>&1 | tail -3 + # External require() forces Node's package-exports gate. If bin/* + # is not declared in exports, this fails with ERR_PACKAGE_PATH_NOT_EXPORTED. + if node -e "require('@aiox-squads/core/bin/aiox.js')" 2>&1 | head -20; then + echo "✅ require('@aiox-squads/core/bin/aiox.js') succeeds on Node ${{ matrix.node }}" + else + echo "❌ REGRESSION: require() blocked by exports field on Node ${{ matrix.node }}" + echo "❌ Issue #734 has returned — check exports field in package.json" + exit 1 + fi + rm -rf "${SMOKE_DIR}" + notify: - needs: [build, publish, publish_workspace_packages, publish_legacy_aiox_core] + needs: [build, publish, publish_workspace_packages, publish_legacy_aiox_core, smoke_test_exports] if: always() runs-on: ubuntu-latest steps: - name: Notify completion run: | - if [ "${{ needs.publish.result }}" = "failure" ] || [ "${{ needs.publish_workspace_packages.result }}" = "failure" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "failure" ]; then + if [ "${{ needs.publish.result }}" = "failure" ] || [ "${{ needs.publish_workspace_packages.result }}" = "failure" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "failure" ] || [ "${{ needs.smoke_test_exports.result }}" = "failure" ]; then echo "❌ Publishing failed" exit 1 fi - if [ "${{ needs.publish.result }}" = "cancelled" ] || [ "${{ needs.publish_workspace_packages.result }}" = "cancelled" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "cancelled" ]; then + if [ "${{ needs.publish.result }}" = "cancelled" ] || [ "${{ needs.publish_workspace_packages.result }}" = "cancelled" ] || [ "${{ needs.publish_legacy_aiox_core.result }}" = "cancelled" ] || [ "${{ needs.smoke_test_exports.result }}" = "cancelled" ]; then echo "❌ Publishing was cancelled" exit 1 fi From 4aa7d5391e4aab79a1316fc062c19f532bbed84d Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 15 May 2026 10:31:51 -0300 Subject: [PATCH 3/4] docs(stories): add STORY-124.13 [Story 124.13] Story documenting Issue #734 fix: - Root cause analysis (Story 124.8 wrapper + restrictive exports field) - Acceptance criteria - Tasks (impl + CI hardening + publish + deprecation) - File list - Dev notes including CI gap analysis (why npx smoke missed it 3 times) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../STORY-124.13-fix-issue-734-exports-bin.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md diff --git a/docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md b/docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md new file mode 100644 index 0000000000..f68b769ce5 --- /dev/null +++ b/docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md @@ -0,0 +1,80 @@ +# Story 124.13: Fix Issue #734 — Expose `bin/*` in Package Exports + +## Status + +Ready for Review + +## Story + +As an AIOX user installing `aiox-core@5.2.x` or `@aiox-squads/core@5.2.x` on Node.js 22+, +I want the CLI to execute without `ERR_PACKAGE_PATH_NOT_EXPORTED`, +so that every published patch release after the `@aiox-squads` scope migration remains usable on supported Node versions. + +## Acceptance Criteria + +- [x] Root `package.json` `exports` field exposes `./bin/*` so external `require('@aiox-squads/core/bin/aiox.js')` resolves under Node ≥22. +- [x] Compat wrapper (`compat/aiox-core/package.json`) bumped to 5.2.5 with matching `@aiox-squads/core` dependency. +- [x] Local smoke validates `aiox --version`, `aiox-core --version`, and `require()` direct on Node 22.16 against the locally packed tarballs. +- [x] CI workflow has a `smoke_test_exports` job (matrix Node 20/22/24) that runs after publish and forces the `require()` resolution path, catching future regressions of this exact bug. +- [ ] After publish, `npm i -g aiox-core@5.2.5 && aiox --version` works on Node 22, 24, and 25. + +## Tasks + +- [x] Add `"./bin/*": "./bin/*"` to `exports` in `package.json`. +- [x] Bump `@aiox-squads/core` 5.2.4 → 5.2.5. +- [x] Bump `compat/aiox-core` 5.2.4 → 5.2.5 (version + dependency). +- [x] Add `smoke_test_exports` job to `.github/workflows/npm-publish.yml` with Node 20/22/24 matrix, using external `require()` to force the package-exports gate. +- [x] Update `notify` job to fail if `smoke_test_exports` fails. +- [x] Local smoke validated (Node 22.16): `require('@aiox-squads/core/bin/aiox.js')` loads CLI banner instead of throwing `ERR_PACKAGE_PATH_NOT_EXPORTED`. +- [ ] Push branch via @devops + open PR linking #734 and Epic 124. +- [ ] CodeRabbit review pass. +- [ ] @qa final review. +- [ ] Merge + tag `v5.2.5`. +- [ ] @devops deprecates 5.2.2, 5.2.3, 5.2.4 on npm (both `aiox-core` and `@aiox-squads/core`) with message pointing to 5.2.5. +- [ ] @qa runs post-publish global install smoke on Node 22/24/25 and closes Issue #734. + +## File List + +- `package.json` +- `compat/aiox-core/package.json` +- `.github/workflows/npm-publish.yml` +- `docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.9-fix-issue-734-exports-bin.md` + +## Dev Notes + +### Root Cause + +Story 124.8 introduced the `compat/aiox-core` wrapper that delegates to `@aiox-squads/core` via: + +```js +require(`@aiox-squads/core/bin/${targetBin}`); +``` + +The root `package.json` `exports` field (introduced earlier in Epic 124) restricts subpath access to `./resilience/*`, `./installer/*`, and `./package.json` only. Node.js 22+ enforces the package-exports spec strictly: any subpath not declared in `exports` is rejected with `ERR_PACKAGE_PATH_NOT_EXPORTED`, even if the file physically exists. + +This bug affected three consecutive published versions (5.2.2, 5.2.3, 5.2.4) because the existing CI smoke (`npx --yes aiox-core --version`) uses npm bin shims which resolve internally without triggering the exports gate. Only external `require()` (or `npm i -g` followed by direct shell invocation that re-imports the package from outside the package boundary) reveals the bug. + +### Fix Strategy + +Single-pattern entry `"./bin/*": "./bin/*"` (pass-through, no extension transformation) added before the `./installer/*` entries. The pattern is pass-through because the existing wrapper passes `.js` extensions already (e.g. `bin/aiox.js`); transforming with `.js` suffix would yield invalid `bin/aiox.js.js`. + +### CI Hardening (Regression Guard) + +New job `smoke_test_exports` runs after publish and: +1. Sets up Node from a matrix of `['20', '22', '24']`. +2. Waits for npm propagation of `@aiox-squads/core@${version}`. +3. Installs the published package in a fresh tempdir. +4. Executes `node -e "require('@aiox-squads/core/bin/aiox.js')"` — this forces external resolution that triggers the package-exports gate, catching this exact bug class. + +The existing `Smoke test legacy npx` step is preserved (it validates the bin shim path); the new job complements it by covering the external-require path that escaped detection three times. + +### Related Stories / Issues + +- Issue #734 — bug report (2026-05-14). +- Story 124.8 — introduced the wrapper that triggered the issue via internal `require()`. +- Story 124.3 — published `@aiox-squads/core` with the restrictive `exports` field. + +### Risks + +- Pattern `./bin/*` exposes any future file added under `bin/`. Current contents: 5 declared binaries + `aiox-init.js`, `aiox-ids.js` + `modules/`, `utils/` (already part of CLI runtime). No sensitive files in `bin/`. +- Deprecation of 5.2.2/.3/.4 happens post-merge via `npm deprecate` and requires @devops. Pinned consumers must read the deprecation message. From be47d900324fd6d7c0d0cf2c65d133802cc3e66a Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 15 May 2026 12:25:53 -0300 Subject: [PATCH 4/4] fix(ci,docs): address CodeRabbit review [Story 124.13] - ci: add `set -o pipefail` to smoke_test_exports so node's non-zero exit from require() failure isn't masked by `head -20`. Without this, the regression guard silently passes on any future ERR_PACKAGE_PATH_ NOT_EXPORTED, defeating its purpose. - docs: correct File List self-reference in STORY-124.13 (was pointing to the old 124.9 filename used during draft). CodeRabbit findings: - Major (workflow L506): pipefail missing - Minor (story L41): stale filename reference Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/npm-publish.yml | 2 ++ .../STORY-124.13-fix-issue-734-exports-bin.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index d1c8fb8d71..50df755762 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -496,12 +496,14 @@ jobs: env: CORE_SPEC: '@aiox-squads/core@${{ needs.build.outputs.version }}' run: | + set -o pipefail SMOKE_DIR=$(mktemp -d) cd "${SMOKE_DIR}" npm init -y >/dev/null 2>&1 npm install "${CORE_SPEC}" 2>&1 | tail -3 # External require() forces Node's package-exports gate. If bin/* # is not declared in exports, this fails with ERR_PACKAGE_PATH_NOT_EXPORTED. + # set -o pipefail above ensures node's exit status survives the head pipe. if node -e "require('@aiox-squads/core/bin/aiox.js')" 2>&1 | head -20; then echo "✅ require('@aiox-squads/core/bin/aiox.js') succeeds on Node ${{ matrix.node }}" else diff --git a/docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md b/docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md index f68b769ce5..b95b31f060 100644 --- a/docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md +++ b/docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md @@ -38,7 +38,7 @@ so that every published patch release after the `@aiox-squads` scope migration r - `package.json` - `compat/aiox-core/package.json` - `.github/workflows/npm-publish.yml` -- `docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.9-fix-issue-734-exports-bin.md` +- `docs/stories/epic-124-aiox-squads-scope-migration/STORY-124.13-fix-issue-734-exports-bin.md` ## Dev Notes