* fix(sdk): decouple SDK from build-from-source install path, close#2441 and #2453
Ship sdk/dist prebuilt in the tarball and replace the npm-install-g
sub-install with a parent-package bin shim (bin/gsd-sdk.js). npm chmods
bin entries from a packed tarball correctly, eliminating the mode-644
failure (#2453) and the full class of NPM_CONFIG_PREFIX/ignore-scripts/
corepack/air-gapped failure modes that caused #2439 and #2441.
Changes:
- sdk/package.json: prepublishOnly runs `rm -rf dist && tsc && chmod +x
dist/cli.js` (stale-build guard + execute-bit fix at publish time)
- package.json: add "gsd-sdk": "bin/gsd-sdk.js" bin entry; add sdk/dist
to files so the prebuilt CLI ships in the tarball
- bin/gsd-sdk.js: new back-compat shim — resolves sdk/dist/cli.js relative
to the package root and delegates via `node`, so all existing PATH call
sites (slash commands, agents, hooks) continue to work unchanged (S1 shim)
- bin/install.js: replace installSdkIfNeeded() build-from-source + global-
install dance with a dist-verify + chmod-in-place guard; delete
resolveGsdSdk(), detectShellRc(), emitSdkFatal() helpers now unused
- .github/workflows/install-smoke.yml: add smoke-unpacked job that strips
execute bit from sdk/dist/cli.js before install to reproduce the exact
#2453 failure mode
- tests/bug-2441-sdk-decouple.test.cjs: new regression tests asserting all
invariants (no npm install -g from sdk/, shim exists, sdk/dist in files,
prepublishOnly has rm -rf + chmod)
- tests/bugs-1656-1657.test.cjs: update stale assertions that required
build-from-source behavior (now asserts new prebuilt-dist invariants)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(release): bump to 1.38.2, wire release.yml to build SDK dist
- Bump version 1.38.1 -> 1.38.2 for the #2441/#2453 fix shipped in 0f6903d.
- Add `build:sdk` script (`cd sdk && npm ci && npm run build`).
- `prepublishOnly` now runs hooks + SDK builds as a safety net.
- release.yml (rc + finalize): build SDK dist before `npm publish` so the
published tarball always ships fresh `sdk/dist/` (kept gitignored).
- CHANGELOG: document 1.38.2 entry and `--sdk` flag semantics change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: build SDK dist before tests and smoke jobs
sdk/dist/ is gitignored (built fresh at publish time via release.yml),
but both the test suite and install-smoke jobs run `bin/install.js`
or `npm pack` against the checked-out tree where dist doesn't exist yet.
- test.yml: `npm run build:sdk` before `npm run test:coverage`, so tests
that spawn `bin/install.js` don't hit `installSdkIfNeeded()`'s fatal
missing-dist check.
- install-smoke.yml (both smoke and smoke-unpacked): build SDK before
pack/chmod so the published tarball contains dist and the unpacked
install has a file to strip exec-bit from.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sdk): lift SDK runtime deps to parent so tarball install can resolve them
The SDK's runtime deps (ws, @anthropic-ai/claude-agent-sdk) live in
sdk/package.json, but sdk/node_modules is NOT shipped in the parent
tarball — only sdk/dist, sdk/src, sdk/prompts, and sdk/package.json are.
When a user runs `npm install -g get-shit-done-cc`, npm installs the
parent's node_modules but never runs `npm install` inside the nested
sdk/ directory.
Result: `node sdk/dist/cli.js` fails with ERR_MODULE_NOT_FOUND for 'ws'.
The smoke tarball job caught this; the unpacked variant masked it
because `npm install -g <dir>` copies the entire workspace including
sdk/node_modules (left over from `npm run build:sdk`).
Fix: declare the same deps in the parent package.json so they land in
<pkg>/node_modules, which Node's resolution walks up to from
<pkg>/sdk/dist/cli.js. Keep them declared in sdk/package.json too so
the SDK remains a self-contained package for standalone dev.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(lockfile): regenerate package-lock.json cleanly
The previous `npm install` run left the lockfile internally inconsistent
(resolved esbuild@0.27.7 referenced but not fully written), causing
`npm ci` to fail in CI with "Missing from lock file" errors.
Clean regen via rm + npm install fixes all three failed jobs
(test, smoke, smoke-unpacked), which were all hitting the same
`npm ci` sync check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(deps): remove unused esbuild + vitest from root devDependencies
Both were declared but never imported anywhere in the root package
(confirmed via grep of bin/, scripts/, tests/). They lived in sdk/
already, which is the only place they're actually used.
The transitive tree they pulled in (vitest → vite → esbuild 0.28 →
@esbuild/openharmony-arm64) was the root of the CI npm ci failures:
the openharmony platform package's `optional: true` flag was not being
applied correctly by npm 10 on Linux runners, causing EBADPLATFORM.
After removal: 800+ transitive packages → 155. Lockfile regenerated
cleanly. All 4170 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sdk): pretest:coverage builds sdk; tighten shim test assertions
Add "pretest:coverage": "npm run build:sdk" so npm run test:coverage
works in clean checkouts where sdk/dist/ hasn't been built yet.
Tighten the two loose shim assertions in bug-2441-sdk-decouple.test.cjs:
- forwards-to test now asserts path.resolve() is called with the
'sdk','dist','cli.js' path segments, not just substring presence
- node-invocation test now asserts spawnSync(process.execPath, [...])
pattern, ruling out matches in comments or the shebang line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address PR review — pretest:coverage + tighten shim tests
Review feedback from trek-e on PR 2457:
1. pretest:coverage + pretest hooks now run `npm run build:sdk` so
`npm run test[:coverage]` in a clean checkout produces the required
sdk/dist/ artifacts before running the installer-dependent tests.
CI already does this explicitly; local contributors benefit.
2. Shim tests in bug-2441-sdk-decouple.test.cjs tightened from loose
substring matches (which would pass on comments/shebangs alone) to
regex assertions on the actual path.resolve call, spawnSync with
process.execPath, process.argv.slice(2), and process.exit pattern.
These now provide real regression protection for #2453-class bugs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: correct CHANGELOG entry and add [1.38.2] reference link
Two issues in the 1.38.2 CHANGELOG entry:
- installSdkIfNeeded() was described as deleted but it still exists in
bin/install.js (repurposed to verify sdk/dist/cli.js and fix execute bit).
Corrected the description to say 'repurposes' rather than 'deletes'.
- The reference-link block at the bottom of the file was missing a [1.38.2]
compare URL and [Unreleased] still pointed to v1.37.1...HEAD. Added the
[1.38.2] link and updated [Unreleased] to compare/v1.38.2...HEAD.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sdk): double-cast WorkflowConfig to Record for strict tsc build
TypeScript error on main (introduced in #2611) blocks `npm run build`
in sdk/, which now runs as part of this PR's tarball build path. Apply
the double-cast via `unknown` as the compiler suggests.
Same fix as #2622; can be dropped if that lands first.
* test: remove bug-2598 test obsoleted by SDK decoupling
The bug-2598 test guards the Windows CVE-2024-27980 fix in the old
build-from-source path (npm spawnSync with shell:true + formatSpawnFailure
diagnostics). This PR removes that entire code path — installSdkIfNeeded
no longer spawns npm, it just verifies the prebuilt sdk/dist/cli.js
shipped in the tarball.
The test asserts `installSdkIfNeeded.toString()` contains a
formatSpawnFailure helper. After decoupling, no such helper exists
(nothing to format — there's no spawn). Keeping the test would assert
invariants of the rejected architecture.
The original #2598 defect (silent failure of npm spawn on Windows) is
structurally impossible in the shim path: bin/gsd-sdk.js invokes
`node sdk/dist/cli.js` directly via child_process.spawn with an
explicit argv array. No .cmd wrapper, no shell delegation.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
* fix(sdk): bump engines.node from >=20 to >=22.0.0
Node 20 reaches EOL April 30 2026. The root package already declares
>=22.0.0 and CI only runs Node 22 and 24. Align sdk/package.json so
`npm install` on Node 20 fails with a clear engines mismatch rather
than a silent install that breaks at runtime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(release): publish @gsd-build/sdk alongside get-shit-done-cc in release pipeline
Closes#2309
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## 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>
## 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>
Repos that disable "Allow GitHub Actions to create and approve pull
requests" (org-level policy or repo-level setting) cause the "Create PR
to merge release back to main" step to fail with a GraphQL 403. That
failure cascades: Tag and push, npm publish, GitHub Release creation
are all skipped, and the entire release aborts.
The merge-back PR is a convenience — it's re-openable manually after
the release. Making it non-fatal with continue-on-error lets the rest
of the release complete. The step now emits ::warning:: annotations
pointing at the manual-recovery command when it fails.
Shell pipelines also fall through with `|| echo "::warning::..."` so
transient gh CLI failures don't mask the underlying policy issue.
Covers the failure mode seen on run 24596079637 where dry-run publish
validation passed but the release halted at the PR-creation step.
- Update actions/checkout from v4.2.2 to v6.0.2 in release.yml and
hotfix.yml (prevents breakage after June 2026 Node.js 20 deprecation)
- Update actions/setup-node from v4.1.0 to v6.3.0 in both workflows
- Add release/** and hotfix/** to test.yml push triggers
- Add release/** and hotfix/** to security-scan.yml PR triggers
test.yml already used v6 pins — this aligns the release pipelines.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: 3-tier release strategy with hotfix, release, and CI workflows
Supersedes PRs #1208 and #1210 with a consolidated approach:
- VERSIONING.md: Strategy document with 3 release tiers (patch/minor/major)
- hotfix.yml: Emergency patch releases to latest
- release.yml: Standard release cycle with RC/beta pre-releases to next
- auto-branch.yml: Create branches from issue labels
- branch-naming.yml: Convention validation (advisory)
- pr-gate.yml: PR size analysis and labeling
- stale.yml: Weekly cleanup of inactive issues/PRs
- dependabot.yml: Automated dependency updates
npm dist-tags: latest (stable) and next (pre-release) only,
following Angular/Next.js convention.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review findings for release workflow security and correctness
- Move all ${{ }} expression interpolation from run: blocks into env: mappings
in both hotfix.yml (~12 instances) and release.yml (~16 instances) to prevent
potential command injection via GitHub Actions expression evaluation
- Reorder rc job in release.yml to run npm ci and test:coverage before pushing
the git tag, preventing broken tagged commits when tests fail
- Update VERSIONING.md to accurately describe the implementation: major releases
use beta pre-releases only, minor releases use rc pre-releases only (no
beta-then-rc progression)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: harden release workflows — SHA pinning, provenance, dry-run guards
Addresses deep adversarial review + best practices research:
HIGH:
- Fix release.yml rc/finalize: dry_run now gates tag+push (not just npm publish)
- Fix hotfix.yml finalize: reorder tag-before-publish (was publish-before-tag)
MEDIUM — Security hardening:
- Pin ALL actions to SHA hashes (actions/checkout@11bd7190,
actions/setup-node@39370e39, actions/github-script@60a0d830)
- Add --provenance --access public to all npm publish commands
- Add id-token: write permission for npm provenance OIDC
- Add concurrency groups (cancel-in-progress: false) on both workflows
- Add branch-naming.yml permissions: {} (deny-all default)
- Scope permissions per-job instead of workflow-level where possible
MEDIUM — Reliability:
- Add post-publish verification (npm view + dist-tag check) after every publish
- Add npm publish --dry-run validation step before actual publish
- Add branch existence pre-flight check in create jobs
LOW:
- Fix VERSIONING.md Semver Rules: MINOR = "enhancements" not "new features"
(aligns with Release Tiers table)
Tests: 1166/1166 pass
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: pin actions/stale to SHA hash
Last remaining action using a mutable version tag. Now all actions
across all workflow files are pinned to immutable SHA hashes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address all Copilot review findings on release strategy workflows
- Configure git identity in all committing jobs (hotfix + release)
- Base hotfix on latest patch tag instead of vX.Y.0
- Add issues: write permission for PR size labeling
- Remove stale size labels before adding new one
- Make tagging and PR creation idempotent for reruns
- Run dry-run publish validation unconditionally
- Paginate listFiles for large PRs
- Fix VERSIONING.md table formatting and docs accuracy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clean up next dist-tag after finalize in release and hotfix workflows
After finalizing a release, the next dist-tag was left pointing at the
last RC pre-release. Anyone running npm install @next would get a stale
version older than @latest. Now both workflows point next to the stable
release after finalize, matching Angular/Next.js convention.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ci): address blocking issues in 3-tier release workflows
- Move back-merge PR creation before npm publish in hotfix/release finalize
- Move version bump commit after test step in rc workflow
- Gate hotfix create branch push behind dry_run check
- Add confirmed-bug and confirmed to stale.yml exempt labels
- Fix auto-branch priority: critical prefix collision with hotfix/ naming
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add GitHub Actions CI for cross-platform testing (ubuntu/windows/macos × node 18/20/22)
- Add release workflow that auto-creates GitHub Releases and publishes to npm on tag push
- Add CONTRIBUTING.md with branching strategy (maintainers direct commit, contributors PR)
- Add MAINTAINERS.md with release workflows and recovery procedures
- Add PR template for contributors
Closes#221
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>