## Why
#2386 added `installSdkIfNeeded()` to build @gsd-build/sdk from bundled
source and `npm install -g .`, because the npm-published @gsd-build/sdk
is intentionally frozen and version-mismatched with get-shit-done-cc.
But every failure path in that function was warning-only — including
the final `which gsd-sdk` verification. When npm's global bin is off a
user's PATH (common on macOS), the installer printed a yellow warning
then exited 0. Users saw "install complete" and then every `/gsd-*`
command crashed with `command not found: gsd-sdk` (the #2439 symptom).
No CI job executed the install path, so this class of regression could
ship undetected — existing "install" tests only read bin/install.js as
a string.
## What changed
**bin/install.js — installSdkIfNeeded() is now transactional**
- All build/install failures exit non-zero (not just warn).
- Post-install `which gsd-sdk` check is fatal: if the binary landed
globally but is off PATH, we exit 1 with a red banner showing the
resolved npm bin dir, the user's shell, the target rc file, and the
exact `export PATH=…` line to add.
- Escape hatch: `GSD_ALLOW_OFF_PATH=1` downgrades off-PATH to exit 2
for users with intentionally restricted PATH who will wire up the
binary manually.
- Resolver uses POSIX `command -v` via `sh -c` (replaces `which`) so
behavior is consistent across sh/bash/zsh/fish.
- Factored `resolveGsdSdk()`, `detectShellRc()`, `emitSdkFatal()`.
**.github/workflows/install-smoke.yml (new)**
- Executes the real install path: `npm pack` → `npm install -g <tgz>`
→ run installer non-interactively → `command -v gsd-sdk` → run
`gsd-sdk --version`.
- PRs: path-filtered to installer-adjacent files, ubuntu + Node 22 only.
- main/release branches: full matrix (ubuntu+macos × Node 22+24).
- Reusable via workflow_call with `ref` input for release gating.
**.github/workflows/release.yml — pre-publish gate**
- New `install-smoke-rc` and `install-smoke-finalize` jobs invoke the
reusable workflow against the release branch. `rc` and `finalize`
now `needs: [validate-version, install-smoke-*]`, so a broken SDK
install blocks `npm publish`.
## Test plan
- Local full suite: 4154/4154 pass
- install-smoke.yml will self-validate on this PR (ubuntu+Node22 only)
Addresses root cause of #2439 (the per-command pre-flight in #2440 is
the complementary defensive layer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>